GVKun编程网logo

JAVA IO BIO NIO AIO

1

关于JAVAIOBIONIOAIO的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于11Java网络IO编程总结(BIO、NIO、AIO均含完整实例代码)、BIO&NIO&AIO、bio,n

关于JAVA IO BIO NIO AIO的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于11 Java 网络 IO 编程总结(BIO、NIO、AIO 均含完整实例代码)、BIO&NIO&AIO、bio,nio,aio、BIO,NIO,AIO,select,poll,epoll详解等相关知识的信息别忘了在本站进行查找喔。

本文目录一览:

JAVA IO BIO NIO AIO

JAVA IO BIO NIO AIO

一、什么是IO

IO 输入、输出 (read write accept)IO是面向流的

二、BIO

BIO是同步阻塞式IO 服务端与客户端进行三次握手后一个链路建立一个线程面向流的通信
在单线程模式下只能为一个客户端服务  可以采用建立线程池来创建多个服务 然而这样建立多个线程是对性能消耗非常大的 
while(true){ 
socket = accept();//阻塞等待client连接,直到client连接成功。 
handle(socket) 
} 

三、NIO

同步非阻塞式IO 以块的方式处理数据 面向缓存区的 采用多路复用Reactor模式 基于事件驱动
Netty是实现了NIO的一个流行框架,JBoss的。Apache的同类产品叫Mina。阿里云分布式文件系统TFS里用的就是Mina。

150046-20170901082719280-29887948.png

四、AIO

异步非阻塞式IO 基于unix事件驱动,不需要多路复用器对注册通道进行轮询,采用Proactor设计模式。

五、什么是多路复用

所谓的多路复用是指 多路是多个网络连接 复用是复用同一个线程 在同一个线程里面 通过拨开关的方式,来同时传输多个I/O流 经典的像Nginx是一个多进程单线程的模型
22312037_13832287181u34.png

Nginx会有多个连接进来 epoll会把他们监视起来 谁有请求就拨向谁然后调用响应的代码处理

43136-20170319171538682-1895749865.jpg


参考文章:

http://www.iteye.com/magazines/132-Java-NIO
https://www.cnblogs.com/xiexj/p/6874654.html



11 Java 网络 IO 编程总结(BIO、NIO、AIO 均含完整实例代码)

11 Java 网络 IO 编程总结(BIO、NIO、AIO 均含完整实例代码)

Java 网络 IO 编程总结(BIO、NIO、AIO 均含完整实例代码)

 

 

BIO、NIO、AIO 的基本定义与类比描述:

  • BIO (Blocking I/O):同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO 的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

  • NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞 I/O 模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO 的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。

  • AIO ( Asynchronous I/O):异步非阻塞 I/O 模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有 IO 操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

进程中的 IO 调用步骤大致可以分为以下四步: 

  1. 进程向操作系统请求数据;

  2. 操作系统把外部数据加载到内核的缓冲区中; 

  3. 操作系统把内核的缓冲区拷贝到进程的缓冲区;

  4. 进程获得数据完成自己的功能;

当操作系统在把外部数据放到进程缓冲区的这段时间(即上述的第二,三步),如果应用进程是挂起等待的,那么就是同步 IO,反之,就是异步 IO,也就是 AIO 。

 

从编程模式上来看 AIO 相对于 NIO 的区别在于,NIO 需要使用者线程不停的轮询 IO 对象,来确定是否有数据准备好可以读了,而 AIO 则是在数据准备好之后,才会通知数据使用者,这样使用者就不需要不停地轮询了。

 

http://blog.csdn.net/anxpp/article/details/51512200

 

https://mp.weixin.qq.com/s/lZGL6Tpb2Lpd3EvqnB-0ig

 

https://mp.weixin.qq.com/s?__biz=MzIzMzgxOTQ5NA==&mid=2247483922&idx=1&sn=c9fdc5cd64df5a412b9dc2fc6f9e4880&chksm=e8fe9e1bdf89170d9f0d08dc0320b15238869dd6c8b9496d82b1af75d0ddc102327438029a89&scene=21#wechat_redirect

BIO&NIO&AIO

BIO&NIO&AIO

同步与异步 

    同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。

阻塞与非阻塞

    阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

    同 / 异、阻 / 非堵塞 组合,有四种类型,如下表:

组合方式 性能分析
同步阻塞 最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态。
同步非阻塞 提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。 这种方式通常能提升 I/O 性能,但是会增加 CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上。
异步阻塞 这种方式在分布式数据库中经常用到,例如在网一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些备份记录通常都是采用异步阻塞的方式写 I/O。异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况。
异步非阻塞 这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高。

 

BIO

    

    BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。

    BIO 每个连接都需要占用一个线程没有读到 IO 流的时候都处于阻塞。IO 阻塞会导致线程无法释放,即便是不需要 IO 时也会阻塞,会导致服务端线程增大,线程池也无法解决。

        

    优点:代码比较简单、直观;

    缺点: IO 的效率和扩展性很低,容易成为应用性能瓶颈。

    代码实现

    Server 端首先创建一个 serverSocket 来监听 8000 端口,然后创建一个线程,线程里面不断调用阻塞方法 serverSocket.accept() 获取新的连接,见 (1)。
    当获取到新的连接之后,给每条连接创建一个新的线程,这个线程负责从该连接中读取数据,见 (2),
    然后读取数据是以字节流的方式,见 (3)。
public class IOServer {
    public static void main(String[] args) throws Exception {

        ServerSocket serverSocket = new ServerSocket(8000);

        // (1) 接收新连接线程
        new Thread(() -> {
            while (true) {
                try {
                    // (1) 阻塞方法获取新的连接
                    Socket socket = serverSocket.accept();

                    // (2) 每一个新的连接都创建一个线程,负责读取数据
                    new Thread(() -> {
                        try {
                            int len;
                            byte[] data = new byte[1024];
                            InputStream inputStream = socket.getInputStream();
                            // (3) 按字节流方式读取数据
                            while ((len = inputStream.read(data)) != -1) {
                                System.out.println(new String(data, 0, len));
                            }
                        } catch (IOException e) {
                        }
                    }).start();

                } catch (IOException e) {
                }

            }
        }).start();
    }
}

    客户端相对简单,连接上服务端的 8000 端口之后,每隔 2 秒,我们向服务端写一个带有时间戳的 "hello world"。

public class IOClient {

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Socket socket = new Socket("127.0.0.1", 8000);
                while (true) {
                    try {
                        socket.getOutputStream().write((new Date() + ": hello world").getBytes());
                        Thread.sleep(2000);
                    } catch (Exception e) {
                    }
                }
            } catch (IOException e) {
            }
        }).start();
    }
}

 

