BIO、NIO、AIO及网络编程

一  网络编程的一些基础概念(重点)

        1.  线程的挂起、阻塞、睡眠

                    线程从创建、运行到结束总是处于下面五个状态之一:新建状

             态、就绪状态、运行状态、阻塞状态及死亡状态。下图是使用时间

             片轮转法的操作系统进程的状态和它们之间的转换。

                    挂起和睡眠是主动的,挂起恢复需要主动完成,睡眠恢复则是

             自动完成的,因为睡眠有一个睡眠时间,睡眠时间到则恢复到就绪

             态。而阻塞是被动的,是在等待某种事件或者资源的表现,一旦获

             得所需资源或者事件信息就自动回到就绪态。睡眠和挂起是两种行

             为,阻塞则是一种状态。

             挂起:一般是主动的,由系统或程序发出,甚至于辅存中去,不释

                        放cpu。

             阻塞:一般是被动的,是放过cpu,不释放内存。在抢占资源中得

                        不到资源,被动的挂起在内存,等待某种资源或信号量(即

                        有了资源)将他唤醒。

                        sleep()是属于阻塞。

                        关于sleep的说明,如下例子:

class ThreadA extends Thread  
{  
    public void run(){  
        System.out.println("ThreadA is running");  
    }  
}  
  
public class TestNew {  
    public static void main(String[] args)throws InterruptedException {  
        // TODO Auto-generated method stub  
        ThreadA ta = new ThreadA();  
        ta.start();  
        ta.sleep(5000);  
        System.out.println("TestNew is running");  
    }  
} 

                            其实这段语句是主线程睡眠5秒,而不是ta线程,sleep

                            声明在哪个线程里哪个线程睡眠

            2.  同步/异步

                 同步和异步关注的是消息通信机制。

               (1)同步:就是指在发出一个功能调用时,在没有得到结果之

                                   前,该调用就不返回。按照这个定义,其实绝大多数

                                   函数都是同步调用。

               (2)异步:概念和同步相对。 当一个异步过程调用发出后,调用

                                   者不会立刻得到结果。实际处理这个调用的部件是在

                                   调用发出后, 通过状态、通知来通知调用者,或通过

                                   回调函数处理这个调用。

                 举例:当到银行后, .可以去ATM机前排队等候 – (排队等候)就是同

                            步等待消息。可以去大厅拿号,等到排到我的号时, 柜台

                            的人会通知我轮到我去办理业务. – (等待别人通知)就是异步

                            等待消息。

            3.  阻塞/非阻塞

                 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时

                 的状态(无所谓同步或者异步,注意这里的“调用结果”就是指“2”

                 中标黑的“调用”的结果)

               (1)阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起

                                 (被挂起就是阻塞,因为是被动的)。函数只有在得到

                                   结果之后才会返回。 有人也许会把阻塞调用和同步调

                                   用等同起来,实际上他是不同的。 对于同步调用来说,

                                   很多时候当前线程还是激活的,只是从逻辑上当前函

                                   数没有返回而已。

               (2)非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结

                                      果之前,该函数不会阻塞当前线程,而会立刻返回。

                举例:继续上面的那个例子,不论是排队等待,还是使用号码等

                           待通知,如果在这个等待的过程中,等待者除了等待消息

                           之外不能做其它的事情,那么该机制就是阻塞的,表现在

                           程序中,也就是该程序一直阻塞在该函数调用处不能继续

                           往下执行。相反,有的人喜欢在银行办理这些业务的时候

                           一边打打电话发发短信一边等待,这样的状态就是非阻塞

                           的。

二  Java的AIO、NIO、BIO(重点)

        1.  先看一些详细解释,否则直接看2看不懂

                    在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,

             需要先在服务端启动一个ServerSocket,然后在客户端启动Socket

             来对服务端进行通信,默认情况下服务端需要对每个请求建立一堆

             线程等待请求(如果这个连接不做任何事情会造成不必要的线程开

             销)而客户端发送请求后,先咨询服务端是否有线程响应,如果没

             有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会

              等待请求结束后才继续执行。

                      BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往

              会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程

              或者只使用少量的多线程,每个连接共用一个线程。如下图,上面

              为BIO,下面为NIO。

              

              

                    NIO的最重要的地方是当一个连接创建后,不需要对应一个线

             程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一

             个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发

             现连接上有请求的话,才开启一个线程进行处理,也就是一个请求

             一个线程模式。

                    在NIO的处理方式中,当一个请求来的话,开启线程进行处理,

             可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞

             了,当并发上来的话,还是会有BIO一样的问题(不过2中的NIO中

             说的,通过注册的回调函数来通知客服端服务器端已经可以处理请

             求的方式,避免了这种问题),这时,就进化出了AIO。

                    HTTP/1.1出现后,有了HTTP长连接,这样除了超时和指明特定

             关闭的http header外,这个链接是一直打开的状态的,这样在NIO

             处理中可以进一步的进化,在后端资源中可以实现资源池或者队列,

             当请求来的话,开启的线程把请求和请求数据传送给后端资源池或

             者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接

             的哪个请求等),这样前面的线程还是可以去接受其他的请求, 而

             后端的应用的处理只需要执行队列里面的就可以了,这样请求处理

             和后端应用是异步的(这就是AIO异步的体现)。当后端处理完,

             到全局地方得到现场,产生响应,这个就实现了异步处理。

             可见:BIO是一个连接一个线程;NIO是一个请求一个线程;AIO是

                        一个有效请求一个线程。

              在举一个烧水的例子来说明一下:

              - AIO的做法是,每个水壶上装一个开关,当水开了以后会提醒对

                应的线程去处理。

              - NIO的做法是,叫一个线程不停的循环观察每一个水壶,根据每

                个水壶当前的状态去处理。

              - BIO的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧

                开,才去处理下一个水壶。

        2.  这回在看概念总结

           (1)Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线

                                         程,即客户端有连接请求时服务器端就需要启动

                                         一个线程进行处理,如果这个连接不做任何事情

                                         会造成不必要的线程开销,当然可以通过线程池

                                         机制改善。

                                         好理解的解释:线程发起IO请求,不管内核是否

                                                                  准备好IO操作,从发起请求起,

                                                                  线程一直阻塞,直到操作完成。

           (2)Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线

                                         程,即客户端发送的连接请求都会注册到多路复

                                         用器上,多路复用器轮询到连接有I/O请求时才启

                                         动一个线程进行处理。

                                         好理解的解释:线程(用户空间的哈)发起IO请

                                                                  求,立即返回;内核在做好IO操

                                                                  作的准备之后,通过调用注册的

                                                                  回调函数通知线程做IO操作(IO

                                                                  操作我自己理解就是将数据拷贝

                                                                  到用户内存),线程开始阻塞,

                                                                  直到操作完成。

           (3)Java AIO(NIO.2) :异步非阻塞,服务器实现模式为一个有效

                                                   请求一个线程,客户端的I/O请求都是由OS

                                                   先完成了再通知服务器应用去启动线程进

                                                   行处理。

                                                   好理解的解释:线程发起IO请求,立即返回;

                                                                            内核做好IO操作的准备之后,

                                                                            做IO操作(IO操作我自己理

                                                                            解就是将数据拷贝到用户内

                                                                            存),直到操作完成或

                                                                            者失败,通过调用注册的回

                                                                            调函数通知线程做IO操作完

                                                                            成或者失败。

           关于AIO、NIO、BIO推荐比较不错的一篇博客

           https://www.ibm.com/developerworks/cn/linux/l-async/

           有空去阅读

        3.  BIO、NIO、AIO的使用场景

           (1)BIO方式适用于连接数目比较小且固定的架构,这种方式对服务

                    器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选

                    择,但程序直观简单易理解。

           (2)NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比

                    如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开

                    始支持。

           (3)AIO方式使用于连接数目多且连接比较长(重操作)的架构,比

                    如相册服务器,充分调用OS参与并发操作,编程比较复杂,

                    JDK7开始支持

