image
Данная статья будет интересна людям, интересующимся сетями, логическим устройством серверов и нативным программированием. Здесь не будет долгих листингов исходных кодов, а только общие наброски и подходы.
TL;DR GitHub

Это как имиджборж только текстовый

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



Постановка проблемы


  • Реализовать TCP-multithread текстовый чат-сервер, к которому можно коннектиться и получать рассылку сообщений, а так же отправлять свои. В качестве клиента должно быть невероятно простое ПО – например nc. Или любой TCP-сокет, который принимает и отправляет данные (keep-alive, как привыкли web-программисты,.т.е поддерживает соединение).
  • Сегментация трафика по средствам стандартных чатрумов (меняем чатрум больше не получаем данные пользователей в старом чатруме).
  • Т.к. есть чатрумы – возможность просмотра людей в чатруме.
  • Привязка ника к определенной паре ip:port, при этом возможность его менять.
  • Ну и такая же маленькая текстовая админка (выкл сервера, получения статы и подобное)


Теоритическая часть


В сокетах беркли, которые работают с TCP протоколом есть API connect/accept, чтобы выполнить трехстороннее рукопожатие, и потом в скрытом от программиста режиме организовать поток, с контролем доставки и целостности данных. Проблема в том, что conntect/accept это блокирующий вызов, и нельзя подключится к нескольким портам одновременно (одним сокетом), чтобы этого не делать и снизить нагрузку на вычислительное устройство (поддержание большого количества коннектов) придумали клиент-серверную архитектуру, в которой сервер будет агрегировать весь трафик присылаемый на него разными клиентами и поддерживать те самые множественные соединения.
Есть два решения проблемы поддержания множественных connect/accept для серверов – это мультипоточность и использование неблокирующих сокетов. Я буду рассматривать первый случай, потому что это мне ближе.

Проектирование



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

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

Как я делал это

Для того, чтобы написать TCPConnectionHandler, сначала необходимо написать ThreadPool, который бы имел возможность создания и прерывания потоков (и разделение потоков на тех, которые находятся в состоянии завершения, и тех, которые находятся в работе).
А TCPConnectionHandler смотрел бы какие из соединений завершились, отвечал за broadcast/multicast рассылки к подключенным сокетам, и хранение/уничтожение пользовательских данных, которыми обладает сокет.

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

Микро-Админка


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

  1. Сервер генерирует challenge число или набор байт.
  2. Клиент хеширует одноразовым ключем challenge + комманду и отравляет хеш и комманду в открытом виде.
  3. Сервер повторно хеширует команду с challenge и сравнивает результат хеша с полученным.
  4. Если хеш валидный то комманда выполняется.


Примеры команд


Окно приветствия с маленьким help.


Скрытые возможности текстового чата


1. Обмен данными по хеш-чатрумам

Например не один нормальный пользователь не будет сидеть в чатруме с названием PbgkkCzrM8VToEgcDcCSfQdw5p1IaoRHiBu5d21XGv92c0fKmJUo3XoxFqtdN5tOzmRY5PrSQti6uKFOZTatQQ==
А вот боты вполне. И менять эти комнаты спустя некоторое время.

2. Прозрачное шифрование в текстовом представлении Base64.

Мы все уже привыкли к автоматическому шифрованию данных, а как будет ностальгично слать данные собеседникам, которые были зашифрованы с pre-shared-key.

3. Анонимность, привязанная только к паре ip:port (привет tor, i2p), и идентификация/аутентификация с собеседниками в пределах поддерживаемой сессии.


4. Легковесность данных.

Никакого дополнительного payload для поддержки сессии, или передачи контента. Только текстовый контент.

5. Простой клиент (nc, putty, etc)


6. Легковесность и производительность сервера

На моем маке используемая библиотека и клиент скомпилированные без оптимизаций весят 156 + 40 = 196 Кб. Т.е. возможность запускать на старых устройствах, с малым количеством оперативной памяти, и минимальной поддержкой POSIX.

