
文章目录概要内核协议栈收发网络数据的流程接收数据包发送数据包粘包与分包以太网帧的结构硬中断、软中断和线程网络缓冲区技术方案代码实现概要用户态网络缓冲区是用于用户态从内核态拷贝从TCP对端发送过来字节流信息或者用于用户态向内核态写入向对端发送的字节流信息的缓冲区。用户态缓冲区用于解决posixAPI的send和recv的行为和使用逻辑不一致的情况。send和recv是用户态和内核态直接进行数据交互的接口而实际上TCP通信是由内核决定的。即send只做了把目标buffer指定长度内的信息拷贝到内核socket中recv只做了把内核中socket的指定长度信息拷贝到目标buffer。何时进行传输由内核决定网络/IO。而内核中用于收发的字节流的buffer不是无限长的存在send会失败实际返回50但期望写100字节,recv不到一条完整语义比如resp协议消息这种情况。所以我们需要一个网络缓冲区(C)。内核协议栈收发网络数据的流程接收数据包数据在不同层的名称用户层datatcpsegmentippacket链路层frame协议栈中都由sk_buff来表示。网卡收到数据包后通过DMA一种独立于CPU的硬件设备执行操作将数据写入一个环形缓冲区。网卡会向CPU发起硬件中断争夺CPU执行权同时屏蔽网卡的硬件中断避免频繁让CPU被打断。发起软中断使用软中断避免由于CPU的处理时间过长导致CPU没法及时响应其他硬件中断比如键盘、鼠标、硬盘、时钟等。内核中有专门的线程ksoftirqd负责处理软中断该中断从ringbuffer中逐个读取数据帧到sk_buff用于网络协议栈的data封装拆解按照封装的顺序依次取出。在帧头判断上一层是IPv4还是IPv6IP头判断是TCP还是UDP根据五元组源/目端口 源/目IP 传输层协议类型 找到socket。直到ringbuffer为空软中断处理结束开启网卡硬件中断。通过iret软中断把数据从socket中搬运到用户态。在接收数据包时TCP和UDP区别是会多回一个ACK包确认应答机制。发送数据包发送就是以上过程的逆。以下细节注意UDP没有使用socket的发送缓冲区因为没有确认应答机制不需要维护一个缓冲区确认对方是否收到。TCP会从发送缓冲区取出sk_buff并克隆一个新的sk_buffUDP则不会。TCP支持超时重传。TCP分段因此要粘包处理UDP则不会分段而是IP分片保证MTU1500区别在于用户在调用recv时感知不到UDP分片若是一个丢了那么整个IP片重组就无法完成TCP/UDP层的recv也感知不到IP层边界问题。在网卡发送成功触发硬件中断TCP释放克隆的sk_buff当收到对端传来的ACK应答释放原始的sk_buffUDP直接释放原始sk_buff。协议栈找socket是通过五元组用户态找socket是通过sockfd。粘包与分包①特殊字符 /r/n②长度界定数据包以太网帧的结构MAC地址属于网卡设计意图上是唯一的相当于数字身份证。使用ifconfig查询到的mtu 1500意味着ip头载荷最大长度为1500如果 IP 包大于 1500 字节就必须在网络层进行分片Fragmentation分成多个小于等于 MTU 的小片来传输。以太网帧头部14字节 源MAC 6 目的MAC 6 类型/长度字段 2以太网帧尾部4字节 帧校验序列硬中断、软中断和线程硬中断不能处理耗时任务的原因抢占性当硬中断发生时CPU 会立即暂停当前正在执行的任何任务无论是用户态线程还是内核态代码转而去执行中断服务例程响应性如果 ISR 执行时间过长CPU 就会长时间被占用导致其他硬件中断无法得到及时响应系统会变得非常卡顿甚至无响应。软中断可以处理耗时任务的原因延迟执行软中断不是在中断信号到来的那一刻立即执行的。硬中断处理程序只是快速地“标记”一下有软中断待处理然后就结束了。真正的处理被推迟到了一个更合适的时机。CPU在执行线程陷入内核态时CPU才有机会执行软中断比如执行recv。网络缓冲区网络缓冲区解决以下问题用户态接收缓冲区 TCP UDP 都需要①粘包问题缓存非完整的数据包 TCP②生产者速度 消费者的速度 缓存没来得及处理的数据包 TCP UDP用户态发送缓冲区 TCP 需要 UDP不需要①数据不可能一次全部发送缓存非完整的数据包 TCP②生产者速度 消费者的速度 缓存没有发送出去的数据TCP技术方案①定长buffer 实现简单内存浪费伸缩性差频繁腾挪数据②ring_buffer 解决了频繁腾挪数据的问题③chain_buffer在C中可以借助vector实现MsgBuffer的动态扩容解决了内存浪费伸缩性差的问题。在动态扩容方案中核心的问题是解决何时扩容的问题。分为以下情况①C 段足够写入 不需处理②AC段够写腾挪buff.data()rpos到buff.data()③AC段不够写要进行扩容代码实现#pragmaonce#includebits/types/struct_iovec.h#includestdint.h#includevector#includecstring#includesys/uio.h#includeerrno.hclassMessageBuffer{public:MessageBuffer():rpos_(0),wpos_(0){buffer_.resize(4096);// Initial size}explicitMessageBuffer(std::size_t size):rpos_(0),wpos_(0){buffer_.resize(size);}// 不允许拷贝MessageBuffer(constMessageBuffer)delete;//删除构造函数MessageBufferoperator(constMessageBuffer)delete;// 允许移动MessageBuffer(MessageBufferother)noexcept:buffer_(std::move(other.buffer_)),rpos_(other.rpos_),wpos_(other.wpos_){other.rpos_0;other.wpos_0;}MessageBufferoperator(MessageBufferother)noexcept{if(this!other){buffer_std::move(other.buffer_);rpos_other.rpos_;wpos_other.wpos_;other.rpos_0;other.wpos_0;}return*this;}uint8_t*GetBasePointer(){returnbuffer_.data();}uint8_t*GetReadPointer(){returnbuffer_.data()rpos_;}uint8_t*GetWritePointer(){returnbuffer_.data()wpos_;}voidReadCompleted(std::size_t size){rpos_size;}voidWriteCompleted(std::size_t size){wpos_size;}std::size_tGetActiveSize()const{returnwpos_-rpos_;}std::size_tGetFreeSize()const{returnbuffer_.size()-wpos_;}std::size_tGetBufferSize()const{returnbuffer_.size();}//将数据腾挪到前段voidNormalize(){if(rpos_0){std::memmove(buffer_.data(),buffer_.data()rpos_,GetActiveSize());wpos_-rpos_;rpos_0;}}// 扩容voidEnsureFreeSpace(std::size_t size){if(GetBufferSize()-GetActiveSize()size){Normalize();buffer_.resize(buffer_.size()std::max(size,buffer_.size()/2));}elseif(GetFreeSize()size){Normalize();}}// windows iocp boost.asiovoidWrite(constuint8_t*data,std::size_t size){if(size0){EnsureFreeSpace(size);std::memcpy(GetWritePointer(),data,size);WriteCompleted(size);}}std::pairuint8_t*,std::size_tGetAllData(){return{GetReadPointer(),GetActiveSize()};}//以上是windows iocp// linux reactor readv// 1. 尽可能的不腾挪数据// 2. 避免了每次都从栈上拷贝到堆上而是不是先输出到栈上然后再clone到缓冲区。intRecv(intfd,int*err){charextra[65535];// 65535是UDP的最大值structioveciov[2];iov[0].iov_baseGetWritePointer();iov[0].iov_lenGetFreeSize();iov[1].iov_baseextra;iov[1].iov_lensizeof(extra);std::size_t nreadv(fd,iov,2);if(n0){*errerrno;returnn;}elseif(n0){*errECONNRESET;return0;}elseif(nGetFreeSize()){WriteCompleted(n);returnn;}else{// WRN: GetfreeSize() 在 WriteCompleted() 中会被更新, extra_size 需要提前计算std::size_t extra_sizen-GetFreeSize();WriteCompleted(GetFreeSize());Write(reinterpret_castuint8_t*(extra),extra_size);returnn;}}private:std::vectoruint8_tbuffer_;std::size_t rpos_;//用于读取std::size_t wpos_;//用于追加}https://github.com/0voice