HTTP 1 & 2

t8J6C.png

HTTP协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,我们今天就来深度剖析这个古老的协议。

HTTP 1

协议

HTTP 协议的构建很简单,协议分为了 RequestResponse 部分。

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();

// 获得第一行的 method 即可
let method = request_line_values[0];

通信

协议部分不是很复杂,但是 HTTP 通信却有一些很大的弊端。我们先看下常见的 HTTP 通讯

tXynw.png

一来一回就构成了,我们的 HTTP 通讯的全部。在 HTTP 1.0 的时候,当我们完成一次通讯的时候,我就会 Close 底层的 TCP 协议。但是我们知道开通一个 TCP 连接是至少需要 3RTT+ 1RTT 的 HTTP 发送,因此成本是很高的。因此在 HTTP 1.1 里面提出一些改进计划。

特性

持久连接

即是不声明 Connection: keep-aliveHTTP 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)才可以使用管道功能。不过值得注意的,大部分的浏览器并没有启用这个功能。

tX9t8.png

分块传输编码

使用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 类型。比较常见的就是 HeaderData

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 的发送是一个阻塞的事件,尽快的快递数据才是我们需要的。

tcgSR.png

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

tcuo2.png

Steam 的状态变化依靠的是 Frame 数据不同进行驱动的。比如从 idle 状态切换到 open 状态,就需要通过发送/接受 Headers Frame,除了 Stream Frame 还一个 Message 的概念,这个对应到 1.1Request/Reponse

流控

在看 Header Frame 定义的时候,我们发现有一个字段叫 Stream Dependency,看上去很像是什么依赖关系,其实不是,这个字段是用来标记 优先级 而不是 从属关系。因为我们在 HTTP2中实际上仍是通过一个 TCP 连接进行数据传输。

tcJVL.png

面临 拥堵 问题,因此增加了一个 拥塞控制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的资源。

服务器推送

tcNyU.png

HTTP/2 脱离了严格的请求-响应语义,支持一对多和服务器启动的推送工作流,在浏览器内外开辟了一个新的交互可能性世界。这是一个有利的特性,对于我们如何看待该协议,以及在何处和如何使用它,都将产生重要的长期影响。[看起来是抢了 Websocket 的生意]

交换数据

客户端发起一次 HTTP 请求(H帧)就会生成一个 Stream,服务端响应使用相同的 Stream进行响应。

会不会创建相同的Stream ID呢?协议也考虑到了,客户端创建的ID是奇数,服务器端创建的ID是偶数,就不会重复了。

响应的时候可以选择返回 H 或者直接返回 D 都是可以的 文档,最后 Frame 需要包含 END_STREAM 标记,这样我们就可以关闭这个 Stream 了。

实现

从协议上,我们可以发现其实整个 HTTP 2 的协议实现,抛开 流控 部分,其他的还是比较好实现的。我们来看看 Netty 的实现重点。

第一步显然是读取到 H 帧之后的操作,从入口处开始阅读:

readFramesource 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 {
// 如果还没有读取到 Header部分,这里不是 Header Frame,是说我们的 HTTP2 协议的头部
if (readingHeaders) {

// 处理 Header 头部,比如 FLAGS,STREAM ID, FRAME TYPE 等
processHeaderState(input);
if (readingHeaders) {
// Wait until the entire header has arrived.
return;
}
}

// 处理数据体
processPayloadState(ctx, input, listener);
if (!readingHeaders) {
// Wait until the entire payload has arrived.
return;
}
} while (input.isReadable());
}
}

我们下来要看如果我们的 FRAME TYPEHEADER 的应该怎么处理,我们知道应该创建一个 STREAM 才是,我们继续来看代码

onDataReadsource 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 {

// 根据 ID 获得 Stream,这里其实不销毁(实际上非协议),可以复用这个对象
Http2Stream stream = connection.stream(streamId);
boolean allowHalfClosedRemote = false;
// 没有就创建
if (stream == null && !connection.streamMayHaveExisted(streamId)) {
stream = connection.remote().createStream(streamId, endOfStream);
// Allow the state to be HALF_CLOSE_REMOTE if we're creating it in that state.
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 内置的一个 LinkedHashSetStreams

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);
}
});
}
}
}

因此对于读取的部分,我们就更可以信手拈来了。

onDataReadsource 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 {

// 根据 ID 获得 Stream
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 {
// 处理是不是流程控制帧,就是上面的 H ES PP 等
flowController.receiveFlowControlledFrame(stream, data, padding, endOfStream);

// 应该被消费的数据长度
unconsumedBytes = unconsumedBytes(stream);

// 调用消费者进行数据读取
bytesToReturn = listener.onDataRead(ctx, streamId, data, padding, endOfStream);
return bytesToReturn;
}
}

参考