
На фоне обсуждения будущего интернет мессенджеров и прочтения статьи «Почему ваш любимый мессенджер должен умереть», решил поделиться своим опытом создания P2P приложения для общения независимо от сторонних серверов. Точнее — это просто заготовка, передающая одно сообщение от клиента серверу, дальнейшее расширение функционала зависит только от Вашей фантазии.
В этой публикации мы напишем 3 простых приложения для связи P2P из любой точки Земного шара — клиент, сервер и сигнальный сервер.
Нам понадобится:
— один сервер с белым статическим IP адресом;
— 2 компьютера за NAT с типом соединения Full Cone NAT (либо 1 компьютер с 2-мя виртуальными машинами);
— STUN-сервер.
Full Cone NAT — это такой тип преобразования сетевых адресов, при котором существует однозначная трансляция между парами «внутренний адрес: внутренний порт» и «публичный адрес: публичный порт».
Вот, что мы можем прочесть о STUN-сервере на Wiki:
«Существуют протоколы, использующие пакеты UDP для передачи голоса, изображения или текста по IP-сетям. К сожалению, если обе общающиеся стороны находятся за NAT’ом, соединение не может быть установлено обычным способом. Именно здесь STUN и оказывается полезным. Он позволяет клиенту, находящемуся за сервером трансляции адресов (или за несколькими такими серверами), определить свой внешний IP-адрес, способ трансляции адреса и порта во внешней сети, связанный с определённым внутренним номером порта.»
При решении задачи использовались следующие питоновские модули: socket, twisted, stun, sqlite3, os, sys.
Для обмена данными, как между Сервером и Клиентом, так и между Сервером, Клиентом и Сигнальным Сервером — используется UDP протокол.
В общих чертах механизм функционирования выглядит так:
Сервер <-> STUN сервер
Клиент <-> STUN сервер
Сервер <-> Сигнальный Сервер
Клиент <-> Сигнальный Сервер
Клиент -> Сервер
1. Клиент, находясь за NAT с типом соединения Full Cone NAT, отправляет сообщение на STUN сервер, получает ответ в виде своего внешнего IP и открытого PORT;
2. Сервер, находясь за NAT с типом соединения Full Cone NAT, отправляет сообщение на STUN сервер, получает ответ в виде своего внешнего IP и открытого PORT;
При этом, Клиенту и Серверу известен внешний (белый) IP и PORT Сигнального Сервера;
3. Сервер отправляет на Сигнальный Сервер данные о своих внешних IP и PORT, Сигнальный Сервер их сохраняет;
4. Клиент отправляет на Сигнальный Сервер данные о своих внешних IP и PORT и id_destination искомого Сервера, для которого ожидает его внешний IP, PORT.
Сигнальный Сервер их сохраняет, осуществляет поиск по базе, используя id_destination и, в ответ, отдает найденную информацию в виде строки: 'id_host, name_host, ip_host, port_host';
5. Клиент принимает найденную информацию, разбивает по разделителю и, используя (ip_host, port_host), отправляет сообщение Серверу.
Приложения написаны на Python версии 2.7, протестированы под Debian 7.7.
Создадим файл server.py с содержимым:
server.py
# -*- coding: utf-8 -*- #SERVER from socket import * import sys import stun def sigserver_exch(): # СЕРВЕР <-> СИГНАЛЬНЫЙ СЕРВЕР # СЕРВЕР <- КЛИЕНТ # СЕРВЕР - отправляет запрос на СИГНАЛЬНЫЙ СЕРВЕР с белым статическим IP со своими данными о текущих значениях IP и PORT. Принимает запрос от КЛИЕНТА. #Внешний IP и PORT СИГНАЛЬНОГО СЕРВЕРА: v_sig_host = 'XX.XX.XX.XX' v_sig_port = XXXX #id этого КЛИЕНТА, имя этого КЛИЕНТА, id искомого СЕРВЕРА v_id_client = 'id_server_1002' v_name_client = 'name_server_2' v_id_server = 'none' #IP и PORT этого КЛИЕНТА v_ip_localhost = 'XX.XX.XX.XX' v_port_localhost = XXXX udp_socket = '' try: #Получаем текущий внешний IP и PORT при помощи утилиты STUN nat_type, external_ip, external_port = stun.get_ip_info() #Присваиваем переменным белый IP и PORT сигнального сервера для отправки запроса host_sigserver = v_sig_host port_sigserver = v_sig_port addr_sigserv = (host_sigserver,port_sigserver) #Заполняем словарь данными для отправки на СИГНАЛЬНЫЙ СЕРВЕР: #текущий id + имя + текущий внешний IP и PORT, #и id_dest - укажем 'none' #В качестве id можно использовать хеш случайного числа + соль data_out = v_id_client + ',' + v_name_client + ',' + external_ip + ',' + str(external_port) + ',' + v_id_server #Создадим сокет с атрибутами: #использовать пространство интернет адресов (AF_INET), #передавать данные в виде отдельных сообщений udp_socket = socket(AF_INET, SOCK_DGRAM) #Присвоим переменным свой локальный IP и свободный PORT для получения информации host = v_ip_localhost port = v_port_localhost addr = (host,port) #Свяжем сокет с локальными IP и PORT udp_socket.bind(addr) print('socket binding') #Отправим сообщение на СИГНАЛЬНЫЙ СЕРВЕР udp_socket.sendto(data_out,addr_sigserv) while True: #Если первый элемент списка - 'sigserv' (сообщение от СИГНАЛЬНОГО СЕРВЕРА), #печатаем сообщение с полученными данными #Иначе - печатаем сообщение 'Message from CLIENT!' data_in = udp_socket.recvfrom(1024) if data_in[0] == 'sigserv': print('signal server data: ', data_in) else: print('Message from CLIENT!') #Закрываем сокет udp_socket.close() except: print('exit!') sys.exit(1) finally: if udp_socket <> '' udp_socket.close() sigserver_exch()
Заполним соответствующие поля разделов: «Внешний IP и PORT СИГНАЛЬНОГО СЕРВЕРА» и «IP и PORT этого КЛИЕНТА».
Создадим файл client.py с содержимым:
client.py
# -*- coding: utf-8 -*- # CLIENT from socket import * import sys import stun def sigserver_exch(): # КЛИЕНТ <-> СИГНАЛЬНЫЙ СЕРВЕР # КЛИЕНТ -> СЕРВЕР # КЛИЕНТ - отправляет запрос на СИГНАЛЬНЫЙ СЕРВЕР с белым IP # для получения текущих значений IP и PORT СЕРВЕРА за NAT для подключения к нему. #Внешний IP и PORT СИГНАЛЬНОГО СЕРВЕРА: v_sig_host = 'XX.XX.XX.XX' v_sig_port = XXXX #id этого КЛИЕНТА, имя этого КЛИЕНТА, id искомого СЕРВЕРА v_id_client = 'id_client_1001' v_name_client = 'name_client_1' v_id_server = 'id_server_1002' #IP и PORT этого КЛИЕНТА v_ip_localhost = 'XX.XX.XX.XX' v_port_localhost = XXXX udp_socket = '' try: #Получаем текущий внешний IP и PORT при помощи утилиты STUN nat_type, external_ip, external_port = stun.get_ip_info() #Присваиваем переменным белый IP и PORT сигнального сервера для отправки запроса host_sigserver = v_sig_host port_sigserver = v_sig_port addr_sigserv = (host_sigserver,port_sigserver) #Заполняем словарь данными для отправки на СИГНАЛЬНЫЙ СЕРВЕР: #текущий id + имя + текущий внешний IP и PORT, #и id_dest - id известного сервера с которым хотим связаться. #В качестве id можно использовать хеш случайного числа + соль data_out = v_id_client + ',' + v_name_client + ',' + external_ip + ',' + str(external_port) + ',' + v_id_server #Создадим сокет с атрибутами: #использовать пространство интернет адресов (AF_INET), #передавать данные в виде отдельных сообщений udp_socket = socket(AF_INET, SOCK_DGRAM) #Присвоим переменным свой локальный IP и свободный PORT для получения информации host = v_ip_localhost port = v_port_localhost addr = (host,port) #Свяжем сокет с локальными IP и PORT udp_socket.bind(addr) #Отправим сообщение на СИГНАЛЬНЫЙ СЕРВЕР udp_socket.sendto(data_out, addr_sigserv) while True: #Если первый элемент списка - 'sigserv' (сообщение от СИГНАЛЬНОГО СЕРВЕРА), #печатаем сообщение с полученными данными и отправляем сообщение #'Hello, SERVER!' на сервер по указанному в сообщении адресу. data_in = udp_socket.recvfrom(1024) data_0 = data_in[0] data_p = data_0.split(",") if data_p[0] == 'sigserv': print('signal server data: ', data_p) udp_socket.sendto('Hello, SERVER!',(data_p[3],int(data_p[4]))) else: print("No, it is not Rio de Janeiro!") udp_socket.close() except: print ('Exit!') sys.exit(1) finally: if udp_socket <> '' udp_socket.close() sigserver_exch()
Заполним соответствующие поля разделов: «Внешний IP и PORT СИГНАЛЬНОГО СЕРВЕРА» и «IP и PORT этого КЛИЕНТА».
Создадим файл signal_server.py с содержимым:
signal_server.py
# -*- coding: utf-8 -*- # SIGNAL SERVER #Twisted - управляемая событиями(event) структура #Событиями управляют функции – event handler #Цикл обработки событий отслеживает события и запускает соответствующие event handler #Работа цикла лежит на объекте reactor из модуля twisted.internet from twisted.internet import reactor from twisted.internet.protocol import DatagramProtocol import sys, os import sqlite3 class Query_processing_server(DatagramProtocol): # СИГНАЛЬНЫЙ СЕРВЕР <-> КЛИЕНТ # КЛИЕНТ -> СЕРВЕР # либо # СИГНАЛЬНЫЙ СЕРВЕР <-> СЕРВЕР # СИГНАЛЬНЫЙ СЕРВЕР - принимает запросы от КЛИЕНТА и СЕРВЕРА # сохраняет их текущие значения IP и PORT # (если отсутствуют - создает новые + имя и идентификатор) # и выдает IP и PORT СЕРВЕРА запрошенного КЛИЕНТОМ. def datagramReceived(self, data, addr_out): conn = '' try: #Разбиваем полученные данные по разделителю (,) [id_host,name_host,external_ip,external_port,id_dest] #id_dest - искомый id сервера data = data.split(",") #Запрос на указание пути к файлу БД sqlite3, при отсутствии будет создана новая БД по указанному пути: path_to_db = raw_input('Enter name db. For example: "/home/user/new_db.db": ') path_to_db = os.path.join(path_to_db) #Создать соединение с БД conn = sqlite3.connect(path_to_db) #Преобразовывать байтстроку в юникод conn.text_factory = str #Создаем объект курсора c = conn.cursor() #Создаем таблицу соответствия для хостов c.execute('''CREATE TABLE IF NOT EXISTS compliance_table ("id_host" text UNIQUE, "name_host" text, "ip_host" text, \ "port_host" text)''') #Добавляем новый хост, если еще не создан #Обновляем данные ip, port для существующего хоста c.execute('INSERT OR IGNORE INTO compliance_table VALUES (?, ?, ?, ?);', data[0:len(data)-1]) #Сохраняем изменения conn.commit() c.execute('SELECT * FROM compliance_table') #Поиск данных о сервере по его id c.execute('''SELECT id_host, name_host, ip_host, port_host from compliance_table WHERE id_host=?''', (data[len(data)-1],)) cf = c.fetchone() if cf == None: print ('Server_id not found!') else: #transport.write - отправка сообщения с данными: id_host, name_host, ip_host, port_host и меткой sigserver lst = 'sigserv' + ',' + cf[0] + ',' + cf[1] + ',' + cf[2] + ',' + cf[3] self.transport.write(str(lst), addr_out) #Закрываем соединение conn.close() except: print ('Exit!') sys.exit(1) finally: if conn <> '' conn.close() reactor.listenUDP(9102, Query_processing_server()) print('reactor run!') reactor.run()
Готово!
Порядок запуска приложения следующий:
— signal_server.py
— server.py
— client.py
