首先说说为什么需要缓冲区?
第一,我们在就行linux网络编程的时候,每次recv/send,数据都不一定能完全接收/发送,因为这里面还会涉及内核的缓冲区容量问题,
第二,我们接收到的数据需要我们的处理函数处理,但是有时候我们的处理函数可能来不及处理,或者处理完的数据太多,我们的socket来不及发送,这些都需要我们在应用层把数据保存起来
网络数据包接收的流程:
1.物理层:数据包到达网卡,网卡接收以太网帧,进行基本验证,比如crc循环冗余验证
2.网卡 DMA 上传到内核内存,网卡利用DMA技术讲数据直接写入网卡驱动预先分配好的接收缓冲区
3,网卡触发硬件中断,网卡经过硬中断告诉cpu接收到数据
4.内核进入中断上下文 → 软中断处理
5.链路层处理(以太网),解析以太网头:目的 MAC、源 MAC、协议类型,判断是ipv4,ipv6还是arp,交给上层函数
6.网络层处理(IP),解析 IP 头:版本、长度、TTL、协议(TCP/UDP/ICMP)、校验和,路由判断:是不是本机接收,是:继续往上,不是:转发(如果开启转发),根据协议(TCP/UDP)交给传输层
7.传输层处理
udp:校验和检查,根据目的端口找到对应 socket,把数据放到 socket 接收队列
tcp:解析 TCP 头:端口、序号、确认号、标志位(SYN/ACK/FIN),查找 socket,做滑动窗口、重传、拥塞控制、时序排序,合法数据放入socket 接收队列
8.应用层:从内核拷贝到用户空间,应用程序调用:read()/recv()/recvfrom(),内核把sk_buff里的数据拷贝到用户态缓冲区,应用拿到数据,流程结束
怎么设计网络缓冲区呢
目前的解决方案有:
1. 定长 buffer
2. ringbuffer
3. chainbuffer
定长buffer:优点是简单,容易实现,缺点是数据挪动频繁,导致开销大,且需要实现扩容机制
ringbuffer:其实就是一个类似队列的一个环形数组,有一个头指针还有尾指针,特点是取数据后不需要挪移数据,但是一样需要实现扩容机制
chainbuffer:
这种设计就比较特别了,不需要大规模挪移数据,而且天生就具有自动扩容的机制。
下面就介绍一下简单的实现,也许用的结构体字段和上面不太一样:
结构体构建:
// 缓冲区节点结构 struct buf_chain_s { struct buf_chain_s *next; // 下一个节点 uint32_t buffer_len; // 节点内存总容量 uint32_t off; // 已写入数据长度 uint32_t is_erad; uint8_t *buffer; // 节点内存指针 }; // 缓冲区管理结构 struct buffer_s { buf_chain_t *first; // 第一个节点 buf_chain_t *last; // 最后一个节点 buf_chain_t *last_read_p; //最后读到数据的点,刚开始就是null uint32_t total_len; // 缓冲区总有效数据长度 uint32_t BUFFER_CHAIN_MAX; // 缓冲区总容量上限 };我们为调用者暴露了几个方法:
buffer_t *buffer_new(void); int buffer_add(buffer_t *buf, const void *data_in, uint32_t datlen); uint32_t buffer_read(buffer_t *buf, void *data_out, uint32_t datlen); void buffer_destroy(buffer_t *buf)接下来就是我的整体实现:
#include "buffer.h" // 缓冲区节点结构 struct buf_chain_s { struct buf_chain_s *next; // 下一个节点 uint32_t buffer_len; // 节点内存总容量 uint32_t off; // 已写入数据长度 uint32_t is_erad; uint8_t *buffer; // 节点内存指针 }; // 缓冲区管理结构 struct buffer_s { buf_chain_t *first; // 第一个节点 buf_chain_t *last; // 最后一个节点 buf_chain_t *last_read_p; //最后读到数据的点,刚开始就是null uint32_t total_len; // 缓冲区总有效数据长度 uint32_t BUFFER_CHAIN_MAX; // 缓冲区总容量上限 }; //创建缓冲区 buffer_t *buffer_new() { buffer_t *buf = (buffer_t *)malloc(sizeof(buffer_t)); if(buf == NULL){ return NULL; } buf->first = NULL; buf->last = NULL; buf->last_read_p = NULL; buf->total_len = 0; buf->BUFFER_CHAIN_MAX = DEFAULT_BUFFER_MAX; return buf; } static buf_chain_t *buf_chain_new(uint32_t size){ buf_chain_t *buff_node = (buf_chain_t *)malloc(sizeof(buf_chain_t)); if(buff_node == NULL){ return NULL; } buff_node->buffer_len = size; buff_node->next = NULL; buff_node->off = 0; buff_node->is_erad = 0; buff_node->buffer = (uint8_t *)malloc(size); if(buff_node->buffer == NULL){ free(buff_node); return NULL; } return buff_node; } int buffer_add(buffer_t *buf, const void *data_in, uint32_t datlen){ buf_chain_t *chain; if (datlen > DEFAULT_BUFFER_MAX - buf->total_len) { printf("总容量不够,需要%u, 还剩%u\n", datlen, buf->BUFFER_CHAIN_MAX - buf->total_len); return -1; } const uint8_t *data = (const uint8_t *)data_in; chain = buf->last; if (chain == NULL) { chain = buf_chain_new(DEFAULT_CHAIN_SIZE); if(chain == NULL){ return -1; } buf->last_read_p = chain; buf->first = chain; buf->last = chain; } //循环写入 while(datlen > 0){ //计算最后一个节点是否够用 uint32_t size = chain->buffer_len - chain->off; if(size >= datlen){ //直接复制 memcpy(chain->buffer + chain->off, data, datlen); chain->off += datlen; buf->total_len += datlen; datlen = 0; } else { // 先复制,然后创建节点,继续复制 memcpy(chain->buffer + chain->off, data, size); chain->off += size; buf->total_len += size; data += size; datlen -= size; if(buf->first != NULL && buf->first != buf->last_read_p ){ buf_chain_t *newnode = buf->first; buf->first = newnode->next; newnode->is_erad = 0; newnode->next = NULL; newnode->off = 0; chain->next = newnode; // 链接新节点 chain = newnode; // 切换到新节点 buf->last = newnode; // 更新last指针 printf("复用成功\n"); }else{ buf_chain_t *newnode = buf_chain_new(DEFAULT_CHAIN_SIZE); chain->next = newnode; // 链接新节点 chain = newnode; // 切换到新节点 buf->last = newnode; // 更新last指针 } } } return 0; } uint32_t buffer_read(buffer_t *buf, void *data_out, uint32_t datlen){ if(buf == NULL || data_out == NULL || datlen == 0 || buf->total_len == 0){ return 0; } uint32_t read_len = 0; uint8_t *dest_data = (uint8_t*)data_out; buf_chain_t *chain = buf->first; //找到还没有读取的数据 while(chain){ if(chain->is_erad < chain->off){ break; } chain = chain->next; } while(chain && read_len < datlen){ //当前节点能够读取的数据 uint32_t avail_read_len = chain->off - chain->is_erad; if(avail_read_len > (datlen - read_len)){ memcpy(dest_data + read_len,(chain->buffer) + chain->is_erad,datlen - read_len); buf->last_read_p = chain; buf->total_len -= datlen - read_len; chain->is_erad += datlen - read_len; //以后得更新缓冲区总长度 read_len = datlen; return datlen; }else{ //当前节点没有datlen长度的数据,读取当前节点数据,节点next memcpy(dest_data + read_len,(chain->buffer) + chain->is_erad,avail_read_len); buf->total_len -= avail_read_len; chain->is_erad += avail_read_len; //chain->off += avail_read_len; buf->last_read_p = chain; chain =chain->next; read_len += avail_read_len; } } //以后得更新缓冲区总长度 return read_len; } void buffer_destroy(buffer_t *buf){ if(buf == NULL){ return; } buf_chain_t *chain = buf->first; while(chain != NULL){ buf_chain_t *temp = chain; chain = chain->next; free(temp->buffer); free(temp); } free(buf); return ; } int main() { buffer_t *buffer_ = buffer_new(); char *data = "我真的好想你,在每个雨季!"; char *data1 = "abcdefghijklmnopqrstuvwxyz\n"; char *data2 = "我真的好"; buffer_add(buffer_, data, strlen(data)); buffer_add(buffer_, data1, strlen(data1)); printf("缓冲区总长度:%u\n", buffer_->total_len); uint32_t datlen = buffer_->total_len; char data_out[100] = {0}; buffer_read(buffer_,data_out,12); buffer_add(buffer_, data2, strlen(data2)); buffer_read(buffer_,data_out,datlen); printf("%s",data_out); printf("缓冲区总长度:%u\n", buffer_->total_len); buffer_destroy(buffer_); return 0; }代码可能有瑕疵,如果你发现了,请在评论区写出来,谢谢;
此外,知识来源于网上学习,感谢零声,还有网络数据包接受过程分析——从网卡到内核协议栈(以Intel e1000 + Linux 4.4为例) | rand0m's blog