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

Комментарии (46)


  1. mwizard
    27.06.2016 18:15
    +3

    И в чем же децентрализация, если достаточно погасить сигнальный сервер, чтобы все умерло?


    1. D_T
      27.06.2016 19:07

      Тут можно 100 сигнальных серверов поднять и гасить устанешь. Он же не несет никакой нагрузки, просто сводит отправителя и получателя.


      1. gurinderu
        27.06.2016 19:17

        Достаточно создать правило deny all и сделать whitelist как в Китае, чтобы все перестало работать.


        1. D_T
          27.06.2016 19:37

          Достаточно выйти в инет через GSM-свисток и все перестанет работать. UDP хорошо ходит в открытом инете, а вот «последняя миля» все портит. А так да, админ может легко все запретить.


    1. 0x9d8e
      27.06.2016 19:36

      Возможно в качестве таких серверов могут выступать клиенты, имеющие статический адрес. Но тут возникает другая проблема: как без сервера узнавать адреса таких клиентов? Вариантов вижу не много:
      0) Зашитые в клиенте адреса;
      1) Широкополосные запросы в локальных сетях;
      2) Ссылки, получаемые по другим каналам (от знакомых например);
      3) Перебор (полушутка);
      Может быть ещё можно было бы выделить какое-то использование сторонних сервисов (независимо от их «согласия»).


      1. Labunsky
        27.06.2016 20:42

        Еще есть довольно разумный вариант с броадкастом по списку, хранящимся в публичном репозитории. Осталось только репозиторий как-нибудь хранить децентрелизовано, но это уже задача привычнее


      1. trapwalker
        29.06.2016 11:18

        Я бы добавил пункт номер -1: DHT сети. Чуть подробнее я о них писал в комментарии ниже.
        А в дополнение к этому можно сделать открытый сервер анонсов, который может установить на свой сайт кто угодно. Этот сервер поддерживает стандартный URL вида mysite.com/announce.p2p или что-то вроде этого. Это просто json (возможно даже статический), где перечислены домены других таких же серверов, IP пиров, которые имеют белый адрес и часто онлайн (т.е. те, что с высоким рейтингом в сети). При самом первом входе в сеть нам нужен хотя бы один живой такой сервер анонсов. Дальше ссылки на сервера анонсов уже будут накапливаться клиентом, а в DHT будет таблица рейтинга серверов и нодов, чтобы сеть голосованием могла маркировать недобросовестные пиры и сервера анонсов.
        Ну и, чтобы два раза не вставать, можно подумать как приспособить тут технологию blockchain.


        1. GamePad64
          30.06.2016 12:48

          На самом деле, для обнаружения узлов можно использовать открытые BitTorrent-трекеры и сеть Mainline DHT (которая основана на Kademlia). Тогда свой сервер анонсов писать не придётся. И клиент будет подключён к той же DHT-сети, что и торрент-клиенты (которых много), таким образом имея доступ к огромной хеш-таблице и, в свою очередь, помогая работе DHT.

          Я для своего полностью распределённого приложения инкрементальной синхронизации файлов для service discovery использовал именно такой подход, работает замечательно.


  1. webtrium
    27.06.2016 18:21

    Правда Ваша, удалил лишний хаб.


  1. tmnhy
    27.06.2016 18:36

    Приложения написаны на Python версии 2.7, протестированы под Debian 7.7.


    Какова причина такого выбора?


    1. webtrium
      27.06.2016 18:47

      Модуль pystun под python 2.7. Если смущает Debian 7.7, то под 8.3 и Xubuntu 14.04 тоже работает.


    1. webtrium
      27.06.2016 18:59

      Под Alt Linux 6.0, кстати, тоже работает.


  1. tmnhy
    27.06.2016 18:42
    +2

    Что-то мне подсказывает, что это не p2p в привычном понимании, а просто еще одна реализация режима direct, который давным давно уже был в мессенджерах. Могу ошибаться, но по-моему одна из реализаций icq-клиента подразумевала именно такой режим работы.


  1. D_T
    27.06.2016 19:04
    +1

    Тут еще неплохо бы вычислить MTU. UDP пакет может быть до 65536 байт, но как только он превышает IP пакет, то потери резко увеличиваются.
    И через сотовые интернеты работать не будет, т.к. там потери UDP 80%+


    1. webtrium
      27.06.2016 19:35

      Дельное замечание, спасибо!


  1. D_T
    27.06.2016 19:17

    Есть хитрый ход (вместо STUN сервера), тройной пинг, узел А шлет пакет на B, а B на A, как только узел А получил пакет от B, то можно сразу слать инфу, т.к. B готов к приему, маршруты на роутерах сформировались. Для ускорения установка связи в три шага, условно 3 пакета A-B-A с обоих сторон, тогда первый пробивший NAT максимально быстро установит двухстороннюю связь, т.е. НАТы будут готовы слать в обе стороны.
    А СТУНы тут лишние, виндовый брандмауэр всего минуту ждет прежде чем входящие пакеты запретить. Т.е. если открываешь случайный порт, то у тебя минута чтобы получить на него ответ.
    Это все работает при идеальных условиях, пока пакеты не теряются. Как начинают теряться тоже работает, но сложнее.


    1. 0x9d8e
      27.06.2016 19:40

      О, спасибо! Давным-давно нечто подобное было очень нужно для одной поделки, но по незнанию думал, что ничего подобного провернуть нельзя и даже гуглить в эту сторону не стал, а зря.


      1. D_T
        27.06.2016 19:56

        Советую что-нибудь почитать про маршрутизацию IP сетей. Опять же решение не идеальное, будет работать в 80-90% случаев. Если надо 100%, то без TCP не обойтись. Не любят UDP инет-провайдеры, отчасти из-за торрентов, отчасти из-за неуправляемости.


    1. ValdikSS
      28.06.2016 00:17

      Можно и не друг-друга пинговать, а любой адрес, при этом A может не знать адреса B заранее. Скажем, A пингует 1.2.3.4 и не знает адреса B, а B, в свою очередь, знает адрес A, отправляет ему ICMP-пакет от своего адреса, что 1.2.3.4 недоступен, и все, NAT пробит.

      Есть работающая реализация — pwnat.


    1. at0mic
      28.06.2016 01:05

      Вы только что описали TURN реализацию

      Она повсеместно используется если NAT закрыт, беда в том, что В приходится гонять кучу траффика через себя. В паблике TURN серверов почти нет.


  1. D_T
    27.06.2016 19:50
    +3

    Есть еще одна проблема: два клиента за одним роутером могут друг-друга не увидеть. Некоторые роутеры пускают пакеты изнутри-внутрь, а некоторые рубят. Поэтому тут локальные IP тоже надо использовать.


    1. webtrium
      27.06.2016 19:53

      Вы ценный комментатор ), была бы карма, плюсанул бы!


      1. D_T
        27.06.2016 20:04
        +3

        Год бился об эту тему ))) не взлетело. Взял паузу. Точнее взлетело, но не на 100% как хотелось бы. GSM-свистки все испортили. Поэтому делюсь забегами по граблям, может кому поможет. Сейчас с мыслями собираюсь, время появится и пойду на второй забег, версия 2.0 )))
        А так UDP — тема. В 100 Мбит сети одного провайдера выжимал 11 Мбайт/сек, в гигабитной локалке 86 Мбайт/сек.


        1. Ivan_83
          27.06.2016 21:10

          Плохо жал.
          Я в локалке 100к+ ппс делал. Если бы это были пакеты по 1400 байт то получилось бы 140 гигабайт/сек.
          Имеет смысл замерять пакеты а не их размер.

          Максимальный размер UDP не 65535 а 65507 байт.


          1. D_T
            27.06.2016 21:21

            Пакет был 1500 пакет езернета минус 20 заголовок IP, минус 8 заголовок UDP. Минус мой заголовок 16 байт. Замерял полезные данные (без заголовков). 140 гигабайт/сек это сетка 1200-1300 Гбит, завидую :)


            1. Ivan_83
              28.06.2016 10:09

              Сетка гигабит, на обычном ширпотребе, просто если бы вместо 64 байта были пакеты по 1500 то да, было бы
              143 гигабита/сек, ошибся немного.

              А смысл замерять объёмы?
              Нужно измерять сколько пакетов связка прожуёт, а далее просто умножать на размер полезной нагрузки.

              2 mwizard:
              Учимся считать.
              100кпакетов * 64 байта в пакете = 6,2 мегабайта — такое даже в 100 мегабит уложится.
              Суть теста была в том, сколько пакетов в ответ сможет сгенерировать тазик уровня коредуо Е5300 в один поток, и получилось где то 140-160кппс, что в общем для данной задачи было весьма круто.


              1. D_T
                28.06.2016 10:32

                143 гигабита в гигабитной сетке не может быть. Я так понимаю что гигабит это максимальная пропускная способность канала. Т.е. идеальная передача, когда канал нисколько не простаивает. Если данных для передачи будет больше, то они просто будут скапливаться на входе в очереди.
                Если пакеты маленькие и их много, то тут возрастает нагрузка на свичи, т.к. они оперируют пакетами, т.е. им есть разница обработать 64 пакета по 1500 байт или 1500 пакетов по 64 байта, во втором случае работы больше и обычное домашнее железо может просто не успеть их обработать, отсюда будут простои канала в ожидании данных, т.е. скорость упадет ниже гигабита.


                1. Ivan_83
                  28.06.2016 16:46

                  Я делал нагрузочное тестирование.
                  Нагружал мелкими пакетами по 64 байта (может и меньше, но минимальный размер эзернет фрейма 64 байта).
                  У меня получилось 100к пакетов в секунду, на 6,2 мегабайта/сек.
                  Если бы это были пакеты по 1500 размером то их было бы сильно меньше ибо гиговый линк, но если предположить что линк резиновый то я бы утилизировал 143 гигабита/сек.

                  Это всё к тому, что нагрузочные тестирования имеют смысл в пакетах а не мегабитах.


          1. mwizard
            27.06.2016 22:49
            +1

            Я в локалке 100к+ ппс делал
            Судя по цифрам, гоняли через loopback? :D


  1. D_T
    27.06.2016 20:35

    Раз у начал делиться, пишу все что было. Еще одни грабли от нехороших провайдеров: задержка пакетов до десятков секунд. Это тоже нездорово, т.к. считаешь пакет потерянным, а он приходит через полминуты-минуту, завис где-то в кэше. Одни провайдеры его просто убивают, а другие доставляют любой ценой.


    1. webtrium
      27.06.2016 20:40

      Не хотите статью написать с подробностями о встреченных подводных камнях, думаю Хабрасообществу был-бы интересен такой опыт? На каком языке реализовывали задумки и каков конечный результат?


      1. D_T
        27.06.2016 20:50

        Как будет результат — тогда и напишу. А пока просто опыт неудачника. Не думаю что он кому-то интересен.


        1. 15432
          27.06.2016 23:51

          Очень даже интересен! Тоже хочу аналог TCP сделать, опыта набираюсь потихоньку. Интересуют любые подробности про подводные камни. Про 80% потерь на GSM я и не подозревал…


      1. D_T
        27.06.2016 20:56

        Язык С. Результат не очень. Повторить TCP не удалось. Задача была: direct-доставка сообщений любого размера. Решил проблемы на 90%, надо 100%.


    1. port443
      28.06.2016 11:29

      Мы экспериментальным путём обнаружили немного другое. После определённого периода (секунд 10-20) неиспользования канала данных, канал разрывается оператором где-то на нижнем (канальном?) уровне; при этом высшие уровни не информируются. Т.е. если было поднято TCP соединение, оно так и останется поднятым и неразорванным. При дальнейшей попытке использования канал восстанавливается (опять же, без сигнализации), на что уходит 5-10 секунд. То есть на уровне TCP, если передавать часто — отклик очень быстрый, но стоит немного задержаться — и получаем огромный лаг. Опять же, нет никакой гарантии, что восстановить канал удастся. В общем выходит так, что ориентироваться на сигнализацию в TCP нельзя, и быть уверенным в передаче, если соединение установилось — тоже нельзя; только ACK на уровне приложения!
      Я почти уверен, что большинство мобильных сетей ведут себя подобным образом; различаются детали (период ожидания до разъединения и т.п.). Просто они же так экономят критический ресурс — ёмкость соты.
      Про UDP, к сожалению, не знаю, но что-то мне подсказывает, что поведение может быть похожим: пока канал поднят, UDP будет пролетать; чуть задержались — UDP или будет теряться, или, как вы пишете, буферизоваться где-то в том месте, где управляется состояние канала.


      1. D_T
        28.06.2016 14:26

        В GSM просто терялись UDP пакеты. Главная проблема кстати и были эти провалы. То связь есть, долетает относительно быстро и вдруг раз и встало все от секунд до десятков секунд, и никак под это не подстроишься, не предскажешь, не продиагностируешь быстро, нет какой-то стабильной последовательности. Есть подозрение что там просто под TCP приоритеты выставлены, т.к. потери IP пакетов от TCP соединения гарантированно вызовут повторные отправки что еще больше забьет канал. Во-вторых по UDP идут в основном торренты да скайпы всякие, что сотовым не особо и надо. TeamViewer тоже очень плохо работает, т.к. тоже UDP.

        Запаздывание пакетов я наблюдал не в GSM, а на входе в ЦОД, где хостилась моя виртуалка с сервером. Тут кстати UDP тоже быстрее работает, т.к. TCP зажимается провайдером и при заявленном интерфейсе 100 Мбит реально одно TCP соединение дает 10-30, UDP немного побольше, но далеко не 100.


  1. D_T
    27.06.2016 20:49

    И еще, в одном провайдере не проходил пакет с конкретными данными. Просто не проходил сколько не посылай. Как подсказали, оказалось первые 8 байт совпали с торрентами, провайдер просто рубил установку соединения между пирами. Т.е. тупо рубилось по сигнатуре. Не любят провайдеры UDP, там где не любят надо туннелировать в TCP.


    1. port443
      28.06.2016 14:19

      Я и с TCP наступал на похожее, только не у провайдера, а на MS TMG (мётрвая штука, но всё же вполне допустимый прецендент). Один абстрактный пакет данных не проходит вообще. Оказалось, совпал с сигнатурой какого-то вируса в его базе данных.


  1. Mugik
    27.06.2016 22:33

    Мутная тема, сложная, всё хочу добраться до неё, да всё никак. Автор молодец, полезное дело.


    1. webtrium
      28.06.2016 02:24

      Спасибо!


  1. Nirvano
    28.06.2016 19:02
    +1

    Интересная тема, я тоже написал чат работающий примерно по такой схеме.
    Правда без STUN сервера, у меня публичный сервер и его функции выполняет. Связь клиент-сервер сделана по TCP, а клиент-клиент по UDP.
    Исходники, кстати, в открытом доступе: GitHub


  1. trapwalker
    29.06.2016 11:00

    А вы не думали использовать DHT вместо сигнальных серверов? Там можно распределенно держать как таблицу пользовательских профилей, так и сигналы (приглашения) на соединение.
    В двух словах DHT можно представить как распределенное key-value хранилище, Только ключи упорядоченны, хотя и рандомны, а пирам присвоены ключи из того же пространства. Зная несколько произвольных пиров (поддерживая с ними коннекты), всегда можно понять какие из них «ближе» к искомому ключу. Делается запрос именно на эти пиры и, если они сами не знают значения, они делятся контактами с теми своими «знакомыми» пирами, которые в свою очередь ещё «ближе» к вашей цели. Таким образом по принципу «теории 6 рукопожатий» мы очень быстро выходим на пир с самым «ближайшим» адресом к искомому ключу. Если этот пир не знает значения, то никто не знает или просто все кто знают оффлайн и нужно ждать. Пиры хранят не только свои значения, но и ближайшие. Обычно у пиров есть лимит на хранение данных, скажем 100мб, и они складывают туда все ближайшие пары ключ-значение, что влезут в ограничение.
    Это основа. На ней реализованы реально работающие сети и можно ими пользоваться для хранения данных о контактах.


    1. webtrium
      29.06.2016 15:05

      DHT не использовалась, т.к. в формулировке задачи был указан именно сигнальный сервер.
      Идея распределённых хеш-таблиц в децентрализованных распределённых системах — хорошая, с точки зрения доступности информации. Но нужно учитывать среднее время получения ключа по значению при поиске подходящего пира, с учетом времени, в течении которого нужный порт абонента, с которым хочу связаться, открыт. Исходя из комментариев коллег, полученные опытным путем данные говорят, что это время составляет всего десятки секунд.
      На сигнальном сервере информация всегда актуальна, т.к. обновляется в заданные промежутки времени запросами от клиентов (при необходимости можно поднять несколько сигнальных серверов, между которыми реплицировать обновляемые данные, а если один из основных сигнальных серверов гаснет, клиент связывается с другим доступным).
      Как быстро данные будут обновляться от пира к пиру, как быстро нужное значение может быть найдено?


      1. trapwalker
        29.06.2016 16:36

        Не буду строить из себя эксперта, ни разу ещё не экспериментировал с этой штукой. Однако мне кажется информация по DHT может ходить не так уж и медленно. Если коннекты с соседями у каждого онлайн-пира установлены и поддерживаются постоянно, то длительность передачи пакета по такой сети будет примерно равно удвоенному логарифму от объёма сети, умноженному на время передачи пакета по уже поднятому коннекту. Ну то есть не десятки секунд. Да, наврено при малом объёме сети и низкой связности вся эта схема будет работать нестабильно, но в штатном режиме при некоторой степени избыточности в построении маршрутов (посылать запросы не ближайшему к цели соседу, а нескольким ближайшим) всё должно быть нормально… по крайней мере я не вижу явных проблем.
        Ктсати, следует ранжирвать «соседних» пиров не только по их «ключевому расстоянию», но и по их субъективному рейтингу, расчитываемому на основе пинга и прочих показателей стабильности канала до них.


        1. webtrium
          29.06.2016 17:34

          Тоже далеко не эксперт, а Ваша идея интересная и требует своего кропотливого исследователя.


  1. RevenantX
    29.06.2016 14:35

    Делал подобные вещи и для C# даже написал велосипедную библиотеку для этого всего.
    Если знать нюансы которые описаны в wikipedia большинство и вдобавок посмотреть практическую часть,
    то ничего сложного нет. И в понимании NAT, STUN, MTU и прочих вещей.
    Вот если что библиотека. Может кому интересно. Кода там мало, он как по мне читабельный и подойдет если не хочется писать велоспиеды.
    github.com/RevenantX/LiteNetLib