三  Linux IO模式及 select、poll、epoll详解(了解内容)

        注:本节和“四”内容大多摘子博客:

                https://segmentfault.com/a/1190000003063859#articleHeader6

                https://www.cnblogs.com/zengzy/p/5113910.html(专讲IO多路复用)

                本节并为全部摘录,如有时间进行全部阅读。

       同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不

       同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的

       上下文。

       本节讨论的背景是Linux环境下的network IO。

       1.  一些概念说明

            在解释正题之前需要了解如下一些概念:

            - 用户空间和内核空间

            - 进程切换

            - 进程的阻塞

            - 文件描述符

            - 缓存 I/O

       (1)用户空间与内核空间

                现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,

                它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统

                的核心是内核,独立于普通的应用程序,可以访问受保护的内存

                空间,也有访问底层硬件设备的所有权限。为了保证用户进程不

                能直接操作内核(kernel),保证内核的安全,操心系统将虚拟

                空间划分为两部分,一部分为内核空间,一部分为用户空间。针

                对linux操作系统而言,将最高的1G字节(从虚拟地址

                0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,

                而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),

                供各个进程使用,称为用户空间。

       (2)进程切换

                       为了控制进程的执行,内核必须有能力挂起正在CPU上运行

                的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进

                程切换。因此可以说,任何进程都是在操作系统内核的支持下运

                行的,是与内核紧密相关的(所以说线程切换比较消耗资源,它

                需要重用户态转换到内核态)

                       从一个进程的运行转到另一个进程上运行,这个过程中经过

                下面这些变化:

              (a)保存处理机上下文,包括程序计数器和其他寄存器。

              (b)更新PCB信息。

              (c)把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队

                       列。

              (d)选择另一个进程执行,并更新其PCB。

              (e)更新内存管理的数据结构。

              (f)恢复处理机上下文。

                注:总而言之就是很耗资源,具体的可以参考这篇文章:进程切换

       (3)进程的阻塞

                       注:阻塞是线程的一种状态。

                       正在执行的进程,由于期待的某些事件未发生,如请求系统资

                源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,

                则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状

                态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处

                于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程

                进入阻塞状态,是不占用CPU资源的。

       (4)文件描述符fd

                       文件描述符(File descriptor)是计算机科学中的一个术语,是

                一个用于表述指向文件的引用的抽象化概念。

                       文件描述符在形式上是一个非负整数。实际上,它是一个索引

                值,指向内核为每一个进程所维护的该进程打开文件的记录表。当

                程序打开一个现有文件或者创建一个新文件时,内核向进程返回一

                个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围

                绕着文件描述符展开。但是文件描述符这一概念往往只适用于

                UNIX、Linux这样的操作系统。

       (5)缓存 I/O

                        缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都

                是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据

                缓存在文件系统的页缓存( page cache )中,也就是说,数据会先

                被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓

                冲区拷贝到应用程序的地址空间。

                缓存 I/O 的缺点:

                       数据在传输过程中需要在应用程序地址空间和内核进行多次数据

                拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大

                的。

四   IO模式((3)I/O 多路复用( IO multiplexing)其

      它均为了解内容)

                刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操

         作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用

         程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶

         段:

         - 等待数据准备 (Waiting for the data to be ready)

         - 将数据从内核拷贝到用户进程中

           (Copying the data from the kernel to the process)

         正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。

         - 阻塞 I/O(blocking IO)

         - 非阻塞 I/O(nonblocking IO)

         - I/O 多路复用( IO multiplexing)

         - 信号驱动 I/O( signal driven IO)

         - 异步 I/O(asynchronous IO)

         注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四

                种IO Model。

       (1)阻塞 I/O(blocking IO)

                       在linux中,默认情况下所有的socket都是blocking,一个典

                型的读操作流程大概是这样(左边是用户空间,右边是内核空

                间):

           

                             当用户进程调用了recvfrom这个系统调用,kernel(内核)就

                      开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候

                      数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。

                      这个时候kernel就要等待足够的数据到来)。这个过程需要等待,

                      也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程

                      的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己

                      选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从

                      kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除

                      block的状态,重新运行起来。

                              所以,blocking IO的特点就是在IO执行的两个阶段都被

                      block了。

       (2)非阻塞 I/O(nonblocking IO)

                       linux下,可以通过设置socket使其变为non-blocking。当对一个

                non-blocking socket执行读操作时,流程是这个样子(左边是用户

                空间,右边是内核空间):

         

                        当用户进程发出read操作时,如果kernel中的数据还没有准备好,

                那么它并不会block用户进程,而是立刻返回一个error。从用户进程角

                度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一

                个结果。用户进程判断结果是一个error时,它就知道数据还没有准备

                好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,

                并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到

                了用户内存,然后返回。

                       所以,nonblocking IO的特点是用户进程需要不断的主动询问

                kernel数据好了没有。

       (3)I/O 多路复用( IO multiplexing)

                       IO multiplexing就是我们说的select,poll,epoll,有些地方也称这

                种IO方式为event driven IO。select/epoll的好处就在于单个process就

                可以同时处理多个网络连接的IO。首先理解两个概念,“多路”和“复用”。

                * 多路

                         指的是多条独立的IO流,IO流可以这么理解:读是一条流(称之

                   为读流,比如输入流),写是一条流(称之为写流,比如输出流),

                   异常也是一条流(称之为异常流),每条流用一个文件描述符表示,

                   同一个文件描述符可以同时表示读流和写流。

                   * 复用

                            复用的是线程,复用线程来跟踪每路IO的状态,然后用一个线程

                     就可以处理所有的IO。

                            当然,不提什么I/O多路复用也能在一个线程就处理完所有的IO

                     流,用个while循环挨个处理一次不就解决了嘛?那为什么还要提出

                     这个技术呢?原因就是刚才我们想的方法(轮询)效率太低了,资

                     源利用率也不高。试想一下,在单核CPU的情况下,如果设置成了

                     阻塞IO,那么其他的IO将被卡死,也就浪费掉了其他的IO资源。另

                     一方面,假设所有IO被设置成非阻塞,那CPU一天到晚也不用干别

                     的事了,就在这不停的问,现在可以进行IO操作了吗,直到有一个

                     设备准备好环境才能进行IO,也就是在设备准备io环境的这一段时

                     间,cpu是没必要瞎问的,问了也没结果。

                            随后硬件发展起来了,有了多核的概念,也就有了多线程。这个

                     时候可以这样做,来一条IO我开一个线程,这样的话再也不用轮询了。

                     然而,管理线程是要耗费系统资源的,程序员也开始头疼了,线程之

                     间的交互是十分麻烦的。这样一来程序的复杂性蹭蹭蹭地往上涨,IO

                     效率是可能提高了,但是软件的开发效率却可能减低了。

                            所以也就有了I/O多路复用这一技术。简单来说,就是一个线程

                     追踪多条IO流(读,写,异常),但不使用轮询,而是由设备本身

                     告知程序(select)哪条流可用了,这样一来就解放了CPU,也充

                     分利用IO资源,下文主要讲解如何实现这一技术,linux下这一技术

                     有三个实现,select,poll,epoll。今天主要记录自己对select的理

                     解,从接口到原理再到实现。

                     select及其原理

                            * 接口定义
                                   

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 

           struct timeval *timeout);

- readfds,读流集合,也就是程序员希望从这些描述符中读内容

- writefds,写流集合,也就是程序员希望向这些描述符中写内容

- exceptfds,异常流集合,也就是中间过程发送了异常

- nfds,上面三种事件中,最大的文件描述符+1

- timeout,程序员的容忍度,可等待的时间

struct timeval{

  long tv_sec;//second

  long tv_usec;//minisecond

}

timeout有三种取值:

NULL:select一直阻塞,知道readfds、writefds、exceptfds集合中至少一个文件描述符可用才唤醒

0:select不阻塞

timeout_value:select在timeout_value这个时间段内阻塞

如果非得与“多路”这个词关联起来,那就是readfds+writefds+exceptfds的数量和就是路数。

另外,还有一组与fd_set 有关的操作

