Интернет сейчас переживает не лучшие времена: блокировки (facebook.com*, twitter.com) и сбои в работе сайтов (vk.com, google.com), накручивание донатов (Telegram), поэтому я задался вопросом о создании мессенджера без всех этих проблем. Да это много кто-пытался делать (не только я), но без особого успеха. Но я преследую прежде всего другую цель в попытках писать велосипеды: узнать точно (не считая схем) как работает та или иная программа. Сначала я писал свою программу на Python, используя самописанный эхо-сервер. Даже тестировал её со своим другом. Но у программы был серьёзный баг - при выходе пользователя сервер и клиент крашился, наверное из-за того что я не реализовал закрытие сокета и удаления пользователя из списка. Я хотел исправить этот баг (кто знает в чём ошибка тому кидаю свой код), но со временем код испортился из-за того что я преждевременно хотел добавить новые фичи: параллельный веб-сервер например. Поэтому я решил полностью переписать свой мессенджер сделав его лучше. Для этого я выбрал протокол UDP, С# и технологию P2P вместо федерации серверов.

Устройство мессенджера

Принцип работы я взял у Torrent'а: имеется центральный сервер которому хосты сливают свои IP-адреса и информацию (имена/никнеймы) потом сами хосты получают эту информацию обратно и таким образом узнают друг друга. Таким образом легче модерировать список хостов.

Код координирующего сервера:

import socket

sock = socket.socket()

sock.bind(('', 9090))
sock.listen(5)
addrs = []
#addrs.append('blank')
print("Coordinating server running")
while True:
    conn, addr = sock.accept()
    #conn.send('\n'.join(addrs).encode())
    data = conn.recv(27+10)
    message = data.decode()
    if message == 'get_users':
        conn.send('\n'.join(addrs).encode())
    else:
        addrs.append(message)
    print(message)
    
    conn.close()

Получив IP-адрес другого хоста вызывающий хост отправляет ему вместе с сообщением своё имя (по которому в дальнейшим получается IP-адрес) благодаря чему устанавливается связь между хостами.

