用戶層網絡緩衝區(固定內存,ringbuffer,chainbuffer)

1:網絡緩衝區理解

1.1:理解背景

我們在網絡編程時,通常以五元組,一個 fd 標識一個連接(套接字 fd)。

==》每個連接其實有接收消息和發送消息的功能。

==》內核爲每個連接分配了固定大小的發送緩衝區和接收緩衝區(套接字緩衝區)。

==》我們通過相關 api 接口 (如 send(),recv()) 根據五元組標識操作對應緩衝區。

以網絡通信爲例理解,個人理解如下:

註釋:這裏套接字 fd 對應的緩衝區中的數據,其實是由內核協議棧解析後,我們實際發送 / 接收的數據。(udp/tcp 協議,由內部協議棧處理,我們關注的是我們的數據)

1.2:用戶層思考(引入緩衝區)

1.2.1:概述:

用戶層,我們主要就是通過套接字 fd,使用接口 send() 或者 recv(),操作內核中對應套接字的發送緩衝區和接收緩衝區。

1.2.2:方案思考:

針對每個連接,每個緩衝區中存儲的數據可能是多次發送的多個消息。(tcp 是可靠的能保證接收順序,udp 可能需要用戶層控制一下)

===》我們需要定製協議,區分多個數據包,每個數據包的完整性。

===》針對發送緩衝區,如果緩衝區剩餘內存不夠,我們應該怎麼處理。

===》針對接收緩衝區,如果我們取數據,如果沒能取到一個完整數據包的數據,該怎麼處理?

1.2.3:如何區分多個包?(用戶層協議)

相關方案可以自己設計,大概方案有:

1:特定結束收尾符標識。(例如:telnet 用 \ n 標識 redis 用 \r\n 標識)

2:發送固定大小的數據包。(根據固定大小進行數據的解析)

3:固定內存大小標識接收數據長度 + 數據包。(長度 + 數據)

4:類似 tcp/udp 協議包,自己設計協議。

1.2.4:優化發送與接收,如何保證接收到完整數據包,發送時內核緩衝區不夠?(緩衝區)

1.2.4.1:引入問題

接收時,如果收到的不是完整的數據包,需要對數據進行緩存。

發送時,如果緩衝區內存不夠,需要對未發送的數據進行緩存。

1.2.4.2:引入解決方案(緩衝區)

問題引申爲需要緩衝區的問題,在用戶層處理時,我們需要緩衝區來協調業務。

這裏可以知道,我們緩衝區的方案應該是:

===》每個連接(套接字 fd),對應一個發送緩衝區和一個接收緩衝區。

1.2.4.3:緩衝區實現方案

1:可以用固定內存作爲緩衝區(需要變量維護讀寫指針位置,每次完數據對緩衝區剩餘數據進行移動處理)==》這個方案我用過

2:ringbuffer (用結構體標識內存地址,內存大小,數據讀的位置,寫的位置對結構進行控制)

3:chainbuffer(對 ringbuffer 的優化)

2:緩衝區實現方案。

2.1:固定內存作爲緩衝區(用過)

2.1.1:認知

申請固定內存作爲緩衝區,用讀指針和寫指針進行標識。

==》初始化,申請堆內存,作爲緩衝區。

==》寫數據,從 write_ptr,判斷數據是否可寫,可寫才能寫入。

==》讀數據,從 read_ptr 開始,需要根據 ** 用戶協議 (如包尾特定標識,TLV)** 判斷是否夠完整的包,取數據進行消費。(同時,爲了適應業務,需要把讀後 read_ptr 指針指向堆內存起始位置)

==》釋放,釋放堆內存即可。

2.1.2:實現方案:

==》申請一塊內存

==》每次取數據後,始終把可讀區域放在內存最前面,保證空閒內存可用。(每次都要 memmove)

==》需要標識可寫位置,以及剩餘可寫的內存的大小。

==》需要標識的參數:內存地址(也是讀首地址),可寫地址(指針 / 數字標識即可),可寫內存大小(數字標識即可)

