http长连接

Published On May 06, 2017

category http | tags tcpdump websocket


本文讨论的是HTTP长连接,也叫做HTTP keep-alive或HTTP连接复用。同时介绍websocket——一种全双工的应用层协议。

HTTP长连接

http协议是一种通用的应用层协议,不仅可以用在浏览器和服务器之间,它可以用在任何两个需要通信的应用之间。它的工作模式很简单,客户端向服务端发送请求,服务端接收到请求并处理后返回响应,站在客户端的角度也就是send reqeust, receive response。因为http协议基于TCP协议,所以,每次请求和响应都需要建立(三次握手)和断开(四次握手)TCP连接,因此实际上完成一次http请求/响应至少需要9个TCP报文,效率很低。http长连接是在一个TCP连接里发送和接收多个请求/响应,对于客户端而言可以减小后续请求的响应时间,加快访问速度,对于服务器端可以减少建立的socket连接的次数,降低CPU和内存使用。HTTP/2甚至支持多个请求和响应并发。

两种工作模式的对比: 两种工作模式的对比

Connection: keep-alive

在http server和client之间保持连接需要server和client同时支持, 在http的请求头里经常看到Connection: keep-alive,对应的http响应里也有这个头。 在HTTP/1.0的请求报文和响应报文中使用Connection: keep-alive分别表示客户端和服务端使用长连接进行数据传输。 从HTTP/1.1开始,所有的http请求都被认为是持久连接,除非有特殊说明,也就是说不需要在请求头里指定Connection: keep-alive。 现在使用的基本都是HTTP/1.1协议。

抓包实验

下面使用tcpdump抓取tcp报文来验证http长连接的工作原理,以下测试使用nginx(需要lua模块)作为http服务器,netcat作为客户端。

HTTP保持连接直到超时

nginx安装目录的html文件夹下面增加test.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <h1>hello world</h1>
</body>
</html>
在默认的server块中增加location:
location /test2 {
    content_by_lua_block {
        ngx.sleep(3)
        ngx.say('Hello,world!')
    }
}

为了方便测试,基于nginx的默认配置修改keepalive_timeout参数:

keepalive_timeout  10s;

使用netcat作为http客户端进行测试:

REQ="GET /test.html HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\n\r\n"
(echo -ne $REQ;sleep 15)|nc 127.0.0.1 80
使用sleep 15的目的是使nc不立刻退出,否则nc会在退出的时候关闭tcp连接。

抓包的命令:

tcpdump -A -nl -i lo host 127.0.0.1 and port 80|tee out.txt
tcpdump的一些选项说明:

  • -A: 以ASCII格式打印出所有分组,以下输出中比较奇怪的字符是tcp报文的头信息,暂时没有什么好的办法去除
  • -l: 使标准输出变为缓冲行形式,可以把标准输出重定向到文件。如果不使用这个选项,则不会进行重定向
  • -n: 不把网络地址转换成名字,比如127.0.0.1会被转换成localhost,如果同时希望不进行端口名称的转换,可以使用-nn
  • -i: 「监听」的网络接口,例如 eth0, lo,因为服务器的地址是127.0.0.1,请求是通过lo这个接口传输的

TCP报文的输出信息主要有:

  • timestamps: 时间戳
  • src > dst: 源ip:端口到目的ip:端口
  • flags: TCP报文中的标志信息(控制位),S是SYN标志,F(FIN),P(PUSH),R(RST)等
  • seq: 报文的序列号,也是首字节的编号
  • win: 接收缓存的窗口大小
  • ack: 下次期望的字节编号
  • options: 可选的头部字段
  • length: tcp报文body的大小(字节数)

tee可以把标准输入写入到标准输出和文件,这样既可以重定向到文件又可以在屏幕上看到实时的输出。

输出:

15:06:15.736560 IP 127.0.0.1.52374 > 127.0.0.1.http: Flags [S], seq 2380704866, win 32792, options [mss 16396,sackOK,TS val 1366361905 ecr 0,nop,wscale 9], length 0
E..<.S@.@..f...........P...b......../.....@....
Qq.1....... 
15:06:15.736588 IP 127.0.0.1.http > 127.0.0.1.52374: Flags [S.], seq 1331656395, ack 2380704867, win 32768, options [mss 16396,sackOK,TS val 1366361905 ecr 1366361905,nop,wscale 9], length 0
E..<..@.@.<..........P..O_v....c..........@....
Qq.1Qq.1... 
15:06:15.736605 IP 127.0.0.1.52374 > 127.0.0.1.http: Flags [.], ack 1, win 65, options [nop,nop,TS val 1366361905 ecr 1366361905], length 0
E..4.T@.@..m...........P...cO_v....A.......
Qq.1Qq.1
15:06:15.736656 IP 127.0.0.1.52374 > 127.0.0.1.http: Flags [P.], seq 1:58, ack 1, win 65, options [nop,nop,TS val 1366361905 ecr 1366361905], length 57
E..m.U@.@..3...........P...cO_v....A.a.....
Qq.1Qq.1GET /test.html HTTP/1.1
Host: 127.0.0.1
Accept: */*


15:06:15.736666 IP 127.0.0.1.http > 127.0.0.1.52374: Flags [.], ack 58, win 64, options [nop,nop,TS val 1366361905 ecr 1366361905], length 0
E..4q.@.@............P..O_v........@.......
Qq.1Qq.1
15:06:15.736990 IP 127.0.0.1.http > 127.0.0.1.52374: Flags [P.], seq 1:238, ack 58, win 64, options [nop,nop,TS val 1366361906 ecr 1366361905], length 237
E..!q.@.@............P..O_v........@.......
Qq.2Qq.1HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Fri, 05 May 2017 07:06:15 GMT
Content-Type: text/html
Content-Length: 148
Last-Modified: Fri, 05 May 2017 06:00:39 GMT
Connection: keep-alive
ETag: "590c1507-94"
Accept-Ranges: bytes


15:06:15.737026 IP 127.0.0.1.52374 > 127.0.0.1.http: Flags [.], ack 238, win 67, options [nop,nop,TS val 1366361906 ecr 1366361906], length 0
E..4.V@.@..k...........P....O_w....C.......
Qq.2Qq.2
15:06:15.737071 IP 127.0.0.1.http > 127.0.0.1.52374: Flags [P.], seq 238:386, ack 58, win 64, options [nop,nop,TS val 1366361906 ecr 1366361906], length 148
E...q.@.@............P..O_w........@.......
Qq.2Qq.2<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <h1>hello world</h1>
</body>
</html>


15:06:15.737086 IP 127.0.0.1.52374 > 127.0.0.1.http: Flags [.], ack 386, win 69, options [nop,nop,TS val 1366361906 ecr 1366361906], length 0
E..4.W@.@..j...........P....O_xM...E.......
Qq.2Qq.2
15:06:25.746377 IP 127.0.0.1.http > 127.0.0.1.52374: Flags [F.], seq 386, ack 58, win 64, options [nop,nop,TS val 1366371915 ecr 1366361906], length 0
E..4r.@.@............P..O_xM.......@.......
Qq.KQq.2
15:06:25.746428 IP 127.0.0.1.52374 > 127.0.0.1.http: Flags [F.], seq 58, ack 387, win 69, options [nop,nop,TS val 1366371915 ecr 1366371915], length 0
E..4.X@.@..i...........P....O_xN...E.......
Qq.KQq.K
15:06:25.746443 IP 127.0.0.1.http > 127.0.0.1.52374: Flags [.], ack 59, win 64, options [nop,nop,TS val 1366371915 ecr 1366371915], length 0
E..4r.@.@............P..O_xN.......@.......
Qq.KQq.K

报文解读:

  • 前3个报文是tcp建立连接的三次握手
  • 第4个报文是nc向nginx发送http请求
  • 第5个报文是确认(ACK)报文,tcp协议通过这种方式做到可靠传输
  • 第6/8个报文是nginx发送的http响应,header和body在不同的报文里
  • 等待10s...服务器端的keep alive的时间到了
  • 第10个报文是服务器端主动断开连接的FIN报文
  • 第11个报文是客户端的ACK报文同时也是另一个方向断开连接的FIN报文,按照TCP协议,断开连接是四次握手,但是实际上优化之后只有三次
  • 第12个报文是服务器端的ACK报文,此时服务器的tcp连接还没有释放,而是进入了Time Wait阶段

TCP断开连接的状态图: TCP断开连接的状态图

TCP连接复用

浏览器在访问同一个主机(通过Host头指定,包括ip和端口)的不同资源(uri),会复用TCP连接,下面使用nc进行模拟

REQ2="GET /test2 HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\n\r\n"
(echo -ne $REQ;sleep 5;echo -ne $REQ2;sleep 15)|nc 127.0.0.1 80

输出:

17:30:35.175529 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [S], seq 1714215918, win 32792, options [mss 16396,sackOK,TS val 1375021344 ecr 0,nop,wscale 9], length 0
E..<..@.@.I............Pf,...........x....@....
Q.) ....... 
17:30:35.175557 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [S.], seq 1334878643, ack 1714215919, win 32768, options [mss 16396,sackOK,TS val 1375021344 ecr 1375021344,nop,wscale 9], length 0
E..<..@.@.<..........P..O...f,.......&....@....
Q.) Q.) ... 
17:30:35.175573 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [.], ack 1, win 65, options [nop,nop,TS val 1375021344 ecr 1375021344], length 0
E..4..@.@.I............Pf,..O......A{......
Q.) Q.) 
17:30:35.175621 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [P.], seq 1:58, ack 1, win 65, options [nop,nop,TS val 1375021344 ecr 1375021344], length 57
E..m..@.@.I............Pf,..O......A.a.....
Q.) Q.) GET /test.html HTTP/1.1
Host: 127.0.0.1
Accept: */*


