網絡緩沖區

用戶態網絡緩沖區

  • 網絡緩沖區原理
    • 為什么需要用戶態網絡緩沖區
    • 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 時鐘 …