Game Server Protocols part 3: Login

by Christoffer Lernö

  1. Protocol handshake
  2. Login
  3. Receive table lists
  4. Join game table
  5. Participate in play
  6. Leave table

Login considerations

Since we have a poker server handling money, sending your login credentials in plain view over the wire is DEFINITELY NOT OK. To be complete, this section should really have information on how to secure the connection and send login credentials safely over that channel. There are quite a bit of those schemes (many with their fair share of security issues!). This is a deep and complicated subject in itself and frankly I personally do not have sufficient expertise to give solid recommendations. That said, anything not intended for public viewing should always encrypted and tamper-proofed – including login credentials.

Despite this, we’re going to simply send username/password in the clear for this protocol – but note that this is not something you should to for any server where you actually use real money – we’re doing it here because this is a tutorial on how to build a protocol and not the definite guide to implementing secure server protocols.

This is admittedly a large omission but can be acceptable for many types of games. Plus, first time you write a networking solution you’ll have your hands full anyway. Creating a secure solution is something you can try once you’ve gotten comfortable with building networking protocols.

Packet design

For this protocol we’ll simply treat the login as any sort of packet. It’s possible to bundle the login packet with the handshake, but that would mean that you cannot use the standard packet processing for the login packet. This is certainly feasible, but often ends up requiring additional complexity in packet handling.

In order to separate one packet from another, we need an identifier of some sort. This identifier could be a string or a number, but unless you’re writing a string based protocol, you’ll want to use numbers. An unsigned 8-bit or 16-bit number is sufficient. Eight bits are often enough, but you can use 16-bit to get a bit more freedom in assigning ids.

For our protocol this packet id will be sent first, followed by optional packet data. This is convenient since we can handle packets differently without having to inspect the entire packet data.

The client-to-server packets may look something like this (assuming 16-bit packet ids)

Packet Id  Name
   100     LOGIN_REQUEST
   200     REGISTER_REQUEST
   300     REQUEST_GAME_LIST
   400     JOIN_GAME
   ... and so on ...

Similarly you’ll have packets going from server to client:

Packet Id  Name   
   100     LOGIN_RESPONSE
   200     REGISTER_RESPONSE
   300     REQUEST_GAME_LIST_RESPONSE
   400     JOIN_GAME_RESPONSE

In this case we’re reusing the same ids, since they effectively form two separate channels.

In these simple packet examples, a client request will have a single corresponding server response, but in practice that’s not always true.

Before going into details about how to design the login and other requests we’ll need discuss packet payload serialization, since that has a profound effect on how many packet types you need.

Serialization

There are two main approaches to serialization. The first technique is to hard code the data for each packet. This means that each byte in the packet payload is statically specified. The extreme version of hard coded packets may employ bit-packing (squeezing values for multiple fields into a single byte) to minimize packet size.

Typically you’ll end up with something like this:

Byte   Value
   0   Table position (0-9)
 1-4   Money bet in cents (unsigned 32-bit int)
   5   Betting round complete (boolean)

The second technique is to use a dynamic format, much like a serialized hash map, e.g.

Key       Value
position      4
bet         100
complete   true

At first glance, the latter method might seem very wasteful on space, but there are several libraries that offer fairly tight space usage.

Hard coded payloads

The nice thing about hard coded payloads is that data size is straightforward, the technique is easy to understand and typically the deserialized data can be carried around in a struct or object which communicates (and documents) the data much better than pulling a data out of a hash map.

However, there are also downsides to hard coded payloads:

  • Each packet must have it’s own serialization / deserialization routine.
  • Messy to change when requirements change.
  • The more complex the payload data structure, the more effort it takes to add the packet.
  • Usually requires more packet types compared to a dynamic format.

Many of these drawbacks can be addressed by using code generation (e.g. protocol buffers) and macros, but that is also something which in itself adds complexity.

Hard coded payloads is great if your protocol only has a handful of packets that doesn’t have too complex data structure, but the more flexible the data, and the more packet types there are, the more effort they take.

Dynamic payloads

Outside of game servers, you typically see this JSON-based APIs (or XML APIs without validation).

For games, there are two important advantages:

  1. A single packet may cover a wider range of responses.
  2. Complex data structures are easy to transfer

The effect is that the number of different packet types are typically fewer than the corresponding hard coded payload version, and you don’t really have to worry too much when you send complex data, like tree structures.

The drawback with dynamic payloads is that it needs to provide its own definition. Unlike for hard coded payloads where the deserialization is inferred by looking at the packet type, the dynamic data needs to provide both type and data for each field.

There are various libraries to do this for you: HessianBSONMessagePack etc.

Typically you can expect slightly larger memory usage with dynamic payloads, but it can still be very competitive.

I strongly suggest using dynamic payloads when you start out, as they are faster to develop with and can still be shifted to hard coded data at a later stage.

Data checking

Regardless of whether you use a hard coded approach or dynamic, you will need to check the incoming data. For the dynamic payloads you also need to verify that the expected parameters actually exist.

In any case, never make any assumption on the correctness of data sent from client to server. There are many sources for incorrect data, including but not limited to: client bugs, hacking attempts and server bugs.

Our login packet

I’m going to use MessagePack for our protocol. It’s fairly straightforward to use and there are implementations for many languages.

Since we’re not using any login security, we’re simply going to send our login name and password in the clear (again, remember that this isn’t secure!). Our login payload will look like this in JSON notation: { "user": "Foo", "password": "Bar" }.

With MessagePack that payload becomes:

82 A4 75 73 65 72 A3 46 
6F 6F A8 70 61 73 73 77 
6F 72 64 A3 42 61 72

The server will respond with { "result": 0 } if the login was successful. Again, with MessagePack that is:

81 A6 72 65 73 75 6C 74 00

Consequently, our login exchange will look like this:

CLIENT                         SERVER
00 19 00 64               ->
----- ----- 
  |     |
  |     +--- packet id = 100
  +--- size of packet = 25
82 A4 75 73 65 72 A3 46 
6F 6F A8 70 61 73 73 77 
6F 72 64 A3 42 61 72
--------------------
  |
  +---- { "user": "Foo", "password": "Bar" }

                          <-   00 0B 00 64
                               ----- -----
                                 |     | 
           size of packet = 11 --+     | 
               packet_id = 100 --------+
                              
                               81 A6 72 65 73 
                               75 6C 74 00
                               -----------
                                    |
              { "result" : 0 } -----+

LOGIN_OK = 0
NO_SUCH_USER = 1
INVALID_PASSWORD = 2
SERVER_ERROR = -1

If login fails with NO_SUCH_USER we allow the client to send a registration request instead (REGISTER_REQUEST) which is handled almost identically to login. Some typical result values could be:

Result            Value
REGISTER_OK         0
USERNAME_IN_USE     1
ILLEGAL_USERNAME    2
ILLEGAL_PASSWORD    3
SERVER_ERROR       -1

Even in our simple example we see that we quickly find a lot of different error scenarios. Not all of these will need to be treated differently. For example, if the client SHOULD have checked that the username is legal, but still received the “illegal username” error the client might treat it the same as if the username is in use. The player doesn’t really care why the username can’t be used, just that it needs to be changed.

The data in our login packet has been very simple, but theoretically the client could send all sorts of additional information, such as currently used language, version of the operating system etc.

(For privacy reasons, do not send unnecessary information and try to anonymize the data where applicable)

This has been a quick overview. In the next part, we look at requesting the list of tables.

Advertisements