C#下利⽤封包、拆包原理解决Socket粘包、半包问题(新⼿篇)
介于⽹络上充斥着⼤量的含糊其辞的Socket初级教程,扰乱着新⼿的学习⽅向,我来扼要的教⼀下新⼿应该怎么合理的处理Socket这个玩意⼉。
⼀般来说,教你C#下Socket编程的⽼师,很少会教你如何解决Socket粘包、半包问题。
更甚⾄,某些师德有问题的⽼师,根本就没跟你说过Socket的粘包、半包问题是什么玩意⼉。
直到有⼀天,你的Socket程序在传输信息时出现了你预期之外的结果(多余的信息、不完整的信息、乱码、Bug等等)。
任你喊了⼀万遍“喔嚓”,依旧是不知道问题出在哪⼉!
好了,不说废话了,进⼊正题,包教包会,学不会,送路费。
如果你读到这篇⽂章了,想必你已经遇到了以上问题,情况再理想⼀点⼉,其实你在原理上已经知道怎么解决这个问题了,只是不知道怎么动⼿。
那么,⾸先,你需要新建⼀个【消息协议类】,这个类我们暂定由5⼤属性组成,分别是:
【(①=1个byte)(②=1个byte])(③=1个int)(④=1个byte[])(⑤=1个byte[])】
解释⼀下这个【消息协议类】:
(①=1个byte):这个属性占1个字节,可以放⼀个0到254的正整数,我称作1号标志;
(②=1个byte):这个属性占1个字节,可以放⼀个0到254的正整数,我称作2号标志。
那么1号标志和2号标志就有多达255×255个组合,我们⽤它来⾃定义这个消息对象的标志,⽐如,0-0表⽰登录请求消息,1-1表⽰物理攻击,1-2表⽰魔法攻击,3-0表⽰坐标移动;
(③=1个int):这个属性占4个字节,可以放⼀个0到2147483647的正整数,它表⽰后⾯(④=1个byte[])的长度,即实际发送的实质内容的长度;
(④=1个byte[]):这个属性存着你要发送的全部实质内容消息体的字节,所以,你的消息体需要被转化为字节数组才可存放进去;
(⑤=1个byte[]):这个属性存着⼀条完整信息之外【多余的消息体字节】。那么问题来了,什么是【多余的消息体字节】?先不忙往下看,⾃⼰思考⼀下这个问题,再继续往下看。
然后我们来解释⼀下这个【消息协议类】具体怎么⽤。
1,【消息发送⽅】先定义【消息协议类】①②属性,也就是随便写2个数,⾄于这两数代表什么含义,你⾃⼰定就⾏,这是为了消息接收⽅接受到消息后根据这个码来执⾏相应的逻辑;
2,【消息发送⽅】再将消息“装⼊”【消息协议类】的④属性,即实质消息内容;那么③属性也就随之有了,因为有了实质消息就算的出它的长度,这应该不难理解吧;
3,【消息发送⽅】将封装好的【消息协议类】转为byte[],发送给【消息接收⽅】,我们把这道⼯序称作【封包】;
------------------------------------------------------
4,【消息接收⽅】接收到【消息发送⽅】发来的byte[]时,先判断这个byte[]长度是否⼤于6,为什么是6?其实就是否⼤于①属性+②属性+③属性的长度之和(2个byte是2字节,1个int是4字节,2+6=6),
如果byte[]长度⼩于6,说明没收到有效消息,则【消息接收⽅】就循环继续接收;
5,【消息接收⽅】接收到【消息发送⽅】发来的byte[]长度如果⼤于等于6了!!则将byte[]还原为【消息协议类】对象;
6,循环判断【消息发送⽅】发来的byte[]长度减去6之后的值是否⼤于等于【消息协议类】对象③的值,
如果⼤于等于,则把④属性拆出来,这就得到了⼀个刚刚好完整的消息,不多也不少。我们就把这道⼯序称作【拆包】。,
7,那么⑤这个【多余的消息体字节】是⼲嘛⽤的呢??上⼀步当中,如果byte[]信息刚好是⼀个完整长度,⾃然⽤不到⑤了,但是在⽹络传输中byte[]肯定不会永远那么刚好了,所以当byte[]长度⼤于⼀个完整消息时,我们就把多余的byte放⼊⑤当中,和下次新接收的byte[]依次拼合在⼀起,再次进⾏【拆包】的循环,保证数据的完整性和独⽴性。
8,好了,以上过程就是利⽤封包、拆包原理解决Socket粘包、半包问题,接下来你可以在发送⽅以⾮常频繁的发送频率来发送信息,接收⽅依然会规规矩矩完完整整的以正确的姿势来接收消息了。最下⾯是要点代码,相信聪明的你⼀定学会了,如果还没学会,可以加我QQ:119945778,包教包会,不然我还得送路费不是...
9,补充⼀点,很多看了我这个帖⼦的朋友,是做硬件设备的,整了半天没整明⽩,这篇的逻辑主要是做IM、⽹络游戏等应⽤居多的⼀种逻辑,⽽硬件开发中很多都是⼀条消息使⽤ \r\n 来作结束标记的,实现逻辑其实更简单粗暴⼀些,不需要像现在这么⿇烦的,这类简单粗暴的处理办法我以为⽐较容易参考资料,所以没有兼顾,今天(2021-01-23)⼜有个朋友加我QQ问这⽅⾯的,已经有⼏⼗个了,所以我⼲脆再写⼀篇 \r\n 作结束标记的吧↓
《C#下利⽤封包、拆包原理解决Socket粘包、半包问题(\r\n作结尾篇)》wwwblogs/sungong1987/p/14317579.html
下⾯是源码时间:
1///<summary>
2///【消息协议】=【协议⼀级标志】+【协议⼆级标志】+【实际消息长度】+【实际消息内容】+【多余的消息内容】
3///</summary>
4public class MessageXieYi
5    {
6#region⾃定义
7#region协议⼀级标志,值 = (0 ⾄ 254 )
8private byte xieYiFirstFlag;
9///<summary>
10///协议类别,值 = ( 0 直 254 )
11///</summary>
12public byte XieYiFirstFlag
13        {
14get { return xieYiFirstFlag; }
15set { xieYiFirstFlag = value; }
16        }
17#endregion
18
19#region协议⼆级标志,值 = (0 ⾄ 254 )
20private byte xieYiSecondFlag;
21///<summary>
22///协议⼆级标志,值 = (0 ⾄ 254 )
23///</summary>
24public byte XieYiSecondFlag
25        {
26get { return xieYiSecondFlag; }
怪我一错再错学不会
27set { xieYiSecondFlag = value; }
28        }
29#endregion
30
31#region实际消息长度
32private int messageContentLength;
33///<summary>
34///实际消息长度
35///</summary>
36public int MessageContentLength
37        {
38get { return messageContentLength; }
39set { messageContentLength = value; }
40        }
41#endregion
42
43#region实际消息内容
44private byte[] messageContent = new byte[] { };
45///<summary>
46///实际消息内容
47///</summary>
48public byte[] MessageContent
49        {
50get { return messageContent; }
51set { messageContent = value; }
52        }
53#endregion
54
55#region多余的Bytes
56private byte[] duoYvBytes;
57///<summary>
58///多余的Bytes
59///</summary>
60public byte[] DuoYvBytes
61        {
62get { return duoYvBytes; }
63set { duoYvBytes = value; }
64        }
65
66#endregion
67#endregion
68
69#region构造函数两个
70public MessageXieYi()
71        {
72//
73        }
74
75public MessageXieYi(byte _xieYiFirstFlage, byte _xieYiSecondFlage, byte[] _messageContent)
76        {
77            xieYiFirstFlag = _xieYiFirstFlage;
78            xieYiFirstFlag = _xieYiSecondFlage;
79            messageContentLength = _messageContent.Length;
80            messageContent = _messageContent;
81        }
82#endregion
83
84#region MessageXieYi 转换为 byte[]
85///<summary>
86/// MessageXieYi 转换为 byte[]
87///</summary>
88///<returns></returns>
89public byte[] ToBytes()
90        {
91byte[] _bytes; //⾃定义字节数组,⽤以装载消息协议
92
93using (MemoryStream memoryStream = new MemoryStream()) //创建内存流
94            {
95                BinaryWriter binaryWriter = new BinaryWriter(memoryStream); //以⼆进制写⼊器往这个流⾥写内容
96
97                binaryWriter.Write(xieYiFirstFlag); //写⼊协议⼀级标志,占1个字节
98                binaryWriter.Write(xieYiSecondFlag); //写⼊协议⼆级标志,占1个字节
99                binaryWriter.Write(messageContentLength); //写⼊实际消息长度,占4个字节
100
101if (messageContentLength > 0)
102                {
103                    binaryWriter.Write(messageContent); //写⼊实际消息内容
104                }
105
106                _bytes = memoryStream.ToArray(); //将流内容写⼊⾃定义字节数组
107
108                binaryWriter.Close(); //关闭写⼊器释放资源
109            }
110
111return _bytes; //返回填充好消息协议对象的⾃定义字节数组
112        }
113#endregion
114
115#region byte[] 转换为 MessageXieYi
116///<summary>
117/// byte[] 转换为 MessageXieYi
118///</summary>
119///<param name="buffer">字节数组缓冲器。</param>
120///<returns></returns>
121public static MessageXieYi FromBytes(byte[] buffer)
122        {
123int bufferLength = buffer.Length;
124
125            MessageXieYi messageXieYi = new MessageXieYi();
126
127using (MemoryStream memoryStream = new MemoryStream(buffer)) //将字节数组填充⾄内存流
128            {
129                BinaryReader binaryReader = new BinaryReader(memoryStream); //以⼆进制读取器读取该流内容
130
131                messageXieYi.xieYiFirstFlag = binaryReader.ReadByte(); //读取协议⼀级标志,读1个字节
132                messageXieYi.xieYiSecondFlag = binaryReader.ReadByte(); //读取协议⼆级标志,读1个字节
133                ssageContentLength = binaryReader.ReadInt32(); //读取实际消息长度,读4个字节
134
135//如果【进来的Bytes长度】⼤于【⼀个完整的MessageXieYi长度】
136if ((bufferLength - 6) > ssageContentLength)
137                {
138                    ssageContent = binaryReader.ssageC
ontentLength); //读取实际消息内容,从第7个字节开始读139                    messageXieYi.duoYvBytes = binaryReader.ReadBytes(bufferLength - 6 - ssageContentLength);
140                }
141
142//如果【进来的Bytes长度】等于【⼀个完整的MessageXieYi长度】
143if ((bufferLength - 6) == ssageContentLength)
144                {
145                    ssageContent = binaryReader.ssageContentLength); //读取实际消息内容,从第7个字节开始读146                }
147
148                binaryReader.Close(); //关闭⼆进制读取器,是否资源
149            }
150
151return messageXieYi; //返回消息协议对象
152        }
153#endregion
154    }
1///<summary>
2///按照先后顺序合并字节数组类
3///</summary>
4public class CombineBytes
5    {
6///<summary>
7///按照先后顺序合并字节数组,并返回合并后的字节数组。
8///</summary>
9///<param name="firstBytes">第⼀个字节数组</param>
10///<param name="firstIndex">第⼀个字节数组的开始截取索引</param>
11///<param name="firstLength">第⼀个字节数组的截取长度</param>
12///<param name="secondBytes">第⼆个字节数组</param>
13///<param name="secondIndex">第⼆个字节数组的开始截取索引</param>
14///<param name="secondLength">第⼆个字节数组的截取长度</param>
15///<returns></returns>
16public static byte[] ToArray(byte[] firstBytes, int firstIndex, int firstLength,  byte[] secondBytes, int secondIndex, int secondLength)
17        {
18using (MemoryStream ms = new MemoryStream())
19            {
20                BinaryWriter bw = new BinaryWriter(ms);
21                bw.Write(firstBytes, firstIndex, firstLength);
22                bw.Write(secondBytes, secondIndex, secondLength);
23
24                bw.Close();
25                bw.Dispose();
26
27return ms.ToArray();
28            }
29        }
30    }
1byte[] msgBytes = Encoding.Unicode.GetBytes("要发送的消息");
2 MessageXieYi msgXY = new MessageXieYi(0, 0, msgBytes);
3 networkStream.Write(msgXY.ToBytes(), 0, msgXY.ToBytes().Length);
1#region线程执⾏体,接收消息
2///<summary>
3///线程执⾏体,接收消息
4///</summary>
5///<param name="obj">传递给线程执⾏体的⽤户名,⽤以与⽤户通信</param>
6private void ThreadReceive(object obj)
7        {
8//通过⽤户名出已经保存在哈希表⾥的Socket
9            Socket savedSocket = hashtable_UserNameToSocket[obj] as Socket;
10
11            MessageXieYi msgXY = new MessageXieYi();
12
13byte[] buffer = new byte[64];//定义⼀个⼤⼩为64的缓冲区
14//byte[] receivedBytes = new byte[] { };
15byte[] newBuffer = new byte[] { };//⼤⼩可变的缓存器
16
17int receivedLength;
18int availableLength;//没什么实际意义,就是为了⽅便理解Socket传输机制
19
20while (true)
21            {
22try
23                {
24                    buffer = new byte[64];
25
26for (int i = 0; i < 10; i++)
27                    {
28                        availableLength = savedSocket.Available;
29
30                        Console.WriteLine("【循环判断有多少可读Bytes】savedSocket.Available[" + i + "]=" + availableLength);//没实际意义,就是来个直观感受Socket的原理
31                    }
32
33                    Console.WriteLine("【可变缓存器⼤⼩】newBuffer.Length=" + newBuffer.Length);
34
35                    receivedLength = savedSocket.Receive(buffer);
36
37                    Console.WriteLine("【接收到数据】buffer.Length=" + receivedLength);
38
39                    newBuffer = CombineBytes.ToArray(newBuffer, 0, newBuffer.Length, buffer, 0, receivedLength);
40
41                    Console.WriteLine("【将接收到的数据追加在newBuffer后】newBuffer.Length=" + newBuffer.Length);
42
43if (newBuffer.Length < 6)
44                    {
45                        Console.WriteLine("newBuffer.Length=" + newBuffer.Length + "< 6 \t -> \t continue");
46continue;
47                    }
48else//newBuffer.Length >= 6
49                    {
50//取msgXY包头部分
51                        msgXY = MessageXieYi.FromBytes(newBuffer);
52int firstFlag = msgXY.XieYiFirstFlag;
53int secondFlag = msgXY.XieYiSecondFlag;
54int msgContentLength = msgXY.MessageContentLength;
55
56
57//判断去掉msgXY包头剩下的长度是否达到可以取包实质内容
58while ((newBuffer.Length - 6) >= msgContentLength)
59                        {
60                            Console.WriteLine("【newBuffer去掉包头的长度=" + (newBuffer.Length - 6) + "】>=【" + "包实质内容长度=" + msgContentLength + "】");
61                            msgXY = null;
62                            msgXY = MessageXieYi.FromBytes(newBuffer);
63                            Console.WriteLine("\n【拆包】=" + Encoding.Unicode.GetString(msgXY.MessageContent) + "\n");
64
65                            newBuffer = msgXY.DuoYvBytes;
66                            Console.WriteLine("【剩余的newBuffer】newBuffer.Length=" + newBuffer.Length);
67
68if (newBuffer.Length >= 6)
69                            {
70                                msgXY = MessageXieYi.FromBytes(newBuffer);
71                                firstFlag = msgXY.XieYiFirstFlag;
72                                secondFlag = msgXY.XieYiSecondFlag;
73                                msgContentLength = msgXY.MessageContentLength;
74continue;
75                            }
76else
77                            {
78break;
79                            }
80                        }
81                    }
82
83                    availableLength = savedSocket.Available;
84                    Console.WriteLine("savedSocket.Available=" + availableLength + "\n\n\n\n");
85
86continue;
87                }
88catch
89                {
90//异常处理
91                }
92            }
93        }
94#endregion