Android開發(fā)之使用Netty進(jìn)行Socket編程(一) 概括了一些
Socket
、NIO
的基本概念颇玷,下面正式介紹開發(fā)中使用到的Netty API以及在Android客戶端中如何使用Netty通過Socket與服務(wù)器交互就缆。
1 Channel
A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind.A channel provides a user:
- the current state of the channel (e.g. is it open? is it connected?),
- the configuration parameters of the channel (e.g. receive buffer size),
- the I/O operations that the channel supports (e.g. read, write, connect, and bind), and
- the ChannelPipeline which handles all I/O events and requests associated with the channel.
Java NIO中竭宰,Channel
的作用類似于Java IO的Stream
(實(shí)際上有所不同,參考上一篇文章)狞甚。文檔里也說的很清楚廓旬,在客戶端與服務(wù)端建立連接后孕豹,網(wǎng)絡(luò)IO操作是在Channel
對(duì)象上進(jìn)行的。
2 ChannelHandler
public interface ChannelHandler
Handles an I/O event or intercepts an I/O operation, and forwards it to its next handler in its ChannelPipeline
.
當(dāng)客戶端與服務(wù)器建立起連接后春霍,ChannelHandler
的方法是被網(wǎng)絡(luò)event(這里的event是廣義的)觸發(fā)的叶眉,由ChannelHandler
直接處理輸入輸出數(shù)據(jù),并傳遞到管道中的下一個(gè)ChannelHandler
中莲趣。
通過Channel
或者ChannelHandlerContext
發(fā)生的請(qǐng)求/響應(yīng)event 就是在管道中ChannelHandler
傳遞喧伞。
ChannelInboundHandler對(duì)從客戶端發(fā)往服務(wù)器的報(bào)文進(jìn)行處理,一般用來執(zhí)行解碼绿聘、讀取客戶端數(shù)據(jù)次舌、進(jìn)行業(yè)務(wù)處理等彼念;ChannelOutboundHandler對(duì)從服務(wù)器發(fā)往客戶端的報(bào)文進(jìn)行處理,一般用來進(jìn)行編碼哲思、發(fā)送報(bào)文到客戶端吩案。
一般就是繼承ChannelInboundHandlerAdapter
和ChannelOutboundHandlerAdapter
徘郭,因?yàn)?strong>Adapter把定制(custom) ChannelHandler的麻煩減小到了最低,Adapter本身已經(jīng)實(shí)現(xiàn)了基礎(chǔ)的數(shù)據(jù)處理邏輯(例如將event轉(zhuǎn)發(fā)到下一個(gè)handler)胧后,你可以只重寫那些你想要特別實(shí)現(xiàn)的方法抱环。
示例:
/**
* Handles a server-side channel.
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter{ // (1)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);// (3)
String body = new String(req, "UTF-8");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
具體使用:
- 一般做法是繼承
ChannelInboundHandlerAdapter
镇草,這個(gè)類實(shí)現(xiàn)了ChannelHandler
接口陶夜,ChannelHandler
提供了許多事件處理的接口方法,然后你可以覆蓋這些方法∏玻現(xiàn)在僅僅只需要繼承ChannelHandlerAdapter類而不是你自己去實(shí)現(xiàn)接口方法。 - 以上我們覆蓋了channelRead()事件處理方法肩袍。每當(dāng)從客戶端收到新的數(shù)據(jù)時(shí)婚惫,這個(gè)方法會(huì)在收到消息時(shí)被調(diào)用先舷,這個(gè)例子中,收到的消息的類型是ByteBuf牲芋。
- 和NIO一樣捺球,讀取數(shù)據(jù)時(shí)氮兵,它是直接讀到緩沖區(qū)中;在寫入數(shù)據(jù)時(shí)卜高,它也是寫入到緩沖區(qū)中秩霍。在TCP/IP中铃绒,NETTY會(huì)把讀到的數(shù)據(jù)放到ByteBuf的數(shù)據(jù)結(jié)構(gòu)中螺捐。所以這里讀取在ByteBuf的信息定血,得到服務(wù)器返回的內(nèi)容。
3 ChannelPipeline
public interface ChannelPipeline extends Iterable < Map.Entry < String , ChannelHandler >>
A list of ChannelHandler
s which handles or intercepts inbound events and outbound operations of a Channel
. ChannelPipeline
implements an advanced form of the Intercepting Filter pattern to give a user full control over how an event is handled and how the ChannelHandler
s in a pipeline interact with each other.
ChannelPipeline
作為放置ChannelHandler
的容器灾票,采用了J2EE的 攔截過濾模式刊苍,用戶可以定義管道中的ChannelHandler
以哪種規(guī)則去攔截并處理事件以及在管道中的ChannelHandler
之間如何通信。每個(gè)Channel都有它自己的Pipeline啥纸,當(dāng)一個(gè)新的Channel被創(chuàng)建時(shí)會(huì)自動(dòng)被分配到一個(gè)Pipeline中婴氮。
ChannelHandler
按如下步驟安裝到ChannelPipeline
中:
- 一個(gè)ChannelInitializer接口實(shí)現(xiàn)被注冊(cè)到一個(gè)Bootstrap上:
bootstrap.handler(new ChannelInitializer<SocketChannel>() {//(1)
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
pipeline.addLast(nettyClientHandler);//自定義的ChannelInboundHandlerAdapter子類
}
});
注意:上面編碼器(encoders)主经,解碼器(decoders)罩驻,和ChannelInboundHandlerAdapter的子類都屬于ChannelHandler。
- 當(dāng)
ChannelInitializer.initChannel()
被調(diào)用時(shí)迷扇,這個(gè)ChannelInitializer會(huì)往管道(pipeline)中安裝定制的一組ChannelHandler - 然后這個(gè)ChannelInitializer把自己從ChannelPipeline中移除
4 NioEventLoopGroup
MultithreadEventLoopGroup
implementations which is used for NIO Selector
based Channel
s.
NioEventLoopGroup
繼承了MultithreadEventLoopGroup
蜓席,是用來處理NIO操作的多線程事件循環(huán)器课锌,Netty提供了許多不同的EventLoopGroup的實(shí)現(xiàn)用來處理不同傳輸協(xié)議渺贤。
NioEventLoopGroup實(shí)際上就是個(gè)線程池,NioEventLoopGroup在后臺(tái)啟動(dòng)了n個(gè)IO線程(NioEventLoop)來處理Channel事件瞭亮,每一個(gè)NioEventLoop負(fù)責(zé)處理m個(gè)Channel统翩,NioEventLoopGroup從NioEventLoop數(shù)組里挨個(gè)取出NioEventLoop來處理Channel(詳見《NioEventLoopGroup繼承層次結(jié)構(gòu)》)此洲。
相比于服務(wù)端,客戶端只需要?jiǎng)?chuàng)建一個(gè)EventLoopGroup娶桦,因?yàn)樗恍枰?dú)立的線程去監(jiān)聽客戶端連接衷畦,而且Netty是異步事件驅(qū)動(dòng)的NIO框架,它的連接和所有IO操作都是異步的戴差,因此不需要?jiǎng)?chuàng)建單獨(dú)的連接線程铛嘱。
5 Bootstrap
Bootstrap
以及 ServerBootstrap
類都繼承自 AbstractBootstrap
墨吓。官方API文檔中的解釋是:
AbstractBootstrap
is a helper class that makes it easy to bootstrap a Channel
. It support method-chaining to provide an easy way to configure the AbstractBootstrap
.
Bootstrap中文翻譯就是引導(dǎo)程序帖烘,就是作為管理Channel的一個(gè)輔助類≌肇裕可以通過“方法鏈”的代碼形式(類似Builder模式)去配置一個(gè)Bootstrap乡摹,創(chuàng)建Channel并發(fā)起請(qǐng)求。Bootstrap類為一個(gè)應(yīng)用的網(wǎng)絡(luò)層配置提供了容器瞬痘,客戶端通過它來jianjie框全。
示例:
EventLoopGroup group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class)
.group(group);
.handler(new ChannelInitializer<SocketChannel>() {//(1)
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
pipeline.addLast(nettyClientHandler);//(2)
}
});
-
ChannelInitializer是一個(gè)特殊的
ChannelHandler
類津辩,作用是幫助使用者配置一個(gè)新的Channel筒严。也許你想通過增加一些新的ChannelHandler子類來操作一個(gè)新的Channel或者通過其對(duì)應(yīng)的ChannelPipeline來實(shí)現(xiàn)你的網(wǎng)絡(luò)程序鸭蛙。當(dāng)你的程序變的復(fù)雜時(shí)筋岛,可能會(huì)增加更多的ChannelHandler子類到pipeline上。 - 每個(gè)Channel都有ChannelPipeline,在Channel的pipeline中加入handler肪获,這里的ChannelHandler類經(jīng)常會(huì)被用來處理一個(gè)最近的已經(jīng)接收的Channel寝凌。所以這里的Channel已經(jīng)不是NIO中的Channel了,她是netty的Channel孝赫。
6 建立連接并發(fā)起請(qǐng)求
public class NettyClient {
private Channel channel ;
public void connect(int port,String host){
EventLoopGroup group = new NioEventLoopGroup();
try {//配置Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
pipeline.addLast(new NettyClientHandler () );
}
});
//發(fā)起異步連接操作
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
channel = channelFuture.channel();
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//關(guān)閉较木,釋放線程資源
group.shutdownGracefully();
}
}
public void sendMessage(String msg){//連接成功后,通過Channel提供的接口進(jìn)行IO操作
try {
if (channel != null && channel.isOpen()) {
channel.writeAndFlush(sendMsg).sync(); //(1)
Log.d(TAG, "send succeed " + sendMsg);
} else {
throw new Exception("channel is null | closed");
}
} catch (Exception e) {
sendReconnectMessage();
e.printStackTrace();
}
}
@Test
public void nettyClient(){
new NettyClient().connect(8989, "localhost");
}
}
- Channel 及ChannelHandlerContext對(duì)象提供了許多操作青柄,能夠觸發(fā)各種各樣的I/O事件和操作伐债。
write(Object)方法不會(huì)使消息寫入到Channel上致开,他被緩沖在了內(nèi)部峰锁,你需要調(diào)用flush()方法來把緩沖區(qū)中數(shù)據(jù)強(qiáng)行輸出∷粒或者可以用更簡(jiǎn)潔的writeAndFlush(msg)以達(dá)到同樣的目的虹蒋。
7 總結(jié)
簡(jiǎn)而言之,在客戶端上使用Netty飒货,業(yè)務(wù)流程如下:
- 構(gòu)建Bootstrap魄衅,其中包括設(shè)置好ChannelHandler來處理將來接收到的數(shù)據(jù)。
- 由Boostrap發(fā)起連接塘辅。
- 連接成功建立后徐绑,得到一個(gè)ChannelFuture對(duì)象,代表了一個(gè)還沒有發(fā)生的I/O操作莫辨。這意味著任何一個(gè)請(qǐng)求操作都不會(huì)馬上被執(zhí)行傲茄,因?yàn)樵贜etty里所有的操作都是異步的。
- 通過Channel對(duì)象的
writeAndFlush(Object msg)
方法往服務(wù)端發(fā)送數(shù)據(jù)沮榜,接收到的數(shù)據(jù) 會(huì)在ChannelHandler的實(shí)現(xiàn)類中的channelRead(ChannelHandlerContext ctx, Object msg)
中獲取到被讀到緩沖區(qū)的數(shù)據(jù)——(ByteBuf) msg
盘榨。
8 問題
- TCP連接中,NETTY會(huì)把讀到的數(shù)據(jù)放到ByteBuf的數(shù)據(jù)結(jié)構(gòu)中蟆融〔菅玻基于流的傳輸并不是一個(gè)數(shù)據(jù)包隊(duì)列,而是一個(gè)字節(jié)隊(duì)列型酥。即使服務(wù)器發(fā)送了2個(gè)獨(dú)立的消息山憨,客戶端也不會(huì)作為2次消息處理而僅僅是作為一連串的字節(jié)進(jìn)行讀取。因此這是不能保證你遠(yuǎn)程寫入的數(shù)據(jù)就會(huì)準(zhǔn)確地讀取弥喉。尤其是 服務(wù)器發(fā)回的消息長(zhǎng)度 過長(zhǎng)的時(shí)候郁竟,一次消息將有可能被拆分到不同的ByteBuf數(shù)據(jù)段中,很有可能需要多次讀取ByteBuf由境,才能把一個(gè)消息完整拿到棚亩。
解決方案:
ByteToMessageDecoder是ChannelHandler的一個(gè)實(shí)現(xiàn)類蓖议,他可以在處理數(shù)據(jù)拆分的問題上變得很簡(jiǎn)單。