• Socket keepalive的配置本质是TCP的保活机制。
    • Socket是客户端和服务端建立的虚拟通信通道。客户端的Socket和服务端的Socket逻辑上是同一个。
    • Java Socket编程中有个keepalive选项不是用来表示长链接的。
    • socket 连接建立之后,只要双方均未主动关闭连接,那这个连接就是会一直保持的,就是持久的连接
      keepalive 只是为了防止连接的双方发生意外而通知不到对方,导致一方还持有连接,占用资源。

      java.net.Socket#setKeepAlive ```java public void setKeepAlive(boolean on) throws SocketException { if (isClosed())

      1. throw new SocketException("Socket is closed");

      getImpl().setOption(SocketOptions.SO_KEEPALIVE, Boolean.valueOf(on)); }

    public boolean getKeepAlive() throws SocketException { if (isClosed()) throw new SocketException(“Socket is closed”); return ((Boolean) getImpl().getOption(SocketOptions.SO_KEEPALIVE)).booleanValue(); }

    1. ```java
    2. public interface SocketOptions {
    3. /**
    4. * When the keepalive option is set for a TCP socket and no data
    5. * has been exchanged across the socket in either direction for
    6. * 2 hours (NOTE: the actual value is implementation dependent),
    7. * TCP automatically sends a keepalive probe to the peer. This probe is a
    8. * TCP segment to which the peer must respond.
    9. * One of three responses is expected:
    10. * 1. The peer responds with the expected ACK. The application is not
    11. * notified (since everything is OK). TCP will send another probe
    12. * following another 2 hours of inactivity.
    13. * 2. The peer responds with an RST, which tells the local TCP that
    14. * the peer host has crashed and rebooted. The socket is closed.
    15. * 3. There is no response from the peer. The socket is closed.
    16. *
    17. * The purpose of this option is to detect if the peer host crashes.
    18. *
    19. * Valid only for TCP socket: SocketImpl
    20. *
    21. * @see Socket#setKeepAlive
    22. * @see Socket#getKeepAlive
    23. */
    24. @Native public final static int SO_KEEPALIVE = 0x0008; //0x0008表示的是操作id
    25. }

    源码注释的意思是,如果这个连接上双方任意方向在2小时之内没有发送过数据,那么tcp会自动发送一个探测探测包给对方,这种探测包对方是必须回应的,回应结果有三种:

    1. 正常ACK,继续保持连接;
    2. 对方响应RST信号,双方重新连接。
    3. 对方无响应。

    这里说的两小时,其实是依赖于系统配置,在linux系统中(windows在注册表中,可以自行查询资料),tcp的keepalive参数。

    1. [root@JD1 ~]# sysctl -a|grep tcp_keepalive
    2. net.ipv4.tcp_keepalive_intvl = 75
    3. net.ipv4.tcp_keepalive_probes = 9
    4. net.ipv4.tcp_keepalive_time = 7200
    1. net.ipv4.tcp_keepalive_intvl = 75
      1. 发送探测包的周期,前提是当前连接一直没有数据交互,才会以该频率进行发送探测包,如果中途有数据交互,则会重新计时tcp_keepalive_time,到达规定时间没有数据交互,才会重新以该频率发送探测包
    2. net.ipv4.tcp_keepalive_probes = 9
      1. 探测失败的重试次数,发送探测包达次数限制对方依旧没有回应,则关闭自己这端的连接
    3. net.ipv4.tcp_keepalive_time = 7200
      1. 空闲多长时间,则发送探测包

    可以通过 sysctl -w net.ipv4.tcp_keepalive_time=60进行修改,执行sysctl -p刷新配置生效;
    可以通过修改/etc/sysctl.conf永久生效


    当建立TCP链接后,如果应用程序或者上层协议一直不发送数据,或者隔很长一段时间才发送数据,当链接很久没有数据报文传输时就需要通过keepalive机制去确定对方是否在线,链接是否需要继续保持。当超过一定时间没有发送数据时,TCP会自动发送一个数据为空的报文给对方,如果对方回应了报文,说明对方在线,链接可以继续保持,如果对方没有报文返回,则在重试一定次数之后认为链接丢失,就不会释放链接。

    控制对闲置连接的检测机制,链接闲置达到7200秒,就开始发送探测报文进行探测。

    net.ipv4.tcp_keepalive_time:单位秒,表示发送探测报文之前的链接空闲时间,默认为7200。
    net.ipv4.tcp_keepalive_intvl:单位秒,表示两次探测报文发送的时间间隔,默认为75。
    net.ipv4.tcp_keepalive_probes:表示探测的次数,默认9次。


    为了能验证所说的,我们来进行测试一下,本人测试环境是客户端在本地windows上,服务端是在远程linux上,主要测试服务器端向客户端发送探测包(客户端向服务端发送是一样的原理,这里测试服务器端到客户端的原因是我们修改了服务端的keep-alive便于观察)。

    1. 首先需要装一个抓包工具,本人用的wireshark;
    2. 然后修改一下tcp_keepalive_time系统配置,改成1分钟,2小时太长了,其余配置不变。

    修改方法:执行sysctl -w net.ipv4.tcp_keepalive_time=60进行修改,执行sysctl -p刷新配置生效;

    1. 最后写一个服务器端和一个客户端,分别启动。

    服务器端代码如下(java8):

    1. import java.io.IOException;
    2. import java.io.InputStream;
    3. import java.io.OutputStream;
    4. import java.net.ServerSocket;
    5. import java.net.Socket;
    6. public class Server {
    7. public static void main(String[] args) throws IOException {
    8. ServerSocket ss = new ServerSocket(12345);
    9. while (true) {
    10. //建立虚拟连接通道
    11. Socket socket = ss.accept();
    12. new Thread(() -> {
    13. try {
    14. //开启keppAlive机制
    15. socket.setKeepAlive(true);
    16. socket.setReceiveBufferSize(8 * 1024);
    17. socket.setSendBufferSize(8 * 1024);
    18. InputStream is = socket.getInputStream();
    19. OutputStream os = socket.getOutputStream();
    20. try {
    21. byte[] bytes = new byte[1024];
    22. while (is.read(bytes) > -1) {
    23. System.out.println(System.currentTimeMillis()
    24. + " received message: "
    25. + new String(bytes, "UTF-8").trim());
    26. os.write("ok".getBytes("UTF-8"));
    27. os.flush();
    28. bytes = new byte[1024];
    29. }
    30. } catch (IOException e) {
    31. e.printStackTrace();
    32. } finally {
    33. if (!socket.isInputShutdown()) {
    34. socket.shutdownInput();
    35. }
    36. if (!socket.isOutputShutdown()) {
    37. socket.shutdownOutput();
    38. }
    39. if (!socket.isClosed()) {
    40. socket.close();
    41. }
    42. }
    43. } catch (IOException e) {
    44. e.printStackTrace();
    45. }
    46. }).start();
    47. }
    48. }
    49. }

    客户端代码如下:

    1. public class Client {
    2. public static void main(String[] args) throws IOException, InterruptedException {
    3. Socket socket = new Socket("192.168.16.84", 12345);
    4. //开启tcp的keepAlive机制
    5. socket.setKeepAlive(true);
    6. socket.setSendBufferSize(8192);
    7. socket.setReceiveBufferSize(8192);
    8. InputStream is = socket.getInputStream();
    9. OutputStream os = socket.getOutputStream();
    10. os.write("get test-key".getBytes("UTF-8"));
    11. os.flush();
    12. Thread.sleep(155 * 1000L);
    13. os.write("get test-key".getBytes("UTF-8"));
    14. os.flush();
    15. byte[] bytes = new byte[1024];
    16. while (is.read(bytes) > -1) {
    17. System.out.println(System.currentTimeMillis()
    18. + " received message: "
    19. + new String(bytes, "UTF-8").trim());
    20. bytes = new byte[1024];
    21. }
    22. if (!socket.isOutputShutdown()) {
    23. socket.shutdownOutput();
    24. }
    25. if (!socket.isInputShutdown()) {
    26. socket.shutdownInput();
    27. }
    28. if (!socket.isClosed()) {
    29. socket.close();
    30. }
    31. }
    32. }

    分别启动服务端和客户端之后,抓包工具抓到的数据:
    image.png
    可以看到,60秒时服务器发送了探测包,探测客户端是否正常,客户端正常响应了,之后以tcp_keepalive_intvl(75秒)的周期进行发送,可以看到135秒又进行发送了探测包。

    但是因为我们客户端的代码是在155秒重新发送了数据,所以需要继续空闲60秒,直到215秒才继续发送探测包,后续没有数据交互,所以还是以75秒间隔频率进行发送探测包。从抓包的数据上很容易看出来。

    keepalive默认是关闭的,下面我们把服务器端的socket.setKeepAlive(true)一行注释掉的抓包结果:
    Socket keepalive - 图2
    可以看到服务器端没有向客户端发送探测包,其实客户端设置了socket.setKeepAlive(true),客户端在7355(7200+155)秒时应该会向服务器发送探测包(我把程序挂了2小时。。。结果如下)
    Socket keepalive - 图3
    验证无误。


    windows下的tcp keepalive
    缺省情况下,如果空闲连接在7200000毫秒(2小时)内没有活动,系统就会发送保持连接的消息。 具体操作:浏览至HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters 注册表子键,在Parameters子键下创建或修改名为KeepAliveTime的REG_DWORD值,为该值设置适当的毫秒数。


    socket 连接建立之后,只要双方均未主动关闭连接,那这个连接就是会一直保持的,就是持久的连接
    keepalive 只是为了防止连接的双方发生意外而通知不到对方,导致一方还持有连接,占用资源。

    其实这个选项的意思是TCP连接空闲时是否需要向对方发送探测包,实际上是依赖于底层的TCP模块实现的,java中只能设置是否开启,不能设置其详细参数,只能依赖于系统配置。

    keepalive 不是说TCP的长连接,当我们作为服务端,一个客户端连接上来,如果设置了keeplive为 true,当对方没有发送任何数据过来,超过一个时间(看系统内核参数配置),那么我们这边会发送一个ack探测包发到对方,探测双方的TCP/IP连接是否有效(对方可能断电,断网)。如果不设置,那么客户端宕机时,服务器永远也不知道客户端宕机了,仍然保存这个失效的连接。

    当然,在客户端也可以使用这个参数。客户端Socket会每隔段的时间(大约两个小时)就会利用空闲的连接向服务器发送一个数据包。这个数据包并没有其它的作用,只是为了检测一下服务器是否仍处于活动状态。如果服务器未响应这个数据包,在大约11分钟后,客户端Socket再发送一个数据包,如果在12分钟内,服务器还没响应,那么客户端Socket将关闭。如果将Socket选项关闭,客户端Socket在服务器无效的情况下可能会长时间不会关闭。