引言
套接字的读写在一般编写业务代码时可能并不会注意这个问题,但是其是网络编程中非常值得注意的一个问题.要解决这个问题的方法就是使用buffer,该怎么做呢,接下来我们谈谈这个问题.
以下讨论建立在ET非阻塞IO的基础上.
input buffer:
因为分包的原因,在数据包数据量不确定的情况下我们无法确定一次完整的收到数据包,所以导致我们显然无法直接把这不完整的数据直接交给程序的业务逻辑处理部分,所以这时就需要我们在用户态存储数据,通过循环直接读取客户端"一个数据包"发送来的是数据,客户端所发送的一个数据包会在一次IO multiplexing事件通知中到达(TCP的可靠性),但不一定会在一个数据包中(IP分片),这意味着我们如果不一次读取完毕的话就会导致多次触发事件,这是不必要的性能损失,同时还会使得这个分件描述符的存活时间增长,使得出现busy_loop的可能性提升(busy_loop可通过拒绝连接来避免,muduo中的实现很不错(即通过一个闲置的文件描述符,来达到正常close文件)).当我们接收完毕后把数据发送给业务逻辑处理,这里我们需要一个buffer,原因如上所述.
ouput buffer
为什么还需要一个buffer呢,我们再想象一个场景,当服务器接收到了数据时,我们已经进行了处理,这个时候剩下的事情当然是把客户端请求的数据发送回去了,但是一个棘手的问题出现了,就是如果我们有2000字节的数据,但仅仅发送出去了1800,也就是说我们调用了一个send也好,write也好,返回值并不是我们发送出去的数据,这种情况什么时候会出现呢,就是客户端发送的TCP报文中的窗口大小小于我们要写入的数据时,也就是流量控制的手法,这不是我们本文重点阐述的事情,有需要请点击这里.然后发生了什么事情呢,写入失败,显然我们不应该睁一只眼闭一只眼当它没发生,我们的做法就是使用为每一个用户维护一个ouput buffer,然后记录发送位置,当出现write不全的情况的时候,正确的做法是维护这个连接,然后注册可写事件,当客户端发送的窗口大小大于我们的目标写入大小时触发可写事件,然后直接发送所有的数据,则可保证再出现这种情况的不丢失数据,
关于buffer的一系列问题
- 为每个连接分配固定大小buffer是否合适?
这个问题很有意思,即如果我们为每个连接分配两个25KB的的缓冲区,当我们有一万个并发连接的时候,仅缓冲区就消耗了近似488G的内存,一般我们的栈上空间默认为10MB,显然buffer使用宝贵的栈上空间是不太现实的,所以只能使用heap空间,但是我们显然无法预先知道一次接收多少数据,所以只能预先分配一些内存,当然我们可以选择vector作为buffer,免去了这些苦恼,还有一种高效的做法就是预先给每个线程分配一个thread_local的buffer,这个可以分配的稍微大一点,毕竟救急用的,然后使用readv在一次用户态到内核态的切换下拿到所有的数据,利用readv的特性,当我们的member的成员buffer使用完以后才会写到thread_local中去,如果出现了member的成员buffer不够用时,当input_buffer中读取完毕时,进行拷贝即可,但是理论上来说还是治标不治本,但是对于某些服务器例如http服务器来说遇到两个缓冲区放不下的请求,拒绝连接也许是更好的选择,如果遇到被请求大量数据的服务器,完全可以考虑使用vector当做buffer,但是也有致命问题,即erase的效率. - 当使用vector作为buffer的时候index用指针还是数字?
考虑迭代器失效,index一律使用数字,使用std::distance与std::advance辅助数字来达到与记录迭代器一样的效果,这两个函数均提供指针的全特化版本. - 当写asynclog的时候的double buffer处理
同样是很有意思的一个问题,即要完成一个高效的日志,我们显然不能每一句话就进行写入磁盘的操作,毕竟磁盘操作是如此的昂贵.有兴趣可以看看fwrite与write的性能比较,大概是150倍左右,我们需要把每一次的写入存储起来,通过用户直接可用(不可见)的buffer与两个内部的buffer进行切换,达到一个高效的写入(double buufer).