На фоне обсуждения будущего интернет мессенджеров и прочтения статьи «Почему ваш любимый мессенджер должен умереть», решил поделиться своим опытом создания 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
Комментарии (46)
tmnhy
27.06.2016 18:42+2Что-то мне подсказывает, что это не p2p в привычном понимании, а просто еще одна реализация режима direct, который давным давно уже был в мессенджерах. Могу ошибаться, но по-моему одна из реализаций icq-клиента подразумевала именно такой режим работы.
D_T
27.06.2016 19:17Есть хитрый ход (вместо STUN сервера), тройной пинг, узел А шлет пакет на B, а B на A, как только узел А получил пакет от B, то можно сразу слать инфу, т.к. B готов к приему, маршруты на роутерах сформировались. Для ускорения установка связи в три шага, условно 3 пакета A-B-A с обоих сторон, тогда первый пробивший NAT максимально быстро установит двухстороннюю связь, т.е. НАТы будут готовы слать в обе стороны.
А СТУНы тут лишние, виндовый брандмауэр всего минуту ждет прежде чем входящие пакеты запретить. Т.е. если открываешь случайный порт, то у тебя минута чтобы получить на него ответ.
Это все работает при идеальных условиях, пока пакеты не теряются. Как начинают теряться тоже работает, но сложнее.0x9d8e
27.06.2016 19:40О, спасибо! Давным-давно нечто подобное было очень нужно для одной поделки, но по незнанию думал, что ничего подобного провернуть нельзя и даже гуглить в эту сторону не стал, а зря.
D_T
27.06.2016 19:56Советую что-нибудь почитать про маршрутизацию IP сетей. Опять же решение не идеальное, будет работать в 80-90% случаев. Если надо 100%, то без TCP не обойтись. Не любят UDP инет-провайдеры, отчасти из-за торрентов, отчасти из-за неуправляемости.
ValdikSS
28.06.2016 00:17Можно и не друг-друга пинговать, а любой адрес, при этом A может не знать адреса B заранее. Скажем, A пингует 1.2.3.4 и не знает адреса B, а B, в свою очередь, знает адрес A, отправляет ему ICMP-пакет от своего адреса, что 1.2.3.4 недоступен, и все, NAT пробит.
Есть работающая реализация — pwnat.
at0mic
28.06.2016 01:05Вы только что описали TURN реализацию
Она повсеместно используется если NAT закрыт, беда в том, что В приходится гонять кучу траффика через себя. В паблике TURN серверов почти нет.
D_T
27.06.2016 19:50+3Есть еще одна проблема: два клиента за одним роутером могут друг-друга не увидеть. Некоторые роутеры пускают пакеты изнутри-внутрь, а некоторые рубят. Поэтому тут локальные IP тоже надо использовать.
webtrium
27.06.2016 19:53Вы ценный комментатор ), была бы карма, плюсанул бы!
D_T
27.06.2016 20:04+3Год бился об эту тему ))) не взлетело. Взял паузу. Точнее взлетело, но не на 100% как хотелось бы. GSM-свистки все испортили. Поэтому делюсь забегами по граблям, может кому поможет. Сейчас с мыслями собираюсь, время появится и пойду на второй забег, версия 2.0 )))
А так UDP — тема. В 100 Мбит сети одного провайдера выжимал 11 Мбайт/сек, в гигабитной локалке 86 Мбайт/сек.Ivan_83
27.06.2016 21:10Плохо жал.
Я в локалке 100к+ ппс делал. Если бы это были пакеты по 1400 байт то получилось бы 140 гигабайт/сек.
Имеет смысл замерять пакеты а не их размер.
Максимальный размер UDP не 65535 а 65507 байт.D_T
27.06.2016 21:21Пакет был 1500 пакет езернета минус 20 заголовок IP, минус 8 заголовок UDP. Минус мой заголовок 16 байт. Замерял полезные данные (без заголовков). 140 гигабайт/сек это сетка 1200-1300 Гбит, завидую :)
Ivan_83
28.06.2016 10:09Сетка гигабит, на обычном ширпотребе, просто если бы вместо 64 байта были пакеты по 1500 то да, было бы
143 гигабита/сек, ошибся немного.
А смысл замерять объёмы?
Нужно измерять сколько пакетов связка прожуёт, а далее просто умножать на размер полезной нагрузки.
2 mwizard:
Учимся считать.
100кпакетов * 64 байта в пакете = 6,2 мегабайта — такое даже в 100 мегабит уложится.
Суть теста была в том, сколько пакетов в ответ сможет сгенерировать тазик уровня коредуо Е5300 в один поток, и получилось где то 140-160кппс, что в общем для данной задачи было весьма круто.D_T
28.06.2016 10:32143 гигабита в гигабитной сетке не может быть. Я так понимаю что гигабит это максимальная пропускная способность канала. Т.е. идеальная передача, когда канал нисколько не простаивает. Если данных для передачи будет больше, то они просто будут скапливаться на входе в очереди.
Если пакеты маленькие и их много, то тут возрастает нагрузка на свичи, т.к. они оперируют пакетами, т.е. им есть разница обработать 64 пакета по 1500 байт или 1500 пакетов по 64 байта, во втором случае работы больше и обычное домашнее железо может просто не успеть их обработать, отсюда будут простои канала в ожидании данных, т.е. скорость упадет ниже гигабита.Ivan_83
28.06.2016 16:46Я делал нагрузочное тестирование.
Нагружал мелкими пакетами по 64 байта (может и меньше, но минимальный размер эзернет фрейма 64 байта).
У меня получилось 100к пакетов в секунду, на 6,2 мегабайта/сек.
Если бы это были пакеты по 1500 размером то их было бы сильно меньше ибо гиговый линк, но если предположить что линк резиновый то я бы утилизировал 143 гигабита/сек.
Это всё к тому, что нагрузочные тестирования имеют смысл в пакетах а не мегабитах.
D_T
27.06.2016 20:35Раз у начал делиться, пишу все что было. Еще одни грабли от нехороших провайдеров: задержка пакетов до десятков секунд. Это тоже нездорово, т.к. считаешь пакет потерянным, а он приходит через полминуты-минуту, завис где-то в кэше. Одни провайдеры его просто убивают, а другие доставляют любой ценой.
webtrium
27.06.2016 20:40Не хотите статью написать с подробностями о встреченных подводных камнях, думаю Хабрасообществу был-бы интересен такой опыт? На каком языке реализовывали задумки и каков конечный результат?
D_T
27.06.2016 20:50Как будет результат — тогда и напишу. А пока просто опыт неудачника. Не думаю что он кому-то интересен.
15432
27.06.2016 23:51Очень даже интересен! Тоже хочу аналог TCP сделать, опыта набираюсь потихоньку. Интересуют любые подробности про подводные камни. Про 80% потерь на GSM я и не подозревал…
D_T
27.06.2016 20:56Язык С. Результат не очень. Повторить TCP не удалось. Задача была: direct-доставка сообщений любого размера. Решил проблемы на 90%, надо 100%.
port443
28.06.2016 11:29Мы экспериментальным путём обнаружили немного другое. После определённого периода (секунд 10-20) неиспользования канала данных, канал разрывается оператором где-то на нижнем (канальном?) уровне; при этом высшие уровни не информируются. Т.е. если было поднято TCP соединение, оно так и останется поднятым и неразорванным. При дальнейшей попытке использования канал восстанавливается (опять же, без сигнализации), на что уходит 5-10 секунд. То есть на уровне TCP, если передавать часто — отклик очень быстрый, но стоит немного задержаться — и получаем огромный лаг. Опять же, нет никакой гарантии, что восстановить канал удастся. В общем выходит так, что ориентироваться на сигнализацию в TCP нельзя, и быть уверенным в передаче, если соединение установилось — тоже нельзя; только ACK на уровне приложения!
Я почти уверен, что большинство мобильных сетей ведут себя подобным образом; различаются детали (период ожидания до разъединения и т.п.). Просто они же так экономят критический ресурс — ёмкость соты.
Про UDP, к сожалению, не знаю, но что-то мне подсказывает, что поведение может быть похожим: пока канал поднят, UDP будет пролетать; чуть задержались — UDP или будет теряться, или, как вы пишете, буферизоваться где-то в том месте, где управляется состояние канала.D_T
28.06.2016 14:26В GSM просто терялись UDP пакеты. Главная проблема кстати и были эти провалы. То связь есть, долетает относительно быстро и вдруг раз и встало все от секунд до десятков секунд, и никак под это не подстроишься, не предскажешь, не продиагностируешь быстро, нет какой-то стабильной последовательности. Есть подозрение что там просто под TCP приоритеты выставлены, т.к. потери IP пакетов от TCP соединения гарантированно вызовут повторные отправки что еще больше забьет канал. Во-вторых по UDP идут в основном торренты да скайпы всякие, что сотовым не особо и надо. TeamViewer тоже очень плохо работает, т.к. тоже UDP.
Запаздывание пакетов я наблюдал не в GSM, а на входе в ЦОД, где хостилась моя виртуалка с сервером. Тут кстати UDP тоже быстрее работает, т.к. TCP зажимается провайдером и при заявленном интерфейсе 100 Мбит реально одно TCP соединение дает 10-30, UDP немного побольше, но далеко не 100.
D_T
27.06.2016 20:49И еще, в одном провайдере не проходил пакет с конкретными данными. Просто не проходил сколько не посылай. Как подсказали, оказалось первые 8 байт совпали с торрентами, провайдер просто рубил установку соединения между пирами. Т.е. тупо рубилось по сигнатуре. Не любят провайдеры UDP, там где не любят надо туннелировать в TCP.
port443
28.06.2016 14:19Я и с TCP наступал на похожее, только не у провайдера, а на MS TMG (мётрвая штука, но всё же вполне допустимый прецендент). Один абстрактный пакет данных не проходит вообще. Оказалось, совпал с сигнатурой какого-то вируса в его базе данных.
trapwalker
29.06.2016 11:00А вы не думали использовать DHT вместо сигнальных серверов? Там можно распределенно держать как таблицу пользовательских профилей, так и сигналы (приглашения) на соединение.
В двух словах DHT можно представить как распределенное key-value хранилище, Только ключи упорядоченны, хотя и рандомны, а пирам присвоены ключи из того же пространства. Зная несколько произвольных пиров (поддерживая с ними коннекты), всегда можно понять какие из них «ближе» к искомому ключу. Делается запрос именно на эти пиры и, если они сами не знают значения, они делятся контактами с теми своими «знакомыми» пирами, которые в свою очередь ещё «ближе» к вашей цели. Таким образом по принципу «теории 6 рукопожатий» мы очень быстро выходим на пир с самым «ближайшим» адресом к искомому ключу. Если этот пир не знает значения, то никто не знает или просто все кто знают оффлайн и нужно ждать. Пиры хранят не только свои значения, но и ближайшие. Обычно у пиров есть лимит на хранение данных, скажем 100мб, и они складывают туда все ближайшие пары ключ-значение, что влезут в ограничение.
Это основа. На ней реализованы реально работающие сети и можно ими пользоваться для хранения данных о контактах.webtrium
29.06.2016 15:05DHT не использовалась, т.к. в формулировке задачи был указан именно сигнальный сервер.
Идея распределённых хеш-таблиц в децентрализованных распределённых системах — хорошая, с точки зрения доступности информации. Но нужно учитывать среднее время получения ключа по значению при поиске подходящего пира, с учетом времени, в течении которого нужный порт абонента, с которым хочу связаться, открыт. Исходя из комментариев коллег, полученные опытным путем данные говорят, что это время составляет всего десятки секунд.
На сигнальном сервере информация всегда актуальна, т.к. обновляется в заданные промежутки времени запросами от клиентов (при необходимости можно поднять несколько сигнальных серверов, между которыми реплицировать обновляемые данные, а если один из основных сигнальных серверов гаснет, клиент связывается с другим доступным).
Как быстро данные будут обновляться от пира к пиру, как быстро нужное значение может быть найдено?trapwalker
29.06.2016 16:36Не буду строить из себя эксперта, ни разу ещё не экспериментировал с этой штукой. Однако мне кажется информация по DHT может ходить не так уж и медленно. Если коннекты с соседями у каждого онлайн-пира установлены и поддерживаются постоянно, то длительность передачи пакета по такой сети будет примерно равно удвоенному логарифму от объёма сети, умноженному на время передачи пакета по уже поднятому коннекту. Ну то есть не десятки секунд. Да, наврено при малом объёме сети и низкой связности вся эта схема будет работать нестабильно, но в штатном режиме при некоторой степени избыточности в построении маршрутов (посылать запросы не ближайшему к цели соседу, а нескольким ближайшим) всё должно быть нормально… по крайней мере я не вижу явных проблем.
Ктсати, следует ранжирвать «соседних» пиров не только по их «ключевому расстоянию», но и по их субъективному рейтингу, расчитываемому на основе пинга и прочих показателей стабильности канала до них.webtrium
29.06.2016 17:34Тоже далеко не эксперт, а Ваша идея интересная и требует своего кропотливого исследователя.
RevenantX
29.06.2016 14:35Делал подобные вещи и для C# даже написал велосипедную библиотеку для этого всего.
Если знать нюансы которые описаны в wikipedia большинство и вдобавок посмотреть практическую часть,
то ничего сложного нет. И в понимании NAT, STUN, MTU и прочих вещей.
Вот если что библиотека. Может кому интересно. Кода там мало, он как по мне читабельный и подойдет если не хочется писать велоспиеды.
github.com/RevenantX/LiteNetLib
mwizard
И в чем же децентрализация, если достаточно погасить сигнальный сервер, чтобы все умерло?
D_T
Тут можно 100 сигнальных серверов поднять и гасить устанешь. Он же не несет никакой нагрузки, просто сводит отправителя и получателя.
gurinderu
Достаточно создать правило deny all и сделать whitelist как в Китае, чтобы все перестало работать.
D_T
Достаточно выйти в инет через GSM-свисток и все перестанет работать. UDP хорошо ходит в открытом инете, а вот «последняя миля» все портит. А так да, админ может легко все запретить.
0x9d8e
Возможно в качестве таких серверов могут выступать клиенты, имеющие статический адрес. Но тут возникает другая проблема: как без сервера узнавать адреса таких клиентов? Вариантов вижу не много:
0) Зашитые в клиенте адреса;
1) Широкополосные запросы в локальных сетях;
2) Ссылки, получаемые по другим каналам (от знакомых например);
3) Перебор (полушутка);
Может быть ещё можно было бы выделить какое-то использование сторонних сервисов (независимо от их «согласия»).
Labunsky
Еще есть довольно разумный вариант с броадкастом по списку, хранящимся в публичном репозитории. Осталось только репозиторий как-нибудь хранить децентрелизовано, но это уже задача привычнее
trapwalker
Я бы добавил пункт номер -1: DHT сети. Чуть подробнее я о них писал в комментарии ниже.
А в дополнение к этому можно сделать открытый сервер анонсов, который может установить на свой сайт кто угодно. Этот сервер поддерживает стандартный URL вида mysite.com/announce.p2p или что-то вроде этого. Это просто json (возможно даже статический), где перечислены домены других таких же серверов, IP пиров, которые имеют белый адрес и часто онлайн (т.е. те, что с высоким рейтингом в сети). При самом первом входе в сеть нам нужен хотя бы один живой такой сервер анонсов. Дальше ссылки на сервера анонсов уже будут накапливаться клиентом, а в DHT будет таблица рейтинга серверов и нодов, чтобы сеть голосованием могла маркировать недобросовестные пиры и сервера анонсов.
Ну и, чтобы два раза не вставать, можно подумать как приспособить тут технологию blockchain.
GamePad64
На самом деле, для обнаружения узлов можно использовать открытые BitTorrent-трекеры и сеть Mainline DHT (которая основана на Kademlia). Тогда свой сервер анонсов писать не придётся. И клиент будет подключён к той же DHT-сети, что и торрент-клиенты (которых много), таким образом имея доступ к огромной хеш-таблице и, в свою очередь, помогая работе DHT.
Я для своего полностью распределённого приложения инкрементальной синхронизации файлов для service discovery использовал именно такой подход, работает замечательно.