注意:前提是可判斷包的完整,自定方案(TLV, 或者包尾特定標識或者固定長度)

3.1.3:優缺點分析

最簡單的緩存方案,能做臨時緩存

==》1:可以發現可寫數據包大小有限制,在包過大時,是不可行的(空間利用率低,適應業務場景侷限(限制包的大小),伸縮性查,)

==》2:每次讀數據,需要移動緩存中數據位置,memmove

2.2:ringbuffer 作爲緩衝區(用過有源碼)

2.2.1:原理

==》底層還是固定內存,

==》只是針對存 / 取數據的方案由我們進行控制,實現 api,

==》從封裝後使用上看,對內存的操作,更像是一個環。

總結:起始固定緩衝區,進行封裝 api,使首部已取空閒區和尾部未用空閒區可以連接使用,同事消費是也要注意,到內存尾部後,取最前面的數據。

2.2.2:實現

定義結構:需要標識內存地址,可讀位置,可寫位置,以及剩餘可寫大小。

需要根據業務場景做一定的協調,這裏主要是放數據時,環的處理,和寫數據時,環的處理。

==》其實就是需要注意:讀寫指針錯位時,對內存的控制。

2.2.3:主要定義數據結構和 api 接口如下:

以前根據業務實現的一塊邏輯,業務不一定適配,但是對讀寫指針錯位環的處理思路可參考,留作自己筆記備份。

typedef struct RINGBUFF_T{
  void * data;
  unsigned int size;
  unsigned int read_pos;   //數據起始位置
  unsigned int write_pos;  //數據終止位置
}ringbuffer_t;
//創建ringbuffer
ringbuffer_t * ringbuffer_create(unsigned int size);
//銷燬ringbuffer
void ringbuffer_destroy(ringbuffer_t * ring_buffer);
//判斷是否是完整的數據  然後進行處理 自行根據業務實現
//int ringbuffer_get_len(ringbuffer_t *ring_buffer);
//往ringbuffer中存數據 寫入
int ringbuffer_put(ringbuffer_t * ring_buffer, const char* buffer, unsigned int len)
{
    //兩塊空閒區 並且可以放入
  if(ring_buffer->write_pos >=ring_buffer->read_pos &&(len <(ring_buffer->size - ring_buffer->write_pos +ring_buffer->read_pos)))
  {
    //進行拷貝
    if(ring_buffer->size - ring_buffer->write_pos >len)
    {
      memcpy(ring_buffer->data + ring_buffer->write_pos, buffer, len);
      ring_buffer->write_pos += len;
    }else
    {
      unsigned int right_space_len = ring_buffer->size - ring_buffer->write_pos;
      memcpy(ring_buffer->data + ring_buffer->write_pos, buffer, right_space_len);
      memcpy(ring_buffer->data, buffer+right_space_len, len - right_space_len);
      ring_buffer->write_pos = len - right_space_len;
    }
    return 0;
  }
  if(ring_buffer->write_pos <ring_buffer->read_pos && (ring_buffer->read_pos - ring_buffer->write_pos) >len)
  {
    memcpy(ring_buffer->data + ring_buffer->write_pos, buffer, len);
    ring_buffer->write_pos += len;
    return 0;
  }
  return -1;
}
//從ringbuffer中取數據做處理, 判斷接收到的字符是否是終結符號,就可以去做處理
//取完數據後重置ringbuffer的位置  讀取
int ringbuffer_get(ringbuffer_t * ring_buffer, char * buffer, unsigned int len)
{
  //這裏建立在ringbuffer_get_len 的基礎上,傳入入參,取出數據
  int data_len = ringbuffer_use_len(ring_buffer);
  if(data_len >= len)
  {
    LOG_ERROR("para buffer is not enough space \n");
    return -1;
  }
  if(ring_buffer->write_pos >ring_buffer->read_pos )
  {
    memcpy(buffer, ring_buffer->data + ring_buffer->read_pos, data_len);
  }else
  {
    memcpy(buffer, ring_buffer->data+ring_buffer->read_pos, ring_buffer->size - ring_buffer->read_pos);
    memcpy(buffer+ring_buffer->size - ring_buffer->read_pos, ring_buffer->data, data_len - (ring_buffer->size - ring_buffer->read_pos));
  }
    //這裏是符合我業務的一個處理,判斷可取,一次取完,可自行設計
  ring_buffer->write_pos = 0;
  ring_buffer->read_pos = 0;
  return 0;
}

