Автопоиск IP-адресов

в 6:32, , рубрики: python, сетевое программирование, сокеты

Preview

Зачем вообще искать IP?

На днях я столкнулся с задачей отправки обновлений базы данных на определенные терминалы. Но прежде чем отправлять, мне необходимо было выяснить куда отправлять, либо откуда забирать. На первый взгляд логичнее сообщить терминалам IP-адрес сервера и забирать данные, но следующие нюансы помешали такой реализации:

  • Данные терминалы будут общедоступными и работать в режиме киоска. Поэтому идея добавить на них какую-нибудь панель администрирования, сразу же отпадала, ибо случайный юзер сможет «наклацать» в настройках IP-адреса что ему заблагорассудится.
  • Можно было бы зашить в терминалы IP-адрес сервера обновлений, но так как сервер, в моем случае, — это всего лишь десктопное приложение, которое пользователь может запускать на любом компьютере в подсети, то такое решение тоже не подошло.
  • Взяв в учет предыдущие два пункта, можно было бы реализовать панель администрирования, со входом по паролю, но, все же, постоянно вбивать новый IP-адрес сервера обновлений — это лишняя головная боль обслуживающему персоналу.

Поэтому от идеи «забирать» я перешел к идее «отправлять» и начал мастерить реализацию автоматического поиска IP-адресов на Python 3.

Первая идея, которая пришла в голову — это периодическая рассылка базы и ее хеш-суммы через udp broadcast, но, к сожалению, протокол UDP не гарантирует целостность доставленной информации. Однако идея использования широковещательных адресов легла в конечный способ реализации.

Итак, в итоге я решил отправлять с сервера UDP рассылку на широковещательный адрес 255.255.255.255, а на терминалах установить UDP-серверы, которые после получения команды по этой рассылке, будут открывать TCP-соединение на центральный сервер.

приблизительная топология сети

Первый шаг: пишем UDP клиент

На официальном сайте Python есть несколько примеров реализации сокетов, однако я сразу же столкнулся с проблемой. При отправке на широковещательный адрес интерпретатор выдал: PermissionError: [Errno 13] Permission denied. На «stackoverflow» я нашел решение проблемы — для такой рассылки сокету нужно выставить специальный флаг SO_BROADCAST. С учетом данного факта функция создания UDP клиента приняла следующий вид:

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

А следующие функции отправляют различные сообщения через этот сокет.

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))

Думаю из названий видно что они делают, и особых пояснений здесь не нужно.

Второй шаг: пишем UDP сервер и TCP клиент в нем

К счастью, в стандартной библиотеке языка уже есть модуль socketserver. Чтобы создать полноценный UDP-сервер, достаточно наследоваться от класса DatagramRequestHandler и реализовать логику в методе handle().

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('shown'.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('getn'.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)

Данный сервер слушает UDP соединения на определенном порту (номер порта хранится в глобальной переменной PORT). После получения пакета он проверяет его содержимое, если в пакете сообщение «show yourself», то он открывает TCP соединение и шлет сообщение «shown», после чего TCP сервер сервера обновлений, добавляет данный IP адрес в свое множество(set) адресов. Если же в пакете пришло сообщение «get updates», то терминал откроет TCP соединение, в котором пошлет сообщение «getn», после чего начнется скачивание файла базы данных SQLite. Символ 'n' в конце сообщений я использовал для удобства, дабы на TCP-сервере можно было вызывать на сокете метод readline()

Запускается все это добро следующим образом:

def run_echo_server():
    server = socketserver.UDPServer(('', PORT), EchoServer)
    server.serve_forever()

Пустая строка, вместо адреса сообщает серверу слушать соединения на всех доступных сетевых интерфейсах.

Третий шаг: пишем TCP клиент

Последним звеном в этой цепочке коммуникаций будет TCP сервер на сервере обновлений. Он реализован на основе класса StreamRequestHandler из того же модуля socketserver. Как и в первом случае, тут также оставалось лишь реализовать метод handle():

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') == 'shown':

            scales_catalogue.add(self.client_address[0])
            # print(scales_catalogue)

        elif request_type.decode('UTF-8') == 'getn':
            file = open('internal.db', 'rb')
            part = file.read(1024)
            while part:
                self.wfile.write(part)
                part = file.read(1024)

            file.close()

Как уже было сказано выше, при получении сообщения «shown» сервер добавит IP-адрес, с которого пришло сообщение, в свой внутренний массив, а точней множество, дабы избежать дублирование адресов. Если же сервер получит сообщение «getn», то он начнет оправку файла базы данных, порциями по 1024 байта.

Запускается данный сервер следующими функциями:

def create_server():
    return socketserver.TCPServer(('', PORT), NetworkController)

def run_pong_server():
    server = create_server()
    server.serve_forever()

Как и в случае с UDP сервером, пустая строка заставляет сервер слушать соединения на всех доступных сетевых интерфейсах.

Итог

Таким образом получилась система, которая через UDP рассылку может узнать адреса терминалов, а потом либо выборочно заставить терминалы из списка IP-адресов забрать обновленный файл базы данных, либо опять же через UDP рассылку заставить все терминалы в сети забрать обновления.

Если какие-то нюансы остались не понятными, полный исходный код лежит в открытом доступе на моем репозитории в GitHub.

Автор: witadol

Источник

Поделиться