Unity C# 自定义TCP传输协议以及封包拆包、解决粘包问题

时间:2022-12-15 23:23:44

本文只是初步实现了一个简单的TCP自定协议,更为复杂的协议可以根据这种方式去扩展。

TCP协议,通俗一点的讲,它是一种基于socket传输的由发送方和接收方事先协商好的一种消息包组成结构,主要由消息头和消息体组成。

众所周知,基于socket的信息交互有两个问题:

第一、接收方不能主动识别发送方发送的信息类型,例如A方(客户端)向B方(服务器)发送了一条信息:123,没有事先经过协议规定的话,B方不可能知道这条信息123到底是一个int型123还是一个string型123,甚至他根本就不知道这条信息解析出来是123,所以B方找不到处理这条信息的方式;

第二、接收方不能主动拆分发送方发送的多条信息,例如A方连续向B方发送了多条信息:123、456、789,由于网络延迟或B方接收缓冲区大小的不同设置,B方收到的信息可能是:1234、5678、9,也可能是123456789,也可能是1、2、3、4、5、6、7、8、9,还可能是更多意想不到的情况......

所以TCP协议就是为了解决这两个问题而存在的,当然为消息包加密也是它的另一个主要目的。

TCP协议的格式一般都是:消息头+消息体,消息头的长度是固定的,A方和B方都事先知道消息头长度,以及消息头中各个部位的值所代表的意义,其中包含了对消息体的描述,包括消息体长度,消息体里的消息类型,消息体的加密方式等。

B方在收到A方消息后,先按协议中规定的方式解析消息头,获取到里面对消息体的描述信息,他就可以知道消息体的长度是多少,以便于跟这条消息后面所紧跟的下一条消息进行拆分,他也可以从描述信息中得知消息体中的消息类型,并按正确的解析方式进行解析,从而完成信息的交互。

这里以一个简单的TCP协议作为例子:

第一步:定义协议(我们将协议定义如下)

消息头(28字节):(int)消息校验码4字节 + (int)消息体长度4字节 + (long)身份ID8字节 + (int)主命令4字节 + (int)子命令4字节 + (int)加密方式4字节

消息体:(int)消息1长度4字节 + (string)消息1 + (int)消息2长度4字节 + (string)消息2 + (int)消息3长度4字节 + (string)消息3 + ......

第二步:服务器建立监听

    //SocketTCPServer.cs
private static string ip = "127.0.0.1";
private static int port = 5690;
private static Socket socketServer;
public static List<Socket> listPlayer = new List<Socket>();
private static Socket sTemp;
///<summary>
///绑定地址并监听
///</summary>
///ip地址 端口 类型默认为TCP
public static void init(string ipStr, int iPort)
{
try
{
ip = ipStr;
port = iPort;
socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketServer.Bind(new IPEndPoint(IPAddress.Parse(ip), port));
Thread threadListenAccept = new Thread(new ThreadStart(ListenAccept));
threadListenAccept.Start();
}
catch (ArgumentNullException e)
{
Debug.Log(e.ToString());
}
catch (SocketException e)
{
Debug.Log(e.ToString());
}
}
///<summary>
///监听用户连接
///</summary>
private static void ListenAccept()
{
socketServer.Listen(0); //对于socketServer绑定的IP和端口开启监听
sTemp = socketServer.Accept(); //如果在socketServer上有新的socket连接,则将其存入sTemp,并添加到链表
listPlayer.Add(sTemp);
Thread threadReceiveMessage = new Thread(new ThreadStart(ReceiveMessage));
threadReceiveMessage.Start();
while (true)
{
sTemp = socketServer.Accept();
listPlayer.Add(sTemp);
}
}


第三步:客户端连接服务器

 //SocketTCPClient.cs
private static string ip = "127.0.0.1";
private static int port = 5690;
private static Socket socketClient;
public static List<string> listMessage = new List<string>();
///<summary>
///创建一个SocketClient实例
///</summary>
///ip地址 端口 类型默认为TCP
public static void CreateInstance(string ipStr, int iPort)
{
ip = ipStr;
port = iPort;
socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
ConnectServer();
}
/// <summary>
///连接服务器
/// </summary>
private static void ConnectServer()
{
try
{
socketClient.Connect(IPAddress.Parse(ip), port);
Thread threadConnect = new Thread(new ThreadStart(ReceiveMessage));
threadConnect.Start();
}
catch (ArgumentNullException e)
{
Debug.Log(e.ToString());
}
catch (SocketException e)
{
Debug.Log(e.ToString());
}
}

