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

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('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.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.