热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Tcp网络通讯详解二(解决分包粘包)

解决分包粘包系统缓冲区要想知道为什么在Tcp通讯中会存在分包粘包的现象,首先你必须先了解Tcp网络通讯的消息传播机制,而系统缓冲区将是不得不讲的一个话题,那么什么是系统缓冲区呢?其




解决分包粘包

系统缓冲区


要想知道为什么在Tcp通讯中会存在分包粘包的现象,首先你必须先了解Tcp网络通讯的消息传播机制,而系统缓冲区将是不得不讲的一个话题,那么什么是系统缓冲区呢?其实就是接到对端信息数据的时候,操作系统会将数据存入到Socket的接收缓冲区中,而在这一段时间,系统缓冲区完全是由操作系统进行操作,程序并不能直接操作它们,只能通过Socket.Receive();Socket.Send等方法来间接进行操作。其中,Socket.Receive()方法只是把接收缓冲区的数据提取出来, 比如调用Receive(readBuff,0,2) , 接收2个字节的数据到了用户缓冲区readbuff,当系统的接收缓冲区为空, Receive方法会被阻塞, 直到里面有数据。同样地, Socket的Send方法只是把数据写入到发送缓冲区里, 具体的发送过程由操作系统负责。当操作系统的发送缓冲区满了, Send方法将会阻塞。



粘包半包现象


粘包



  • 如果发送端快速发送多条数据,接收端没有及时调用Receive,那么数据便会在接收端的缓冲区中累积。客户端先发送“ 1、2、3、4"四个字节的数据,紧接看又发送“ 5、6、7、8"四个字节的数据。等到服务端调用Receive时,服务端操作系统巳经将接收到的数据全部写入缓冲区,共接收到8个数据。这样一来,明明对方发送的是两条消息,但却当成了一条数据进行处理,明显与功能不符。Receive方法返回多少个数据,取决于操作系统接收缓冲区中存放的内容。


半包



  • 发送端发送的数据还有可能被拆分,如发送“ HelloWorld",但在接收端调用Receive时,操作系统只接收到了部分数据,如“ Hel ” ,在等待一小段时间后再次调用Receive才接收到另一部分数据“ loWorld"。这样一来对方明明发送的是一条消息,但却被当成了两条进行处理,肯定也是不能符合规则。


解决粘包问题的方法


一般有三种方法可以解决粘包和半包问题, 分别是长度信息法、固定长度法和结束符号法。一般的游戏开发会在每个数据包前面加上长度字节, 以方便解析, 本文也将详细介绍这种方法。



长度信息法


长度信息法是指在每个数据包前面加上长度信息。每次接收到数据后, 先读取表示长度的字节, 如果缓冲区的数据长度大于要取的字节数, 则取出相应的字节, 否则等待下一次数据接收。假如客户端要发送“HelloWorld”,那么为了让服务端判断是不是接收到了完整的消息,通常在“HelloWorld”前面加上长度即“10HelloWorld”。加入服务端第一次Receive接收到的是“10Hello”,先读取第一个字节“10”,这时候服务端知道了完整消息的长度,而显然此时消息的长度并不能达到要求,所以服务端不进行任何处理,等待下一次接收。这样就可以保证每次接收到的消息都是完整的。



结束符号法


规定一个结束符号,作为消息间的分隔符假设规定结束符号为"@",那么发送" Hello" “Unity"两条信息可以发送成"Hello@“和“Unity@”接收方每次读取数据,直到”@ ” 出现为止,并且使用“ @ “ 去分割消息。比如接收方第一次读到“Hello@Un”,那它把结束符前面的Hello提取出来,作为第一条消息去处理,再把“Un"保存起来。待后续读到“ ity@". 再把“ Un"和“ ity"拼成第二条消息。



固定长度法


每次都以相同的长度发送数据,假设规定每条信息的长度都为10个字符,那么发送"Hello" “Unity"两条信息可以发送成“ Hello…” “Unity… “,其中的”.”表示填充字符,是为凑数,没有实际意义,只为了每次发送的数据都有固定长度,接收方每次读取10个字符,作为一条消息去处理。如果读到的字符数大于10,比如第1次读到“Hello…Un” , 那它只要把前10个字节“Hello "抽取出来,再把后面的两个字节”Un"存起来,等到再次接收数据,拼接第二条信息。



代码实现

本文会展示在异步客户端上,实现带有32字节长度信息的协议,来解决粘包问题。用Vs创建控制台应用进行测试长度信息发解决分包粘包问题。

