有朋友问我,关于使用Netty开发长连接应用,为什么要加一个Keep Alive为true的配置。你是否也有这样的疑惑呢?
要解答这个问题,首先要了解Keep Alive。所以,今天笔者给大家分享笔者对Keep alive的一点浅薄的了解,再来说下应用层为什么要自己实现Keep Alive,就是常说的“心跳保活”,以及什么是Idle机制,Netty是怎么实现的。
对网络编程不了解的朋友可能都没听过Keep Alive,但Http请求头携带的Connection:keep-alive大家可能熟悉。不过HTTP1.1版本是默认开启的,也就导致很少能看到请求头中有Connection这个参数。
KeepAlive译为“保活”,就是“心跳保活”。KeepAlive最早应该是TCP协议定义的规范,即支持TCP协议的设备都应该要实现这个规范。我们知道,两个Socket建立连接后就可以发送和接收数据,但是连接是否健康双方是无法感知的,只有下次想给对方发送数据,结果发送失败,才判断对方是否断开连接了。
怎么理解Socket通信是无法感知对方状态的呢。来个简单的比喻。小明和小红修了一条路,目的是可以去对方家里玩,路修好了,小明和小红随时都可以走到对方家里玩。假设没有任何的通信设备的情况下,比如写信、打电话、发短信等,小明去不去小红家,小红没办法知道,也不知道小明是不是今天有事来不了,或者因为上次来玩时生气了,以后都不来了,同样的,小明也不知道小红的情况。某天,小红突然很想找小明玩,但是走到半路,发现路已经被堵住了。
数据包在网络传输中会出现丢包的情况,所以不能仅仅通过发送一次报文失败,就判定是对方断开连接,这样未免果断了些,网络波动还是很正常的现象。所以,就需要经过多次试探来确认对方是否真的不可达。这是TCP协议规定的,也可以通过修改配置,默认是重试9次,每次间隔75ms,每7200ms内没有接收到任何数据报文,则发送KeppAlive探测报文。详细的可以查阅下资料,这里不展开说了。
从上面的默认参数可以看出,TCP的KeepAlive需要两分多钟才能完成。既然可以修改默认配置,那为什不改配置就好了。修改配置是对整个操作系统上的应用起作用的,而一个操作系统上不只部署一个应用。
那为什么TCP提供了KeepAlive,应用还需要自己去实现呢。一个是修改配置会影响到整个系统的应用,二是不修改的话,默认2分多钟才完成一个连接是否健康的检测。2分钟是什么概念,对于高并发的应用服务器而言,2分钟黄花菜都凉了。三是不同厂商的路由器可能对keepalive的配置默认是关闭的,这个我不是很了解。
那HTTP协议中的Connection:keep-alive又跟TCP协议的KeepAlive有什么联系呢?没有联系。Http是应用层协议。说到协议,我记得有一次我说了句“协议,协议,协议只是协议”,同事笑了,然后我解释到“本来就是嘛,协议就是一种约定,是需要你去实现它才能用它定义的特性”。
HTTP1.1版本默认开启keep-alive,只是说协议这样规定,而如果某公司提供的HTTP服务器声明实现了HTTP1.1版本,比如Nginx,则说明Nginx是支持Http协议规定的,默认保持长连接的特性,再比如tomcat、jetty等Servlet容器。
以上只是笔者对KeepAlive的一点了解。那么,我们自己基于Netty开发为什么配置KeepAlive,相必大家也都清楚了。看Netty源码你会发现,这其实是为Jdk的SocketChannel配置的。
前面提到,我们在开发网络通信应用时,需要自己去实现KeepAlive的原因。在高并发场景下,2分钟堆积的SocketChannel有多少,除占用文件句柄数,导致文件句柄数达到最大限制,无法接收新的连接外,大量无效连接也会影响epoll轮询事件的效率。
所以,我们在应用层一般设置6秒到15秒的时间间隔,规定客服端给服务端定时发送心跳包,当服务端超过这个时间没有接收到任何心跳包时,就认为客户端掉线,不会设置重试,即便网络波动也断开连接。
但是,只客户端给服务端发送心跳包还不行啊,我客户端也需要感知连接是否可用,也需要你服务器回应我的心跳包,或者也定时给我发送心跳包啊。
即便是设置为1分钟,假设有1w个长连接,一分钟内给服务端发送一个心跳数据包,这对客户端来说并没有什么影响,但是服务端一分钟内就接收1w个数据包,心跳包也是需要编码和解码,也占用buff接收,在一次解码时也会伴随一次内存拷贝,因为要解决数据粘包和半包问题。
怎样才能优化不必要的心跳包发送和接收。就是idle机制。比如netty提供的IdleChannelHandle(是这这个名吧,记不清),在读空闲超时或写空闲超时时会发送一个IdleStatusEvent事件,我们可以监听这些事件,比如服务端监听读空闲超时事件,当多久没有接收过客户端发来数据包时,触发keepalive,这时再主动去给客户端发送一条数据包,如果发送成功呢,说明客户端还活着,发送失败则关闭连接。也可以空闲超时直接关闭连接。
我们可以只关心读空闲超时事件,也可以只关心写空闲超时事件,也可以即关心读也关心写空闲超时事件。读空闲就是很久没有接收到客户端发来消息,写空闲超时就是很久没有给客户端发送消息。
其实Netty的Idle实现并不难理解,就是在每次接收到客户端发来的数据包时,更新一下最近一次读的时间,在每次给客户端发送数据包时,更新一下最近一次写的时间。然后根据配置的读或写空闲超时时间分别注册两个定时任务,定时检测当前时间与最近一次更新的时间比较,超过配置的时间间隔,则发送一个Idle事件。定时任务是一次性的,每次执行完后都重新注册定时任务。
照这么说,我们也可以自己实现一个。其实Netty提供的Idle实现,并没有那么简单,如果去看它的源码,你会发现,写空闲超时检测的实现, Netty会判断当前是否有数据在写,但未写完成。比如大文件传输,文件未传输完,最近一次写时间是还没得到更新的,此时如果就触发Idle超时事件,显然是不能的。
关于keep alive与idle,就说这么多,看完后是否对学习Netty过程中遇到的keep alive与如何运用idle+心跳包实现应用层的keep alive更了解了呢?