- PVSM.RU - https://www.pvsm.ru -
На днях я столкнулся с задачей отправки обновлений базы данных на определенные терминалы. Но прежде чем отправлять, мне необходимо было выяснить куда отправлять, либо откуда забирать. На первый взгляд логичнее сообщить терминалам IP-адрес сервера и забирать данные, но следующие нюансы помешали такой реализации:
Поэтому от идеи «забирать» я перешел к идее «отправлять» и начал мастерить реализацию автоматического поиска IP-адресов на Python 3.
Первая идея, которая пришла в голову — это периодическая рассылка базы и ее хеш-суммы через udp broadcast [2], но, к сожалению, протокол UDP не гарантирует [3] целостность доставленной информации. Однако идея использования широковещательных адресов легла в конечный способ реализации.
Итак, в итоге я решил отправлять с сервера UDP рассылку на широковещательный адрес 255.255.255.255, а на терминалах установить UDP-серверы, которые после получения команды по этой рассылке, будут открывать TCP-соединение на центральный сервер.
На официальном сайте Python есть несколько примеров [4] реализации сокетов, однако я сразу же столкнулся с проблемой. При отправке на широковещательный адрес интерпретатор выдал: PermissionError: [Errno 13] Permission denied. На «stackoverflow» я нашел решение проблемы [5] — для такой рассылки сокету нужно выставить специальный флаг 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))
Думаю из названий видно что они делают, и особых пояснений здесь не нужно.
К счастью, в стандартной библиотеке языка уже есть модуль socketserver [6]. Чтобы создать полноценный UDP-сервер, достаточно наследоваться от класса DatagramRequestHandler [7] и реализовать логику в методе 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 сервер на сервере обновлений. Он реализован на основе класса StreamRequestHandler [8] из того же модуля socketserver [6]. Как и в первом случае, тут также оставалось лишь реализовать метод 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 рассылку заставить все терминалы в сети забрать обновления.
Если какие-то нюансы остались не понятными, полный исходный код лежит в открытом доступе на моем репозитории [9] в GitHub.
Автор: witadol
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/234366
Ссылки в тексте:
[1] режиме киоска: https://support.google.com/chrome/a/answer/6137028?hl..
[2] udp broadcast: https://en.wikipedia.org/wiki/Broadcast_address
[3] не гарантирует: https://www.ietf.org/rfc/rfc768.txt
[4] примеров: https://docs.python.org/3/library/socket.html#example
[5] решение проблемы: http://stackoverflow.com/questions/11457676/python-socket-error-errno-13-permission-denied
[6] socketserver: https://docs.python.org/3/library/socketserver.html
[7] DatagramRequestHandler: https://docs.python.org/3.4/library/socketserver.html#socketserver-udpserver-example
[8] StreamRequestHandler: https://docs.python.org/3/library/socketserver.html#socketserver-tcpserver-example
[9] репозитории: https://github.com/witadol/networksearch
[10] Источник: https://habrahabr.ru/post/319706/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.