【转载】【Unity】WebSocket通信

1 前言


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

2 WebSocket简介


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

3 代码


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

3.1 客户端代码


GameStart.cs

using UnityEngine;public class GameStart : MonoBehaviour
{//发送的消息变量private string msg = null;void Start(){//连接服务器。NetManager.M_Instance.Connect("ws://127.0.0.1:8888");   //本机地址}//绘制UIprivate 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() { }//客户端webSocketprivate ClientWebSocket m_clientWebSocket;//处理接收数据的线程private Thread m_dataReceiveThread;//线程持续执行的标识符private bool m_isDoThread;/// <summary>/// ClientWebSocket,与服务器建立连接。/// </summary>/// <param name="uriStr"></param>public void Connect(string uriStr){try{//创建ClientWebSocketm_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(){//关闭Socketif (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 服务器代码


Program.cs


using System;
using System.Threading.Tasks;internal class Program
{//创建一个WebSocketServiceprivate static WebSocketService m_serviceSocket;static void Main(string[] args){//开启后台线程,监听客户端连接Task.Run(() =>{m_serviceSocket = new WebSocketService();           //实例化一个WebSocketServicem_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;              //socketpublic 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 演示


通信、断开连接: 

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/232617.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【QT】解决QTableView鼠标点击合并单元格高亮显示问题

问题 正常显示 鼠标单击时只会选中当前行 异常显示 解决方案 在操作之前清除单元格合并&#xff0c;然后再合并单元格&#xff1b; 合并单元格是好的&#xff0c;说明合并没有问题&#xff0c;所以最简单的办法就是将单元格的状态恢复到初始的时候&#xff0c;在进行操作&…

bottom-up-attention-vqa-master 成功复现!!!

代码地址 1、create_dictionary.py 建立词典和使用预训练的glove向量 &#xff08;1&#xff09;create_dictionary() 遍历每个question文件取出所关注的question部分&#xff0c;qs 遍历qs&#xff0c;对每个问题的文本内容进行分词&#xff0c;并将分词结果添加到字典中&…

LabVIEW开发自动驾驶的双目测距系统

LabVIEW开发自动驾驶的双目测距系统 随着车辆驾驶技术的不断发展&#xff0c;自动驾驶技术正日益成为现实。从L2级别的辅助驾驶技术到L3级别的受条件约束的自动驾驶技术&#xff0c;车辆安全性和智能化水平正在不断提升。在这个过程中&#xff0c;车辆主动安全预警系统发挥着关…

WebMvcConfigurer接口详解及使用方式(Spring-WebMvc)

简介 如下图所示WebMvcConfigurer是spring-webmvc jar包下的一个接口&#xff0c;spring-webmvc jar包又来源于spring-boot-starter-web&#xff0c;所以要使用WebMvcConfigurer要引入spring-boot-starter-web依赖。WebMvcConfigurer接口提供了常用的web应用拦截方法。通过实现…

Lazada物流禁运规则是什么?Lazada物流禁运商品有哪些?——站斧浏览器

Lazada物流禁运规则 Lazada平台对物流禁运商品的规则主要有以下几个方面&#xff1a; 1.卖家在上传商品时需要填写正确的商品信息&#xff0c;包括商品名称、描述、图片等。如果卖家填写不准确或者存在虚假信息&#xff0c;将会导致物流禁运商品的误判。 2.卖家需要遵守平台…

H5网页流媒体播放器EasyPlayer播放H.265录像时,无法倍速回放是什么原因?

流媒体播放器EasyPlayer是我们流媒体组件系列中关注度较高的产品&#xff0c;经过多年的发展和迭代&#xff0c;目前已经有多个应用版本&#xff0c;包括RTSP版、RTMP版、Pro版&#xff0c;以及js版&#xff0c;其中js版本作为网页播放器&#xff0c;受到了用户的广泛使用。在功…

Mac M系列安装配置VSCode

一、终端输入 安装command line tools xcode-select --install 这里是已经下载了 如果没有下载点击安装&#xff0c;等待安装完成即可 检验是否安装成功&#xff0c;终端输入 clang 如图所示是代表之前的command line tools安装 是安装成功的&#xff08;Clang会不断更新…

spring boot版本升级遇到的一些问题

背景&#xff1a;由于项目需求&#xff0c;需要将nacos 1.4.6版本升级到2.x版本&#xff0c;由此引发的springboot、springcloud、springcloud Alibaba一系列版本变更。 旧版本分别为&#xff1a; Spring Boot 2.3.5.RELEASE Spring Cloud Hoxton.SR9 Spring Cloud Alibaba 2.2…

数据库性能测试报告总结模板

1计划概述 目的&#xff1a;找出系统潜在的性能缺陷 目标&#xff1a;从安全&#xff0c;可靠&#xff0c;稳定的角度出发&#xff0c;找出性能缺陷&#xff0c;并且找出系统最佳承受并发用户数&#xff0c;以及并发用户数下长时间运行的负载情况&#xff0c;如要并发100用户&a…

集合工具类Collections

概述 java.utils.Collections:是集合工具类 作用&#xff1a;Collections并不属于集合&#xff0c;是用来操作集合的工具类。 Collections常用的API Collections排序相关API 使用范围&#xff1a;只能对于List集合的排序。 排序方式1&#xff1a; 注意&#xff1a;本方式…

SecureCRT for Mac/win强大安全的终端SSH工具,SecureCRT助您网络连接无忧

在当今数字化时代&#xff0c;网络连接已成为生活和工作中不可或缺的一部分。而对于需要进行远程访问和管理的用户来说&#xff0c;一个稳定、安全的终端SSH工具是至关重要的。SecureCRT作为一款强大的终端SSH工具&#xff0c;为用户提供了安全、高效的远程连接解决方案。 首先…

【兔子王赠书第13期】AI绘画实战:Midjourney从新手到高手

文章目录 写在前面AI绘画推荐图书一本书读懂AI绘画关键点内容简介作者简介 推荐理由粉丝福利写在后面 写在前面 如今AI技术已经进入了我们的日常学习生活中&#xff0c;如何用一本书轻松玩转AI绘画&#xff0c;领略无限艺术可能呢&#xff1f; AI绘画 AI绘画是指利用人工智能…

紫光FPGA学习之常见报错

紫光pango design suite报错&#xff1a; 一、4005: [D:/**/rtl/burstORsingle.v(line number: 47)] Logic for ddr_head_addr_rr does not match a standard flip-flop. 看来看去都没有发现这个定义没有问题呀&#xff0c;检查发现&#xff1a; 原来代码&#xff1a; always…

【Docker离线安装与注册服务】

Docker离线安装与注册服务 一、离线安装1. Docker2. Docker-compose3. docker卸载4. 注册docker服务 ​ 一、离线安装 1. Docker Docker 官网离线安装文档&#xff1a;Install Docker Engine from binaries 整理步骤如下&#xff1a; 1.去官网下载 docker 安装包&#xff0c;…

Appcelerator打包ipa有哪些优势

大家好&#xff0c;我是咕噜-凯撒&#xff0c;我们得先知道Appcelerator是啥&#xff0c;Appcelerator&#xff08;现在更名为Axway Titanium&#xff09;是一个跨平台的移动应用开发框架通过提供一种简化和加速移动应用开发的方式帮助你构建高质量的跨平台应用程序。那使用App…

复旦团队提出思维交流框架EoT,由CoT到EoT,可跨模型通信,表现更出色

大型语言模型&#xff08;LLM&#xff09;通过利用庞大的训练语料和强大的计算资源&#xff0c;在众多 NLP 任务中表现卓越。然而&#xff0c;在理解和进行推理方面&#xff0c;这些模型仍显得相对薄弱&#xff0c;仅依靠增加模型的大小无法解决这一问题。 然而&#xff0c;现…

如何使用 TailwindCSS 画一条0.5px的线条

背景 在移动端项目&#xff0c;一般为了让线条看起来更细、更锐利&#xff0c;此时使用0.5px的线条是非常合适的。那么如何使用TailwindCSS画一条0.5px的线条呢&#xff1f; 在实现这个需求的时候&#xff0c;首先去TailwindCSS官网查了一下border有没有对应的内置工具类&…

LeetCode 141. 环形链表

给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;…

人工智能在约会APP开发中的作用

约会APP已成为当今技术世界中结识人们的流行方式。这意味着您不必要求您的朋友去见某人约会。简而言之&#xff0c;技术改善了约会过程&#xff0c;而人工智能在约会APP开发中的兴起极大地影响了人们今天的约会方式。 在约会APP中使用人工智能技术可以改善个人寻找完美匹配对象…