服務端如何得知客戶端的存活狀態?
服務端想知道客戶端是否掛掉,就必須間斷獲得客戶端相關的信息,這種每個一段時間,客戶端就會向服務端發送存活狀態的機制,我們成為心跳。當然如果客戶端正常向服務端發送請求時,并不需要再執行心跳請求,只在空閑時間發送心跳請求,這樣也就提高了發送效率。
客戶端如何得知服務端的存活狀態?
顯然我們不可能再像客戶端向服務端發送心跳請求一樣,發送給客戶端,畢竟在一個高并發的狀態,服務端還要做這種多余的操作,就顯得很力不從心了。那如何得知服務端掛掉了呢?netty為我們提供了一個接口ChannelInboundHandler
,并實現這個方法channelInactive
,當服務端掛掉,客戶端會異步調用這個方法。
參數介紹
netty為我們提供IdleStateHandler 類,可以實現對三種心跳的檢測,分別是readerIdleTime、writerIdleTime和allIdleTime。
- readerIdleTime:為讀超時時間(即測試端一定時間內未接受到被測試端消息);
- writerIdleTime:為寫超時時間(即測試端一定時間內未向被測試端發送消息)
- allIdleTime:所有類型的超時時間;
服務端搭建
Server
public class NettyServer {
private static final int port = 6789; //設置服務端端口
private static EventLoopGroup group = new NioEventLoopGroup(); // 通過nio方式來接收連接和處理連接
private static ServerBootstrap b = new ServerBootstrap();
/**
* Netty創建全部都是實現自AbstractBootstrap。
* 客戶端的是Bootstrap,服務端的則是 ServerBootstrap。
**/
public static void main(String[] args) throws InterruptedException {
try {
b.group(group);
b.channel(NioServerSocketChannel.class);
b.childHandler(new NettyServerFilter()); //設置過濾器
// 服務器綁定端口監聽
ChannelFuture f = b.bind(port).sync();
System.out.println("服務端啟動成功...");
// 監聽服務器關閉監聽
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully(); ////關閉EventLoopGroup,釋放掉所有資源包括創建的線程
}
}
}
ChannelInitializer
public class NettyServerFilter extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline ph = ch.pipeline();
// 以("\n")為結尾分割的 解碼器
// ph.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
// 解碼和編碼,應和客戶端一致
ph.addLast("decoder", new StringDecoder());
ph.addLast("encoder", new StringEncoder());
//1)readerIdleTime:為讀超時時間(即測試端一定時間內未接受到被測試端消息);
//2)writerIdleTime:為寫超時時間(即測試端一定時間內未向被測試端發送消息)
//3)allIdleTime:所有類型的超時時間;
ph.addLast(new IdleStateHandler(5,0,0));
ph.addLast("handler", new NettyServerHandler());// 服務端業務邏輯
}
}
ChannelInboundHandlerAdapter
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/** 空閑次數 */
private int idle_count =1;
/** 發送次數 */
private int count = 1;
/**
* 超時處理
* 如果5秒沒有接受客戶端的心跳,就觸發;
* 如果超過兩次,則直接關閉;
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) obj;
if (IdleState.READER_IDLE.equals(event.state())) { //如果讀通道處于空閑狀態,說明沒有接收到心跳命令
System.out.println("已經5秒沒有接收到客戶端的信息了");
if (idle_count > 2) {
System.out.println("關閉這個不活躍的channel");
ctx.channel().close();
}
idle_count++;
}
} else {
super.userEventTriggered(ctx, obj);
}
}
/**
* 業務邏輯處理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("第"+count+"次"+",服務端接受的消息:"+msg);
String message = (String) msg;
if ("hb_request".equals(message)) { //如果是心跳命令,則發送給客戶端;否則什么都不做
ctx.write("服務端成功收到心跳信息");
ctx.flush();
}
count++;
}
/**
* 異常處理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
客戶端
Client
public class NettyClient {
public static String host = "127.0.0.1"; //ip地址
public static int port = 6789; //端口
/// 通過nio方式來接收連接和處理連接
private static EventLoopGroup group = new NioEventLoopGroup();
private static Bootstrap b = new Bootstrap();
private static Channel ch;
/**
* Netty創建全部都是實現自AbstractBootstrap。
* 客戶端的是Bootstrap,服務端的則是 ServerBootstrap。
**/
public static void main(String[] args) throws InterruptedException, IOException {
System.out.println("客戶端成功啟動...");
b.group(group);
b.channel(NioSocketChannel.class);
b.handler(new NettyClientFilter());
// 連接服務端
ch = b.connect(host, port).sync().channel();
star();
}
public static void star() throws IOException{
String str="Hello Netty";
ch.writeAndFlush(str+ "\r\n");
System.out.println("客戶端發送數據:"+str);
}
}
ChannelInitializer
public class NettyClientFilter extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline ph = ch.pipeline();
/*
* 解碼和編碼,應和服務端一致
* */
// ph.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
ph.addLast("decoder", new StringDecoder());
ph.addLast("encoder", new StringEncoder());
ph.addLast( new IdleStateHandler(0, 4, 0, TimeUnit.SECONDS));
ph.addLast("handler", new NettyClientHandler()); //客戶端的邏輯
}
}
ChannelInboundHandlerAdapter
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/** 客戶端請求的心跳命令 */
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("hb_request",
CharsetUtil.UTF_8));
/** 空閑次數 */
private int idle_count = 1;
/** 發送次數 */
private int count = 1;
/**循環次數 */
private int fcount = 1;
/**
* 建立連接時
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("建立連接時:"+new Date());
ctx.fireChannelActive();
}
/**
* 關閉連接時
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("關閉連接時:"+new Date());
}
/**
* 心跳請求處理
* 每4秒發送一次心跳請求;
*
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
System.out.println("循環請求的時間:"+new Date()+",次數"+fcount);
if (obj instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) obj;
if (IdleState.WRITER_IDLE.equals(event.state())) { //如果寫通道處于空閑狀態,就發送心跳命令
if(idle_count <= 3){ //設置發送次數
idle_count++;
ctx.channel().writeAndFlush(HEARTBEAT_SEQUENCE.duplicate());
}else{
System.out.println("不再發送心跳請求了!");
}
fcount++;
}
}
}
/**
* 業務邏輯處理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("第"+count+"次"+",客戶端接受的消息:"+msg);
count++;
}
}
最終效果
服務端
image.png
客戶端
image.png
項目地址https://github.com/DespairYoke/netty/tree/master/netty-heartbeat