2.2.4:優缺點分析。

封裝控制了讀寫指針的操作,使我們不再關注細節。

==》0:需要控制讀寫指針錯誤情況。

==》1:還是固定的內存塊,對包的大小有限制(空間利用率不高)

==》2:參考固定內存做緩存,雖然不必指針的移動,但在特定情況下,需要 memcpy 多次。

==》3:可擴展,可伸縮性差。

注意:一般控制內存都在堆上,棧上申請過多內存會導致棧溢出。

2.2.5:可優化

對多塊分開的內存需要寫數據,除了 memcpy 多次,可以用 readv()

取多塊不連續的內存中的數據到一塊緩存,除了 memcpy 多次,可以用 writev()

#include <sys/uio.h>
//從fd中讀數據,寫入兩個或者多個不連續的內存中
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
//讀取兩個或多個不連續的內存,寫入到fd中
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
    void  *iov_base;    /* Starting address */
    size_t iov_len;     /* Number of bytes to transfer */
};

2.3:chainbuffer(理論整理思路)

環形緩衝區,可以通過 writev() 和 readv() 優化多次的 IO 調用。

如何優化使內存可擴展,提高其伸縮性?

2.3.1:原理

固定內存和 ringbuffer 都是使用了一塊內存作爲緩衝區,需要處理讀寫錯位的現象

我們可以使用多塊內存作爲緩衝區,把讀寫錯誤以及可擴展性,擴展到內存結構控制上。

參考結構定義看圖:

2.3.2:結構源碼

//可以調整控制  就是管理一塊內存的讀與寫
typedef struct buff_chans_s
{
  struct buff_chans_s *next; //多塊緩存鏈表,start指向鏈表最開始的元素,end指向最後一個,last_use是中間正在使用的塊
  unsigned int all_size;    //內存塊總大小
  unsigned int misalign;    //開始讀數據的位置
  unsigned int buffer_size;  //還有數據的大小,不一定寫滿
  unsigned char *buffer;    //內存塊的地址
}buff_chans_t;
typedef struct buffer_s
{
  buff_chans_t* start;         //緩衝區第一塊內存
  buff_chans_t* end;      //緩衝區最後一塊內存指向
  buff_chans_t** last_use;  //最後一個正在使用的內存塊,即正在寫的緩存
  unsigned int total_len;    //所有內存的總大小,即緩衝區總大小
}buffer_t;

2.3.3:接口分析

1:初始化:定義 buffer_t 結構的對象,未對內部結構進行初始化。

2:存數據:

===》申請一塊內存,作爲緩衝區的一個節點,開始使用。(這裏可以做大小控制,也可以根據包的大小做適當調整)

===》開始申請內存,和沒有可用內存時(end=*last_use)申請內存一樣

===》申請內存,由 buff_chans_t 結構進行控制

3:取數據

==》從 start 節點開始取數據,符合要求則取。

==》如果 start 節點數據取完,可以對節點進行移動控制,達到內存可重用。

4:釋放

==》釋放所有節點內存

2.3.4:優缺點分析

實現了內存的可伸縮擴展。

還是會有需要多次拷貝的場景,但是理解上更直觀,(本節點尾部 + next 節點的首部)

3:總結

其實就是對緩衝區的實現方案,在自己已有的理解上做了整理。

固定內存,ringbuffer, 以及 chain buffer, 其實就是操作一塊或者多塊內存,作爲中間緩衝區爲業務服務。

固定內存的方案和 ringbuffer 的方案我有用過,且有對應的源碼 demo。

chain buffer 侷限於個人理解。

除此之外,新接觸到的接口知識點 readv() 和 writev()。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OkMBCm2uzuuxq6KpWRKvbA