群里有一位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办法。
@Overridepublic 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 COMPLETEkeepAlive=truechannel id=734e2ebbhttp uri: /a.txt?name=chen&f=123;key=456name=chenf=123key=456keepAlive=truechannel id=734e2ebbhttp uri: /favicon.icokeepAlive=truechannel id=734e2ebbhttp uri: /a.txt?name=chen&f=123;key=456name=chenf=123key=456keepAlive=truechannel id=734e2ebbhttp 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 COMPLETEkeepAlive=truechannel id=26d40041http uri: /a.txt?name=chen&f=123;key=456name=chenf=123key=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 COMPLETEkeepAlive=truechannel id=600995e6http uri: /a.txt?name=chen&f=123;key=456name=chenf=123key=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还失效吗?