Multi-client, async sockets in c#, best practices?

Jason Miesionczek picture Jason Miesionczek · Nov 12, 2008 · Viewed 10.2k times · Source

I am trying to gain a better understanding of tcp/ip sockets in c#, as i want to challenge myself to see if i can create a working MMO infrastructure (game world, map, players, etc) purely for educational purposes as i have no intention of being another one of those "OMGZ iz gonna make my r0x0r MMORPG that will be better than WoW!!!", you know what im talking about.

Anyway, i was wondering if anyone can shed some light as to how one might approach designing this kind of system and what kinds of things are required, and what i should watch out for?

My initial idea was to break up the system into seperate client/server connections with each connection (on its own port) performing a specific task, such as updating player/monster positions, sending and receiving chat messages, etc. which to me would make processing the data easier because you wouldn't always need to put a header on the data to know what information the packet contains.

Does that make sense and is useful or am i just way over complicating things?

your responses are very much appreciated.

Answer

grieve picture grieve · Nov 13, 2008

If you are doing socket level programming, then regardless of how many ports you open for each message type, you still need to have a header of some sort. Even if it is just the length of the rest of the message. Having said that it is easy to add a simple header and tail structure to a message. I would think it is easier to only have to deal with one port on the client side.

I believe the modern MMORPGs (and maybe even the old ones) had two levels of servers. The login servers which verify you as a paying client. Once verified these pass you off to the game server which contain all the game world information. Even so this still only requires the client to have one socket open, but does not disallow having more.

Additionally most MMORPGS also encrypt all their data. If you are writing this as an exercise for fun, then this will not matter so much.

For designing/writing protocols in general here are the things I worry about:

Endianess

Are the client and server always guaranteed to have the same endianess. If not I need to handle that in my serialization code. There are multiple ways to handle endianess.

  1. Ignore it - Obviously a bad choice
  2. Specify the endianness of the protocol. This is what the older protocols did/do hence the term network order which was always big endian. It doesn't actually matter which endianess you specify just that you specify one or the other. Some crusty old network programmers will get up in arms if you don't use big endianess, but if your servers and most clients are little endian you really aren't buying yourself anything other than extra work by making the protocol big endian.
  3. Mark the Endianess in each header - You can add a cookie which will tell you the endianess of the client/server and let each message be converted accordingly as needed. Extra work!
  4. Make your protocol agnostic - If you send everything as ASCII strings, then endianess is irrelevant, but also a lot more inefficient.

Of the 4 I would usually choose 2, and specify the endianess to be that of the majority of the clients, which now days will be little endian.

Forwards and Backwards Compatibility

Does the protocol need to be forwards and backwards compatible. The answer should almost always be yes. In which case this will determine how I design the over all protocol in terms of versioning, and how each individual message is created to handle minor changes that really shouldn't be part of the versioning. You can punt on this and use XML, but you lose a lot of efficiency.

For the overall versioning I usually design something simple. The client sends a versioning message specifying that it speaks version X.Y, as long as the server can support that version it sends back a message acknowledging the version of the client and everything proceeds forward. Otherwise it nacks the client and terminates the connection.

For each message you have something like the following:

+-------------------------+-------------------+-----------------+------------------------+
| Length of Msg (4 bytes) | MsgType (2 bytes) | Flags (4 bytes) | Msg (length - 6 bytes) |
+-------------------------+-------------------+-----------------+------------------------+

The length obviously tells you how long the message is, not including the length itself. The MsgType is the type of the message. Only two bytes for this, since 65356 is plenty of messages types for applications. The flags are there to let you know what is serialized in the message. This field combined with the length is what gives you your forwards and backwards compatibility.

const uint32_t FLAG_0  = (1 << 0);
const uint32_t FLAG_1  = (1 << 1);
const uint32_t FLAG_2  = (1 << 2);
...
const uint32_t RESERVED_32 = (1 << 31);

Then your deserialization code can do something like the following:

uint32 length = MessageBuffer.ReadUint32();
uint32 start = MessageBuffer.CurrentOffset();
uint16 msgType = MessageBuffer.ReadUint16();
uint32 flags = MessageBuffer.ReadUint32();

if (flags & FLAG_0)
{
    // Read out whatever FLAG_0 represents.
    // Single or multiple fields
}
// ...
// read out the other flags
// ...

MessageBuffer.AdvanceToOffset(start + length);

This allows you to add new fields to the end of the messages without having to revision the entire protocol. It also ensures that old servers and clients will ignore flags they don't know about. If they have to use the new flags and fields, then you simply change the overall protocol version.

Use a Frame Work or Not

There are various network frameworks I would consider using for a business application. Unless I had a specific need to scratch I would go with a standard framework. In your case you want to learn socket level programming, so this is a question already answered for you.

If one does use a framework make sure it addresses the two concerns above, or at least does not get in your way if you need to customize it in those areas.

Am I dealing with a third party

In many cases you may be dealing with a third party server/client you need to communicate with. This implies a few scenarios:

  • They already have a protocol defined - Simply use their protocol.
  • You already have a protocol defined (and they are willing to use it) - again simple use the defined protocol
  • They use a standard Framework (WSDL based, etc) - Use the framework.
  • Neither party has a protocol defined - Try to determing the best solution based on all the factors at hand (all the ones I mentioned here), as well as their level of competencey (at least as far as you can tell). Regardless make sure both sides agree on and understand the protocol. From experience this can be painful or pleasant. It depends on who you are working with.

In either case you will not be working with a third party, so this is really just added for completeness.

I feel as if I could write much more about this, but it is already rather lengthy. I hopes this helps and if you have any specific questions just ask on Stackoverflow.

An edit to answer knoopx's question: