HTTP协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,我们今天就来深度剖析这个古老的协议。
HTTP 1 协议 HTTP
协议的构建很简单,协议分为了 Request
和 Response
部分。
HTTP-message = Request | Response ; HTTP/1.1 messages
无论是 Request
或者是 Response
所使用的的格式也是一致的,遵守 rfc822
格式,如下所示
1 2 3 4 5 generic-message = start-line *(message-header CRLF) CRLF [ message-body ] start-line = Request-Line | Status-Line
值得注意的是,HTTP
是一个文本协议,是可以非常方便的打印出来的。
1 2 3 4 5 6 7 GET /ocsp-devid01/ME4wTKADAgEAMEUwQzBBMAkGBSsOAwIaBQAEFDOB0e%2FbaLCFIU0u76%2BMSmlkPCpsBBRXF%2B2iz9x8mKEQ4Py%2Bhy0s8uMXVAIIF5cOVThOvII%3D HTTP/1.1 Host: ocsp.apple.com Accept: */* Accept-Language: en-us Connection: keep-alive Accept-Encoding: gzip, deflate User-Agent: com.apple.trustd/2.0
对于 HTTP
协议的解析就等同于解析文本,没有什么复杂可言。
1 2 3 4 5 6 7 8 9 10 11 12 let http_hex_data = hex::decode ("474554202f6f6373702d646576696430312f4d453477544b4144416745414d455577517a42424d416b474253734f417749614251414546444f42306525324662614c43464955307537362532424d536d6c6b50437073424252584625324232697a3978386d4b45513450792532426879307338754d58564149494635634f5654684f76494925334420485454502f312e310d0a486f73743a206f6373702e6170706c652e636f6d0d0a4163636570743a202a2f2a0d0a4163636570742d4c616e67756167653a20656e2d75730d0a436f6e6e656374696f6e3a206b6565702d616c6976650d0a4163636570742d456e636f64696e673a20677a69702c206465666c6174650d0a557365722d4167656e743a20636f6d2e6170706c652e7472757374642f322e300d0a0d0a" );let http_data = String ::from_utf8 (http_hex_data.unwrap ()).unwrap ();let lines : Vec <&str > = http_data.lines () .filter (|it| it.len () > 0 ) .collect (); let request_line = lines[0 ];let request_line_values : Vec <&str > = request_line.split_whitespace ().collect ();let method = request_line_values[0 ];
通信 协议部分不是很复杂,但是 HTTP
通信却有一些很大的弊端。我们先看下常见的 HTTP
通讯
一来一回就构成了,我们的 HTTP
通讯的全部。在 HTTP 1.0
的时候,当我们完成一次通讯的时候,我就会 Close
底层的 TCP
协议。但是我们知道开通一个 TCP
连接是至少需要 3RTT
+ 1RTT
的 HTTP 发送,因此成本是很高的。因此在 HTTP 1.1
里面提出一些改进计划。
特性 持久连接 即是不声明 Connection: keep-alive
,HTTP 1.1
也不会关闭底层的 TCP
连接,客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送 Connection: close
,明确要求服务器关闭TCP连接。
管道机制 1.1 版还引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。
举例来说,客户端需要请求两个资源。以前的做法是,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。
为了达成 pipeling
的机能,我们需要分区响应的数据,所以在 Reponse line
中的 Context-Length
是必须设置的,并且只有幂等方法(即GET、HEAD、PUT和DELETE)才可以使用管道功能。不过值得注意的,大部分的浏览器并没有启用这个功能。
分块传输编码 使用Content-Length
字段的前提条件是,服务器发送回应之前,必须知道回应的数据长度。
对于一些很耗时的动态操作来说,这意味着,服务器要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用”流模式”(stream)取代”缓存模式”(buffer)。
因此,1.1版规定可以不使用Content-Length
字段,而使用”分块传输编码”(chunked transfer encoding)。只要请求或回应的头信息有Transfer-Encoding
字段,就表明回应将由数量未定的数据块组成。
虽然 1.1
改进了很多,但是我们还是发现了,所有的请求都是要按顺序处理的,就算是在管道化之后,当第一个请求未完成的时候我们也不能返回,导致了 Head-of-line blocking
头部阻塞。如果很不幸的第一个请求特别慢,导致我们只能等待。
HTTP 2 因为现代网络的发展,大量的数据需要被处理,显然 HTTP 1.1
已经有点跟不上时代了,HTTP 2 他来了。
协议 HTTP 2
基于 Frame
进行通讯。
1 2 3 4 5 6 7 8 9 10 11 +-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+=============================================================+ | Frame Payload (0...) ... +---------------------------------------------------------------+ Figure 1: Frame Layout
Length
: 载荷的长度, 无符号24位整型. 对于发送值大于2^14 (长度大于16384字节)的载荷, 只有在接收方设置SETTINGS_MAX_FRAME_SIZE为更大的值时才被允许
Type
: 8位的值表示帧类型, 决定了帧的格式和语义. 协议实现上必须忽略任何未知类型的帧.
Flags
: 为Type保留的bool标识, 大小是8位. 对确定的帧类型赋予特定的语义, 否则发送时必须忽略(设置为0x0).
R
: 1位的保留字段, 尚未定义语义. 发送和接收必须忽略(0x0).
Stream Identifier
: 31位无符号整型的流标示符. 其中0x0作为保留值, 表示与连接相关的frames作为一个整体而不是一个单独的流.
Frames HTTP2
有不同的 Frame
类型。比较常见的就是 Header
和 Data
header 1 2 3 4 5 6 7 8 9 10 11 +---------------+ |Pad Length? (8)| +-+-------------+-----------------------------------------------+ |E| Stream Dependency? (31) | +-+-------------+-----------------------------------------------+ | Weight? (8) | +-+-------------+-----------------------------------------------+ | Header Block Fragment (*) ... +---------------------------------------------------------------+ | Padding (*) ... +---------------------------------------------------------------+
因为 Header
常常被使用,Http2
针对 Header Frame
还有一个单独的 HPACK: Header Compression for HTTP/2 ,就是针对 Header
数据进行数据压缩,因为 Header
的发送是一个阻塞的事件,尽快的快递数据才是我们需要的。
data 1 2 3 4 5 6 7 +---------------+ |Pad Length? (8)| +---------------+-----------------------------------------------+ | Data (*) ... +---------------------------------------------------------------+ | Padding (*) ... +---------------------------------------------------------------+
除了这些之外还要 PRIORITY
RST_STREAM
SETTINGS
PUSH_PROMISE
PING
GOAWAY
WINDOW_UPDATE
CONTINUATION
,对于实现框架的同学们来说看起来要吃不少的苦了。
通信 多路复用 因为 HTTP 2
是多路复用的,有点类似于 TCP
的协议,因此对于一次完整的请求我们定义为 Steam 流
,那显然涉及到 State
的切换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 +--------+ send PP | | recv PP ,--------| idle |--------. / | | \ v +--------+ v +----------+ | +----------+ | | | send H / | | ,------| reserved | | recv H | reserved |------. | | (local) | | | (remote) | | | +----------+ v +----------+ | | | +--------+ | | | | recv ES | | send ES | | | send H | ,-------| open |-------. | recv H | | | / | | \ | | | v v +--------+ v v | | +----------+ | +----------+ | | | half | | | half | | | | closed | | send R / | closed | | | | (remote) | | recv R | (local) | | | +----------+ | +----------+ | | | | | | | | send ES / | recv ES / | | | | send R / v send R / | | | | recv R +--------+ recv R | | | send R / `----------->| |<-----------' send R / | | recv R | closed | recv R | `----------------------->| |<----------------------' +--------+ send: endpoint sends this frame recv: endpoint receives this frame H: HEADERS frame (with implied CONTINUATIONs) PP: PUSH_PROMISE frame (with implied CONTINUATIONs) ES: END_STREAM flag R: RST_STREAM frame Figure 2: Stream States
Steam
的状态变化依靠的是 Frame
数据不同进行驱动的。比如从 idle
状态切换到 open
状态,就需要通过发送/接受 Headers Frame
,除了 Stream
Frame
还一个 Message
的概念,这个对应到 1.1
的 Request/Reponse
。
流控 在看 Header Frame
定义的时候,我们发现有一个字段叫 Stream Dependency
,看上去很像是什么依赖关系,其实不是,这个字段是用来标记 优先级
而不是 从属关系
。因为我们在 HTTP2
中实际上仍是通过一个 TCP
连接进行数据传输。
面临 拥堵
问题,因此增加了一个 拥塞控制
在 HTTP2
协议之中,因为通过 Stream Dependency
我们可以构建出一个层级树
1 2 3 A A / \ ==> /|\ B C B D C
并且在 H
帧中,还包含一个 Weight
字段,可用来标记权重,所有的从属流会分配到一个介于[1, 256]之间的整数, 表示权重,依赖于相同上级的流应该依据其权重比例分配资源. 因此, 如果权重为4的流B和权重为12的流C都依赖于流A, 并且A上不会有任何动作, 那么B会分得1/4的资源, C分得3/4的资源。
服务器推送
HTTP/2 脱离了严格的请求-响应语义,支持一对多和服务器启动的推送工作流,在浏览器内外开辟了一个新的交互可能性世界。这是一个有利的特性,对于我们如何看待该协议,以及在何处和如何使用它,都将产生重要的长期影响。[看起来是抢了 Websocket 的生意]
交换数据 客户端发起一次 HTTP
请求(H
帧)就会生成一个 Stream
,服务端响应使用相同的 Stream
进行响应。
小声比比
会不会创建相同的Stream ID呢?协议也考虑到了,客户端创建的ID是奇数,服务器端创建的ID是偶数,就不会重复了。
响应的时候可以选择返回 H
或者直接返回 D
都是可以的 文档 ,最后 Frame
需要包含 END_STREAM
标记,这样我们就可以关闭这个 Stream
了。
实现 从协议上,我们可以发现其实整个 HTTP 2
的协议实现,抛开 流控
部分,其他的还是比较好实现的。我们来看看 Netty
的实现重点。
第一步显然是读取到 H
帧之后的操作,从入口处开始阅读:
readFrame source code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void readFrame (ChannelHandlerContext ctx, ByteBuf input, Http2FrameListener listener) throws Http2Exception { try { do { if (readingHeaders) { processHeaderState(input); if (readingHeaders) { return ; } } processPayloadState(ctx, input, listener); if (!readingHeaders) { return ; } } while (input.isReadable()); } }
我们下来要看如果我们的 FRAME TYPE
是 HEADER
的应该怎么处理,我们知道应该创建一个 STREAM
才是,我们继续来看代码
onDataRead source code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void onHeadersRead (ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception { Http2Stream stream = connection.stream(streamId); boolean allowHalfClosedRemote = false ; if (stream == null && !connection.streamMayHaveExisted(streamId)) { stream = connection.remote().createStream(streamId, endOfStream); allowHalfClosedRemote = stream.state() == HALF_CLOSED_REMOTE; } if (shouldIgnoreHeadersOrDataFrame(ctx, streamId, stream, "HEADERS" )) { return ; } }
而这个 connection.stream(streamId);
实际上创建的对象是由 DefaultHttp2Connection
创建的 DefaultStream
,创建完成之后需要执行激活。
1 2 3 4 5 6 public DefaultStream createStream (int streamId, boolean halfClosed) throws Http2Exception { State state = activeState(streamId, IDLE, isLocal(), halfClosed); DefaultStream stream = new DefaultStream (streamId, state); stream.activate(); return stream; }
激活的逻辑比较简单,直接将 Stream
放置于 Connection
内置的一个 LinkedHashSet
的 Streams
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private final class ActiveStreams { private final List<Listener> listeners; private final Queue<Event> pendingEvents = new ArrayDeque <Event>(4 ); private final Set<Http2Stream> streams = new LinkedHashSet <Http2Stream>(); private int pendingIterations; public ActiveStreams (List<Listener> listeners) { this .listeners = listeners; } public void activate (final DefaultStream stream) { if (allowModifications()) { addToActiveStreams(stream); } else { pendingEvents.add(new Event () { @Override public void process () { addToActiveStreams(stream); } }); } } }
因此对于读取的部分,我们就更可以信手拈来了。
onDataRead source code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Override public int onDataRead (final ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception { Http2Stream stream = connection.stream(streamId); Http2LocalFlowController flowController = flowController(); int bytesToReturn = data.readableBytes() + padding; Http2Exception error = null ; switch (stream.state()) { case OPEN: case HALF_CLOSED_LOCAL: break ; case HALF_CLOSED_REMOTE: case CLOSED: error = streamError(stream.id(), STREAM_CLOSED, "Stream %d in unexpected state: %s" , stream.id(), stream.state()); break ; default : error = streamError(stream.id(), PROTOCOL_ERROR, "Stream %d in unexpected state: %s" , stream.id(), stream.state()); break ; } try { flowController.receiveFlowControlledFrame(stream, data, padding, endOfStream); unconsumedBytes = unconsumedBytes(stream); bytesToReturn = listener.onDataRead(ctx, streamId, data, padding, endOfStream); return bytesToReturn; } }
参考