切换导航
{{systemName}}
{{ info.Title }}
{{info.Title}}
{{ menu.Title }}
{{menu.Title}}
登录
|
退出
搜索
C# 自定义TCP传输协议以及封包拆包、解决粘包问题(网络应用层协议)
作者:ych
网络应用层协议,通俗一点的讲,它是一种基于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,还可能是更多意想不到的情况...... 所以网络应用层协议就是为了解决这两个问题而存在的,当然为消息包加密也是它的另一个主要目的。 网络应用层协议的格式一般都是:消息头+消息体,消息头的长度是固定的,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
listPlayer = new List
(); private static Socket sTemp; ///
///绑定地址并监听 ///
///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()); } } ///
///监听用户连接 ///
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
listMessage = new List
(); ///
///创建一个SocketClient实例 ///
///ip地址 端口 类型默认为TCP public static void CreateInstance(string ipStr, int iPort) { ip = ipStr; port = iPort; socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); ConnectServer(); } ///
///连接服务器 ///
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()); } } ``` #### 第四步:封包以及发送消息包 ``` ///
/// 构建消息数据包 ///
///
消息校验码,判断消息开始 ///
用户登录成功之后获得的身份ID ///
主命令 ///
子命令 ///
加密方式 ///
消息内容(string数组) ///
返回构建完整的数据包
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; } ///
///发送信息 ///
public static void SendMessage(byte[] sendBytes) { //确定是否连接 if (socketClient.Connected) { //获取远程终结点的IP和端口信息 IPEndPoint ipe = (IPEndPoint)socketClient.RemoteEndPoint; socketClient.Send(sendBytes, sendBytes.Length, 0); } } ``` #### 第五步:接收消息以及解析消息包 ``` ///
///接收消息 ///
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); } } ///
/// 解析消息包 ///
///
消息头 ///
消息体 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(); } ```
相关推荐
WPF中实现两个窗口之间传值
SharpPcap抓包工具
C#使用selenium实现爬虫
.NET跨端大杀器MAUI基础学习
很多开发的同学碰到.net framework或.net core升级到最新框架的问题,如何解决?
Unity通过构造函数实现依赖注入
WPF简单MVVM
js加密处理实战
评论区
先去登录
版权所有:机遇屋在线 Copyright © 2021-2025 jiyuwu Co., Ltd.
鲁ICP备16042261号-1