Search code examples
c#unity-game-enginejobsterminate

How to stop an endless server thread by escape key


I am trying to write a webSocket server in c#. The server runs as a job:

using Unity.Jobs;
using UnityEngine;

public class ServerStartup : MonoBehaviour
{
    WebSocketServerJob server = new WebSocketServerJob();
    JobHandle jobHandle;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Starting up the websocket server...");

        jobHandle = server.Schedule();
        
        Debug.Log("...the server should be running in it's own thread now :-)");
    }

    private void Update()
    {
        if (Input.GetKey(KeyCode.Escape))
        {
            Debug.Log("Escape key detected...stopping server!");
            server.StopSwitch = true;
            Debug.Log("Waiting for server tread to complete!");
            jobHandle.Complete();
            Debug.Log("Server tread complete...quitting app!");
            Application.Quit();
        }
    }


}

The server job is currently an endless job, but I would like it to end when I press Escape. However, just cancelling it from outside (using Abort or some such) will not be good as I first want to close the sockets and clean up. So the thread should get some kind of notice from outside that it is to terminate.


        //
// csc wsserver.cs
// wsserver.exe

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using Unity.Jobs;
using UnityEngine;


public struct WebSocketServerJob : IJob
{
    bool stopSwitch;

    public bool StopSwitch
    {
        set { stopSwitch = value; }
    }

    public void Execute()
    {
        StopSwitch = false;
        string ip = "127.0.0.1";
        int port = 80;
        var server = new TcpListener(IPAddress.Parse(ip), port);

        server.Start();
        Debug.Log("Server has started on " + ip + ":" + port + " Waiting for a connection...");

        TcpClient client = server.AcceptTcpClient();
        Debug.Log("ServerMsg: A client connected.");

        NetworkStream stream = client.GetStream();

        // enter to an infinite cycle to be able to handle every change in stream
        while (!stopSwitch)
        {
            while (!stream.DataAvailable) ;
            while (client.Available < 3) ; // match against "get"

            byte[] bytes = new byte[client.Available];
            stream.Read(bytes, 0, client.Available);
            string s = Encoding.UTF8.GetString(bytes);

            if (Regex.IsMatch(s, "^GET", RegexOptions.IgnoreCase))
            {
                Debug.Log("=====Handshaking from client=====\n" + s);

                // 1. Obtain the value of the "Sec-WebSocket-Key" request header without any leading or trailing whitespace
                // 2. Concatenate it with "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (a special GUID specified by RFC 6455)
                // 3. Compute SHA-1 and Base64 hash of the new value
                // 4. Write the hash back as the value of "Sec-WebSocket-Accept" response header in an HTTP response
                string swk = Regex.Match(s, "Sec-WebSocket-Key: (.*)").Groups[1].Value.Trim();
                string swka = swk + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
                byte[] swkaSha1 = System.Security.Cryptography.SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(swka));
                string swkaSha1Base64 = Convert.ToBase64String(swkaSha1);

                // HTTP/1.1 defines the sequence CR LF as the end-of-line marker
                byte[] response = Encoding.UTF8.GetBytes(
                    "HTTP/1.1 101 Switching Protocols\r\n" +
                    "Connection: Upgrade\r\n" +
                    "Upgrade: websocket\r\n" +
                    "Sec-WebSocket-Accept: " + swkaSha1Base64 + "\r\n\r\n");

                stream.Write(response, 0, response.Length);
            }
            else
            {
                bool fin = (bytes[0] & 0b10000000) != 0,
                    mask = (bytes[1] & 0b10000000) != 0; // must be true, "All messages from the client to the server have this bit set"

                int opcode = bytes[0] & 0b00001111, // expecting 1 - text message
                    msglen = bytes[1] - 128, // & 0111 1111
                    offset = 2;

                if (msglen == 126)
                {
                    // was ToUInt16(bytes, offset) but the result is incorrect
                    msglen = BitConverter.ToUInt16(new byte[] { bytes[3], bytes[2] }, 0);
                    offset = 4;
                }
                else if (msglen == 127)
                {
                    Debug.Log("TODO: msglen == 127, needs qword to store msglen");
                    // i don't really know the byte order, please edit this
                    // msglen = BitConverter.ToUInt64(new byte[] { bytes[5], bytes[4], bytes[3], bytes[2], bytes[9], bytes[8], bytes[7], bytes[6] }, 0);
                    // offset = 10;
                }

                if (msglen == 0)
                    Debug.Log("msglen == 0");
                else if (mask)
                {
                    byte[] decoded = new byte[msglen];
                    byte[] masks = new byte[4] { bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3] };
                    offset += 4;

                    for (int i = 0; i < msglen; ++i)
                        decoded[i] = (byte)(bytes[offset + i] ^ masks[i % 4]);

                    string text = Encoding.UTF8.GetString(decoded);
                    Debug.Log(text);
                }
                else
                    Debug.Log("mask bit not set");

                //Debug.Log();
                //StopSwitch = false;
            }
        }
        
        Console.WriteLine("server thread interrupted by main thread.");
        stream.Close();
        Debug.Log("stream closed...");
        client.Close();
        Debug.Log("TcpClient closed...");
        
        
                
    }
}

I thought about using an external static variable "stop" from the calling routine and set that to "true" and replace the WHILE clause with something like "while(!stop)", but the documentation warns against that so what is the solution?

Looking at the sample in Microsoft's doc about Thread.Interrupt.Method I saw how they communicated with the thread using a public variable and changed the code to use that..."server.stopSwitch". Looking at the cpu usage that seems to stop the thread, but the Unity editor gets stuck as well... so I have to terminate the task from the task manager :-/


Solution

  • You could use the Interlocked class to read and write thread save from an long variable.

    So somewhere define the variable long isRunning.

    Then at the start or before your schedule the server, you just the isRunning to 0

    ...
    isRunning = 0;
    ...
    jobHandle = server.Schedule();
    ...
    

    The server can the read from this variable to check if it should run

    while (Interlocked.Read(ref isRunning) == 0) { .. }
    

    To communicate that the server should stop use Interlocked.Increment

    Interlocked.Increment(ref isRunning);
    

    The server will then stop working after the current loop-iteration is finished.