FD_SET(fd, _fdset),把fd加入_fdset集合中

FD_CLR(fd, _fdset),把fd从_fdset集合中清除

FD_ISSET(fd, _fdset),判定fd是否在_fdset集合中

FD_ZERO(_fdset),清除_fdset有描述符

                            * select实现原理

                                     select的实现依赖于设备的驱动函数poll,poll的功能是检

                              查设备的哪条条流可用(一个设备一般有三条流,读流,写流,

                              设备发生异常的异常流),如果其中一条流可用,返回一个

                              mask(表示可用的流),如果不可用,把当前进程加入设备

                              的流等待队列中,例如读等待队列、写等待队列,并返回资源

                              不可用。

                                     select正是利用了poll的这个功能,首先让程序员告知自己

                              关心哪些IO流(用文件描述符表示,也就是上文的readfds、

                              writefds和exceptfds),并让程序员告知自己这些流的范围

                            (也就是上文的nfds参数)以及程序的容忍度(timeout参数),

                             然后select会把她们拷贝到内核,在内核中逐个调用流所对应

                             的设备的驱动poll函数,当范围内的所有流也就是描述符都遍

                             历完之后,他会检查是否至少有一个流发生了,如果有,就修

                             改那三个流集合,把她们清空,然后把发生的流加入到相应的

                             集合中,并且select返回。如果没有,就睡眠,让出cpu,直

                             到某个设备的某条流可用,就去唤醒阻塞在流上的进程,这个

                             时候,调用select的进程重新开始遍历范围内的所有描述符。

                             直接看这个步骤可能会好理解些

                             - 拷贝nfds、readfds、writefds和exceptfds到内核

                             - 遍历[0,nfds)范围内的每个流,调用流所对应的设备的驱动poll

                               函数

                             - 检查是否有流发生,如果有发生,把流设置对应的类别,并执

                               行4,如果没有流发生,执行5。或者timeout=0,执行

                             - 4select返回

                             - select阻塞当前进程,等待被流对应的设备唤醒,当被唤醒时,

                               执行2。或者timeout到期,执行4

                              select在内核中的流程图

                            * select实现

int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
         retval = 0;        //retval用于保存已经准备好的描述符数,初始为0
         for (;;) {
                   unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
                   long __timeout;
                   set_current_state(TASK_INTERRUPTIBLE);    //将当前进程状态改为TASK_INTERRUPTIBLE,可中断
                   inp = fds->in; outp = fds->out; exp = fds->ex;
                   rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
 
                   for (i = 0; i < n; ++rinp, ++routp, ++rexp) { //遍历每个描述符
                            unsigned long in, out, ex, all_bits, bit = 1, mask, j;
                            unsigned long res_in = 0, res_out = 0, res_ex = 0;
                            const struct file_operations *f_op = NULL;
                            struct file *file = NULL;
 
                            in = *inp++; out = *outp++; ex = *exp++;
                            all_bits = in | out | ex;
                            if (all_bits == 0) {
                                     i += __NFDBITS;       //all_bits的类型是unsigned long int ,大小为4个字节32位,all_bits=0,说明连续32个描述符(流)不在readdfs、writedfs、execptdfs集合中,所以i+=32,而__NFDBITS=32。
                                     continue;
                            }
 
                            for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {     //遍历每个长字里的每个位
                                     int fput_needed;
                                     if (i >= n)
                                               break;
                                     if (!(bit & all_bits))
                                               continue;
                                     file = fget_light(i, &fput_needed);
                                     if (file) {
                                               f_op = file->f_op;
                                               MARK(fs_select, "%d %lld", i, (long long)*timeout);
                                               mask = DEFAULT_POLLMASK;
                                               if (f_op && f_op->poll)
                                                        mask = (*f_op->poll)(file, retval ? NULL : wait);//调用设备的驱动poll函数
                                               fput_light(file, fput_needed);
                                               if ((mask & POLLIN_SET) && (in & bit)) {
                                                        res_in |= bit; //如果是这个描述符可读, 将这个位置位
                                                        retval++;  //返回描述符个数加1
                                               }
                                               if ((mask & POLLOUT_SET) && (out & bit)) {
                                                        res_out |= bit;
                                                        retval++;
                                               }
                                               if ((mask & POLLEX_SET) && (ex & bit)) {
                                                        res_ex |= bit;
                                                        retval++;
                                               }
                                     }
                            }
                            if (res_in)
                                     *rinp = res_in;
                            if (res_out)
                                     *routp = res_out;
                            if (res_ex)
                                     *rexp = res_ex;
                   }
                   wait = NULL;
                   if (retval || !*timeout || signal_pending(current))//如果retval!=0,也就是有readdfs、writedfs、execptdfs至少有一个发生,跳出循环
                            break;
                   /*以下处理timeout参数*/
            __timeout = schedule_timeout(__timeout);
                   if (*timeout >= 0)
                            *timeout += __timeout;
         }
         __set_current_state(TASK_RUNNING);
     return retval;
}

                   补充一张IO多路复用的图

          

                   下面的内容和上面的内容差不多,只不过来自两篇博客

                  上图具体过程分大约两个阶段:

                         第一阶段:当用户进程调用了select,那么整个进程会被block,

                                           而同时,kernel会“监视”所有select负责的socket

                                         (IO流),当任何一个socket(IO流)中的数据准备

                                           好了,select就会返回。

                         第二阶段:用户进程再调用read操作,将数据从kernel拷贝到用

                                           户进程。

                        所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待

                 多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一

                 个进入读就绪状态,select()函数就可以返回。

                        这个图和blocking IO的图其实并没有太大的不同,事实上,还更

                 差一些。因为这里需要使用两个system call (select 和 recvfrom),而

                 blocking IO只调用了一个system call (recvfrom)。但是,用select的

                 优势在于它可以同时处理多个connection。

                        所以,如果处理的连接数不是很高的话,使用select/epoll的

                 web server不一定比使用multi-threading + blocking IO的

                 web server性能更好,可能延迟还更大。select/epoll的优势并不是对

                 于单个连接能处理得更快,而是在于能处理更多的连接。

                        在IO multiplexing Model中,实际中,对于每一个socket,一般

                 都设置成为non-blocking,但是,如上图所示,整个用户的process其

                 实是一直被block的。只不过process是被select这个函数block,而不

                 是被socket IO给block。

       (4)异步 I/O(asynchronous IO,AIO)

                Linux下的asynchronous IO其实用得很少。先看一下它的流程:

     

                       用户进程发起read操作之后,立刻就可以开始去做其它的事。而

                另一方面,从kernel的角度,当它受到一个asynchronous read之后,

                首先它会立刻返回,所以不会对用户进程产生任何block。然后,

                kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切

                都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完

                成了。

一. 网络编程的一些基础

     1.先说明一下线程的挂起、阻塞、睡眠

        线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状

        态、阻塞状态及死亡状态。下图是使用时间片轮转法的操作系统进程的状态和它们之

        间的转换。

               挂起和睡眠是主动的,挂起恢复需要主动完成,睡眠恢复则是自动完成的,因为

        睡眠有一个睡眠时间,睡眠时间到则恢复到就绪态。而阻塞是被动的,是在等待某种

        事件或者资源的表现,一旦获得所需资源或者事件信息就自动回到就绪态。睡眠和挂

        起是两种行为,阻塞则是一种状态。

        挂起:一般是主动的,由系统或程序发出,甚至于辅存中去join()、wait()是挂起。

        阻塞:一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待某种资

                   源或信号量(即有了资源)将他唤醒。sleep()是属于阻塞。

     2.同步/异步

        同步和异步关注的是消息通信机制。

      (1)同步:就是指在发出一个功能调用时,在没有得到结果之前,该调用就不返回。

                          按照这个定义,其实绝大多数函数都是同步调用。

      (2)异步:概念和同步相对。 当一个异步过程调用发出后,调用者不会立刻得到结

                          果。实际处理这个调用的部件是在调用发出后, 通过状态、通知来通知

                          调用者,或通过回调函数处理这个调用。

      举例:当到银行后, .可以去ATM机前排队等候 – (排队等候)就是同步等待消息 .可以去

                 大厅拿号,等到排到我的号时, 柜台的人会通知我轮到我去办理业务. – (等待别

                 人通知)就是异步等待消息。

     3.阻塞/非阻塞

        阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态(无所谓同

        步或者异步,注意这里的“调用结果”就是指“2”中标黑的“调用”的结果)

      (1)阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起(被挂起就是阻塞,

                          因为是被动的)。函数只有在得到结果之后才会返回。 有人也许会把阻

                          塞调用和同步调用等同起来,实际上他是不同的。 对于同步调用来说,

                          很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

      (2)非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不

                             会阻塞当前线程,而会立刻返回。

        举例:继续上面的那个例子,不论是排队等待,还是使用号码等待通知,如果在这个

                   等待的过程中,等待者除了等待消息之外不能做其它的事情,那么该机制就是

                   阻塞的,表现在程序中,也就是该程序一直阻塞在该函数调用处不能继续往下

                   执行。相反,有的人喜欢在银行办理这些业务的时候一边打打电话发发短信一

                   边等待,这样的状态就是非阻塞的。

