一、Netty框架简介
(本文中部分图片摘自Netty-In-Depth)
Netty是一款以异步事件为驱动的网络开发框架和工具,能够快速的帮助开发者开发出可维护的高性能,高扩张性的服务器和客户端。
二、Netty相较于其他I/O编程的优点
1、BIO编程
在基于传统同步阻塞模型开发中,ServerSocket 负责绑定 IP 地址,启动监听端口;Socket 负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。server端为每一个连上来的client端新建一个线程进行链路处理,处理完成之后通过输出流返回应答到客户端,然后线程销毁。也就是典型的一请求一应答通信模型。但是这种模型在连接的客户端数量庞大的时候,相应的服务端线程数量也会剧增,这就会使服务端因为线程数量过多而宕机。
2、伪异步IO编程
伪异步IO线程其实就是在BIO编程基础上增加了线程池,将处理客户端连接请求的操作交给线程池去处理,这样线程数量就处于可控状态,可以有效的防止线程耗尽,但是这种模型会出现通信时间过长导致级联故障:比如服务端处理时间过长,或者其他线程出现故障,由于IO操作是阻塞的,因此假如当前所有可用线程都被阻塞了,那么后续的所有连接都会在队列中排队等待,当队列达到最大可容纳数量时,后续入队列操作会被阻塞。这样acceptor因为阻塞在线程池的队列中,所以无法处理后续客户端的连接请求,出现大量的连接超时。
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是异步非阻塞的,它与阻塞、非阻塞、多路复用等IO的区别可以参考 http://blog.csdn.net/zhangzeyuaaa/article/details/42609723 简单来说异步非阻塞就是应用发起读写请求之后就交由系统去处理,等操作完成之后,系统会通过回调来通知应用操作结果。
三、Netty架构
Reactor层的职责主要是负责监听网络读写、客户端连接等事件,将网络数据读到内存缓存中,上层可通过ByteBuf类读取数据,Reactor还负责触发事件,产生的事件交由Pipeline处理。
Pipeline层是基于责任链模式实现的,用户定制的各种Handler组成一个链式结构由Pipeline管理,当事件触发时,Pipeline寻找最接近的Handler并执行,处理完后继续将事件传给下一个Handler处理。如下图所示:
以下内容根据官网的描述翻译:
一个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提供了多种线程模型的实现方式,用户可以根据自身应用场景选择相应的线程模型。
由于Netty使用的是异步非阻塞I/O,所有的I/O操作都不会导致线程被挂起,所以理论上一个线程是可以处理所有跟I/O有关的操作。通过 Acceptor 类接收客户端的 TCP连接请求消息,当链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上,进行消息解码。用户线程消息编码后通过 NIO 线程将消息发送给客户端。在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并发的应用场景却不合适,会出现如下问题:
- 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送。
- 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
- 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
为了处理这些问题,就演进出了Reactor多线程模型:
Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O操作。Reactor 多线程模型的特点如下。
- 有专门一个NIO线程——Acceptor线程用于监听服务端,接收客户端的TCP连接请求。
- 网络I/O操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
- 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
在绝大多数场景下,Reactor 多线程模型可以满足性能需求。但是,在个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能会存在性能不足的问题,为了解决性能问题,产生了第三种 Reactor 线程模型——主从Reactor 多线程模型。
主从 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() {
@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() {
@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
@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);