(Russian) Современное серверосторение часть #1. На чем работают топовые сервера

Недавно мне в руки попал код эмулятора Season 8 Ep 2. Эмулятор изначально был написан eRRoR’ом и активно использовался на таких серверах как OnlyMu, Esthus и на других топовых серверах. Именно его сейчас активно использует Z-Team продавая свои файлы. Откуда появился этот исходной код – совсем другая история которая выходит за рамки текущей. В этом рассказе мы поговорим о том что же стояло на самых топовых серверах и какая собака была зарыта внутри.


Итак. На руках не просто файлы а файлы с исходным кодом от самых топовых серверов. Тем более что разработчиком был eRRoR.  Код более чем актуальный (начало 2014 года) а в довесок идет еще последний код Z-Team который и базировался на этих исходниках. Устроившись поудобней и взяв немного попкорна я начал изучать код. По мере написания статьи я исправлял ошибки и тестировал новшества/изменения. Так что по мере описания проблем я еще буду описывать методы решения этих проблем.

Все еще Visual Studio 6.0

Вот вроде за окном уже 2014 год а скоро и 2015 но даже самые суровые разработчики все еще используют Visual Studio 6.0. Честно говоря для меня это был некий шок. Почему? Зачем? Неужели современные компиляторы настолько плохи? Может быть причина в том что так проще дизассемблировать? Единственное что пришло в голову это неопытность разработчиков или нежелание что-либо сделать вообще. Код написан в чистом стиле “Си” с некоторыми намеками на “С++”. Я не могу считать что этот код написан именно на “С++” т.к.  использование виртуальных деструкторов в нескольких классах не говорит о том что язык используется по назначению. Грубо говоря передомной был все тот же код который много лет назад выложил Deathway. В нем не было даже намека на то что что-то изменилось. Да, добавились новые возможности но не более. Грубо говоря “код” просто обновился до поддержки новых сезонов и не более.

Я сразу начал миграцию с Visual Studio 6.0 на Visual Studio 2013 но с использование toolchain от 2010. Собственно сразу вылезло около 100 ошибок вида

Собственно ошибки то очень простые. Раньше модыфикатор auto обозначал что тип переменной будет автоматически сгенерирован опираясь на инициализируемое значение. Например:

Переменная “с” становилась символьной переменной. Сейчас же стандарт C++ 11 имеет несколько другое определение и записи вида

попросту не валидны. Все такие ошибки убираются простым удалением модыфикатора auto.

Так же во времена VC 6.0 переменные обявленные в определении цикла прописывалась в текущий блок а не в тело цикла, так что следующий код был вполне работоспособен

и результатом можно было получить “1” а не как ожидается – “ошибка компиляции”.

Выдержка из документации

Выдержка из документации

Убрав пару сотен таких записей и убрав несколько других ошибок – проект был успешно портирован на Visual Studio 2010 toolchain.

 

Парад библиотек и портирование на Visual Studio 2013

Эмуляторы под MuOnline всегда славились тем что у них было много библиотек от которых зависило построение сервера. Эти библиотеки использовались всегда в единичном варианте. Кроме GameServer их никто больше неиспользовал, так что никакой смысловой нагрузки в них никогда небыло. Так что роль таких библиотек была неизвестной. Статические библиотеки всегда использовались для создания SDK либо передачи их третим лицам. Ни первого ни второго здесь не наблюдалось. К тому же все эти библиотеки состояли из нескольких экспортируемых функций и одного или двух классов.Libraries list

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

Чтобы портировать “правильно” на Visual Studio 2013 надо всего лишь пересобрать все подключаемые библиотеки с тем же toolchain. Это связано с /SAFESEH. Грубо говоря надо перекомпилировать все подключаемые библиотеки на Visual Studio 2013.

К сожалению простота оказалась чисто на словах т.к. в проект добавились такие библиотеки как pugixml и liblua. Своим присутствием они добавили еще несколько зависимостей в проект. Такая отборная солянка привнесла в проект не просто дополнительные зависимости, проект попросту перестал собиратся без исправления кода. Прежде всего мешал подключенный ATL. Каким образом тут оказался ATL? Интересный вопрос о котором мы еще поговорим.

 Знакомство с кодом

