需求
如果讓你寫一段程序,解析http協(xié)議的請求報文倍奢,你會怎么寫彪见?
在實現(xiàn)這個需求之前,我們先了解一下http協(xié)議格式娱挨。http協(xié)議有很多種規(guī)范,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
開始行又可以分為請求行或者狀態(tài)行贴届,對于一個請求為請求行,對于一個返回則為狀態(tài)行蜡吧。
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
狀態(tài)行的格式為HTTP版本 單個空格 狀態(tài)碼 單個空格 原因短語。例如
HTTP/1.1 200 OK
后面的規(guī)則類似昔善,大家可以對照協(xié)議文檔看一下元潘。
設計
了解了http協(xié)議的規(guī)范以后,再想怎么設計程序君仆,大家可能一陣頭大翩概。對于請求和響應的消息體,要分成兩種邏輯處理返咱。如果只看請求的分支钥庇,直接按照規(guī)范解析,那么我們的代碼基本就是
if (validMethods.contains(str)) {
...
if (str.equals(" ")) {
}
}
用上面的寫法的話咖摹,最后會有一堆嵌套评姨。稍微好一點的話,改成用衛(wèi)語句的寫法
if (!validMethods.contains(str)) {
throw new Exception();
}
...
if (!str.equals(" ")) {
throw new Exception();
}
...
這種寫法雖然避免了大堆的嵌套楞艾,書寫更叫流暢参咙,但是不夠優(yōu)雅。至少有以下兩點問題
- 對于需要了解這塊業(yè)務的人來將硫眯,閱讀成本太高蕴侧;
- 當后面的處理依賴當前所處的分支時,比較難處理两入。
狀態(tài)機
讓我們看一下jetty9是如何處理的净宵。它引入了一個狀態(tài)機的概念。流轉圖如下
通過狀態(tài)機,jetty將對協(xié)議格式的解析轉換成了對狀態(tài)的維護择葡。每個狀態(tài)下都只需要關注自己的業(yè)務邏輯就可以了紧武,極大地提高了維護性,對于代碼的可閱讀性來講也提升了很多敏储。
// 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;
}
細心的同學還會發(fā)現(xiàn)阻星,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
}
這一點也和協(xié)議規(guī)則的特點有關已添,協(xié)議的格式從上到下基本是固定的妥箕。
改進
其實jetty的這段邏輯,只是引入了state這個狀態(tài)變量更舞,具體的邏輯還是比較冗長的畦幢。
如果再進一步,引入狀態(tài)模式缆蝉,對每一種狀態(tài)實現(xiàn)一個狀態(tài)類宇葱,將相應的邏輯封裝在狀態(tài)類下,就更優(yōu)雅了刊头。
適用狀態(tài)機的場景
讓我們再將思路擴展一下黍瞧,除了規(guī)則解析,還有什么比較常用的場景適用使用狀態(tài)機呢芽偏?
后臺的操作流程其實也是比較適用的雷逆。我們最常接觸的,就是軟件安裝的流程污尉,第一步膀哲、第二步、第三步......這種操作用狀態(tài)機實現(xiàn)也是比較容易的被碗。通過把變化封裝在特定的狀態(tài)之中某宪,維護成本也會變得比較低。