netty对keepalive和idle的支持

netty对keepalive和idle的支持

为什么需要keepalive

keepalive就是心跳,一个人的心跳证明人还活着,那么在网络通信的双方如何证明对端还活着着,两个服务之间使用心跳来检测对方是否还活着。

为什么要检测对方是否还活着呢?假如客户端因为某种原因(停电宕机、终止运行)没有发送关闭连接的数据包,那么服务器就会一直维持着连接,占用服务器资源,后面需要使用连接的时候还会报错。有了心跳,服务器就能及时释放资源。

TCP中的keepalive

TCP keepalive核心参数如下:

$ sysctl -a| grep tcp_keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200

TCP在连接没有数据通过后的7200s(tcp_keepalive_time)后会发送keepalive消息,当消息没有被确认后,按75s(tcp_keepalive_intvl)的频率重新发送,一直发送9(tcp_keepalive_probes)个探测包都没有被确认,就认定这个连接失效了。

由了TCP的keepalive,为什么还需要应用层的keepalive

  • TCP中的keepalive默认是关闭,因为探测包可能在传递过程中会丢失(例如用了代理)。
  • 默认的超时时间太长,默认是7200+9*75秒,也就是2个多小时。
  • TCP是一个传输层的协议,传输层的数据畅通并不一定操作系统进程所对应的服务畅通。

HTTP中的Keep-Alive是指在HTTP的请求头部携带参数Connection: Keep-Alive,这样浏览器与服务器端就会保持一个长连接,HTTP1.1协议默认是长连接,可以不用携带这个参数。

Idle检测

Idle是空闲的意思,也就是当客户端不向服务器端发送数据了,不会立马发送心跳包,会等待一段时间,判断这个连接空闲时才会发送。

keepalive的设计思路:

  1. 开启一个定时任务,不管客户端和服务器端有没有数据的传输,定时发送心跳包。
  2. 在连接通道中有数据传送的时候不发送心跳包,无数据传送超过一定时间判定为空闲时再发送。

netty中使用的是第二种。

netty中开启keepalive和idle

server端开启keepalive:

.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(NioChannelOption.SO_KEEPALIVE, true)

option(ChannelOption.SO_KEEPALIVE, true)存在但是设置无效。

server端开启idle:

ch.pipeline().addLast(new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));

IdleStateHandler参数说明:

  • readerIdleTime:读空闲时间,超过指定时间未读取数据就会触发IdleState.READER_IDLE事件。
  • writerIdleTime:写空闲时间,超过指定时间未发送数据就会触发IdleState.WRITER_IDLE事件。
  • allIdleTime:读或写空闲时间,超过指定时间未读取或者发送数据就会触发IdleState.ALL_IDLE事件。
  • unit:时间单位。

idle的使用

服务器端的代码实现:

package com.morris.netty.keepalive;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioChannelOption;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;

import java.net.StandardSocketOptions;
import java.util.concurrent.TimeUnit;

@Slf4j
public class Server {

    public static final int PORT = 8899;

    public static void main(String[] args) throws InterruptedException {

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LoggingHandler());
                            ch.pipeline().addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS));
                            ch.pipeline().addLast(new LineBasedFrameDecoder(1 << 10));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new StringEncoder());
                            ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                                @Override
                                protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                                    log.info("receive from client: {}", msg);
                                    ctx.writeAndFlush("ok\n");
                                }

                                @Override
                                public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                                    if(evt instanceof IdleStateEvent) {
                                        IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
                                        if(idleStateEvent.state() == IdleState.READER_IDLE) {
                                            // 60s未收到数据就会关闭连接
                                            log.warn("timeout: {}", ctx.channel().remoteAddress());
                                            ctx.channel().close();
                                        }
                                    } else {
                                        super.userEventTriggered(ctx, evt);
                                    }
                                }

                                @Override
                                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
                                    cause.printStackTrace();
                                    ctx.close();
                                }
                            });
                        }
                    });

            // 启动 server.
            ChannelFuture f = b.bind(PORT).sync();

            System.out.println("server is start on port: " + PORT);

            // 等待socket关闭
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

}

服务器端开启读空闲监测,60s未收到数据就会关闭连接。

客户端代码的实现:

package com.morris.netty.keepalive;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;

import java.sql.Time;
import java.util.concurrent.TimeUnit;

@Slf4j
public class Client {

    public static void main(String[] args) throws InterruptedException {

        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LoggingHandler());
                            ch.pipeline().addLast(new IdleStateHandler(0, 30, 0, TimeUnit.SECONDS));
                            ch.pipeline().addLast(new LineBasedFrameDecoder(1 << 10));
                            ch.pipeline().addLast(new StringEncoder());
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    ctx.writeAndFlush("hello\n");
                                }

                                @Override
                                public void channelRead0(ChannelHandlerContext ctx, String msg) {
                                    log.info("receive from server: {}", msg);
                                }

                                @Override
                                public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                                    if(evt instanceof IdleStateEvent) {
                                        IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
                                        if(idleStateEvent.state() == IdleState.WRITER_IDLE) {
                                            // 30s未写数据就会发送心跳
                                            ctx.writeAndFlush("hi\n");
                                        }
                                    } else {
                                        super.userEventTriggered(ctx, evt);
                                    }
                                }

                                @Override
                                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
                                    cause.printStackTrace();
                                    ctx.close();
                                }
                            });
                        }
                    });

            // 启动 server.
            ChannelFuture f = b.connect("127.0.0.1", 8899).sync();

            // 等待socket关闭
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }

}

客户端开启写空闲监测,30s未写数据就会发送心跳。

当然正常情况下本地测试上面的代码,服务器端是不会触发读空闲的事件,即使强制关闭了客户端,客户端也会发送关闭连接的请求给服务器,然后服务器端将连接关闭。

服务器端要想触发读空闲的事件,可以在使用两台机器或者虚拟机来测试,客户端启动后直接把网线拔出,如果是在linux下可以使用下面的命令关闭网络来测试:

# ifconfig ens32 down // 关闭网卡
# ifconfig ens32 up  // 开启网卡
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

morris131

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值