用戶層網絡緩衝區(固定內存,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