二. Java BIO、NIO、AIO

      1.先看一些详细解释,否则直接看2看不懂

         在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一

         个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端

         需要对每个请求建立一堆线程等待请求(如果这个连接不做任何事情会造成不必要的

         线程开销)而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直

         等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。

                 BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个

         连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用

         一个线程。如下图,上面为BIO,下面为NIO。

         

               NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注

        册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的

        多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就

        是一个请求一个线程模式。

                在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应

        用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一

        样的问题(不过2中的NIO中说的,通过注册的回调函数来通知客服端服务器端已经可以

        处理请求的方式,避免了这种问题),这时,就进化出了AIO。

               HTTP/1.1出现后,有了Http长连接,这样除了超时和指明特定关闭的http header外,

        这个链接是一直打开的状态的,这样在NIO处理中可以进一步的进化,在后端资源中可

        以实现资源池或者队列,当请求来的话,开启的线程把请求和请求数据传送给后端资源

        池或者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接的哪个请求等),

        这样前面的线程还是可以去接受其他的请求, 而后端的应用的处理只需要执行队列里面

        的就可以了,这样请求处理和后端应用是异步的(这就是AIO异步的体现)。当后端处

        理完,到全局地方得到现场,产生响应,这个就实现了异步处理。

        可见:BIO是一个连接一个线程;NIO是一个请求一个线程;AIO是一个有效请求一个线

                   程。

        在举一个烧水的例子来说明一下:

        * AIO的做法是,每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。

        * NIO的做法是,叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处

          理。

        * BIO的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水

          壶。

      2.这回在看概念总结

       (1)Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连

                                    接请求时服务器端就需要启动一个线程进行处理,如果这个连接不

                                    做任何事情会造成不必要的线程开销,当然可以通过线程池机制改

                                    善。

                                    好理解的解释:线程发起IO请求,不管内核是否准备好IO操作,从

                                                             发起请求起,线程一直阻塞,直到操作完成。

       (2)Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送

                                     的连接请求都会注册到多路复用器上,多路复用器轮询到连接有

                                     I/O请求时才启动一个线程进行处理。

                                     好理解的解释:线程发起IO请求,立即返回;内核在做好IO操作的

                                                              准备之后,通过调用注册的回调函数通知线程做IO

                                                              操作,线程开始阻塞,直到操作完成。

       (3)Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客

                                                户端的I/O请求都是由OS先完成了再通知服务器应用去启动

                                                线程进行处理。

                                                好理解的解释:线程发起IO请求,立即返回;内存做好IO操

                                                                         作的准备之后,做IO操作,直到操作完成或

                                                                         者失败,通过调用注册的回调函数通知线程

                                                                         做IO操作完成或者失败。

      2. BIO、NIO、AIO的使用场景

          (1)BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比

                   较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

          (2)NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,

                   并发局限于应用中,编程比较复杂,JDK1.4开始支持。

          (3)AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,

                  充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值