C#中TCP粘包问题的解决方法

一、TCP粘包产生的原理

1.TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

2.发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。

3.这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。、

二、解决原理及代码实现

1.采用包头(固定长度,里面存着包体的长度,发送时动态获取)+包体的传输机制。如图

HeaderSize 存放着包体的长度,其HeaderSize本身是定长4字节;

一个完整的数据包(L)=HeaderSize+BodySize;

2.分包算法

  其基本思路是首先将待处理的接收数据流即系统缓冲区数据(长度设为M)强行转换成预定的结构数据形式,并从中取出结构数据长度字段L,而后根据包头计算得到第一包数据长度。

M=系统缓冲区大小;L=用户发送的数据包=HeaderSize+BodySize;

1)若L<M,则表明数据流包含多包数据,从其头部截取若干个字节存入临时缓冲区,剩余部分数据依此继续循环处理,直至结束。

2)若L=M,则表明数据流内容恰好是一完整结构数据(即用户自定义缓冲区等于系统接收缓冲区大小),直接将其存入临时缓冲区即可。

3)若L>M,则表明数据流内容尚不够构成一完整结构数据,需留待与下一包数据合并后再行处理。

4)下面是代码代码实现(HP-SOCKET框架的服务器端来接收数据)

int headSize = 4;//包头长度 固定4
  byte[] surplusBuffer = null;//不完整的数据包,即用户自定义缓冲区
  /// <summary>
  /// 接收客户端发来的数据
  /// </summary>
  /// <param name="connId">每个客户的会话ID</param>
  /// <param name="bytes">缓冲区数据</param>
  /// <returns></returns>
  private HandleResult OnReceive(IntPtr connId, byte[] bytes)
  {
   //bytes 为系统缓冲区数据
   //bytesRead为系统缓冲区长度
   int bytesRead = bytes.Length;
   if (bytesRead > 0)
   {
    if (surplusBuffer == null)//判断是不是第一次接收,为空说是第一次
     surplusBuffer = bytes;//把系统缓冲区数据放在自定义缓冲区里面
    else
     surplusBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
    //已经完成读取每个数据包长度
    int haveRead = 0;
    //这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
    int totalLen = surplusBuffer.Length;
    while (haveRead <= totalLen)
    {
     //如果在N此拆解后剩余的数据包连一个包头的长度都不够
     //说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
     if (totalLen - haveRead < headSize)
     {
      byte[] byteSub = new byte[totalLen - haveRead];
      //把剩下不够一个完整的数据包存起来
      Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
      surplusBuffer = byteSub;
      totalLen = 0;
      break;
     }
     //如果够了一个完整包,则读取包头的数据
     byte[] headByte = new byte[headSize];
     Buffer.BlockCopy(surplusBuffer, haveRead, headByte, 0, headSize);//从缓冲区里读取包头的字节
     int bodySize = BitConverter.ToInt32(headByte, 0);//从包头里面分析出包体的长度

     //这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
     //如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
     if (haveRead + headSize + bodySize > totalLen)
     {
      byte[] byteSub = new byte[totalLen - haveRead];
      Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
      surplusBuffer = byteSub;
      break;
     }
     else
     {
      //挨个分解每个包,解析成实际文字
      String strc = Encoding.UTF8.GetString(surplusBuffer, haveRead + headSize, bodySize);
      //AddMsg(string.Format(" > [OnReceive] -> {0}", strc));
      //依次累加当前的数据包的长度
      haveRead = haveRead + headSize + bodySize;
      if (headSize + bodySize == bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
      {
       surplusBuffer = null;//设置空 回到原始状态
       totalLen = 0;//清0
      }
     }
    }
   }
   return HandleResult.Ok;
  }

值此完成拆包解析文字工作。但实际上还没完成,如果这段代码是客户端接收来自服务器的数据的话就没问题了。

仔细看IntPtr connId 每个连接的会话ID

private HandleResult OnReceive(IntPtr connId, byte[] bytes)

{

}

但是服务器端还要分辨出 每个数据包是哪个会话产生的,因为服务器端是多线程,多用户的模式,第一个数据包和第二个可能来自不同会话的数据,所以上面的代码只适用于单会话模式。

下面我要解决这个问题。

采用c#安全的ConcurrentDictionary,具体参考 https://msdn.microsoft.com/zh-cn/library/dd287191(v=vs.110).aspx

最新的代码

//线程安全的字典
  ConcurrentDictionary<IntPtr, byte[]> dic = new ConcurrentDictionary<IntPtr, byte[]>();
  int headSize = 4;//包头长度 固定4
  /// <summary>
  /// 接收客户端发来的数据
  /// </summary>
  /// <param name="connId">每个客户的会话ID</param>
  /// <param name="bytes">缓冲区数据</param>
  /// <returns></returns>
  private HandleResult OnReceive(IntPtr connId, byte[] bytes)
  {
   //bytes 为系统缓冲区数据
   //bytesRead为系统缓冲区长度
   int bytesRead = bytes.Length;
   if (bytesRead > 0)
   {
    byte[] surplusBuffer = null;
    if (dic.TryGetValue(connId, out surplusBuffer))
    {
     byte[] curBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
     //更新会话ID 的最新字节
     dic.TryUpdate(connId, curBuffer, surplusBuffer);
     surplusBuffer = curBuffer;//同步
    }
    else
    {
     //添加会话ID的bytes
     dic.TryAdd(connId, bytes);
     surplusBuffer = bytes;//同步
    }

    //已经完成读取每个数据包长度
    int haveRead = 0;
    //这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
    int totalLen = surplusBuffer.Length;
    while (haveRead <= totalLen)
    {
     //如果在N此拆解后剩余的数据包连一个包头的长度都不够
     //说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
     if (totalLen - haveRead < headSize)
     {
      byte[] byteSub = new byte[totalLen - haveRead];
      //把剩下不够一个完整的数据包存起来
      Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
      dic.TryUpdate(connId, byteSub, surplusBuffer);
      surplusBuffer = byteSub;
      totalLen = 0;
      break;
     }
     //如果够了一个完整包,则读取包头的数据
     byte[] headByte = new byte[headSize];
     Buffer.BlockCopy(surplusBuffer, haveRead, headByte, 0, headSize);//从缓冲区里读取包头的字节
     int bodySize = BitConverter.ToInt32(headByte, 0);//从包头里面分析出包体的长度

     //这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
     //如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
     if (haveRead + headSize + bodySize > totalLen)
     {
      byte[] byteSub = new byte[totalLen - haveRead];
      Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
      dic.TryUpdate(connId, byteSub, surplusBuffer);
      surplusBuffer = byteSub;
      break;
     }
     else
     {
      //挨个分解每个包,解析成实际文字
      String strc = Encoding.UTF8.GetString(surplusBuffer, haveRead + headSize, bodySize);
      AddMsg(string.Format(" > {0}[OnReceive] -> {1}", connId, strc));
      //依次累加当前的数据包的长度
      haveRead = haveRead + headSize + bodySize;
      if (headSize + bodySize == bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
      {
       byte[] xbtye=null;
       dic.TryRemove(connId, out xbtye);
       surplusBuffer = null;//设置空 回到原始状态
       totalLen = 0;//清0
      }
     }
    }
   }
   return HandleResult.Ok;
  }

这样就解决了,多客户端会话造成的接收混乱。至此所有工作完成。以上代码就是为了参考学习,如果实在不想这么麻烦。可以直接使用HP-SOCKET通信框架的PACK模型,里面自动实现了解决粘包的问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C# Socket粘包处理讲解示例

    当socket接收到数据后,会根据buffer的大小一点一点的接收数据,比如: 对方发来了1M的数据量过来,但是,本地的buffer只有1024字节,那就代表socket需要重复很多次才能真正收完这逻辑上的一整个消息.对方发来了5条2个字符的消息,本地的buffer(大小1024字节)会将这5条消息全部收入囊下...那么,如何处理呢?下面我以最简单的一种文本消息来demo 根据上面所描述的情况,最重要的关键落在了下面3个因素的处理上 消息的结尾标记接收消息时判断结尾标记当本次buffer中没有结

  • 使用C#实现基于TCP和UDP协议的网络通信程序的基本示例

    C#中使用TCP通信 TCP通信需要通信双方都在线,所以需要先启动服务端进行监听,客户端才能获得连接,服务端代码: static void Main(string[] args) { TcpClient client = null; NetworkStream stream = null; byte[] buffer = null; string receiveString = null; IPAddress localIP = IPAddress.Parse("127.0.0.1")

  • 在C#中对TCP客户端的状态封装详解

    TCP客户端连接TCP服务器端有几种应用状态:1.与服务器的连接已建立2.与服务器的连接已断开3.与服务器的连接发生异常 应用程序可按需求合理处理这些逻辑,比如:1.连接断开后自动重连2.连接断开后选择备用地址重连3.所有状态变化上报告警本文描述的TcpClient实现了状态变化的事件通知机制. 复制代码 代码如下: /// <summary>   /// 异步TCP客户端   /// </summary>   public class AsyncTcpClient : IDisp

  • C#基于TCP协议的服务器端和客户端通信编程的基础教程

    运行在TCP之上常见的网络应用协议有比如HTTP.FTP.SMTP.POP3.IMAP. TCP是TCP/IP体系中最重要的传输协议,它提供全双工和可靠交付的服务,是大多数应用协议工作的基础. TCP是一种面向连接(连接导向)的,可靠的,基于字节流的传输层通信协议. TCP的工作过程 建立连接 传输数据 连接的终止 TCP的主要特点 1.TCP是面向连接的协议 2.是端到端的通信.每个TCP连接只能有两个端点,而且只能一对一通信,不能点对多的 的直接通信 3.高可靠性 4.全双工方式传输 5.数

  • C#实现TCP连接信息统计的方法

    本文实例讲述了C#实现TCP连接信息统计的方法.分享给大家供大家参考.具体实现方法如下: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Net.Netw

  • C#中TCP粘包问题的解决方法

    一.TCP粘包产生的原理 1.TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾.出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成. 2.发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据.若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据.接收方引起的粘包是由于接收方用户进程不及时接收数据,从

  • golang网络socket粘包问题的解决方法

    本文实例讲述了golang网络socket粘包问题的解决方法.分享给大家供大家参考,具体如下: 看到很多人问这个问题, 今天就写了个例子, 希望能帮助大家 首先说一下什么是粘包:百度上比较通俗的说法是指TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾. 解决方案如下: 服务端: 复制代码 代码如下: package main import (     "bytes"     "encoding/binary&quo

  • Golang通过包长协议处理TCP粘包的问题解决

    tcp粘包产生的原因这里就不说了,因为大家能搜索TCP粘包的处理方法,想必大概对TCP粘包有了一定了解,所以我们直接从处理思路开始讲起 tcp粘包现象代码重现 首先,我们来重现一下TCP粘包,然后再此基础之上解决粘包的问题,这里给出了client和server的示例代码如下 /* 文件名:client.go client客户端的示例代码(未处理粘包问题) 通过无限循环无时间间隔发送数据给server服务器 server将会不间断的出现TCP粘包问题 */ package main import

  • 6行代码快速解决golang TCP粘包问题

    前言 什么是TCP粘包问题以及为什么会产生TCP粘包,本文不加讨论.本文使用golang的bufio.Scanner来实现自定义协议解包. 下面话不多说了,来一起看看详细的介绍吧. 协议数据包定义 本文模拟一个日志服务器,该服务器接收客户端传到的数据包并显示出来 type Package struct { Version [2]byte // 协议版本,暂定V1 Length int16 // 数据部分长度 Timestamp int64 // 时间戳 HostnameLength int16

  • Golang TCP粘包拆包问题的解决方法

    什么是粘包问题 最近在使用Golang编写Socket层,发现有时候接收端会一次读到多个数据包的问题.于是通过查阅资料,发现这个就是传说中的TCP粘包问题.下面通过编写代码来重现这个问题: 服务端代码 server/main.go func main() { l, err := net.Listen("tcp", ":4044") if err != nil { panic(err) } fmt.Println("listen to 4044")

  • 使用Netty解决TCP粘包和拆包问题过程详解

    前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络转播的,当tcp三次握手简历通信后,客户端服务端之间就建立了一种通讯管道,我们可以想象成自来水管道,流出来的水是连城一片的,是没有分界线的. TCP底层并不了解上层的业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分. 所以对于我们应用层而言.我们直观是发送一个个连续完整TCP数据包的,

  • 详解python中TCP协议中的粘包问题

    TCP协议中的粘包问题 1.粘包现象 基于TCP实现一个简易远程cmd功能 #服务端 import socket import subprocess sever = socket.socket() sever.bind(('127.0.0.1', 33521)) sever.listen() while True: client, address = sever.accept() while True: try: cmd = client.recv(1024).decode('utf-8') p

  • Netty解决 TCP 粘包拆包的方法

    什么是粘包/拆包 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在.处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要. 我们都知道TCP是基于字节流的传输协议. 那么数据在通信层传播其实就像河水一样并没有明显的分界线,而数据具体表示什么意思什么地方有句

  • python3 tcp的粘包现象和解决办法解析

    这篇文章主要介绍了python3 tcp的粘包现象和解决办法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 服务器端 import socket sk = socket.socket() sk.bind(("127.0.0.1", 6666)) sk.listen() conn, address = sk.accept() def my_send(msg): bs = msg.encode("utf-8") l

  • GO语言如何手动处理TCP粘包详解

    前言 一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据.TCP通讯为何存在粘包呢?主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在.处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要.在应用中处理粘包的基础方法主要有两种分别是以4节字描述消息大小或以结束符,实际上也有两者相结合的如HTTP,redis的通讯协议等. 应用场景 大

随机推荐