[Unity] WebSocket communication

1 Introduction

        Commonly used methods for the Unity client to communicate with the server include socket, http, and webSocket. This article mainly implements a simple WebSocket communication case, including the client and the server, realizing the communication between the two ends and the function of the client sending a close connection request to the server. No Unity-related plug-ins are used in the implementation, but .Net's own WebSocket is used.

2 Introduction to WebSocket

        WebSocket is an application layer network protocol based on TCP. After an HTTP handshake between the client and the server, a persistent connection can be established between the two, thereby enabling two-way real-time communication (full duplex) between the client and the server. communication). PS: There is more detailed information online, so I won’t go into it here.

3 code

        The code is divided into client code and server code. The client is the Unity client, and the server is the VS console program. First run the server code, and then run the client code. After completing the connection, enter content in the client input box, then click the "Send Information" button to send information to the server, and click the "Disconnect" button to send a disconnection to the server. Open a connection request, enter the content in the server command line window and press Enter to send information to the client.
        PS: It is okay to run the client first and then the server, but if the client requests a connection and does not receive a reply within a short period of time, an exception will be thrown, so you need to be quick. Therefore, it is best to run the server first and enable monitoring in advance.

3.1 Client code

GameStart.cs

using UnityEngine;

public class GameStart : MonoBehaviour
{
    //发送的消息变量
    private string msg = null;

    void Start()
    {
        //连接服务器。
        NetManager.M_Instance.Connect("ws://127.0.0.1:8888");   //本机地址
    }

    //绘制UI
    private void OnGUI()
    {
        //绘制输入框,以及获取输入框中的内容
        //PS:第二参数必须是msg,否则在我们输入后,虽然msg可以获得到输入内容,但马上就被第二参数在下一帧重新覆盖。
        msg = GUI.TextField(new Rect(10, 10, 100, 20), msg);

        //绘制按钮,以及按下发送信息按钮,发送信息
        if (GUI.Button(new Rect(120, 10, 80, 20), "发送信息") && msg != null)
        {
            NetManager.M_Instance.Send(msg);
        }

        //绘制按钮,以及按下断开连接按钮,发送断开连接请求
        if (GUI.Button(new Rect(210, 10, 80, 20), "断开连接"))
        {
            Debug.Log("向服务器请求断开连接......");
            NetManager.M_Instance.CloseClientWebSocket();
        }
        
    }
}

NetManager.cs (single case, no need to hang it on the game object)

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using UnityEngine;

public class NetManager
{
    #region 实现单例的代码
    //变量
    private volatile static NetManager m_instance;          //单例本身。使用volatile关键字修饰,禁止优化,确保多线程访问时访问到的数据都是最新的
    private static object m_locker = new object();          //线程锁。当多线程访问时,同一时刻仅允许一个线程访问

    //属性
    public static NetManager M_Instance
    {
        get
        {
            //线程锁。防止同时判断为null时同时创建对象
            lock (m_locker)
            {
                //如果不存在对象则创建对象
                if (m_instance == null)
                {
                    m_instance = new NetManager();
                }
            }
            return m_instance;
        }
    }
    #endregion

    //私有化构造
    private NetManager() { }

    //客户端webSocket
    private ClientWebSocket m_clientWebSocket;
    //处理接收数据的线程
    private Thread m_dataReceiveThread;
    //线程持续执行的标识符
    private bool m_isDoThread;


