閱讀本文建議從第一篇開始往後看
本系列文章
- Netty在Android開發中的應用實戰系列(一)——— 搭建服務端與客戶端
- Netty在Android開發中的應用實戰系列(二)——— Encoder | Decoder | Handler 的使用
- Netty在Android開發中的應用實戰系列(三)——— 心跳處理 | 斷線重連
- Netty在Android開發中的應用實戰系列(四)——— 粘包 | 拆包 處理
一、客戶端添加心跳處理
這裏就需要提到一個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
public void connect() {
try {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap 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();
}
}
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.connect();
}
}, 5, TimeUnit.SECONDS);
}
}
通過獲得EventLoop,然後創建一個循環任務
3.3 當把客戶端的網絡斷開然後在打開,然後發現瞭如下錯誤
爲什麼拋了個錯呢❌?原因當然是客戶端訪問不到服務端了,自然就拋了個錯誤;這樣也就導致了剛纔設置的5秒重連任務也就終止了。
那麼這種情況要怎麼處理呢?這種情況就需要用到ChannelFutureListener
,監聽連接狀態
3.4 在NettyClient
中爲連接添加一個ChannelFutureListener
監聽連接狀態,如下:
public void connect() {
try {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap 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();
}
}
//添加連接狀態監聽
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.connect();
}
}, 5, TimeUnit.SECONDS);
}
}
}
- 現在我們再來看下運行效果
- 可以看到程序一直在重連中達到了我們需要的效果,當再次打開網絡的時候就可以連接成功了
四、到這裏這篇文章的內容就說完了,下一篇將說下Netty的粘包和拆包這也是開發中經常遇見的問題
Demo將會在本系列文章中最後一篇給出