Netty是什么
Netty是一个高性能的、异步的、基于事件驱动的网络应用框架。
-
核心:
可扩展的事件模型
统一的通信api
无论是http 还是socket都使用统一的api,简化了操作
零拷贝机制与字节缓冲区 -
传输服务
支持socket 以及datagram(数据报)
持http协议
In-VM Pipe (管道协议) -
协议支持
http 以及 websocket
SSL 安全套接字协议⽀持
Google Protobuf (序列化框架)
持zlib、gzip压缩
持大文件的传输
RTSP(实时流传输协议,是TCP/IP协议体系中的⼀个应⽤层协议)
持二进制协议并且提供了完整的单元测试
Netty优势
- Netty是基于Java的NIO实现的,Netty将各种传输类型、协议的实现API进行了统一封装,实现了阻塞和非阻塞Socket。
- 基于事件模型实现,可以清晰的分离关注点,让开发者可以聚焦业务,提升开发效率。
- 高度可定制的线程模型-单线程、一个或多个线程池,如SEDA(Staged Event-DrivenArchitecture)
SEDA:把⼀个请求处理过程分成⼏个Stage,不同资源消耗的Stage使⽤不同数量的线程来处理,Stage间使⽤事件驱动的异步通信模式。 - Netty只依赖了JDK底层api,没有其他的依赖,如:Netty 3.X依赖JDK5以上,Netty4.x依赖JDK6以上。
- Netty在网络通信方面更加的高性能、低延迟,尽可能的减少不必要的内存拷贝,提高性能。
- 在安全方面,完整的SSL/TLS和StartTLS支持。
- 社区比较活跃,版本迭代周期短,发现bug可以快速修复,新版本也会不断的加入。
为什么选择Netty,不选择原⽣的NIO?
- NIO的类库和API繁杂,使用麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
- 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和⽹路编程⾮常熟悉,才能编写出高质量的NIO程序。
- 可靠性能力补齐,工作量和难度都非常大。例如客户端⾯临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,
NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。 - JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK 1.6版本的update18修复了该问题,
但是直到JDK 1.7版本该问题仍旧存在,只不过该BUG发生概率降低了而已,它并没有得到根本性解决。
Netty的⾼性能设计
要想了解netty性能高在哪儿,需要从java的IO模型聊起
java中的IO模型
- 在JDK1.4之前,基于Java所有的socket通信都采用了同步阻塞模型(BIO),这种模型性能低下,当时大型的服务均采用C或C++开发,
因为它们可以直接使用操作系统提供的异步IO或者AIO,使得性能得到大幅提升。 - 2002年,JDK1.4发布,新增了java.nio包,提供了许多异步IO开发的API和类库。新增的NIO,极大的促进了基于Java的异步非阻塞的发展和应用。
- 2011年,JDK7发布,将原有的NIO进行了升级,称为NIO2.0,其中也对AIO进进了支持。
BIO模型
java中的BIO是blocking I/O的简称,它是同步阻塞型IO,其相关的类和接口在java.io下。
BIO模型简单来讲,就是服务端为每一个请求都分配一个线程进⾏处理,如下:
NIO模型
NIO,称之为New IO 或是 non-block IO (非阻塞IO),NIO相关的代码都放在了java.nio包下,其三大核心组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复⽤器)
- Buffer
在NIO中,所有的读写操作都是基于缓冲区完成的,底层是通过数组实现的,常用的缓冲区是ByteBuffer,
每一种java基本类型都有对应的缓冲区对象(除了Boolean类型),如:CharBuffer、IntBuffer、LongBuffer等。 - Channel
在BIO中是基于Stream实现,在NIO中是基于通道实现,与流不同的是,通道是双向的,既可以读也可以写 - Selector
Selector是多路复用器,它会不断的轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就处于就绪状态,
会被Selector轮询出来,然后通过SelectionKey获取就绪Channel的集合,进行IO的读写操作。
通过多路复用器就可以实现一个线程处理多个通道,避免了多线程之间的上下文切换导致系统开销过大。
NIO无需为每个连接开个线程处理,并且只有通道真正有有事件时,才进行读写操作,这样大大的减少了系统开销
AIO
在NIO中,Selector多路复用器在做轮询时,如果没有事件发生,也会进行阻塞,如何能把这个阻塞也优化掉呢?那么AIO就在这样的背景下诞生了。
AIO是asynchronous I/O的简称,是异步IO,该异步IO是需要依赖于操作系统底层的异步IO实现。
AIO的基本流程是:用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。
kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
AIO模型存在的不足:
- 需要完成事件的注册与传递,需要底层操作系统提供大量的支持,去做大量的工作。
- Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,Windows 系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。
- 在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以,这也是在 Linux 下,实现高并发网络编程时都是以 NIO 多路复用模型模式为主。
Reactor线程模型
Reactor线程模型不是Java专属,也不是Netty专属,它其实是一种并发编程模型,是一种思想,具有指导意义。
列如,Netty就是结合了NIO的特点,应用了Reactor线程模型所实现的。
Reactor模型中定义的三种角色:
Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
Acceptor:处理客户端新连接,并分派请求到处理器链中。
Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。
常见的Reactor线程模型有三种,如下:
Reactor单线程模型
Reactor多线程模型
主从Reactor多线程模型
单Reactor单线程模型
说明:
- Reactor充当多路复用器角色,监听多路连接的请求,由单线程完成
- Reactor收到客户端发来的请求时,如果是新建连接通过Acceptor完成,其他的请求由Handler完成。
- Handler完成业务逻辑的处理,基本的流程是:Read --> 业务处理 --> Send 。
优点
结构简单,由单线程完成,没有多线程、进程通信等问题。
适合用在一些业务逻辑比较简单、对于性能要求不高的应用场景。
缺点
由于是单线程操作,不能充分发挥多核CPU的性能。
当Reactor线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,
这更加重Reactor线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
可靠性差,如果该线程进⼊死循环或意外终止,就会导致整个通信系统不可用,容易造成单点故障。
单Reactor多线程模型
说明:
在Reactor多线程模型相比较单线程模型而言,不同点在于,Handler不会处理业务逻辑,只是负责响应用户请求,真正的业务逻辑,在另外的线程中完成。
这样可以降低Reactor的性能开销,充分利用CPU资源,从而更专注的做事件分发工作了,提升整个应用的吞吐。
但是这个模型存在的问题:
多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程Reactor进进发送,就会涉及共享数据的互斥和保护机制。
Reactor承担所有事件的监听和响应,只在主线程中运,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。
为了解决性能问题,产生了第三种主从Reactor多线程模型。
主从Reactor多线程模型
在主从模型中,将Reactor分成2部分:
MainReactor负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给SubReactor。
SubReactor主要完成和建立起来的socket的数据交互和事件业务处理操作。
该模型的优点:
响应快,不必为单个同步事件所阻塞,虽然Reactor本身依然是同步的。
可扩展性强,可以方便地通过增加SubReactor实例个数来充分利用CPU资源。
可复用性高,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性。
Netty模型
说明
在Netty模型中,负责处理新连接事件的是BossGroup,负责处理其他事件的是WorkGroup。Group就是线程池的概念。
NioEventLoop表示一个不断循环的执行处理任务的线程,用于监听绑定在其上的读/写事件。
通过Pipeline(管道)执行业务逻辑的处理,Pipeline中会有多个ChannelHandler,真正的业务逻辑是在ChannelHandler中完成的。
Netty核心组件
channel
Channel可以理解为是socket连接,在客户端与服务端连接的时候就会建立一个Channel,它负责基本的IO操作,列如:bind()、connect(),read(),write() 等。
Netty 的Channel接口所提供的 API,大大地降低了直接使用Socket类的复杂性。
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型:
NioSocketChannel,NIO的客户端 TCP Socket 连接。
NioServerSocketChannel,NIO的服务器端 TCP Socket 连接。
NioDatagramChannel, UDP 连接。
NioSctpChannel,客户端 Sctp 连接。
NioSctpServerChannel,Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件IO。
EventLoop、EventLoopGroup
有了 Channel 连接服务,连接之间可以消息流动。如果服务器发出的消息称作“出站”消息,服务器接受的消息称作“入站”消息。那么消息的“出站”/“入站”就会产生事件(Event)。
例如:连接已激活;数据读取;用户事件;异常事件;打开链接;关闭链接等等。
有了事件,就需要一个机制去监控和协调事件,这个机制(组件)就是EventLoop。 在 Netty 中每个 Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。
每个 EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件。
EventLoopGroup 是用来生成 EventLoop
总结:
一个 EventLoopGroup 包含一个或者多个 EventLoop;
一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
一个 Channel 在它的⽣命周期内只注册于一个 EventLoop;
一个 EventLoop 可能会被分配给一个或多个 Channel。
ChannelHandler
ChannelHandler是最重要的组件,因为对于数据的入站和出站的业务逻辑的编写都是在ChannelHandler中完成的。
在前面的例子中,MyChannelHandler就是实现了channelRead方法,获取到客户端传来的数据。
对于数据的出站和入站,有着不同的ChannelHandler类型与之对应:
ChannelInboundHandler 入站事件处理器
ChannelOutBoundHandler 出站事件处理器
ChannelPipeline
在Channel的数据传递过程中,对应着有很多的业务逻辑需要处理,列如:编码解码处理、读写操作等,
那么对于每种业务逻辑实现都需要有个ChannelHandler完成,也就意味着,一个Channel对应着多个ChannelHandler,
多个ChannelHandler如何去管理它们,它们的执行顺序又该是怎么样的,这就需要ChannelPipeline进行管理了。
一个Channel包含了一个ChannelPipeline,而ChannelPipeline中维护了一个ChannelHandler的列表。
ChannelHandler与Channel和ChannelPipeline之间的映射关系,由ChannelHandlerContext进行维护。
ChannelHandler按照加入的顺序会组成一个双向链表,入站事件从链表的head往后传递到最后一个ChannelHandler,
出站事件从链表的tail向前传递,直到最后一个ChannelHandler,两种类型的ChannelHandler相互不会影响。
Bootstrap
Bootstrap是引导的意思,它的作用是配置整个Netty程序,将各个组件都串起来,最后绑定端扣、启动Netty服务。
Netty中提供了2种类型的引导类,一种用于客户端(Bootstrap),而另一种(ServerBootstrap)用于服务
器。
它们的区别在于:
ServerBootstrap 将绑定到某个端口,因为服务器必须要监听连接,Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。
引导客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个。
因为服务器需要两组不同的 Channel
第一组将只包含一个 ServerChannel,代表服务器已绑定到某个本地端口的正在监听的套接字。
第二组将包含所有已创建的用来处理传入客户端连接。
与ServerChannel相关联的EventLoopGroup 将分配一个负责为传入连接请求创建 Channel 的EventLoop。一旦连接被接受,第一个 EventLoopGroup 就会给它的 Channel 分配一个 EventLoop。
Future
Future提供了在操作完成时通知应用程序的方式。这个对象可以看作是异步操作的结果的占位符,它将在未来的某个时刻完成,并提供对其结果的访问。
JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。
这是非常繁琐的,所以 Netty 提供了自己的实现——ChannelFuture,用于在执行异步操作的时候使用。
ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。
监听器的回调方法operationComplete(),将会在对应的操作完成时被调用 。然后监听器可以判断该操作是成功地完成了还是出错了。
每个 Netty 的出站 I/O 操作都将返回ChannelFuture,也就是说,它们都不会阻塞。 所以说,Netty完全是异步和事件驱动的。