    /// <summary>
    /// ClientWebSocket,与服务器建立连接。
    /// </summary>
    /// <param name="uriStr"></param>
    public void Connect(string uriStr)
    {
        try
        {
            //创建ClientWebSocket
            m_clientWebSocket = new ClientWebSocket();

            //初始化标识符
            m_isDoThread = true;

            //创建线程
            m_dataReceiveThread = new Thread(ReceiveData);  //创建数据接收线程  
            m_dataReceiveThread.IsBackground = true;        //设置为后台可以运行,主线程关闭时,此线程也会关闭(实际在Unity中并没什么用,还是要手动关闭)

            //设置请求头部
            //m_clientWebSocket.Options.SetRequestHeader("headerName", "hearValue");

            //开始连接
            var task = m_clientWebSocket.ConnectAsync(new Uri(uriStr), CancellationToken.None);
            task.Wait();    //等待

            //启动数据接收线程
            m_dataReceiveThread.Start(m_clientWebSocket);

            //输出提示
            if (m_clientWebSocket.State == WebSocketState.Open)
            {
                Debug.Log("连接服务器完毕。");
            }
        }
        catch (WebSocketException ex)
        {
            Debug.LogError("连接出错:" + ex.Message);
            Debug.LogError("WebSokcet状态:" + m_clientWebSocket.State);
            //关闭连接
            //函数内可能需要考虑WebSokcet状态不是WebSocketState.Open时如何关闭连接的情况。目前没有处理这种情况。
            //比如正在连接时出现了异常,当前状态还是Connecting状态,那么该如何停止呢?
            //虽然我有了解到ClientWebSocket包含的Abort()、Dispose()方法,但并未出现过这种异常情况所以也没继续深入下去,放在这里当个参考吧。
            CloseClientWebSocket();
        }

    }

    /// <summary>
    /// 持续接收服务器的信息。
    /// </summary>
    /// <param name="socket"></param>
    private void ReceiveData(object socket)
    {
        //类型转换
        ClientWebSocket socketClient = (ClientWebSocket)socket;
        //持续接收信息
        while (m_isDoThread)
        {
            //接收数据
            string data = Receive(socketClient);
            //数据处理(可以和服务器一样使用事件(委托)来处理)
            if (data != null)
            {
                Debug.Log("接收的服务器消息:" + data);
            }
        }
        Debug.Log("接收信息线程结束。");
    }

    /// <summary>
    /// 接收服务器信息。
    /// </summary>
    /// <param name="socket"></param>
    /// <returns></returns>
    private string Receive(ClientWebSocket socket)
    {
        try
        {
            //接收消息时,对WebSocketState是有要求的,所以加上if判断(如果不是这两种状态,会报出异常)
            if (socket != null && (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent))
            {
                byte[] arrry = new byte[1024];  //注意长度,如果服务器发送消息过长,这也需要跟着调整
                ArraySegment<byte> buffer = new ArraySegment<byte>(arrry);  //实例化一个ArraySegment结构体
                //接收数据
                var task = socket.ReceiveAsync(buffer, CancellationToken.None);
                task.Wait();//等待

                //仅作状态展示。在客户端发送关闭消息后,服务器会回复确认信息,在收到确认信息后状态便是CloseReceived,这里打印输出。
                Debug.Log("socekt当前状态:" + socket.State);

                //如果是结束消息确认,则返回null,不再解析信息
                if (socket.State == WebSocketState.CloseReceived || task.Result.MessageType == WebSocketMessageType.Close)
                {
                    return null;
                }
                //将接收数据转为string类型,并返回。注意只解析我们接收到的字节数目(task.Result.Count)
                return Encoding.UTF8.GetString(buffer.Array, 0, task.Result.Count);
            }
            else
            {
                return null;
            }
        }
        catch (WebSocketException ex)
        {
            Debug.LogError("接收服务器信息错误:" + ex.Message);
            CloseClientWebSocket();
            return null;
        }
    }

