Game Server Protocols part 4: Lobby subscription

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

The list of tables on a poker server is an example of a very common problem when doing client-server programming. There are various aspects to consider, so let’s first review those.

Refresh or subscription?

There are basically two ways to update lists – either using push (the server send data when it detects updates) or using pull (the client requests updated data). Pull is the only thing that works on stateless servers, but even over persistent connections this method can be useful.

As an example of this, imagine that you want to show a list of all the players on the server:

Name                Ranking    Table
Tomaz H              1239      Stockholm Omaha
Amanda K             5998      Gothenburg Texas Holdem
Lars W              19622      Stockholm Texas Holdem
... etc ...

For long lists, having push updates would make the list jump about quite a bit as people join or leave the server, making it hard to read. Here it’s better that the player makes a request for update using a refresh button or similar.

On the other hand, if we have a table list like this, we likely want it to automatically to update:

Name                     Limit        Players
Stockholm Omaha          $1-$2          3/6
Gothenburg Texas Holdem  $1-$2          1/6
Stockholm Texas Holdem   $15-$30       10/10
... etc ...

This is because although tables may occasionally appear and disappear, it’s important for us to know what tables are full before we click on them.

Explicit or implicit subscription

The other main consideration is whether to automatically send the information from the server, or if the client needs to explicitly send a subscription packet.

There are two variants for the former method:

CLIENT              SERVER (a)
LOGIN_REQUEST  ->   
               <-   LOGIN_RESPONSE with table list data

CLIENT              SERVER (b)
LOGIN_REQUEST  ->
               <-   LOGIN_RESPONSE
               <-   TABLE_LIST

The latter method looks like this:

CLIENT              SERVER (c)
LOGIN_REQUEST  ->
               <-   LOGIN_RESPONSE
SUB_TABLE_LIST ->
               <-   TABLE_LIST

This is a trade-off in terms of flexibility. For simple lists that the client is guaranteed to need, strategy (a) is useful as the client does not need to handle the state where login is complete but it still hasn’t got a table list. (c) has maximum flexibility, allowing you to skip subscription if it isn’t necessary. (b) is somewhere in between, having disadvantages and some of the advantages of both.

Handling updates

The initial table list will give you all the relevant tables, but how do we handle updates? Either the update will give you the full list again, or it will just give you the updated tables.

Again, this is a question of how long your lists are. If you typically have a list with around 2-10 tables that are very rarely updated, then you might want to send the entire list again in the update packet. It’s simple and guaranteed not to be wrong.

The more complicated way is to let the update packet look something like this:

{ "new_tables": [ ... full table descriptions ... ],
  "updated_tables": [ ... updated table fields ... ],
  "deleted_tables": [ ... deleted table ids ... ] }

This vastly cuts down on update packet sizes, but it requires the server to keep track of the currently viewed data for every player – which can add significantly to the complexity of the solution on both client and server. That said, it’s necessary if the server has large lists.

Long lists

The table list packet is also the first time where we might run into the packet size limit. If we already have the update packets which send the delta rather than the full list, we can use those:

CLIENT             SERVER 
LOGIN_REQUEST  ->
               <-  LOGIN_RESPONSE
SUB_TABLE_LIST ->
               <-  TABLE_LIST { "tables": [ ... table 1-100 ... ] }
               <-  TABLE_UPDATE { "new_tables": [ ... table 101 - 200 ...]
               <-  TABLE_UPDATE { "new_tables": [ ... table 201 - 247 ...]

This makes it easy for the client to handle, as incomplete lists and updates can be handled in the same manner.

There are other ways around this issue, but this method is among the most straightforward ones.

And finally, our packets

For our example I’ll use an explicit subscription model with update packets that send the changes and not the full lists, but first we need to design the data structs for each type:

TABLE:
{ "name": "Stockholm Omaha",
  "id": 1931,
  "players": 5,
  "max_players": 7,
  "limit-low": 100,
  "limit-high": 200,
  "game": "Omaha" }

TABLE_UPDATE:
{ "id": 1931,
  "players": 4 }

We add the following packets:

Packet Id  Name (Client to Server)   
   300     SUB_TABLE_LIST_REQUEST
   301     END_SUB_TABLE_LIST_REQUEST

Packet Id  Name (Server to Client)
   300     SUB_TABLE_LIST_RESPONSE
   301     END_SUB_TABLE_LIST_RESPONSE
   302     TABLE_LIST_UPDATE

SUB_TABLE_LIST_RESPONSE 
{ "result": <result code>, "tables": [ TABLE, TABLE, ... ] }

TABLE_LIST_UPDATE
{ "new": [ TABLE, TABLE, ... ],
  "updated": [ TABLE_UPDATE, TABLE_UPDATE, ... ],
  "removed": [ <removed table id>, ... ] }

Some suggestions

  • It’s easy to get the subscription deltas wrong, and trying to track down the bugs using the client to reproduce it is very time-consuming. If nothing else, make sure you have very good unit tests on the code that creates the delta updates.
  • When the lists get large you might find yourself splitting up subscriptions, so that instead of subscribing to all tables, you subscribe to a subset of them. It’s at this point you’re likely to start needing the “end subscription” packets.
  • It’s very easy to allow subscriptions to get complicated. Find the simplest solution that works for you, don’t try to be clever.

Summary

In this part we’ve added a few packets for sending the list of poker tables, but the procedure is the same for virtually any list of data that may need to be updated.

In the next entry we look at joining a game and observing the game state.

Advertisement