Network messages
From NetGore Wiki
| Difficulty | |
|---|---|
| Topics | Networking |
| (view all tutorials) | |
This page describes the structure of the messages that go between the server and client, and how to create new messages.
Contents |
Introduction
A network message refers to a single, independent piece of information sent over the network. Examples include the server telling the client that a character attacked, the client telling the server it wants to move in a direction, etc. These messages are sent between the server and clients very frequently at all times. Since NetGore uses a client-server topology, clients never actually send messages directly to one another.
Message declaration
The first step to creating a network message is to give it a unique identifier. Messages that go from server->client and messages that go from client->server both have their own set of IDs. So while the process is very similar for server->client messages and client->server messages, they are done separately.
Messages that go from client->server are defined in DemoGame.ClientPacketID, and those that go from server->client are in DemoGame.ServerPacketID. The actual value does not matter, as long as it is not zero (which is a reserved value used by NetGore internally). So define a new name in the enum anywhere you want after the RESERVED value. Generally, its best to keep it alphabetical just for code organizational reasons, but it really doesn't matter at all. Unless you have a very good reason to do otherwise, you should never explicitly specify a value. This will let the compiler pick the numbers for you.
Writing the message
Its best to follow the same style NetGore provides by default for sending messages. The server and client both have a public static class full of methods following the same template:
public static PacketWriter MESSAGE_NAME(/* parameters */) { var pw = GetWriter(ClientPacketID.MESSAGE_NAME); // Use ServerPacketID instead in the server // write logic return pw; }
In this server, this is in DemoGame.Server.ServerPacket. In the client, it is in DemoGame.Client.ClientPacket. The purpose of these classes are to provide a singular place to construct the message. It makes maintenance much easier when there is only one place that defines the serialization of a message, and one place that defines the deserialization. Just follow the same pattern as shown above, and write the values into the NetGore.Network.PacketWriter.
Reading the message
You now have a way to identify the message, write it - all that is left is to read and handle it. Just like before, the client and server both have one class that holds all of these operations. In the client, this is DemoGame.Client.ClientPacketHandler. In the server, it is DemoGame.Server.ServerPacketHandler. Again, you just want to follow the existing template:
[MessageHandler((uint)ClientPacketID.MESSAGE_NAME)] void RecvMESSAGE_NAME(IIPSocket conn, BitStream r) { // read logic // handling logic }
Everything should seem obvious except for the NetGore.Network.MessageHandlerAttribute method attribute and the NetGore.Network.IIPSocket parameter. The NetGore.Network.MessageHandlerAttribute is what binds the method to the message ID. When the client (or server) receives a message that matches the ID specified in the MessageHandler attached to the method, the data will be forwarded to the method. The NetGore.Network.IIPSocket parameter conn is the connection the message came from. In the client, it is pretty useless since you are only connected to one server, so its not usually used in the client. But in the server, it is vital to have. The NetGore.IO.BitStream parameter r contains the message data.
At the start of the method, you want to read all of the data from the NetGore.IO.BitStream. Then, process the data in any way you need.
Extra considerations
The NetGore.Network.IIPSocket passed to the receiver method is actually for the whole packet received, not just the single message your are handling. In other words, there may (and likely is) other data in the stream. As a result, under-reading and over-reading will both result in corrupting the rest of the stream and potentially cause issues.
First off, you should always read everything first before using the values. Even if the first value you read is invalid, you must continue to read all the remaining values before aborting.
Secondly, make sure you only read what you wrote. If you read more than you wrote, it can also lead to issues and will corrupt the rest of the packet.
It is important your reads and writes always match up. NetGore has absolutely no idea how large your individual messages are, or where one ends and another begins. This is done to increase performance and reduce bandwidth. Though it also makes it more difficult to detect messages being serialized improperly.
Example
This section shows a step-by-step example of implementing a new message that goes from the server to the client. Whenever the character attacks, we will send the text "<Name> attacked" to every user on the map and have it appear in the chat box.
1. Add the message ID MyTestMessage in DemoGame.Server.ServerPacket:
MyTestMessage,
2. Add the message creation method MyTestMessage in DemoGame.Server.ServerPacket:
public static PacketWriter MyTestMessage(string name) { var pw = GetWriter(ServerPacketID.MyTestMessage); pw.Write(name); return pw; }
3. Add the method RecvMyTestMessage in DemoGame.Client.ClientPacketHandler:
[MessageHandler((uint)ServerPacketID.MyTestMessage)] void RecvMyTestMessage(IIPSocket conn, BitStream r) { var name = r.ReadString(); GameplayScreen.AppendToChatOutput(string.Format("{0} attacked!", name)); }
4. Make the server send the message at the appropriate time by going to DemoGame.Server.Character and find method:
public void Attack(Character target, ItemEntity weapon)
At the very end of the method, add the line:
var charMap = Map; if (charMap != null) { using (var pw = ServerPacket.MyTestMessage(Name)) { charMap.Send(pw, ServerMessageType.General); } }
Rebuild the client and server, run it, and you should now be spammed with text whenever someone on the map attacks.