第四步:封包以及发送消息包 

    /// <summary>
/// 构建消息数据包
/// </summary>
/// <param name="Crccode">消息校验码,判断消息开始</param>
/// <param name="sessionid">用户登录成功之后获得的身份ID</param>
/// <param name="command">主命令</param>
/// <param name="subcommand">子命令</param>
/// <param name="encrypt">加密方式</param>
/// <param name="MessageBody">消息内容(string数组)</param>
/// <returns>返回构建完整的数据包</returns>
public static byte[] BuildDataPackage(int Crccode,long sessionid, int command,int subcommand, int encrypt, string[] MessageBody)
{
//消息校验码默认值为0x99FF
Crccode = 65433;
//消息头各个分类数据转换为字节数组(非字符型数据需先转换为网络序 HostToNetworkOrder:主机序转网络序)
byte[] CrccodeByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Crccode));
byte[] sessionidByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(sessionid));
byte[] commandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(command));
byte[] subcommandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(subcommand));
byte[] encryptByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(encrypt));
//计算消息体的长度
int MessageBodyLength = 0;
for (int i = 0; i < MessageBody.Length; i++)
{
if (MessageBody[i] == "")
break;
MessageBodyLength += Encoding.UTF8.GetBytes(MessageBody[i]).Length;
}
//定义消息体的字节数组(消息体长度MessageBodyLength + 每个消息前面有一个int变量记录该消息字节长度)
byte[] MessageBodyByte = new byte[MessageBodyLength + MessageBody.Length*4];
//记录已经存入消息体数组的字节数,用于下一个消息存入时检索位置
int CopyIndex = 0;
for (int i = 0; i < MessageBody.Length; i++)
{
//单个消息
byte[] bytes = Encoding.UTF8.GetBytes(MessageBody[i]);
//先存入单个消息的长度
BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bytes.Length)).CopyTo(MessageBodyByte, CopyIndex);
CopyIndex += 4;
bytes.CopyTo(MessageBodyByte, CopyIndex);
CopyIndex += bytes.Length;
}
//定义总数据包(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 + 消息体)
byte[] totalByte = new byte[28 + MessageBodyByte.Length];
//组合数据包头部(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节)
CrccodeByte.CopyTo(totalByte,0);
BitConverter.GetBytes(IPAddress.HostToNetworkOrder(MessageBodyByte.Length)).CopyTo(totalByte,4);
sessionidByte.CopyTo(totalByte, 8);
commandByte.CopyTo(totalByte, 16);
subcommandByte.CopyTo(totalByte, 20);
encryptByte.CopyTo(totalByte, 24);
//组合数据包体
MessageBodyByte.CopyTo(totalByte,28);
Debug.Log("发送数据包的总长度为:"+ totalByte.Length);
return totalByte;
}
///<summary>
///发送信息
///</summary>
public static void SendMessage(byte[] sendBytes)
{
//确定是否连接
if (socketClient.Connected)
{
//获取远程终结点的IP和端口信息
IPEndPoint ipe = (IPEndPoint)socketClient.RemoteEndPoint;
socketClient.Send(sendBytes, sendBytes.Length, 0);
}
}

第五步:接收消息以及解析消息包 

  ///<summary>
