首页 > 编程知识 正文

那些框架底层用到netty,netty源码剖析与实战

时间:2023-05-05 02:10:42 阅读:11372 作者:83

一、Netty框架介绍

(本文部分照片摘自Netty-In-Depth )

Netty是一个以异步事件为中心的网络开发框架和工具,帮助开发人员快速开发可维护的高性能、可扩展的服务器和客户端。

二. Netty优于其他I/O编程

1、BIO编程

在基于传统同步阻塞模型的开发中,服务器套接字绑定IP地址并启动监听端口。 套接字开始连接操作。 连接成功后,双方通过输入输出流进行同步块通信。 服务器端为每个连接的客户端重新创建线程进行链接处理,处理完成后通过输出流向客户端返回响应,并丢弃线程。 是一种典型的单请求单响应通信模型。 但是,在这种模式下,当连接的客户端数量增加时,相应的服务端线程数量也会急剧增加,而服务端线程数量过多,导致系统瘫痪。

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

2、伪异步IO编程

伪异步IO线程实际上可以通过向BIO编程中添加线程池,并让线程池执行处理客户端连接请求的操作,来控制线程数量并有效防止线程耗尽。 但是,在该模型中,通信时间太长,会发生级联故障。 例如,如果服务端处理时间过长或其他线程出现故障,则IO操作被阻止,因此如果当前所有可用线程都被阻止,后续的所有连接将排队,队列将到达

伪异步io (摘自netty-in-depth ) ) ) ) ) ) ) ) ) ) ) ) ) ) )

3、NIO编程

前面两种编程模型出现的问题实际上是IO操作被同步阻止,因此要解决这些问题,最好从“同步阻止”方面开始。 因此,JAVA提供了NIO类库。 也就是说,JAVA支持非块IO是传统BIO编程中与套接字的连接方式。 通过Socket和ServerSocket进行连接、端口侦听、输入输出流获取等操作,与此相对应,NIO提供了SocketChannel和ServerSocketChannel。 这被称为“通道”,它同时支持块和非块模式,块方式会出现上述问题。通常,在低负载、低并发的APP应用中,它会同步阻止I/O,以降低编程复杂性

4、AIO编程

NIO编程虽然无阻塞,但仍然采用同步IO (复用)。 这意味着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 ) () ) ) ) ) ) ) ) ) 652 ) ) )

Reactor层的作用主要是监听网络读写、客户端连接等事件,将网络数据读取到内存缓存中,上层可以在ByteBuf类中读取数据,Reactor可以触发事件

Pipeline层基于责任链模型实现,由用户定制的各种Handler组成链结构由Pipeline管理。 事件发生时,Pipeline查找并运行最近的处理程序,处理完成后将事件传递给下一个处理程序继续处理。 如下图所示。

ChannelPipeline.png

以下内容将根据官方网站的说明进行翻译。

Inbound事件传递给Inbound处理程序类,并由下而上处理。 传入的数据通常通过实际输入操作(如套接字通道.读取(字节缓冲器) )从服务端读取。 一个Inbound事件流向最上层的InboundHandler后,被丢弃或记录(需要聪明的蜡烛时)。

Outbound事件由自上而下处理的Outbound处理程序类处理。 通常,Outbound处理程序会生成或转换Outbound数据流,如write请求。 当Outbound事件通过底部的Outbound处理程序时,它会传递到与Channel关联的I/O线程。 I/O线程通常执行实际的输出操作,如套接字通道. write (字节缓冲器)

例如,假设我们创建如下的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() {

@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);

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。