Implementing WebSocket Client in Unity

How hard would it be to create a simple WebSocket Client in Unity?

Well, recently, we did a series on implementing WebSocket Server using Node.js. So I think it is the time to make the client-side for it using Unity!

Let’s jump into it!

The idea 💡

As I mention, you can find the whole series on building WebSocket Server in Node.JS, and this will be a continuation of it. The goal of here will be to create a simple but easy to extend the solution to communicate with the server.

I won’t build a game here, but I might do it in upcoming posts, so stay tuned!

Coding 👨‍💻

At the start, we will build two scripts — one which will be responsible for handling communication between server and client. And the second one, which will be the forefront of the game.

Let’s start with the WsClient! This class will be instantiated by the other class so we can’t use MonoBehaviour here.

using System;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using UnityEngine;

/// <summary>
/// WebSocket Client class.
/// Responsible for handling communication between server and client.
/// </summary>
public class WsClient
{
    // WebSocket
    private ClientWebSocket ws = new ClientWebSocket();
    private UTF8Encoding encoder; // For websocket text message encoding.
    private const UInt64 MAXREADSIZE = 1 * 1024 * 1024;

    // Server address
    private Uri serverUri;

    // Queues
    public ConcurrentQueue<String> receiveQueue { get; }
    public BlockingCollection<ArraySegment<byte>> sendQueue { get; }

