Network subsystem

Материал из Руководство по OpenKore
Перейти к: навигация, поиск

Это достаточно старая статья, может быть что-то уже и не так, но в общем и целом полезно прочитать.

Введение

Сетевая подсистема OpenKore состоит, грубо говоря, из следующих классов: Network, Network::MessageTokenizer, Network::PacketParser, а также из идущих вместе классов Network::Receive, Network::Send и Network::ClientReceive...

Давайте рассмотрим их, чем они занимаются и для чего нужны.

Класс Network

Данный класс отвечает за соединение с сервером, т.е.:

  • управляет TCP/IP сокетом соединения с сервером
  • соединяет с сервером и отключает от него
  • отправляет данные на сервер
  • принимает данные от сервера

и кое-что ещё, но не слишком многое. Схематично класс можно представить так:

Файл:network-subsystem.jpg - к сожалению, файл с изображением схемы пока что отсутствует.

При подключении к серверу он создает два объекта:

  • Приёмник сообщений (класс Network::Receive::ServerTypeX). Когда от сервера приходит сообщение, оно передаётся в приёмник и он разбирает его в удобоваримую переменную с несколькими полями данных.
  • Передатчик сообщений (класс Network::Send::ServerTypeX). Он составляет сообщение и готовит его к отправке на сервер.

Есть ещё несколько ипостасей класса Network, например: Network::DirectConnection, Network::XKore и Network::XKoreProxy.

Класс Network::MessageTokenizer

Это т.н. tokenizer. Он разбивает непрерывный поток байтов на отдельные сообщения. Сообщения также называют пакетами.

Класс Network::PacketParser

На этом классе лежит основная работа по разбору и составлению сообщений (или по другому - пакетов). То есть с одной стороны он может из сообщения - т.е. некоторого массива байтов, создать переменную-хэш, в котором будет несколько элементов, полей с данными, с которыми очень удобно будет работать в программе. С другой стороны он может выполнить обратную операцию - взять такой хэш и составить сообщение для отправки на сервер.

Разобранные сообщения передаются дальше стандартным функциям-обработчикам из классов, о которых мы поговорим ниже. Эти стандартные функции сами делают что-то с полученными данными, а также дают возможность плагинам и другим модулям тоже отреагировать. Это делается через события, называемые также hook. Hook - это дословно крючок, который цепляет подписанные на данное событие функции.

Кроме того есть дополнительные функции (читай - API) для изменения или удаления сообщений. Удобно, если надо как-то повлиять на то, что будет дальше с сообщением.

Классы Network::Receive, Network::Send и Network::ClientReceive

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

Итак, классы Network::Receive и Network::ClientReceive обрабатывают поступившие сообщения. Сообщениями от сервера занимаются функции из Network::Receive, а от игрового клиента - Network::ClientReceive. Из сообщений вынимается информация и раскладывается по полочкам, чтобы она потом была легкодоступна для других модулей программы.

Как уже было сказано выше, класс Network::Send - это передатчик сообщений, он скрывает от нас низкоуровневую работу с сообщениями. Он даёт нам простые функции, которые можно легко использовать в любом месте программы за пределами сетевой подсистемы.

Из классов Network::Receive и Network::Send выводятся более специализированные классы, т.н. ServerType, которые могут переопределить стандартные функции, чтобы подстроиться под используемый сервером протокол и его особенности.

Классы Network::Receive::ServerTypeX и Network::Send::ServerTypeX

Данные классы описывают т.н. тип сервера - serverType. Здесь можно описать специфические для сервера сообщения - надо указать идентификатор и структуру, чтобы было понятно, как разбирать и составлять сообщения.

Вот мы и закончили рассматривать классы, на которых держится сетевая подсистема. Обратите внимание, что во всей сетевой подсистеме, начиная от класса для работы с соединением и до описания типа сервера, вы не найдёте кода, отвечающего за автоматическое поведение бота. Так и должно быть.

Как оно всё работает

Начинается всё с создания двух объектов:

  • $net - для работы с сетью
  • $incomingMessages - т.н. tokenizer, для того, чтобы разбивать непрерывный поток байтов на отдельные пакеты. Обратите внимание, что для этого ему потребуются знание о том, какие пакеты бывают, т.е. какому заголовку какая длина соответствует. Эти данные хранятся в знаменитом файле recvpackets.
$net = new Network::DirectConnection;

$incomingMessages = new Network::MessageTokenizer(\%recvpackets);

Объект $net создаёт приёмник сообщений $packetParser, указывая при этом, о каком типе сервера идёт речь.

$packetParser = Network::Receive->create($wrapper, $serverType);

Объект $net постоянно подкидывает принятый от сервера поток байтов в буфер $incomingMessages:

