需求
如果让你写一段程序,解析http协议的请求报文,你会怎么写?
在实现这个需求之前,我们先了解一下http协议格式。http协议有很多种规范,rfc2616、rfc7230等等,这里我们以rfc7230为例,拿一个具体的例子分析:
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
All HTTP/1.1 messages consist of a start-line followed by a sequence
of octets in a format similar to the Internet Message Format
[RFC5322]: zero or more header fields (collectively referred to as
the "headers" or the "header section"), an empty line indicating the
end of the header section, and an optional message body.
HTTP-message = start-line
*( header-field CRLF )
CRLF
[ message-body ]
可以知道,一个http请求分为三大部分,分别为开始行、头部以及消息体。
start-line
start-line = request-line / status-line
开始行又可以分为请求行或者状态行,对于一个请求为请求行,对于一个返回则为状态行。
request-line
request-line = method SP request-target SP HTTP-version CRLF
请求行的格式为method 单个空格 请求的目标 单个空格 HTTP版本 回车换行。例如
GET /hello HTTP/1.1
status-line
status-line = HTTP-version SP status-code SP reason-phrase CRLF
状态行的格式为HTTP版本 单个空格 状态码 单个空格 原因短语。例如
HTTP/1.1 200 OK
后面的规则类似,大家可以对照协议文档看一下。
设计
了解了http协议的规范以后,再想怎么设计程序,大家可能一阵头大。对于请求和响应的消息体,要分成两种逻辑处理。如果只看请求的分支,直接按照规范解析,那么我们的代码基本就是
if (validMethods.contains(str)) {
...
if (str.equals(" ")) {
}
}
用上面的写法的话,最后会有一堆嵌套。稍微好一点的话,改成用卫语句的写法
if (!validMethods.contains(str)) {
throw new Exception();
}
...
if (!str.equals(" ")) {
throw new Exception();
}
...
这种写法虽然避免了大堆的嵌套,书写更叫流畅,但是不够优雅。至少有以下两点问题
- 对于需要了解这块业务的人来将,阅读成本太高;
- 当后面的处理依赖当前所处的分支时,比较难处理。
状态机
让我们看一下jetty9是如何处理的。它引入了一个状态机的概念。流转图如下
通过状态机,jetty将对协议格式的解析转换成了对状态的维护。每个状态下都只需要关注自己的业务逻辑就可以了,极大地提高了维护性,对于代码的可阅读性来讲也提升了很多。
// Start a request/response
if (_state==State.START)
{
_version=null;
_method=null;
_methodString=null;
_endOfContent=EndOfContent.UNKNOWN_CONTENT;
_header=null;
if (quickStart(buffer))
return true;
}
// Request/response line
if (_state.ordinal()>= State.START.ordinal() && _state.ordinal()= State.CONTENT.ordinal() && _state.ordinal()0 && _headResponse)
{
setState(State.END);
return handleContentMessage();
}
else
{
if (parseContent(buffer))
return true;
}
}
// parse headers
if (_state==State.TRAILER)
{
if (parseFields(buffer))
return true;
}
细心的同学还会发现,jetty还使用了枚举的顺序来做校验。枚举类定义如下:
// States
public enum State
{
START,
METHOD,
RESPONSE_VERSION,
SPACE1,
STATUS,
URI,
SPACE2,
REQUEST_VERSION,
REASON,
PROXY,
HEADER,
CONTENT,
EOF_CONTENT,
CHUNKED_CONTENT,
CHUNK_SIZE,
CHUNK_PARAMS,
CHUNK,
TRAILER,
END,
CLOSE, // The associated stream/endpoint should be closed
CLOSED // The associated stream/endpoint is at EOF
}
这一点也和协议规则的特点有关,协议的格式从上到下基本是固定的。
改进
其实jetty的这段逻辑,只是引入了state这个状态变量,具体的逻辑还是比较冗长的。
如果再进一步,引入状态模式,对每一种状态实现一个状态类,将相应的逻辑封装在状态类下,就更优雅了。
适用状态机的场景
让我们再将思路扩展一下,除了规则解析,还有什么比较常用的场景适用使用状态机呢?
后台的操作流程其实也是比较适用的。我们最常接触的,就是软件安装的流程,第一步、第二步、第三步......这种操作用状态机实现也是比较容易的。通过把变化封装在特定的状态之中,维护成本也会变得比较低。
参考资料
Jetty9源码剖析 - Connection组件 - HttpParser
rfc7230