引用netty介紹文檔中的一句話:Netty適合用來開發高性能長連接的客戶端或服務器
其次netty基于NIO,并做了封裝與優化以及一些高級算法,使得使用起來相比原生NIO更加容易,性能更好。
timg.jpg
1.使用方法
- 1.1建立連接
//進行初始化
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(); //初始化線程組
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class).group(nioEventLoopGroup);
bootstrap.option(ChannelOption.TCP_NODELAY, true); //無阻塞
bootstrap.option(ChannelOption.SO_KEEPALIVE, true); //長連接
bootstrap.option(ChannelOption.SO_TIMEOUT, SLEEP_TIME); //收發超時
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ByteArrayDecoder()) //接收解碼方式
.addLast(new ByteArrayEncoder()) //發送編碼方式
.addLast(new ChannelHandle(ConnectService.this)); //處理數據接收
}
});
//開始建立連接并監聽返回
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(AppInfo.serverIp, AppInfo.serverPort));
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
AppInfo.isConnected = future.isSuccess();
if (future.isSuccess()) {
Log.d(TAG, "connect success !");
} else {
Log.d(TAG, "connect failed !");
}
}
});
- 1.2數據發送
private void sendDataToServer(byte[] sendBytes) {
if (sendBytes != null && sendBytes.length > 0) {
if (channelFuture != null && channelFuture.channel().isActive()) {
channelFuture.channel().writeAndFlush(sendBytes);
}
}
}
- 1.3實現數據接收類:
public static class ChannelHandle extends SimpleChannelInboundHandler<SimpleProtocol> {
private ConnectService connectService;
public ChannelHandle(ConnectService connectService) {
this.connectService = connectService;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
Log.e(TAG, "channelInactive 連接失敗");
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
if (evt instanceof IdleStateEvent) {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state().equals(IdleState.WRITER_IDLE)) {
Log.d(TAG, "userEventTriggered write idle");
if (connectService == null){
return;
}
}else if (idleStateEvent.state().equals(IdleState.READER_IDLE)){
Log.d(TAG, "userEventTriggered read idle");
ctx.channel().close();
}
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, byte[] data) throws Exception {
if (simpleProtocol == null){
return;
}
//處理接收到的數據data
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
Log.i(TAG, "exceptionCaught");
cause.printStackTrace();
ctx.close();
}
}
2.常見問題處理及通信優化
-
2.1心跳
在初始化時添加:.addLast(new IdleStateHandler(30, 10, 0)) //參數1:代表讀套接字超時的時間,例如30秒沒收到數據會觸發讀超時回調;參數2:代表寫套接字超時時間,例如10秒沒有進行寫會觸發寫超時回調;參數3:將在未執行讀取或寫入時觸發超時回調,0代表不處理;讀超時盡量設置大于寫超時代表多次寫超時時寫心跳包,多次寫了心跳數據仍然讀超時代表當前連接錯誤,即可斷開連接重新連接
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ByteArrayDecoder()) //接收解碼方式
.addLast(new ByteArrayEncoder()) //發送編碼方式
.addLast(new ChannelHandle(ConnectService.this)); //處理數據接收
.addLast(new IdleStateHandler(30, 10, 0))
}
});
在添加初始化.addLast(new ChannelHandle(ConnectService.this));
的ChannelHandle 類中重寫超時回調方法
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
if (evt instanceof IdleStateEvent) {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state().equals(IdleState.WRITER_IDLE)) {
//寫超時,此時可以發送心跳數據給服務器
Log.d(TAG, "userEventTriggered write idle");
if (connectService == null){
return;
}
}else if (idleStateEvent.state().equals(IdleState.READER_IDLE)){
//讀超時,此時代表沒有收到心跳返回可以關閉當前連接進行重連
Log.d(TAG, "userEventTriggered read idle");
ctx.channel().close();
}
}
}
- 2.2重連接
private void reConnect(){
if (channelFuture != null && channelFuture.channel() != null && channelFuture.channel().isActive()){
channelFuture.channel().close();//已經連接時先關閉當前連接,關閉時回調exceptionCaught進行重新連接
}else {
connect(); //當前未連接,直接連接即可
}
}
連接失敗回調
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
Log.e(TAG, "channelInactive 連接失敗");
if (connectService != null) {
connectService.connect(); //重新連接
}
}
連接錯誤回調
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
Log.i(TAG, "exceptionCaught");
cause.printStackTrace();
ctx.close(); //關閉連接后回調channelInactive會重新調用connectService.connect();
}
-
2.3數據接收不全
出現問題的原因1:接收數據的緩沖區太小 解決辦法:
bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(5000, 5000, 8000)); //接收緩沖區 最小值太小時數據接收不全
出現問題的原因2:沒有處理粘包拆包問題 解決方法:
添加包的接收處理:addLast(new SimpleProtocolDecoder())
解包處理避免粘包的主要步驟是:1.解協議頭 2.解包長 3.讀指定的包長后返回
public class SimpleProtocolDecoder extends ByteToMessageDecoder{
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
if (buffer.readableBytes() >= SimpleProtocol.BASE_LENGTH){
int beginReader;
while (true){
beginReader = buffer.readerIndex();
buffer.markReaderIndex();
if (buffer.readShort() == AppInfo.HEAD_TAG){
buffer.readByte();
break;
}
buffer.resetReaderIndex();
buffer.readByte();
if (buffer.readableBytes() < SimpleProtocol.BASE_LENGTH){
return;
}
}
int length = MyUtil.shortToBig(buffer.readShort()) - SimpleProtocol.BASE_LENGTH;
//buffer.readerIndex(beginReader);
//Log.e("TAG","length:"+buffer.readableBytes());
if (buffer.readableBytes() < length){
buffer.readerIndex(beginReader);
return;
}
//byte[] data = new byte[length];
byte[] data = new byte[length + SimpleProtocol.BASE_LENGTH];
int pos = buffer.readerIndex() - SimpleProtocol.BASE_LENGTH;
if (pos < 0){
return;
}
buffer.readerIndex(pos);
buffer.readBytes(data);
SimpleProtocol simpleProtocol = new SimpleProtocol((short) data.length,data);
out.add(simpleProtocol);
}
}
}
3.注意細節
- 把連接,讀寫都放到service中,模塊封裝方便各個組件調用
- 每次連接時使用線程池,避免ANR,節省開銷
- 重新連接時不要對netty重復初始化,同時可以使用handler控制重連間隔避免短時間內死循環重復調用
- 重連時close當前channel的同時需要調用以下代碼:
if (channelFuture != null && channelFutureListener != null){
channelFuture.removeListener(channelFutureListener);
channelFuture.cancel(true);
}