7. OpenSource – открытость, и ничего лишнего. + возможность внести свои собственные доработки, как шифрование, идентификацию,
аутентификацию, передачу файлов и т.д.


GitHub, но держите сильно-впечатлительных подальше от монитора.
CMake, позволяет собрать под свою платформу и запустить на локальном хосте, mac/linux (+ windows in future с вашей помощью).
Допиливание фич по требованию.

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


  1. ToSHiC
    07.09.2015 17:23
    +1

    Сетевой чат без ивентлупа? Серьёзно?


    1. StrangerInRed
      07.09.2015 17:46

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


      1. ToSHiC
        07.09.2015 23:24
        +6

        Под ивентлупом я подразумеваю модель сетевого демона, который поллит набор сокетов каким либо образом (будь то select, epoll, kqueue или GetQueuedCompletionStatus) и обрабаывает полученные данные в одном потоке или тредпуле.

        Теперь подробнее. Сетевой чат — это типичный пример сетевого демона, который практически не требует cpu в своей работе. Если делать его на потоках (я мельком глянул на ваш код, и, на сколько я понял, вы использовали именно эту модель), то совершенно бездарно тратится куча памяти и подцессорного времени на контекст-свитчи между потоками. Скажем, 1000 клиентов на дефолтовых настройках в linux отожрут 8 гигов ОЗУ на стеки потоков.

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

        Вариант с потоком на клиента имеет право на жизнь, если расчёт чего либо на cpu занимает существенное время. Скажем, если вы сделали демона рекомендательной системы, который большую нейронку/SVM гоняет на запрос. Или если ваш демон картинку ресайзит. Но если ко мне на собеседование придёт человек и будет делать чат на потоках, да ещё и заявит, что это хорошее решение, то в итогах собеседования я напишу: «про сетевое программирование очень знает мало».


        1. Godless
          07.09.2015 23:55

          ну что ж вы так. придет время и автор сам придет к асинхронному сетевому общению. А так и многопоточный клиент вполне себе начало… Еще есть такой термин как 'преждевременная оптимизация'. Может epoll конечно и круто, но если для задачи хватает и многопоточного клиента, то почему бы и нет?
          хотя посылы, конечно же, верные 8)


        1. StrangerInRed
          08.09.2015 09:02

          Я вас просто не правильно понял. Я не написал, что использовал эту модель именно потому, что к такой модели на порядок проще прикрутить шифрование + ОС дает большую изолированность контекстов для отдельных клиентов, именно когда используются потоки. Так сказать задел на будущее. + Еще хотелось погонять потоки, потому что

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


          1. ToSHiC
            08.09.2015 12:36
            +1

            OpenSSL поддерживает шифрование для non-blocking сокетов, например, в nginx используется именно такой режим.
            Изолированность контекстов для отдельных клиентов — это исключительно вопрос того, как вы напишете код. Если вы контекст приклеите к файловому дескриптору, то тоже не будет никаких проблем.

            Возможно, я был излишне резок, но решение «потоки vs асинхронный код с poll» — это архитектура, самый фундамент сетевого демона, и от выбора сильно зависит его внутреннее устройство. Переписать потом с одного варианта на другой чаще всего означает переписать всё. И в данном случае, я считаю, оно принято неправильно, почему — написал выше, в предыдущем комментарии.


  1. Zyoma
    08.09.2015 13:52

    Проблема в том, что conntect/accept это блокирующий вызов, и нельзя подключится к нескольким портам одновременно (одним сокетом)

    Не до конца так. Штука в том, что если вы будете дергать accept на один и тот же сокет, но из разных тредов, ядро само все разрулит и запаркует ненужные треды «до лучших времен». С другой стороны, когда придет новое соединение оно разбудит сразу все треды, хотя по факту нужен только один. Так что, в любом случае, такой фокус применять не рекомендуется. По крайней мере, так это работает в Linux'е.