NIO

    

    NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。

    

    对比 BIO 的同步阻塞 IO 操作,实际上 NIO 是同步非阻塞 IO:

        一个线程在同步的进行轮询检查,Selector 不断轮询注册在其上的 Channel,某个 Channel 上面发生读写连接请求,这个 Channel 就处于就绪状态,被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。

        同步和异步说的是消息的通知机制,这个线程仍然要定时的读取 stream,判断数据有没有准备好,client 采用循环的方式去读取(线程自己去抓去信息),CPU 被浪费。

        非阻塞:体现在这个线程可以去干别的,不需要一直在这等着。Selector 可以同时轮询多个 Channel,因为 JDK 使用了 epoll () 代替传统的 select 实现,没有最大连接句柄限制。所以只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

 

    多路复用器 selector

    当一个客户端请求到来的时候,我们会将其(Channel)注册到 Selector 上,然后 Selector 会不断的轮询注册在其上的 Channel,如果某个 Channel 上面发生了读或者写事件,这个 Channel 就会处于就绪状态,会被 Selector 轮询出来,然后通过调用方法获取所有就绪 Channel 的集合,进行后续的操作。

    一个多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了 epoll() 代替传统的 select,所以没有数量 1024/2048 的上限限制。这也就意味着每一个线程负责 Seletor 的轮询,就可以接入成千上万个客户端,这确实是非常巨大的进步。

    通道 Channel

    可以将其想象成一个水管,一个客户端的连接成功,可以想象成这根水管一头插入了服务器,一头插入了客户端,它们之间的通信就靠的这根水管。

    与传统的流不同,流只能在一个方向是移动(如上述代码,input 只能写入,output 只能写出)。但是 Channel 是全双工的,意思是能同时支持读写操作。

    

    缓存区 Buffer

    在 NIO 库类中加入了一个 Buffer 对象。它区别于传统的流,能写入或者将数据直接读到 Stream 对象中。NIO 所有数据都是基于 Buffer 处理的,在读取数据的时候直接读取 Buffer 里的数据,写数据的时候直接往 Buffer 里写数据。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作的。

    通常情况下,操作系统的一次写操作分为两步: 1. 将数据从用户空间拷贝到系统空间。 2. 从系统空间往网卡写。同理,读操作也分为两步: ① 将数据从网卡拷贝到系统空间; ② 将数据从系统空间拷贝到用户空间。

    但是值得注意的是,如果使用了 DirectByteBuffer(继承 Buffer),一般来说可以减少一次系统空间到用户空间的拷贝。但 Buffer 创建和销毁的成本更高,更不宜维护,通常会用内存池来提高性能。

    如果数据量比较小的中小应用情况下,可以考虑使用 heapBuffer;反之可以用 directBuffer。

    多路复用 IO 模型JAVA NIO 就是采用此模式

    在多路复用 IO 模型中,会有一个线程(Java 中的 Selector)不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。

     select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式:

    代码实现

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4,
        60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        try (Selector selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
            serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select(); // 阻塞等待就绪的Channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {
                        channel.write(Charset.defaultCharset().encode("你好,世界"));
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
// Socket 客户端(接收信息并打印)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("NIO 客户端:" + s));
} catch (IOException e) {
    e.printStackTrace();
}

 

AIO

    AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

    AIO 不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写。

    NIO 采用轮询的方式,一直在轮询的询问 stream 中数据是否准备就绪,如果准备就绪发起处理。但是 AIO 就不需要了,AIO 框架在 windows 下使用 windows IOCP 技术,在 Linux 下使用 epoll 多路复用 IO 技术模拟异步 IO, 即:应用程序向操作系统注册 IO 监听,然后继续做自己的事情。操作系统发生 IO 事件,并且准备好数据后,在主动通知应用程序,触发相应的函数(这就是一种以订阅者模式进行的改造)。由于应用程序不是 “轮询” 方式而是订阅 - 通知方式,所以不再需要 selector 轮询,由 channel 通道直接到操作系统注册监听。

    在 JDK1.7 中,这部分内容被称作 NIO.2,主要在 java.nio.channels 包下增加了下面四个异步通道:
    AsynchronousSocketChannel
    AsynchronousServerSocketChannel
    AsynchronousFileChannel
    AsynchronousDatagramChannel

    异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了,再通知服务器应用去启动线程进行处理。

Thread sThread = new Thread(new Runnable() {
    @Override
    public void run() {
        AsynchronousChannelGroup group = null;
        try {
            group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));
            AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            server.accept(null, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
                @Override
                public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
                    server.accept(null, this); // 接收下一个请求
                    try {
                        Future<Integer> f = result.write(Charset.defaultCharset().encode("你好,世界"));
                        f.get();
                        System.out.println("服务端发送时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                        result.close();
                    } catch (InterruptedException | ExecutionException | IOException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
                }
            });
            group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
});

sThread.start();
// Socket 客户端
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();

Future<Void> future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));

future.get();

ByteBuffer buffer = ByteBuffer.allocate(100);

