Пишем свой мессенджер P2P
На фоне обсуждения будущего интернет мессенджеров и прочтения статьи «Почему ваш любимый мессенджер должен умереть», решил поделиться своим опытом создания 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 с содержимым:
# -*- 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 с содержимым:
# -*- 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 с содержимым:
# -*- 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