Socket 如何处理粘包

socket,如何,处理 · 浏览次数 : 807

小编点评

# 生成内容方法 1. 定义变量: - 存储消息长度的总和的字节数组 - 存储数据长度的字节数组 - 存储协议定义的长度 - 存储接收数据的字节数组 - 存储接收数据的长度 2. 使用循环将接收到的数据存入变量中 3. 计算数据长度 4. 根据数据长度创建文件 5. 将文件写入到协议定义的长度位置 6. 返回协议定义的长度 # 生成协议定义的长度 1. 定义变量: - 存储数据长度的字节数组 - 存储协议定义的长度 2. 使用循环将数据长度的字节数组复制到协议定义的长度位置 3. 返回协议定义的长度 # 结束位置 1. 如果接收数据的长度小于协议定义的长度,则将接收数据的长度设置为协议定义的长度 2. 否则,将接收数据的长度设置为协议定义的长度

正文

Socket 如何处理粘包

什么是粘包什么是半包?

粘包:
比如发送了AA BB 两条消息,但是另一方接收到的消息却是AAB,像这种一次性读取了俩条数据的情况就是粘包
半包:
比如发送的消息是ABC时,另一方收到的是AB和C俩条消息,这就是半包

为什么会有这种问题呢?

这是因为 TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息。
粘包的主要原因:
发送方每次写入数据 < 套接字(Socket)缓冲区大小;
接收方读取套接字(Socket)缓冲区数据不够及时。
半包的主要原因:
发送方每次写入数据 > 套接字(Socket)缓冲区大小;
发送的数据大于协议的 MTU (Maximum Transmission Unit,最大传输单元),因此必须拆包。

知识点:什么是缓冲区?

缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。

缓冲区的优势以文件流的写入为例,如果我们不使用缓冲区,那么每次写操作 CPU 都会和低速存储设备也就是磁盘进行交互,那么整个写入文件的速度就会受制于低速的存储设备(磁盘)。但如果使用缓冲区的话,每次写操作会先将数据保存在高速缓冲区内存上,当缓冲区的数据到达某个阈值之后,再将文件一次性写入到磁盘上。因为内存的写入速度远远大于磁盘的写入速度,所以当有了缓冲区之后,文件的写入速度就被大大提升了。

如何解决它呢?

下面我们将用.Net7做示例

1.实现项目创建 我们需要创建三个项目
分别是 Share SocketClient SocketServer

如图下图所示

2.实现SocketServer项目

创建一个SocketServer的Class 然后写入以下代码

using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;

namespace SocketServer;

public class SocketServer : IDisposable
{
    /// <summary>
    /// 是否被释放
    /// </summary>
    private bool _disposable;
    /// <summary>
    /// 当前的server的Socket对象
    /// </summary>
    private readonly Socket _socket;

    /// <summary>
    /// 所有的SocketManager的管理集合
    /// 所有客户端都在这里进行管理
    /// </summary>
    private ConcurrentBag<SocketManager> SocketManagers = new ConcurrentBag<SocketManager>();
    /// <summary>
    /// 创建SocketServer对象
    /// </summary>
    /// <param name="ip">监听的ip</param>
    /// <param name="port">监听的端口</param>
    public SocketServer(string ip, int port = 1314)
    {
        _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        var ipAddress = IPAddress.Parse(ip);
        _socket.Bind(new IPEndPoint(ipAddress, port));
        _socket.Listen();
    }

    public void Dispose()
    {
        _disposable = true;
    }

    /// <summary>
    /// 开始等待客户端链接
    /// </summary>
    public void Start()
    {
        _ = Task.Factory.StartNew(async () =>
        {
            while (!_disposable)
            {
                var socket = await _socket.AcceptAsync();
                SocketManagers.Add(new SocketManager(socket));
            }
        });
    }
}

我们看到创建了一个Socket并且监听了指定ip和端口

然后Start()方法是将链接的客户端添加到SocketManager集合管理

在看看SocketManager的方法

using Share;
using System.Net.Sockets;
using System.Text;

namespace SocketServer;

public class SocketManager : IDisposable
{
    /// <summary>
    /// 是否被释放
    /// </summary>
    private bool _disposable = false;

    /// <summary>
    /// 客户端的Socket链接对象
    /// </summary>
    private readonly Socket ClientSocket;

    /// <summary>
    /// 接收缓冲区大小
    /// 默认8kb
    /// </summary>
    private int MessageBufferSize;
    public SocketManager(Socket clientSocket, int? messageBufferSize = null)
    {
        ClientSocket = clientSocket;
        MessageBufferSize = messageBufferSize ?? 1024 * 8;
        Start();
    }

    public void Dispose()
    {
        _disposable = true;
    }

    public void Start()
    {
        _ = Task.Factory.StartNew(async () =>
        {
            while (!_disposable)
            {
                //设置了缓冲区
                var buffer = new byte[MessageBufferSize];

                // 接送的数据不一定有缓冲区那么大
                var len = await ClientSocket.ReceiveAsync(buffer);

                // 获取协议定义的长度
                var length = ByteUtil.BytesToInt(buffer.AsSpan(0, 4).ToArray(), 0);
			   // 注:请自行修改地址
                var file = File.Create("E:\\迅雷下载\\cs.zip");

                // 删掉协议的长度的四个字节
                await file.WriteAsync(buffer.AsSpan(4).ToArray());

                length -= buffer.AsSpan(4).ToArray().Length;

                while ((len = await ClientSocket.ReceiveAsync(buffer))>0)
                {
                    await file.WriteAsync(buffer);
                    length -= len;
                    if(length <= 0)
                    {
                        Console.WriteLine("获取完成");
                        file.Close();
                        break;
                    }
                }
            }
        });
    }
}

当前SocketManager是接收Socket客户端发送的文件并且进行写入到文件中 我们看到ByteUtil.BytesToInt的方法

这个方法可能有些人有疑惑了这是个什么了

首先我们需要解决粘包问题 我们就一开始在发送和接收的时候就定义好了整个发送的消息体,其实可以理解为协议

为了解决掉socket粘包 需要自定义一个通讯协议处理粘包

首先定义消息结构

消息结构由俩部分组成

第一部分由四个字节的消息长度组成 第二部分由消息的整体数据组成这个消息的长度一定是第一部分的所表示的长度

长度 具体消息
xxxx xxxxxxxxx01xxx

这个四个字节就是这条消息的长度(不包括当前四个字节)一般来说四个字节是够用的,这样整个消息体的组成结构就是 0000 0000000000 这样的,前面四个字节一定是整个消息的长度 后面的就是数据

然后需要定义字节转换int和int转换byte的一个方法如下代码

ByteUtil:当前方法是为了协议的长度转换

namespace Share
{
    /// <summary>
    /// 转换协议字节帮助类
    /// </summary>
    public class ByteUtil
    {
        /// <summary>
        /// byte数组转换int
        /// </summary>
        /// <param name="src"></param>
        /// <param name="offset"></param>
        /// <returns></returns>
        public static int BytesToInt(byte[] src, int offset)
        {
            int value;
            value = (int)((src[offset] & 0xFF)
                    | ((src[offset + 1] & 0xFF) << 8)
                    | ((src[offset + 2] & 0xFF) << 16)
                    | ((src[offset + 3] & 0xFF) << 24));
            return value;
        }

        /// <summary>
        /// int转byte数组
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static byte[] IntToBytes(int value)
        {
            byte[] src = new byte[4];
            src[3] = (byte)((value >> 24) & 0xFF);
            src[2] = (byte)((value >> 16) & 0xFF);
            src[1] = (byte)((value >> 8) & 0xFF);
            src[0] = (byte)(value & 0xFF);
            return src;
        }
    }
}

好我们在看看SocketClient:

using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Net;
using System.Text;
using Share;

namespace SocketClient;

public class SocketClient : IDisposable
{
    /// <summary>
    /// 是否释放
    /// </summary>
    private bool _disposable;
    /// <summary>
    /// 当前链接socket对象
    /// </summary>
    private readonly Socket _socket;

