热门标签 | 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
私信

关注

推荐阅读
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
    字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 本文详细解析了客户端与服务器之间的交互过程,重点介绍了Socket通信机制。IP地址由32位的4个8位二进制数组成,分为网络地址和主机地址两部分。通过使用 `ipconfig /all` 命令,用户可以查看详细的IP配置信息。此外,文章还介绍了如何使用 `ping` 命令测试网络连通性,例如 `ping 127.0.0.1` 可以检测本机网络是否正常。这些技术细节对于理解网络通信的基本原理具有重要意义。 ... [详细]
  • 如何在PHP中准确获取服务器IP地址?
    如何在PHP中准确获取服务器IP地址? ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • Python 程序转换为 EXE 文件:详细解析 .py 脚本打包成独立可执行文件的方法与技巧
    在开发了几个简单的爬虫 Python 程序后,我决定将其封装成独立的可执行文件以便于分发和使用。为了实现这一目标,首先需要解决的是如何将 Python 脚本转换为 EXE 文件。在这个过程中,我选择了 Qt 作为 GUI 框架,因为之前对此并不熟悉,希望通过这个项目进一步学习和掌握 Qt 的基本用法。本文将详细介绍从 .py 脚本到 EXE 文件的整个过程,包括所需工具、具体步骤以及常见问题的解决方案。 ... [详细]
  • 在Java Web服务开发中,Apache CXF 和 Axis2 是两个广泛使用的框架。CXF 由于其与 Spring 框架的无缝集成能力,以及更简便的部署方式,成为了许多开发者的首选。本文将详细介绍如何使用 CXF 框架进行 Web 服务的开发,包括环境搭建、服务发布和客户端调用等关键步骤,为开发者提供一个全面的实践指南。 ... [详细]
  • 利用ZFS和Gluster实现分布式存储系统的高效迁移与应用
    本文探讨了在Ubuntu 18.04系统中利用ZFS和Gluster文件系统实现分布式存储系统的高效迁移与应用。通过详细的技术分析和实践案例,展示了这两种文件系统在数据迁移、高可用性和性能优化方面的优势,为分布式存储系统的部署和管理提供了宝贵的参考。 ... [详细]
  • 本文介绍了如何利用 Delphi 中的 IdTCPServer 和 IdTCPClient 控件实现高效的文件传输。这些控件在默认情况下采用阻塞模式,并且服务器端已经集成了多线程处理,能够支持任意大小的文件传输,无需担心数据包大小的限制。与传统的 ClientSocket 相比,Indy 控件提供了更为简洁和可靠的解决方案,特别适用于开发高性能的网络文件传输应用程序。 ... [详细]
  • Java能否直接通过HTTP将字节流绕过HEAP写入SD卡? ... [详细]
  • Linux入门教程第七课:基础命令与操作详解
    在本课程中,我们将深入探讨 Linux 系统中的基础命令与操作,重点讲解网络配置的相关知识。首先,我们会介绍 IP 地址的概念及其在网络协议中的作用,特别是 IPv4(Internet Protocol Version 4)的具体应用和配置方法。通过实际操作和示例,帮助初学者更好地理解和掌握这些基本技能。 ... [详细]
  • 华为AP3010DNAGN从胖AP转换为瘦AP的过程及版本升级详细记录
    华为AP3010DNAGN从胖AP模式转换为瘦AP模式的过程及其版本升级的详细记录如下:首先,需要了解胖AP与瘦AP的区别。瘦AP(FIT)模式下,设备无法独立运行Wi-Fi功能,必须与AC控制器配合使用,适用于企业多AP的集中管理场景。本文将详细介绍转换步骤和版本升级的具体操作,帮助用户顺利完成配置。 ... [详细]
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社区 版权所有