阅读本文建议从第一篇开始往后看
本系列文章
- Netty在Android开发中的应用实战系列(一)——— 搭建服务端与客户端
- Netty在Android开发中的应用实战系列(二)——— Encoder | Decoder | Handler 的使用
- Netty在Android开发中的应用实战系列(三)——— 心跳处理 | 断线重连
- Netty在Android开发中的应用实战系列(四)——— 粘包 | 拆包 处理
- Netty在Android开发中的应用实战系列(五)——— 创建Web服务 | 作为HTTP服务器
一、客户端添加心跳处理
这里就需要提到一个netty重要的
IdleStateHandler
,用于处理心跳机制;为当前连接通道设置 读、写、读写 空闲超时时间,当达到了设定的时间那么就会回调ClientHandler
中的userEventTriggered(ChannelHandlerContext ctx, Object evt)
函数。
这里在解释一下什么叫空闲超时:假设你设置了客户端读超时为10s,如果持续10s内客户端没有收到数据(也就是服务端没有发送数据过来)那么就会回调
userEventTriggered
函数,如果有一直收到数据那么就不会回调userEventTriggered
函数;直到持续10s没有收到数据则继续触发userEventTriggered
函数。
IdleStateHandler()
常用的构造函数,三个参数的释义
public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
}
- readerIdleTimeSeconds 空闲读的时长
- writerIdleTimeSeconds 空闲写的时长
- writerIdleTimeSeconds 空闲读写的时长
IdleStateHandler的使用
- 与之前添加编解码器一样,往
ChannelPipeline
添加即可
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap()
// 省略部分代码
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//添加心跳处理Handler
pipeline.addLast(new IdleStateHandler(10, 0, 0));
//省略部分代码
}
});
// 连接到服务端
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
ClientHandler
中进行心跳数据包发送
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
if (evt instanceof IdleStateEvent) {
if (((IdleStateEvent) evt).state() == IdleState.READER_IDLE) {
sendHeartPkg(ctx);
}
} else {
super.userEventTriggered(ctx, evt);
}
}
/**
* 发送心跳
*/
private void sendHeartPkg(ChannelHandlerContext ctx) {
PkgDataBean bean = new PkgDataBean();
bean.setCmd((byte) 0x02);
bean.setData("心跳数据包");
bean.setDataLength((byte) bean.getData().getBytes().length);
ctx.channel().writeAndFlush(bean);
Log.d(TAG, "客户端发送心跳成功");
}
- 客户端运行的效果
- 服务端运行的效果
二、服务端收到了客户端的心跳,也是需要回应客户端;服务端之需要在ServerHandler
中稍微修改一下即可
/**
* 当收到数据的回调
*
* @param ctx 封装的连接对像
* @param bean
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, PkgDataBean bean) throws Exception {
switch (bean.getCmd()) {
case 0x02:
//响应客户端心跳
bean.setCmd((byte) 0x03);
ctx.channel().writeAndFlush(bean);
break;
default:
break;
}
Log.d(TAG, "收到了解码器处理过的数据:" + bean.toString());
}
- 我们将收到的数据包,修改下命令字节为
0x03
然后再发送回给客户端
有了心跳机制就可以很好的判断连接是否是通畅,是否可以正常收发数据了
三、断线重连处理
当我们的网络不稳定,或者服务端一直未响应心跳、又或者切换网络导致连接中断;这个是就需要让它自动重连服务端,恢复链接
3.1 首先需要修改下NettyClient
的连接代码,将NettyClient实例传递给ClientHandler
//...
private Bootstrap bootstrap;
public void connect() {
try {
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap = new Bootstrap()
// 指定channel类型
.channel(NioSocketChannel.class)
// 指定EventLoopGroup
.group(group)
// 指定Handler
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new IdleStateHandler(10, 0, 0));
//添加发送数据编码器
pipeline.addLast(new ClientEncoder());
//添加数据处理器
pipeline.addLast(new ClientHandler(NettyClient.this));
}
});
// 连接到服务端
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
//获取连接通道
channel = channelFuture.sync().channel();
handler.obtainMessage(0, "连接成功").sendToTarget();
} catch (Exception e) {
handler.obtainMessage(0, "连接失败").sendToTarget();
Log.e(TAG, "连接失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 重连
*/
public void reConnect() {
try {
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
channel = channelFuture.sync().channel();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.2 客户端与服务端连接断开了,那么会回调我们Handler
中的channelInactive
函数;所以只需要在这断开的回调启动重连任务就可以
public class ClientHandler extends SimpleChannelInboundHandler<Object> {
private static final String TAG = "ClientHandler";
private NettyClient client;
public ClientHandler(NettyClient nettyClient) {
this.client = nettyClient;
}
/**
* 与服务端断开的回调
*
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
Log.d(TAG, "与服务端断开连接:" + ctx.toString());
//启动重连
reConnect(ctx);
}
/**
* 5s重连一次服务端
*/
private void reConnect(final ChannelHandlerContext ctx) {
EventLoop loop = ctx.channel().eventLoop();
loop.schedule(new Runnable() {
@Override
public void run() {
Log.d(TAG, "连接断开,发起重连");
client.reConnect();
}
}, 5, TimeUnit.SECONDS);
}
}
通过获得EventLoop,然后创建一个循环任务
3.3 当把客户端的网络断开然后在打开,然后发现了如下错误
为什么抛了个错呢❌?原因当然是客户端访问不到服务端了,自然就抛了个错误;这样也就导致了刚才设置的5秒重连任务也就终止了。
那么这种情况要怎么处理呢?这种情况就需要用到ChannelFutureListener
,监听连接状态
3.4 在NettyClient
中为连接添加一个ChannelFutureListener
监听连接状态,如下:
//...
private Bootstrap bootstrap;
public void connect() {
try {
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap = new Bootstrap()
// 指定channel类型
.channel(NioSocketChannel.class)
// 指定EventLoopGroup
.group(group)
// 指定Handler
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new IdleStateHandler(10, 0, 0));
//添加发送数据编码器
pipeline.addLast(new ClientEncoder());
//添加数据处理器
pipeline.addLast(new ClientHandler(NettyClient.this));
}
});
// 连接到服务端
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
// 添加连接状态监听
channelFuture.addListener(new ConnectListener(this));
//获取连接通道
channel = channelFuture.sync().channel();
handler.obtainMessage(0, "连接成功").sendToTarget();
} catch (Exception e) {
handler.obtainMessage(0, "连接失败").sendToTarget();
Log.e(TAG, "连接失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 重连
*/
public void reConnect() {
try {
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(IP, PORT));
// 添加连接状态监听
channelFuture.addListener(new ConnectListener(this));
channel = channelFuture.sync().channel();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//添加连接状态监听
channelFuture.addListener(new ConnectListener(this));
- ConnectListener.java实现ChannelFutureListener来获取连接状态
public class ConnectListener implements ChannelFutureListener {
private static final String TAG = "ConnectListener";
private NettyClient nettyClient;
public ConnectListener(NettyClient nettyClient) {
this.nettyClient = nettyClient;
}
@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.d(TAG, "连接失败,发起重连");
nettyClient.reConnect();
}
}, 5, TimeUnit.SECONDS);
}
}
}
- 现在我们再来看下运行效果
- 可以看到程序一直在重连中达到了我们需要的效果,当再次打开网络的时候就可以连接成功了
四、到这里这篇文章的内容就说完了,下一篇将说下Netty的粘包和拆包这也是开发中经常遇见的问题
Demo将会在本系列第四篇文章中给出