张赐荣 | C# TCP 通信粘包、半包问题解决方法

C# TCP 通信粘包、半包问题解决方法

[作者:张赐荣,地址:https://www.cnblogs.com/netlog/p/16194812.html]

问题形成原因
TCP是一种面向连接,基于字节流的可靠流式传输协议。
因为TCP是流模式协议,面向字节流,所以没有数据块,发送的数据没有边界,好比一根水管,水流没有边界。
与UDP协议不同,UDP是基于报文的传输协议,UDP的消息是基于数据块的,发送端一次发送多少数据,客户端一次就要接收多少数据。
TCP则不同,发送端可以连续不断的发多次包,接收端可以一次接收,这样就产生了“数据粘连”的问题。
实际上准确的来说,基于流的传输协议根本没有“数据包”的概念,所谓数据边界,如何获取一个完整的包这样的问题都是应用层协议的问题。
因此,如何解决所谓“粘包、分包”问题就变成了“如何基于TCP传输模式设计一个应用层协议”的问题了。
本文用C#、易语言两种编程语言演示解决该问题的方法。
三种解决办法
TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:
1.发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
这种方法虽然实现较为复杂,但是依然推荐使用这种方法,用这种方法,主要是可靠,更通用,也避免了以下两种方法的缺点。
本文主要讲述这种方案的实现过程。
2.发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
虽然这种方式可以用最简单的办法解决粘包和拆包的问题,但这种固定缓冲区大小的方式增加了不必要的数据传输;当这种方式当发送的数据比较小时会使用空字符来弥补,所以这种方式就大大的增加了网络传输的负担,所以最不推荐这种方案。
3.可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
这种方法并不推荐,原因是因为TCP是面向流的;所以应该认为TCP传输的是字节流,任何一个字节都可能被传输;在这种情况下,特殊字符也不特殊了,想要和正常数据区分就非常困难。
解决方法(自定义通信协议)
这种方式是最完美的,无论发送端发送多块,连续发送多少数据,客户端都能完整的,可靠的接收下来,并且进行拆分,形成读立的数据包。
定义消息协议类
首先我们要新建一个消息协议类来封装数据报文,此类暂定由五个属性组成。
分别为
(1) = 1个字节(信令);
(2) = 1个字节 (参数);
(3) = 1个整数变量4个字节 (数据长度);
(4) = 1个字节数组 byte[] (数据正文)
(5) =1 个字节数组 byte[] ()多余数据;
消息协议类说明
(1) Command: 1个byte,这个属性占1个字节,可以放一个0到255的正整数,作为信令标志;
(2) Param: 1个byte,这个属性占1个字节,可以放一个0到255的正整数,作为参数标志;
那么信令标志和参数标志进行组合就多达 256*256=65536种变化,我们用它来自定义这个消息报文的命令标志,比如,0X0表示登录请求消息,1X1表示发送文字,1X2表示发送二进制文件,3X0表示退出等等,可以用来决定程序执行流程;
(3) DataLength,1个int,这个属性占4个字节,可以放一个0到2147483647的正整数,它表示后面(④=1个byte[])的长度,即实际发送数据在和的长度;
(4) MessageData: 1个 byte[],这个字节数组存着你要发送的全部数据内容消息体的字节,所以,你的消息体需要被转化为字节数组才可存放进去;
(5) MoreData: 1个byte[],这个字节数组存着一条完整信息之外多余的消息体字节。
在拆包过程中,如果要拆包的字节信息刚好是一个完整长度,自然用不到MoreData了,但是在网络传输中字节数据肯定不会永远那么刚好了,所以当拆包字节数据长度大于一个完整消息正文时,我们就把多余的byte放入MoreData当中,和下次新接收的字节数据依次拼合在一起,再次进行拆包的循环,保证数据的完整性和独立性。
消息协议类用法
1.消息发送方先定义消息协议类,Command、Param属性,也就是随便写2个数,至于这两数代表什么含义,你自己定就行,这是为了消息接收方接受到消息后根据这个码来执行相应的逻辑;
2.消息发送方再将消息“装入”消息协议类的MessageData属性,即有效数据内容;那么DataLength属性也就随之有了,因为有了实际消息就算的出它的长度,这应该不难理解;
3.消息发送方将封装好的消息协议类转为byte[]字节数组,发送给消息接收方,我们把这道步骤称作“封包”过程;
4.消息接收方接收到消息发送方发来的byte[]时,先判断这个byte[]长度是否大于6,为什么是6?
实际上就是判断数据是否大于(1)属性+(2)属性+(3)属性的长度之和(2个byte是2字节,1个int是4字节,2+6=6),
如果byte[]长度小于6,说明暂时还未收到有效的完整消息,则消息接收方就跳过本次循环,进入下一次继续循环累积接收数据。
5.消息接收方接收到消息发送方发来的byte[]长度如果大于等于6了,则将byte[]还原为消息协议类对象,因为>=6一个完整的数据包头数据就被接收了,可以由此解读出数据正文所需要的数据长度;
6.循环判断消息发送方发来的byte[]长度减去头部长度6之后的值是否大于等于消息协议类对象DataLength的值,如果大于等于,则把MessageData属性拆出来,这就得到了一个正正好好完整的消息,不多也不少。
我们就把这道过程称作拆包。,
7.那么MoreData这个多余的消息体字节是干啥用的呢?
在上一步当中,如果byte[]信息刚好是一个完整长度,自然用不到MoreData了,但是在网络传输中byte[]肯定不会永远那么刚好了,所以当byte[]长度大于一个完整消息时,我们就把多余的字节放入MoreData当中,和下次新接收的byte[]依次拼合在一起,再次进行拆包的循环,保证数据的完整性和独立性,拆包循环要始终进行道多余的字节长度减去解读出的头部长度不能大于等于数据正文的长度时,就停止循环,回到外层循环,继续接收数据,与之前多余的数据进行拼接,再一次解包。
代码演示
封装消息协议类代码
// 消息协议类由 【协议命令】+【协议参数】+【实际消息长度】+【实际消息内容】+【多余的消息内容】 组成
public sealed class MessageProtocol
{
public const int HEADLENGTH = 6;  // 协议首部长度(命令1字节+参数1字节+数据长度4字节=6字节)
private byte _Command = 0;
public byte Command { get => this._Command; }  // 协议信令标志 (值范围0~255)
private byte _Param = 0;
public byte Param { get => this._Param; }  // 信令参数标志(范围0~255)
private int _DataLength = 0;
public int DataLength { get => this._DataLength; }  // 有效载荷数据长度
private byte[] _MessageData = new byte[0];
public byte[] MessageData { get => this._MessageData; }  // 完整消息体
private byte[] _MoreData = new byte[0];
public byte[] MoreData { get => this._MoreData; }  // 完整消息体多余的数据
public MessageProtocol (byte[] buffer)  // 通过字节数组创建消息协议对象(相当于拆包过程)
{
if (buffer==null || buffer.Length < HEADLENGTH)  // 当要拆包的数据长度小于协议首部长度,就不进行拆包,因为根本无法得出需要的信息,比如有效数据长度
{
return;
}
MemoryStream ms = new MemoryStream(buffer);
BinaryReader br = new BinaryReader(ms);
try
{
this._Command = br.ReadByte();
this._Param = br.ReadByte();
this._DataLength = br.ReadInt32();
if (buffer.Length-HEADLENGTH>=this._DataLength)  // 如果要拆包的数据长度减去协议首部长度大于等于需要的数据长度就可以提取出实际的数据
{
this._MessageData = br.ReadBytes(this._DataLength);  // 读取DataLength长度的数据
}
if (buffer.Length-HEADLENGTH-this.DataLength>0)  // 如果要拆包的数据长度减去协议头长度减去实际数据的长度大于0就说明还有多余数据
{
this._MoreData = br.ReadBytes(buffer.Length-HEADLENGTH-this._DataLength);  // 读取多余数据
}
}
catch (Exception)
{
// 异常处理
}
br.Close();
ms.Close();
}
public MessageProtocol (byte command,byte param,byte[] messageData)  // 通过传入协议参数构造一个协议对象
{
this._Command = command;
this._Param = param;
this._MessageData = messageData;
this._DataLength = messageData.Length;
}
public byte[] GetBytes ()  // 将协议对象转换成二进制数据包(相当于封包过程)
{
try
{
byte[] bytes = null;
MemoryStream ms = new MemoryStream();
BinaryWriter bw = new BinaryWriter(ms);
bw.Write(this._Command);
bw.Write(this._Param);
bw.Write(this._DataLength);
bw.Write(this._MessageData);
bytes = ms.ToArray();
bw.Close();
ms.Close();
return (bytes);
}
catch (Exception)
{
return (new byte[0]);
}
}
public (byte Command,byte Param,int DataLength,byte[] MessageData) GetMessage ()  // 获取完整的一条消息(忽略多余字节部分)
{
(byte Command, byte Param, int DataLength, byte[] MessageData) returnValue = (this._Command,this._Param,this._MessageData.Length,this._MessageData);
return (returnValue);
}
public static (byte Command,byte Param,int DataLength) GetHeadInfo (byte[] buffer)  // 读取协议头部分
{
(byte Command,byte Param,int DataLength) returnValue = (0,0,0);
if (buffer == null || buffer.Length < HEADLENGTH)  // 如果数据长度小于协议头根本无法读出完整的协议头内容
{
return (returnValue);
}
returnValue.Command = buffer[0];
returnValue.Param = buffer[1];
returnValue.DataLength = BitConverter.ToInt32(buffer,2);
return (returnValue);
}
}
合并字节数组函数代码
public static byte[] CombineBytes(byte[] firstBytes, int firstIndex, int firstLength, byte[] secondBytes, int secondIndex, int secondLength)
{
byte[] bytes = null;
MemoryStream ms = new MemoryStream();
ms.Write(firstBytes, firstIndex, firstLength);
ms.Write(secondBytes, secondIndex, secondLength);
bytes = ms.ToArray();
ms.Close();
return (bytes);
}
接收消息函数
重点,主要演示如何通过循环接收一个完整的消息报文,代码如下:
public static int ReceiveData(Socket socket)  // 接收消息函数(传入一个socket对象)
{
MessageProtocol mp = null;
int ReceiveLength = 0;
byte[] staticReceiveBuffer = new byte[65536];  // 接收缓冲区(固定长度)
byte[] dynamicReceiveBuffer = new byte[] { };  // 累加数据缓存(不定长)
do
{
ReceiveLength = socket.Receive(staticReceiveBuffer);  // 同步接收数据
dynamicReceiveBuffer = CombineBytes(dynamicReceiveBuffer, 0, dynamicReceiveBuffer.Length, staticReceiveBuffer, 0, ReceiveLength);  // 将之前多余的数据与接收的数据合并,形成一个完整的数据包
if (ReceiveLength <= 0)  // 如果接收到的数据长度小于0(通常表示socket已断开,但也不一定,需要进一步判断,此处可以忽略)
{
Console.WriteLine("收到0字节数据");
break;  // 终止接收循环
}
else if (dynamicReceiveBuffer.Length < MessageProtocol.HEADLENGTH)  // 如果缓存中的数据长度小于协议头长度,则继续接收
{
continue;  // 跳过本次循环继续接收数据
}
else  // 缓存中的数据大于等于协议头的长度(dynamicReadBuffer.Length >= 6)
{
var headInfo = MessageProtocol.GetHeadInfo(dynamicReceiveBuffer);  // 解读协议头的信息
while (dynamicReceiveBuffer.Length - MessageProtocol.HEADLENGTH >= headInfo.DataLength)  // 当缓存数据长度减去协议头长度大于等于实际数据的长度则进入循环进行拆包处理
{
mp = new MessageProtocol(dynamicReceiveBuffer);  // 拆包
dynamicReceiveBuffer = mp.MoreData;  // 将拆包后得出多余的字节付给缓存变量,以待下一次循环处理数据时使用,若下一次循环缓存数据长度不能构成一个完整的数据包则不进入循环跳到外层循环继续接收数据并将本次得出的多余数据与之合并重新拆包,依次循环。
headInfo = MessageProtocol.GetHeadInfo(dynamicReceiveBuffer);  // 从缓存中解读出下一次数据所需要的协议头信息,已准备下一次拆包循环,如果数据长度不能构成协议头所需的长度,拆包结果为0,下一次循环则不能成功进入,跳到外层循环继续接收数据合并缓存形成一个完整的数据包
Console.WriteLine("Command:{0},Param:{1},Data:{2}", mp.Command, mp.Param,Encoding.UTF8.GetString(mp.MessageData));  // 显示对方发来的信息(假定对方采用UTF-8编码发来文本)
} // 拆包循环结束
}
} while (ReceiveLength > 0);
return (0);
}
服务器和客户端的完整通信案例
服务器用 C# 编写,客户端用易语言编写
C# 服务器完整代码
以下给出C# 写的一个简易服务器的完整代码,功能很简单,接收客户发来的文本信息,将文本倒置后发送回去。
// 服务器代码 (没有过多测试,有问题请见谅)
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace CRApp
{
class Program
{
static int Main(string[] args)
{
Socket server = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
server.Bind(new IPEndPoint(IPAddress.Any, 8888));
server.Listen(10);
Task.Run(()=>AcceptClient(server)).Wait(10);
Console.WriteLine("服务器以启动,IP:\'{0}\',正在等待客户端连接 ...\r\n按 Enter 键退出 ->",server.LocalEndPoint.ToString());
Console.ReadLine();
server.Close();
Console.WriteLine("服务器已停止运行");
return (0);
}
public static void AcceptClient (Socket server)
{
Socket client = null;
do
{
try
{
client = server.Accept();
if (client!=null)
{
Console.WriteLine("客户 \'{0}\' 已连接。", client.RemoteEndPoint.ToString());
Task.Run(()=>ReceiveData(client)).Wait(100);
}
}
catch (Exception ex)
{
client = null;
}
} while (client!=null);
}
public static void ReceiveData(Socket socket)  // 接收消息函数(传入一个socket对象)
{
try
{
MessageProtocol mp = null;
int ReceiveLength = 0;
byte[] staticReceiveBuffer = new byte[65536];  // 接收缓冲区(固定长度)
byte[] dynamicReceiveBuffer = new byte[] { };  // 累加数据缓存(不定长)
do
{
ReceiveLength = socket.Receive(staticReceiveBuffer);  // 同步接收数据
dynamicReceiveBuffer = CombineBytes(dynamicReceiveBuffer, 0, dynamicReceiveBuffer.Length, staticReceiveBuffer, 0, ReceiveLength);  // 将之前多余的数据与接收的数据合并,形成一个完整的数据包
if (ReceiveLength <= 0)  // 如果接收到的数据长度小于0(通常表示socket已断开,但也不一定,需要进一步判断,此处可以忽略)
{
break;  // 终止接收循环
}
else if (dynamicReceiveBuffer.Length < MessageProtocol.HEADLENGTH)  // 如果缓存中的数据长度小于协议头长度,则继续接收
{
continue;  // 跳过本次循环继续接收数据
}
else  // 缓存中的数据大于等于协议头的长度(dynamicReadBuffer.Length >= 6)
{
var headInfo = MessageProtocol.GetHeadInfo(dynamicReceiveBuffer);  // 解读协议头的信息
while (dynamicReceiveBuffer.Length - MessageProtocol.HEADLENGTH >= headInfo.DataLength)  // 当缓存数据长度减去协议头长度大于等于实际数据的长度则进入循环进行拆包处理
{
mp = new MessageProtocol(dynamicReceiveBuffer);  // 拆包
dynamicReceiveBuffer = mp.MoreData;  // 将拆包后得出多余的字节付给缓存变量,以待下一次循环处理数据时使用,若下一次循环缓存数据长度不能构成一个完整的数据包则不进入循环跳到外层循环继续接收数据并将本次得出的多余数据与之合并重新拆包,依次循环。
headInfo = MessageProtocol.GetHeadInfo(dynamicReceiveBuffer);  // 从缓存中解读出下一次数据所需要的协议头信息,已准备下一次拆包循环,如果数据长度不能构成协议头所需的长度,拆包结果为0,下一次循环则不能成功进入,跳到外层循环继续接收数据合并缓存形成一个完整的数据包
if (mp.Command==0&&mp.Param==0)
{
socket.Send(new MessageProtocol(0,0,Encoding.UTF8.GetBytes("Goodbye\r\n再见!")).GetBytes());
return ;
}
ResponseRequest(socket,mp.GetMessage());
} // 拆包循环结束
}
} while (ReceiveLength > 0);
}
finally
{
Console.WriteLine("客户 \'{0}\' 已离开", socket.RemoteEndPoint.ToString());
socket.Close();
}
}
public static void ResponseRequest (Socket client,(byte Command,byte Param,int DataLength,byte[] MessageData) message)
{
string text = Encoding.UTF8.GetString(message.MessageData);
Console.WriteLine("客户 \'{0}\' 发来信息: \'{1}\'。",client.RemoteEndPoint.ToString(),text);
text = new String(text.Reverse().ToArray());
client.Send(new MessageProtocol(1,1,Encoding.UTF8.GetBytes(text)).GetBytes());
}
public static byte[] CombineBytes(byte[] firstBytes, int firstIndex, int firstLength, byte[] secondBytes, int secondIndex, int secondLength)
{
byte[] bytes = null;
MemoryStream ms = new MemoryStream();
ms.Write(firstBytes, firstIndex, firstLength);
ms.Write(secondBytes, secondIndex, secondLength);
bytes = ms.ToArray();
ms.Close();
return (bytes);
}
}
public sealed class MessageProtocol
{
public const int HEADLENGTH = 6;
private byte _Command = 0;
public byte Command { get => this._Command; }
private byte _Param = 0;
public byte Param { get => this._Param; }
private int _DataLength = 0;
public int DataLength { get => this._DataLength; }
private byte[] _MessageData = new byte[0];
public byte[] MessageData { get => this._MessageData; }
private byte[] _MoreData = new byte[0];
public byte[] MoreData { get => this._MoreData; }
public MessageProtocol(byte[] buffer)
{
if (buffer == null || buffer.Length < HEADLENGTH)
{
return;
}
MemoryStream ms = new MemoryStream(buffer);
BinaryReader br = new BinaryReader(ms);
try
{
this._Command = br.ReadByte();
this._Param = br.ReadByte();
this._DataLength = br.ReadInt32();
if (buffer.Length - HEADLENGTH >= this._DataLength)
{
this._MessageData = br.ReadBytes(this._DataLength);
}
if (buffer.Length - HEADLENGTH - this.DataLength > 0)
{
this._MoreData = br.ReadBytes(buffer.Length - HEADLENGTH - this._DataLength);
}
}
catch (Exception)
{
// 异常处理
}
br.Close();
ms.Close();
}
public MessageProtocol(byte command, byte param, byte[] messageData)
{
this._Command = command;
this._Param = param;
this._MessageData = messageData;
this._DataLength = messageData.Length;
}
public byte[] GetBytes()
{
try
{
byte[] bytes = null;
MemoryStream ms = new MemoryStream();
BinaryWriter bw = new BinaryWriter(ms);
bw.Write(this._Command);
bw.Write(this._Param);
bw.Write(this._DataLength);
bw.Write(this._MessageData);
bytes = ms.ToArray();
bw.Close();
ms.Close();
return (bytes);
}
catch (Exception)
{
return (new byte[0]);
}
}
public (byte Command, byte Param, int DataLength, byte[] MessageData) GetMessage()
{
(byte Command, byte Param, int DataLength, byte[] MessageData) returnValue = (this._Command, this._Param, this._MessageData.Length, this._MessageData);
return (returnValue);
}
public static (byte Command, byte Param, int DataLength) GetHeadInfo(byte[] buffer)
{
(byte Command, byte Param, int DataLength) returnValue = (0, 0, 0);
if (buffer == null || buffer.Length < HEADLENGTH)
{
return (returnValue);
}
returnValue.Command = buffer[0];
returnValue.Param = buffer[1];
returnValue.DataLength = BitConverter.ToInt32(buffer, 2);
return (returnValue);
}
}
}
易语言客户端完整代码
' 程序集代码
.版本 2
.支持库 sock
.支持库 EThread
.程序集 程序集
.程序集变量 客户端, 网络客户端
.子程序 _启动子程序, 整数型, , 本子程序在程序启动后最先执行
.如果真 (客户端.连接 (“127.0.0.1”, 8888) = 假)
信息框 (“服务器连接失败”, 64, “提示”, )
返回 (0)
.如果真结束
标准输出 (#标准输出设备, “正在接收数据,请稍等...” + #换行符 + “按 回车键 退出” + #换行符)
启动线程 (&取回数据, , )
客户端.发送 (封包 (1, 1, 文本到UTF8 (“Hello World!”)), 0)
客户端.发送 (封包 (1, 1, 文本到UTF8 (“世界,你好!”)), 0)
客户端.发送 (封包 (1, 1, 文本到UTF8 (“千江有水千江月,万里无云万里天。”)), 0)
客户端.发送 (封包 (1, 1, 文本到UTF8 (“海内存知己,天涯若比邻。”)), 0)
客户端.发送 (封包 (1, 1, 文本到UTF8 (“!啦功成试测信通器务服”)), 0)
' 客户端.发送 (封包 (0, 0, 取空白字节集 (0)), 0)
标准输入 ()
客户端.断开 ()
返回 (0)
.子程序 取回数据, , , 取回发送端发来的数据
.局部变量 是否成功, 逻辑型, , , 获取数据是否成功
.局部变量 读入长度, 整数型
.局部变量 缓冲区, 字节集
.局部变量 动态缓存, 字节集
.局部变量 拆包结果, 消息协议结构
.循环判断首 ()
缓冲区 = 客户端.接收 (0, 是否成功)
读入长度 = 取字节集长度 (缓冲区)
动态缓存 = 动态缓存 + 缓冲区
.如果 (取字节集长度 (动态缓存) < #协议头长度)
到循环尾 ()
.否则
拆包结果 = 拆包 (动态缓存)
.判断循环首 (取字节集长度 (动态缓存) - #协议头长度 ≥ 拆包结果.数据长度)
标准输出 (#标准输出设备, UTF8到文本 (拆包结果.数据正文) + #换行符)
' 信息框 (“命令:” + 到文本 (拆包结果.命令) + “,参数:” + 到文本 (拆包结果.参数) + “数据长度:” + 到文本 (拆包结果.数据长度) + “,数据:” + UTF8到文本 (拆包结果.数据正文) + “,多余数据长度:” + 到文本 (取字节集长度 (拆包结果.多余数据)), 0, , )
动态缓存 = 拆包结果.多余数据
拆包结果 = 拆包 (动态缓存)
.判断循环尾 ()
.如果结束
.循环判断尾 (是否成功)
.子程序 封包, 字节集, 公开, 封装要发送的数据包
.参数 命令, 字节型, , 准备执行的命令
.参数 参数, 字节型, , 附加的参数
.参数 数据, 字节集, , 要发送的数据
.局部变量 结果, 字节集
结果 = 结果 + 到字节集 (命令)
结果 = 结果 + 到字节集 (参数)
结果 = 结果 + 到字节集 (取字节集长度 (数据))
结果 = 结果 + 数据
返回 (结果)
.子程序 拆包, 消息协议结构, 公开, 将字节集消息报文转换为消息结构
.参数 报文, 字节集, , 要拆包的数据
.局部变量 结果, 消息协议结构
.如果真 (取字节集长度 (报文) < #协议头长度)
返回 (结果)
.如果真结束
结果.命令 = 报文 [1]
结果.参数 = 报文 [2]
结果.数据长度 = 取字节集数据 (报文, #整数型, 3)
结果.数据正文 = 取字节集中间 (报文, 7, 结果.数据长度)
结果.多余数据 = 取字节集中间 (报文, 7 + 结果.数据长度, 取字节集长度 (报文) - 结果.数据长度 - 6)
返回 (结果)
' 自定义数据类型代码
.版本 2
.数据类型 消息协议结构, 公开, 消息协议结构体
.成员 命令, 字节型, , , 准备执行的命令
.成员 参数, 字节型, , , 命令附加参数
.成员 数据长度, 整数型, , , 数据正文的长度
.成员 数据正文, 字节集, , , 数据体
.成员 多余数据, 字节集, , , 多余的数据
' 定义常量代码
.版本 2
.常量 协议头长度, "6", , 消息协议首部长度(命令1字节+参数1字节+数据长度4字节=6字节)
结语
到此为止,本文就写完了,希望本文能给你带来一些启发,期待大家做出高质量网络程序。
本文不正之处,欢迎批评指正。
谢谢!
部分参考资料:
[1] 《怎么解决TCP网络传输「粘包」问题? · 知乎》: https://www.zhihu.com/question/20210025
[2] 《硬核图解 | tcp为什么会粘包?背后的原因让人暖心 · SegmentFault》: https://segmentfault.com/a/1190000039691657
[3] 《Socket 类 (System.Net.Sockets) · MSDN》: https://docs.microsoft.com/zh-cn/dotnet/api/system.net.sockets.socket
 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
.版本 2 .支持库 spec .支持库 sock .程序集 窗口程序集_启动窗口 .子程序 _按钮2_被单击 客户1.发送数据 (取重复字节集 (10000, 到字节集 (“1”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (20000, 到字节集 (“2”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (30000, 到字节集 (“3”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (40000, 到字节集 (“4”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (50000, 到字节集 (“5”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (60000, 到字节集 (“6”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (70000, 到字节集 (“7”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (80000, 到字节集 (“8”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (90000, 到字节集 (“9”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (100000, 到字节集 (“0”)) + 到字节集 (“结尾符”)) .子程序 _服务器1_数据到达 .局部变量 取回数据, 字节集 .局部变量 数据数组, 文本型, , "0" .局部变量 次数, 整数型 .局部变量 临时数据, 字节集, 静态 .局部变量 得到的封包, 文本型 取回数据 = 服务器1.取回数据 () .判断开始 (取字节集右边 (取回数据, 6) ≠ 到字节集 (“结尾符”))  ' 6为结尾符的长度;这里检测封包是否全部发送完毕,如果没有发送完毕就会把发送来的封包数据进行累加     临时数据 = 临时数据 + 取回数据 .默认     临时数据 = 临时数据 + 取回数据  ' 检测到结尾符出现,说明数据已经发送完毕,这里把最后发送来的带有结尾符的数据加上就OK了。     临时数据 = 子字节集替换 (临时数据, 到字节集 (“结尾符”), , 取字节集长度 (临时数据) - 6, 6)  ' 6为结尾符的长度;这里把结尾符替换尾空,剩余数据尾完整的纯净数据。     数据数组 = 分割文本 (到文本 (临时数据), “分隔符”, )  ' 这里把收到的数据进行分割处理,无论服务器发送了多少次,都统一按分隔符分割     调试输出 (“封包数量:” + 到文本 (取数组成员数 (数据数组)))     .计次循环首 (取数组成员数 (数据数组), 次数)         得到的封包 = 数据数组 [次数]  ' 这里得到分割后的封包文本。         调试输出 (“第” + 到文本 (次数) + “个封包的大小:” + 到文本 (取文本长度 (得到的封包)))  ' 这里的大小和上面发送封包的大小相同,可以看到封包的分割次数。     .计次循环尾 ()     临时数据 = {  } .判断结束 .子程序 __启动窗口_创建完毕 客户1.连接 (取本机名 (), 8888)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值