Android开发之使用Netty进行Socket编程(二)

Android开发之使用Netty进行Socket编程(一) 概括了一些SocketNIO的基本概念,下面正式介绍开发中使用到的Netty API以及在Android客户端中如何使用Netty通过Socket与服务器交互。

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:

  1. the current state of the channel (e.g. is it open? is it connected?),
  2. the configuration parameters of the channel (e.g. receive buffer size),
  3. the I/O operations that the channel supports (e.g. read, write, connect, and bind), and
  4. the ChannelPipeline which handles all I/O events and requests associated with the channel.

Java NIO中,Channel的作用类似于Java IO的Stream(实际上有所不同,参考上一篇文章)。文档里也说的很清楚,在客户端与服务端建立连接后,网络IO操作是在Channel对象上进行的。

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
.

当客户端与服务器建立起连接后,ChannelHandler的方法是被网络event(这里的event是广义的)触发的,由ChannelHandler直接处理输入输出数据,并传递到管道中的下一个ChannelHandler中。
通过Channel或者ChannelHandlerContext发生的请求/响应event 就是在管道中ChannelHandler传递。

ChannelInboundHandler对从客户端发往服务器的报文进行处理,一般用来执行解码、读取客户端数据、进行业务处理等;ChannelOutboundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码、发送报文到客户端。
一般就是继承ChannelInboundHandlerAdapterChannelOutboundHandlerAdapter,因为Adapter把定制(custom) ChannelHandler的麻烦减小到了最低,Adapter本身已经实现了基础的数据处理逻辑(例如将event转发到下一个handler),你可以只重写那些你想要特别实现的方法。

示例:

/**
 * 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();
    }
}

具体使用:

  1. 一般做法是继承ChannelInboundHandlerAdapter,这个类实现了ChannelHandler接口,ChannelHandler提供了许多事件处理的接口方法,然后你可以覆盖这些方法。现在仅仅只需要继承ChannelHandlerAdapter类而不是你自己去实现接口方法。
  2. 以上我们覆盖了channelRead()事件处理方法。每当从客户端收到新的数据时,这个方法会在收到消息时被调用,这个例子中,收到的消息的类型是ByteBuf
  3. 和NIO一样,读取数据时,它是直接读到缓冲区中;在写入数据时,它也是写入到缓冲区中。在TCP/IP中,NETTY会把读到的数据放到ByteBuf的数据结构中。所以这里读取在ByteBuf的信息,得到服务器返回的内容。

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以哪种规则去拦截并处理事件以及在管道中的ChannelHandler之间如何通信。每个Channel都有它自己的Pipeline,当一个新的Channel被创建时会自动被分配到一个Pipeline中。
ChannelHandler按如下步骤安装到ChannelPipeline中:

  1. 一个ChannelInitializer接口实现被注册到一个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

  1. ChannelInitializer.initChannel()被调用时,这个ChannelInitializer会往管道(pipeline)中安装定制的一组ChannelHandler
  2. 然后这个ChannelInitializer把自己从ChannelPipeline中移除

4 NioEventLoopGroup

MultithreadEventLoopGroup
implementations which is used for NIO Selector
based Channel
s.

NioEventLoopGroup继承了MultithreadEventLoopGroup,是用来处理NIO操作的多线程事件循环器,Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。
NioEventLoopGroup实际上就是个线程池,NioEventLoopGroup在后台启动了n个IO线程(NioEventLoop)来处理Channel事件,每一个NioEventLoop负责处理m个Channel,NioEventLoopGroup从NioEventLoop数组里挨个取出NioEventLoop来处理Channel(详见《NioEventLoopGroup继承层次结构》)
相比于服务端,客户端只需要创建一个EventLoopGroup,因为它不需要独立的线程去监听客户端连接,而且Netty是异步事件驱动的NIO框架,它的连接和所有IO操作都是异步的,因此不需要创建单独的连接线程。

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中文翻译就是引导程序,就是作为管理Channel的一个辅助类。可以通过“方法链”的代码形式(类似Builder模式)去配置一个Bootstrap,创建Channel并发起请求。Bootstrap类为一个应用的网络层配置提供了容器,客户端通过它来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)
                        }
                    });
  1. ChannelInitializer是一个特殊的ChannelHandler类,作用是帮助使用者配置一个新的Channel。也许你想通过增加一些新的ChannelHandler子类来操作一个新的Channel或者通过其对应的ChannelPipeline来实现你的网络程序。当你的程序变的复杂时,可能会增加更多的ChannelHandler子类到pipeline上。
  2. 每个Channel都有ChannelPipeline,在Channel的pipeline中加入handler,这里的ChannelHandler类经常会被用来处理一个最近的已经接收的Channel。所以这里的Channel已经不是NIO中的Channel了,她是netty的Channel。

6 建立连接并发起请求

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 () );
                }
            });

            //发起异步连接操作
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            
             channel = channelFuture.channel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            //关闭,释放线程资源
            group.shutdownGracefully();
        }
    }


  public void sendMessage(String msg){//连接成功后,通过Channel提供的接口进行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");
    }
    
}
  1. Channel 及ChannelHandlerContext对象提供了许多操作,能够触发各种各样的I/O事件和操作。
    write(Object)方法不会使消息写入到Channel上,他被缓冲在了内部,你需要调用flush()方法来把缓冲区中数据强行输出。或者可以用更简洁的writeAndFlush(msg)以达到同样的目的。

7 总结

简而言之,在客户端上使用Netty,业务流程如下:

  1. 构建Bootstrap,其中包括设置好ChannelHandler来处理将来接收到的数据。
  2. 由Boostrap发起连接。
  3. 连接成功建立后,得到一个ChannelFuture对象,代表了一个还没有发生的I/O操作。这意味着任何一个请求操作都不会马上被执行,因为在Netty里所有的操作都是异步的。
  4. 通过Channel对象的writeAndFlush(Object msg)方法往服务端发送数据,接收到的数据 会在ChannelHandler的实现类中的channelRead(ChannelHandlerContext ctx, Object msg)中获取到被读到缓冲区的数据——(ByteBuf) msg

8 问题

  1. TCP连接中,NETTY会把读到的数据放到ByteBuf的数据结构中。基于流的传输并不是一个数据包队列,而是一个字节队列。即使服务器发送了2个独立的消息,客户端也不会作为2次消息处理而仅仅是作为一连串的字节进行读取。因此这是不能保证你远程写入的数据就会准确地读取。尤其是 服务器发回的消息长度 过长的时候,一次消息将有可能被拆分到不同的ByteBuf数据段中,很有可能需要多次读取ByteBuf,才能把一个消息完整拿到。
    解决方案:
    ByteToMessageDecoderChannelHandler的一个实现类,他可以在处理数据拆分的问题上变得很简单。

相关链接

  1. Netty 5用户指南
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容