17:30:35.175631 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [.], ack 58, win 64, options [nop,nop,TS val 1375021344 ecr 1375021344], length 0
E..4..@.@..-.........P..O...f,.(...@z......
Q.) Q.) 
17:30:35.175743 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [P.], seq 1:238, ack 58, win 64, options [nop,nop,TS val 1375021344 ecr 1375021344], length 237
E..!..@.@..?.........P..O...f,.(...@.......
Q.) Q.) HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Fri, 05 May 2017 09:30:35 GMT
Content-Type: text/html
Content-Length: 148
Last-Modified: Fri, 05 May 2017 06:00:39 GMT
Connection: keep-alive
ETag: "590c1507-94"
Accept-Ranges: bytes


17:30:35.175768 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [.], ack 238, win 67, options [nop,nop,TS val 1375021344 ecr 1375021344], length 0
E..4..@.@.I............Pf,.(O......Cy......
Q.) Q.) 
17:30:35.175806 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [P.], seq 238:386, ack 58, win 64, options [nop,nop,TS val 1375021344 ecr 1375021344], length 148
E.....@.@............P..O...f,.(...@.......
Q.) Q.) <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <h1>hello world</h1>
</body>
</html>


17:30:35.175819 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [.], ack 386, win 69, options [nop,nop,TS val 1375021344 ecr 1375021344], length 0
E..4..@.@.I............Pf,.(O..5...EyN.....
Q.) Q.) 
17:30:40.178039 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [P.], seq 58:111, ack 386, win 69, options [nop,nop,TS val 1375026347 ecr 1375021344], length 53
E..i..@.@.I............Pf,.(O..5...E.].....
Q.<.Q.) GET /test2 HTTP/1.1
Host: 127.0.0.1
Accept: */*


17:30:40.218097 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [.], ack 111, win 64, options [nop,nop,TS val 1375026387 ecr 1375026347], length 0
E..4..@.@..*.........P..O..5f,.]...@Q......
Q.<.Q.<.
17:30:43.180340 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [P.], seq 386:579, ack 111, win 64, options [nop,nop,TS val 1375029349 ecr 1375026347], length 193
E.....@.@..h.........P..O..5f,.]...@.......
Q.HeQ.<.HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Fri, 05 May 2017 09:30:43 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

d
Hello,world!

0


17:30:43.180366 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [.], ack 579, win 71, options [nop,nop,TS val 1375029349 ecr 1375029349], length 0
E..4..@.@.I............Pf,.]O......G9......
Q.HeQ.He
17:30:53.190325 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [F.], seq 579, ack 111, win 64, options [nop,nop,TS val 1375039359 ecr 1375029349], length 0
E..4..@.@..(.........P..O...f,.]...@.......
Q.o.Q.He
17:30:53.190376 IP 127.0.0.1.54161 > 127.0.0.1.http: Flags [F.], seq 111, ack 580, win 71, options [nop,nop,TS val 1375039359 ecr 1375039359], length 0
E..4..@.@.I............Pf,.]O......G.......
Q.o.Q.o.
17:30:53.190397 IP 127.0.0.1.http > 127.0.0.1.54161: Flags [.], ack 112, win 64, options [nop,nop,TS val 1375039359 ecr 1375039359], length 0
E..4..@.@..'.........P..O...f,.^...@.......
Q.o.Q.o.

报文解读:

  • 前9个报文和上一个例子一样
  • 等待5s...
  • 第10个报文发送第二次请求,使用上一次的TCP连接
  • 等待3s...
  • 第12个报文收到了第二次请求的响应
  • 等待10s...
  • 服务器端的keep alive超时,第14个报文是http服务器主动断开连接

由此可见keep alive计时器是从上一次响应后重新开始计时

服务器不启用keep alive

如果将nginx的keepalive_timeout设置为0(注意:修改配置后需要reload才会生效),也就是不启用keep alive,服务器端会在发送完响应后立刻关闭连接。

执行测试:

(echo -ne $REQ;sleep 1)|nc 127.0.0.1 80
输出:
15:10:31.092968 IP 127.0.0.1.58842 > 127.0.0.1.http: Flags [S], seq 412529163, win 32792, options [mss 16396,sackOK,TS val 1366617262 ecr 0,nop,wscale 9], length 0
E..<^.@.@..............P..................@....
Qt......... 
15:10:31.092995 IP 127.0.0.1.http > 127.0.0.1.58842: Flags [S.], seq 2836962697, ack 412529164, win 32768, options [mss 16396,sackOK,TS val 1366617262 ecr 1366617262,nop,wscale 9], length 0
E..<..@.@.<..........P....................@....
Qt..Qt..... 
15:10:31.093011 IP 127.0.0.1.58842 > 127.0.0.1.http: Flags [.], ack 1, win 65, options [nop,nop,TS val 1366617262 ecr 1366617262], length 0
E..4^.@.@..............P...........A.......
Qt..Qt..
15:10:31.093061 IP 127.0.0.1.58842 > 127.0.0.1.http: Flags [P.], seq 1:58, ack 1, win 65, options [nop,nop,TS val 1366617262 ecr 1366617262], length 57
E..m^.@.@..............P...........A.a.....
Qt..Qt..GET /test.html HTTP/1.1
Host: 127.0.0.1
Accept: */*