private void sendButton_Click(object sender, EventArgs e)
        {


            client = new UdpClient(/*Int32.Parse(remotePort.Text)*/);
            // запускаем задачу на прием сообщений
            //if (username.Text != "")
            //{
            //    nicknames[Array.IndexOf(nicknames, username.Text)] = "";
            //    source.Clear();
            //    source.AddRange(nicknames);
            //    textBox1.AutoCompleteCustomSource = source;
            //}
            if (username.Text == "")
                MessageBox.Show("Введите имя!");
            else if (!nicknames.Contains(username.Text))
            {
                MessageBox.Show("Нет зарегистрированного пользователя с таким именем!");
            }
            else
            {

                // отправляем первое сообщение о входе нового пользователя
                string message = String.Format("{0}: {1}", username.Text, field.Text);
                //string message = field.Text; //userName + $" ({address}) вошел в чат";
                byte[] data = Encoding.Unicode.GetBytes(message);
                try
                {

                    client.Send(data, data.Length, ports[0], Int32.Parse(ports[1]));
                    field.Clear();
                    string time = DateTime.Now.ToShortTimeString();
                    WriteMessage($"[{time}]  {message}", true, state);//\r\n");
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Получатель не известен!\n" + ex.Message);

                }




            }
            //WriteMessage(message + "\r\n");
            //WriteMessage(ports[1]);
            //Task.Run(ReceiveMessages);
        }
  async void ReceiveMessages()
        {
            //reciever = new UdpClient(Int32.Parse(localPort.Text));
            alive = true;
            try
            {
                while (alive)
                {

                    IPEndPoint remoteIp = new IPEndPoint(IPAddress.Any, 0); //null;
                    byte[] data = client.Receive(ref remoteIp);
                    string message = Encoding.Unicode.GetString(data);
                    //WriteMessage(message);
                    string name = message.Split(':')[0];
                    sender_name = name;
                    if (!nodes.Contains(SetAddress(name))) 
                        nodes.Add(SetAddress(name));
                    //addr = addrs[Array.IndexOf(nicknames, name)].Split(':')[0];
                    //Match match = Regex.Match(message, @"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b");
                    //if (match.Success)
                    //    addr = IPAddress.Parse(message);
                    /*if(((*/
                    //IPAddress.TryParse(message, out addr);//){
                    //MessageBox.Show(addr.ToString());
                    //}
                    // добавляем полученное сообщение в текстовое поле
                    this.Invoke(new MethodInvoker(() =>
                    {
                        string time = DateTime.Now.ToShortTimeString();
                        WriteMessage($"[{time}]  {message}", true, state);//\r\n");

                    }));


                }
            }
            catch (ObjectDisposedException)
            {
                if (!alive)
                    return;
                throw;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

TODO

Можно, конечно, реализовать аккаунты в P2P-системе хранив данные на других активных машинах, но я пока не заморачивался этим вопросом так как я хотел реализовать простой мессенджер (не для домохозяек).

Также мессенджер пока может только общается только один-на-один без бесед. Но я думаю это исправляется рассылкой пришедшим пользователю писем остальным пользователям.

Ссылки

Ссылка на репозиторий с кодом - arutimasu/echo: P2P Messenger (github.com)

Ссылка на первую версию программы - echo/old at main · arutimasu/echo (github.com)

*Запрещённая в России соцсеть

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


  1. Zara6502
    13.12.2023 17:34

    мой минимум функций (можно и максимум) у мессенджера:

    1. отправка сообщений, картинок, ссылок, голоса

    2. пересылка сообщений

    3. пересылка контакта, добавление контакта

    4. создание групп


    1. lolikandr
      13.12.2023 17:34

      Накидываем:

      • не есть батарею мобилки (минимум процессорного времени)

      • не торомозить в фоновом режиме (например, принимать сообщение сразу, а не через 122 минуты)

      • быть устойчивым к бану серверов

      • шифрование сессионным ключом

      • сохранять все данные локально и удалять их только с подтверждением от пользователя ( со стороны сервера/другого пользователя можно максимум разрешить скрыть дааные для удобства)

      • лёгкое создание веток разговоров и перемещение диалогов по этим веткам (как можно смотреть в эту одну длинную ленту?) в рамках одной группы


      1. Aelliari
        13.12.2023 17:34

        • не есть батарею мобилки (минимум процессорного времени)

        • не торомозить в фоновом режиме (например, принимать сообщение сразу, а не через 122 минуты)

        Для любого p2p будет болью. Либо это, либо условно-мгновенные пуши


        1. AgentFire
          13.12.2023 17:34

          p2p в целом плохая затея. сами вспомните, каким овном воспринимался whatsapp в сравнении с телегой.


      1. eri
        13.12.2023 17:34

        Первые два пункта взаимоисключающие, 3 и 5 тоже


      1. Zara6502
        13.12.2023 17:34

        это не функции мессенджера, кроме последнего, хотя последнее я не понял совсем зачем нужно, наверное для больших групп?


  1. eri
    13.12.2023 17:34

    На удп нет надежной доставки. Нужно поверх него поднимать транспортный протокол. Не изобретая велосипед можно взять rtp. Я писал такой, но использовал stun вместо сервера.

    Если мессенджер анонимный ( запрещено хостить в России), то можно не хранить пользователей вообще, а использовать какой-то идентификатор - например отпечаток ключа pgp.

    Возвращаясь к торрентами - можно выложить открытый ключ pgp на торрент трекер и использовать этот трекер как сервер поиска пиров.


    1. qw1
      13.12.2023 17:34

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


  1. AndreyDmitriev
    13.12.2023 17:34

    а если клиенты за NAT сидят?


    1. eri
      13.12.2023 17:34

      Если сервер белый, то UDP пробивает дырку. Через неё уже делается п2п


      1. Vamp
        13.12.2023 17:34

        А если UDP закрыт? В моей корпоративной сети он полностью запрещён. Наверняка найдутся ещё какие-нибудь примеры сетей, где UDP ограничен (публичный wifi, например).


        1. eri
          13.12.2023 17:34

          Тогда у вас не будет работать и видеочат в браузере и много чего ещё.


          1. Vamp
            13.12.2023 17:34

            Без промежуточного сервера тогда не обойтись. Ну я так и думал в принципе.


            1. eri
              13.12.2023 17:34

              Можно через turn


          1. shushu
            13.12.2023 17:34

            Я не уверен, но мне кажется что WebRTC в браузере не работает через UDP. Там WebSocket


            1. eri
              13.12.2023 17:34

              DTLS транспорт там. Это UDP

              RTP может ходить по вэбсокету в теории, но на практике там соединение устанавливается через SDP. Через интернет это обычно приводит к UDP, в редких случаях tcp через turn.


  1. lair
    13.12.2023 17:34

    Я правильно понимаю, что вашему мессенджеру нужно, чтобы у каждого клиента был доступный публичный адрес, открытый для входящих соединений?


    1. eri
      13.12.2023 17:34

      Не нужен, тут аналог stun


      1. lair
        13.12.2023 17:34

        Гм. Я хорошо помню, что торрентам был нужен открытый входящий порт. А как вы без этого обходитесь?


        1. eri
          13.12.2023 17:34

          Не нужен. С открытым портом оно работает лучше, но он не необходим.


          1. lair
            13.12.2023 17:34

            Как именно это работает? Можете показать место в вашем коде?

            (потому что я своими силами в вашем репозитории не могу найти приблизительно ничего)


            1. eri
              13.12.2023 17:34

              В каком репозитории?) я не автор статьи. Ща поищу свой хелловорд - скину)


              1. lair
                13.12.2023 17:34

                А, извините. Я спрашивал у автора статьи, как работает у него. В его конкретном коде.

                То, что это возможно теоретически - я знаю.


      1. qw1
        13.12.2023 17:34

        тут аналог stun

        "тут" его нет


        1. lair
          13.12.2023 17:34

          Вот да, мне было интересно посмотреть, но я на первый взгляд не смог найти.


  1. lair
    13.12.2023 17:34

    Ссылка на репозиторий с кодом

    У вас в репозитории полный бардак. Не надо коммитить туда собранные бинарники.


  1. aamonster
    13.12.2023 17:34

    Я правильно понимаю, что вы пишете проект только для локальной сети, чисто чтобы поиграться с p2p? (иначе чуть ли не первое, что придётся делать – это писать код для проброса соединений через NAT, причём он не всегда будет работать, и когда прямое соединение не установить – вам придётся-таки отправлять сообщения через ваш сервер – с чего, собственно, и стоило начинать, добавив p2p гораздо позже).


  1. no404error
    13.12.2023 17:34

    Принцип работы я взял у Torrent'а: имеется центральный сервер которому хосты сливают свои IP-адреса и информацию (имена/никнеймы) потом сами хосты получают эту информацию обратно и таким образом узнают друг друга. Таким образом легче модерировать список хостов.

    Слишком сложно для децентрализованного. Проще использовать сервисы текста с таймлайном вроде pastebin/justpaste/sharetxt... тонны их.

    А самое обалденное было в эпоху мессенджера whotel, когда сообщения со скользящим шифром хранились прямо в DNS записях какого-то числа доменов tel.


  1. Maxim_Q
    13.12.2023 17:34

    Может проще присоединится к уже имеющемуся проекту и немного под себя допилить? Вот есть разные мессенджеры, вы их хоть посмотрите может и не нужно велосипед изобретать:

    https://github.com/Nutomic/ensichat - Заброшен проект, но можно взять за основу или сделать клон.

    https://code.briarproject.org/briar/briar - Даже TOR может поддерживать

    https://github.com/mastodon/mastodon-android - Mastodon - это крупнейшая распределённая социальная сеть в интернете

    https://github.com/krille-chan/fluffychat - клиент matrix с открытым исходным кодом


  1. saboteur_kiev
    13.12.2023 17:34

    сами хосты получают эту информацию обратно и таким образом узнают друг друга. Таким образом легче модерировать список хостов.

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


    1. Zara6502
      13.12.2023 17:34

      а вам есть что прятать от товарища майора? на планете есть только одно место, где вы можете с относительной успешностью что-то хранить тайно - это ваша голова и то при определенной сноровке всё это оттуда извлекается.


      1. saboteur_kiev
        13.12.2023 17:34

        считаю, что чем меньше данных доступны кому-либо без предписания суда, тем лучше.

        и IP адрес - именно такие данные.


  1. aax
    13.12.2023 17:34

    Обхожусь без велосипедов. Сейчас пользуюсь GNU/GPL месседжером со сквозным шифрованием Jami(https://jami.net/), есть сборки для Linux, Windows, macOS, Android, iOS. Одноранговая("бессерверная") сеть использует распределенную хеш-таблицу для аудентефикации в аккаунте.

    Документация Ubuntu, исходя из достаточно широкого фунционала Jami рекомендует его и для корпоративного использования(есть поддержка видеоконференций, расшаривание рабочего стола и т.п., Jami так-же поддерживает функционал SIP/IAX софтофона).