当前位置:   article > 正文

【转载】【Unity】WebSocket通信_unitywebsocket

unitywebsocket

1 前言


        Unity客户端常用的与服务器通信的方式有socket、http、webSocket。本文主要实现一个简单的WebSocket通信案例,包含客户端、服务器,实现了两端的通信以及客户端向服务器发送关闭连接请求的功能。实现上没有使用Unity相关插件,使用的就是.Net本身的WebSocket。

2 WebSocket简介


        WebSocket是一种基于TCP的应用层网络协议,客户端与服务器经过一次 HTTP 握手,两者之间便可以建立持久性的连接,进而使得客户端与服务器之间能够进行双向实时通信(全双工通信)。PS:网上有更详细的信息,这里就不展开了。

3 代码


        代码分客户端代码、服务器代码。客户端为Unity客户端,服务器是VS控制台程序。首先运行服务器代码,之后再运行客户端代码,完成连接后,在客户端输入框中输入内容,之后点击“发送信息”按钮,向服务器发送信息,点击“断开连接”按钮,向服务器发送断开连接请求,在服务器命令行窗口内输入内容按下回车即可向客户端发送信息。
        PS:先运行客户端再运行服务器也行,但客户端请求连接短时间内得不到回复便会抛出异常,手速得快。所以最好先运行服务器提前开启监听。

3.1 客户端代码


GameStart.cs

  1. using UnityEngine;
  2.  
  3. public class GameStart : MonoBehaviour
  4. {
  5.     //发送的消息变量
  6.     private string msg = null;
  7.  
  8.     void Start()
  9.     {
  10.         //连接服务器。
  11.         NetManager.M_Instance.Connect("ws://127.0.0.1:8888");   //本机地址
  12.     }
  13.  
  14.     //绘制UI
  15.     private void OnGUI()
  16.     {
  17.         //绘制输入框,以及获取输入框中的内容
  18.         //PS:第二参数必须是msg,否则在我们输入后,虽然msg可以获得到输入内容,但马上就被第二参数在下一帧重新覆盖。
  19.         msg = GUI.TextField(new Rect(10, 10, 100, 20), msg);
  20.  
  21.         //绘制按钮,以及按下发送信息按钮,发送信息
  22.         if (GUI.Button(new Rect(120, 10, 80, 20), "发送信息") && msg != null)
  23.         {
  24.             NetManager.M_Instance.Send(msg);
  25.         }
  26.  
  27.         //绘制按钮,以及按下断开连接按钮,发送断开连接请求
  28.         if (GUI.Button(new Rect(210, 10, 80, 20), "断开连接"))
  29.         {
  30.             Debug.Log("向服务器请求断开连接......");
  31.             NetManager.M_Instance.CloseClientWebSocket();
  32.         }
  33.         
  34.     }
  35. }