15:10:31.093071 IP 127.0.0.1.http > 127.0.0.1.58842: Flags [.], ack 58, win 64, options [nop,nop,TS val 1366617262 ecr 1366617262], length 0
E..4q$@.@............P.........E...@.......
Qt..Qt..
15:10:31.093472 IP 127.0.0.1.http > 127.0.0.1.58842: Flags [P.], seq 1:233, ack 58, win 64, options [nop,nop,TS val 1366617262 ecr 1366617262], length 232
E...q%@.@............P.........E...@.......
Qt..Qt..HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Fri, 05 May 2017 07:10:31 GMT
Content-Type: text/html
Content-Length: 148
Last-Modified: Fri, 05 May 2017 06:00:39 GMT
Connection: close
ETag: "590c1507-94"
Accept-Ranges: bytes


15:10:31.093513 IP 127.0.0.1.58842 > 127.0.0.1.http: Flags [.], ack 233, win 67, options [nop,nop,TS val 1366617262 ecr 1366617262], length 0
E..4^.@.@..............P...E...r...C.......
Qt..Qt..
15:10:31.093560 IP 127.0.0.1.http > 127.0.0.1.58842: Flags [P.], seq 233:381, ack 58, win 64, options [nop,nop,TS val 1366617262 ecr 1366617262], length 148
E...q&@.@............P.....r...E...@.......
Qt..Qt..<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <h1>hello world</h1>
</body>
</html>