///接收消息
///</summary>
private static void ReceiveMessage()
{
while (true)
{
//接受消息头(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 = 28字节)
int HeadLength = 28;
//存储消息头的所有字节数
byte[] recvBytesHead = new byte[HeadLength];
//如果当前需要接收的字节数大于0,则循环接收
while (HeadLength > 0)
{
byte[] recvBytes1 = new byte[28];
//将本次传输已经接收到的字节数置0
int iBytesHead = 0;
//如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收
if (HeadLength >= recvBytes1.Length)
{
iBytesHead = socketClient.Receive(recvBytes1, recvBytes1.Length, 0);
}
else
{
iBytesHead = socketClient.Receive(recvBytes1, HeadLength, 0);
}
//将接收到的字节数保存
recvBytes1.CopyTo(recvBytesHead, recvBytesHead.Length - HeadLength);
//减去已经接收到的字节数
HeadLength -= iBytesHead;
}
//接收消息体(消息体的长度存储在消息头的4至8索引位置的字节里)
byte[] bytes = new byte[4];
Array.Copy(recvBytesHead, 4, bytes, 0, 4);
int BodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0));
//存储消息体的所有字节数
byte[] recvBytesBody = new byte[BodyLength];
//如果当前需要接收的字节数大于0,则循环接收
while (BodyLength > 0)
{
byte[] recvBytes2 = new byte[BodyLength < 1024 ? BodyLength : 1024];
//将本次传输已经接收到的字节数置0
int iBytesBody = 0;
//如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收
if (BodyLength >= recvBytes2.Length)
{
iBytesBody = socketClient.Receive(recvBytes2, recvBytes2.Length, 0);
}
else
{
iBytesBody = socketClient.Receive(recvBytes2, BodyLength, 0);
}
//将接收到的字节数保存
recvBytes2.CopyTo(recvBytesBody, recvBytesBody.Length - BodyLength);
//减去已经接收到的字节数
BodyLength -= iBytesBody;
}
//一个消息包接收完毕,解析消息包
UnpackData(recvBytesHead,recvBytesBody);
}
}
/// <summary>
/// 解析消息包
/// </summary>
/// <param name="Head">消息头</param>
/// <param name="Body">消息体</param>
public static void UnpackData(byte[] Head, byte[] Body)
{
byte[] bytes = new byte[4];
Array.Copy(Head, 0, bytes, 0, 4);
Debug.Log("接收到数据包中的校验码为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[8];
Array.Copy(Head, 8, bytes, 0, 8);
Debug.Log("接收到数据包中的身份ID为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt64(bytes, 0))); bytes = new byte[4];
Array.Copy(Head, 16, bytes, 0, 4);
Debug.Log("接收到数据包中的数据主命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[4];
Array.Copy(Head, 20, bytes, 0, 4);
Debug.Log("接收到数据包中的数据子命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[4];
Array.Copy(Head, 24, bytes, 0, 4);
Debug.Log("接收到数据包中的数据加密方式为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0))); bytes = new byte[Body.Length];
for (int i = 0; i < Body.Length;)
{
byte[] _byte = new byte[4];
Array.Copy(Body, i, _byte, 0, 4);
i += 4;
int num = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_byte, 0)); _byte = new byte[num];
Array.Copy(Body, i, _byte, 0, num);
i += num;
Debug.Log("接收到数据包中的数据有:" + Encoding.UTF8.GetString(_byte, 0, _byte.Length));
}
}


第六步:测试,同时发送两个包到服务器 

private string Ip = "127.0.0.1";
private int Port = 5690;
void Start()
{
SocketTCPServer.init(Ip, Port); //开启并初始化服务器
SocketTCPClient.CreateInstance(Ip, Port); //客户端连接服务器
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
string[] str = {"测试字符串1","test1","test11"};
SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 2, 3, 4,5, str));
string[] str2 = { "我是与1同时发送的测试字符串2,请注意我是否与其他信息粘包", "test2", "test22" };
SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 6, 7, 8, 9, str2));
}
}
void OnApplicationQuit()
{
SocketTCPClient.Close();
SocketTCPServer.Close();
}

输出结果如下,可见粘包问题已得到解决:


Unity C# 自定义TCP传输协议以及封包拆包、解决粘包问题

Unity C# 自定义TCP传输协议以及封包拆包、解决粘包问题的更多相关文章

  1. TCP传输协议使用

    TCP传输协议,也称之为套接字连接,比较安全,三次握手!,必须确保对方计算机存在,才能连接,而且是长时间连接. 缺点是传输速度有点慢. 你用 socket 去连接 ServiceSocaket 服务器 ...

  2. Netty系列(四)TCP拆包和粘包

    Netty系列(四)TCP拆包和粘包 一.拆包和粘包问题 (1) 一个小的Socket Buffer问题 在基于流的传输里比如 TCP/IP,接收到的数据会先被存储到一个 socket 接收缓冲里.不 ...

  3. Dealing with a Stream-based Transport 处理一个基于流的传输 粘包 即使关闭nagle算法,也不能解决粘包问题

    即使关闭nagle算法,也不能解决粘包问题 https://waylau.com/netty-4-user-guide/Getting%20Started/Dealing%20with%20a%20S ...

  4. Netty处理TCP拆包、粘包

    Netty实践(二):TCP拆包.粘包问题-学海无涯 心境无限-51CTO博客 http://blog.51cto.com/zhangfengzhe/1890577 2017-01-09 21:56: ...

  5. tcp粘包、解决粘包问题

    目录 subproess模块 TCP粘包问题 粘包两种情况 解决粘包问题 struct模块的使用 使用struct模块解决粘包 优化解决粘包问题 上传大文件 服务端 客户端 UDP协议 upd套接字 ...

  6. 深入了解Netty【八】TCP拆包、粘包和解决方案

    1.TCP协议传输过程 TCP协议是面向流的协议,是流式的,没有业务上的分段,只会根据当前套接字缓冲区的情况进行拆包或者粘包: 发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由 ...

  7. TCP粘包问题的解决方案02——利用readline函数解决粘包问题

      主要内容: 1.read,write 与 recv,send函数. recv函数只能用于套接口IO ssize_t recv(int sockfd,void * buff,size_t len,i ...

  8. day31——recv工作原理、高大上版解决粘包方式、基于UDP协议的socket通信

    day31 recv工作原理 源码解释: Receive up to buffersize bytes from the socket. 接收来自socket缓冲区的字节数据, For the opt ...

  9. netty权威指南学习笔记三——TCP粘包&sol;拆包之粘包现象

    TCP是个流协议,流没有一定界限.TCP底层不了解业务,他会根据TCP缓冲区的实际情况进行包划分,在业务上,一个业务完整的包,可能会被TCP底层拆分为多个包进行发送,也可能多个小包组合成一个大的数据包 ...

随机推荐

  1. 构造函数&comma;const char&ast;与c&lowbar;str

    /******************************************************************************* * 版权所有: * 模 块 名: * ...

  2. 详解Adorner Layer&lpar;zz&rpar;

    首先,千万不要觉得Adorner离你很远,因为最简单的WPF界面也会用到Adorner.在WPF中,下面的几个很常见的功能,都是用Adorner实现的.     1. 光标(caret)     2. ...

  3. 1029&colon; &lbrack;JSOI2007&rsqb;建筑抢修 - BZOJ

    Description 小刚在玩JSOI提供的一个称之为“建筑抢修”的电脑游戏:经过了一场激烈的战斗,T部落消灭了所有z部落的入侵者.但是T部落的基地里已经有N个建筑设施受到了严重的损伤,如果不尽快修 ...

  4. Wix installer&colon; suppressing the License Dialog

    Reference Link:  http://blog.robseder.com/2014/02/20/more-on-wix-and-suppressing-the-license-dialog/ ...

  5. 关于无法使用python执行进入百度页面的代码修改

    前几天听了个坑爹的视频教学,按照你们的方法做了,但尼玛,执行下来各种问题啊: 首先进入页面,总是提示开发者模式,删了下次执行又挂了,于是乎我就找网上帖子解决问题,果然被我解决了 先装这两个文件,把浏览 ...

  6. 架构之CDN缓存

    CDN缓存 CDN主要解决将数据缓存到离用户最近的位置,一般缓存静态资源文件(页面,脚本,图片,视频,文件等).国内网络异常复杂,跨运营商的网络访问会很慢.为了解决跨运营商或各地用户访问问题,可以在重 ...

  7. 小程序-camera

    camera 使用这个组件使用手机的拍摄功能.实现如下操作 打开拍摄画面,在手机上半屏显示拍摄取景,下面有一个拍摄按钮.点击后,取景器位置显示拍摄画面,下面显示确定取消按钮. 确定后,下方的预览图片列 ...

  8. Git:常用命令(二)

    查看提交历史 git log 撤消操作 任何时候,你都有可能需要撤消刚才所做的某些操作.接下来,我们会介绍一些基本的撤消操作相关的命令.请注意,有些操作并不总是可以撤消的,所以请务必谨慎小心,一旦失误 ...

  9. ny58 最小步数

    最少步数 时间限制:3000 ms  |  内存限制:65535 KB 难度:4 描述 这有一个迷宫,有0~8行和0~8列: 1,1,1,1,1,1,1,1,1 1,0,0,1,0,0,1,0,1 1 ...

  10. 第六章 通过Service访问Pod&lpar;中&rpar;

    6.2 Cluster IP 底层实现 Cluster IP 是一个虚拟IP,是由K8s节点上的iptables规则管理的. 使用类似轮询的方法访问Pod. 6.3 DNS 访问Service 在Cl ...