HTTPkeep-alive详解
1.为什么要有Connection: keep-alive?
在早期的HTTP/1.0中,每次http请求都要创建⼀个连接,⽽创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重⽤连接。在后来的HTTP/1.0中以及HTTP/1.1中,引⼊了重⽤连接的机制,就是在http请求头中加⼊Connection: keep-alive来告诉对⽅这个请求响应完成后不要关闭,下⼀次咱们还⽤这个请求继续交流。协议规定HTTP/1.0如果想要保持长连接,需要在请求头中加
上Connection: keep-alive,⽽HTTP/1.1默认是⽀持长连接的,有没有这个请求头都⾏。
当然了,协议是这样规定的,⾄于⽀不⽀持还得看服务器(⽐如tomcat)和客户端(⽐如浏览器)的具体实现。在实践过程中发现⾕歌浏览器使⽤HTTP/1.1协议时请求头中总会带上Connection: keep-alive,另外通过httpclient使⽤HTTP/1.0协议去请求tomcat时,即使带
上Connection: keep-alive请求头也保持不了长连接。如果HTTP/1.1版本的http请求报⽂不希望使⽤长连接,则要在请求头中加
上Connection: close,接收到这个请求头的对端服务就会主动关闭连接。
但是http长连接会⼀直保持吗?肯定是不会的。⼀般服务端都会设置keep-alive超时时间。超过指定的时间间隔,服务端就会主动关闭连接。同时服务端还会设置⼀个参数叫最⼤请求数,⽐如当最⼤请求数是300时,只要请求次数超过300次,即使还没到超时时间,服务端也会主动关闭连接。
2.Transfer-Encoding和Content-Length
谈到http长连接,都绕不开这两个请求/响应头。其中Transfer-Encoding不建议在请求头中使⽤,因为⽆法知道服务端能否解析这个请求头,⽽应该在响应头中使⽤,因为客户端浏览器都能解析这个响应头。Content-Length在请求⽅法为GET的时候不能使⽤,在请求⽅法为POST的时候需要使⽤,同时也常常出现在响应头中。为了⽅便描述,下⾯只说明响应头中出现这两个属性的情况。
要实现长连接很简单,只要客户端和服务端都保持这个http长连接即可。但问题的关键在于保持长连接后,浏览器如何知道服务器已经响应完成?在使⽤短连接的时候,服务器完成响应后即关闭http连接,这样浏览器就能知道已接收到全部的响应,同时也关闭连接(TCP连接是双向的)。在使⽤长连接的时候,响应完成后服务器是不能关闭连接的,那么它就要在响应头中加上特殊标志告诉浏览器已响应完成。
⼀般情况下这个特殊标志就是Content-Length,来指明响应体的数据⼤⼩,⽐如Content-Length: 120表⽰响应体内容有120个字节,这样浏览器接收到120个字节的响应体后就知道了已经响应完成。
由于Content-Length字段必须真实反映响应体长度,但实际应⽤中,有些时候响应体长度并没那么好获得,例如响应体来⾃于⽹络⽂件,或者由动态语⾔⽣成。这时候要想准确获取长度,只能先开⼀个⾜够⼤的内存空间,等内容全部⽣成好再计算。但这样做⼀⽅⾯需要更⼤的内存开销,另⼀⽅⾯也会让客户端等更久。这时候Transfer-Encoding: chunked响应头就派上⽤场了,该响应头表⽰响应体内容⽤的是分块传输,此时服务器可以将数据⼀块⼀块地分块响应给浏览器⽽不必⼀次性全部响应,待浏览器接收到全部分块后就表⽰响应结束。
以分块传输⼀段⽂本内容:“⼈的⼀⽣总是在追求⾃由的⼀⽣ So easy”来说明分块传输的过程,如下图所⽰
图中每个分块的第⼀⾏是分块内容的⼤⼩,⼗六进制表⽰,后⾯跟CRLF(\r\n),第⼀⾏本⾝以及分块内容末尾的CRLF不计⼊⼤⼩。第⼆⾏是分块内容,后⾯也跟CRLF。最后⼀个分块虽然⼤⼩为零,但是必不可少,表⽰分块的结束,后⾯也跟CRLF,同时内容为空。最后,响应体以CRLF结束。将它们结合起来的响应内容就是:
HTTP/1.1 200 OK
Content-Type: text/plain;charset=utf-8
Connection: keep-alive
Transfer-Encoding: chunked
21\r\n
⼈的⼀⽣总是在追求⾃由\r\n
11\r\n
的⼀⽣ So easy\r\n
0\r\n
\r\n
不过以上格式的响应体内容⽤浏览器⾃带的调试⼯具是看不出来的,浏览器⾃带调试⼯具对分块传输和⾮分块传输响应体的显⽰是⼀样的,要想看到区别,需要⽤Wireshark、Fiddler等抓包⼯具查看。
3.HTTP keep-alive和TCP keepalive的区别
TCP keepalive指的是TCP保活计时器(keepalive timer)。设想有这样的情况:客户已主动与服务器建⽴了TCP连接。但后来客户端的主机突然出故障。显然,服务器以后就不能再收到客户发来的数据。因此,应当有措施使服务器不要再⽩⽩等待下去。这就是使⽤保活计时器。服务器每收到⼀次客户的数据,就重新设置保活计时器,时间的设置通常是两⼩时。若两⼩时没有收到客户的数据,服务器就发送⼀个探测报⽂段,以后则每隔75秒发送⼀次。若⼀连发送10个探测报⽂段后仍⽆客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。
——摘⾃谢希仁《计算机⽹络》
针对linux系统,TCP保活超时时间、探测报⽂段发送间隔、探测报⽂段最⼤发送次数都是可以设置的,如下
# cat /proc/sys/net/ipv4/tcp_keepalive_time 7200 当keepalive启⽤的时候,TCP发送keepalive消息的频度。缺省是2⼩时
# cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 当探测没有确认时,重新发送探测的频度。缺省是75秒
# cat /proc/sys/net/ipv4/tcp_keepalive_probes 9 探测尝试的次数。如果第1次探测包就收到响应了,则后8次的不再发
4.分块传输⽰例代码
这是⽤netty4写的⽰例代码,主要⽤到了netty中关于HTTP协议的编解码部分,⽬的是⽤来⾃定义分块传输的块⼤⼩。运⾏程序后,可以在ctx.writeAndFlush(httpContent);处打个断点,⼀个分块⼀个分块地调试,同时⽤wireshark抓TCP包,可以发现每响应⼀个分块,都会产⽣⼀次TCP传输,同时浏览器页⾯也会实时显⽰最新的响应数据(运⾏环境:Ubuntu 16.04LTS + Firefox63.0 + netty4.1.15.Final,强调运⾏环境是因为在mac系统的⽕狐或者⾕歌浏览器中,⽆法重现同时浏览器页⾯也会实时显⽰最新的响应数据这个现象)。
package cn.cjc.keepalive;
import ioty.bootstrap.ServerBootstrap;
import ioty.buffer.Unpooled;
import ioty.channel.ChannelFuture;
import ioty.channel.ChannelHandlerContext;
import ioty.channel.ChannelInitializer;
import ioty.channel.SimpleChannelInboundHandler;
import ioty.channel.nio.NioEventLoopGroup;
import ioty.channel.socket.SocketChannel;
import ioty.channel.socket.nio.NioServerSocketChannel;
import dec.http.*;
import ioty.util.CharsetUtil;
/
**
* @author chenjc
* @since 2017-10-14
*/
public class KeepAliveTest {
public static void main(String[] args){
NioEventLoopGroup group =new NioEventLoopGroup();
try{
ServerBootstrap sb =new ServerBootstrap();
.channel(NioServerSocketChannel.class)
.
childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel socketChannel)throws Exception {
socketChannel.pipeline()
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(512*1024))
.addLast(new HttpRequestHandler());
}
});
ChannelFuture future = sb.bind(8088).sync();
System.out.println("==========服务已启动==========");
future.channel().closeFuture().sync();
System.out.println("==========服务已关闭==========");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
}finally{
group.shutdownGracefully();
}
}
private static class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest)throws Exception {
DefaultHttpResponse response =new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpHeaders headers = response.headers();
boolean keepAlive = HttpUtil.isKeepAlive(fullHttpRequest);
if(keepAlive){
为什么使用bootstrap?headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN +";charset=utf-8");
headers.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
headers.add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
// headers.add(HttpHeaderNames.CONTENT_LENGTH, 11 * 10 + 1);//+1的原因是最后⼀个分块的10占两个字节}
ctx.writeAndFlush(response);//写响应⾏和响应头
for(int i =1; i <=10; i++){
HttpContent httpContent =new piedBuffer("第"+ i +"分块 ", CharsetUtil.UTF_8)); ctx.writeAndFlush(httpContent);//分批次写响应体,每次运⾏完此⾏代码,浏览器页⾯也会显⽰最新的响应内容}
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);//告诉netty结束响应
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx)throws Exception {
System.out.println("inactive");
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx)throws Exception {
System.out.println("unregister");
}
}
}
参考资料
/HOWTO/html_single/TCP-Keepalive-HOWTO/
wwwblogs/cswuyg/p/3653263.html
xls9577087.iteye/blog/2268698
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论