15:10:31.093580 IP 127.0.0.1.58842 > 127.0.0.1.http: Flags [.], ack 381, win 69, options [nop,nop,TS val 1366617262 ecr 1366617262], length 0
E..4^.@.@..............P...E.......E.
.....
Qt..Qt..
15:10:31.093620 IP 127.0.0.1.http > 127.0.0.1.58842: Flags [F.], seq 381, ack 58, win 64, options [nop,nop,TS val 1366617262 ecr 1366617262], length 0
E..4q'@.@............P.........E...@.......
Qt..Qt..
15:10:31.093658 IP 127.0.0.1.58842 > 127.0.0.1.http: Flags [F.], seq 58, ack 382, win 69, options [nop,nop,TS val 1366617262 ecr 1366617262], length 0
E..4^.@.@..............P...E.......E.......
Qt..Qt..
15:10:31.093687 IP 127.0.0.1.http > 127.0.0.1.58842: Flags [.], ack 59, win 64, options [nop,nop,TS val 1366617262 ecr 1366617262], length 0
E..4q(@.@............P.........F...@.
.....
Qt..Qt..
以上测试结果的第10个报文说明,服务器发送完请求后立刻关闭了连接。

HTTP Long Polling

但需要服务器发送消息给客户端的时候,传统的web页面使用轮询(polling)的方式,即客户端周期性地向服务器发送请求,服务器在没有消息的时候返回空,这种方式不仅无法做到实时,而且非常浪费服务器的资源(处理器和带宽)。Http Long Polling是一种使用http协议来模拟服务器推(server push)的技术,当发送一个http请求后,服务器并不是马上返回响应,而是等到某个事件发生,比如有新信息或超时后才返回。当客户端接收到响应,马上又发起一个新的请求。这也意味着,服务器端需要保持这个连接对应的socket。它的一个明显的缺点是,当http响应头占了响应的很大比例时,有效载荷很小。

示例

tornado是一个FriendFeed开源的异步网络通信框架,擅长处理大量的长连接,比如websocket和long polling。它的chat示例同时包含了long polling的客户端和服务端的实现。

Websocket

TCP协议本身是一种全双工(full-duplex communication channels)的通信协议,但是HTTP协议的工作模式限制了服务器端只能被动的等待客户端请求,而不能主动发送数据。因此当服务器端需要给客户端发送通知的时候就不适合使用HTTP协议,典型的比如chat,一般都是基于TCP自定义应用层协议,虽然有的网页使用ajax轮询(polling),但这种方式代价很大而且无法做到实时。为了解决这个问题,一种新的协议Websocket诞生了。它充分发挥了TCP的灵活性,允许服务器端向客户端主动推送消息。

与现有协议对比

websocket和http协议的关系

websocket和http都是应用层协议。但Websocket不是基于HTTP协议,它们的联系是websocket的握手报文需要使用http协议,并且使用同样的端口。websocket与http相比除了支持服务端push消息之外,它的另一个优势是轻量,因为只有几个字节的头,其余部分都是数据。

websocket与TCP相比的优势

直接使用TCP协议的好处是灵活,但缺点是麻烦,因为TCP是面向字节的协议,它传输的是字节流,也就是说调用一次socket的send函数仅仅是将要发送的消息(字节数组)放到一个发送缓存里,实际发送的tcp报文不受开发者控制,应用层需要从连续的多个报文组成的字节流中解析出消息,那么如何确定消息的边界呢?http协议使用\r\n这样的特殊标记,再加上Content-Length头从而确定完整的请求和响应内容,redis设计了一种自己的应用层协议(RESP)。websocket提供了将多个TCP报文段(frames)组装成消息(message)的功能,需要依靠头部的payload length字段。

另一方面,当无法预先获取要传输的消息的长度的时候,可以发送多次,也就是一个message可以拆分成多个fragment。http通过Transfer-Encoding: chunked头实现该功能。

实现

  1. www.websocket.org提供了一个demo,并且附有使用js编写websocket客户端的示例代码。chrome的开发者工具可以轻松的查看websocket的通信内容。 chrome调试websocket

  2. github上的dpallot/simple-websocket-server提供了websocket server的python实现,有两个现成的示例:echo和chat。

  3. aaugustin/websockets是一个更加完善的websocket协议的实现,包含服务端和客户端。

  4. 类似的还有Lawouach/WebSocket-for-Python

参考


qq email facebook github
© 2018 - Xurui Yan. All rights reserved
Built using pelican