NetManager.cs(单例,不需要挂到游戏对象上)

  1. using System;
  2. using System.Net.WebSockets;
  3. using System.Text;
  4. using System.Threading;
  5. using UnityEngine;
  6.  
  7. public class NetManager
  8. {
  9.     #region 实现单例的代码
  10.     //变量
  11.     private volatile static NetManager m_instance;          //单例本身。使用volatile关键字修饰,禁止优化,确保多线程访问时访问到的数据都是最新的
  12.     private static object m_locker = new object();          //线程锁。当多线程访问时,同一时刻仅允许一个线程访问
  13.  
  14.     //属性
  15.     public static NetManager M_Instance
  16.     {
  17.         get
  18.         {
  19.             //线程锁。防止同时判断为null时同时创建对象
  20.             lock (m_locker)
  21.             {
  22.                 //如果不存在对象则创建对象
  23.                 if (m_instance == null)
  24.                 {
  25.                     m_instance = new NetManager();
  26.                 }
  27.             }
  28.             return m_instance;
  29.         }
  30.     }
  31.     #endregion
  32.  
  33.     //私有化构造
  34.     private NetManager() { }
  35.  
  36.     //客户端webSocket
  37.     private ClientWebSocket m_clientWebSocket;
  38.     //处理接收数据的线程
  39.     private Thread m_dataReceiveThread;
  40.     //线程持续执行的标识符
  41.     private bool m_isDoThread;
  42.  
  43.  
  44.     /// <summary>
  45.     /// ClientWebSocket,与服务器建立连接。
  46.     /// </summary>
  47.     /// <param name="uriStr"></param>
  48.     public void Connect(string uriStr)
  49.     {
  50.         try
  51.         {
  52.             //创建ClientWebSocket
  53.             m_clientWebSocket = new ClientWebSocket();
  54.  
  55.             //初始化标识符
  56.             m_isDoThread = true;
  57.  
  58.             //创建线程
  59.             m_dataReceiveThread = new Thread(ReceiveData);  //创建数据接收线程  
  60.             m_dataReceiveThread.IsBackground = true;        //设置为后台可以运行,主线程关闭时,此线程也会关闭(实际在Unity中并没什么用,还是要手动关闭)
  61.  
  62.             //设置请求头部
  63.             //m_clientWebSocket.Options.SetRequestHeader("headerName", "hearValue");
  64.  
  65.             //开始连接
  66.             var task = m_clientWebSocket.ConnectAsync(new Uri(uriStr), CancellationToken.None);
  67.             task.Wait();    //等待
  68.  
  69.             //启动数据接收线程
  70.             m_dataReceiveThread.Start(m_clientWebSocket);
  71.  
  72.             //输出提示
  73.             if (m_clientWebSocket.State == WebSocketState.Open)
  74.             {
  75.                 Debug.Log("连接服务器完毕。");
  76.             }
  77.         }
  78.         catch (WebSocketException ex)
  79.         {
  80.             Debug.LogError("连接出错:" + ex.Message);
  81.             Debug.LogError("WebSokcet状态:" + m_clientWebSocket.State);
  82.             //关闭连接
  83.             //函数内可能需要考虑WebSokcet状态不是WebSocketState.Open时如何关闭连接的情况。目前没有处理这种情况。
  84.             //比如正在连接时出现了异常,当前状态还是Connecting状态,那么该如何停止呢?
  85.             //虽然我有了解到ClientWebSocket包含的Abort()、Dispose()方法,但并未出现过这种异常情况所以也没继续深入下去,放在这里当个参考吧。
  86.             CloseClientWebSocket();
  87.         }
  88.  
  89.     }
  90.  
  91.     /// <summary>
  92.     /// 持续接收服务器的信息。
  93.     /// </summary>
  94.     /// <param name="socket"></param>
  95.     private void ReceiveData(object socket)
  96.     {
  97.         //类型转换
  98.         ClientWebSocket socketClient = (ClientWebSocket)socket;
  99.         //持续接收信息
  100.         while (m_isDoThread)
  101.         {
  102.             //接收数据
  103.             string data = Receive(socketClient);
  104.             //数据处理(可以和服务器一样使用事件(委托)来处理)
  105.             if (data != null)
  106.             {
  107.                 Debug.Log("接收的服务器消息:" + data);
  108.             }
  109.         }
  110.         Debug.Log("接收信息线程结束。");
  111.     }
  112.  
  113.     /// <summary>
  114.     /// 接收服务器信息。
  115.     /// </summary>
  116.     /// <param name="socket"></param>
  117.     /// <returns></returns>
  118.     private string Receive(ClientWebSocket socket)
  119.     {
  120.         try
  121.         {
  122.             //接收消息时,对WebSocketState是有要求的,所以加上if判断(如果不是这两种状态,会报出异常)
  123.             if (socket != null && (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent))
  124.             {
  125.                 byte[] arrry = new byte[1024];  //注意长度,如果服务器发送消息过长,这也需要跟着调整
  126.                 ArraySegment<byte> buffer = new ArraySegment<byte>(arrry);  //实例化一个ArraySegment结构体
  127.                 //接收数据
  128.                 var task = socket.ReceiveAsync(buffer, CancellationToken.None);
  129.                 task.Wait();//等待
  130.  
  131.                 //仅作状态展示。在客户端发送关闭消息后,服务器会回复确认信息,在收到确认信息后状态便是CloseReceived,这里打印输出。
  132.                 Debug.Log("socekt当前状态:" + socket.State);
  133.  
  134.                 //如果是结束消息确认,则返回null,不再解析信息
  135.                 if (socket.State == WebSocketState.CloseReceived || task.Result.MessageType == WebSocketMessageType.Close)
  136.                 {
  137.                     return null;
  138.                 }
  139.                 //将接收数据转为string类型,并返回。注意只解析我们接收到的字节数目(task.Result.Count)
  140.                 return Encoding.UTF8.GetString(buffer.Array, 0, task.Result.Count);
  141.             }
  142.             else
  143.             {
  144.                 return null;
  145.             }
  146.         }
  147.         catch (WebSocketException ex)
  148.         {
  149.             Debug.LogError("接收服务器信息错误:" + ex.Message);
  150.             CloseClientWebSocket();
  151.             return null;
  152.         }
  153.     }
  154.  
  155.     /// <summary>
  156.     /// 发送消息
  157.     /// </summary>
  158.     /// <param name="content"></param>
  159.     public void Send(string content)
  160.     {
  161.         try
  162.         {
  163.             //发送消息时,对WebSocketState是有要求的,加上if判断(如果不是这两种状态,会报出异常)
  164.             if (m_clientWebSocket != null && (m_clientWebSocket.State == WebSocketState.Open || m_clientWebSocket.State == WebSocketState.CloseReceived))
  165.             {
  166.                 ArraySegment<byte> array = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content)); //创建内容的字节编码数组并实例化一个ArraySegment结构体
  167.                 var task = m_clientWebSocket.SendAsync(array, WebSocketMessageType.Binary, true, CancellationToken.None);  //发送
  168.                 task.Wait();  //等待
  169.  
  170.                 Debug.Log("发送了一个消息到服务器。");
  171.             }
  172.         }
  173.         catch (WebSocketException ex)
  174.         {
  175.             Debug.LogError("向服务器发送信息错误:" + ex.Message);
  176.             CloseClientWebSocket();
  177.         }
  178.     }
  179.  
  180.     /// <summary>
  181.     /// 关闭ClientWebSocket。
  182.     /// </summary>
  183.     public void CloseClientWebSocket()
  184.     {
  185.         //关闭Socket
  186.         if (m_clientWebSocket != null && m_clientWebSocket.State == WebSocketState.Open)
  187.         {
  188.             var task = m_clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
  189.             Debug.Log("如果打印过快,↓下面↓这个socket状态可能为Open,出现Open就多试几次,我们想看的是CloseSent状态。");
  190.             Debug.Log("socekt当前状态:" + m_clientWebSocket.State);
  191.             task.Wait();
  192.             Debug.Log("socekt当前状态:" + m_clientWebSocket.State);
  193.             Debug.Log("连接已断开。");
  194.         }
  195.         //关闭线程
  196.         if (m_dataReceiveThread != null && m_dataReceiveThread.IsAlive)
  197.         {
  198.             m_isDoThread = false;   //别想Abort()了,unity中的线程关闭建议使用bool来控制线程结束。
  199.             m_dataReceiveThread = null;
  200.         }
  201.     }
  202. }


 3.2 服务器代码


