Network subsystem

Материал из Руководство по OpenKore
Версия от 11:36, 20 августа 2023; Manticora (обсуждение | вклад) (События (т.н. хуки))
Перейти к: навигация, поиск

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

Введение

Сетевая подсистема 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" наступаю после того, как принятый пакет был разобран соответствующей функцией-обработчиком, если она вообще была определена. Аргументы события - это тупо массив данных из сообщения.

Appendix A: introduction to the Ragnarok Online protocol

The Ragnarok Online protocol uses TCP as its transport protocol. Every message* that the RO server sends to its clients has the following format:

packet switch (2 bytes) + message content (variable length)

A message consists of at least 1 field: the message identifier (also known as the packet switch, but I think "message identifier" is easier to understand). This message identifier tells the client what kind of message this is. How the message is to be interpreted depends on this message identifier.

There are two kinds of messages:

Static length messages

These messages are always of the same length. Examples of such messages are the "someone has sent an emoticon" message and the "a monster has appeared" message.

Variable length messages

The length of these messages depend on their contents. Because their lengths vary, they have a special message length field which tells the client exactly how long the message is. This length is the length of the entire message, including message identifier. The "you have sent a public chat message" message is an example of a variable length message. Finally, we have the message arguments. The exact contents of the arguments depends on the message. For example, the "someone has sent an emoticon" message has the following information in its message arguments:


The ID of the actor who sent the emoticon. What kind of emoticon it was.


(*) There is one exception to the rule. If the client is in-game, and the user instructs the client to switch character, then the client will disconnect from the map server and connect to the character server. The first message that we receive, in this case, is the account ID, which is exactly 4 bytes. It is not a "normal" RO message in that it has no message ID - it's just a serialized integer.

Appendix B: recvpackets.txt and handling message lengths

When we receive data through a socket from the RO server, we cannot assume that we receive exactly 1 complete message every time we read from the socket. We may receive a part of a message, or we may receive two messages, or we may receive a complete message and an incomplete part of the next message. This is why we must buffer data received from the RO server. Whenever we've determined that we've received at least one complete message, we'll process that message and remove it from the buffer. Then we keep waiting until we know we have another complete message, and so forth.

But how do we know whether a message is complete? To know that we have to know every message's exact length. That's what recvpackets.txt is for: it specifies which messages have what length. For example, recvpackets.txt has this line:

00C0 3

This means that the message with identifier "00C0" is a static length message, and has length 3. But sometimes you also see a line like this:

00D4 0
00D4 -1

The 0/-1 means variable length, so in this case it means message 00D4 is a variable length message. As mentioned in appendix A, variable length messages have a message length field which tell us how long that message is.

Appendix C: obfuscation of outgoing messages

RO has made several attempts to prevent third party clients from (correctly) accessing the server. The most important attempts involve the obfuscation of outgoing messages. That is: messages that are to be sent from an RO client to the RO server are first obfuscated using some algorithm. This appendix describes a few obfuscation techniques.

Padded packets

This is not used anymore Some RO servers, such as euRO (Europe), iRO (International) and rRO (Russia) use so-called padded packets. The RO client will insert what seems to be garbage data into parts of certain message. In reality, this "garbage" is the result of of a complex hashing algorithm. Furthermore, the size of the garbage data, and the algorithm that is used, varies every time a sync is received from the RO server. Thus, a packet may have different sizes during different times.

We refer to messages that contain such data, as "padded packets". The padded packets emulator subsystem in OpenKore is responsible for generating correct padded packets.

Padded packets only affect packets that are sent from the client to the server, not packets received from server to client. Furthermore, not all packets are padded - only some are, usually the "sit", "stand", "attack" and "use skill" packets.

See the file "src/auto/XSTools/PaddedPackets/README.TXT" in the OpenKore source code for more information.

Encrypted message IDs

In this technique, the message ID of an outgoing message (i.e. the first 2 bytes) might be encrypted. This is only applicable to messages that are sent to the map server - account server and character server messages are unaffected. Messages are unencrypted, until the map server, at some point, sends the encryption key. This encryption key is valid as long as the connection to the map server is alive. Once the client disconnects from the map server, the encryption key is invalid and should not be used.

See the file "src/Network/Send.pm" in the OpenKore source code for the exact algorithm. Look for function encryptMessageID().

Original article

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