Открыв код прежде всего я надеялся увидеть кардинальные изменения, например: улучшения структуры кода, исправление старых ошибок, добавление юнит-тестов и т.д. Увы, но ничего такого я не увидел. Как уже было написано выше – это тот самый код далекого 2007-го года. В нем нет ничего нового кроме поддержки новшеств MuOnline. Даже не все старые ошибки были исправлены. Возможно для некоторых людей будет открытием но уже ровно 7 лет в коде есть комментарии следующего вида:

Как минимум такой комментарий будет возле подозрительного места, но как правило он явно говорит что здесь ошибка. Дальше мне бросилась в глаза новая структура OBJECTSTRUCT. С нормальной точки зрения все правильно. Она самая толстая, больше всего потребляет оперативной памяти она в таком виде попросту не нужна. Вообще это все надо было бы разьеденить, создать понятие разных обьектов, создать обьекты игрока, NPC, монстра, создать под обьекты контейнеры и работать уже с контейнерами (намример тем же list) получив при этом примерно +300% к скорсоти и на пару сотнет мегабайт меньше потребление памяти.

Увы, но вместо этого всего вся структура была просто переведена с статической памяти на динамическую. С какой целью это делалось мне так и не ясно. К тому же скорость динамически выделеной памяти примерно на 20% хуже чем статической.

Это интересно

Это интересно

1 обьект OBJECTSTRUCT занимает в памяти 10 килобайт (10400 байт)

все обьекты OBJECTSTRUCT занимают 124.5 мегабайта (124496000 байт)

Вот и собственно сам код представляющий эту реализацию:

Чем плох этот код и такая реализация? Сделать Memory Management в такой реализации будет очень сложно да и не рентабельно. Собственно все минусы по-пунктах:

  • Уменьшение скорости работы. Поскольку это главная структура сервера с которой взаимодействует всё то можно считать что происходит тотальное замедление сервера
  • Некоректный менеджмент памяти и не понимаение устройства памяти. Обьекты которые живут всю жизнь сервера должны размещатся в статической памяти, а не динамической
  • Память создается не под одного игрока/монстра/NPC, а под всех сразу. Вследствии этого идет неоправданное потребление ресурсов ПК.

Для тех кому интересно как работает память

Для тех кому интересно как работает память

 

ATL

 Untitled1

Итак.. каким же образом здесь причастен ATL? Что делает такой динозавр в данном коде? Как причастен MFC к GameServer’у ? Оказывается все очень просто. Весь интерфейс сервера создан с использованием GDI+ и WinApi, а MFC, вместо своего прямого назначения, используется только для проверки времени (CTime & CTimeSpan). Зачем было подключать огромную библиотеку только ради 2-х классов ? На этот вопрос мне сложнее всего ответить. Тем более что некоторые Эвенты уже использовали ctime. Возможно автор кода хотел сделать его более приближенным к ООП и С++, но вместо этого добавил очень много зависимостей. Более того. На протяжении более 10 лет сервер собирался с библиотекой MFC.

 

Утечка памяти

Собственно как уже говорилось выше – OBJECTSTRUCT создавалась в динамической памяти. Если система не могла выделить в динамической памяти участок памяти на 100+ мегабайт то код, который написан без единой проверки на такую возможность исхода событий, сразу падал с ошибкой выделения памяти. Как только OBJECTSTRUCT был обратно переведен на статическую память – все заработало. Но при закрытии появился еще очень занятный output log:

Представьте себе насколько много проблем если даже Visual Studio увидела memory leak. Почему я говорю “даже Visual Studio”? Студия очень плохо работает с утечками памяти и диагностирует их довольно редко. Данная возможность добавлена скорее для галочки нежели как полноценная “фитча”. Именно по этому есть множество плагинов которые делают это намного лучше, например тот же Intel Parallel Studio. Но довести код до такого состояния что уже и Visual Studio видит утечки памяти – талант.

Ниже представлен вариант с утечкой памяти что выловил Intel Parallel Studio. Как  можно увидеть, на рисунке ниже, идет создание массива классов CItem. Другими словами создается X обектов CItem, где Х = INVENTORY_SIZE. А вот удаляется (верхняя часть рисунка) всего один.

3

