Jetty9源码剖析 - Connection组件 - HttpParser

一、概念

HttpParser是专门用于解析HTTP协议的类,它能够完成HTTP/0.9,HTTP/1.0,HTTP/1.1的解析,支持rfc2616、rfc7230两种规范下的解析。HttpParser解析完成后会调用HttpHandler来将数据放到相关的请求行、请求头或请求体中(当然HttpParser也支持响应的处理,不过是jetty-client包下面的,服务端还是以HttpChannelOverHttp为准)

二、流程图

由于HttpParser就是单纯的一个工具类,不继承任何类,因此这里我们就看下它的解析流程图

流程图

HttpParser的解析主要包括上面图中的几部分,quickStart主要是快速检测当前的HTTP报文请求行部分是否有效,如果是脏数据直接快速失败,parseLine则解析请求行(当然也能处理响应头,不过通常都是解析请求),parseHeaders解析请求头,parseContent解析请求体

三、解析状态机

状态机

HttpParser总共有25种状态,状态迁移基本如上图所示(可能会有细节的遗漏),先后顺序从上到下,最开始HttpParser是START状态,对于请求报文和响应报文解析,分别走左右两个分支,HEADER相关的部分状态请求和响应共享,请求体或响应体也是共享解析算法

状态机-1

1. 请求报文解析状态迁移

请求行:START -> METHOD -> SPACE1 -> URI -> SPACE2 -> REQUEST_VERSION

2. 响应报文解析状态迁移

响应行:START -> RESPONSE_VERSION -> SPACE1 -> STATUS -> SPACE2 -> REASON

3. Header和Body的解析状态迁移

请求头

  • HEADER -> HEADER_IN_NAME -> HEADER_VALUE -> HEADER_IN_VALUE ,这种状态迁移方式一般是用户自定义Header
  • HEADER -> HEADER_VALUE -> HEADER_IN_VALUE,这种状态迁移一般是HttpParser使用Trie树快速匹配到了Header,但是Value还需要进一步获取
  • HEADER -> HEADER_IN_VALUE,这种状态迁移一般是HttpParseer使用Trie树完全匹配,直接进入HEADER_IN_VALUE切换下一个状态

请求体

  • CONTENT -> END,这种是普通的带Content-Length头的报文,HttpParser一直运行CONTENT状态,直到最后ContentLength达到了指定的数量,则进入END状态

另一种是chunked分块传输的数据,不清楚总体大小,只知道每一块的大小,这种常见于大数据量传输

  • CHUNKED_CONTENT -> CHUNK_SIZE -> CHUNK -> CHUNK_END -> END,这种状态迁移是常规的分块大小比较小的情况走的状态路线,先拿到分块大小,然后读分块的数据,并继续CHUNKED_CONTENT,直到读到CHUNK结束标记进入END状态
  • CHUNKED_CONTENT -> CHUNK_SIZE -> CHUNK -> CHUNK_TRAILER -> CHUNK_END -> END,这个是rfc规定的一个在CHUNK数据里面加类似于Header的解析方式,即CHUNK_TRAILER这个状态,其他基本一致

四、源码剖析

1. 快速解析

首先看下请求行在rfc7230中的定义,如下图

rfc7230-req-line

当发起请求时,我们使用Request Line的格式

rfc7230-status-line

当响应请求时,我们使用Status Line的格式

相信阅读这篇博客读者都能理解,标准的HTTP/1.x协议格式,SP表示Single Space,单空格,HttpParser可以解析请求报文和响应报文

quickStart

如果设置了RequestHandler就认为是请求解析,如果设置了ResponseHandler就认为是响应解析
如果是解析请求报文,就会快速定位Http方法,如果定位成功,先会记录方法,然后将状态切换到下一个状态SPACE1,表示下一次需要解析空格
如果是解析响应报文,先快速定位版本,然后同样记录下来,切到下一状态SPACE1,要求下一次解析空格
如果前面解析都没成功,如果当前的第一个字符不是空格,就会认为是合法的方法,会直接切换状态到METHOD或RESPONSE_VERESION,否则就会认为是错误的字符,直接报错

2. 请求行解析

parseLine

这里截图就不截全了,很长,简单说下思路就是我们上面说的状态机切换,这个parseLine其实是解析开始行(请求行或响应行),我们可以看到是一个while循环,判断这个state状态的枚举的索引,其实就是前面我们状态机的状态顺序,通过大小就能确定是否在这个区间
思路比较简洁,根据状态命中分支,执行每个状态下的解析逻辑。例如METHOD这个状态,就要把缓冲区首行的METHOD拿出来,虽然这里用了Trie树加速查找,不过逻辑就是看下是否有这个已知的METHOD,后面可以判断是否合法,然后把状态切到SPACE1,解析下一个空格,然后会做一些校验,整体HttpParser思路就是这样,使用state来控制不同分支的代码执行。开始行解析完成后,会将首行参数放到请求处理器或相应处理器

startRequest

我们可以看到REQUEST_VERSION状态下会调用_requestHandler.startRequest,其实就是调到了HttpChannelOverHttp,将数据存到其中
对于REASON状态,会放到_responseHandler.startResponse,同理仍然是把数据放进去

3. 请求头解析

parseHeaders-1

parseHeaders-2

请求头解析和上面一样,通过状态轮转,通常是:HEADER -> HEADER_IN_NAME -> HEADER_VALUE -> HEADER_IN_VALUE(当然不一定是这个,可以看下上面的状态机讲解)
HEADER_IN_NAME会拿头的名称,HEADER_IN_VALUE会拿头的值,这些值会暂存到内存,当完成一个完整的K/V解析后,解析下一个HEADER的时候才会触发上一个HEADER的存储,也就是default分支会利用parsedHeader,将这个数据放到HttpChannelOverHttp的MetaData中,这里由于篇幅有限就不展开讲了
当完成头部处理后,如上图的LINE_FEED分支,说明当前头部已经解析完成,这个时候会仍然是把上一次解析的头放到HttpChannelOverHttp中去,并切换状态,如果前面声明的Content-Length为0就直接认为当前解析以及完成了,会将状态切位EOF_CONTENT,否则根据头部标识来处理,如果头部声明了Content-Length大小,就设置为CONTENT,如果声明了Transfer-Encoding: chunked,说明要分块传输,就会将状态切为CHUNKED_CONTENT

4. 请求体解析

4.1 定长解析

contentLength

定长解析很简单,直接根据头部声明的Content-Length来读数据,如果数据Content-Length比实际给的数据小,那就会造成数据截断,如果Content-Length比实际给的数据大,那就会HttpParser等待下一次的数据读取并解析,如果数据达到Content-Length大小,就进入END状态,表示当前Content以及完成处理,并调用handleContentMessage,这个方法会让HttpInput进入eof状态,也就是没数据了

4.2 分块解析

先来看下分块传输的在rfc7230中的定义

rfc7230-chunked

格式就是在Header中声明Transfer-Encoding: chunked,然后在body体里面按照上面的格式传数据,为了让大家更好理解分块传输,我在网上找了几张图,以便理解

chunked

结合rfc7230定义可以看到,分块数据的每一块都需要标识当前这块的长度,以\r\n换行,然后紧接着是chunk块数据,接着是下一块数据,最后以0\r\n结束这个块传输

例如下面这种格式:

1
2
3
4
5
6
7
8
9
10
11
12
GET /hello HTTP/1.1
Host: localhost
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n

这个例子数据分为3块,第一块7个字节,第二块9字节,第三块7字节,最后以0表示分块传输结束

下面来看下HttpParser的代码怎么处理的

httpparser-chunked

这里就截取一小部分CHUNK处理的代码,从上面可以看到当状态迁移至CHUNK时,会调用_handler.content(_contentChunk)把读到的整块CHUNK数据放到HttpChannelOverHttp,这最终会被放到HttpInput的inputQ里面去,这样业务层就能消费到这些数据

五、总结

HttpParser总体来说还是比较复杂的一个组件,需要对HTTP协议的规范有比较深入的理解,这样才能写出一个正确的Http服务器。对这块感兴趣的读者可以下来仔细研究下HTTP协议本身,特别是rfc7230、rfc2616这两个协议,完整讲述了实现HTTP协议的需要东西,例如长连接、管道化、分块传输等等。这篇文章就写到这里,接下来的文章我会带领大家继续探索Jetty里面HttpInput、HttpOutput、HttpGenerator等组件,另外喜欢这篇文章的,觉得对自己有很大成长的,可以来一波打赏,感谢~

六、参考资料

rfc7230
rfc2616
http-trailers-in-jetty
Transfer-Encoding