转载

Jetty9源码剖析 - Connection组件 - HttpGenerator

HttpGenerator顾名思义就是HTTP协议报文生成器,用于HTTP响应报文的生成,和HttpParser相对应。相比于HttpParser复杂的状态机,HttpGenerator状态机设计更精简一点

二、流程图

Jetty9源码剖析 - Connection组件 - HttpGenerator

Servlet(或Filter)通过向输出流中写数据(当然也可以不写),完成业务处理后,通过HttpOutput.close操作调回HttpChannel,再调到HttpConnection,HttpConnection利用HttpGenerator利用三步操作:generateResponseLine、generateHeaders、content或prepareChunk来生成响应报文,而其中content是不需要处理的,chunk分块传输是需要每次计算块的大小,并按照块格式传输,最后HttpConnection的SendCallback会调用EndPoint.write,往SocketChannel写入数据

三、状态机

Jetty9源码剖析 - Connection组件 - HttpGenerator

状态机相比于HttpParser还算是比较简单,就5种状态,而且轮转也比较直接

相比于HttpParser,HttpGenerator并不会将每个请求行、请求头、请求体每个位置都标上一个状态,而是简化成数据提交状态,这样状态机就会简单很多,之所以这里可以简化,是因为HttpGenerator并不会像HttpParser一样将解析后的数据放到其他组件里面,HttpGenerator只是生成HTTP报文

Jetty9源码剖析 - Connection组件 - HttpGenerator

  • START -> COMMITTED -> COMPLETING -> END,这种状态迁移是当数据一次性写不完,从START切位COMMITTED,之后可能多次停留在COMMITTED状态,表示要不断生成新的数据,当数据生成完了,就会进入COMPLETING状态,这个状态会处理一些善后操作,之后切为END状态,表示本次生成完全结束
  • START -> COMPLETING -> END,这种状态迁移一般是一次性就能将数据完,也就不需要COMMITTED这个中间状态
  • START -> COMPLETING_1XX,这种状态迁移是出现了1XX的状态码,这个状态其实也是一种终结态,不会响应body体

四、源码剖析

1. 创建

Jetty9源码剖析 - Connection组件 - HttpGenerator

构造函数比较简单,没啥说的,就是把Jetty服务器目前的版本号记下来,后面再response报文里面带上

2. 生成响应

2.1 START状态

Jetty9源码剖析 - Connection组件 - HttpGenerator

Jetty9源码剖析 - Connection组件 - HttpGenerator

START相对还是稍微冗长一点,这里会根据HTTP协议版本来判断是否是持久连接,如果没有Buffer就会要求一块新的(通过Action来让外层的SendCallback处理),值得注意的是这里申请的头部Buffer是HeapByteBuffer,也就是堆内缓冲区,区别于响应体的DirectByteBuffer,使用了堆外缓冲区(头部通常都是小数据量),也是不难理解的。申请完Buffer就可以切换到读模式,生成响应行(状态行),调用的是generateResponseLine,之后调用generateHeaders生成响应头,再通过应用层传入的content(响应体)是否有数据,如果有数据,就需要判断头部是否设置了要按照分块来传输,如果分块传输就需要准备分块传输的元数据,最后根据当前是否是最后一次生成(请求体的数据可能会很大,分多次),如果是就会直接将状态切成COMPLETING,否则进入COMMITTED状态

2.2.1 状态行(响应行)生成

Jetty9源码剖析 - Connection组件 - HttpGenerator

可以看到状态行(响应行)实现还是比较简单,唯一需要注意的是这里用了status状态码来从__preprepared里面去索引响应行数据,这个算是一个比较有特点的优化,如果状态码在这里查不到,就按照标准的协议格式一个个字节放进去

2.2.2 响应头生成

由于篇幅有限,这里截取比较核心的代码

Jetty9源码剖析 - Connection组件 - HttpGenerator

生成响应头的时候会判断一些需要特殊处理的头,例如CONTENT_LENGTH,因为生成响应头就需要知道当前响应体大小,或者对于TRANSFER_ENCODING就需要标识当前是分块传输,后面在处理响应体时就能判断,如果是普通的不需要处理的响应头,直接调putTo,这个方法会将头按照标准格式放到缓冲中

2.2.3 分块传输生成

Jetty9源码剖析 - Connection组件 - HttpGenerator

分块传输其实并不复杂,可以看到,直接往这个chunk缓冲按照格式放数据,先把块大小放进去,然后放换行

2.2 COMMITTED状态

前面其实说到COMMITTED就是内容较大的情况使用的状态,可能是定长响应体或分块传输响应体

Jetty9源码剖析 - Connection组件 - HttpGenerator

这里会看是否上层给的content仍然有,如果有就要看是不是分块传输,如果分块一样的准备元数据,如果是最后一块数据,直接切成COMPLETING状态,表示可以结束了,否则继续刷数据,而且状态仍然停留在COMMITTED,也就是下次SendCallback会继续掉HttpGenerator生成时,仍然继续这个分支,继续传数据

2.3 COMPLETING状态

数据传输完成后,就需要进行收尾工作,也就是COMPLETING状态要做的,如下图

Jetty9源码剖析 - Connection组件 - HttpGenerator

这个状态会判断是否是分块传输,如果是分块传输,就需要补一个结束的分块标记,即0/r/n(不清楚的读者可以去看下HttpParser章节,有专门将分块传输的格式),最后将状态切为END,至此HttpGenerator状态轮转就完成了

2.4 END状态

END是HttpGenerator的终结状态,这里会返回上层一个完成的标记,表示不需要调用HttpGenerator生成数据了

Jetty9源码剖析 - Connection组件 - HttpGenerator

五、总结

HttpGenerator作为响应报文生成的工具类,设计上不算很复杂,其核心就是按照HTTP规范生成这些字符串,保证正确性的同时,HttpGenerator也做了很多性能优化,例如将状态行按照状态码存放一个512大小的数组,这样就能通过数组的索引查找到这个状态码的响应行,用空间换时间的思路在Jetty的优化中还是比较常见的。这篇文章就写到这里,相信读者对Jetty的HTTP响应解析有了一些认识。后续的文章我会做一些专题,例如Jetty中的性能优化,以及HTTP/2等专题文章,欢迎大家持续关注~

原文  https://www.ph0ly.com/2018/10/07/jetty/connection/http-generator/
正文到此结束
Loading...