Program.cs

  1. using System;
  2. using System.Threading.Tasks;
  3.  
  4. internal class Program
  5. {
  6.     //创建一个WebSocketService
  7.     private static WebSocketService m_serviceSocket;
  8.     static void Main(string[] args)
  9.     {
  10.         //开启后台线程,监听客户端连接
  11.         Task.Run(() =>
  12.         {
  13.             m_serviceSocket = new WebSocketService();           //实例化一个WebSocketService
  14.             m_serviceSocket.m_DataReceive += HandleDataRecive;    //监听消息事件,处理函数,当有接收到客户端消息时会调用此处理函数来处理
  15.             m_serviceSocket.Listening();                        //开始监听
  16.         });
  17.  
  18.         //持续接收键盘输入,为了能多次向客户端发消息,同时起到不关闭控制台程序的作用
  19.         while (true)
  20.         {
  21.             //输入内容,发送消息到客户端
  22.             string msg = Console.ReadLine();
  23.             m_serviceSocket.Send(msg);
  24.         }
  25.     }
  26.  
  27.     /// <summary>
  28.     /// 消息事件处理函数
  29.     /// </summary>
  30.     /// <param name="sender"></param>
  31.     /// <param name="e"></param>
  32.     private static void HandleDataRecive(object sender, string e)
  33.     {
  34.         Console.WriteLine("接收的客户端消息:" + e);
  35.     }
  36. }

WebSocketService.cs 

  1. using System;
  2. using System.Net;
  3. using System.Net.WebSockets;
  4. using System.Text;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7.  
  8. internal class WebSocketService
  9. {
  10.     HttpListener m_httpListener;                //监听者
  11.     private WebSocket m_webSocket;              //socket
  12.     public event EventHandler<string> m_DataReceive;  //事件(委托),消息处理函数添加到这里。
  13.     private bool m_isDoThread;              //线程持续执行标识符
  14.  
  15.     public void Listening()
  16.     {
  17.         Console.WriteLine("正在监听...");
  18.         //监听Ip、端口
  19.         m_httpListener = new HttpListener();
  20.         m_httpListener.Prefixes.Add("http://127.0.0.1:8888/");  //监听本机地址
  21.         m_httpListener.Start();
  22.         var httpListenContext = m_httpListener.GetContext();    //这里就等待客户端连接了。
  23.         var webSocketContext = httpListenContext.AcceptWebSocketAsync(null);
  24.         m_webSocket = webSocketContext.Result.WebSocket;
  25.         //初始化标识符
  26.         m_isDoThread = true;
  27.         //开启后台线程,持续接收客户端消息
  28.         Task.Run(() =>
  29.         {
  30.             while (m_isDoThread)
  31.             {
  32.                 //接收消息
  33.                 string msg = Receive(m_webSocket);
  34.                 if (msg != null)
  35.                 {
  36.                     m_DataReceive?.Invoke(m_webSocket, msg);  //数据处理
  37.                 }
  38.             }
  39.             Console.WriteLine("接收信息线程结束。");
  40.         });
  41.         Console.WriteLine("连接建立成功!");
  42.     }
  43.  
  44.     /// <summary>
  45.     /// 发送信息
  46.     /// </summary>
  47.     /// <param name="content">发送的内容</param>
  48.     public void Send(string content)
  49.     {
  50.         //同客户端,WebSocketState要求
  51.         if (m_webSocket != null && (m_webSocket.State == WebSocketState.Open || m_webSocket.State == WebSocketState.CloseReceived))
  52.         {
  53.             ArraySegment<byte> array = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content)); //创建数组,并存储发送内容字节编码
  54.             var task = m_webSocket.SendAsync(array, WebSocketMessageType.Binary, true, CancellationToken.None);  //发送   
  55.             task.Wait();          //等待
  56.             Console.WriteLine("发送了一个消息到客户端。");
  57.         }
  58.     }
  59.  
  60.     /// <summary>
  61.     /// 接收信息
  62.     /// </summary>
  63.     /// <param name="webSocket"></param>
  64.     /// <returns></returns>
  65.     private string Receive(WebSocket webSocket)
  66.     {
  67.         //同客户端,WebSocketState要求
  68.         if (webSocket != null && (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseSent))
  69.         {
  70.             //接收消息
  71.             byte[] arrry = new byte[1024];  //大小根据情况调整
  72.             ArraySegment<byte> buffer = new ArraySegment<byte>(arrry);
  73.             var task = webSocket.ReceiveAsync(buffer, CancellationToken.None);
  74.             task.Wait();
  75.  
  76.             Console.WriteLine("当前socket状态:" + webSocket.State);
  77.             //当收到关闭连接的请求时(关闭确认)
  78.             if (webSocket.State == WebSocketState.CloseReceived || task.Result.MessageType == WebSocketMessageType.Close)
  79.             {
  80.                 webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None);//关闭确认
  81.                 Console.WriteLine("当前socket状态:" + webSocket.State);
  82.                 Console.WriteLine("连接已断开。");
  83.                 //关闭线程
  84.                 m_isDoThread = false;
  85.  
  86.                 return null;
  87.             }
  88.             //将接收数据转为string类型,并返回。注意只解析我们接收到的字节数目(task.Result.Count)
  89.             return Encoding.UTF8.GetString(buffer.Array, 0, task.Result.Count);
  90.         }
  91.         else
  92.         {
  93.             return null;
  94.         }
  95.     }
  96. }