client.read(buffer, null, new CompletionHandler<Integer, Void>() {
    @Override
    public void completed(Integer result, Void attachment) {
        System.out.println("客户端打印:" + new String(buffer.array()));
    }

    @Override
    public void failed(Throwable exc, Void attachment) {
        exc.printStackTrace();
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

Thread.sleep(10 * 1000);

bio,nio,aio

bio,nio,aio

下面我们再来理解组合方式的 IO 类型,就好理解多了。 

同步阻塞 IO(JAVA BIO): 
    同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 

同步非阻塞 IO (Java NIO) : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。用户进程也需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问。 

异步阻塞 IO(Java NIO):  
   此种方式下是指应用发起一个 IO 操作以后,不等待内核 IO 操作的完成,等内核完成 IO 操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问 IO 是否完成,那么为什么说是阻塞的呢?因为此时是通过 select 系统调用来完成的,而 select 函数本身的实现方式是阻塞的,而采用 select 函数有个好处就是它可以同时监听多个文件句柄(如果从 UNP 的角度看,select 属于同步操作。因为 select 之后,进程还需要读写数据),从而提高系统的并发性!  


(Java AIO (NIO.2))异步非阻塞 IO:  
   在此种模式下,用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 IO 读写操作,因为真正的 IO 读取或者写入操作已经由内核完成了。    

BIO,NIO,AIO,select,poll,epoll详解

BIO,NIO,AIO,select,poll,epoll详解

了解BIO,NIO,AIO就需要了解几个概念:,

概念1: BIO是同步阻塞,NIO是同步非阻塞,AIO是异步非阻塞, NIO,BIO的实质都调用系统内核提供的不同的方法;

概念2:select,poll,epoll都是实现NIO的一种技术,其实质就是系统内核提供的一个API。从select到epoll,每一次系统内核的升级,每一个新的思想和技术的更新,都为了解决上一代思想和技术存在的问题;

概念3:在Linux操作系统中,万物皆文件,设备,套接字,目录等都是文件,每一个打开的文件(正在使用的套接字也是一个打开的文件)都对应一个文件描述符(非负整数,类似Java的变量,C语言的指针);

概念4:每个进程的PCB中都有一个指向一张表的指针,该表最重要的部分就是包含一个指针数组,数组里的每个元素就是一个文件描述符,每个文件描述符都指向一个文件 ,一个进程中,文件描述符默认从0开始,0表示IO标准输入,1表示标准输出,进程的文件描述符位于/proc/进程号/fd/目录下,如下图所示;

有了以上概念,下面将具体介绍BIO和NIO:

BIO : Block-IO 阻塞IO

BIO的实质就是调用内核的socket(),bind(),listen(),accept()等内核AIP完成服务端对客户端的监听;

BIO实例

上图是一个Java代码编写的简单的socket监听程序,该程序执行时(线程在执行时),系统内核进行了以下几步操作:

1.调用内核socket(...,...)方法,创建一个服务端socket,该方法返回一个数值,该数值被称为文件描述符(fd),这个fd指向这个服务端socket;

2.调用内核bind(5,8090,...)方法绑定socket(fd=5),以及端口8090;

3.调用内核listen(5,....)方法开启监听,监听socket(fd=5);

4.调用accept(5,)阻塞,等待客户端建立连接;

5.当有客户端建立连接时,内核accept()方法返回该客户端socket的文件描述符(比如fd=6) ,Java代码通过accept  ()返回的客户端socket实际就是这个fd=6,只是被Java包装成了Socket类;

6.调用内核recvfrom(6,....)方法,获取fd=6的客户端发送的数据,该方法是阻塞的;

7.调用内核write(1,....)方法,将获取到的数据输出(写入fd=1 ,文件描述符1表示输出);

以上是一个Java socket程序在建立监听到数据时与内核交互的主要流程;

(注意,以上流程基于Java1.4版本之前,使用BIO技术的老版本Java。1.4之后的Java版本,在实际监听时已经不使用BIO阻塞等待了,而是使用poll,本文主要为了循序渐进的介绍BIO和NIO)

以上的方案:问题主要有几点:

1.当有一个客户端建立连接后,服务端线程会 一直阻塞,等待客户端发送数据,此时f服务端无法再接收其他客户端的连接;

2.如果按照图中,为了能接收不同的客户端,每接收一个客户端就新建一个线程,则资源过于浪费(创建新线程的相关系统资源耗费很大);

因此,BIO方案在连接量大的时候是行不通的。

由以上分析可以得出,BIO方案要么需要一直阻塞等待,要么就需要创建大量的线程,而创建线程也是为了解决阻塞的问题,因而根源就在于---阻塞!

如果能解决阻塞,就可以解决BIO存在的问题。

为了解决BIO的问题,提出了NIO的概念,NIO主要有两层意思:

1.:框架层面: new-IO

2.系统内核层面 : non-block 非阻塞IO。

NIO的实质就在于系统内核调用上发生了变化,当调用内核socket()方法时,传入SOCK_NONLOCK属性,则在后续调用accept()或者recvfrom()方法时无论是否获取到数据,都会立即返回,如果有数据(有客户端建立连接),则返回该客户端的文件描述符,如果未获取到信息,则返回负数。

因此上述流程可以更改为:

1.调用socket(...,SOCK_NONLOCK,...)方法,创建一个服务端socket,并得到文件描述符(fd=5),注意此时设置了参数SOCK_NONLOCK,表示该socket是非阻塞的;

2.调用内核bind(5,8090,...)方法绑定socket(fd=5),以及端口8090;

3.调用内核listen(5,....)方法开启监听;

4.调用accept(5,...)无论是否有客户端建立连接,均直接返回,由于程序是while(true)循环,因此会不断重复调用accept(),也就实现了非阻塞,在一个线程内可以监听多个客户端;

5.当有客户端建立连接时,内核accept()方法返回客户端的信息(fd=6);

6.调用内核recvfrom(6,....)方法,获取fd=6的客户端发送的数据,该方法也不再阻塞,如果fd=6的客户端此时没有数据发过来,则recvfrom()直接返回,程序继续循环,此时如果有新的客户端过来(fd=7),则服务端可以正常接收新的客户端访问;

可以看到,我们通过在socket()方法增加SOCK_NONBLOCK参数,将同步阻塞变成了同步非阻塞,实现了简单的NIO。

多路复用器:

这种模式就是SELECT

这其中涉及到一个概念,IO多路复用,是一种同步非阻塞模式,也就是同一个线程能够监控(轮询)多个channel,或者说一次系统调用,可以同时获取多个IO的状态(句柄 fd)。 

select 就是一种多路复用器的实现

但以上方案也有问题:

1.为了获取到客户端的响应数据,服务端需要不断的调用recvfrom()方法,轮询每个客户端(fd)。极端情况下,如果有10000个客户端,但只有1个客户端有响应数据,则需要调用10000次recvfrom(),但只有一次是有意义的,其他9999次都是浪费!

为了解决上述这种做无用功的问题,内核提供了select()方法,在调用recvfrom()方法之前先调用一次select()方法,将需要监听的客户端都传入select()中(实际传入的就是fd的列表),该方法会通过返回值告诉程序哪些客户端(fd)有数据响应,则程序只需要对有数据响应的客户端调用recvfrom()方法,获取数据。

poll和select是类似的原理,不再赘述。

SELECT方式减少了recvfrom()方法的调用,解决了一重复调用导致的资源消耗问题,但select本身还是有问题:

1.每次调用select方法都需要把所有的fd作为参数传给内核,在有很多个fd的时候,如果每次都作为参数传过去,也是很耗费资源的;

2.实际上内核对于select()方法的实现方式也是遍历,也就是说select()方法把程序调内核遍历变成了内核自己遍历自己,虽然减少了外部程序调内核的开销,但遍历的本质没有变;

为了彻底解决每次都传值以及循环遍历导致的性能开销问题,人们提出了几种方案:

1.对于每次传fd列表的问题,在内存中开辟一个空间,将fd列表存储起来,以后只在有新客户端建立时才传fd;

2.对于循环遍历的问题,引入了操作系统的中断事件概念,在操作系统中,任何操作都会产生一个中断,CPU通过中断的级别来判断需要优先处理哪些中断。数据传输也是一样,当有数据进来时(数据通过IO进入内存后),操作系统会产生一个中断事件,可以通过监听这种中断事件达到监听数据的目的。

这种基于中断事件的技术就是epoll

系统内核提供了三个epoll的方法:

epoll_create():先在内存创建一个eventpoll对象,并返回该对象的文件描述符efd,eventpolll对象包含一个就绪队列和一个等待队列,等待队列使用了红黑树,便于数据的删除和增加,因为epoll_ctl()方法会不断的增加和删除等待队列的数据,就绪队列使用了一个双向链表; 这里需要注意的是,这颗红黑树是采用了mmap技术,也就是说这颗红黑树的内存是用户空间和内核空间共享的,当有一个新的文件描述符产生时,用户线程对该红黑树进行操作,添加一个节点,内核就可以通过操作该空间,直接读取到最新的文件描述符集合。

epoll_ctl(efd,add,fd,accept):该方法用于向eventpoll对象的等待队列添加需要监听的socket,监听socket的的accept事件;

epoll_wait(efd):监听 eventpoll对象的就绪列表,有数据则返回,没有则阻塞,epoll_wait()可以设置超时等待时间,负数表示一直阻塞;

1.调用socket(...,SOCK_NONLOCK,...)方法,创建一个服务端socket,并得到文件描述符(fd=5),注意此时设置了参数SOCK_NONLOCK,表示该socket是非阻塞的;

2.调用内核bind(5,8090,...)方法绑定socket(fd=5),以及端口8090;

3.调用内核listen(5,....)方法开启监听;

4.调用epoll_create()方法创建一个eventpoll对象,并返回该对象的文件描述符efd=8;

5.调用epoll_ctl(efd8,add,fd5,accept)方法,在eventpoll(efd=8)的等待队列中添加socket(fd=5),同时向内核的中断处理程序注册一个回调函数,监听socket(fd=5)的accept事件,该函数的作用就是当有事件响应时,向就绪列表添加该事件(socket)的描述符fd;

6.调用epoll_wait(efd=8),监听eventpoll的就绪队列,是否有事件过来,epoll_wait()返回的是该事件的fd,如果没有则阻塞等待或者直接返回;

7.当有客户端连接进来时(TCP建立连接时,会产生一个中断事件),当事件到来时,中断程序会通过之前注册的回调函数给epollevent对象的就绪列表添加已经就绪的socket的fd(fd=5),当程序调用epoll_wait()的时候,会返回该该socket的fd(fd=5),程序收到后可以判断出是服务端socket的accept()事件有了响应,会调用accept()方法,与新的客户端建立连接,并返回新客户端的fd(fd=9)。然后会再一次调用epoll_ctl(efd8,add,fd9,read),表示向eventpoll(efd8)的等待列表增加客户端efd9的读(read)事件,实现对客户端efd9的读事件监听;

8.此时如果有另外一个客户端连接进来(socket fd=5的accept事件),同时客户端fd9有数据传输进来(socket fd=9 的read事件),中断程序会通过回调函数将这两个socket对应的fd添加到就绪列表,当程序调用eopll_wait()时,这两个fd会通过epoll_wait()返回给服务端,服务端收到后根据判断会分别调用recvfrom()方法读取数据以及accept()返回新的客户端(fd=10),然后调用epoll_ctl(efd8,add,fd10,read)向eventpoll的等待队列增加一个新的客户端fd10的读取事件。

可以看到,程序只有在由新的事件需要监听时才调用epoll_ctl()方法,同时只需要调用epoll_wait()就可以知道有哪些事件到达了,然后再去调用accept()或者recvfrom()方法,如此则解决了select中需要遍历所有客户端的问题。

与select和poll相比,epoll有以下几个优点:

1.通过epoll_ctl()方法将需要监听的socket(fd)放在内核空间efd中,就不需要像select一样每次都把所有的socket作为参数传进去,减少资源消耗;

2.select和poll是需要不断调用函数遍历查询是否有事件到达,而对于epoll而言,当有事件到来时,通过操作系统的中断机制就可以获取到到达的事件,中断程序通过注册的回调函数将事件的描述符fd添加到就绪列表,整个过程不需要程序参与,相当于异步,程序只需要调用epoll_wait()方法收集就绪的事件就可以了,这样就解决了遍历的问题;

3.select和poll在得到事件响应后是需要通过read()或者write()从内核读取数据,数据的流向相当于设备-内核-用户空间,而epoll由于使用了mmap技术,可以将设备的内存地址直接映射到用户空间,数据流向为设备-用户,中间省去了从内核拷贝数据的过程,因此可以节省大量的开销;

关于JAVA IO BIO NIO AIO的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于11 Java 网络 IO 编程总结(BIO、NIO、AIO 均含完整实例代码)、BIO&NIO&AIO、bio,nio,aio、BIO,NIO,AIO,select,poll,epoll详解的相关知识,请在本站寻找。

本文标签: