Why look for an IP at all?
The other day I was faced with the task of sending database updates to certain terminals. But before sending, I had to find out where to send, or where to take it from. At first glance, it is more logical to tell the terminal the IP address of the server and collect data, but the following nuances prevented such an implementation:
- These terminals will be publicly available and operate in kiosk mode . Therefore, the idea to add some kind of administration panel to them immediately fell away, because a random user will be able to “naklats” in the settings of the IP address that he likes.
- It would be possible to stitch the update server IP address into the terminals, but since the server, in my case, is just a desktop application that the user can run on any computer on the subnet, this solution did not work either.
- Taking into account the previous two points, it would be possible to implement the administration panel, with password entry, but, nevertheless, constantly driving in a new IP address of the update server is an extra headache for the maintenance staff.
Therefore, from the idea of ​​“pick up,” I proceeded to the idea of ​​“send” and began to tinkering with the implementation of automatic search for IP addresses in Python 3.
')
The first idea that came to mind is the periodic distribution of the base and its hash sum via
udp broadcast , but, unfortunately, the UDP protocol
does not guarantee the integrity of the delivered information. However, the idea of ​​using broadcast addresses lay in the final implementation method.
So, in the end, I decided to send a UDP mailing to the broadcast address 255.255.255.255 from the server, and install UDP servers on the terminals that, after receiving the command on this mailing, will open a TCP connection to the central server.
First step: write a UDP client
The official Python website has several
examples of socket implementations, but I immediately ran into a problem. When sent to a broadcast address, the interpreter issued:
PermissionError: [Errno 13] Permission denied . On "stackoverflow" I found a
solution to the problem - for such a mailing the socket needs to set the special flag
SO_BROADCAST . Given this fact, the function of creating a UDP client has taken the following form:
def create_broadcast_socket(): udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) udp_sock.settimeout(2) return udp_sock
And the following functions send various messages through this socket.
def ask_addresses(): with create_broadcast_socket() as sock: sock.sendto('show yourself'.encode('utf-8'), ('255.255.255.255', PORT)) def update_many(): with create_broadcast_socket() as sock: sock.sendto('get updates'.encode('utf-8'), ('255.255.255.255', PORT)) def update_one(ip): with create_broadcast_socket() as sock: sock.sendto('get updates'.encode('utf-8'), (ip, PORT))
I think the names show what they are doing, and there is no need for specific explanations here.
Second step: write a UDP server and TCP client in it
Fortunately, the
socketserver module already exists in the standard language library. To create a full-fledged UDP server, it is enough to inherit from the
DatagramRequestHandler class and implement the logic in the
handle () method.
class EchoServer(socketserver.DatagramRequestHandler): def handle(self): data = self.request[0].strip().decode('utf-8') client_ip = self.client_address[0] if data.startswith('show yourself'): print('show myself') with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as tcp_sock: tcp_sock.connect((client_ip, PORT)) tcp_sock.send('show\n'.encode('UTF-8')) elif data.startswith('get updates'): print('get updates FROM ') with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as tcp_sock: tcp_sock.connect((client_ip, PORT)) tcp_sock.send('get\n'.encode('UTF-8')) part = tcp_sock.recv(1024) file = open('internal.db', 'wb') while part: file.write(part) print(part) part = tcp_sock.recv(1024) file.close() print(self.request)
This server listens to UDP connections on a specific port (the port number is stored in the global variable PORT). After receiving the packet, he checks its contents, if the packet
shows a “show yourself” message, it opens a TCP connection and sends the message
“show \ n” , after which the TCP server of the update server adds this IP address to its set of addresses. If the package received the message
“get updates” , the terminal will open a TCP connection, in which it will send the message
“get \ n” , after which the download of the SQLite database file will begin. The symbol '\ n' at the end of the messages I used for convenience, so that on the TCP server you could call the
readline () method on the socket
All this stuff starts like this:
def run_echo_server(): server = socketserver.UDPServer(('', PORT), EchoServer) server.serve_forever()
An empty string, instead of an address, tells the server to listen for connections on all available network interfaces.
The third step: we write TCP client
The last link in this chain of communications will be a TCP server on the update server. It is implemented on the basis of the
StreamRequestHandler class from the same
socketserver module. As in the first case, it also remained only to implement the
handle () method:
class NetworkController(socketserver.StreamRequestHandler): def handle(self): request_type = self.rfile.readline() print("{} wrote: {}".format(self.client_address[0], request_type)) if request_type.decode('UTF-8') == 'show\n': scales_catalogue.add(self.client_address[0])
As mentioned above, when receiving the
“show \ n” message, the server will add the IP address from which the message was sent to its internal array, or rather the set, in order to avoid duplicate addresses. If the server receives the
“get \ n” message, it will begin sending the database file in 1024 byte portions.
This server is started with the following functions:
def create_server(): return socketserver.TCPServer(('', PORT), NetworkController) def run_pong_server(): server = create_server() server.serve_forever()
As with the UDP server, a blank line forces the server to listen on connections on all available network interfaces.
Total
In this way, a system turned out that through the UDP mailing list can find out the addresses of the terminals, and then either selectively force the terminals from the list of IP addresses to pick up the updated database file, or again, via the UDP mailing, force all terminals on the network to pick up the updates.
If some nuances are not clear, the full source code is publicly available on my GitHub repository.