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.

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


  1. MichaelBorisov
    28.08.2015 20:22
    +4

    В принципе согласен, опытным путем я для себя выработал похожие принципы. Конечно, где-то они неприменимы, но как начальное направление разработки — очень даже пойдет.


  1. MaxFactor
    28.08.2015 22:03
    +1

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


  1. mbait
    28.08.2015 23:08
    -1

    Начинающим разработчикам совет может быть только один — не читать вот такие вот статьи, а читать фундаментальные труды http://www.amazon.com/Unix-Network-Programming-Volume-Networking/dp/0131411551.

    Основное узкое место TCP, в которое вы упрётесь начав его использовать в realtime задачах — задержка перед отправкой данных. Но это поправимо...

    Автор, ну, серьезно — что это за хрень я читаю?