实战 HTTP 连接管理
1.背景
- 短连接
HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求 - 应答”方式。它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接,所以就被称为“短连接”(short-lived connections)。
短连接的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常“昂贵”的操作。
- 长连接
针对短连接暴露出的缺点,HTTP 1.1 版本协议就提出了“长连接”的通信方式,也叫“持久连接”(persistent connections)、“连接保活”(keep alive)、“连接复用”(connection reuse)。
2.实战
注意:以下的结果都是基于 HTTP/1.1 协议。
2.1 长短连接的特点
HTTP 1.1 默认会开启长连接,相应的头字段为 Connection,它可能会出现在请求头和相应头中。请求头中出现 Connection:keep-alive,表示客户端希望和服务器进行长连接,而响应头中使用了 Connection:keep-alive 表示最后确实是用了长连接。
我们此次实战后端使用 JavaScript 的服务端框架 Koa。接下来使用一个简单的实例来演示:
- 后端
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx,next) => {
ctx.body = 'hello'
})
app.listen(3002)
启动后端服务,使用浏览器打开 http://localhost:3002,我们来观察请求头和响应头。
在上面的图片中,我们可以观察客户端想要使用长连接,服务器支持并且允许开启了长连接。
接下来我们来修改一下代码:
- 后端
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx,next) => {
ctx.set('Connection','close')
ctx.body = 'hello'
})
app.listen(3002)
服务端对所有请求采用短连接的方式。我们来观察一下结果。
此时虽然客户端想使用长链接,但是服务器最后策略使用了短链接。
2.2 长链接和短链接的区别
长链接可以进行链接复用。我们接下来使用代码来演示。
- 后端 demo 结构
projectRoot
|
|_static
|
|_test.html
|_test1.png
.
.
.
|_test9.png
|
|_app.js
我们在 static 文件夹中放了 10 个文件,分别是 test.html 和 9 张图片。
- app.js
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const app = new Koa()
app.use(async (ctx,next) => {
// 拿到静态文件夹的路径
const staticPath = path.resolve(__dirname,'static')
if(ctx.url === '/'){
// 根路径时返回 test.html 文件
const indexPagePath = staticPath + '/test.html'
ctx.set('Content-Type','text/html');
ctx.body = fs.readFileSync(indexPagePath)
}else {
if(ctx.url.includes('test')){
// /testx.png 返回图片
ctx.set('Content-Type','image/png');
ctx.body = fs.readFileSync(staticPath + ctx.url)
}
}
})
app.listen(3002)
- test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<img src="/test1.png"/>
<img src="/test2.png"/>
<img src="/test3.png"/>
<img src="/test4.png"/>
<img src="/test5.png"/>
<img src="/test6.png"/>
<img src="/test7.png"/>
<img src="/test8.png"/>
<img src="/test9.png"/>
</body>
</html>
接下来启动后端服务,使用浏览器打开 http://localhost:3002/test.html ,并且打开谷歌浏览器的开发者工具 Network 面板。注意,为了更好的观察现象,我们需要将网络模拟 Fast 3G,并且显示抓包表头项里面的 Connection ID 项,以及只看图片 Img 类型。
观察上面的结果,我们可以发现 Connection ID 有六个不同的 Id,剩下的三个 Id 是重复的。这个可以说明,chrome 浏览器在一个域名下只能同时创建 6 个 TCP 链接并发进行数据传输,剩下三个多余的请求为了复用 TCP 链接需要等待,我们通过 waterfall 表头也能观察到灰色的等待时间(stalled)。
接下来我们修改一下代码:
- app.js
app.use(async (ctx,next) => {
const staticPath = path.resolve(__dirname,'static')
if(ctx.url === '/'){
const indexPagePath = staticPath + '/test.html'
ctx.set('Content-Type','text/html');
ctx.body = fs.readFileSync(indexPagePath)
}else {
if(ctx.url.includes('test')){
ctx.set('Connection','close');// 使用短链接
ctx.set('Content-Type','image/png');
ctx.body = fs.readFileSync(staticPath + ctx.url)
}
}
})
服务端使用短链接之后,我们发现 Connection ID 不再重复。
2.3 长链接优化
由于长时间进行连接是很消耗服务器性能的,当链接没有数据传输或者 TCP 链接的请求数到达一定数量时,我们想要关闭长链接,这时候应该怎么做呢?
我们可以通过配置 nginx,如果配置了“keepalive_timeout 60”和“keepalive_requests 5”,意思是空闲连接最多 60 秒,最多发送 5 个请求。所以,如果连续刷新五次页面,就能看到响应头里的“Connection: close”了。