需求
如果讓你寫一段程序,解析http協(xié)議的請(qǐng)求報(bào)文,你會(huì)怎么寫?
在實(shí)現(xiàn)這個(gè)需求之前,我們先了解一下http協(xié)議格式。http協(xié)議有很多種規(guī)范,rfc2616、rfc7230等等,這里我們以rfc7230為例,拿一個(gè)具體的例子分析:
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 ]
可以知道,一個(gè)http請(qǐng)求分為三大部分,分別為開始行、頭部以及消息體。
start-line
start-line = request-line / status-line
開始行又可以分為請(qǐng)求行或者狀態(tài)行,對(duì)于一個(gè)請(qǐng)求為請(qǐng)求行,對(duì)于一個(gè)返回則為狀態(tài)行。
request-line
request-line = method SP request-target SP HTTP-version CRLF
請(qǐng)求行的格式為method 單個(gè)空格 請(qǐng)求的目標(biāo) 單個(gè)空格 HTTP版本 回車換行。例如
GET /hello HTTP/1.1
status-line
status-line = HTTP-version SP status-code SP reason-phrase CRLF
狀態(tài)行的格式為HTTP版本 單個(gè)空格 狀態(tài)碼 單個(gè)空格 原因短語。例如
HTTP/1.1 200 OK
后面的規(guī)則類似,大家可以對(duì)照協(xié)議文檔看一下。
設(shè)計(jì)
了解了http協(xié)議的規(guī)范以后,再想怎么設(shè)計(jì)程序,大家可能一陣頭大。對(duì)于請(qǐng)求和響應(yīng)的消息體,要分成兩種邏輯處理。如果只看請(qǐng)求的分支,直接按照規(guī)范解析,那么我們的代碼基本就是
if (validMethods.contains(str)) {
...
if (str.equals(" ")) {
}
}
用上面的寫法的話,最后會(huì)有一堆嵌套。稍微好一點(diǎn)的話,改成用衛(wèi)語句的寫法
if (!validMethods.contains(str)) {
throw new Exception();
}
...
if (!str.equals(" ")) {
throw new Exception();
}
...
這種寫法雖然避免了大堆的嵌套,書寫更叫流暢,但是不夠優(yōu)雅。至少有以下兩點(diǎn)問題
- 對(duì)于需要了解這塊業(yè)務(wù)的人來將,閱讀成本太高;
- 當(dāng)后面的處理依賴當(dāng)前所處的分支時(shí),比較難處理。
狀態(tài)機(jī)
讓我們看一下jetty9是如何處理的。它引入了一個(gè)狀態(tài)機(jī)的概念。流轉(zhuǎn)圖如下
通過狀態(tài)機(jī),jetty將對(duì)協(xié)議格式的解析轉(zhuǎn)換成了對(duì)狀態(tài)的維護(hù)。每個(gè)狀態(tài)下都只需要關(guān)注自己的業(yè)務(wù)邏輯就可以了,極大地提高了維護(hù)性,對(duì)于代碼的可閱讀性來講也提升了很多。
// 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.HEADER.ordinal())
{
if (parseLine(buffer))
return true;
}
// parse headers
if (_state== State.HEADER)
{
if (parseFields(buffer))
return true;
}
// parse content
if (_state.ordinal()>= State.CONTENT.ordinal() && _state.ordinal()<State.TRAILER.ordinal())
{
// Handle HEAD response
if (_responseStatus>0 && _headResponse)
{
setState(State.END);
return handleContentMessage();
}
else
{
if (parseContent(buffer))
return true;
}
}
// parse headers
if (_state==State.TRAILER)
{
if (parseFields(buffer))
return true;
}
細(xì)心的同學(xué)還會(huì)發(fā)現(xiàn),jetty還使用了枚舉的順序來做校驗(yàn)。枚舉類定義如下:
// 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
}
這一點(diǎn)也和協(xié)議規(guī)則的特點(diǎn)有關(guān),協(xié)議的格式從上到下基本是固定的。
改進(jìn)
其實(shí)jetty的這段邏輯,只是引入了state這個(gè)狀態(tài)變量,具體的邏輯還是比較冗長(zhǎng)的。
如果再進(jìn)一步,引入狀態(tài)模式,對(duì)每一種狀態(tài)實(shí)現(xiàn)一個(gè)狀態(tài)類,將相應(yīng)的邏輯封裝在狀態(tài)類下,就更優(yōu)雅了。
適用狀態(tài)機(jī)的場(chǎng)景
讓我們?cè)賹⑺悸窋U(kuò)展一下,除了規(guī)則解析,還有什么比較常用的場(chǎng)景適用使用狀態(tài)機(jī)呢?
后臺(tái)的操作流程其實(shí)也是比較適用的。我們最常接觸的,就是軟件安裝的流程,第一步、第二步、第三步......這種操作用狀態(tài)機(jī)實(shí)現(xiàn)也是比較容易的。通過把變化封裝在特定的狀態(tài)之中,維護(hù)成本也會(huì)變得比較低。