⬆️ ⬇️

Auto-find IPs

Preview



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:





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.



approximate network topology


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]) # print(scales_catalogue) elif request_type.decode('UTF-8') == 'get\n': file = open('internal.db', 'rb') part = file.read(1024) while part: self.wfile.write(part) part = file.read(1024) file.close() 


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.

Source: https://habr.com/ru/post/319706/



All Articles