项目八股[Buffer]

发布时间 2023-09-08 21:50:49作者: timeMachine331

我的项目里带了自己封装的缓冲区,为什么webserver项目里会用到这种应用层的缓冲区?

 

分两个角度来讨论,一个是为什么应用层会需要读缓冲区(读取TCP接收缓冲区),另一个是为什么需要写缓冲区(往TCP发送缓冲区写入)

 

为什么需要读缓冲

因为我的应用层业务逻辑是:线程拿到任务之后,尽快开始读取sock接收缓冲区里的数据并且解析,怎么解析取决于当前状态机是位于哪个解析阶段 是请求行还是请求头还是消息体(POST),然后处理并且响应。

 

我能决定TCP接收缓冲区里的请求的情况吗?决定不了

  • 一次性收到 20k 数据
  • 分两次收到,第一次 5k,第二次 15k
  • 分两次收到,第一次 15k,第二次 5k
  • 分两次收到,第一次 10k,第二次 10k
  • 分三次收到,第一次 6k,第二次 8k,第三次 6k
  • 其他任何可能

我解析请求是一个请求一个请求的进行,假设我没有应用层缓冲区,而TCP缓冲区里头碰巧又有多个http请求,那我一点一点自己操作read直接从tcp里解析请求,处理完了逻辑,准备给epoll中这个文件描述符重新注册epollout跟epolloneshot的时候我该返回了,tcp缓冲区里是还有残留的,这就跟epoll在工作在et边缘触发模式下的要求矛盾了-一次把缓冲区读空,读空了下一次才能正常触发。

更何况是如果请求还不完整的情况,那更需要保存下来等到消息完整了才能判断请求逻辑是怎样的。

所以读取请求并解析是通过操作应用层buffer而不是从tcp的缓冲区里开始读操作。

 

为什么需要写缓冲?

响应体里头的东西多一点的话,一次就写不完。但是写不写的完不应该是应用层逻辑考虑的事情,写不完也应该结束逻辑,原地等待很危险(被动等待)。响应中没写完的东西就应该存buffer里,等待下一次可写了再尝试写入。

 

好,现在理解了为什么需要有缓冲,那缓冲为什么要设计成这样子呢?栈上临时固定空间 + 堆上vector?

这个设计是从muduo库里借鉴来的

设计初衷是结合适应性和性能取平衡

栈上6m,堆上1m初始大小。

堆上的这个vector控制的空间是缓冲区的空间是每一个经过listen fd 建立好连接之后connect 的http对象的缓冲区,底层封装的是vector<char> 类型的数据,主要目的是想利用vector能进行扩容的特性。

服务器接收一个tcp建立链接的请求,会初始化一个http client对象,此时就会初始化这个缓冲区的vector部分,初始化大小为1024字节,之后根据需要再增长。

栈上空间采取的是线程延时创建的策略,推迟到需要读取的时候才创建,为的是能尽量减少read或者readv的系统调用的次数,尽量一次就将tcp缓冲区读空,然后一个业务逻辑结束之后就会释放,并不会占用很大的空间。

如果说vector当前大小可写入的空间能满足要求,那么就不会经过这个栈上空间,否则的话会先将tcp缓冲全读进临时空间,再将vector扩容到能装下临时空间内数大小的情况,再将数据拷贝到vector中,因为临时空间是栈上创建的,拷贝的时候就不用再从用户态切换到内核态,减少了切换的开销。

把响应组织到vector的时候就用不到栈上临时空间了,要写入响应行跟响应头的内容比较短,也不需要经过系统调用,vector可以直接适应这个写入的大小。

把响应写入tcp发送缓冲区的时候,vector里的内容是状态行+响应头,而响应体里的内容由于多半都是请求的各自文件比如html css js,以及服务器端的静态资源像视频文件压缩包图片等,这些都利用iovec结构体,将资源使用mmap映射,返回一个指向文件资源地址的指针,然后写入的时候针对iov[0]的vector,iov[1]的静态资源,进行分别写入,如果写不完的话就记录写入下标,结束当前的服务,下次epoll有可写事件时再继续。