Советы начинающим разработчикам сетевых приложений

image


В данной статье будет дана серия рекомендаций начинающим сетевым разработчикам. Возможно она поможет вам не наступать на некоторые грабли, а так же предложит менее очевидные, но более правильные способы решения ваших задач. Отчасти статья может является холиварной и противоречить вашему опыту. В этом случае — добро пожаловать в комментарии.
Совет 1
По возможности — используйте TCP. В противном случае вам придется самому заботится о целостности ваших данных, а это не так просто, как кажется на первый взляд. Основное узкое место TCP, в которое вы упрётесь начав его использовать в realtime задачах — задержка перед отправкой данных. Но это поправимо — нужно лишь установить сокету флаг TCP_NODELAY:

int flag = 1;
int result = setsockopt(sock, IPPROTO_TCP,  TCP_NODELAY, (char *) &flag, sizeof(int));
if (result < 0)
    ... handle the error ...


Для некоторых задач использовать TCP невозможно. К примеру, при реализации техники обхода NAT. В этом случае можно воспользовать готовой библиотекой, обеспечивающей гарантированную доставку поверх UDP, к примеру UDT library. Для других задач (например, передача мультимедиа) иногда так же есть смысл использовать UDP — многие кодеки поддерживают потерю пакетов. Но тут стоит быть аккуратным — возможно лучшим решением будет всё же не терять данные, а вместо этого обеспечить ускоренное воспроизведение фрагмента, полученного с задержкой.

Совет 2
Правильно пакуйте и вычитывайте данные. В TCP вы имеете дело с потоком, а не пакетами, поэтому, к примеру при отправке двух сообщений вы можете получить одно, с думя склеенными (и наоборот, при отправке одного большого сообщения вы можете вначале получить только первую часть вашего сообщения, а через пару миллисекунд вторую). Один из стандартных паттернов приёма-получения данных выглядит так:

  • при отправке — записать длину пакета, затем сам пакет
  • при получении — прочитать все данные и положить их в буфер (этот буфер должен жить всё то время, пока установлено соединение)
  • до тех пор, пока в буфере достаточно данных:
  • десериализовать длину сообщения. Если в буфере данные больше, чем длина этого сообщения — распарсить сообщение и удалить его (и только его) из буфера
  • если данных меньше чем длина сообщения — прервать цикл и ждать данных дальше

Совет 3
Используйте механизмы ассинхронной работы с сокетами вместо многопоточности. Несмотря на то, что в современных ОС потоки практически ничего не стоят у ассинхронного подхода есть пара преимуществ:

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


Для этих целей в linux существует epoll, во freebsd и macosx — kqueue, а в windows — io completion port. Вместо того, чтобы вручную работать со всем этим зоопарком есть смысл посмотреть в сторону boost asio, libevent и прочих подобных библиотек.

Совет 4
Прежде чем писать свой велосипед сериализации — посмотрите на существующие решения. Для сериализации в бинарном виде есть google protobuff, MessagePack / CBOR и прочие. В текстовом — json && xml и их вариации.

Совет 5
Правильно выбирайте модель синхронизации. Основные модели синхронизации следующие:

  • Передача полного состояния. С точки зрения реализации тут всё просто. Берём объект, сериализуем и отправляем всем кому он нужен. Пока объект имеет небольшой размер или редко запрашивается — всё хорошо. Как только размер становится относительно большой или же требуется частая синхронизация — начинает рости потребление трафика вплоть до невозможности обеспечить требуемую скорость обновления.
  • Передача изменений (дифов). В этом подходе, вместо отправки полного состояния считается разница между старым состоянием и новым и отправляется лишь разница между ними. Разница может получатся разными способами. Например, её может явно запрашивать принимающая сторона (eg. пришли мне все данные с марта по июнь, остальное у меня уже есть). Или же отправляющая сторона сама следит за тем, какие данные уже есть у принимающей, в самом простом случае — считая что принимающая успешно получила все старые данные (eg. вот тебе данные за март; во тебе за апрель; вот тебе за май …).
  • Воспроизведение всеми одинакового набора событий. В этом подходе само состояние вообще не передаётся. Вместо этого передаются события, приводяющие к изменению состоянию.


В случе если у вас сериализованные события имеют размер сильно меньший чем состояния, рекомендую использовать именно третий способ. Однако он требует аккуратной реализаци. Иногда этот способ используют в комбинации с первым (например, в бд redis при master-slave синхронизации баз — подключившийся слейв вначале загружает полную копию базы, а затем начинает проигрывать все команды синхронно с мастером). Так же этот способ позволяет делать простое журналирование происходящего. Для этого достаточно просто сохранять все команды. Так сделано, к примеру, в игре StarCraft. Сохранённые команды используютеся в дальнейшем при просмотре реплеев.

Совет 6
По возможности выбирайте централизованную архитектуру. Децентрализация выглядит заманчиво, однако существенно усложняет (и иногда замедляет) реализацию. При нескольких равноправных узлах вам необходимо думать о том, кто же в итоге будет принимать решения. Для решения этой задачи разработаны различные схемы принятия решений. Самые популярные — paxos (поверьте, вы не хотите это программировать) и raft. Но, даже в случае отсутствия необходимости консенсуса вам придется использовать разные хитрые методы решения тривиальных для централизованных систем задач. Например, dht для поиска, proof of work для защиты от атак, etc.

© Habrahabr.ru