Network subsystem — различия между версиями

Материал из Руководство по OpenKore
Перейти к: навигация, поиск
м (Handling multiple server types (message parser part))
м (Пример 2: исправление другого сервертипа)
Строка 169: Строка 169:
 
Вот и всё! Остальное происходит само по себе.
 
Вот и всё! Остальное происходит само по себе.
  
==== Example 2: handling a different server type ====
+
==== Пример 2: исправление другого сервертипа ====
Let's say the server with server type 12 has a slightly different "you are dancing" packet, in which the X and Y coordinate fields are swapped. That is, after the Y-coordinate comes the X-coordinate (instead of first X and then Y). You can modify the packet_list hash to update its message structure. So in the 'new' function of Network::Receive::ServerType12, you write this:
+
Пусть пакет "вы танцуете" немного отличается на серверах с типом 12. Например координаты X и Y идут в обратном порядке, то есть сначала Y, а потом X. В таком случае можно в хэше packet_list поменять поля сообщения местами. Тогда функция 'new' в  Network::Receive::ServerType12 будет выглядеть так:
  
 
  sub new {
 
  sub new {
Строка 176: Строка 176:
 
     my $self = $class->SUPER::new;
 
     my $self = $class->SUPER::new;
 
   
 
   
     # BEGIN: YOUR CODE
+
     # код начинается тут
 
     $self->{packet_list}{1234}[2] = [qw(y x type)];
 
     $self->{packet_list}{1234}[2] = [qw(y x type)];
     # END: YOUR CODE
+
     # а тут он заканчивается
 
   
 
   
 
     return $self;
 
     return $self;
 
  }
 
  }
If your Perl is not good enough: recall that $self->{packet_list}{1234} looked like this:
+
Если вы не сильны в Perl, вспомните, как выглядело описание этого пакета ($self->{packet_list}{1234}) в стандартном сервертипе из прошлого примера:
  
 
  ['dancing', 'v v C', qw(x y type)]
 
  ['dancing', 'v v C', qw(x y type)]
You wanted to modify its variable names list, which has index 2 in this array. That's why you wrote
+
Вы хотели поменять массив с названиями полей, который стоит на второй (считая с нуля) позиции. Поэтому пишем:
  
 
  $self->{packet_list}{1234}[2] = [qw(y x type)];
 
  $self->{packet_list}{1234}[2] = [qw(y x type)];
And now, $self->{packet_list}{1234} looks like this:
+
и теперь  $self->{packet_list}{1234} выглядит как надо:
  
 
  ['dancing', 'v v C', qw(y x type)]
 
  ['dancing', 'v v C', qw(y x type)]

Версия 16:49, 17 августа 2023

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

Введение

Сетевая подсистема 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)]

Handling multiple server types (message sender part)

Handling multiple server types in the message sender part is almost the same as the message parser part. The class hierarchy looks like this:


The difference is that the server type 0 handling code is in the class Network::Send::ServerType0. All other Network::Send::ServerTypeX subclasses inherit from Network::Send::ServerType0.

Interestingly, most of the difference between server protocol types is in the sending part! The messages that we receive from the server don't differ a lot between different server types, but the messages we have to send to the server differ a lot more.

Using the message sender

Like the message parser object, the message sender object is also created by the connection manager. That object is stored in the global variable $messageSender. To send a message to the server, write:

$messageSender->sendFoo();

Each message sender function has the prefix 'send'. See Network::Send::ServerType0 for a list of possible sender functions. For example, if you want to send the "public chat" message to the RO server, write:

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

Compatibility notes

The object-oriented message sender architecture (as described) on this page was introduced on December 20 2006. To send a message to the server in earlier OpenKore versions, you had to write one of these:

OpenKore 1.6 and 1.9.0-1.9.2 syntax

sendFoo(\$remote_socket, args);

OpenKore 1.9.0-1.9.2 syntax

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

These won't work anymore. Instead, write:

$messageSender->sendFoo(args);

Or, if your plugin must have 1.6 compatibility write this:

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

Hooks

The message sender class provides some hooks which allows plugins to handle messages.

"packet_pre/$HANDLER_NAME"

Here, $HANDLER_NAME is the name of the handler function, as specified in the packet_list hash. For example, "packet_pre/account_server_info".

This hook is called just before the handler function is called. But this hook is only called if there is a handler function for the current packet. The argument given to this hook is an array containing the message fields (the message arguments).

"packet/$HANDLER_NAME"

This hook is called after the handler function is called, or when there is no handler function for the current message. Its argument is the message arguments.

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