Проектирование сетевого ядра изначально преподнесло несколько интересных сюрпризов. Простейший клиент, который передавал и принимал данные через сервер, был написан достаточно быстро. Но в связи с тем, что он дописывался в местах, где не было интернета, а графическое ядро претерпевало глобальные изменения, которые не позволяли объединить ядра, весь код отлаживался только на localhost’е. Как только я начал объединять ядра, точнее встраивать своё ядро в зачатки движка игры начались проблемы. В этом посте я опишу две, на мой взгляд, интересные проблемы сервера, с которыми я столкнулся при тестировании сетевого ядра игры на разных компьютерах, и их решения.
Первой проблемой, с которой я столкнулся, это было нежелание ядра работать на других компьютерах, точнее, как позже выяснилось, на компьютерах с операционной системой Windows Vista. Система отказывалась принимать некоторые пакеты. Оказалось, что переписав сетевой стек, компания Microsoft исключила поддержку флагов сообщений, поэтому сообщения, которые были помечены флагом, не принимались (во всяком случае, тем методом, который я использовал). Однако, к чести Microsoft, на MSDN уже давно висела пометка, что использовать флаги не желательно. После того как флаги были убраны из сообщений клиент заработал и разницы в сетевых ядрах XP и Vista я больше не замечал.
Вторая проблема заключалась в алгоритме обработки различий скоростей подключения клиентов. Изначально сервер был простым ретранслятором информации, то есть клиент присылал ему сообщение о своих координатах, а сервер отправлял это сообщение каждому из подключившихся клиентов. Получалось так, что клиент А с хорошей скоростью соединения передавал данные в штатном режиме, а клиенту Б своего канала не хватало и принимать данные со скоростью клиента А он не мог (не говоря о том, что клиентов может быть несколько). Данные приходили с возрастающей задержкой и вскоре движения копии игрока отставали от оригинала больше чем на минуту. После этого, стало ясно, что серверу необходимо масштабировать информационные потоки в зависимости от пропускной способности канала клиента. Были опробованы различные варианты, но все они имели недостатки. Однако, в сессию мне пришло довольно интересное решение, которое я на днях воплотил в коде сервера. Оно основано на том, что у класса на сервере, который работает с подключённым клиентом, создаётся собственное хранилище, где он хранит все данные о состоянии других клиентов-игроков. Я делю все данные, которые появляются в процессе игры, на два потока: поток синхронизации и поток команд. Состояние клиента описывается одним синхронизационным пакетом, поэтому если такой пакет игрока А ещё не был отправлен игроку Б, но уже пришёл новый пакет от игрока А, то игроку Б необходимо отправлять только новый пакет. Также необходимо поддерживать равную плотность синхронизационных пакетов от игроков-противников, независимо от скорости подключения этих игроков (за некоторый промежуток времени от всех игроков должно поступить равное количество пакетов, если они предоставляли их с требуемой скоростью). Совершенно другая ситуация складывается у командных пакетов. Они должны быть доставлены в максимально короткие сроки в полном объёме. Итак, у каждого игрока-клиента на сервере есть хранилище, в котором описаны состояния всех игроков. Также в хранилище есть очередь команд. Игроку-клиенту с сервера по очереди посылаются состояния других игроков, находящиеся в хранилище класса-представителя игрока, если данные изменились с момента прошлой посылки. Между посылками состояний игроков проверяется командный буфер и если в нём есть данные, то они отправляются вне очереди. Когда же информация о состоянии игрока приходит на сервер, то она копируется в соответствующие игроку ячейки других классов-представителей, затирая информацию о прошлом состоянии игрока. Плюсы этой системы состоят в том, что на сервере хранятся только самые последние данные и система весьма быстра, по сравнению с известными мне аналогами. К минусам такой системы можно отнести больший объём затрачиваемой памяти, по сравнению с аналогами и необходимость правильной синхронизации между потоками клиентов.
Первой проблемой, с которой я столкнулся, это было нежелание ядра работать на других компьютерах, точнее, как позже выяснилось, на компьютерах с операционной системой Windows Vista. Система отказывалась принимать некоторые пакеты. Оказалось, что переписав сетевой стек, компания Microsoft исключила поддержку флагов сообщений, поэтому сообщения, которые были помечены флагом, не принимались (во всяком случае, тем методом, который я использовал). Однако, к чести Microsoft, на MSDN уже давно висела пометка, что использовать флаги не желательно. После того как флаги были убраны из сообщений клиент заработал и разницы в сетевых ядрах XP и Vista я больше не замечал.
Вторая проблема заключалась в алгоритме обработки различий скоростей подключения клиентов. Изначально сервер был простым ретранслятором информации, то есть клиент присылал ему сообщение о своих координатах, а сервер отправлял это сообщение каждому из подключившихся клиентов. Получалось так, что клиент А с хорошей скоростью соединения передавал данные в штатном режиме, а клиенту Б своего канала не хватало и принимать данные со скоростью клиента А он не мог (не говоря о том, что клиентов может быть несколько). Данные приходили с возрастающей задержкой и вскоре движения копии игрока отставали от оригинала больше чем на минуту. После этого, стало ясно, что серверу необходимо масштабировать информационные потоки в зависимости от пропускной способности канала клиента. Были опробованы различные варианты, но все они имели недостатки. Однако, в сессию мне пришло довольно интересное решение, которое я на днях воплотил в коде сервера. Оно основано на том, что у класса на сервере, который работает с подключённым клиентом, создаётся собственное хранилище, где он хранит все данные о состоянии других клиентов-игроков. Я делю все данные, которые появляются в процессе игры, на два потока: поток синхронизации и поток команд. Состояние клиента описывается одним синхронизационным пакетом, поэтому если такой пакет игрока А ещё не был отправлен игроку Б, но уже пришёл новый пакет от игрока А, то игроку Б необходимо отправлять только новый пакет. Также необходимо поддерживать равную плотность синхронизационных пакетов от игроков-противников, независимо от скорости подключения этих игроков (за некоторый промежуток времени от всех игроков должно поступить равное количество пакетов, если они предоставляли их с требуемой скоростью). Совершенно другая ситуация складывается у командных пакетов. Они должны быть доставлены в максимально короткие сроки в полном объёме. Итак, у каждого игрока-клиента на сервере есть хранилище, в котором описаны состояния всех игроков. Также в хранилище есть очередь команд. Игроку-клиенту с сервера по очереди посылаются состояния других игроков, находящиеся в хранилище класса-представителя игрока, если данные изменились с момента прошлой посылки. Между посылками состояний игроков проверяется командный буфер и если в нём есть данные, то они отправляются вне очереди. Когда же информация о состоянии игрока приходит на сервер, то она копируется в соответствующие игроку ячейки других классов-представителей, затирая информацию о прошлом состоянии игрока. Плюсы этой системы состоят в том, что на сервере хранятся только самые последние данные и система весьма быстра, по сравнению с известными мне аналогами. К минусам такой системы можно отнести больший объём затрачиваемой памяти, по сравнению с аналогами и необходимость правильной синхронизации между потоками клиентов.