    // Threads
    private Thread receiveThread { get; set; }
    private Thread sendThread { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="T:WsClient"/> class.
    /// </summary>
    /// <param name="serverURL">Server URL.</param>
    public WsClient(string serverURL)
    {
        encoder = new UTF8Encoding();
        ws = new ClientWebSocket();

        serverUri = new Uri(serverURL);

        receiveQueue = new ConcurrentQueue<string>();
        receiveThread = new Thread(RunReceive);
        receiveThread.Start();

        sendQueue = new BlockingCollection<ArraySegment<byte>>();
        sendThread = new Thread(RunSend);
        sendThread.Start();
    }

    /// <summary>
    /// Method which connects client to the server.
    /// </summary>
    /// <returns>The connect.</returns>
    public async Task Connect()
    {
        Debug.Log("Connecting to: " + serverUri);
        await ws.ConnectAsync(serverUri, CancellationToken.None);
        while (IsConnecting())
        {
            Debug.Log("Waiting to connect...");
            Task.Delay(50).Wait();
        }
        Debug.Log("Connect status: " + ws.State);
    }

    #region [Status]

    /// <summary>
    /// Return if is connecting to the server.
    /// </summary>
    /// <returns><c>true</c>, if is connecting to the server, <c>false</c> otherwise.</returns>
    public bool IsConnecting()
    {
        return ws.State == WebSocketState.Connecting;
    }

    /// <summary>
    /// Return if connection with server is open.
    /// </summary>
    /// <returns><c>true</c>, if connection with server is open, <c>false</c> otherwise.</returns>
    public bool IsConnectionOpen()
    {
        return ws.State == WebSocketState.Open;
    }

    #endregion

    #region [Send]

    /// <summary>
    /// Method used to send a message to the server.
    /// </summary>
    /// <param name="message">Message.</param>
    public void Send(string message)
    {
        byte[] buffer = encoder.GetBytes(message);
        //Debug.Log("Message to queue for send: " + buffer.Length + ", message: " + message);
        var sendBuf = new ArraySegment<byte>(buffer);

        sendQueue.Add(sendBuf);
    }

    /// <summary>
    /// Method for other thread, which sends messages to the server.
    /// </summary>
    private async void RunSend()
    {
        Debug.Log("WebSocket Message Sender looping.");
        ArraySegment<byte> msg;
        while (true)
        {
            while (!sendQueue.IsCompleted)
            {
                msg = sendQueue.Take();
                //Debug.Log("Dequeued this message to send: " + msg);
                await ws.SendAsync(msg, WebSocketMessageType.Text, true /* is last part of message */, CancellationToken.None);
            }
        }
    }

    #endregion

    #region [Receive]

    /// <summary>
    /// Reads the message from the server.
    /// </summary>
    /// <returns>The message.</returns>
    /// <param name="maxSize">Max size.</param>
    private async Task<string> Receive(UInt64 maxSize = MAXREADSIZE)
    {
        // A read buffer, and a memory stream to stuff unknown number of chunks into:
        byte[] buf = new byte[4 * 1024];
        var ms = new MemoryStream();
        ArraySegment<byte> arrayBuf = new ArraySegment<byte>(buf);
        WebSocketReceiveResult chunkResult = null;

        if (IsConnectionOpen())
        {
            do
            {
                chunkResult = await ws.ReceiveAsync(arrayBuf, CancellationToken.None);
                ms.Write(arrayBuf.Array, arrayBuf.Offset, chunkResult.Count);
                //Debug.Log("Size of Chunk message: " + chunkResult.Count);
                if ((UInt64)(chunkResult.Count) > MAXREADSIZE)
                {
                    Console.Error.WriteLine("Warning: Message is bigger than expected!");
                }
            } while (!chunkResult.EndOfMessage);
            ms.Seek(0, SeekOrigin.Begin);

            // Looking for UTF-8 JSON type messages.
            if (chunkResult.MessageType == WebSocketMessageType.Text)
            {
                return CommunicationUtils.StreamToString(ms, Encoding.UTF8);
            }

        }

        return "";
    }

    /// <summary>
    /// Method for other thread, which receives messages from the server.
    /// </summary>
    private async void RunReceive()
    {
        Debug.Log("WebSocket Message Receiver looping.");
        string result;
        while (true)
        {
            //Debug.Log("Awaiting Receive...");
            result = await Receive();
            if (result != null && result.Length > 0)
            {
                receiveQueue.Enqueue(result);
            }
            else
            {
                Task.Delay(50).Wait();
            }
        }
    }

    #endregion
}
using System.IO;
using System.Text;

/// <summary>
/// Static class with additional methods used in communication.
/// </summary>
public static class CommunicationUtils
{
    /// <summary>
    /// Converts memory stream into string.
    /// </summary>
    /// <returns>The string.</returns>
    /// <param name="ms">Memory Stream.</param>
    /// <param name="encoding">Encoding.</param>
    public static string StreamToString(MemoryStream ms, Encoding encoding)
    {
        string readString = "";
        if (encoding == Encoding.UTF8)
        {
            using (var reader = new StreamReader(ms, encoding))
            {
                readString = reader.ReadToEnd();
            }
        }
        return readString;
    }
}

You can notice that WsClient will be running additional threads to send and receive data from the server. This is necessary as we don’t want to slow down the game itself. It also presents some challenges for Unity, as Unity doesn’t like multithreaded code besides the JobSystem.

So we need to create a component that will take that into consideration! Don’t worry. It’s not that hard as you think. 😄

In this component, we will also need to specify some information about how to connect to the server and where it is.

using UnityEngine;

/// <summary>
/// Forefront class for the server communication.
/// </summary>
public class ServerCommunication : MonoBehaviour
{
    // Server IP address
    [SerializeField]
    private string hostIP;

    // Server port
    [SerializeField]
    private int port = 3000;

    // Flag to use localhost
    [SerializeField]
    private bool useLocalhost = true;

    // Address used in code
    private string host => useLocalhost ? "localhost" : hostIP;
    // Final server address
    private string server;

    // WebSocket Client
    private WsClient client;

    /// <summary>
    /// Unity method called on initialization
    /// </summary>
    private void Awake()
    {
        server = "ws://" + host + ":" + port;
        client = new WsClient(server);
    }

