[Из песочницы] Автопоиск IP-адресов
Зачем вообще искать 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('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)
Данный сервер слушает UDP соединения на определенном порту (номер порта хранится в глобальной переменной PORT). После получения пакета он проверяет его содержимое, если в пакете сообщение «show yourself», то он открывает TCP соединение и шлет сообщение «show\n», после чего TCP сервер сервера обновлений, добавляет данный IP адрес в свое множество (set) адресов. Если же в пакете пришло сообщение «get updates», то терминал откроет TCP соединение, в котором пошлет сообщение «get\n», после чего начнется скачивание файла базы данных 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') == '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()
Как уже было сказано выше, при получении сообщения «show\n» сервер добавит IP-адрес, с которого пришло сообщение, в свой внутренний массив, а точней множество, дабы избежать дублирование адресов. Если же сервер получит сообщение «get\n», то он начнет оправку файла базы данных, порциями по 1024 байта.
Запускается данный сервер следующими функциями:
def create_server():
return socketserver.TCPServer(('', PORT), NetworkController)
def run_pong_server():
server = create_server()
server.serve_forever()
Как и в случае с UDP сервером, пустая строка заставляет сервер слушать соединения на всех доступных сетевых интерфейсах.
Итог
Таким образом получилась система, которая через UDP рассылку может узнать адреса терминалов, а потом либо выборочно заставить терминалы из списка IP-адресов забрать обновленный файл базы данных, либо опять же через UDP рассылку заставить все терминалы в сети забрать обновления.
Если какие-то нюансы остались не понятными, полный исходный код лежит в открытом доступе на моем репозитории в GitHub.
Комментарии (2)
17 января 2017 в 11:37
0↑
↓
сервер, в моем случае, — это всего лишь десктопное приложение, которое пользователь может запускать на любом компьютере в подсети
А как часто реально будет изменяться этот «любой» компьютер?17 января 2017 в 12:26
0↑
↓
Ну это уже зависит не от меня, а от обслуживающего персонала, у меня же стояла задача максимально упростить им жизнь