JSON Serialization for Server-Client Communication in “Warring States”

Nalu Zou
4 min readNov 29, 2020

--

A common challenge that I face when developing multiplayer games has been keeping message serialization and deserialization consistent between the client and the server. In my previous projects, I used Node.js on the backend with Unity C# on the frontend. Since the backend and the frontend used different languages, I needed to write client code to serialize a C# object into a string, and write a symmetrical javascript method to parse that string into the corresponding JSON object. This was a very tedious and error-prone process.

During my internship at Wizards of the Coast, I discovered the NewtonSoft package, which is a very convenient tool to automatically serialize and deserialize C# objects into JSON strings. I decided to try it out in my latest multiplayer game “Warring States” to shorten the development time for new features. Here’s how it works.

Setup

For “Warring States,” I am using ASP.NET on the backend, hosted with Heroku. Since ASP.NET is a C# framework, I can utilize NewtonSoft on both the client and the server.

Serializing a payload

Here is the payload structure for a JoinGame request.

As you can see, it’s a very simple class that stores the player’s name and the code for the lobby that the player wants to join.

This payload is wrapped in a Request class, which stores the request type and the transaction ID (more on this later).

The “Payload” field is a string, instead of a JoinLobbyRequest. Thus, there are two levels of serialization: One for the payload, and a second serialization for the entire Request object. A request is structured this way since it allows the server to retrieve the top-level information for a request, such as the request type, and use that to determine how to deserialize the payload.

When the client wants to send a request to the server, it calls the generic method “SendRequest,” which takes a RequestType and a generic payload. There is a nested serialization, once for the payload, and a second one for the request object.

Deserializing a payload

When the server receives a request from the client, it will need to deserialize the payload twice. First, the server converts the byte array into a string, and then deserializes that string into a Request object.

Once a request has been successfully deserialized, the socket handler will call the OnRequest event, passing the sending socket and the request to all listeners.

For example, one of the listeners is MessageRouter.cs. Using the request object, it deserializes the request’s payload based on the request’s type. If the request type is JoinLobby, then it will deserialize the payload into a JoinLobbyRequest and send it to the LobbyJoiner class.

Server Responses

Deserializing a message on the server-side isn’t the end of the story. How can the server send a response back to the client, and how can the client know which request the response is associated to?

In order for the server to send the joinResponse object to the client, it passes the payload into the SendResponse method in _socketHandler.

SendResponse is a generic method takes the client’s socket, the original Request object, and a generic payload parameter. It can then use the original request to construct a corresponding Response object, using the original request’s tranasaction ID to associate this response with the request. Once again, there is a nested serialization. Once for the payload, and another for the entire Response object.

On the client, we can then add a callbacks dictionary, which stores transaction IDs as keys, and callbacks as values. There’s a second SendRequest method, this time taking in a second type parameter for the response payload type. When we send a request to the server, we register a callback using the generated transaction ID. When the callback is invoked, we deserialize the response’s payload using the response type parameter.

I added a TaskCompletionSource to allow the client to make an asynchronous call to the server, which improves readability. When the callback is invoked, the TaskCompletionSource is resolved.

The callbacks dictionary is invoked whenever the client receives a packet from the server. If the response’s transaction ID is recognized, then we invoke the callback stored in the callbacks dictionary.

Joining a Lobby

Using the established architecture, if the client wants to join a lobby, it simply needs to run the following code.

It simply creates a JoinLobbyRequest and passes it to the SendRequest method. On the very next line, it can take the returned response and handle it accordingly.

Versioning

One thing to note is that the client and the server must have the exact same payload classes. One way to verify that the client has the correct version of the payload classes is to have a handshake where the client passes its version to the server. If the client’s version is out of date, the server can reject the connection.

Final Thoughts

Using this design pattern, I was able to quickly build out the backend for “Warring States” in a month. Using NewtonSoft to automatically serialize and deserialize requests and handling responses asyncrhonously makes developing new features quicker, cleaner, and less error prone.

--

--

Nalu Zou
Nalu Zou

Written by Nalu Zou

Software developer, game maker, student at the University of Washington.

No responses yet