> 技术文档 > Unity的TCP同步通信_在unity中如何在两台电脑上使用tcp收发消息

Unity的TCP同步通信_在unity中如何在两台电脑上使用tcp收发消息


1.Socket中的重要API

using System.Collections;using System.Collections.Generic;using System.Net;using System.Net.Sockets;using UnityEngine;public class Lesson5 : MonoBehaviour{ // Start is called before the first frame update void Start() { #region 知识点一 Socket套接字的作用 //它是C#提供给我们用于网络通信的一个类(在其它语言当中也有对应的Socket类) //类名:Socket //命名空间:System.Net.Sockets //Socket套接字是支持TCP/IP网络通信的基本操作单位 //一个套接字对象包含以下关键信息 //1.本机的IP地址和端口 //2.对方主机的IP地址和端口 //3.双方通信的协议信息 //一个Sccket对象表示一个本地或者远程套接字信息 //它可以被视为一个数据通道 //这个通道连接与客户端服务端之间 //数据的发送和接受均通过这个通道进行 //一般在制作长连接游戏时,我们会使用Socket套接字作为我们的通信方案 //我们通过它连接客户端和服务端,通过它来收发消息 //你可以把它抽象的想象成一根管子,插在客户端和服务端应用程序上,通过这个管子来传递交换信息 #endregion #region 知识点二 Socket的类型 //Socket套接字有3种不同的类型 //1.流套接字 // 主要用于实现TCP通信,提供了面向连接、可靠的、有序的、数据无差错且无重复的数据传输服务 //2.数据报套接字 // 主要用于实现UDP通信,提供了无连接的通信服务,数据包的长度不能大于32KB,不提供正确性检查,不保证顺序,可能出现重发、丢失等情况 //3.原始套接字(不常用,不深入讲解) // 主要用于实现IP数据包通信,用于直接访问协议的较低层,常用于侦听和分析数据包 //通过Socket的构造函数 我们可以申明不同类型的套接字 //Socket s = new Socket() //参数一:AddressFamily 网络寻址 枚举类型,决定寻址方案 // 常用: // 1.InterNetwork IPv4寻址 // 2.InterNetwork6 IPv6寻址 // 做了解: // 1.UNIX UNIX本地到主机地址 // 2.ImpLink ARPANETIMP地址 // 3.Ipx  IPX或SPX地址 // 4.Iso  ISO协议的地址 // 5.Osi  OSI协议的地址 // 7.NetBios NetBios地址 // 9.Atm  本机ATM服务地址 //参数二:SocketType 套接字枚举类型,决定使用的套接字类型 // 常用: // 1.Dgram 支持数据报,最大长度固定的无连接、不可靠的消息(主要用于UDP通信) // 2.Stream 支持可靠、双向、基于连接的字节流(主要用于TCP通信) // 做了解: // 1.Raw  支持对基础传输协议的访问 // 2.Rdm  支持无连接、面向消息、以可靠方式发送的消息 // 3.Seqpacket 提供排序字节流的面向连接且可靠的双向传输 //参数三:ProtocolType 协议类型枚举类型,决定套接字使用的通信协议 // 常用: // 1.TCP  TCP传输控制协议 // 2.UDP  UDP用户数据报协议 // 做了解: // 1.IP IP网际协议 // 2.Icmp Icmp网际消息控制协议 // 3.Igmp Igmp网际组管理协议 // 4.Ggp  网关到网关协议 // 5.IPv4 Internet协议版本4 // 6.Pup  PARC通用数据包协议 // 7.Idp  Internet数据报协议 // 8.Raw  原始IP数据包协议 // 9.Ipx  Internet数据包交换协议 // 10.Spx 顺序包交换协议 // 11.IcmpV6 用于IPv6的Internet控制消息协议 //2、3参数的常用搭配: // SocketType.Dgram + ProtocolType.Udp = UDP协议通信(常用,主要学习) // SocketType.Stream + ProtocolType.Tcp = TCP协议通信(常用,主要学习) // SocketType.Raw + ProtocolType.Icmp = Internet控制报文协议(了解) // SocketType.Raw + ProtocolType.Raw = 简单的IP包通信(了解) //我们必须掌握的 //TCP流套接字 Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //UDP数据报套接字 Socket socketUdp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); #endregion #region 知识点三 Socket的常用属性 //1.套接字的连接状态 if(socketTcp.Connected) { } //2.获取套接字的类型 print(socketTcp.SocketType); //3.获取套接字的协议类型 print(socketTcp.ProtocolType); //4.获取套接字的寻址方案 print(socketTcp.AddressFamily); //5.从网络中获取准备读取的数据数据量 print(socketTcp.Available); //6.获取本机EndPoint对象(注意 :IPEndPoint继承EndPoint) //socketTcp.LocalEndPoint as IPEndPoint //7.获取远程EndPoint对象 //socketTcp.RemoteEndPoint as IPEndPoint #endregion #region 知识点四 Socket的常用方法 //1.主要用于服务端 // 1-1:绑定IP和端口 IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(\"127.0.0.1\"), 8080); socketTcp.Bind(ipPoint); // 1-2:设置客户端连接的最大数量 socketTcp.Listen(10); // 1-3:等待客户端连入 socketTcp.Accept(); //2.主要用于客户端 // 1-1:连接远程服务端 socketTcp.Connect(IPAddress.Parse(\"118.12.123.11\"), 8080); //3.客户端服务端都会用的 // 1-1:同步发送和接收数据 // 1-2:异步发送和接收数据 // 1-3:释放连接并关闭Socket,先与Close调用 socketTcp.Shutdown(SocketShutdown.Both); // 1-4:关闭连接,释放所有Socket关联资源 socketTcp.Close(); #endregion #region 总结 //这节课我们只是对Socket有一个大体的认识 //主要要建立的概念就是 //TCP和UDP两种长连接通信方案都是基于Socket套接字的 //我们之后只需要使用其中的各种方法,就可以进行网络连接和网络通信了 //这节课必须掌握的内容就是如何声明TCP和UDP的Socket套接字 #endregion } // Update is called once per frame void Update() { }}

2.TCP同步通信基本写法

注意:如果需要在两台PC上使用以下代码测试通信需要确保两台PC处于统一局域网(连接同一WIFI),并将服务器与客户端代码中的本机回环IP(127.0.0.1)改为作为服务器PC的IP地址(可以在cmd中输入ipconfig查询)

2.1服务端

using System;using System.Net;using System.Net.Sockets;using System.Text;namespace TeachTcpServer{ class Program { static void Main(string[] args) { #region 知识点一 回顾服务端需要做的事情 //1.创建套接字Socket //2.用Bind方法将套接字与本地地址绑定 //3.用Listen方法监听 //4.用Accept方法等待客户端连接 //5.建立连接,Accept返回新套接字 //6.用Send和Receive相关方法收发数据 //7.用Shutdown方法释放连接 //8.关闭套接字 #endregion #region 知识点二 实现服务端基本逻辑 //1.创建套接字Socket(TCP) Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.用Bind方法将套接字与本地地址绑定 try { IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(\"127.0.0.1\"), 8080); socketTcp.Bind(ipPoint); } catch (Exception e) { Console.WriteLine(\"绑定报错\" + e.Message); return; } //3.用Listen方法监听 socketTcp.Listen(1024); Console.WriteLine(\"服务端绑定监听结束,等待客户端连入\"); //4.用Accept方法等待客户端连接 //5.建立连接,Accept返回新套接字 Socket socketClient = socketTcp.Accept(); Console.WriteLine(\"有客户端连入了\"); //6.用Send和Receive相关方法收发数据 //发送 socketClient.Send(Encoding.UTF8.GetBytes(\"欢迎连入服务端\")); //接受 byte[] result = new byte[1024]; //返回值为接受到的字节数 int receiveNum = socketClient.Receive(result); Console.WriteLine(\"接受到了{0}发来的消息:{1}\", socketClient.RemoteEndPoint.ToString(), Encoding.UTF8.GetString(result, 0, receiveNum)); //7.用Shutdown方法释放连接 socketClient.Shutdown(SocketShutdown.Both); //8.关闭套接字 socketClient.Close(); #endregion #region 总结 //1.服务端开启的流程每次都是相同的 //2.服务端的 Accept、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容 //抛出问题: //如何让服务端可以服务n个客户端? //我们会在之后的综合练习题进行讲解 #endregion Console.WriteLine(\"按任意键退出\"); Console.ReadKey(); } }}

2.2客户端

using System.Collections;using System.Collections.Generic;using System.Net;using System.Net.Sockets;using System.Text;using UnityEngine;public class Lesson6 : MonoBehaviour{ // Start is called before the first frame update void Start() { #region 知识点一 回顾客户端需要做的事情 //1.创建套接字Socket //2.用Connect方法与服务端相连 //3.用Send和Receive相关方法收发数据 //4.用Shutdown方法释放连接 //5.关闭套接字 #endregion #region 知识点二 实现客户端基本逻辑 //1.创建套接字Socket Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //2.用Connect方法与服务端相连 //确定服务端的IP和端口 IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(\"127.0.0.1\"), 8080); try { socket.Connect(ipPoint); } catch (SocketException e) { if (e.ErrorCode == 10061) print(\"服务器拒绝连接\"); else print(\"连接服务器失败\" + e.ErrorCode); return; } //3.用Send和Receive相关方法收发数据 //接收数据 byte[] receiveBytes = new byte[1024]; int receiveNum = socket.Receive(receiveBytes); print(\"收到服务端发来的消息:\" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum)); //发送数据 socket.Send(Encoding.UTF8.GetBytes(\"你好,我是唐老狮的客户端\")); //4.用Shutdown方法释放连接 socket.Shutdown(SocketShutdown.Both); //5.关闭套接字 socket.Close(); #endregion #region 总结 //1.客户端连接的流程每次都是相同的 //2.客户端的 Connect、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容 //抛出问题: //如何让客户端的Socket不影响主线程,并且可以随时收发消息? //我们会在之后的综合练习题讲解 #endregion } // Update is called once per frame void Update() { }}

3.TCP同步通信服务器与多个客户端连接

3.1服务端

3.1.1封装服务端套接字

using System;using System.Collections.Generic;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;namespace TeachTcpServerExercises2{ class ServerSocket { //服务端Socket public Socket socket; //客户端连接的所有Socket public Dictionary clientDic = new Dictionary(); private bool isClose; //开启服务器端 public void Start(string ip, int port, int num) { isClose = false; socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port); socket.Bind(ipPoint); socket.Listen(num); ThreadPool.QueueUserWorkItem(Accept); ThreadPool.QueueUserWorkItem(Receive); } //关闭服务器端 public void Close() { isClose = true; foreach (ClientSocket client in clientDic.Values) { client.Close(); } clientDic.Clear(); socket.Shutdown(SocketShutdown.Both); socket.Close(); socket = null; } //接受客户端连入 private void Accept(object obj) { while (!isClose) { try {  //连入一个客户端  Socket clientSocket = socket.Accept();  ClientSocket client = new ClientSocket(clientSocket);  client.Send(\"欢迎连入服务器\");  clientDic.Add(client.clientID, client); } catch (Exception e) {  Console.WriteLine(\"客户端连入报错\" + e.Message); } } } //接收客户端消息 private void Receive(object obj) { while (!isClose) { if(clientDic.Count > 0) {  foreach (ClientSocket client in clientDic.Values)  { client.Receive();  } } } } public void Broadcast(string info) { foreach (ClientSocket client in clientDic.Values) { client.Send(info); } } }}

3.1.2封装连接进来的客户端套接字方便管理

using System;using System.Collections.Generic;using System.Net.Sockets;using System.Text;using System.Threading;namespace TeachTcpServerExercises2{ class ClientSocket { private static int CLIENT_BEGIN_ID = 1; public int clientID; public Socket socket; public ClientSocket(Socket socket) { this.clientID = CLIENT_BEGIN_ID; this.socket = socket; ++CLIENT_BEGIN_ID; } ///  /// 是否是连接状态 ///  public bool Connected => this.socket.Connected; //我们应该封装一些方法 //关闭 public void Close() { if(socket != null) { socket.Shutdown(SocketShutdown.Both); socket.Close(); socket = null; } } //发送 public void Send(string info) { if(socket != null) { try {  socket.Send(Encoding.UTF8.GetBytes(info)); } catch(Exception e) {  Console.WriteLine(\"发消息出错\" + e.Message);  Close(); } } } //接收 public void Receive() { if (socket == null) return; try { if(socket.Available > 0) {  byte[] result = new byte[1024 * 5];  int receiveNum = socket.Receive(result);  ThreadPool.QueueUserWorkItem(MsgHandle, Encoding.UTF8.GetString(result, 0, receiveNum)); } } catch (Exception e) { Console.WriteLine(\"收消息出错\" + e.Message); Close(); } } private void MsgHandle(object obj) { string str = obj as string; Console.WriteLine(\"收到客户端{0}发来的消息:{1}\", this.socket.RemoteEndPoint, str); } }}

3.1.3服务端程序入口

using System;namespace TeachTcpServerExercises2{ class Program { static void Main(string[] args) { ServerSocket socket = new ServerSocket(); socket.Start(\"127.0.0.1\", 8080, 1024); Console.WriteLine(\"服务器开启成功\"); while (true) { string input = Console.ReadLine(); if(input == \"Quit\") {  socket.Close(); } else if( input.Substring(0,2) == \"B:\" ) {  socket.Broadcast(input.Substring(2)); } } } }}

3.2客户端

3.2.1客户端网络管理类

using System.Collections;using System.Collections.Generic;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;using UnityEngine;public class NetMgr : MonoBehaviour{ private static NetMgr instance; public static NetMgr Instance => instance; //客户端Socket private Socket socket; //用于发送消息的队列 公共容器 主线程往里面放 发送线程从里面取 private Queue sendMsgQueue = new Queue(); //用于接收消息的对象 公共容器 子线程往里面放 主线程从里面取 private Queue receiveQueue = new Queue(); //用于收消息的水桶(容器) private byte[] receiveBytes = new byte[1024 * 1024]; //返回收到的字节数 private int receiveNum; //是否连接 private bool isConnected = false; void Awake() { instance = this; DontDestroyOnLoad(this.gameObject); } // Update is called once per frame void Update() { if(receiveQueue.Count > 0) { print(receiveQueue.Dequeue()); } } //连接服务端 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); try { 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 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)); } } } public void Close() { if(socket != null) { socket.Shutdown(SocketShutdown.Both); socket.Close(); isConnected = false; } } private void OnDestroy() { Close(); }}

3.2.2自动在场景中创建NetMgr物体

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Main : MonoBehaviour{ // Start is called before the first frame update void Start() { if(NetMgr.Instance == null) { GameObject obj = new GameObject(\"Net\"); obj.AddComponent(); } NetMgr.Instance.Connect(\"127.0.0.1\", 8080); } // Update is called once per frame void Update() { }}

4.消息分区

4.1.1数据基类

在网络中传输的消息都继承自这个类

提供了序列化,反序列化各个基础数据类型的方法

自定义数据只需要继承该类重写Writing、Reading、GetNum方法,使用提供的基础类型序列化/反序列化方法即可实现自定义数据类型的序列化/反序列化

using System;using System.Collections;using System.Collections.Generic;using System.Text;using UnityEngine;public abstract class BaseData{ ///  /// 用于子类重写的 获取字节数组容器大小的方法 ///  ///  public abstract int GetBytesNum(); ///  /// 把成员变量 序列化为 对应的字节数组 ///  ///  public abstract byte[] Writing(); ///  /// 把2进制字节数组 反序列化到 成员变量当中 ///  /// 反序列化使用的字节数组 /// 从该字节数组的第几个位置开始解析 默认是0 public abstract int Reading(byte[] bytes, int beginIndex = 0); ///  /// 存储int类型变量到指定的字节数组当中 ///  /// 指定字节数组 /// 具体的int值 /// 每次存储后用于记录当前索引位置的变量 protected void WriteInt(byte[] bytes, int value, ref int index) { BitConverter.GetBytes(value).CopyTo(bytes, index); index += sizeof(int); } protected void WriteShort(byte[] bytes, short value, ref int index) { BitConverter.GetBytes(value).CopyTo(bytes, index); index += sizeof(short); } protected void WriteLong(byte[] bytes, long value, ref int index) { BitConverter.GetBytes(value).CopyTo(bytes, index); index += sizeof(long); } protected void WriteFloat(byte[] bytes, float value, ref int index) { BitConverter.GetBytes(value).CopyTo(bytes, index); index += sizeof(float); } protected void WriteByte(byte[] bytes, byte value, ref int index) { bytes[index] = value; index += sizeof(byte); } protected void WriteBool(byte[] bytes, bool value, ref int index) { BitConverter.GetBytes(value).CopyTo(bytes, index); index += sizeof(bool); } protected void WriteString(byte[] bytes, string value, ref int index) { //先存储string字节数组的长度 byte[] strBytes = Encoding.UTF8.GetBytes(value); //BitConverter.GetBytes(strBytes.Length).CopyTo(bytes, index); //index += sizeof(int); WriteInt(bytes, strBytes.Length, ref index); //再存 string字节数组 strBytes.CopyTo(bytes, index); index += strBytes.Length; } protected void WriteData(byte[] bytes, BaseData data, ref int index) { data.Writing().CopyTo(bytes, index); index += data.GetBytesNum(); } ///  /// 根据字节数组 读取整形 ///  /// 字节数组 /// 开始读取的索引数 ///  protected int ReadInt(byte[] bytes, ref int index) { int value = BitConverter.ToInt32(bytes, index); index += sizeof(int); return value; } protected short ReadShort(byte[] bytes, ref int index) { short value = BitConverter.ToInt16(bytes, index); index += sizeof(short); return value; } protected long ReadLong(byte[] bytes, ref int index) { long value = BitConverter.ToInt64(bytes, index); index += sizeof(long); return value; } protected float ReadFloat(byte[] bytes, ref int index) { float value = BitConverter.ToSingle(bytes, index); index += sizeof(float); return value; } protected byte ReadByte(byte[] bytes, ref int index) { byte value = bytes[index]; index += sizeof(byte); return value; } protected bool ReadBool(byte[] bytes, ref int index) { bool value = BitConverter.ToBoolean(bytes, index); index += sizeof(bool); return value; } protected string ReadString(byte[] bytes, ref int index) { //首先读取长度 int length = ReadInt(bytes, ref index); //再读取string string value = Encoding.UTF8.GetString(bytes, index, length); index += length; return value; } protected T ReadData(byte[] bytes, ref int index) where T:BaseData,new() { T value = new T(); index += value.Reading(bytes, index); return value; }}

(更新中)