Netty框架浅析--内附Android实现demo

一、Netty框架简介

(本文中部分图片摘自Netty-In-Depth)
Netty是一款以异步事件为驱动的网络开发框架和工具,能够快速的帮助开发者开发出可维护的高性能,高扩张性的服务器和客户端。

二、Netty相较于其他I/O编程的优点

1、BIO编程

在基于传统同步阻塞模型开发中,ServerSocket 负责绑定 IP 地址,启动监听端口;Socket 负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。server端为每一个连上来的client端新建一个线程进行链路处理,处理完成之后通过输出流返回应答到客户端,然后线程销毁。也就是典型的一请求一应答通信模型。但是这种模型在连接的客户端数量庞大的时候,相应的服务端线程数量也会剧增,这就会使服务端因为线程数量过多而宕机。


BIO编程(摘自Netty-In-Depth)

2、伪异步IO编程

伪异步IO线程其实就是在BIO编程基础上增加了线程池,将处理客户端连接请求的操作交给线程池去处理,这样线程数量就处于可控状态,可以有效的防止线程耗尽,但是这种模型会出现通信时间过长导致级联故障:比如服务端处理时间过长,或者其他线程出现故障,由于IO操作是阻塞的,因此假如当前所有可用线程都被阻塞了,那么后续的所有连接都会在队列中排队等待,当队列达到最大可容纳数量时,后续入队列操作会被阻塞。这样acceptor因为阻塞在线程池的队列中,所以无法处理后续客户端的连接请求,出现大量的连接超时。


伪异步IO(摘自Netty-In-Depth)

3、NIO编程

前面两种编程模型出现的问题其实还是在于IO操作是同步阻塞的,所以要解决这些问题,最好的办法就是从“同步阻塞”这方面入手,因此JAVA提供了NIO类库,其实就是让JAVA支持非阻塞IO,与传统BIO编程中的Socket连接方式来说,我们通过Socket跟ServerSocket来进行连接、监听端口、获取输入输出流等操作,而与之对应的,NIO提供了SocketChannel和ServerSocketChannel,我们可以称其为“通道”,它们支持阻塞跟非阻塞两种模式,阻塞方式会出现上面我们提到过的问题,而非阻塞模式则可以大大提高性能。一般来说,低负载、低并发的应用程序可以选择同步阻塞 I/O 以降低编程复杂度,但是对于高负载、高并发的网络应用,需要使用 NIO 的非阻塞模式进行开发。

4、AIO编程

NIO编程虽然是非阻塞的,但是他依然采用的是同步IO(多路复用)。也就是说需要通过一个多路复用器(Selector)对注册的通道进行轮询操作,这对性能也会有所影响,所以便有了NIO2.0,它引入了异步通道的概念,提供了异步文件通道和异步套接字通道的实现。NIO2.0是异步非阻塞IO,不需要通过多路复用器(Selector)来对注册的通道进行轮询操作即可实现异步读写。

IO模型对比(摘自Netty-In-Depth)

刚刚有提到异步IO是异步非阻塞的,它与阻塞、非阻塞、多路复用等IO的区别可以参考http://blog.csdn.net/zhangzeyuaaa/article/details/42609723 简单来说异步非阻塞就是应用发起读写请求之后就交由系统去处理,等操作完成之后,系统会通过回调来通知应用操作结果。

三、Netty架构

Netty架构(摘自Netty-In-Depth)

Reactor层的职责主要是负责监听网络读写、客户端连接等事件,将网络数据读到内存缓存中,上层可通过ByteBuf类读取数据,Reactor还负责触发事件,产生的事件交由Pipeline处理。
Pipeline层是基于责任链模式实现的,用户定制的各种Handler组成一个链式结构由Pipeline管理,当事件触发时,Pipeline寻找最接近的Handler并执行,处理完后继续将事件传给下一个Handler处理。如下图所示:

ChannelPipeline.png

以下内容根据官网的描述翻译:
一个Inbound事件交由InboundHandler类处理,方向为自底向上,流入的数据通常是通过实际的输入操作从服务端读取,如SocketChannel.read(ByteBuffer)
。当一个Inbound事件流到最顶层的InboundHandler后将会被废弃或者被记录下来(当你需要的时候)。
一个Outbound事件由OutboundHandler类处理,处理方向为由上至下,一个OutboundHandler通常会生成或转换Outbound数据流,如write请求。如果Outbound事件流过最底部的OutboundHandler,它将会交给关联了一个Channel的I/O线程处理,I/O线程通常会执行实际的输出操作如SocketChannel.write(ByteBuffer)

