Пишем свой мессенджер 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 с содержимым:
    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
    Share post

    Similar posts

    Comments 46

      +3
      И в чем же децентрализация, если достаточно погасить сигнальный сервер, чтобы все умерло?
        0
        Тут можно 100 сигнальных серверов поднять и гасить устанешь. Он же не несет никакой нагрузки, просто сводит отправителя и получателя.
          0
          Достаточно создать правило deny all и сделать whitelist как в Китае, чтобы все перестало работать.
            0
            Достаточно выйти в инет через GSM-свисток и все перестанет работать. UDP хорошо ходит в открытом инете, а вот «последняя миля» все портит. А так да, админ может легко все запретить.
          0
          Возможно в качестве таких серверов могут выступать клиенты, имеющие статический адрес. Но тут возникает другая проблема: как без сервера узнавать адреса таких клиентов? Вариантов вижу не много:
          0) Зашитые в клиенте адреса;
          1) Широкополосные запросы в локальных сетях;
          2) Ссылки, получаемые по другим каналам (от знакомых например);
          3) Перебор (полушутка);
          Может быть ещё можно было бы выделить какое-то использование сторонних сервисов (независимо от их «согласия»).
            0
            Еще есть довольно разумный вариант с броадкастом по списку, хранящимся в публичном репозитории. Осталось только репозиторий как-нибудь хранить децентрелизовано, но это уже задача привычнее
              0
              Я бы добавил пункт номер -1: DHT сети. Чуть подробнее я о них писал в комментарии ниже.
              А в дополнение к этому можно сделать открытый сервер анонсов, который может установить на свой сайт кто угодно. Этот сервер поддерживает стандартный URL вида mysite.com/announce.p2p или что-то вроде этого. Это просто json (возможно даже статический), где перечислены домены других таких же серверов, IP пиров, которые имеют белый адрес и часто онлайн (т.е. те, что с высоким рейтингом в сети). При самом первом входе в сеть нам нужен хотя бы один живой такой сервер анонсов. Дальше ссылки на сервера анонсов уже будут накапливаться клиентом, а в DHT будет таблица рейтинга серверов и нодов, чтобы сеть голосованием могла маркировать недобросовестные пиры и сервера анонсов.
              Ну и, чтобы два раза не вставать, можно подумать как приспособить тут технологию blockchain.
                0
                На самом деле, для обнаружения узлов можно использовать открытые BitTorrent-трекеры и сеть Mainline DHT (которая основана на Kademlia). Тогда свой сервер анонсов писать не придётся. И клиент будет подключён к той же DHT-сети, что и торрент-клиенты (которых много), таким образом имея доступ к огромной хеш-таблице и, в свою очередь, помогая работе DHT.

                Я для своего полностью распределённого приложения инкрементальной синхронизации файлов для service discovery использовал именно такой подход, работает замечательно.
            0
            Правда Ваша, удалил лишний хаб.
              0
              Приложения написаны на Python версии 2.7, протестированы под Debian 7.7.


              Какова причина такого выбора?
                0
                Модуль pystun под python 2.7. Если смущает Debian 7.7, то под 8.3 и Xubuntu 14.04 тоже работает.
                  0
                  Под Alt Linux 6.0, кстати, тоже работает.
                  +2
                  Что-то мне подсказывает, что это не p2p в привычном понимании, а просто еще одна реализация режима direct, который давным давно уже был в мессенджерах. Могу ошибаться, но по-моему одна из реализаций icq-клиента подразумевала именно такой режим работы.
                    +1
                    Тут еще неплохо бы вычислить MTU. UDP пакет может быть до 65536 байт, но как только он превышает IP пакет, то потери резко увеличиваются.
                    И через сотовые интернеты работать не будет, т.к. там потери UDP 80%+
                      0
                      Дельное замечание, спасибо!
                      0
                      Есть хитрый ход (вместо STUN сервера), тройной пинг, узел А шлет пакет на B, а B на A, как только узел А получил пакет от B, то можно сразу слать инфу, т.к. B готов к приему, маршруты на роутерах сформировались. Для ускорения установка связи в три шага, условно 3 пакета A-B-A с обоих сторон, тогда первый пробивший NAT максимально быстро установит двухстороннюю связь, т.е. НАТы будут готовы слать в обе стороны.
                      А СТУНы тут лишние, виндовый брандмауэр всего минуту ждет прежде чем входящие пакеты запретить. Т.е. если открываешь случайный порт, то у тебя минута чтобы получить на него ответ.
                      Это все работает при идеальных условиях, пока пакеты не теряются. Как начинают теряться тоже работает, но сложнее.
                        0
                        О, спасибо! Давным-давно нечто подобное было очень нужно для одной поделки, но по незнанию думал, что ничего подобного провернуть нельзя и даже гуглить в эту сторону не стал, а зря.
                          0
                          Советую что-нибудь почитать про маршрутизацию IP сетей. Опять же решение не идеальное, будет работать в 80-90% случаев. Если надо 100%, то без TCP не обойтись. Не любят UDP инет-провайдеры, отчасти из-за торрентов, отчасти из-за неуправляемости.
                          0
                          Можно и не друг-друга пинговать, а любой адрес, при этом A может не знать адреса B заранее. Скажем, A пингует 1.2.3.4 и не знает адреса B, а B, в свою очередь, знает адрес A, отправляет ему ICMP-пакет от своего адреса, что 1.2.3.4 недоступен, и все, NAT пробит.

                          Есть работающая реализация — pwnat.
                            0
                            Вы только что описали TURN реализацию

                            Она повсеместно используется если NAT закрыт, беда в том, что В приходится гонять кучу траффика через себя. В паблике TURN серверов почти нет.
                            +3
                            Есть еще одна проблема: два клиента за одним роутером могут друг-друга не увидеть. Некоторые роутеры пускают пакеты изнутри-внутрь, а некоторые рубят. Поэтому тут локальные IP тоже надо использовать.
                              0
                              Вы ценный комментатор ), была бы карма, плюсанул бы!
                                +3
                                Год бился об эту тему ))) не взлетело. Взял паузу. Точнее взлетело, но не на 100% как хотелось бы. GSM-свистки все испортили. Поэтому делюсь забегами по граблям, может кому поможет. Сейчас с мыслями собираюсь, время появится и пойду на второй забег, версия 2.0 )))
                                А так UDP — тема. В 100 Мбит сети одного провайдера выжимал 11 Мбайт/сек, в гигабитной локалке 86 Мбайт/сек.
                                • UFO just landed and posted this here
                                    0
                                    Пакет был 1500 пакет езернета минус 20 заголовок IP, минус 8 заголовок UDP. Минус мой заголовок 16 байт. Замерял полезные данные (без заголовков). 140 гигабайт/сек это сетка 1200-1300 Гбит, завидую :)
                                    • UFO just landed and posted this here
                                        0
                                        143 гигабита в гигабитной сетке не может быть. Я так понимаю что гигабит это максимальная пропускная способность канала. Т.е. идеальная передача, когда канал нисколько не простаивает. Если данных для передачи будет больше, то они просто будут скапливаться на входе в очереди.
                                        Если пакеты маленькие и их много, то тут возрастает нагрузка на свичи, т.к. они оперируют пакетами, т.е. им есть разница обработать 64 пакета по 1500 байт или 1500 пакетов по 64 байта, во втором случае работы больше и обычное домашнее железо может просто не успеть их обработать, отсюда будут простои канала в ожидании данных, т.е. скорость упадет ниже гигабита.
                                        • UFO just landed and posted this here
                                      +1
                                      Я в локалке 100к+ ппс делал
                                      Судя по цифрам, гоняли через loopback? :D
                                0
                                Раз у начал делиться, пишу все что было. Еще одни грабли от нехороших провайдеров: задержка пакетов до десятков секунд. Это тоже нездорово, т.к. считаешь пакет потерянным, а он приходит через полминуты-минуту, завис где-то в кэше. Одни провайдеры его просто убивают, а другие доставляют любой ценой.
                                  0
                                  Не хотите статью написать с подробностями о встреченных подводных камнях, думаю Хабрасообществу был-бы интересен такой опыт? На каком языке реализовывали задумки и каков конечный результат?
                                    0
                                    Как будет результат — тогда и напишу. А пока просто опыт неудачника. Не думаю что он кому-то интересен.
                                      0
                                      Очень даже интересен! Тоже хочу аналог TCP сделать, опыта набираюсь потихоньку. Интересуют любые подробности про подводные камни. Про 80% потерь на GSM я и не подозревал…
                                      0
                                      Язык С. Результат не очень. Повторить TCP не удалось. Задача была: direct-доставка сообщений любого размера. Решил проблемы на 90%, надо 100%.
                                      0
                                      Мы экспериментальным путём обнаружили немного другое. После определённого периода (секунд 10-20) неиспользования канала данных, канал разрывается оператором где-то на нижнем (канальном?) уровне; при этом высшие уровни не информируются. Т.е. если было поднято TCP соединение, оно так и останется поднятым и неразорванным. При дальнейшей попытке использования канал восстанавливается (опять же, без сигнализации), на что уходит 5-10 секунд. То есть на уровне TCP, если передавать часто — отклик очень быстрый, но стоит немного задержаться — и получаем огромный лаг. Опять же, нет никакой гарантии, что восстановить канал удастся. В общем выходит так, что ориентироваться на сигнализацию в TCP нельзя, и быть уверенным в передаче, если соединение установилось — тоже нельзя; только ACK на уровне приложения!
                                      Я почти уверен, что большинство мобильных сетей ведут себя подобным образом; различаются детали (период ожидания до разъединения и т.п.). Просто они же так экономят критический ресурс — ёмкость соты.
                                      Про UDP, к сожалению, не знаю, но что-то мне подсказывает, что поведение может быть похожим: пока канал поднят, UDP будет пролетать; чуть задержались — UDP или будет теряться, или, как вы пишете, буферизоваться где-то в том месте, где управляется состояние канала.
                                        0
                                        В GSM просто терялись UDP пакеты. Главная проблема кстати и были эти провалы. То связь есть, долетает относительно быстро и вдруг раз и встало все от секунд до десятков секунд, и никак под это не подстроишься, не предскажешь, не продиагностируешь быстро, нет какой-то стабильной последовательности. Есть подозрение что там просто под TCP приоритеты выставлены, т.к. потери IP пакетов от TCP соединения гарантированно вызовут повторные отправки что еще больше забьет канал. Во-вторых по UDP идут в основном торренты да скайпы всякие, что сотовым не особо и надо. TeamViewer тоже очень плохо работает, т.к. тоже UDP.

                                        Запаздывание пакетов я наблюдал не в GSM, а на входе в ЦОД, где хостилась моя виртуалка с сервером. Тут кстати UDP тоже быстрее работает, т.к. TCP зажимается провайдером и при заявленном интерфейсе 100 Мбит реально одно TCP соединение дает 10-30, UDP немного побольше, но далеко не 100.
                                      0
                                      И еще, в одном провайдере не проходил пакет с конкретными данными. Просто не проходил сколько не посылай. Как подсказали, оказалось первые 8 байт совпали с торрентами, провайдер просто рубил установку соединения между пирами. Т.е. тупо рубилось по сигнатуре. Не любят провайдеры UDP, там где не любят надо туннелировать в TCP.
                                        0
                                        Я и с TCP наступал на похожее, только не у провайдера, а на MS TMG (мётрвая штука, но всё же вполне допустимый прецендент). Один абстрактный пакет данных не проходит вообще. Оказалось, совпал с сигнатурой какого-то вируса в его базе данных.
                                        0
                                        Мутная тема, сложная, всё хочу добраться до неё, да всё никак. Автор молодец, полезное дело.
                                          0
                                          Спасибо!
                                          +1
                                          Интересная тема, я тоже написал чат работающий примерно по такой схеме.
                                          Правда без STUN сервера, у меня публичный сервер и его функции выполняет. Связь клиент-сервер сделана по TCP, а клиент-клиент по UDP.
                                          Исходники, кстати, в открытом доступе: GitHub
                                            0
                                            А вы не думали использовать DHT вместо сигнальных серверов? Там можно распределенно держать как таблицу пользовательских профилей, так и сигналы (приглашения) на соединение.
                                            В двух словах DHT можно представить как распределенное key-value хранилище, Только ключи упорядоченны, хотя и рандомны, а пирам присвоены ключи из того же пространства. Зная несколько произвольных пиров (поддерживая с ними коннекты), всегда можно понять какие из них «ближе» к искомому ключу. Делается запрос именно на эти пиры и, если они сами не знают значения, они делятся контактами с теми своими «знакомыми» пирами, которые в свою очередь ещё «ближе» к вашей цели. Таким образом по принципу «теории 6 рукопожатий» мы очень быстро выходим на пир с самым «ближайшим» адресом к искомому ключу. Если этот пир не знает значения, то никто не знает или просто все кто знают оффлайн и нужно ждать. Пиры хранят не только свои значения, но и ближайшие. Обычно у пиров есть лимит на хранение данных, скажем 100мб, и они складывают туда все ближайшие пары ключ-значение, что влезут в ограничение.
                                            Это основа. На ней реализованы реально работающие сети и можно ими пользоваться для хранения данных о контактах.
                                              0
                                              DHT не использовалась, т.к. в формулировке задачи был указан именно сигнальный сервер.
                                              Идея распределённых хеш-таблиц в децентрализованных распределённых системах — хорошая, с точки зрения доступности информации. Но нужно учитывать среднее время получения ключа по значению при поиске подходящего пира, с учетом времени, в течении которого нужный порт абонента, с которым хочу связаться, открыт. Исходя из комментариев коллег, полученные опытным путем данные говорят, что это время составляет всего десятки секунд.
                                              На сигнальном сервере информация всегда актуальна, т.к. обновляется в заданные промежутки времени запросами от клиентов (при необходимости можно поднять несколько сигнальных серверов, между которыми реплицировать обновляемые данные, а если один из основных сигнальных серверов гаснет, клиент связывается с другим доступным).
                                              Как быстро данные будут обновляться от пира к пиру, как быстро нужное значение может быть найдено?
                                                0
                                                Не буду строить из себя эксперта, ни разу ещё не экспериментировал с этой штукой. Однако мне кажется информация по DHT может ходить не так уж и медленно. Если коннекты с соседями у каждого онлайн-пира установлены и поддерживаются постоянно, то длительность передачи пакета по такой сети будет примерно равно удвоенному логарифму от объёма сети, умноженному на время передачи пакета по уже поднятому коннекту. Ну то есть не десятки секунд. Да, наврено при малом объёме сети и низкой связности вся эта схема будет работать нестабильно, но в штатном режиме при некоторой степени избыточности в построении маршрутов (посылать запросы не ближайшему к цели соседу, а нескольким ближайшим) всё должно быть нормально… по крайней мере я не вижу явных проблем.
                                                Ктсати, следует ранжирвать «соседних» пиров не только по их «ключевому расстоянию», но и по их субъективному рейтингу, расчитываемому на основе пинга и прочих показателей стабильности канала до них.
                                                  0
                                                  Тоже далеко не эксперт, а Ваша идея интересная и требует своего кропотливого исследователя.
                                              0
                                              Делал подобные вещи и для C# даже написал велосипедную библиотеку для этого всего.
                                              Если знать нюансы которые описаны в wikipedia большинство и вдобавок посмотреть практическую часть,
                                              то ничего сложного нет. И в понимании NAT, STUN, MTU и прочих вещей.
                                              Вот если что библиотека. Может кому интересно. Кода там мало, он как по мне читабельный и подойдет если не хочется писать велоспиеды.
                                              github.com/RevenantX/LiteNetLib

                                              Only users with full accounts can post comments. Log in, please.