    /// <summary>
    /// Unity method called every frame
    /// </summary>
    private void Update()
    {
        // Check if server send new messages
        var cqueue = client.receiveQueue;
        string msg;
        while (cqueue.TryPeek(out msg))
        {
            // Parse newly received messages
            cqueue.TryDequeue(out msg);
            HandleMessage(msg);
        }
    }

    /// <summary>
    /// Method responsible for handling server messages
    /// </summary>
    /// <param name="msg">Message.</param>
    private void HandleMessage(string msg)
    {
        Debug.Log("Server: " + msg);
    }

    /// <summary>
    /// Call this method to connect to the server
    /// </summary>
    public async void ConnectToServer()
    {
        await client.Connect();
    }

    /// <summary>
    /// Method which sends data through websocket
    /// </summary>
    /// <param name="message">Message.</param>
    public void SendRequest(string message)
    {
        client.Send(message);
    }
}

Now we have the base for our communication with the server, but how can we handle messages?

We already have a place where we can parse incoming messages from the server. Now we just need to recognize the type of message and do something with it! I like to create separate classes for different groups of messages. Here I will call it LobbyMessaging, which will be responsible for messages in the lobby.

/// <summary>
/// Base class for messaging groups.
/// </summary>
public class BaseMessaging
{
    // Reference to the server communication.
    protected ServerCommunication client;

    /// <summary>
    /// Initializes a new instance of the <see cref="T:BaseMessaging"/> class.
    /// </summary>
    /// <param name="client">Client.</param>
    public BaseMessaging(ServerCommunication client)
    {
        this.client = client;
    }
}




The next step will be to add it to the ServerCommunication.

using UnityEngine;

/// <summary>
/// Forefront class for the server communication.
/// </summary>
public class ServerCommunication : MonoBehaviour
{
    ...

    // Class with messages for "lobby"
    public LobbyMessaging Lobby { private set; get; }

    /// <summary>
    /// Unity method called on initialization
    /// </summary>
    private void Awake()
    {
        ...

        // Messaging
        Lobby = new LobbyMessaging(this);
    }

    /// <summary>
    /// Method responsible for handling server messages
    /// </summary>
    /// <param name="msg">Message.</param>
    private void HandleMessage(string msg)
    {
        Debug.Log("Server: " + msg);

        // Deserializing message from the server
        var message = JsonUtility.FromJson<MessageModel>(msg);

        // Picking correct method for message handling
        switch (message.method)
        {
            case LobbyMessaging.Register:
                Lobby.OnConnectedToServer?.Invoke();
                break;
            case LobbyMessaging.Echo:
                Lobby.OnEchoMessage?.Invoke(JsonUtility.FromJson<EchoMessageModel>(message.message));
                break;
            default:
                Debug.LogError("Unknown type of method: " + message.method);
                break;
        }
    }

    ...
}
/// <summary>
/// Message model.
/// </summary>
[System.Serializable]
public class MessageModel
{
    public string method;
    public string message;
}
/// <summary>
/// Echo message model.
/// </summary>
[System.Serializable]
public class EchoMessageModel
{
    public string text;
}

Great! You can continue to add more classes, methods, and events to it. I’ll stop here to make a simple example, how you can use it right now!

Example 🔥

To make this example, we will use our Echo WebSocket Server, so we can send and receive messages to see if our code is working properly.

Let’s build simple UI with the input field and “Send” button.

UI Layout
using UnityEngine;
using UnityEngine.Events;
using TMPro;

/// <summary>
/// UI form class used for example purposes.
/// </summary>
public class UIForm : MonoBehaviour
{
    #region [Input Field]

    // Reference to the input field
    [SerializeField]
    private TMP_InputField inputField;

    // Shortcut to get value from input field
    public string InputFieldText => inputField.text;

    /// <summary>
    /// Clears the input field.
    /// </summary>
    public void ClearInputField()
    {
        inputField.text = "";
    }

    #endregion

    #region [Send Button]

    // Action called on Send button clicked
    public UnityAction OnSendButtonClicked;