    /// <summary>
    /// 发送消息
    /// </summary>
    /// <param name="content"></param>
    public void Send(string content)
    {
        try
        {
            //发送消息时,对WebSocketState是有要求的,加上if判断(如果不是这两种状态,会报出异常)
            if (m_clientWebSocket != null && (m_clientWebSocket.State == WebSocketState.Open || m_clientWebSocket.State == WebSocketState.CloseReceived))
            {
                ArraySegment<byte> array = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content)); //创建内容的字节编码数组并实例化一个ArraySegment结构体
                var task = m_clientWebSocket.SendAsync(array, WebSocketMessageType.Binary, true, CancellationToken.None);  //发送
                task.Wait();  //等待

                Debug.Log("发送了一个消息到服务器。");
            }
        }
        catch (WebSocketException ex)
        {
            Debug.LogError("向服务器发送信息错误:" + ex.Message);
            CloseClientWebSocket();
        }
    }

    /// <summary>
    /// 关闭ClientWebSocket。
    /// </summary>
    public void CloseClientWebSocket()
    {
        //关闭Socket
        if (m_clientWebSocket != null && m_clientWebSocket.State == WebSocketState.Open)
        {
            var task = m_clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
            Debug.Log("如果打印过快,↓下面↓这个socket状态可能为Open,出现Open就多试几次,我们想看的是CloseSent状态。");
            Debug.Log("socekt当前状态:" + m_clientWebSocket.State);
            task.Wait();
            Debug.Log("socekt当前状态:" + m_clientWebSocket.State);
            Debug.Log("连接已断开。");
        }
        //关闭线程
        if (m_dataReceiveThread != null && m_dataReceiveThread.IsAlive)
        {
            m_isDoThread = false;   //别想Abort()了,unity中的线程关闭建议使用bool来控制线程结束。
            m_dataReceiveThread = null;
        }
    }
}

 3.2 Server code

Program.cs

using System;
using System.Threading.Tasks;

internal class Program
{
    //创建一个WebSocketService
    private static WebSocketService m_serviceSocket;
    static void Main(string[] args)
    {
        //开启后台线程,监听客户端连接
        Task.Run(() =>
        {
            m_serviceSocket = new WebSocketService();           //实例化一个WebSocketService
            m_serviceSocket.m_DataReceive += HandleDataRecive;    //监听消息事件,处理函数,当有接收到客户端消息时会调用此处理函数来处理
            m_serviceSocket.Listening();                        //开始监听
        });

        //持续接收键盘输入,为了能多次向客户端发消息,同时起到不关闭控制台程序的作用
        while (true)
        {
            //输入内容,发送消息到客户端
            string msg = Console.ReadLine();
            m_serviceSocket.Send(msg);
        }
    }

    /// <summary>
    /// 消息事件处理函数
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private static void HandleDataRecive(object sender, string e)
    {
        Console.WriteLine("接收的客户端消息:" + e);
    }
}

WebSocketService.cs

using System;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

internal class WebSocketService
{
    HttpListener m_httpListener;                //监听者
    private WebSocket m_webSocket;              //socket
    public event EventHandler<string> m_DataReceive;  //事件(委托),消息处理函数添加到这里。
    private bool m_isDoThread;              //线程持续执行标识符

    public void Listening()
    {
        Console.WriteLine("正在监听...");
        //监听Ip、端口
        m_httpListener = new HttpListener();
        m_httpListener.Prefixes.Add("http://127.0.0.1:8888/");  //监听本机地址
        m_httpListener.Start();
        var httpListenContext = m_httpListener.GetContext();    //这里就等待客户端连接了。
        var webSocketContext = httpListenContext.AcceptWebSocketAsync(null);
        m_webSocket = webSocketContext.Result.WebSocket;
        //初始化标识符
        m_isDoThread = true;
        //开启后台线程,持续接收客户端消息
        Task.Run(() =>
        {
            while (m_isDoThread)
            {
                //接收消息
                string msg = Receive(m_webSocket);
                if (msg != null)
                {
                    m_DataReceive?.Invoke(m_webSocket, msg);  //数据处理
                }
            }
            Console.WriteLine("接收信息线程结束。");
        });
        Console.WriteLine("连接建立成功!");
    }

