群里有一位 gu(a)y 提到过一个面试题,问 HTTP keep alive 和操作系统中 TCP 的 keep alive 有啥区别。
这个问题算是个八股文题,然而细问上来,又很难说出一个有体系的、确定的答案。这也是个不错的面试题,所以这里就联合代码谈下本人的了解。
HTTP keepalive
在 HTTP 1.0 期间,每个 TCP 连贯只会被一个 HTTP Transaction(申请加响应)应用,申请时建设,申请实现开释连贯。当网页内容越来越简单,蕴含大量图片、CSS 等资源之后,这种模式效率就显得太低了。所以,在 HTTP 1.1 中,引入了 HTTP persistent connection 的概念,也称为 HTTP keep-alive,目标是复用 TCP 连贯,在一个 TCP 连贯上进行屡次的 HTTP 申请从而进步性能。
HTTP 1.0 中默认是敞开的,须要在 HTTP 头退出 ”Connection: Keep-Alive”,能力启用 Keep-Alive;HTTP 1.1 中默认启用 Keep-Alive,退出 ”Connection: close “,才敞开。
咱们能够用 Netty 来实现一个 HTTP 服务器。Netty 实现 HTTP 服务器的残缺代码此处不做列举,只看要害的 channelRead 办法。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if (msg instanceof HttpRequest) {HttpRequest request = (HttpRequest) msg;
boolean keepAlive = HttpUtil.isKeepAlive(request);
serverBootstrap.channel(NioServerSocketChannel.class)
.group(boss, work)
.handler(new LoggingHandler(LogLevel.INFO)) // handler 在初始化时就会执行,能够设置打印日志级别
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast("http-coder",new HttpServerCodec());
ch.pipeline().addLast("aggregator",new HttpObjectAggregator(1024*1024)); // 在解决 POST 音讯体时须要加上
ch.pipeline().addLast(new HttpServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true);
//handle 代码
httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes());
if (keepAlive) {httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.writeAndFlush(httpResponse);
} else {ctx.writeAndFlush(httpResponse).addListener(ChannelFutureListener.CLOSE);
}
}
}
Netty 封装了 HTTP 的实现,代码很简略,如果判断申请是 keep-alive 的,那么响应头也加上 keep-alive 标记,从而实现了 keep alive 性能。
就这样就实现了 keep alive 吗?那我怎么晓得 keep alive 是否真的失效呢?咱们在 channelRead 过程中打印下 channel id 等要害信息就晓得了。
System.out.println("keepAlive="+keepAlive);
System.out.println("channel id="+ctx.channel().id());
System.out.println("http uri:" + uri);
// 打印申请参数过程略。
而后咱们在浏览器里申请两次,看下日志
信息: [id: 0xee8bc5e1, L:/0:0:0:0:0:0:0:0:8080] READ: [id: 0x734e2ebb, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:37386]
七月 06, 2021 10:03:48 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xee8bc5e1, L:/0:0:0:0:0:0:0:0:8080] READ COMPLETE
keepAlive=true
channel id=734e2ebb
http uri: /a.txt?name=chen&f=123;key=456
name=chen
f=123
key=456
keepAlive=true
channel id=734e2ebb
http uri: /favicon.ico
keepAlive=true
channel id=734e2ebb
http uri: /a.txt?name=chen&f=123;key=456
name=chen
f=123
key=456
keepAlive=true
channel id=734e2ebb
http uri: /favicon.ico
能够看到,无论刷新多少次,服务器端日志里也只记录了一次 socket 连贯日志,并且每次的 channel id 都是一样的。
如果不是 keep alive 的,那服务端日志是怎么的呢?
七月 06, 2021 9:51:27 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xade39344, L:/0:0:0:0:0:0:0:0:8080] READ: [id: 0x26d40041, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:33130]
七月 06, 2021 9:51:27 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xade39344, L:/0:0:0:0:0:0:0:0:8080] READ COMPLETE
keepAlive=true
channel id=26d40041
http uri: /a.txt?name=chen&f=123;key=456
name=chen
f=123
key=456
七月 06, 2021 9:51:29 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xade39344, L:/0:0:0:0:0:0:0:0:8080] READ: [id: 0x600995e6, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:33156]
七月 06, 2021 9:51:29 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xade39344, L:/0:0:0:0:0:0:0:0:8080] READ COMPLETE
keepAlive=true
channel id=600995e6
http uri: /a.txt?name=chen&f=123;key=456
name=chen
f=123
key=456
客户端两次连贯的 socket 端口一次是 33130, 第二次是 33156,channel id 也不一样,证实的确是两个连贯,keep alive 并没有失效。
当初大家能直观地了解 HTTP keep alive 是怎么回事了吧。其实 HTTP 的 keep alive 很好了解,HTTP 是基于 TCP 协定的(为了谨严,此处仅针对 HTTP 1.1 版本,HTTP 2 比较复杂,HTTP 3 则基于 UDP 协定),TCP 是流式的,那么无状态的 HTTP 申请要在流式的 TCP 上实现 keep alive 是很天然的一件事,但面临一个问题。
要实现长连贯很简略,只有客户端和服务端都放弃这个 HTTP 长连贯即可。但问题的关键在于放弃长连贯后,浏览器如何晓得服务器曾经响应实现?在应用短连贯的时候,服务器实现响应后即敞开 HTTP 连贯,这样浏览器就能晓得已接管到全副的响应,同时也敞开连贯(TCP 连贯是双向的)。在应用长连贯的时候,响应实现后服务器是不能敞开连贯的,那么它就要在响应头中加上非凡标记通知浏览器已响应实现。
个别状况下这个非凡标记就是 Content-Length,来指明响应体的数据大小,比方 Content-Length: 120 示意响应体内容有 120 个字节,这样浏览器接管到 120 个字节的响应体后就晓得了曾经响应实现。
因为 Content-Length 字段必须实在反映响应体长度,但理论利用中,有些时候响应体长度并没那么好取得,例如响应体来自于网络文件,或者由动静语言生成。这时候要想精确获取长度,只能先开一个足够大的内存空间,等内容全副生成好再计算。但这样做一方面须要更大的内存开销,另一方面也会让客户端等更久。这时候 Transfer-Encoding:chunked 响应头就派上用场了,该响应头示意响应体内容用的是分块传输,此时服务器能够将数据一块一块地分块响应给浏览器而不用一次性全副响应,待浏览器接管到全副分块后就示意响应完结。
所以,HTTP 的 keep-alive 实际上就是个连贯复用。
说完了 HTTP 的 keep alive,那么 TCP 的 keep alive 呢?
TCP keepalive
在应用 TCP 长连贯(复用已建设 TCP 连贯)的场景下,须要对 TCP 连贯进行保活,防止被网关干掉连贯。
在应用层,能够通过定时发送心跳包的形式实现。而 Linux 已提供的 TCP KEEPALIVE,在应用层可不关怀心跳包何时发送、发送什么内容,由 OS 治理:OS 会在该 TCP 连贯上定时发送探测包,探测包既起到连贯保活的作用,也能自动检测连贯的有效性,并主动敞开有效连贯。
TCP 的机制,有很多的文章,说的比我分明多了,我就不再细数。简略来说,TCP 的 keepalive 机制用意在于保活、心跳,检测连贯谬误,是一个基于定时器的实现。在 Linux 中默认是 7200 秒。
总结
HTTP 协定的 keep alive 用意在于连贯复用,同一个连贯上串行形式传递申请 - 响应数据。
TCP 的 keep alive 机制用意在于保活、心跳,检测连贯谬误。
二者没有间接关系。回到 netty 代码,留神看这里
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast("http-coder",new HttpServerCodec());
//...
})
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true);
你认为,如果把 ChannelOption.SO_KEEPALIVE 属性设置为 flase,前面的 HTTP keep alive 还失效吗?