JSON

C# JSON Client-Server Implementation

Background

A Socket is one end of a two way communication link. The major protocols for socket communication are TCP and UDP. The primary difference is that TCP guarantees data delivery and the order of data packets. UDP does not make such guarantees, but as a consequence it is faster. Developers typically default to TCP.

The C# socket library is found inthe System.Net package.

using System.Net;
using System.Net.Sockets;

IP Endpoint

An IPEndpoint is the pairing of an IPAddress and a port number. You can use DNS lookup to obtain an IP address.

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("google.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];
IPEndPoint ipEndPoint = new(ipAddress, 7000);

When creating the server endpoint, you can specify ‘any’ for the ip address.

IPEndPoint ipEndPoint = new(IPAddress.Any, 7000);

Terminate a Socket

Shutdown disables sends and receives on a Socket.

Close will terminate the Socket connection and releases all associated resources.

socket.Shutdown(SocketShutdown.Both);
socket.Close();

Server

A server must first listen for and accept connections. Then, when a connection is made, listen for data on a seperate Socket.

Creating a Socket

Bind associates a socket with an endpoint.

Listen causes a connection-oriented (server) Socket to listen for incoming connection attempts.

Socket socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(ipEndPoint);
socket.Listen(port);

Accepting Connections

Accept blocks until an incoming connection attempt is queued then it extracts the first pending request from a queue. It then creates and returns a new Socket. You can call the RemoteEndPoint method of the returned Socket to identify the remote host’s network address and port number.

See AcceptAsync for the asynchronous accept call.

Socket handler = this.listener.Accept();

Client

You create the client side socket in the same manner as the server. The difference being, instead of Bind and Listen, the client uses Connect.

IPAddress ipAdd = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPt = new IPEndPoint(ipAdd, port);
Socket socket = new Socket(ipAdd.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ipEndPt);

Reading & Writing

Reading and writing date on a socket is the same for both the server and client side socket. Once the actual connection is made, the difference between the two is arbitrary. Both the read and operations require a byte array to act as a buffer. So your data will need to be converted to and from an array of bytes.

Write

The Send method is used to write to a socket. While there are a number of different method flavours, the most common is to send the entire contents of a byte array.

byte[] msg = Encoding.ASCII.GetBytes(aString);
this.socket.Send(msg);

If you are using a connection-oriented protocol, Send will block until the requested number of bytes are sent, unless a time-out was set by using Socket.SendTimeout. If the time-out value was exceeded, the Send call will throw a SocketException

If the receiving socket (server) has not been started a SocketException will be thrown. If the server has been started but Accept has not been called reading and writing will hang. This can be remedied by setting a timeout (in ms).

Set Read/Write Timeouts

socket.ReceiveTimeout = 1000;
scoket.SentTimeout = 1000;

Read

If the remote host shuts down the Socket connection with the Shutdown method, and all available data has been received, the Receive method will complete immediately and return zero bytes. This allows you to detect a clean shutdown.

There are many flavours of the Receive method, but we will only concern ourselves with two of them. The first reads all bytes from a socket. It returns the number of bytes read. You will need to provide your own EOF indicator or wait until 0 bytes are read which means the socket has finished writing and closed. This is useful if you are only connecting the sockets for a single read-write operation.

byte[] bytes = new byte[BUFFER_SIZE];
socket.Receive(bytes);

The second, and the one we will be using, is to read a specific number of bytes from the socket.

int count = socket.Receive(bytes, nextReadSize, SocketFlags.None);

We will use this to first read the size of the data, then read the body of the data.

// read size
byte[] bytes = new byte[INT_BUFFER_SIZE];
socket.Receive(bytes, INT_BUFFER_SIZE, SocketFlags.None);
int size = BitConverter.ToInt32(bytes, 0);

// read message
byte[] bytes = new byte[size];
socket.Receive(bytes, size, SocketFlags.None)
string data = Encoding.ASCII.GetString(bytes, 0, count);

Implementation

JSON

The json encoding and decoding will be handled by the Newtonsoft Json.Net library.

dotnet add package Newtonsoft.Json --version 13.0.1

The include statements for this library are.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Layout

There are 3 source files in the project:

  • Server.cs: Manages new client connecting to the server and emits Connection objects.
  • Connection.cs: Wraps a socket providing the read-write capabilities.
  • Client.cs: A connection object that connects to its own socket.

Server

Most of the details for this class are provided in the above background section. The main detail here is the way new connections are managed. I have put the loop inside a IEnumerable method. This should be looped to hand off new connection objects. The following example does this in a new thread.

Thread thread = new Thread(new ThreadStart(()=>{    
    foreach(Connection connection in server.Connections()){
        connection.WriteString("ack");
        connection.Close();
    }
}));

Connection

The connection object reads and writes JSON objects or Strings in two parts. First it writes four bytes representing the integer size of the data. Next it writes the data it’s self.

Client client = new Client().Connect("127.0.0.1", 7000);
client.socket.ReceiveTimeout = 3000;
Console.WriteLine("> " + client.ReadString());
server.Stop();

JSON Serialization in Unity

JSONUtility (Unity Class)

Unity has it’s own JSON serialization package [ref] which is very close in usage to the native C# serialization. It works with public field, or private fields marked with the ‘SerializeField’ attribute. It does not work with class properties, or fields marked ‘readonly’. To otherwise skip a field mark it with the ‘NonSerialized’ attribute.

// Will serialize
public string name;
[SerializeField] private string value;

// Will not serialize
public readonly string name;
public string name {get; set;}
[NonSerialized] int age;

The serialization method does not require type information, it will simply create json text based on the public field values. This means you do not need to deserialize into the same object you serialized from. The class it’s self must be marked with the ‘Serializable’ attribute.

using UnityEngine;

[System.Serializable]
public class PlayerState : MonoBehaviour{
    public string name;
    public int age;

    public string SaveToString(){
        return JsonUtility.ToJson(this);
    }

    // Given:
    // name = "Dr Charles"
    // age = 33
    // SaveToString returns:
    // {"name":"Dr Charles","age":33}
}

Unlike serialization, deserialization requires a type. Any values found in the JSON text that are not fields in the provided type will be ignored.

PlayerState playerState = JsonUtility.FromJson<PlayerState>(string);

JSONEncoder (Helper Class)

In order to automatically decode a json object to the correct type I have included the type name in the JSON text object. The emitted JSON has two root fields: type & instance.

using UnityEngine;
using System;

public class JSONDetails<T>{
    public T instance;
    public string type;

    public JSONDetails(T instance) {
        this.instance = instance;
        this.type = instance.GetType().ToString();
    }    
}

public class JSONEncoder {
    public string type = "";
    public JSONEncoder() { }

    public static string Serialize<T>(T anObject) {
        return JsonUtility.ToJson(new JSONDetails<T>(anObject), true);
    }

    public static object Deserialize(string json) {
        JSONEncoder wrapper = JsonUtility.FromJson<JSONEncoder>(json);
        Type g = Type.GetType(wrapper.type);
        Type t = typeof(JSONDetails<>).MakeGenericType(new Type[] { g });
        var hydrated = JsonUtility.FromJson(json, t);   
        return t.GetField("instance").GetValue(hydrated);
    }
}

At times serialization may require type information to ensure the correct type is serialized. Deserialization returns an object which can be cast as necessary. Typically this should used to detect the presence of an interface and cast accordingly.

public void ReceiveEvent(string json, string source) {
    var isEvent = JSONEncoder.Deserialize(json);

    if (isEvent is IModelEvent<M> != false) {
        IModelEvent<M> modelEvent = (IModelEvent<M>)isEvent;
        this.model = modelEvent.UpdateModel(this.model);
    }

    if (isEvent is IModelEvent<N> != false) {
        this.Broadcast(isEvent, source);
    }
}