Jetty源码阅读——用状态隔离变化

需求

如果让你写一段程序,解析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();
}
...

这种写法虽然避免了大堆的嵌套,书写更叫流畅,但是不够优雅。至少有以下两点问题

  1. 对于需要了解这块业务的人来将,阅读成本太高;
  2. 当后面的处理依赖当前所处的分支时,比较难处理。

状态机

让我们看一下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.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;
            }

细心的同学还会发现,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

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容