4 演示


通信、断开连接: 

socket状态变化截图:

5 webSocket状态变化


        这里说一下客户端在发送断开连接请求时,客户端与服务器socket的状态变化,在代码中socket状态变化时都会有打印出来。
        状态变化:客户端使用CloseAsync申请关闭,客户端socket转为CloseSent状态;服务器接收到请求后,服务器socket转为CloseReceived状态;服务器执行CloseOutputAsync确认关闭,自己转为Closed状态;客户端受到确认转为CloseReceived,经过一小段时间(非常短)转为Closed状态。

6 结束语


        开始在找Unity WebSocket通信这方面资料时,发现大多数的方案都是使用插件,插件的确很方便,用起来也比较舒服,但我个人还是倾向于使用非插件的方法,所以就研究了下。所提供的代码只是实现了简单的通信与控制,演示了相关API,在具体到项目中时肯定还要根据需要进行修改补充。

        列举目前代码中可优化的部分内容(想要真正应用到项目,那么要完善和考虑的东西非常多,这里只列举几个):

  1. 发送和关闭方法处于主线程中(异常抛出时,关闭方法在非主线程中执行,直接调用在主线程),当task.Wait()时便会阻塞主线程,当时间等待过长时就会出现卡死主线程的情况,所以可以考虑创建两个新线程来处理发送、关闭,就像接收消息线程一样,发送线程负责整个客户端的消息发送,关闭线程负责客户端断开连接请求,避免可能出现的卡死主线程的情况。
  2. 在CloseClientWebSocket()方法中还应该考虑到连接到一半时出错该如何关闭socket的处理情况,但目前并未遇到这种情况,所以也没有针对这种情况进行处理。我是在查阅资料时看到有人出现过这种错误,但后来再找那篇文章时找不到了.......所以目前就先这样吧,以后有机会再更新。更具体的内容在代码注释中有说明。
  3. 关于catch抛出的异常的问题,在代码中我catch的是WebSocketException,但到具体项目中要根据情况再修改,如服务器不监听的情况下客户端连接超时时会抛出异常,但使用WebSocketException我们就catch不到抛出的异常,使用Exception才可以catch到,那如果我们想捕获此异常并提示连接超时之类信息时就需要将WebSocketException修改为Exception了。
  4. 使用事件(委托)来处理接收到的消息时,最好单独开个线程来处理消息,接收消息的线程只负责接收并存储消息,而处理则由另一个线程负责(也可以多个线程处理),当然,也可以直接使用主线程来处理消息。这样做是为了减少接收线程的任务量,让其可以更专注于接收与存储,及时接收数据。
  5. 代码中接收数据每次都会new一个新数组,接收数据不频繁时无所谓,但如果过于频繁,最好把数组放到外面只new一个,之后每次接收之前Clear一下,以Clear替换new操作,这样性能上会好些。
  6. 断线重连。
  7. 心跳包。

————————————————
版权声明:本文为CSDN博主「EucliwoodXT」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Davidorzs/article/details/131994649

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/898905
推荐阅读
相关标签
  

闽ICP备14008679号