> 技术文档 > 【unity游戏开发——网络】Socket TCP 通信指南_unity socket

【unity游戏开发——网络】Socket TCP 通信指南_unity socket


注意:考虑到热更新的内容比较多,我将热更新的内容分开,并全部整合放在【unity游戏开发——网络】专栏里,感兴趣的小伙伴可以前往逐一查看学习。

文章目录

  • 一、什么是 TCP 通信?
  • 二、基础实现步骤
    • 1、服务端工作流程(客服中心)
    • 2、客户端工作流程(顾客)
    • 3、模拟客户端和服务端的通信
  • 三、多客户端服务(客服中心接待多人)
    • 1、服务器端代码
    • 2、unity客户端代码
    • 3、多客户端通信测试
  • 四、待续
  • 专栏推荐
  • 完结

一、什么是 TCP 通信?

TCP 就像打电话:

  • 服务端是接听电话的人(如客服中心)
  • 客户端是拨打电话的人(如顾客)
  • Socket是电话线,连接双方
  • IP 地址是电话号码,端口是分机号

二、基础实现步骤

1、服务端工作流程(客服中心)

  1. 准备电话线new Socket() 创建 TCP Socket
  2. 绑定号码Bind(IP, 端口) 设置接听号码
  3. 开启接听Listen() 打开电话铃声
  4. 等待来电Accept() 接听电话 → 返回专属通话线路
  5. 收发消息Send()/Receive() 通话交流
  6. 挂断电话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、客户端工作流程(顾客)

  1. 准备电话线new Socket() 创建 TCP Socket
  2. 拨打电话Connect(服务端IP, 端口) 呼叫客服
  3. 收发消息Send()/Receive() 通话交流
  4. 挂断电话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游戏开发——网络】Socket TCP 通信指南_unity socket
将客户端脚本挂载到unity场景中对象上运行,模拟客户端和服务端的通信。
【unity游戏开发——网络】Socket TCP 通信指南_unity socket
【unity游戏开发——网络】Socket TCP 通信指南_unity socket

三、多客户端服务(客服中心接待多人)

#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,配置参数
【unity游戏开发——网络】Socket TCP 通信指南_unity socket

3、多客户端通信测试

开启服务器
【unity游戏开发——网络】Socket TCP 通信指南_unity socket
打包unity应用,开启多个客户端窗口,这里开启两个进行测试
【unity游戏开发——网络】Socket TCP 通信指南_unity socket
客户端给服务端发送消息
【unity游戏开发——网络】Socket TCP 通信指南_unity socket
服务端给客户端发消息
【unity游戏开发——网络】Socket TCP 通信指南_unity socket

四、待续


专栏推荐

地址 【unity游戏开发入门到精通——C#篇】 【unity游戏开发入门到精通——unity通用篇】 【unity游戏开发入门到精通——unity3D篇】 【unity游戏开发入门到精通——unity2D篇】 【unity实战】 【制作100个Unity游戏】 【推荐100个unity插件】 【实现100个unity特效】 【unity框架/工具集开发】 【unity游戏开发——模型篇】 【unity游戏开发——InputSystem】 【unity游戏开发——Animator动画】 【unity游戏开发——UGUI】 【unity游戏开发——联网篇】 【unity游戏开发——优化篇】 【unity游戏开发——shader篇】 【unity游戏开发——编辑器扩展】 【unity游戏开发——热更新】 【unity游戏开发——网络】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述