例如,假设我们创建如下的ChannelPipeline:
ChannelPipeline p = ...;
p.addLast("1", new InboundHandlerA());
p.addLast("2", new InboundHandlerB());
p.addLast("3", new OutboundHandlerA());
p.addLast("4", new OutboundHandlerB());
p.addLast("5", new InboundOutboundHandlerX());
如上所示,开头为Inbound的类意味着它是一个实现了ChannelInboundHandler接口的类,以Outbound为开头的类则是实现了ChannelOutboundHandler接口的类,当一个Inbound事件触发,ChannelPipeline只会把Inbound事件交给实现了ChannelInboundHandler接口的类处理,而且执行顺序是1、2、5;同理,当一个Outbound事件触发则只会交给实现了ChannelOutboundHandler接口的类处理,执行顺序相反,为5、4、3(因为“5”号Handler两种接口都实现了,所以当然两种事件发生时都会流入该类)。

四、Netty线程模型

Netty提供了多种线程模型的实现方式,用户可以根据自身应用场景选择相应的线程模型。

单线程模型.png

由于Netty使用的是异步非阻塞I/O,所有的I/O操作都不会导致线程被挂起,所以理论上一个线程是可以处理所有跟I/O有关的操作。通过 Acceptor 类接收客户端的 TCP连接请求消息,当链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上,进行消息解码。用户线程消息编码后通过 NIO 线程将消息发送给客户端。在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并发的应用场景却不合适,会出现如下问题:

  • 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。
  • 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
  • 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

为了处理这些问题,就演进出了Reactor多线程模型:

多线程模型.png

Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O操作。Reactor 多线程模型的特点如下。

  • 有专门一个NIO线程——Acceptor线程用于监听服务端,接收客户端的TCP连接请求。
  • 网络I/O操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
  • 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。

在绝大多数场景下,Reactor 多线程模型可以满足性能需求。但是,在个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能会存在性能不足的问题,为了解决性能问题,产生了第三种 Reactor 线程模型——主从Reactor 多线程模型。


主从Reactor模型.png

主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接收到客户端 TCP连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel注 册 到 I/O 线 程 池(sub reactor 线 程 池) 的 某 个 I/O 线 程 上, 由 它 负 责SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。利用主从 NIO 线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在 Netty 的官方 demo 中,推荐使用该线程模型。

五、Android端基于Netty实现的Socket通信Demo

Demo中后台开启一个Service作为服务端,客户端输入信息发送给服务端,服务端接收到信息后在客户端发送过来的信息前加上“res”后返回给客户端显示,数据传输格式使用的是Google使用的Protocol Buffer。Demo链接:https://github.com/qaz3366639/NettyDemo
服务端的配置代码如下:

        mWorkerGroup = new NioEventLoopGroup();
        //服务端启动引导类,负责配置服务端信息
        mServerBootstrap = new ServerBootstrap();
        mServerBootstrap.group(mWorkerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new ChannelInitializer<NioServerSocketChannel>() {
                    @Override
                    protected void initChannel(NioServerSocketChannel nioServerSocketChannel) throws Exception {
                        ChannelPipeline pipeline = nioServerSocketChannel.pipeline();
                        pipeline.addLast("ServerSocketChannel out", new OutBoundHandler());
                        pipeline.addLast("ServerSocketChannel in", new InBoundHandler());
                    }
                })
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //为连接上来的客户端设置pipeline
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast("decoder", new ProtobufDecoder(Test.ProtoTest.getDefaultInstance()));
                        pipeline.addLast("encoder", new ProtobufEncoder());
                        pipeline.addLast("out1", new OutBoundHandler());
                        pipeline.addLast("out2", new OutBoundHandler());
                        pipeline.addLast("in1", new InBoundHandler());
                        pipeline.addLast("in2", new InBoundHandler());
                        pipeline.addLast("handler", new ServerChannelHandler());
                    }
                });

        channelFuture = mServerBootstrap.bind(PORT_NUMBER);```

客户端配置如下:

if (mBootstrap == null) {
mWorkerGroup = new NioEventLoopGroup();
mBootstrap = new Bootstrap();
mBootstrap.group(mWorkerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder", new ProtobufDecoder(Test.ProtoTest.getDefaultInstance()));
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast("handler", mDispatcher);

                    }
                })
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);

}

ChannelFuture future = mBootstrap.connect(mServerAddress);
future.addListener(mConnectFutureListener);

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

推荐阅读更多精彩内容