赞
踩
Unity客户端常用的与服务器通信的方式有socket、http、webSocket。本文主要实现一个简单的WebSocket通信案例,包含客户端、服务器,实现了两端的通信以及客户端向服务器发送关闭连接请求的功能。实现上没有使用Unity相关插件,使用的就是.Net本身的WebSocket。
WebSocket是一种基于TCP的应用层网络协议,客户端与服务器经过一次 HTTP 握手,两者之间便可以建立持久性的连接,进而使得客户端与服务器之间能够进行双向实时通信(全双工通信)。PS:网上有更详细的信息,这里就不展开了。
代码分客户端代码、服务器代码。客户端为Unity客户端,服务器是VS控制台程序。首先运行服务器代码,之后再运行客户端代码,完成连接后,在客户端输入框中输入内容,之后点击“发送信息”按钮,向服务器发送信息,点击“断开连接”按钮,向服务器发送断开连接请求,在服务器命令行窗口内输入内容按下回车即可向客户端发送信息。
PS:先运行客户端再运行服务器也行,但客户端请求连接短时间内得不到回复便会抛出异常,手速得快。所以最好先运行服务器提前开启监听。
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(单例,不需要挂到游戏对象上)
-
- 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;
- }
- }
- }
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;
- }
- }
- }
通信、断开连接:
socket状态变化截图:
这里说一下客户端在发送断开连接请求时,客户端与服务器socket的状态变化,在代码中socket状态变化时都会有打印出来。
状态变化:客户端使用CloseAsync申请关闭,客户端socket转为CloseSent状态;服务器接收到请求后,服务器socket转为CloseReceived状态;服务器执行CloseOutputAsync确认关闭,自己转为Closed状态;客户端受到确认转为CloseReceived,经过一小段时间(非常短)转为Closed状态。
开始在找Unity WebSocket通信这方面资料时,发现大多数的方案都是使用插件,插件的确很方便,用起来也比较舒服,但我个人还是倾向于使用非插件的方法,所以就研究了下。所提供的代码只是实现了简单的通信与控制,演示了相关API,在具体到项目中时肯定还要根据需要进行修改补充。
列举目前代码中可优化的部分内容(想要真正应用到项目,那么要完善和考虑的东西非常多,这里只列举几个):
————————————————
版权声明:本文为CSDN博主「EucliwoodXT」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Davidorzs/article/details/131994649
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。