前言
在了解一个事物之前,最好能对它的基本属性和相关概念有个基本的认知,所以学习Netty之前,也有必要了解与Netty相关的基础概念知识;本篇将对Netty做一个基础性的介绍,主要包括Netty的适用场景,特色以及基础的IO知识,如果你已经了解这些知识,也可以跳过本篇,直接进入下一篇:Netty剖析 - 2. 实现
Netty是什么?
首先我们来看Netty是什么,关于这个问题,其官网有一段阐述:
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients
这段翻译过来意思就是:
Netty是一个基于NIO的异步网络编程框架,基于Netty能快速的搭建高性能易扩展的网络应用(包括客户端与服务端)
具体来说Netty就是对于Java NIO的封装,NIO又是什么呢?NIO是Java 1.4后引入的基于事件模型的非阻塞IO框架,在NIO之前,对于数据的处理都是基于BIO(blocking IO),从名字上就知道BIO是以阻塞的形式对数据进行处理,这种处理形式比较简单,但是既然阻塞的,那么就不可避免会涉及到线程的操作,熟悉并发的小伙伴应该都知道,线程是一种昂贵的资源,无论是创建,销毁,还是切换,这就导致BIO在面对一些特定场景如高并发等束手无策,而这些场景在互联网应用中却又很常见;对应的,NIO能较好的应对这些场景,遗憾的是,Java在刚推出NIO时,由于各种原因,致使其使用复杂,且经常会出现Bug,结果就是:广大开发者有需求,但解决需求的工具就是不好用这样尴尬的局面,怎么办呢? -- 自己动手,丰衣食足!大不了再造个"轮子",所以就出现了一系列解决NIO问题的框架,而Netty就是其中最著名的那一个(当然Java发展到现在,其NIO库原本的很多问题都得到了解决,不过很多解决方案借鉴的也是Netty的思想)
另外,Netty并不止于解决NIO的问题,它更进一步,还提供了一系列特色功能
Netty的特色
自己的孩子自己最了解,下面试Netty官网对于Netty特色的说明:
It greatly simplifies and streamlines network programming such as TCP and UDP socket server
'Quick and easy' doesn't mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise
这段话大概的意思就是:
首先,Netty能极大的简化你的网络编程;并且,简单好用还不需要以复杂的管理和低效的性能为代价,Netty通过优秀的设计,在易部署,高性能,稳定性,扩展性之间找到了一个较好的平衡点
我们把这句话提炼出来,大概就可以得到Netty的几大特色:
- 针对基本的需求提供了简单易用的接口,直接上手!
- 针对复杂的场景提供了很强的扩展性,轻松应对业务发展!
- 在上面两点的基础上,性能不打折!
而如果对这些特点进行细化,则可以得出:
- 基于事件机制(Pipeline - Handler)达成关注点分离(消息编解码,协议编解码,业务处理)
- 可定制的线程处理模型,单线程,多线程池等
- 屏蔽NIO本身的bug
- 性能上的优化
- 相较于NIO接口功能更丰富
- 对外提供统一的接口,底层支持BIO与NIO两种方式自由切换
这些特性将在本系列第二篇里做详细分析;既然Netty的本质还是一个基于NIO的网络框架,那么想要掌握Netty的精髓,对于NIO的了解就不可或缺
NIO处理模型介绍
在介绍NIO之前,最好了解一下BIO,还没有学习过的小伙伴可以阅读我另外一篇介绍BIO的文章:Java IO使用入门 -- IO其实很简单
NIO是Java 1.4引入的一种同步非阻塞的I/O模型,也是I/O多路复用的基础;相对于Java BIO(OIO)提供的基于面向流的阻塞式编程模型,NIO提供的是面向缓冲区的响应式事件编程模型
读到这里可能有些人会觉得迷糊,什么阻塞?非阻塞?基于流?基于缓冲区?这里有必要介绍一下Linux下的5中IO模型:
- 阻塞I/O模型:
最常用的I/O模型就是阻塞I/O模型,当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO的特点就是在IO执行的两个阶段都被block了
-
非阻塞IO模型:
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回;所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。 -
IO复用模型:
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
当用户进程调用了select,那么整个进程会被block,而同时,kernel会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,内核负责将数据从kernel拷贝到用户进程;所以,I/O 多路复用的特点是通过一种机制使得一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回,所以说它最大的优势是系统开销小,系统不需要创建或维护新的进程/线程。另外,从上面比较IO复用流程图和阻塞IO的图可以发现,多路复用本身也是阻塞的,事实上,其效率可能还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而阻塞IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用阻塞IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)在IO复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block 信号驱动IO模型:
首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号。随即可以在信号处理程序中调用recvfrom来读数据,井通知主循环函数处理数据;一般用的较少-
异步IO:
在异步IO模型下,用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后依然由它将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
介绍完这5种IO模型后,我们回到NIO,NIO基于的是IO复用模型(就是上面的第三种IO模型),正如在介绍IO复用模型时已提到,而在linux下,有三种针对该模型的实现,分别为:select,poll,epoll;select和poll的实现机制类似,主要区别在于描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构;epoll是linux 2.6后才有的,它主要是对select和poll的缺陷做了一些改进,这两种实现方式有几个比较大的缺点:
1) 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2) 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3) select支持的文件描述符数量太小了,默认是1024(当然可以手动改,但改大了不一定效果好,以为前面的1,2两点)
对于第一个缺点,epoll在每次注册新的事件到epoll句柄中时,会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。这样就保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决思路是每当一个fd准备就绪,就调用对应的回调函数将其加入一个就绪链表,然后只需要遍历这个就绪链表即可,不需要遍历所有fd
对于第三个缺点,epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大
另外顺便提一下Windows下的异步IO实现机制:I/O Completion Ports,或简称IOCP,个人觉得它的设计比较好,极大的减少线程切换对性能的影响,同时又能保证CPU保持在较高的利用率,有兴趣可以阅读一下这篇文章
总结
本篇主要介绍了Netty相关的基础知识,核心在于各种IO模型,特别是异步IO模型,作用在于为本系列的第二篇Netty剖析 - 2. 实现做准备,如果需要对IO模型进行更深入的了解,可以参考下面几篇文章: