群里有一位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请求从而提高性能。
我们可以用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还生效吗?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。