用户态网络缓冲区

  • 网络缓冲区原理
    • 为什么需要用户态网络缓冲区
    • Linux下如何接收和发送数据包
    • 用户态网络缓冲区设计的本质
  • 网络缓冲区代码实现

网络缓冲区原理

为什么需要用户态网络缓冲区

在网络开发中,我们经常使用到read/write/recv/send等系统调用接口,我们需要理解这些函数的本质还是个拷贝函数,我们以readwrite为例,他们其实就是将数据从用户空间拷贝到内核空间当中,内核态中是存在接收缓冲区和发送缓冲区的。
在这里插入图片描述
接下来我们来看readwrite系统调用函数的含义:

ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);

对于readwrite函数来说,都是有返回值的,返回值就代表我们实际上拷贝的数量,也就是我当前需要写入到内核缓冲区当中的数量,而参数之一的count所代表的就是预估的一个拷贝的数量。

当前我们就需要思考一个问题,用户态的情况下,我们是不知道对应内核态的缓冲区有多大的,我们怎么能保证我们所需要拷贝的数据一次性就能拷贝过去而不是分好几次进行拷贝的呢?如果一次性拷贝不完剩下的数据不就会被丢掉吗,显然是不行的,所以在这儿我们就需要去设置一个用户态缓冲区来保存这些数据,保证其在没有被完整拷贝之前不会被丢弃掉,这也是用户态缓冲区所需要设置的一个重要的原因之一。

Linux下如何接收和发送数据包

我们都知道,网络通信是围绕着整个网络通信协议栈的:
在这里插入图片描述
在我们用户态看来,数据包就是 data,在 TCP 协议栈当中,是以 segment 表示,IP 协议中以 packet 表示,MAC 当中以 frame 表示,整个协议栈中,数据包都是以 sk_buffer 来进行流转的,协议栈也只会去识别对应的 sk_buffer。

网络数据其实整个流转流程也是在上图这样一个状态下进行流转的,首先我们来看一下接收数据包的流程:

  • 网卡接收到数据报,通过 DMA 将数据包写入到内存(ringbuffer结构当中);
  • 网卡向 CPU 发起硬件中断,CPU 收到硬件中断请求,根据中断表查找中断处理函数,调用中断处理函数;
  • 中断处理函数将屏蔽硬件中断,发起软件中断(硬件中断是一个线程在执行,不能长时间被占用,避免 CPU 频繁被网卡中断,这儿需要使用软件中断处理耗时操作,避免执行时间过长,CPU 无法响应其他的硬件中断);
  • 内核专门线程负责软件中断,从 ringbuffer 当中将数据取出到 sk_buffer 当中(注意,这个是循环操作,直到 ringbuffer 中没有数据);
  • 从帧头取出 IP 协议,判断是 IPV4 还是 IPV6 ,去掉帧头帧尾;
  • 从 IP 头中看出上一层是 TCP 协议还是 UDP 协议,根据五元组或者是 fd 找到对应的 socket ,将数据提取出来放到对应的 socket 接收缓冲区当中,软件中断处理结束以后开启硬件中断;
  • 应用程序通过调用系统调用函数将接收缓冲区当中的数据拷贝到用户的缓冲区当中。

在了解发送数据包的流程的流程时我们需要思考一个问题,UDP/TCP 协议的缓冲区是否一致?

我们要知道对于 UDP 协议来说,他是面向数据报的一种协议,也就是说用户态下发送一个数据包,有多大使用 UDP 协议就会发多大,如果超过对应的长度 UDP 协议就会丢掉多余的部分,也不会重传,这也就意味着 UDP 协议其实是用不到发送缓冲区的,我直接发送原始的数据包即可。但是接收缓冲区却是必不可少的,因为接收数据的过程中我们可能存在一次性接收不完的情况发生,对应的数据就需要先被暂存下来。

再来看一下发送数据包的流程:

  • 用户态下调用系统调用函数将数据拷贝到 sk_buffer 当中并且将数据放到 socket 的发送缓冲区当中(TCP);
  • 网络协议栈从 socket 的发送缓冲区当中取出 sk_buffer 并且会克隆一个新的 sk_buffer(TCP是支持重传机制的,克隆就是为了保证可以进行重传);
  • 根据协议栈向下进行传递,一次增加 TCP/UCP 头部,IP 头部,MAC帧头,帧尾(TCP会进行分段,IP 会进行分片(TCP/UDP 都会));
  • 触发软件中断通知网卡驱动程序,有新的数据包需要进行发送;
  • 网卡驱动程序依次从发送队列中取出数据 sk_buffer 放到 ringbuffer 当中(内存 DMA 区域,网卡读到);
  • 触发网卡发送,发送成功,触发硬件中断,释放掉对应的 ringbuffer 和 sk_buffer(TCP 是克隆的,UDP 是原始的);
  • 当收到 TCP 的 ACK 应答以后,就会释放掉原始的 sk_buffer。

对于 TCP 协议来说,他的发送缓冲区是分段设计的,可以参考一下之前的一片文章TCP协议详解,我们在了解了网络数据包的接收和发送原理以后,我们再回来看发送缓冲区与接收缓冲区。

用户态网络缓冲区设计的本质

发送缓冲区

对于发送缓冲区来说,我们可以理解为生产者与消费者速度不一致的问题,生产者生产数据的速度如果大于消费者消费的速度,我们就需要保证生产者发送的数据被接收到,那我们就需要一个缓冲区将数据先保存下来,等待对端对数据进行处理。

另一个解决的问题就是用户态本身不会知道内核中缓冲区有多大,并不是一次将数据都发送完毕,此时就需要预先将数据存储起来,缓存那些没有被发送出去的数据。

接收缓冲区

接收缓冲区同样也是要去解决掉生产者的速度大于消费者速度的问题,跟发送缓冲区一致,而另一个要解决的问题就是粘包问题。

为什么会出现粘包问题?

对于用户态来说,从内核的接收缓冲区当中读取到的数据是不确定的,我们不能保证他就是一个完整的包,他可能是半个包,也可能是一个半的包,如果是半个包,我们就需要先将这个数据包保存下来,等到读取到一个完整包数据以后在进行处理,如果是一个半包的数据,就需要优先去处理一个完整包的数据,将剩下半个包的数据暂存下来,基于这种考虑,就需要用到我们的用户态接收缓冲区。

如何解决粘包问题?

解决粘包问题有两种方式:

  • 我们程序员自己去制定一套规则对数据包进行处理,比如说用特殊分隔符界定数据包(\r\n),我们再读取到这个数据包的时候,如果读取到的是\r\n,就证明他之前的数据是一个完整的数据包,此时就进行处理即可;
  • 用长度去界定数据包,我们可以让一个数据包的头部分配两个字节去保存一个完整的数据包的长度,我们在读取数据的过程中只读取这个长度的数据包,然后进行处理,就保证了我们处理的是一个完整的数据包。

网络缓冲区代码实现

实现一个用户态的网路缓冲区,我们首先需要考虑什么样的数据结构最为合适,第一种就是定长数组,固定长度。
在这里插入图片描述
如果使用定长数组的话,会存在的问题就在于:

  • 空间大小不确定,会出现分分配空间不足或者是分配的空间太大了,导致空间浪费的现象发生;
  • 会频繁的进行数据的腾挪,因为我们读取到一个完整的数据包以后,就需要将剩下的数据腾挪首部的位置,保证下一次的数据读取。

接下来我们可以考虑 ringbuffer 这种环形队列结构:
在这里插入图片描述
对于这种结构来说:

  • 解决了数据腾挪的问题,因为他是循环的结构,但是他也是固定大小,伸缩性也会比较差,而且还会出现数据离散性的问题。

在这里插入图片描述
对于离散性,我们可以只用系统调用函数readv/writev去解决掉,这两个函数的作用就是用于将多个非连续的内存缓冲区中的数据一次性写入文件描述符,解决掉数据不连续我们依然可以读取到一个 buffer 中的问题。

对于伸缩性,我们可以使用 STL 容器中的 vector 来进行实现,他是可以进行扩容的,那么最终的一个数据结构就是一个 vector 加上 head 与 tail 两个索引来进行设计。
在这里插入图片描述

#ifndef __MESSAGE_BUFFER__
#define __MESSAGE_BUFFER__#include <bits/types/struct_iovec.h>
#include <stdint.h>
#include <vector>
#include <cstring>
#include <sys/uio.h>
#include <errno.h>class MessageBuffer
{
public:MessageBuffer() : rpos_(0), wpos_(0){buffer_.resize(4096);}explicit MessageBuffer(std::size_t size) : rpos_(0), wpos_(0){buffer_.resize(size);}// 允许移动构造MessageBuffer(MessageBuffer &&other) noexcept: buffer_(std::move(other.buffer_)), rpos_(other.rpos_), wpos_(other.wpos_){other.rpos_ = 0;other.wpos_ = 0;}// 移动赋值MessageBuffer &operator=(MessageBuffer &&other) noexcept{if (this != &other){buffer_ = std::move(other.buffer_);wpos_ = other.wpos_;rpos_ = other.rpos_;other.wpos_ = 0;other.rpos_ = 0;}return *this;}// 获取头指针uint8_t* GetBasePointer(){return buffer_.data();}// 获取读指针uint8_t* GetReadPointer(){return buffer_.data() + rpos_;}// 获取写指针uint8_t* GetWritePointer(){return buffer_.data() + wpos_;}// 移动读的下标void ReadCompleted(std::size_t size){rpos_ += size;}// 移动写的下标void WriteCompleted(std::size_t size){wpos_ += size;}// 有效数据长度std::size_t GetActiveSize() const{return wpos_ - rpos_;}// 当前空闲空间,不需要腾挪数据std::size_t GetFreeSize() const{return buffer_.size() - wpos_;}// 整个buffer的大小std::size_t GetBufferSize() const{return buffer_.size();}// 腾挪数据void NormalSize(){if (rpos_ > 0) {std::memmove(buffer_.data(), buffer_.data() + rpos_, GetActiveSize());wpos_ -= rpos_;rpos_ = 0;}}// 确定当前空间是否足够,尽可能的不去进行扩容和腾挪数据void EnsureSpace(std::size_t size){if (GetBufferSize() - GetActiveSize() < size) {buffer_.resize(buffer_.size() + std::max(size, buffer_.size() / 2));NormalSize();}else if (GetFreeSize() < size) {NormalSize();}}// 写进用户态缓冲区void Write(const uint8_t* data, std::size_t size){if (size > 0){EnsureSpace(size);std::memcpy(GetWritePointer(), data, size);WriteCompleted(size);}}// 获取到所有的数据std::pair<uint8_t*, std::size_t> GetAllData(){return {GetReadPointer(), GetActiveSize()};}// 获取第一个 \r\n 之前的数据的指针和大小(若未找到返回nullptr和0)std::pair<uint8_t *, std::size_t> GetDataUntilCRLF(){uint8_t* data = GetReadPointer();std::size_t active_size = GetActiveSize();for(size_t i = 0; i < active_size - 1; i++){if(data[i] == '\r' && data[i + 1] == '\n'){return {data, i};}}return {nullptr, 0};}// linux reactor readv// 1. 尽可能的不腾挪数据// 2. 避免了每次都从栈上拷贝到堆上int Recv(int fd, int* err){char extra[65535]; // UDP最大发送长度,大于这个长度需要在应用自己分层struct iovec iov[2];iov[0].iov_base = GetWritePointer();iov[0].iov_len = GetFreeSize();iov[1].iov_base = extra;iov[1].iov_len = 65535;// 通过readv读去离散型数据int n = readv(fd, iov, 2);if (n < 0) {*err = errno;return n;} else if (n == 0) {*err = ECONNRESET;return 0;} else if (n < GetFreeSize()) {WriteCompleted(n);return n;} else {std::size_t extra_size = n - GetFreeSize();WriteCompleted(GetFreeSize());Write(reinterpret_cast<uint8_t*>(extra), extra_size);return n;}}/*char buffer[65535];int n = read(fd, buffer, 65535);if (n == 0) {// 断开连接} else if (n < 0) (// ETif (errno == EINTR){}if (errno == EAGAIN I| errno == EWOULDBLOCK){//读取数据时没有数据可读}else {// 发生错误}else {//读取到数据Write(buffer, n);*/MessageBuffer(const MessageBuffer &) = delete;MessageBuffer &operator=(const MessageBuffer &) = delete;private:std::vector<uint8_t> buffer_;std::size_t rpos_;std::size_t wpos_;
};#endif

注意:

  • 我们当前的设计当中,应该尽可能的去保证数据不进行腾挪和扩容,这个也是会产生消耗的;
  • 我们从内核的接收缓冲区当中读取数据时,一般情况下都会有一个操作,就是将对应的数据拷贝到我们的栈上,然后在读到对应的用户态缓冲区当中,这相当于是进行了两次数据拷贝,在我们的设计当中,使用了 readv 函数,支持离散性数据拷贝,避免了两次数据拷贝情况的发生,也保证了尽量不去腾挪数据的情况。

在这里插入图片描述
注意,我们这儿所谈到的缓冲区是用户态网络缓冲区,跟内核的网络缓冲区是存在区别的,这两个概念是不可以进行混淆的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/912245.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/912245.shtml
英文地址,请注明出处:http://en.pswp.cn/news/912245.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序实现简版点赞动画

这是第二次写canvas&#xff0c;基于微信小程序文档demo进行改写 demo效果为方块横向来回循环移动 我想做的是直播间那种点赞效果&#xff0c;竖向曲线移动、方块换成图片、点击添加绘制元素 第一阶段实现竖向曲线移动、点击添加绘制元素&#xff1b;下一阶段讲方块替换为图…

实现一个AI大模型当前都无法正确实现的基础二叉树读取算法

概述 图1: 图2: 上图帮大家温习完全二叉树的概念&#xff0c;本文讲的是完全顺序二叉树的初始化 华为的员工、考过华为OD的员工、参加过其他类似大厂的考试的员工一般做过二叉树的初始化&#xff0c;甚至有些还碰到过手撕代码时面试官要求做二叉树遍历&#xff0c;看完本文的…

【攻防篇】阿里云服务器中 如何关闭docker api端口

在阿里云服务器&#xff08;ECS&#xff09;上&#xff0c;Docker API 默认监听 2375&#xff08;非加密&#xff09;和 2376&#xff08;TLS加密&#xff09;端口。如果未正确配置&#xff0c;可能被恶意利用&#xff08;如挖矿攻击&#xff09;。以下是关闭和加固 Docker API…

暑假复习篇之类与对象

面向对象&#xff1a;①类与对象②封装③继承④接口 类与对象&#xff1a; 概念&#xff1a;类就是类别的意思 用class表示 / 面向对象编程&#xff0c;万物皆可编程&#xff0c;在程序中表示一个事物时&#xff0c;往往因为事物的复杂程度导致编程的代码非常复杂 【基本数…

RabbitMQ RPC模式Python示例

文章目录 1.服务端2.客户端3.调用结果 1.服务端 #!/usr/bin/env python3 # -*- coding: UTF-8 -*- """ File: rabbitmq_server.py Date: 2025/6/26 10:42 Author: xxx Description: 1. RabbitMQ服务端&#xff0c;支持多节点命令执行 2. 作为被控…

Rust代码规范之蛇形命名法和驼峰命名法

Rust 使用两种主要的命名风格&#xff1a;驼峰命名法&#xff08;UpperCamelCase&#xff09;和蛇形命名法&#xff08;snake_case&#xff09;。通常&#xff0c;类型&#xff08;如结构体、枚举、特征&#xff09;使用驼峰命名法&#xff0c;而变量、函数、方法等使用蛇形命名…

编写CSS的格式

