Данная статья будет интересна людям, интересующимся сетями, логическим устройством серверов и нативным программированием. Здесь не будет долгих листингов исходных кодов, а только общие наброски и подходы.
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 рассылки к подключенным сокетам, и хранение/уничтожение пользовательских данных, которыми обладает сокет.
Структура пользовательских данных сокета довольно проста – это ник и чатрум, в котором находится данный пользователь. Отсюда вытекает простота использования такого механизма, чтобы создать комнату, нужно просто перейти в нее.
Микро-Админка
Администрирование серверов, которые поддерживают только открытый коннект (без шифрования) вообще должно заслуживать отдельной статьи о том, как можно построить стойкую систему одноразовых паролей и хеш-комманд, но опустим все это. Я просто использовал возможность ввода текстового ключа, и команды за ним, для выполнения (Если длинна ключа будет достаточно велика несколько мегабайт, то можно обезопасить себя от брутфорса) но тяжело обезопасить себя от перехвата этого самого ключа=). Но, теоритически, схема выглядит так:
- Сервер генерирует challenge число или набор байт.
- Клиент хеширует одноразовым ключем challenge + комманду и отравляет хеш и комманду в открытом виде.
- Сервер повторно хеширует команду с challenge и сравнивает результат хеша с полученным.
- Если хеш валидный то комманда выполняется.
Примеры команд
Окно приветствия с маленьким 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)
Zyoma
08.09.2015 13:52Проблема в том, что conntect/accept это блокирующий вызов, и нельзя подключится к нескольким портам одновременно (одним сокетом)
Не до конца так. Штука в том, что если вы будете дергать accept на один и тот же сокет, но из разных тредов, ядро само все разрулит и запаркует ненужные треды «до лучших времен». С другой стороны, когда придет новое соединение оно разбудит сразу все треды, хотя по факту нужен только один. Так что, в любом случае, такой фокус применять не рекомендуется. По крайней мере, так это работает в Linux'е.
ToSHiC
Сетевой чат без ивентлупа? Серьёзно?
StrangerInRed
Всмысле? Если под ивентлупом вы подразумеваете обработку введенных данных, то она там есть. Но мультикастовых сообщений, о том что кто-то присоединился нету.
ToSHiC
Под ивентлупом я подразумеваю модель сетевого демона, который поллит набор сокетов каким либо образом (будь то select, epoll, kqueue или GetQueuedCompletionStatus) и обрабаывает полученные данные в одном потоке или тредпуле.
Теперь подробнее. Сетевой чат — это типичный пример сетевого демона, который практически не требует cpu в своей работе. Если делать его на потоках (я мельком глянул на ваш код, и, на сколько я понял, вы использовали именно эту модель), то совершенно бездарно тратится куча памяти и подцессорного времени на контекст-свитчи между потоками. Скажем, 1000 клиентов на дефолтовых настройках в linux отожрут 8 гигов ОЗУ на стеки потоков.
Если же сделать аналогичный демон, но на мультиплексоре типа epoll, то процессорное время будет тратиться на действительно полезную работу по перекладыванию сообщений между чат-каналами и клиентами, ну и какое-то время уйдёт на синхронизацию потоков, если их несколько. Подобный демон на аналогичной железке потянет как минимум на порядок больше клиентов, если не на два. Ещё и время ответа уменьшится, потому что будет меньше ненужной конкуренции между потоками.
Вариант с потоком на клиента имеет право на жизнь, если расчёт чего либо на cpu занимает существенное время. Скажем, если вы сделали демона рекомендательной системы, который большую нейронку/SVM гоняет на запрос. Или если ваш демон картинку ресайзит. Но если ко мне на собеседование придёт человек и будет делать чат на потоках, да ещё и заявит, что это хорошее решение, то в итогах собеседования я напишу: «про сетевое программирование очень знает мало».
Godless
ну что ж вы так. придет время и автор сам придет к асинхронному сетевому общению. А так и многопоточный клиент вполне себе начало… Еще есть такой термин как 'преждевременная оптимизация'. Может epoll конечно и круто, но если для задачи хватает и многопоточного клиента, то почему бы и нет?
хотя посылы, конечно же, верные 8)
StrangerInRed
Я вас просто не правильно понял. Я не написал, что использовал эту модель именно потому, что к такой модели на порядок проще прикрутить шифрование + ОС дает большую изолированность контекстов для отдельных клиентов, именно когда используются потоки. Так сказать задел на будущее. + Еще хотелось погонять потоки, потому что
ToSHiC
OpenSSL поддерживает шифрование для non-blocking сокетов, например, в nginx используется именно такой режим.
Изолированность контекстов для отдельных клиентов — это исключительно вопрос того, как вы напишете код. Если вы контекст приклеите к файловому дескриптору, то тоже не будет никаких проблем.
Возможно, я был излишне резок, но решение «потоки vs асинхронный код с poll» — это архитектура, самый фундамент сетевого демона, и от выбора сильно зависит его внутреннее устройство. Переписать потом с одного варианта на другой чаще всего означает переписать всё. И в данном случае, я считаю, оно принято неправильно, почему — написал выше, в предыдущем комментарии.