因为最近项目最近要用netty,服务端放在云端,客户端发在内网。那如何实现netty长连接和断线重连呢(网络故障或者其他原因,客户端要无限取重连服务端)。接下来我们看一下如何实现这个两个功能呢。
服务端代码如下:
package com.example.nettydemo.demo;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
/**
* @ClassName: NettyServer
* @Author: huangzf
* @Date: 2018/9/25 15:40
* @Description:
*/
@Slf4j
public class NettyServer {
private NettyServerChannelInitializer serverChannelInitializer = null;
private int port = 8000;
public void bind() throws Exception {
//配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
serverChannelInitializer = new NettyServerChannelInitializer();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
//保持长连接
.childOption(ChannelOption.SO_KEEPALIVE,true)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(serverChannelInitializer);
//绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
//等待服务器监听端口关闭
f.channel().closeFuture().sync();
} finally {
//释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new NettyServer().bind();
}
}
服务端中NettyServerChannelInitializer的实现如下:
package com.example.nettydemo.demo;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: NettyServerChannelInitializer
* @Author: huangzf
* @Date: 2018/9/25 15:43
* @Description:
*/
public class NettyServerChannelInitializer extends ChannelInitializer {
private NettyServerHandler handler ;
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//解决TCP粘包拆包的问题,以特定的字符结尾($_)
pipeline.addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, Unpooled.copiedBuffer("$_".getBytes())));
//字符串解码和编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new IdleStateHandler(40,0,0,TimeUnit.SECONDS));
//服务器的逻辑
handler = new NettyServerHandler();
pipeline.addLast("handler", handler);
}
}
因为TCP在发送过程用可能会发生粘包拆包问题,netty中给了我们很好的解决方法,就是每次发送消息是已特殊的符号(可自定义)$_ 结尾,只收收到的消息以$_ 符号结尾是该消息才算接收完毕。
ipeline.addLast(new IdleStateHandler(40,0,0,TimeUnit.SECONDS)); 该代码实现了心跳检测,每隔40s检测一次是否要读事件,如果超过40s你没有读事件的发生,则执行相应的操作(在handler中实现)
服务端 NettyServerHandler 实现如下:
package com.example.nettydemo.demo;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
/**
* @ClassName: NettyServerHandler
* @Author: huangzf
* @Date: 2018/9/25 15:44
* @Description:
*/
@Slf4j
public class NettyServerHandler extends SimpleChannelInboundHandler {
/**
* 心跳丢失次数
*/
private int counter = 0;
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("Client say : " + msg.toString());
//重置心跳丢失次数
counter = 0;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("RemoteAddress : " + ctx.channel().remoteAddress().toString()+ " active !");
super.channelActive(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state().equals(IdleState.READER_IDLE)){
// 空闲40s之后触发 (心跳包丢失)
if (counter >= 3) {
// 连续丢失3个心跳包 (断开连接)
ctx.channel().close().sync();
log.error("已与"+ctx.channel().remoteAddress()+"断开连接");
System.out.println("已与"+ctx.channel().remoteAddress()+"断开连接");
} else {
counter++;
log.debug(ctx.channel().remoteAddress() + "丢失了第 " + counter + " 个心跳包");
System.out.println("丢失了第 " + counter + " 个心跳包");
}
}
}
}
}
userEventTriggered 该方法中定义了如果服务端40s内没有接收到客户端发来的消息,就将丢失次数嘉兴累加,如果累加超过3次也就是120s内都没有接收到客户端传来的消息,服务端将断开此客户端的连接。
接下来是客户端代码的实现:
package com.example.nettydemo.demo;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
/**
* @ClassName: NettyClinet
* @Author: huangzf
* @Date: 2018/9/25 15:26
* @Description:
*/
@Slf4j
public class NettyClinet {
@Value("${printer.server.host}")
private String host;
@Value("${printer.server.port}")
private int port;
private static Channel channel;
public NettyClinet(){
}
public NettyClinet(String host, int port) {
this.host = host;
this.port = port;
}
public void start() {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.option(ChannelOption.SO_KEEPALIVE,true)
.channel(NioSocketChannel.class)
.handler(new ClientChannelInitializer(host,port));
ChannelFuture f = b.connect(host,port);
//断线重连
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (!channelFuture.isSuccess()) {
final EventLoop loop = channelFuture.channel().eventLoop();
loop.schedule(new Runnable() {
@Override
public void run() {
log.error("服务端链接不上,开始重连操作...");
System.err.println("服务端链接不上,开始重连操作...");
start();
}
}, 1L, TimeUnit.SECONDS);
} else {
channel = channelFuture.channel();
log.info("服务端链接成功...");
System.err.println("服务端链接成功...");
}
}
});
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new NettyClinet ("127.0.0.1",8000).start();
}
}
断线重连实现也很简单,就是给通道加上一个断线重连的监听器ChannelFutureListene,该监听器如果监听到与服务端的连接断开了就会每隔1s触发一次重连操作,担忧一个问题需要注意的是 ChannelFuture f = b.connect(host,port);不能加sync()也就是不能写成 ChannelFuture f = b.connect(host,port).sync();不然重连操作无法触发,我也不知道为啥。。。。还有就是不能有任何关闭通道的代码,也就是group.shutdownGracefully(); 不然断线重连无效,因为你已经把该通道关闭了。
ClientChannelInitializer 该类代码实现如下:
package com.example.nettydemo.demo;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: ClientChannelInitializer
* @Author: huangzf
* @Date: 2018/9/25 15:29
* @Description:
*/
public class ClientChannelInitializer extends ChannelInitializer {
private String host;
private int port;
public ClientChannelInitializer( String host, int port) {
this.host = host;
this.port = port;
}
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//解决TCP粘包拆包的问题,以特定的字符结尾($_)
pipeline.addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, Unpooled.copiedBuffer("$_".getBytes())));
//字符串解码和编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
//心跳检测
pipeline.addLast(new IdleStateHandler(0,30,0,TimeUnit.SECONDS));
//客户端的逻辑
pipeline.addLast("handler", new NettyClientHandler(host,port));
}
}
跟服务端一样的逻辑出操作。。。
客户端 NettyClientHandler 实现如下:
package com.example.nettydemo.demo;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.EventLoop;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* @ClassName: NettyClientHandler
* @Author: huangzf
* @Date: 2018/9/25 15:33
* @Description:
*/
@Slf4j
public class NettyClientHandler extends SimpleChannelInboundHandler {
private String host;
private int port;
private NettyClinet nettyClinet;
private String tenantId;
public NettyClientHandler(String host, int port) {
this.host = host;
this.port = port;
nettyClinet = new NettyClinet(host,port);
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o)
throws Exception {
System.out.println("Server say : " + o.toString());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("通道已连接!!");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("断线了。。。。。。");
//使用过程中断线重连
final EventLoop eventLoop = ctx.channel().eventLoop();
eventLoop.schedule(new Runnable() {
@Override
public void run() {
nettyClinet.start();
}
}, 1, TimeUnit.SECONDS);
ctx.fireChannelInactive();
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state().equals(IdleState.READER_IDLE)) {
System.out.println("READER_IDLE");
} else if (event.state().equals(IdleState.WRITER_IDLE)) {
/**发送心跳,保持长连接*/
String s = "ping$_";
ctx.channel().writeAndFlush(s);
log.debug("心跳发送成功!");
System.out.println("心跳发送成功!");
} else if (event.state().equals(IdleState.ALL_IDLE)) {
System.out.println("ALL_IDLE");
}
}
super.userEventTriggered(ctx, evt);
}
}
channelInactive 该方法中也实现了断线重连的功能,以防止在运行过程中突然断线。
userEventTriggered:该方法中实现了如果30s内客户端没有向服务端写入任何消息,该方法就会触发向服务端发送心跳信息,从而保持客户端与服务端的长连接。
源码请移步码云
https://gitee.com/hzf1993/nettydemo/tree/master/heart-netty