【unity游戏开发——网络】Socket TCP 通信指南_unity socket
注意:考虑到热更新的内容比较多,我将热更新的内容分开,并全部整合放在【unity游戏开发——网络】专栏里,感兴趣的小伙伴可以前往逐一查看学习。
文章目录
- 一、什么是 TCP 通信?
- 二、基础实现步骤
- 三、多客户端服务(客服中心接待多人)
-
- 1、服务器端代码
- 2、unity客户端代码
- 3、多客户端通信测试
- 四、待续
- 专栏推荐
- 完结
一、什么是 TCP 通信?
TCP 就像打电话:
- 服务端是接听电话的人(如客服中心)
- 客户端是拨打电话的人(如顾客)
- Socket是电话线,连接双方
- IP 地址是电话号码,端口是分机号
二、基础实现步骤
1、服务端工作流程(客服中心)
- 准备电话线:
new Socket()创建 TCP Socket - 绑定号码:
Bind(IP, 端口)设置接听号码 - 开启接听:
Listen()打开电话铃声 - 等待来电:
Accept()接听电话 → 返回专属通话线路 - 收发消息:
Send()/Receive()通话交流 - 挂断电话:
Close()结束通话
代码实现如下,注意这里使用原生C#程序编写
using System.Net;using System.Net.Sockets;using System.Text;class Server{ public static void Main(string[] args) { //1.创建套接字Socket(TCP) Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.用Bind方法将套接字与本地地址绑定 try { IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(\"127.0.0.1\"), 8080);//把本机作为服务端程序 IP地址传入本机 socketTcp.Bind(iPEndPoint);//绑定 } catch (Exception e) { //如果IP地址不合法或者端口号被占用可能报错 Console.WriteLine(\"绑定报错\" + e.Message); return; } //3.用Listen方法监听 socketTcp.Listen(1024);//最大接收1024个客户端 Console.WriteLine(\"服务端绑定监听结束,等待客户端连入\"); //5.建立连接,Accept返回新套接字 Socket socketClient = socketTcp.Accept(); //Accept是阻塞式的方法 会把主线程卡主 一定要等到客户端接入后才会继续执行后面的代码 //客户端接入后 返回新的Socket对象 这个新的Socket可以理解为客户段和服务端的通信通道 Console.WriteLine(\"有客户端连入了\"); //6.用Send和Receive相关方法收发数据 //发送字符串转成的字节数组给客户端 socketClient.Send(Encoding.UTF8.GetBytes(\"欢迎连入服务端\")); //声明接受客户端信息的字节数组 声明1024容量代表能接受1kb的信息 byte[] result = new byte[1024]; //接受客户端信息 返回值为接受到的字节数 int receiveNum = socketClient.Receive(result); //打印 远程发送信息的客户端的IP和端口 以及 发送过来的字符串 Console.WriteLine(\"接受到了\" + socketClient.RemoteEndPoint.ToString() + \"发来的消息:\" + Encoding.UTF8.GetString(result, 0, receiveNum)); //7.用Shutdown方法释放连接 //注意断开的是客户段和服务端的通信通道 socketClient.Shutdown(SocketShutdown.Both); //8.关闭套接字 //注意关闭的是客户段和服务端的通信通道 socketClient.Close(); }}
2、客户端工作流程(顾客)
- 准备电话线:
new Socket()创建 TCP Socket - 拨打电话:
Connect(服务端IP, 端口)呼叫客服 - 收发消息:
Send()/Receive()通话交流 - 挂断电话:
Close()结束通话
代码实现如下,注意这里使用unity编写
using System.Net;using System.Net.Sockets;using System.Text;using UnityEngine;public class Client : MonoBehaviour{ private void Start() { //1.创建套接字Socket Tcp Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.用Connect方法与服务端相连 //确定服务端的IP和端口 正常来说填的应该是远端服务器的ip地址以及端口号 //由于只有一台电脑用于测试 本机也当做服务器 所以传入当前电脑的ip地址 IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(\"127.0.0.1\"), 8080); try { //连接 socketTcp.Connect(iPEndPoint); } catch (SocketException e) { //如果连接没有开启或者服务器异常 会报错 不同的返回码代表不同报错 if (e.ErrorCode == 10061) print(\"服务器拒绝连接\"); else print(\"连接服务器失败\" + e.ErrorCode); return; } //3.用Send和Receive相关方法收发数据 //接收数据 //声明接收数据字节数组 byte[] receiveBytes = new byte[1024]; //Receive方法接受数据 返回接收多少字节 int receiveNum = socketTcp.Receive(receiveBytes); print(\"收到服务端发来的消息:\" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum)); //发送数据 socketTcp.Send(Encoding.UTF8.GetBytes(\"你好,这里是客户端\")); //4.用Shutdown方法释放连接 socketTcp.Shutdown(SocketShutdown.Both); //5.关闭套接字 socketTcp.Close(); }}
3、模拟客户端和服务端的通信
先开启服务端控制台

将客户端脚本挂载到unity场景中对象上运行,模拟客户端和服务端的通信。


三、多客户端服务(客服中心接待多人)
#mermaid-svg-XA9sw5jvS8JvdbLK {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-XA9sw5jvS8JvdbLK .error-icon{fill:#552222;}#mermaid-svg-XA9sw5jvS8JvdbLK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XA9sw5jvS8JvdbLK .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-XA9sw5jvS8JvdbLK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XA9sw5jvS8JvdbLK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XA9sw5jvS8JvdbLK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XA9sw5jvS8JvdbLK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XA9sw5jvS8JvdbLK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XA9sw5jvS8JvdbLK .marker.cross{stroke:#333333;}#mermaid-svg-XA9sw5jvS8JvdbLK svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XA9sw5jvS8JvdbLK .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XA9sw5jvS8JvdbLK .cluster-label text{fill:#333;}#mermaid-svg-XA9sw5jvS8JvdbLK .cluster-label span{color:#333;}#mermaid-svg-XA9sw5jvS8JvdbLK .label text,#mermaid-svg-XA9sw5jvS8JvdbLK span{fill:#333;color:#333;}#mermaid-svg-XA9sw5jvS8JvdbLK .node rect,#mermaid-svg-XA9sw5jvS8JvdbLK .node circle,#mermaid-svg-XA9sw5jvS8JvdbLK .node ellipse,#mermaid-svg-XA9sw5jvS8JvdbLK .node polygon,#mermaid-svg-XA9sw5jvS8JvdbLK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XA9sw5jvS8JvdbLK .node .label{text-align:center;}#mermaid-svg-XA9sw5jvS8JvdbLK .node.clickable{cursor:pointer;}#mermaid-svg-XA9sw5jvS8JvdbLK .arrowheadPath{fill:#333333;}#mermaid-svg-XA9sw5jvS8JvdbLK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XA9sw5jvS8JvdbLK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XA9sw5jvS8JvdbLK .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-XA9sw5jvS8JvdbLK .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-XA9sw5jvS8JvdbLK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XA9sw5jvS8JvdbLK .cluster text{fill:#333;}#mermaid-svg-XA9sw5jvS8JvdbLK .cluster span{color:#333;}#mermaid-svg-XA9sw5jvS8JvdbLK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XA9sw5jvS8JvdbLK :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 主线程 接受新客户线程 处理消息线程 新客户加入列表 遍历所有客户处理消息
关键技巧:
- 每个客户单独记录 (
ClientSocket类) - 多线程处理避免卡顿
- 使用线程锁保护共享数据
1、服务器端代码
创建客户端套接字类,ClientSocket类
using System.Net.Sockets;using System.Text;class ClientSocket{ private static int CLIENT_BEGIN_ID = 1; // 静态变量,用于为客户端分配唯一的客户端ID public int clientID; // 客户端的唯一ID public Socket clientSocket; // 与客户端通信的套接字对象 /// /// 是否是连接状态 /// public bool isClientConnected => this.clientSocket.Connected; // 判断套接字是否处于连接状态 public ClientSocket(Socket clientSocket) { this.clientID = CLIENT_BEGIN_ID; // 初始化客户端ID this.clientSocket = clientSocket; // 初始化套接字 ++CLIENT_BEGIN_ID; // 为下一个客户端分配不同的ID } // 关闭套接字连接 public void Close() { if (clientSocket != null) { clientSocket.Shutdown(SocketShutdown.Both); // 关闭套接字的读写 clientSocket.Close(); // 关闭套接字连接 clientSocket = null; } } // 发送消息给客户端 public void Send(string info) { if (clientSocket != null) { try { clientSocket.Send(Encoding.UTF8.GetBytes(info)); // 将消息编码为UTF-8字节数组并发送给客户端 } catch (Exception e) { Console.WriteLine(\"发消息出错\" + e.Message); Close(); // 如果发送出现异常,关闭套接字连接 } } } // 接收来自客户端的消息 public void Receive() { if (clientSocket == null) return; try { if (clientSocket.Available > 0) // 如果套接字中有可读数据 { byte[] resultByteArray = new byte[1024 * 5]; // 创建一个缓冲区来存储接收到的数据 int byteLength = clientSocket.Receive(resultByteArray); // 从套接字接收数据并存储在缓冲区中 ThreadPool.QueueUserWorkItem(HandleMessage, Encoding.UTF8.GetString(resultByteArray, 0, byteLength)); // 异步处理接收到的消息 } } catch (Exception e) { Console.WriteLine(\"收消息出错\" + e.Message); Close(); // 如果接收出现异常,关闭套接字连接 } } // 处理接收到的消息 private void HandleMessage(object obj) { string str = obj as string; Console.WriteLine(\"收到客户端{0}发来的消息:{1}\", this.clientSocket.RemoteEndPoint, str); // 将接收到的消息和客户端的信息输出到控制台 }}
创建服务端套接字类,ServerSocket类
using System.Net;using System.Net.Sockets;class ServerSocket{ // 添加线程锁对象 private readonly object lockObj = new object(); // 服务器端Socket public Socket serverSocket; // 保存客户端连接的所有Socket的字典 public Dictionary<int, ClientSocket> clientSocketDictionary = new Dictionary<int, ClientSocket>(); // 用于标识服务器是否关闭的标志 private bool isServerClose; // 开启服务器端 public void Start(string ipString, int port, int clientSocketMaxNum) { // 初始化服务器关闭标志为假 isServerClose = false; // 创建服务器套接字,指定地址族为IPv4、套接字类型为流套接字、协议类型为TCP serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 创建IP终结点,指定IP地址和端口号 IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ipString), port); // 将套接字绑定到指定的IP终结点 serverSocket.Bind(serverEndPoint); // 启动服务器套接字,同时指定同时等待连接的最大客户端数 serverSocket.Listen(clientSocketMaxNum); // 启动线程池中的线程来处理客户端连接请求和消息接收 ThreadPool.QueueUserWorkItem(AcceptClientConnect); ThreadPool.QueueUserWorkItem(ReceiveClientMessage); } // 关闭服务器端 public void Close() { // 设置服务器关闭标志为真 isServerClose = true; // 使用线程锁保护字典操作 lock (lockObj) { // 关闭所有客户端连接 foreach (ClientSocket client in clientSocketDictionary.Values) { client.Close(); } clientSocketDictionary.Clear(); } // 关闭服务器套接字的读写 serverSocket.Shutdown(SocketShutdown.Both); // 关闭服务器套接字 serverSocket.Close(); // 将服务器套接字设置为null serverSocket = null; } // 接受客户端连接 private void AcceptClientConnect(object obj) { while (!isServerClose) { try { // 等待并接受一个客户端连接请求 Socket clientSocket = serverSocket.Accept(); // 创建一个新的ClientSocket对象来管理客户端连接 ClientSocket client = new ClientSocket(clientSocket); // 向客户端发送欢迎消息 client.Send(\"欢迎连入服务器\"); // 使用线程锁保护字典操作 lock (lockObj) { // 将客户端Socket对象添加到字典中,以客户端ID作为键 clientSocketDictionary.Add(client.clientID, client); } } catch (Exception e) { Console.WriteLine(\"客户端连入报错\" + e.Message); } } } // 接收客户端消息 private void ReceiveClientMessage(object obj) { while (!isServerClose) { // 使用线程锁保护字典操作 lock (lockObj) { if (clientSocketDictionary.Count > 0) { // 创建字典值的副本进行遍历 var clients = new List<ClientSocket>(clientSocketDictionary.Values); // 在锁外遍历副本,避免长时间持有锁 foreach (ClientSocket client in clients) { // 从每个客户端接收消息 client.Receive(); } } } // 添加短暂休眠减少CPU占用 Thread.Sleep(10); } } // 向所有客户端广播消息 public void Broadcast(string info) { // 使用线程锁保护字典操作 lock (lockObj) { // 创建字典值的副本进行遍历 var clients = new List<ClientSocket>(clientSocketDictionary.Values); foreach (ClientSocket client in clients) { client.Send(info); } } }}
主入口创建服务端并开启,定义一些命令
class Program{ static void Main(string[] args) { // 创建一个ServerSocket对象,用于处理服务器端的操作 ServerSocket serverSocket = new ServerSocket(); // 启动服务器,绑定到本地IP地址 127.0.0.1,监听端口 8080,允许最大连接数为 1024 serverSocket.Start(\"127.0.0.1\", 8080, 1024); // 输出服务器开启成功的消息 Console.WriteLine(\"服务器开启成功\"); while (true) { // 从控制台读取用户输入 string input = Console.ReadLine(); // 如果用户输入 \"Quit\",则关闭服务器 if (input == \"Quit\") { serverSocket.Close(); } // 如果用户输入以 \"B:\" 开头,表示要广播消息给所有客户端 else if (input.Substring(0, 2) == \"B:\") { // 提取输入的消息内容(去掉 \"B:\") string message = input.Substring(2); // 调用服务器的广播方法,向所有客户端发送消息 serverSocket.Broadcast(message); } } }}
2、unity客户端代码
继承MonoBehaviour的泛型单例基类
using UnityEngine;/// /// 继承MonoBehaviour的泛型单例基类/// public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour{ //记录单例对象是否存在。用于防止在OnDestroy方法中访问单例对象报错 public static bool IsExisted { get; private set; } = false; private static T instance; public static T Instance { get { if (instance == null) { instance = FindObjectOfType<T>(); if (instance == null) { GameObject go = new GameObject(typeof(T).Name); // 创建游戏对象 instance = go.AddComponent<T>(); // 挂载脚本 } } DontDestroyOnLoad(instance); IsExisted = true; return instance; } } // 构造方法私有化,防止外部 new 对象 protected SingletonMono() { } private void OnDestroy() { IsExisted = false; }}
创建Tcp网络管理器,实现MonoBehaviour单例
using System.Collections.Generic;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;public class TcpNetManager : SingletonMono<TcpNetManager>{ private Socket socket; // 创建Socket对象,用于网络通信 private Queue<string> sendMsgQueue = new Queue<string>(); // 创建一个队列,用于存储待发送的消息 public Queue<string> receiveQueue = new Queue<string>(); // 创建一个队列,用于存储接收到的消息 private byte[] receiveBytes = new byte[1024 * 1024]; // 创建一个字节数组,用于存储接收到的数据 private int receiveNum; // 用于存储接收到的字节数 private bool isConnected = false; // 用于标识是否已连接到服务器 // 连接服务器 public void Connect(string ip, int port) { if (isConnected) // 如果已连接,则直接返回 return; if (socket == null) // 如果套接字为空,创建一个套接字对象 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port); // 创建一个IP终结点对象 try { // 尝试连接到指定的IP地址和端口 socket.Connect(ipPoint); isConnected = true; // 标记已连接 ThreadPool.QueueUserWorkItem(SendMsg); // 创建并启动发送消息的线程 ThreadPool.QueueUserWorkItem(ReceiveMsg); // 创建并启动接收消息的线程 } catch (SocketException e) { if (e.ErrorCode == 10061) // 如果连接被服务器拒绝 print(\"服务器拒绝连接\"); else print(\"连接失败\" + e.ErrorCode + e.Message); // 打印连接失败的信息 } } // 关闭连接 public void Close() { if (socket != null) // 如果套接字对象存在 { socket.Shutdown(SocketShutdown.Both); // 关闭套接字的发送和接收 socket.Close(); // 关闭套接字连接 isConnected = false; // 标记连接已关闭 } } // 当对象被销毁时,确保关闭连接 private void OnDestroy() { Close(); // 调用关闭连接的方法 } // 发送消息 public void Send(string info) { sendMsgQueue.Enqueue(info); // 将消息添加到发送消息队列 } // 在独立线程中处理发送消息的逻辑 private void SendMsg(object obj) { while (isConnected) // 只要连接有效 { if (sendMsgQueue.Count > 0) // 如果发送消息队列中有待发送的消息 { // 从队列中取出消息并发送到服务器 socket.Send(Encoding.UTF8.GetBytes(sendMsgQueue.Dequeue())); } } } // 在独立线程中处理接收消息的逻辑 private void ReceiveMsg(object obj) { while (isConnected) // 只要连接有效 { if (socket.Available > 0) // 如果有可接收的数据 { // 接收从服务器发送来的数据,并将数据转换成字符串后存储到接收消息队列 receiveNum = socket.Receive(receiveBytes); receiveQueue.Enqueue(Encoding.UTF8.GetString(receiveBytes, 0, receiveNum)); } } }}
书写Client类,用于连接服务端,定义按钮和输入框,进行通信测试
using UnityEngine;using UnityEngine.UI;public class Client : MonoBehaviour{ public InputField InputField; public Button sendButton; public Text text; void Start() { TcpNetManager.Instance.Connect(\"127.0.0.1\", 8080); sendButton.onClick.AddListener(() => { if (InputField.text != \"\") TcpNetManager.Instance.Send(InputField.text); }); } void Update() { // 在Unity的每一帧中检查是否有待处理的接收消息,如果有,则打印出来 if (TcpNetManager.Instance.receiveQueue.Count > 0) { //获取并移除接收队列中的消息 string str = TcpNetManager.Instance.receiveQueue.Dequeue(); text.text = str; Debug.Log(str); } }}
挂载脚本,绘制简单的UI,配置参数

3、多客户端通信测试
开启服务器

打包unity应用,开启多个客户端窗口,这里开启两个进行测试

客户端给服务端发送消息

服务端给客户端发消息

四、待续
专栏推荐
完结
好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!