    /// <summary>
    /// 创建SocketClient对象
    /// </summary>
    /// <param name="ip">需要连接的ip</param>
    /// <param name="port">需要连接的端口</param>
    public SocketClient(string ip, int port = 1314)
    {
        _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

        _socket.Connect(new IPEndPoint(IPAddress.Parse(ip), port));
    }

    public void Dispose()
    {
        _disposable = true;
    }


    /// <summary>
    /// 发送信息到服务器
    /// </summary>
    /// <param name="value"></param>
    public async Task SendByteAsync(byte[] buffer)
    {
        await SocketExtension.SendByteAsync(_socket, buffer);
    }
}

然后在看SocketClient的Program的方法就是我们的Client的启动类

// 创建SocketClient对象 连接到127.0.0.1的SocketServer服务
var socketClient = new SocketClient.SocketClient("127.0.0.1");
// 读取一个zip文件
var fileStream = File.ReadAllBytes("C:\\Users\\Administrator\\Downloads\\Files.zip");
// 然后调用SendByteAsync
await socketClient.SendByteAsync(fileStream);
// 暂停
Console.ReadKey();

SocketExtension.SendByteAsync内部方法:

using System;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace Share
{
    public static class SocketExtension
    {
        /// <summary>
        /// 发送定义的协议数据结构
        /// </summary>
        /// <param name="socket"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static async Task<int> SendByteAsync(this Socket socket, byte[] value)
        {
            // 定义消息和消息长度的总和的字节数组 
            // 由于定义的消息长度需要占用四个字节所以这个加4
            var bytes = new byte[4 + value.Length];
            Array.ConstrainedCopy(ByteUtil.IntToBytes(value.Length), 0, bytes, 0, 4);
            Array.ConstrainedCopy(value, 0, bytes, 4, value.Length);
            return await socket.SendAsync(bytes, SocketFlags.None);
        }
    }
}

其实这个方法就是将发送的数据组成我们的协议体 前四个字节就是我们的数据长度 后面就是我们的数据了

SocketServer的Program的类:

Console.WriteLine("启动SocketServer中");
// 监听127.0.0.1的端口
var socketServer = new SocketServer.SocketServer("127.0.0.1");
// 启动客户端接收发送接收的服务
socketServer.Start();

Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.WriteLine("启动SocketServer完成");

Console.ReadKey();

然后我们依次启动SocketServer控制台

在启动SocketClient控制台

这个时候我们就可以观察到SocketClient往SocketServer发送的消息了

我们可以将断点打到这个结束位置

等数据传输完成我们就可以发现进入断点


                //设置了缓冲区
                var buffer = new byte[MessageBufferSize];

                // 接送的数据不一定有缓冲区那么大
                var len = await ClientSocket.ReceiveAsync(buffer);

                // 获取协议定义的长度
                var length = ByteUtil.BytesToInt(buffer.AsSpan(0, 4).ToArray(), 0);

                var file = File.Create("E:\\迅雷下载\\cs.zip");

                // 删掉协议的长度的四个字节
                await file.WriteAsync(buffer.AsSpan(4).ToArray());

                length -= buffer.AsSpan(4).ToArray().Length;

                while ((len = await ClientSocket.ReceiveAsync(buffer))>0)
                {
                    await file.WriteAsync(buffer);
                    length -= len;
                    if(length <= 0)
                    {
                        Console.WriteLine("获取完成");
                        file.Close();
                        break;
                    }
                }

这一块代码主要是做的角色接收第一次的消息体然后解析前面四个字节的协议得到后面需要处理的数据的长度

只要找到数据的长度就可以做到出来粘包的问题因为我们可以吧数据拆分出去,将其不粘包,

这就做到了拆包

好的本文讲完了!源码在群里有兴趣的朋友可以加技术交流群:737776595
微信交流群:


来自token的分享!

与Socket 如何处理粘包相似的内容:

Socket 如何处理粘包

Socket 如何处理粘包 什么是粘包什么是半包? 粘包: 比如发送了AA BB 两条消息,但是另一方接收到的消息却是AAB,像这种一次性读取了俩条数据的情况就是粘包 半包: 比如发送的消息是ABC时,另一方收到的是AB和C俩条消息,这就是半包 为什么会有这种问题呢? 这是因为 TCP 是面向连接的

14.2 Socket 反向远程命令行

在本节,我们将继续深入探讨套接字通信技术,并介绍一种常见的用法,实现反向远程命令执行功能。对于安全从业者而言,经常需要在远程主机上执行命令并获取执行结果。本节将介绍如何利用 `_popen()` 函数来启动命令行进程,并将输出通过套接字发送回服务端,从而实现远程命令执行的功能。在实现反向远程命令执行时,我们可以使用 `_popen(buf, "r")` 函数来执行特定的命令,并将其输出重定向到一个

驱动开发:内核封装WSK网络通信接口

本章`LyShark`将带大家学习如何在内核中使用标准的`Socket`套接字通信接口,我们都知道`Windows`应用层下可直接调用`WinSocket`来实现网络通信,但在内核模式下应用层API接口无法使用,内核模式下有一套专有的`WSK`通信接口,我们对WSK进行封装,让其与应用层调用规范保持一致,并实现内核与内核直接通过`Socket`通信的案例。

《吐血整理》高级系列教程-吃透Fiddler抓包教程(33)-Fiddler如何抓取WebSocket数据包

1.简介 本来打算再写一篇这个系列的文章也要和小伙伴或者童鞋们说再见了,可是有人留言问WebSocket包和小程序的包不会抓,那就关于这两个知识点宏哥就再水两篇文章。 2.什么是Socket? 在计算机通信领域,socket 被翻译为“套接字”(套接字=主机+端口号),它是计算机之间进行通信的一种约

[转帖]Strace + pstack发现耗时点

https://www.jianshu.com/p/10ea6fff562c 如何使用strace+pstack利器分析程序性能 本文摘抄自如何使用strace+pstack利器分析程序性能 程序说明 一个简单的socket程序,由server/client组成。server端监听某端口,等待cli

驱动开发:基于事件同步的反向通信

在之前的文章中`LyShark`一直都在教大家如何让驱动程序与应用层进行`正向通信`,而在某些时候我们不仅仅只需要正向通信,也需要反向通信,例如杀毒软件如果驱动程序拦截到恶意操作则必须将这个请求动态的转发到应用层以此来通知用户,而这种通信方式的实现有多种,通常可以使用创建Socket套接字的方式实现,亦或者使用本章所介绍的通过`事件同步`的方法实现反向通信。

驱动开发:内核封装TDI网络通信接口

在上一篇文章`《驱动开发:内核封装WSK网络通信接口》`中,`LyShark`已经带大家看过了如何通过WSK接口实现套接字通信,但WSK实现的通信是内核与内核模块之间的,而如果需要内核与应用层之间通信则使用TDK会更好一些因为它更接近应用层,本章将使用TDK实现,TDI全称传输驱动接口,其主要负责连接`Socket`和协议驱动,用于实现访问传输层的功能,该接口比`NDIS`更接近于应用层,在早期W

深入Python网络编程:从基础到实践

**Python,作为一种被广泛使用的高级编程语言,拥有许多优势,其中之一就是它的网络编程能力。Python的强大网络库如socket, requests, urllib, asyncio,等等,让它在网络编程中表现优秀。本文将深入探讨Python在网络编程中的应用,包括了基础的socket编程,到

[转帖]提高服务端性能的几个socket选项

https://www.cnblogs.com/charlieroro/p/14140343.html 在之前的一篇文章中,作者在配置了SO_REUSEPORT选项之后,使得应用的性能提高了数十倍。现在介绍socket选项中如下几个可以提升服务端性能的选项: SO_REUSEADDR SO_REUS

14.3 Socket 字符串分块传输

首先为什么要实行分块传输字符串,一般而言`Socket`套接字最长发送的字节数为`8192`字节,如果发送的字节超出了此范围则后续部分会被自动截断,此时将字符串进行分块传输将显得格外重要,分块传输的关键在于封装实现一个字符串切割函数,将特定缓冲区内的字串动态切割成一个个小的子块,当切割结束后会得到该数据块的个数,此时通过套接字将个数发送至服务端此时服务端在依次循环接收数据包直到接收完所有数据包之后