    /// <summary>
    /// 发送信息
    /// </summary>
    /// <param name="content">发送的内容</param>
    public void Send(string content)
    {
        //同客户端,WebSocketState要求
        if (m_webSocket != null && (m_webSocket.State == WebSocketState.Open || m_webSocket.State == WebSocketState.CloseReceived))
        {
            ArraySegment<byte> array = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content)); //创建数组,并存储发送内容字节编码
            var task = m_webSocket.SendAsync(array, WebSocketMessageType.Binary, true, CancellationToken.None);  //发送   
            task.Wait();          //等待
            Console.WriteLine("发送了一个消息到客户端。");
        }
    }

    /// <summary>
    /// 接收信息
    /// </summary>
    /// <param name="webSocket"></param>
    /// <returns></returns>
    private string Receive(WebSocket webSocket)
    {
        //同客户端,WebSocketState要求
        if (webSocket != null && (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseSent))
        {
            //接收消息
            byte[] arrry = new byte[1024];  //大小根据情况调整
            ArraySegment<byte> buffer = new ArraySegment<byte>(arrry);
            var task = webSocket.ReceiveAsync(buffer, CancellationToken.None);
            task.Wait();

            Console.WriteLine("当前socket状态:" + webSocket.State);
            //当收到关闭连接的请求时(关闭确认)
            if (webSocket.State == WebSocketState.CloseReceived || task.Result.MessageType == WebSocketMessageType.Close)
            {
                webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None);//关闭确认
                Console.WriteLine("当前socket状态:" + webSocket.State);
                Console.WriteLine("连接已断开。");
                //关闭线程
                m_isDoThread = false;

                return null;
            }
            //将接收数据转为string类型,并返回。注意只解析我们接收到的字节数目(task.Result.Count)
            return Encoding.UTF8.GetString(buffer.Array, 0, task.Result.Count);
        }
        else
        {
            return null;
        }
    }
}

4 demo

Communication, disconnection: 

Screenshot of socket status changes:

5 webSocket status changes

        Let's talk about the status changes of the client and server sockets when the client sends a disconnection request. When the socket status changes in the code, it will be printed out.
        State change: The client uses CloseAsync to apply for closure, and the client socket changes to the CloseSent state; after the server receives the request, the server socket changes to the CloseReceived state; the server executes CloseOutputAsync to confirm the closure, and changes to the Closed state; the client changes to CloseReceived after receiving the confirmation , after a short period of time (very short), it changes to the Closed state.

6 Conclusion

        When I started looking for information on Unity WebSocket communication, I found that most solutions use plug-ins. Plug-ins are indeed very convenient and comfortable to use, but I personally prefer to use non-plug-in methods, so I did some research. The code provided only implements simple communication and control and demonstrates the relevant API. It will definitely need to be modified and supplemented as needed when it is implemented in the project.

        List some of the things that can be optimized in the current code (if you want to really apply it to the project, there are a lot of things that need to be improved and considered, here are just a few):

  1. The sending and closing methods are in the main thread (when an exception is thrown, the closing method is executed in a non-main thread and directly called on the main thread). When task.Wait() is used, the main thread will be blocked. When the waiting time is too long, the sending and closing methods will be blocked. The main thread may get stuck, so you can consider creating two new threads to handle sending and closing. Just like the message receiving thread, the sending thread is responsible for sending messages to the entire client, and the closing thread is responsible for the client disconnect request to avoid The main thread may be stuck.
  2. In the CloseClientWebSocket() method, we should also consider how to close the socket when an error occurs when the connection is halfway through. However, we have not encountered this situation so far, so we have not dealt with this situation. I saw someone had this error when I was checking the information, but I couldn't find it when I looked for the article later... So I'll leave it at that for now and update it when I have the opportunity. More specific content is explained in the code comments.
  3. Regarding the issue of catching exceptions thrown, in the code I catch WebSocketException, but in specific projects, it needs to be modified according to the situation. For example, if the server is not listening, an exception will be thrown when the client connection times out, but using WebSocketException we If you cannot catch the exception thrown, you can only catch it using Exception. If we want to catch this exception and prompt information such as connection timeout, we need to change WebSocketException to Exception.
  4. When using events (delegate) to process received messages, it is best to open a separate thread to process the message. The thread receiving the message is only responsible for receiving and storing the message, while the processing is handled by another thread (multiple threads can also handle it) , of course, you can also use the main thread directly to process messages. This is done to reduce the task load of the receiving thread, so that it can focus more on receiving and storing, and receive data in a timely manner.
  5. Each time the data is received in the code, a new array will be created. It does not matter if the data is received infrequently, but if it is too frequent, it is best to put the array outside and only create one new array. Then, Clear it before each reception, and replace the new operation with Clear. This way Performance will be better.
  6. Disconnect and reconnect.
  7. Heartbeat package.

Guess you like

Origin blog.csdn.net/Davidorzs/article/details/131994649