Networking

Networking API

It's a layer of abstraction on top of networking technologies / protocols, that simplifies communication between servers and clients. It's designed to be fast, convenient and very extendable - you can fine tune structure of your messages, change communication protocols, and none of your networking code would have to change.

Supported Protocols

At the moment of writing this, two protocols are supported.

  • Websockets/TCP - based on websocket-sharp. Main classes: WsClientSocket, WsServerSocket

General Overview

To establish a connection between two endpoints, one of them must be a client (IClientSocket), and another - a server (IServerSocket).

When connection is established, both server and client will see each other as peers (IPeer). Each one of them can send message (IOutgoingMessage) and receive them (IIncomingMessage).

Starting a Server Socket

Server sockets implemet IServerSocket interface. It exposes two events and two methods

  • OnPeerConnectedEvent (event) - invoked, when client connects to this socket. IPeer instance, which represents a connected client, will be provided as an argument
  • OnPeerDisconnectedEvent (event) - invoked, when client disconnects
  • Listen(port) - opens the socket and starts listening to the port. After calling this method, clients can start connecting
  • Stop() - stops listening
void StartServer()
{
    IServerSocket server = Mst.Create.ServerSocket();

    server.OnPeerConnectedEvent += (peer) =>
    {
        Debug.Log($"Server: client connected: {peer.Id}");
    };

    server.OnPeerDisconnectedEvent += (peer) =>
    {
        Debug.Log($"Server: client disconnected: {peer.Id}");
    };

    server.Listen(5000);
}

Connecting With a Client

Client socket implements IClientSocket interface. Exposed properties and methods are as follow:

  • Peer - peer, to which client has connected. This is what you'd use to send messages to server.
  • Status - Status of the connection
  • IsConnected - Returns true, if we are connected to another socket
  • IsConnecting - Returns true, if we're in the process of connecting
  • OnConnectionOpenEvent (event) - Invoked on successful connection
  • OnConnectionCloseEvent (event) - Invoked when disconnected from server socket
  • OnStatusChangedEvent (event) - Invoked when connection status changes
  • Connect(ip, port, timeoutSeconds) - Starts connecting to server socket at given address
  • Disconnect(fireEvent) - Closes the connection
  • WaitForConnection(callback, timeoutSeconds) - a helpful method, which will invoke a callback when connection is established, or after a failed attempt to connect. If already connected - callback will be invoked instantly.
  • RegisterMessageHandler(handler) - adds a message handler of a specific operation code. If there's already a handler with same op code, it will be overridden.
private void StartClient()
{
    IClientSocket client = Mst.Create.ClientSocket();

    client.OnConnectedEvent += () =>
    {
        Debug.Log("Client: I've connected to server");
    };

    client.Connect("127.0.0.1", 5000);
}

Exchanging Messages

Client and server communicate to each other through IPeer interface. Server will get clients peer when it connects, and client can access servers peer object through IClientSocket. Peer

There are many overload methods for sending and responding to messages. You can check them by opening IMsgDispatcher and IIncomingMessage interfaces. Examples below will show you some of the basic methods you can use and how to use them.

Client to Server

To receive messages, server will need to listen to the event on IPeer:

server.OnPeerConnectedEvent += (peer) =>
{
    Debug.Log($"Server: client connected: {peer.Id}");

    // Client just connected. Let's start listening to all its messages
    peer.OnMessageReceivedEvent += message =>
    {
        // Handle peer messages
        Debug.Log("Server: I've got a message from client: " + message.AsString());
    };

    // Then send a message to this client
    ...
};

Client can send a simple string message like this:

client.OnConnectedEvent += () =>
{
    Debug.Log("Client: I've connected to server");

    // Client connected to server. Let's start listening messages from server
    // ...

    // Then send message to server
    client.Peer.SendMessage(0, "Hey!");
};

Server to Client

You can do it pretty much the same way you sent messages from client to server.

Server:

server.OnPeerConnectedEvent += (peer) =>
{
    Debug.Log($"Server: client connected: {peer.Id}");

    // Client just connected. Let's start listening to all its messages
    // ...

    // Then send a message to this client
    peer.SendMessage(0, "What's up?");
};

Client:

client.OnConnectedEvent += () =>
{
    Debug.Log("Client: I've connected to server");

    // Client connected to server. Let's start listening messages from server
    client.Peer.OnMessageReceivedEvent += message =>
    {
        Debug.Log("I've got the message!: " + message.AsString());
    };

    // Then send message to server
    // ...
};

Responding to Messages

If you want to send a message and get something in return, a.k.a send a request and get a response, there's a useful overload method for that.

Server:

server.OnPeerConnectedEvent += (peer) =>
{
    Debug.Log($"Server: client connected: {peer.Id}");

    // Client just connected. Let's start listening to all its messages
    // ...

    // Then send a message to this client
    peer.SendMessage(0, "What's up?", (status, response) =>
    {
        // This get's called when client responds
        if (status == ResponseStatus.Success)
        {
            Debug.Log("Client responded: " + response.AsString());
        }
    });
};

Client:

client.OnConnectedEvent += () =>
{
    Debug.Log("Client: I've connected to server");

    // Client connected to server. Let's start listening messages from server
    // ...

    // Let's register message response handler 
    client.RegisterMessageHandler(0, message =>
    {
        // Message received, let's respond to it
        message.Respond("Not much", ResponseStatus.Success);
    });

    // Then send message to server
    // ...
};

Message Serialization

Every single peace of data you send is converted into byte[]

To avoid reflection and AOT methods, I didn't use any third party serialization libraries. Instead, every packet is serialized manually - it's not difficult to do, and gives you full control.

IMsgDispatcher.SendMessage supports these types of data:

  • int
  • string
  • byte[]
  • ISerializablePacket - any data structure, which implement ISerializablePacket interface

On the receiving end, you can "read" received data from IIncomingMessage with these methods:

  • AsInt() - uses data in message to convert it to int
  • AsString() - uses data in message to convert it to string
  • AsBytes() - returns your data in byte[] array
  • AsPacket(ISerializablePacket packet) - fills ISerializablePacket implementation with data.
  • AsPacketsList(Func<T> packetCreator) - fills list of ISerializablePacket implementation with data.

There are some helpful extension methods for turning common types to byte arrays and back:

  • String.ToBytes() - use it like this: "my string".ToBytes()
  • Dictionary<string,string>.ToBytes()
  • Dictionary<string,string>.FromBytes(byte[]) - use it like this: new Dictionary<string, string>().FromBytes(data)
  • Dictionary<int,int>.ToBytes()
  • Dictionary<int,int>.FromBytes(byte[]) - use it like this: new Dictionary<int, int>().FromBytes(data)
  • Dictionary<string, float>.ToBytes()
  • Dictionary<string, float>.FromBytes(byte[]) - use it like this: new Dictionary<string, float>().FromBytes(data)
  • Dictionary<string, int>.ToBytes()
  • Dictionary<string, int>.FromBytes(byte[]) - use it like this: new Dictionary<string, int>().FromBytes(data)

You can extend SerializablePacket to create custom packet classes, like this:

public class IntPairPacket : SerializablePacket
{
    public int A { get; set; }
    public int B { get; set; }

    public override void ToBinaryWriter(EndianBinaryWriter writer)
    {
        writer.Write(A);
        writer.Write(B);
    }

    public override void FromBinaryReader(EndianBinaryReader reader)
    {
        A = reader.ReadInt32();
        B = reader.ReadInt32();
    }
}

If you're using SerializablePacket, here's how you serialize, send, receive and deserialize. Sending a message:

var packet = new IntPairPacket()
{
    A = 0,
    B = 2
};

peer.SendMessage(0, packet.ToBytes());

Receive a packet:

peer.OnMessageReceivedEvent += message =>
{
    // Handle peer messages
    var packet = message.AsPacket(new IntPairPacket());
    Debug.Log("Server: I've got a message from client: " + packet.A);
};