    /// <summary>
    /// Method attached to the Send button.
    /// </summary>
    public void SendButtonClicked()
    {
        OnSendButtonClicked?.Invoke();
    }

    #endregion

    #region [Server Response]

    [SerializeField]
    private TextMeshProUGUI serverResponse;

    /// <summary>
    /// Sets label with server response
    /// </summary>
    /// <param name="message">Message.</param>
    public void ShowServerResponse(string message)
    {
        serverResponse.text = $"Server Response:\n{message}";
    }

    #endregion
}

And now, let’s combine these two worlds together!

using UnityEngine;

/// <summary>
/// Class showing usage example.
/// Uses UI and server communication.
/// </summary>
public class UsageExample : MonoBehaviour
{
    // Reference to the servercommunication
    [SerializeField]
    private ServerCommunication communication;

    // Reference to UIForm
    [SerializeField]
    private UIForm uiForm;

    /// <summary>
    /// Unity method called after object initialization.
    /// </summary>
    private void Start()
    {
        communication.Lobby.OnConnectedToServer += OnConnectedToServer;
        communication.ConnectToServer();
    }

    /// <summary>
    /// Method called after connection with server was established.
    /// </summary>
    private void OnConnectedToServer()
    {
        communication.Lobby.OnConnectedToServer -= OnConnectedToServer;

        communication.Lobby.OnEchoMessage += OnReceivedEchoMessage;
        uiForm.OnSendButtonClicked += OnSendForm;
    }

    /// <summary>
    /// Method called when Send button was clicked in form.
    /// Send echo message to the server.
    /// </summary>
    private void OnSendForm()
    {
        var message = new EchoMessageModel
        {
            text = uiForm.InputFieldText
        };
        communication.Lobby.EchoMessage(message);
        uiForm.ClearInputField();
    }

    /// <summary>
    /// Method called when server return echo message.
    /// </summary>
    /// <param name="message">Message.</param>
    private void OnReceivedEchoMessage(EchoMessageModel message)
    {
        //Debug.Log("Echo message received: " + message.text);
        uiForm.ShowServerResponse(message.text);
    }

    /// <summary>
    /// Unity method called when object is disabled.
    /// </summary>
    private void OnDisable()
    {
        communication.Lobby.OnConnectedToServer -= OnConnectedToServer;
        communication.Lobby.OnEchoMessage -= OnReceivedEchoMessage;

        uiForm.OnSendButtonClicked -= OnSendForm;
    }
}

The result 🔨

Sending echo messages to the server

Yay! 🙌

It’s working! I mean… Of course, I knew it would work… 🧐

What do you think about it? Have you ever build WebSocket Client in Unity? Let me know in the comment section down below!

If you want to get notified of future content, sign up for the newsletter!

As always, the whole project is available at my public repository. 🔗

And I hope to see you next time! 🤓

3 2 votes
Article Rating
Subscribe
Notify of
guest
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Aleksander
Aleksander
3 months ago

Hi, thanks for the post, useful stuff!
 
Two small issues: I think the LobbyMessaging class is missing, and also you instantiate the WebSocketClient in WsClient twice.
 
Finally, a question: so far I planned to base my networking on pure TCP as here, but recently I found out that websites are not allowed to open such connections, am I correct thinking that web sockets are a solution?

Aleksander
Aleksander
3 months ago
Reply to  Patryk Galach

Of course, as I said I already had it implemented except with the wrong technology, everything works fine when I run the game as an exe, but when I try to host it on a website as WebGL TCP stops working, that’s why I’m here. The last missing piece is backend / server, I’ve seen the other part of your guide but I need it in c# not node / js.
Thanks again for the reply 🙂

Jacopo Bellù
Jacopo Bellù
2 months ago

Very clean code, but the close/dispose section is missing and it’s necessary. The two threads will remain alive for ever.
Very clean code, but the close/dispose section is missing and it’s necessary. The two threads will remain alive forever.

Last edited 2 months ago by Jacopo Bellù