客户端代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Socket_Package_Test
{
///


/// 客户端
///

class _Client
{
Socket socket;
int BUFFER_SIZE = 1024;//缓冲区的长度
byte[] readBuff;//接收消息的缓冲区
int nowBuffLength = 0;//缓冲区现在的字节长度
byte[] fOntBuff=new byte[sizeof(Int32)];//接收到消息数组的长度的数组

public _Client() {
readBuff = new byte[BUFFER_SIZE];
StartClient();
}
//开始启动客户端
void StartClient() {
Console.WriteLine("开始启动客户端");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint end = new IPEndPoint(ipAdr, 1234);
socket.Connect(end);
try
{
socket.BeginReceive(readBuff,nowBuffLength,BUFFER_SIZE-nowBuffLength,SocketFlags.None,ReceiveCb,socket);

}
catch (Exception)
{
//throw;
}
}
///
/// 接收消息的回调函数
///

///


void ReceiveCb(IAsyncResult ar) {
Socket listen = (Socket)ar.AsyncState;
try
{
//本次接收到数据的字节长度
int count = listen.EndReceive(ar);
//现在缓存区中数据的字节长度
nowBuffLength += count;
//处理接收到的数据
HandleDate(listen);
//继续回调接收消息
listen.BeginReceive(readBuff, nowBuffLength, BUFFER_SIZE-nowBuffLength, SocketFlags.None, ReceiveCb, listen);
}
catch (Exception e)
{
// throw;
}
}
///


/// 处理接收到的数据
///

///


void HandleDate(Socket listen) {
if (nowBuffLength {
return;
}
//消息头长度的数组更新
Array.Copy(readBuff,fontBuff,sizeof(Int32));
//消息头(一段消息的长度)
int receiveLength = BitConverter.ToInt32(fontBuff,0);
if (nowBuffLength {
//一段话没有接收完整,等待下次一块处理
return;
}
//解析出一条消息
string str = UTF8Encoding.UTF8.GetString(readBuff,sizeof(Int32),receiveLength);
Console.WriteLine("接收到服务端的消息:"+str);
Console.WriteLine("客户端回复:");
string s = Console.ReadLine();
if (s != "")
{

//服务器发送消息
SendDate(listen, s);
}
//清除掉已经处理过的数据
int remainCount = nowBuffLength - sizeof(Int32) - receiveLength;
//把剩余没有处理的本次接收到的数据重新拷贝到缓存区
Array.Copy(readBuff,receiveLength+sizeof(Int32),readBuff,0,remainCount);
//缓冲区现在的数据长度
nowBuffLength = remainCount;
if (nowBuffLength>0)
{
HandleDate(listen);
}

}
///


/// 发送消息
///

///


///


void SendDate(Socket listen,string str) {
//消息的内容数组(消息体)
byte[] sendDate = UTF8Encoding.UTF8.GetBytes(str);
//消息体的字节长度
int dateLength = sendDate.Length;
//要发送消息的长度的数组(消息头)
byte[] length = BitConverter.GetBytes(dateLength);
//消息头和消息体进行拼接
byte[] bytes = length.Concat(sendDate).ToArray();
listen.Send(bytes);
}
}
}

服务端代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Socket_Package_Test
{
///


/// 服务端
///

class _Socket
{
Socket socket;
int BUFFER_SIZE = 1024;//缓冲区的长度
byte[] readBuff ;//接收消息的缓冲区
int nowBuffLength=0;//缓冲区现在的字节长度
byte[] fOntBuff=new byte[sizeof(Int32)];//接收到消息数组的长度的数组
int fOntLenth= sizeof(Int32);//消息头占用的字节长度
int receiveDateLength;//接收到消息的长度
public _Socket() {
readBuff = new byte[BUFFER_SIZE];
StartServer();
}
int maxListen = 50;
///
/// 开启服务器
///

void StartServer()
{
Console.WriteLine("开始启动服务器");
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint end = new IPEndPoint(ipAdr, 1234);
socket.Bind(end);
socket.Listen(maxListen);
try
{
socket.BeginAccept(AcceptCb, null);
}
catch (Exception)
{
// throw;
}
}
///
/// BeginAccept的回调
///

///


void AcceptCb(IAsyncResult ar)
{
Socket listen = socket.EndAccept(ar);//直到有人连接服务器 返回连接者的Socket
Console.WriteLine(listen.RemoteEndPoint+"进入房间");

SendDate(listen,"欢迎您进入房间");

//开始接收消息
listen.BeginReceive(readBuff,nowBuffLength,BUFFER_SIZE-nowBuffLength,SocketFlags.None,ReceiveCb,listen);
socket.BeginAccept(AcceptCb, null);
}
///


/// 接收消息的回调
///

///


void ReceiveCb(IAsyncResult ar) {
Socket listen = ar.AsyncState as Socket;
try
{
//本次接收到的数据长度
int count = listen.EndReceive(ar);
//现在的缓冲区长度增加新接收的长度
nowBuffLength += count;
//处理接收的数据
HandleDate(listen);
//循环接收消息
listen.BeginReceive(readBuff, nowBuffLength, BUFFER_SIZE-nowBuffLength, SocketFlags.None, ReceiveCb, listen);
}
catch (Exception)
{
// throw;
}

}
///


/// 处理服务器接收的消息
///

void HandleDate(Socket listen) {
if (nowBuffLength<=fontLenth)//缓冲区中的数据长度小于四个字节
{
return;
}
//消息头长度的数组更新
Array.Copy(readBuff,fontBuff,fontLenth);
//消息头(一段消息的长度)
receiveDateLength = BitConverter.ToInt32(fontBuff,0);
if (nowBuffLength {
return;
}
//解析出一条消息
string str = UTF8Encoding.UTF8.GetString(readBuff,sizeof(Int32),receiveDateLength);
Console.WriteLine("收到客户端消息:"+str);
Console.WriteLine("服务端回复:");
string s = Console.ReadLine();
if (s!="")
{
//服务器发送消息
SendDate(listen, s);
}

int remainCount = nowBuffLength-sizeof(Int32)-receiveDateLength;
//把剩余没有处理的本次接收到的数据重新拷贝到缓存区
Array.Copy(readBuff,sizeof(Int32)+ receiveDateLength, readBuff,0,remainCount);
//缓冲区现在的数据长度
nowBuffLength = remainCount;
if (nowBuffLength>0)//如果缓冲区还有数据
{
HandleDate(listen);
}
}
///
/// 发送消息
///

///


///


void SendDate(Socket listen, string str) {

//消息的内容数组(消息体)
byte[] Date = UTF8Encoding.UTF8.GetBytes(str);
//消息体的字节长度
int sendLength = Date.Length;
//要发送消息的长度的数组(消息头)
byte[] sendDateLength = BitConverter.GetBytes(sendLength);
//消息头和消息体进行拼接
byte[] sendBytes = sendDateLength.Concat(Date).ToArray();
//发送给客户端
listen.Send(sendBytes);
}
}
}

上面的处理方式基本上解决了分包粘包的问题,但是还是存在一些问题,例如:大端小端问题、线程冲突问题。这些问题本片文章先不进行处理。除了这些问题之外还存在一些其他的不足之处,例如:在Copy操作的时候,每次成功接收一条完整的数据后,程序会调用Array.Copy,将缓冲区的数据往前移动。但Array.Copy是个时间复杂度为o(n)的操作,假如缓冲区中的数据很多,那移动全部数据将会花费较长的时间。一个可行的办法是,使用ByteArray结构作为缓冲区,使用readldx指向的数据作为缓冲区的第一个数据,当接收完数据后,只移动readldx,时间复杂度为o(l),当然,肯定还有一些其它的问题,这里就不一一列举了。






  • 点赞



  • 收藏



  • 分享




    • 文章举报






军礼
发布了12 篇原创文章 · 获赞 2 · 访问量 1476
私信

关注

推荐阅读
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • (三)多表代码生成的实现方法
    本文介绍了一种实现多表代码生成的方法,使用了java代码和org.jeecg框架中的相关类和接口。通过设置主表配置,可以生成父子表的数据模型。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 基于移动平台的会展导游系统APP设计与实现的技术介绍与需求分析
    本文介绍了基于移动平台的会展导游系统APP的设计与实现过程。首先,对会展经济和移动互联网的概念进行了简要介绍,并阐述了将会展引入移动互联网的意义。接着,对基础技术进行了介绍,包括百度云开发环境、安卓系统和近场通讯技术。然后,进行了用户需求分析和系统需求分析,并提出了系统界面运行流畅和第三方授权等需求。最后,对系统的概要设计进行了详细阐述,包括系统前端设计和交互与原型设计。本文对基于移动平台的会展导游系统APP的设计与实现提供了技术支持和需求分析。 ... [详细]
  • 本文整理了Java中java.lang.NoSuchMethodError.getMessage()方法的一些代码示例,展示了NoSuchMethodErr ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文介绍了在Cpp中将字符串形式的数值转换为int或float等数值类型的方法,主要使用了strtol、strtod和strtoul函数。这些函数可以将以null结尾的字符串转换为long int、double或unsigned long类型的数值,且支持任意进制的字符串转换。相比之下,atoi函数只能转换十进制数值且没有错误返回。 ... [详细]
  • 使用freemaker生成Java代码的步骤及示例代码
    本文介绍了使用freemaker这个jar包生成Java代码的步骤,通过提前编辑好的模板,可以避免写重复代码。首先需要在springboot的pom.xml文件中加入freemaker的依赖包。然后编写模板,定义要生成的Java类的属性和方法。最后编写生成代码的类,通过加载模板文件和数据模型,生成Java代码文件。本文提供了示例代码,并展示了文件目录结构。 ... [详细]
  • 本文介绍了在实现了System.Collections.Generic.IDictionary接口的泛型字典类中如何使用foreach循环来枚举字典中的键值对。同时还讨论了非泛型字典类和泛型字典类在foreach循环中使用的不同类型,以及使用KeyValuePair类型在foreach循环中枚举泛型字典类的优势。阅读本文可以帮助您更好地理解泛型字典类的使用和性能优化。 ... [详细]
author-avatar
日月小明空间_785
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有