В данном коде не правильно использован оператор delete. Верхнюю строку следовало бы заменить на такую:

Что произойдет если не исправлять код? Удалится только первый созданный элемент. Все остальные элементы будут и дальше хранится в памяти. Но по секрету скажу что вся созданная динамическая память при закрытии приложения будет освобождена. Но с одним но. Открытые дескрипторы системы не освобождаются автоматически. Зная специфику данного кода я не могу сказать что ошибка критическая. Она не приятная но не более чем, она не должна влиять ни на что, но факт её существования уже говорит о том что не все так красиво как хотелось бы.

На самом деле подобных ошибок достаточно много. Если сравнивать со старыми серверами то сразу можно понять что их никто попросту не исправлял.

Ошибки в коде

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

В данном коде два раза сравниваются две переменные с -1.

 

 

К сожалению данный участок кода совсем не валидный т.к. переменная AttackSpeedDiv является переменной типа WORD (0..255) и она не может быть меньше нуля.

 

Аналогично и в данном примере. lpUser->Class не может быть меньше нуля. По этому данный блок не выполнится никогда.

Этот пример более интересен поскольку здесь нету явной ошибки но есть потенциальная угроза. Обратите внимание на переменную Text. Данная переменная имеет размер 256 байт. Теперь обратите на обнуление массива. Обнуляются только первые 255 байт. Последний байт не будет обнулен и в нем по прежнему будет мусор. Потенциально это может привести к выходу за рамки массива (если не будет установлен нулл терминатор). Так же здесь присутствует еще одна очень серйозная проблема. Функция  ZeroMemory вторым параметром принимает количество байт для “зануления”. Если увеличить размер массива то мы будем затирать только часть буфера что в будущем может повлечь за собой неприятные моменты. Если же уменьшить размер буфера тогда мы затрем какую-то другую память отведенную другому обьекту. Результат абсолютно непридвиден. Правильно было бы написать ZeroMemory(Text, sizeof(Text));

 

 

 

Untitled2

 

 

 

 

Новвоведения

К сожалению сейчас мало кто смотрит на качество кода. Люди очень часто смотрят на новые модные “фитчи” которые вообще мало кому нужны. Но оно должно смотрется круто потому что если просто подать стандартный джентельменский набор который просто будет работать на ура – никому не интересно. Надо хотябы переименовать папку, переместить файлы или сделать еще какуе-то хреновину чтобы обратить на себя внимание. Так и сделали красавцы с Z-Team. Чем же они смогли отличится? Пожалуй самое дурное что я видел за несколько лет. Вместо того чтобы исправлять архитектуру/фиксить утечки памяти/оптимизировать ядро и вообще работать над стабильностью… они перевели все файлы в … XML? Вы серйозно? XML? Зачем? Не буду лукавить – сам давно хотел. Но не в XML же! Можно начать с того что XML это самый “медленный” формат. Он используется чисто потому что его удобно использовать в Интернет. Его поддерживает любая технология/язык программирования. Из этих побуждений им просто удобно экспортировать что-то через API. Например так сделала Valve с Dota2. Но зачем XML для считывания ЛОКАЛЬНЫХ и только локальных данных? Какую пользу он несет? Увы но считывание простого текстового файла и то будет быстрее. Зачем переводить MonsterSetBase на XML?

4

Уровень валидации данных в XML? Да никакой. О какой валидации идет речь если все параметры все равно записаны как строка? Поставлю я в какой то параметр случайно букву и все! Вылетит же к чертям. Но конечно… это круто!

Представьте себе. У Вас уже есть коннект к MSSQL серверу без которого всеравно “никак”. Он уже есть и он подключен. Вопрос: зачем переписывать все на XML? Не проще ли перекинуть все файлы в таблицы MSSQL? Сделать нормальную валидацию данных? По типу, по максимальному значению, по дефолтному числу, по дублировнию и т.д. ? Тем более что в таком случае был бы именно прирост скорости + отличная валидация данных. А на худой конец можно было бы вывернутся и сделать подгрузку монстров в real-time и менять MonsterSetBase можно было бы чисто на ходу без каких-либо перезагрузок. Но… наверное мне этого не понять, ведь ребята из Z-team еще те перцы.

 

 

 

 

 

 

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *

*