$incomingMessages->add($net->serverRecv);

Затем приёмник $packetParser разбивает при помощи tokenizer поток байтов на сообщения и обрабатывает их - то есть извлекает информацию и вызывает события:

@packets = $packetParser->process($incomingMessages, $packetParser);

В свою очередь передатчик $messageSender (про который мы не сказали, откуда он взялся) создаёт исходящие сообщения:

@packets = $messageSender->process($outgoingClientMessages, $clientPacketHandler);

Исходящие сообщения передаются объекту $net, а он в свою очередь отправит их в XKore:

$net->clientSend($_) for @packets;

Давайте вернёмся к тому моменту, когда приёмник $packetParser обрабатывал сообщения и посмотрим, что при этом происходит. Сначала идёт попытка вызвать специальный обработчик сообщения, если он есть. Потом наступает событие перед обработкой сообщения, т.е. в этот момент вызываются все функции, которые подписаны на данное событие. Лишь в третью очередь до сообщения добирается стандартная функция-обработчик, если она есть конечно. И в конце наступает второе событие - после обработки сообщения.

my $custom_parser = $self->can("parse_$handler_name")
if ($custom_parser) {
	$self->$custom_parser(\%args);
}

Plugins::callHook("packet_pre/$handler_name", \%args);

my $handler = $messageHandler->can($handler_name);
if ($handler) {
	$messageHandler->$handler(\%args);
}

Plugins::callHook("packet/$handler_name", \%args);

Работа с серверами разных типов (приём)

Давным-давно, в 2003 году, всё было просто. Было только два сервера Ragnarok Online: iRO - международный и kRO - корейский. Сегодня же есть уже минимум пять официальных серверов - в Бразилии, на Филиппинах, в Европе, в России и т.д., а также сотни приватных серверов. У всех этих серверов протоколы слегка отличаются. Было бы очень глупо выпускать отдельную версию OpenKore под каждый из этих серверов, потому что протоколы хоть и отличаются, но совсем немного. Поэтому решили сделать так, чтобы одна версия OpenKore могла работать с любым сервером или, по крайней мере, с большинством из них.

Таким образом появилось понятие "тип сервера", serverType. Скорее всего вы уже видели параметр serverType в файле servers.txt. Указанное в параметре serverType значение определяет какого вида протокол использует данный сервер. Например, на двадцатое декабря 2006-го года были серверы следующих типов:

  • ServerType0 для iRO, pRO, AnimaRO и многих других серверов
  • ServerType8 для kRO
  • ServerType10 для vRO
  • и т.п., всего было тогда 18 типов...

Классы Network::Receive и Network::Receive::ServerType0 содержат код для работы по протоколу на серверов типа 0. Для каждого другого типа есть свой собственный класс, наследуемый от Network::Receive, который заточен под работу с протоколом именно этого типа серверов. Иерархия классов выглядит так: (изображение не сохранилось).

Информация о типе сервера берётся из файла servers.txt и используется при соединении с сервером, чтобы создать подходящий приёмник сообщений.

Подробности реализации приёма сообщений

Если вы посмотрите на метод 'new' в классе Network::Receive, тогда вы увидите нечто вроде:

$self{packet_list} = {
    '0069' => ['account_server_info', 'x2 a4 a4 a4 x30 C1 a*', [qw(sessionID accountID sessionID2 accountSex serverInfo)]],
    '006A' => ['login_error', 'C1', [qw(type)]],
    ...

Где $self{packet_list} - это хэш, где индексом выступает идентификатор сообщения, он же "message ID", а также "packet switch". С индексом связаны данные - массив из трёх элементов: название функции-обработчика, формула пакета, поля сообщения. Итак:

  • название функции-обработчика - эта та функция, которая разбирает пакет и раскладывает полученные данные по полям в сообщении. В приведённом выше примере для обработки пакета '0069' будет вызвана функция 'account_server_info'.
  • формула пакета - это специального вида текст, который определяет структуру пакета, из какого типа данных он состоит. Строки вида 'x2 a4 a4 a4 x30 C1 a*' передаются функции языка Perl unpack(). Если вы первый раз в жизни встречаете эту функцию, то почитайте соответствующую документацию, например тут и тут.
  • поля сообщения - это массив строк, в которых написаны названия переменных, в которые попадёт извлечённая из пакета информация. Порядок полей сообщения соответствует порядку данных, описанных в формуле пакета.

В унаследованных от Network::Receive классах можно изменять элементы в данном хэше. Например, менять порядок полей, подставлять другую функцию для обработки пакета, если это надо работы с сервером нового типа.

В функцию-обработчик передаются следующие два аргумента:

  • сам приёмник сообщений
  • данные из пакета, сообщение в виде ссылки на хэш, это называют "аргументы сообщения"

Обратимся к примеру выше. Функция account_server_info могла бы выглядеть примерно так:

sub account_server_info {
    my ($self, $args) = @_;
    # Тут можно было бы обраться к полю sessionID: $args->{sessionID}
    # А вот так к полю accountIDЮ $args->{accountID}
    # и т.д.
}

Кроме того в хэше всегда есть три служебных поля:

  • $args->{switch} - это идентификатор сообщения, например '0069'.
  • $args->{RAW_MSG_SIZE} - это длина неразобранного пакета в байтах.
  • $args->{RAW_MSG} - это сам пакет целиком, включая идентификатор.

Поле $args->{RAW_MSG} может пригодиться тогда, когда в packet_list невозможно описать формулу пакета строкой для функции unpack(). Тогда придётся в функции-обработчике разбирать пакет самостоятельно. Обратите внимание, что в поле RAW_MSG может быть больше данных, чем нужно - это могут быть кусочки от следующих пакетов. Как правило следует обращать внимание только на первые RAW_MSG_SIZE байтов.

Пример 1: добавить новую функцию-обработчик

Предположим, что у сервера появился новый пакет, который говорит "вы танцуете". Кроме того, пакет сообщает, какой именно это танец. Пусть пакет от сервера выглядит следующим образом:

  • Идентификатор - "1234".
  • Первое поле - это 16-битное число для координаты X
  • Второе поле - это 16-битное число для координаты Y
  • Третье поле - это 1 байт, который говорит, какой это танец

Тогда в хэш $self{packet_list} надо добавить следующий элемент:

'1234' => ['dancing', 'v v C', qw(x y type)]

Где формула 'v v C' говорит о том, что пакет состоит из 16-битного целого ('v'), за которым идёт ещё одно 16-битное целое ('v'), а в конце стоит беззнаковое целое длиной в один байт ('C'). Третий элемент массива 'qw(x y type)', говорит о том, что первое поле должно называться 'x', второе - 'y', а третье - 'type'. См. справочник: https://perldoc.perl.org/functions/pack

И, наконец, надо добавить в Network/Receive.pm новую функцию-обработчик сообщения:

sub dancing {
    my ($self, $args) = @_;
    message "I am dancing on position ($args->{x}, $args->{y})! My dance type is $args->{type}\n";
}

Вот и всё! Остальное происходит само по себе.

Пример 2: исправление другого сервертипа

Пусть пакет "вы танцуете" немного отличается на серверах с типом 12. Например координаты X и Y идут в обратном порядке, то есть сначала Y, а потом X. В таком случае можно в хэше packet_list поменять поля сообщения местами. Тогда функция 'new' в Network::Receive::ServerType12 будет выглядеть так:

sub new {
    my ($class) = @_;
    my $self = $class->SUPER::new;

    # код начинается тут
    $self->{packet_list}{1234}[2] = [qw(y x type)];
    # а тут он заканчивается

    return $self;
}

Если вы не сильны в Perl, вспомните, как выглядело описание этого пакета ($self->{packet_list}{1234}) в стандартном сервертипе из прошлого примера:

['dancing', 'v v C', qw(x y type)]

Вы хотели поменять массив с названиями полей, который стоит на второй (считая с нуля) позиции. Поэтому пишем:

$self->{packet_list}{1234}[2] = [qw(y x type)];

и теперь $self->{packet_list}{1234} выглядит как надо:

['dancing', 'v v C', qw(y x type)]

Передатчики для разных сервертипов

Передатчик устроен таким же образом, что и приёмник. Есть один класс, в котором собраны все функции для отправки сообщений, а в наследуемых от него подклассах находятся специфические для каждого сервертипа дополнения.

Сервертип 0 описывается классом Network::Send::ServerType0, подклассы для остальных сервертипов описываются соответственно в классах Network::Send::ServerTypeX.

Примечательно, что основные отличия между протоколами для разных сервертипов заключаются именно в передатчиках! То есть, получаемые от сервера сообщения практически не отличаются, а вот сообщения от клиента на сервер - очень даже.

Использование передатчика сообщений

Передатчик создаётся одновременно с приёмником. Объект запоминается в глобальной переменной $messageSender. Для отправки сообщения надо просто вызвать нужную функцию:

$messageSender->sendFoo();

Каждая функция передатчика начинается с префикса 'send'. Загляните в Network::Send::ServerType0 и посмотрите на эти функции. Допустим, вам понравилась функция для отправки сообщений в публичный чат:

$messageSender->sendChat("hello there");

Про совместимость

Приведённый выше способ отправки сообщений на сервер при помощи собранных в одном объекте функций появился 20.12.2006. В предыдущих версиях OpenKore нужно было поступать так:

в OpenKore 1.6 и 1.9.0-1.9.2

sendFoo(\$remote_socket, args);

в OpenKore 1.9.0-1.9.2

sendFoo($net, args);
$net->sendFoo(args);

Оно больше не работает таким образом, вместо этого надо писать:

$messageSender->sendFoo(args);

Если же ваш плагин должен быть совместим и с новой и со старой версией OpenKore:

if (defined $Globals::messageSender) {
    $Globals::messageSender->sendFoo(args);
} else {
    sendFoo(\$remote_socket, args);
}

События (т.н. хуки)

В приёмнике и передатчике определены некоторые события, которые позволяют плагинам тоже участвовать в обработке сообщений.

"packet_pre/$HANDLER_NAME"

У всех событий есть текстовое название, например такое - "packet_pre/$HANDLER_NAME", где "packet_pre/" - это неизменная часть, а вторая часть названия - это название функции-обработчика пакета, т.е. та функция, что упомянута в хэше packet_list. Вот например название некоторого события: "packet_pre/account_server_info".

Данное событие (в терминологии OpenKore - хук) вызывается прямо перед тем, как будет вызвана стандартная функция-обработчик для данного пакета. Передаваемые в событие аргументы - это тупо массив из полей сообщения.

"packet/$HANDLER_NAME"

События вида "packet/$HANDLER_NAME" наступаю после того, как принятый пакет был разобран соответствующей функцией-обработчиком, если она вообще была определена. Аргументы события - это тупо массив данных из сообщения.

Приложение A: протокол Ragnarok Online

Протокол Ragnarok Online работает поверх TCP как транспортного протокола. Каждое отправленное сервером RO сообщение имеет следующий формат:

заголовок, он же packet switch (2 байта) + данные (различной длины)

Сообщение состоит как минимум из одного поля - поля заголовка, также называемое как идентификатор сообщения, packet switch или просто switch. По заголовку сообщения клиент может определить, что это за пакет и какие данные он несёт. То есть каждый пакет требует к себе особого подхода.

Можно разделить пакеты на два вида:

  • Пакеты с фиксированным размером
У таких пакетов всегда заранее известная длина. Примером может послужить сообщение "кто-то отправил смайлик" или "появился моб".
  • Пакеты переменной длины
У таких пакетов длина не известна заранее и зависит по сути от передаваемых данных. Так как длина не известна заранее, то у таких пакетов всегда есть специальное поле, где и можно посмотреть длину данного пакета. Длиной считается размер всего сообщения, вместе с заголовком. Типичный пример пакета с переменной длиной может быть "вы отправили сообщение в публичный чат".

Наконец в пакете есть данные, которые называют аргументами сообщения. Конкретный набор аргументов зависит от самого сообщения. Возьмём, например, пакет "кто-то послал смайлик", тогда в пакете будет два поля данных:

  • Идентификатор персонажа, отправившего смайлик.
  • Идентификатор смайлика.

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

Приложение B: recvpackets.txt и длина пакетов

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

Но как же нам определить, что в буфере лежит целый пакет, а не его часть? Для этого надо знать точную длину каждого пакета. Именно для этого и нужен файл recvpackets.txt, в нём для каждого пакета прописана его длина. Пусть в файле recvpackets.txt есть следующая строка:


00C0 3

Она означает, что пакет с заголовком "00C0" имеет фиксированную длину пакета в 3 байта. Однако иногда встречаются строки вида:

00D4 0
00D4 -1

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

Приложение C: шифрование отправляемых пакетов

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

Раздутые пакеты

Раздутые (или иначе padded) пакеты - это очень древний способ, который больше не используется. Но всё равно интересно узнать, как это было.

Некоторые серверы RO, такие как euRO (Europe), iRO (International) и rRO (Russia), применяли так называемые "раздутые пакеты". Клиент RO вставлял в определённые пакеты какие-то непонятные байты, раздувая размер пакета (отсюда и название). Выяснилось, что в этих непонятных данных лежит итог работы сложного хэш-алгоритма. Кроме того, размер данных и сам алгоритм менялись каждый раз после получения особого sync-пакета от сервера RO. То есть, один пакет мог быть разной длины, в зависимости от того, когда он был отправлен на сервер.

Для того, чтобы OpenKore тоже смогла отправлять раздутые пакеты, была написана специальная система-эмулятор.

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

За подробностями обратитесь к файлу "src/auto/XSTools/PaddedPackets/README.TXT".

Шифрование заголовков

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

Как побороли этот метод можно посмотреть в коде OpenKore в файле "src/Network/Send.pm", ищите функцию encryptMessageID().

Оригинальная статья

http://web.archive.org/web/20090305035837/http://www.openkore.com/wiki/index.php/Network_subsystem