使用Netty,可能是相对简单的。但是要搞懂Netty,可能就不是那么容易的事情了,因为要了解的基础知识太多了。如线程模型、IO迷行、NIO、传统IO等等。
笼统的去学习Netty,或者说没有一条学习线路去学习Netty,就会像我一样头脑混乱,即使遇到不懂的问题,可能也不知道要怎么提问。为什么连提问都不会?因为自己的思路都是不清晰的,各种东西糅杂在一起了。
我之前是这样学习Netty的:先了解了Netty怎么使用,再看看相关的介绍文章,看到大家常用Netty和Http作比较,也发现里面讲了Netty的线程模型,提及了Reactor线程模型,还和NIO紧紧相关,然后又发现NIO常常和传统IO作对比,其中包含零拷贝。在NIO中又会提到多路复用IO而传统IO是阻塞IO,以及其他的非阻塞IO和异步IO等模型。在学习过程也产生了很多疑惑:多路复用IO在应用程序层面是个怎样的存在,为什么在Netty的demo里感觉不出跟多路复用有关的东西。Reactor线程模型和多路复用IO模型又是一个怎样的关系?系统调用、recvfrom、内核空间、内核态、用户空间、用户态又是什么,这和IO模型又有什么管理,和Netty又有什么关系?Netty、NIO和操作系统又有什么关系?
于是乎,自己把自己的问倒了。
后来静下来心想想,之所以会这么混乱,是因为涉及到的知识点太多,自己又没有下意识的分类整理,再加上很多基础知识的不了解,才导致的一头雾水吧。于是乎觉得要把知识分块,划分模块层次,这样才不会混乱了。也就是说我应该反着学,也就是先把基础搞懂了,再一层一层的做关联。
于是,做出了下面的分类:
1、操作系统的预备知识
2、操作系统的I/O模型
3、I/O多路复用模型
4、程序的线程/进程模型
5、传统IO
6、NIO
7、Netty
后续按照上面的分类,参考其他博客文章来做学习摘要,参考的文章在文末,都是值得多次阅读的好文。
在操作系统中,CPU通常会在两种不同的模式下工作:
内核模式下,程序代码能够完全,无限制地访问底层硬件,能够执行任意的CPU指令和访问任意的内存地址。内核模式通常留给最底层的,受信任的系统函数来使用。程序在内核模式下崩溃是灾难性的,这甚至可以使整台PC宕机。
用户模式下,程序代码不能够直接访问硬件和内存。执行在用户态的代码必须委托系统函数去访问硬件和内存,因为有这种隔离机制的保护,程序在用户态下崩溃通常是可恢复的。PC中大多数程序也是在用户态下执行。
寻址空间(虚拟存储空间)= 内核空间 + 用户空间
现在操作系统都是采用虚拟存储器,那么对于32位操作系统而言,它的寻址空间为4G(2的32次方)。
而操作系统的核心是内核kernel,它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
为了保证用户进程不能直接操作内核kernel,保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
针对Linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间,即用户空间。
了解这些基础知识,是为接下来的知识做铺垫,这样在后面提及时相关名词时我们才不至于看的云里雾里从而没有抓住重点。
为了保护操作系统内核,因此分了内核空间和用户空间,重要操作再内核空间完成。而对于应用程序而言,数据的获取和输出都得通过通过内核空间,以read操作为例,数据得先加载到操作系统内核空间中,即内核缓冲区中,然后再从内核缓冲区拷贝到应用程序的地址空间上。即这里可以划分为两个阶段。
以read操作为例,这个操作的两个阶段分别为:
1、等待数据加载到内核空间(准备阶段)
2、从内核向用户空间复制数据(这步是真正的IO Operation)
其中,阻塞非阻塞描述的是第一阶段是否block,同步异步指的是第二阶段是否block。
按照上面说的两个阶段的数据加载是否block,产生了5种不同的模型。正因如此,才有了接下来要介绍的操作系统I/O模型。
操作系统的I/O模型有以下5种:
不用的模型区别在于数据准备节点和数据拷贝阶段是否block,我们间接接触的比较多的是前三种。传统IO中的socket编程,就是基于阻塞式IO模型。而Java NIO中则是使用了IO复用模型,而其中的Selector又是参考了non-blocking IO。
IO模型的流程图以及详细介绍可以参考这篇文章:一文读懂高性能网络编程中的I/O模型
这里没有细写不是因为不重要,而是因为觉得自己还没完全吃透。但是这部分的知识却是很重要的。我们平常开发是在应用层基于编程语言提供的各种工具包进行开发的,而IO模型这些操作系统级别的东西我们是没有直接接触到的,但是我自己觉得,把这些相对底层的东西学会了,无论换什么编程语言都是一样的。不过可能是我自己想当然了。
扯远了。总之,这块的知识也是很重要的。
不理解IO multiplexing,就直接去学习Reactor模型,直接去看NIO、Netty,会很吃力,因为它是基础中的重点。当然,如果只是要简单的使用Netty,那不去了解也是可以的。就像socket编程,我们无需知道它是基于阻塞式IO模型,我们只需要知道accept方法是阻塞的,只需要知道accept方法返回socket对象后创建子线程去处理就可以了。但是这样就没办法理解NIO和Netty,不去理解,就会用完就忘。因此,还是老老实实把IO multiplexing给搞明白吧。
那到底什么是I/O多路复用?有这么些回答:
可是还是觉得解释的最好的还是这篇文章里的:并发编程(IO多路复用)
先举例说明复用的含义:
在通信领域中为了充分利用网络连接的物理介质,往往在同一条网络链路上采用时分复用或频分复用的技术使其在同一链路上传输多路信号,到这里我们就基本上理解了复用的含义,即公用某个“介质”来尽可能多的做同一类(性质)的事。
再解释IO复用到底是复用什么:
那IO复用的“介质”是什么呢?为此我们首先来看看服务器编程的模型,客户端发来的请求服务端会产生一个进程来对其进行服务,每当来一个客户请求就产生一个进程来服务,然而进程不可能无限制的产生,因此为了解决大量客户端访问的问题,引入了IO复用技术,即:一个进程可以同时对多个客户请求进行服务。也就是说IO复用的“介质”是进程(准确的说复用的是select和poll,因为进程也是靠调用select和poll来实现的),复用一个进程(select和poll)来对多个IO进行服务,虽然客户端发来的IO是并发的但是IO所需的读写数据多数情况下是没有准备好的,因此就可以利用一个函数(select和poll)来监听IO所需的这些数据的状态,一旦IO有数据可以进行读写了,进程就来对这样的IO进行服务。
在学习的过程中有这样的疑惑:
结合了NIO和Reactor模型的例子,有了这样的理解:
select方法是在单一的线程中执行的,所以我们才说一个线程可以处理多个IO连接。另外select方法确实是阻塞的,它会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。但是我们可以指定方法调用的超时时间。而我们是在select方法中监听各种事件,其中就包括了建立连接。至于IO多路复用和非阻塞IO的区别,确实他们在等待数据和拷贝数据两个阶段都是block的,但是多路复用IO(即select/epoll)的第一阶段可以通过一个selector线程来管理。而blocking IO如果要支持并发只能开启多线程,可是这样就会导致一个请求对应一个连接。也就是说,虽然select/epoll和blocking IO在两个阶段都是阻塞的,可是由于select/epoll可以用一个线程管理多个连接。因此大大缩短了多个线程共存的时间。
以上是自己的理解,可能说的不太准确甚至是错的。如有发现,还请指点一下。
具体是线程还是进程,更多的与平台和编程语言有关。例如C语言既可以使用线程又可以使用进程(例如Nginx使用进程,Memcached使用线程),Java语言一般使用线程(例如Netty),为了方便描述,下面都用线程来进行描述。
1、线程模型1:传统阻塞I/O模型
2、线程模型2:Reactor模式
3、线程模型3:Proactor模型
I/O模型是操作系统层面的东西,而线程模型是应用层面的东西。
Proactor模型是基于AIO模型的,也因为对Proactor模型不了解,因此不对Proactor模型作介绍。
传统阻塞I/O模型,就是传统IO中的socket编程应用。底层是BIO模型,采用多线程方式的话,每个连接需要独立的线程完成数据输入,业务处理,数据返回的完整操作。
Netty是基于NIO,即采用的是多路复用IO,使用的是Reactor线程模型,因此要重点理解Reactor模式。
学习Reactor模式中,有这样的疑问:Reactor模式中IO多路复用到底是用在了哪里?如果没理解错的恶化,是用在了Reactor上,Reactor在一个单独的线程上运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。
根据Reactor的数量和处理资源池线程数量的不同,有3种典型的实现:
1):单Reactor单线程
2):单Reactor多线程
3):主从Reactor多线程
reactor模式参考文章:一文读懂高性能网络编程中的线程模型和Reactor模式详解
传统IO,在Java中,即指jdk1.0中就存在的Java IO包。其中的InputStream、OutputStream、Socket等都是传统IO。我们讲传统IO,为的是和New IO做比较。传统IO使用的IO模型是阻塞IO,即blocking IO,因此我们也用BIO来代指传统IO。
在我们学习socket的时候,我们会创建客户端和服务端,在服务端程序中,我们通过while循环来保证服务端可以一直提供服务,但是由于是单线程,因此如果有多个请求要处理,那么只能一个一个的串行化的去处理,并且,当我们通过socket对象去做read或者write这两个IO操作时,线程是会阻塞的(为什么会阻塞?因为底层使用的IO模型是BIO呀)。因此我们为了提高访问的速度,我们会在serverSocket.accept()接受到请求后,通过创建子线程的方式去处理这个请求,也就是说每一个socket连接都会对应一个子线程。这样一来就会使得当请求量变大的时候,程序创建的线程也会随之增大。而线程的频繁销毁和创建也是很耗资源和时间的,以及当线程数太多时,部分CPU资源会浪费在频繁的线程间切换上。
这就是传统IO,它是以阻塞IO模型为基础的,单线程下会是串行化处理请求的。因此我们通过创建子线程的方式让一个连接对应一个子线程,以提高并发处理效率。如果并发量不大,那么这样也能正常使用的。可是当子线程中执行read或write线程阻塞时,线程资源就算是白白浪费了(把线程想象成宝贵的资源)。
了解NIO:
之前提到NIO,我总是会混乱。首先,NIO并非non-blocking IO的缩写,而是New IO,是Java里的东西,因此也称为Java New IO,有时简写为NIO,之所以叫Java New IO,是相对于传统的Java IO而言的。
当我们要拿NIO跟其他东西比较的时候,要知道可以比较的东西有哪些?是和多路复用IO比较?和non-blocking IO比较?还是和BIO比较?
其实要看你怎么理解,先搞明白自己想要比较的是哪个范畴的东西?如果是操作系统范畴,那么要知道NIO采用的是多路复用IO,那可以比较的就是另外的四种IO模型,常被对比的是阻塞IO即blocking IO。如果比较的是Java范畴,那么要知道NIO采用的是多路复用IO,传统IO即Java IO采用的是blocking IO,除此之外,NIO是面向缓冲区的,而BIO是面向流的。除此之外,NIO还实现了零拷贝(后面会有介绍)。如果是线程模型范畴,那么要知道NIO采用的是Reactor模式,而传统IO是传统阻塞I/O模型,而Netty框架是基础NIO之上的,支持Reactor的三种模式。
NIO相比BIO的优势有:面向缓冲、零拷贝。
Java NIO主要由以下三个核心部分组成:
1、Channel
2、Buffer
3、Selector
这里需要深入去了解学习。 //TODO
另外在Java NIO中多路复用IO指的是selector线程的使用。即原本IO读写是耗时的,但是这些耗时操作的第一阶段(即等待数据从外存或网络拷贝到操作系统内核空间这个耗时操作)可以通过一个selecor线程来并发完成。接下来,第二阶段是很快的,因此进行真正IO操作就会快很多。
Netty待阅读的文章:
https://www.cnblogs.com/cxxjohnson/p/9399831.html
https://segmentfault.com/a/1190000017128263?utm_source=tag-newest#articleHeader0
https://www.jianshu.com/p/a4e03835921a
https://www.jianshu.com/p/c98d14fd2b1b
http://ifeve.com/%e8%b0%88%e8%b0%88netty%e7%9a%84%e7%ba%bf%e7%a8%8b%e6%a8%a1%e5%9e%8b/
其实最开始是要了解分布式框架dubbo的,但是了解到Dubbo中使用了Netty作为网络通信框架,于是乎就先学Netty,又发现想把Netty搞懂,还要了解NIO,以及操作系统层的IO模型以及Linux系统中的基础知识。
所以基础还是很重要的。
这篇文章,未完待续。
一文读懂高性能网络编程中的I/O模型
网络编程 select/epoll分析
I/O复用的理解
新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析
I/O Multiplexing – Linux I/O 多路复用
Java IO:阻塞/非阻塞式IO、同步/异步IO
并发编程(IO多路复用)
Reactor模式详解
彻底搞懂NIO效率高的原理