network subsystem
Overview
The OpenKore network subsystem roughly consists of the following classes:
The Network class
This is the connection manager. It manages the TCP/IP socket connection with the server, and handles things like connection, disconnection, sending data to the server, receiving data from the server, etc. But it doesn't do much else.
Schematically, it looks like this:
Upon connection to the server, it creates two objects:
- A message parser object, which is of class Network::Receive::ServerTypeX. Whenever a message is received from the server, that message is passed to the message parser.
- A message sender object (not seen in the above diagram), which is of class Network::Send::ServerTypeX. This is used for sending messages to the server.
There are several implementations of Network class: Network::DirectConnection, Network::XKore and Network::XKoreProxy.
The Network::MessageTokenizer class
This is a tokenizer class. It extracts discrete server or client messages from a byte stream passed by the connection manager. But it doesn't do much else.
The Network::PacketParser class
This is a base message parser class. It parses messages passed by the connection manager into hashes with message data, as well as does reverse operation - generates messages from hashes with message data. But it doesn't do much else.
Afterwards, parsed messages are handled by built-in handlers in following classes and, with hooks, by plugins or other modules.
Additionally, there is API for modifying or dropping messages to alter any further processing.
The Network::Receive, Network::Send and Network::ClientReceive classes
There classes are descendants of Network::PacketParser class.
Network::Receive and Network::Send are parser helper classes. They contain parser helpers which serve as a workaround in the absense of the capable parser subsystem.
Network::Receive and Network::ClientReceive are message handling classes. They contain built-in handlers for messages coming from the server (Network::Receive) or from the client (Network::ClientReceive), which store information from network messages to be used later in other modules (like the AI).
Network::Send is the message sender class. It encapsulates network messages into simple, easy-to-use functions to be used outside of network subsystem.
Any descendant ServerType class may customize message handlers and parser helpers, but they should refrain from that other than for servertype-specific features and debugging.
The Network::Receive::ServerTypeX and Network::Send::ServerTypeX class
These are serverType description classes. They describe network message identifiers and structures for different servers. But they shouldn't do much else.
Note that none of these classes, from the connection manager to the serverType descriptions, should contain any AI code.
How it all works together
Main initialization code creates a connection manager instance and a message tokenizer instance:
$net = new Network::DirectConnection; $incomingMessages = new Network::MessageTokenizer(\%recvpackets);
Connection manager creates a packet parser instance:
$packetParser = Network::Receive->create($wrapper, $serverType);
Main loop passes information from connection manager $net to message tokenizer $incomingMessages:
$incomingMessages->add($net->serverRecv);
Message tokenizer with data and a message handler $packetParser are passed to a packet parser $packetParser to process all available messages:
@packets = $packetParser->process($incomingMessages, $packetParser); # compare with outgoing packets: # @packets = $messageSender->process($outgoingClientMessages, $clientPacketHandler);
Packets are passed back to the connection manager, which passes them to XKore clients:
$net->clientSend($_) for @packets;
Meanwhile, the packet parser calls custom parsers, hooks and built-in handlers:
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);
Handling multiple server types (message parser part)
Implementation details
Example 1: adding a new message handler
Example 2: handling a different server type
Handling multiple server types (message sender part)
Using the message sender
Compatibility notes
Hooks
The message sender class provides some hooks which allows plugins to handle messages.
"packet_pre/$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