tcp 粘包和拆包 及 解决粘包方案

什么是粘包和拆包

.TCP 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的

粘包和拆包是网络编程中常遇到的问题,主要是因为TCP协议的Nagle算法和接收方缓冲区的不足导致的。

粘包:发送方发送的若干包数据到接收方接收时粘在一起,导致数据不能正确分割。

拆包:发送方发送的包数据在接收方解析时被拆开,导致数据不能正确解析。

客户端

直接发消息

第一种方式发消息的时候 每一条消息的结束时候以特殊符合作为结束标志 例如#、\n等符号

// 其他端拿到数据之后 对数据进行特殊符号的分割

cs 复制代码
TcpClient  client = new TcpClient();
client.Connect("端口号", 8080);
NetworkStream stream = client.GetStream();
Send(stream, "三十而立,指的是拥有了30个重装合成旅,才能在这个星球上拥有立足的资本");
Send(stream, "四十而不惑,指的是拥有40个重装合成旅,这个星球上就没有能让我们感到困惑的事");
Send(stream, "五十而知天命,指的是拥有50个重装合成旅,这个星球的其他国家便能知道我们所说的话便是他的天命");

第二种发送数据时候 出现粘包现象,后台处粘包

//包头当中添加数据长度解决粘包

cs 复制代码
void Send(NetworkStream stream,string msg)
{
    //1定义一个字节数组获取发送数据字节流
    byte[] bs = Encoding.UTF8.GetBytes(msg);

    //2 创建一个内存流,存在内存当中,可以理解为虚拟的文件流
    MemoryStream ms = new MemoryStream();

    //3 创建一个二进制读写对象 写入内存流
    BinaryWriter bw = new BinaryWriter(ms);

    //4写入数据的长度的
    bw.Write(bs.Length);

    //5 写入数据字节数组
    bw.Write(bs);

    //6 发送数据 把数据长度和数据内容同时发给服务器
    stream.Write(ms.ToArray(), 0, ms.ToArray().Length);

    bw.Close();
    ms.Close();

}

服务器:

Server封装:
cs 复制代码
TcpListener listen;
public Server(IPAddress ip,int port) 
{
   listen = new TcpListener(ip, port);
}

public void Start()
{
    listen.Start(100);
    StartConnect();  
}
Dictionary<string,TcpClient> clientDic = new Dictionary<string,TcpClient>();
public event Action<TcpClient> 有客户端连入的事件; 
void StartConnect()
{
    Task.Run(() =>
    {
        while (true) 
        {
         TcpClient client = listen.AcceptTcpClient();
         string ip = client.Client.RemoteEndPoint.ToString(); 
         clientDic.Add(ip, client);
         有客户端连入的事件?.Invoke(client);
         startRead(client);//读取粘包的数据,在这个方法进行拆包处理
        }
    });
}

拆包

cs 复制代码
public void startRead(TcpClient t1)
{
    //接受客户端发来的数据
    NetworkStream stream = t1.GetStream();
    string ip = t1.Client.RemoteEndPoint.ToString();
    byte[] bs = new byte[1024 * 1024];
    Task.Run(() =>
    {
        try
        {
            while (true)
            {
              int count=  stream.Read(bs, 0, bs.Length);
             if (count== 0)
             {
                    throw new Exception("客户端断了");

             }
                //接受数据
                byte[] body = bs.Take(count).ToArray();
                string s = Encoding.UTF8.GetString(body);
                Console.WriteLine("-------------------------------");
                Console.WriteLine("接收到消息为:"+s);
                Console.WriteLine("-------------------------------");
                if (上一个数据半包 != null) //判断是否有半包 如果有,把半包取出来和当前包合并
                {
                    body =  上一个数据半包.Concat(body).ToArray();//把俩个数组合成一个数组
                    上一个数据半包 = null; //清空半包
                }
                //开始拆包 封装一个拆包方法
                //参数1  当前要拆的数据
                //参数2 第几个位置开始拆
                //参数3 当前客户端
                ChaiBao(body, 0, t1);


            }
        }
        catch (Exception ex)
        {
            clientDic.Remove(ip);
        }
    });
}
cs 复制代码
 byte[] 上一个数据半包 = null;
 public void ChaiBao(byte[] bytes ,int startIndex, TcpClient client)
 {
     if(startIndex+4>bytes.Length) 
     {
         //如果开始位置加上4大于该数据包的长度时候 说明当前包是一个半包
         //跳过之前元素,取出剩余的数据
         上一个数据半包 = bytes.Skip(startIndex).ToArray();
         return;
     }
     //计算第一个包长度 获取从开始到startIndex之间的长度
     int len = BitConverter.ToInt32 (bytes,startIndex);

     // 取出对应位置包的总体大小
     // 之前的包长度总和
     int abc = len + startIndex + 4;

     //判断当前包是整包还是半包或者是多包
     if (abc == bytes.Length) 
     { //如果之前所有包的大小加上当前数据包大小等于全包的长度,证明当前是一个整包
          byte[] bs1  = bytes.Skip(startIndex+4).ToArray();
         接受到消息的事件?.Invoke(client, bs1);
        
     }else if (abc < bytes.Length)
     {
          byte[] bs2 =   bytes.Skip(startIndex+4).Take(len).ToArray();
         接受到消息的事件?.Invoke(client, bs2);
         //如果之前包加上当前包小于全包的长度,说明当前包是多包。如果是多包,再进行拆包
         ChaiBao(bytes, abc, client);
     }
     else
     { //目前是一个半包
         上一个数据半包 = bytes.Skip (startIndex).ToArray();
     }
 }
群发方法
cs 复制代码
 public event Action<string> 客户端断开事件; 
 public event Action<TcpClient, byte[]> 接受到消息的事件;
 public Server()
 {

 }
 //群发方法 向所有的客户端发消息
 public void Send(string content)
 {
     byte[] bs = Encoding.UTF8.GetBytes(content);
     foreach (var item in clientDic) //遍历所有的客户端
     {
         item.Value.GetStream().Write(bs, 0, bs.Length);
     }
 }
 //指定给谁发
 public void Send(string content,string ip) {
     byte[] bs = Encoding.UTF8.GetBytes(content);
     //根据ip取出客户端,从字典取
     clientDic[ip].GetStream().Write(bs, 0, bs.Length);
 }

 //指定给哪些客户端发
 //send("你好", ["192.","127"])
 public void Send(string content, string[] ips)
 {
     byte[] bs = Encoding.UTF8.GetBytes(content);
     foreach (var item in clientDic) //所有客户端
     {
         //item.key 键 ip字符串
         //item.value 值 客户端对象
         if (ips.Contains(item.Key))
         {
             //如果ips数组包含目标客户端
             item.Value.GetStream().Write(bs, 0, bs.Length);
         }
     }
 }
Program
cs 复制代码
static Server s;
 static void Main(string[] args)
 {
      s = new Server(IPAddress.Any,8080);
     s.有客户端连入的事件 += f1;
     s.接受到消息的事件 += f2;
     s.Start();
     Console.ReadKey();
 }
 public static void f1(TcpClient t1)
 {
     Console.WriteLine(t1.Client.RemoteEndPoint.ToString()+"连接到服务器");
 }
 public static void f2(TcpClient t2, byte[] bs)
 {
     Console.WriteLine(t2.Client.RemoteEndPoint.ToString()+"发来的消息为+++++++++++:"+ Encoding.UTF8.GetString(bs,0,bs.Length));
     
 }
相关推荐
炫酷的伊莉娜11 分钟前
【计算机网络】数据链路层(作业)
网络·计算机网络·数据链路层
xy1899011 分钟前
http 状态码主要有哪些?【面试】
网络·网络协议·http
来世做春风嘛23 分钟前
第二十一章 网络编程
服务器·网络·php
做跨境的红姐28 分钟前
为什么网络爬虫广泛使用HTTP代理?
tcp/ip·安全·ip
Tony11541 小时前
华为路由器静态路由配置(eNSP模拟实验)
网络·华为·智能路由器
斐夷所非1 小时前
RIP 路由 3 个定时器的工作流程和 4 种防环方法
网络
速盾cdn3 小时前
速盾:cdn发展历程
服务器·网络·安全
阿龍17873 小时前
Qt中udp指令,大小端,帧头帧尾实际示例
网络·c++·qt·网络协议·udp
爱喝矿泉水的猛男4 小时前
无线传感器网络(物联网通信技术)期末考试2024年真题
网络·arm开发·物联网·wsn·物联网通信技术
梦中北山4 小时前
JWT安全分析
开发语言·网络·安全·web安全·php