1、内联样式的css import React, { PureComponent } from reactexport class App extends PureComponent {constructor() {super()this.state {fs: 20}}render() {const { fs } this.statereturn (<div><p style{{ color: red, fontSize: ${fs}px }}>哈哈哈哈哈…

Redis—主从复制

引言 Redis的应用还得是在分布式系统当中。在分布式系统中&#xff0c;涉及到一个非常关键的问题&#xff0c;就是单点问题。例如&#xff0c;如果某个服务器程序&#xff0c;只有一个节点&#xff08;只搞了一个物理服务器&#xff0c;来部署这个服务器程序&#xff09;&…

【网络安全】从IP头部看网络通信:IPv4、IPv6与抓包工具 Wireshark 实战

从IP头部看网络通信&#xff1a;IPv4、IPv6与抓包工具 Wireshark实战 在网络安全分析和数据通信的世界中&#xff0c;一切都始于“数据包”。数据包是网络上传输的基本单位&#xff0c;而数据包的结构与内容&#xff0c;正是我们理解网络行为的核心。本文将带你深入了解 IP 协…

IPv4网络地址分类

目录 一、核心分类标准 二、详细范围与主机数量 1. A类网络&#xff08;超大规模网络&#xff09; 2. B类网络&#xff08;中大型网络&#xff09; 3. C类网络&#xff08;小型网络&#xff09; 三、三类网络对比表 四、保留地址说明 五、现代网络中的变化 六、主机数…

Qt:QCustomPlot库简介

QCustomPlot 是一个基于 Qt 框架的轻量级 C 绘图库&#xff0c;专为高效绘制二维图表&#xff08;如曲线图、柱状图、金融图表等&#xff09;而设计。相比 Qt Charts 模块&#xff0c;它以 高性能 和 高度可定制性 著称&#xff0c;尤其适合需要实时数据可视化的科学计算、工业…

【云桌面容器KasmVNC】如何关闭SSL使用HTTP

1 缘起 根据实际的诉求,调整实现方式。 为用户提供云浏览器(通过浏览器访问远程浏览器),多用户的每个任务提供资源隔离的云浏览器。 该功能,由同事祥嵩曾调研与开发,使用KasmVNC实现功能,非常佩服祥嵩,无论是技术广度还是技术深度都是杠杠滴,无可挑剔。 实际的诉求是…

跟着AI学习C#之项目实战-电商平台 Day5

&#x1f4c5; Day 5&#xff1a;订单提交与支付模拟 ✅ 今日目标&#xff1a; 创建 Order 和 OrderItem 模型实现从购物车生成订单的功能模拟支付流程&#xff08;成功/失败页面&#xff09;添加订单状态跟踪&#xff08;如“待付款”、“已发货”等&#xff09;提交 Git 版…

复杂驱动开发-TLE9471的休眠流程与定时唤醒

文章目录 前言休眠流程定时唤醒功能总结 前言 开发SBC时非常重要的一环就是开发休眠流程&#xff0c;其目的是为了保证接KL30的ECU在休眠模式下尽可能小的消耗低压蓄电池的电量&#xff0c;防止车辆放置长时间后出现亏电。而定时唤醒功能在部分ECU中会有需求休眠后定期对车辆状…

Spark 之 Reuse

src/main/scala/org/apache/spark/sql/execution/reuse/ReuseExchangeAndSubquery.scala case object ReuseExchangeAndSubquery extends Rule[SparkPlan] {def apply(plan: SparkPlan): SparkPlan = {if (conf.exchan

Solidity学习 - 错误处理

文章目录 前言EVM错误处理机制EVM错误处理的核心特性程序中的错误处理 错误抛出方法require()函数require()触发异常的场景关键特性 assert()函数assert()触发异常的场景关键特性 require() vs assert()&#xff1a;选择指南revert()函数关键特性 异常捕获&#xff1a;try/catc…

如何永久删除Android上的短信[无法恢复]

当您不再保留 Android 设备时&#xff0c;您将需要彻底删除所有私人数据&#xff0c;包括短信。因此&#xff0c;有必要了解如何永久删除Android上的短信。现在&#xff0c;阅读本指南&#xff0c;掌握消除信息的实用方法。 第 1 部分&#xff1a;如何一键永久删除 Android 上的…

P12894 [蓝桥杯 2025 国 Java B] 智能交通信号灯

[Problem] \color{blue}{\texttt{[Problem]}} [Problem] 给定一个长度为 n n n 的数组 a 1 … n a_{1\dots n} a1…n​&#xff0c;进行 m m m 次一下操作&#xff1a; 给定 l , r l,r l,r&#xff0c;求出 ∑ l ≤ i < j ≤ r mex { a i , a j } \sum\limits_{l \le…

华为云Flexus+DeepSeek征文|基于华为云一键部署的 Dify-LLM 平台构建智能试卷生成助手

目录 前言 1 华为云Dify-LLM应用平台部署 1.1 一键部署平台简介 1.2 四步完成部署流程 2 接入华为云 DeepSeek 自定义大模型 2.1 ModelArts Studio 模型服务介绍 2.2 配置自定义大模型 3 创建试卷生成工具&#xff08;工作流&#xff09; 3.1 设计 DSL 工作流 3.2 工…

嵌入式硬件与应用篇---寄存器GPIO控制

在 ARM 架构中&#xff0c;通过 32 位寄存器控制 GPIO&#xff08;通用输入输出&#xff09;的核心步骤和方法可分为以下几个关键环节&#xff0c;结合不同芯片的实现差异&#xff0c;具体操作需参考对应的数据手册&#xff1a; 一、GPIO 控制的核心步骤 1. 使能 GPIO 时钟 …