io,nio,aio总结

I/O简介

I/O(Input/Outpu) 即输入/输出

我们先从计算机结构的角度来解读一下 I/O。

根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。

img

输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。

输入设备向计算机输入数据,输出设备接收计算机输出的数据。

从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。

我们再先从应用程序的角度来解读一下 I/O。

根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space)内核空间(Kernel space )

像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。

并且,用户空间的程序不能直接访问内核空间。

当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。

因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间

我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件)网络 IO(网络请求和响应)

从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

当应用程序发起 I/O 调用后,会经历两个步骤:

  1. 内核等待 I/O 设备准备好数据
  2. 内核将数据从内核空间拷贝到用户空间。

同步/异步,阻塞/非阻塞

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

同步是指: 发送方发出数据后, 等待接收方发回响应后才发下一个数据包的通讯方式. 就是在发出一个调用时, 在没有得到结果之前, 该调用就不返回, 但是一旦调用返回, 就得到返回值了. 也就是由"调用者"主动等待这个"调用"的结果.

异步是指: 发送方发出数据后, 不等待接收方发回响应, 接着发送下个数据包的通讯方式. 当一个异步过程调用发出后, 调用者不会立刻得到结果. 而是在调用发出后, "被调用者"通过状态、通知来通知调用者, 或通过回调函数处理这个调用.

阻塞和非阻塞属于进程API执行动作的方式, 关注的是程序在等待调用结果时的状态.

阻塞是指: 调用结果返回之前, 当前线程会被挂起. 函数只有在得到结果之后才会返回, 线程需要等待结果.

非阻塞是指: 与阻塞的概念相对应, 指在不能立刻得到结果之前, 该函数不会阻塞当前线程, 而会立刻返回. 线程不需要等待结果.

常见的 IO 模型

UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O同步非阻塞 I/OI/O 多路复用信号驱动 I/O异步 I/O

这也是我们经常提到的 5 种 IO 模型。

java中的bio,nio,aio的简介

BIO (Blocking I/O)

BIO 属于同步阻塞 IO 模型

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

在这里插入图片描述

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

BIO的优点:

程序简单,在阻塞等待数据期间,用户线程挂起。用户线程基本不会占用 CPU 资源。

BIO的缺点:

一般情况下,会为每个连接配套一条独立的线程,或者说一条线程维护一个连接成功的IO流的读写。在并发量小的情况下,这个没有什么问题。但是,当在高并发的场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上,BIO模型在高并发场景下是不可用的。

NIO (Non-blocking/New I/O)

Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , SelectorBuffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。流行基于Java NIO通信框架有Mina、Netty、Grizzly等

Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。

我们先来看看 同步非阻塞 IO 模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRbNEDoX-1660641084527)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb174e22dbe04bb79fe3fc126aed0c61%7Etplv-k3u1fbpfcp-watermark.image)]

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

NIO的优点:每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

NIO的缺点:需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。

总之,NIO模型在高并发场景下,也是不可用的。一般 Web 服务器不使用这种 IO 模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。java的实际开发中,也不会涉及这种IO模型。

再次说明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型( IO multiplexing )。

I/O 多路复用模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-euHQtmKi-1660641084528)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88ff862764024c3b8567367df11df6ab%7Etplv-k3u1fbpfcp-watermark.image)]

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。

目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。

  • select 调用 :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
  • epoll 调用 :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。

IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yIrVsKl7-1660641084528)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f483f2437ce4ecdb180134270a00144%7Etplv-k3u1fbpfcp-watermark.image)]

多路复用IO的特点:

IO多路复用模型,建立在操作系统kernel内核能够提供的多路分离系统调用select/epoll基础之上的。多路复用IO需要用到两个系统调用(system call), 一个select/epoll查询调用,一个是IO的读取调用。

和NIO模型相似,多路复用IO需要轮询。负责select/epoll查询调用的线程,需要不断的进行select/epoll轮询,查找出可以进行IO操作的连接。

另外,多路复用IO模型与前面的NIO模型,是有关系的。对于每一个可以查询的socket,一般都设置成为non-blocking模型。只是这一点,对于用户程序是透明的(不感知)。

多路复用IO的优点:

用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。

Java的NIO(new IO)技术,使用的就是IO多路复用模型。在linux系统上,使用的是epoll系统调用。

多路复用IO的缺点:

本质上,select/epoll系统调用,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。

AIO (Asynchronous I/O)

AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FnQGakEj-1660641084528)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3077e72a1af049559e81d18205b56fd7%7Etplv-k3u1fbpfcp-watermark.image)]

异步IO模型的特点

在内核kernel的等待数据和复制数据的两个阶段,用户线程都不是block(阻塞)的。用户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动 IO 。

异步IO模型缺点

需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。

目前来说, Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows 系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。

而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以,这也是在 Linux 下,实现高并发网络编程时都是以 IO 复用模型模式为主

目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。

Java 中的 BIO、NIO、AIO对比。

img

select,poll,epoll 对比

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd_add_1, fd_set *readset, fd_set *write_set, fd_set * exceptset, 
            const struct timeval *timeout);
struct timeval{
    long tv_sec;
    long tv_usec;
};
/*设置fd_set,下面的四个函数都是宏,不能取地址*/
//清空
void FD_ZERO(fd_set *fdset);
//设置一个位
void FD_SET(int fd, fd_set *fdset);
//清除一个位
void FD_CLR(int fd, fd_set *fdset);
/*检查某个位是否被设置,可用于函数返回时判断那个文件描述符就绪*/
int FD_ISSET(int fd,fd_set *fdset);

maxfd_add_1后面三个集中设置了的文件描述符的最大值+1,因为这个值表示的是个数。这个参数存在的意义就是内核在每次唤醒的时候需要遍历的文件描述符个数,因而不用全部遍历所有1024个文件描述符的状态

存在这个参数的原因纯粹是为了效率。因为fd_set的最大值典型的是1024.为了避免所有的都被检查,因此使用这个。

最大值为宏FD_SETSIZE

readsetwrite_setexceptset这三个fd_set类型是一个位图。返回的时候,如果对应的位被设置,那么表示文件描述符就绪,因此需要用户再次手动遍历依次。

select的缺点:
select支持的文件描述符数量太小了,默认是1024

每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

每次调用select都需要在内核遍历传递进来的所有fd,查看有没有就绪的fd,这个开销在fd很多时也很大,效率随FD数目增加而线性下降

poll

#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long n, int timeout);
//成功个数,错误-1,超时0

struct pollfd{
  int fd;
  short events;
  short revents;
};
  • fdarray是一个pollfd类型的数组(首指针),n表示这个数组的个数。
  • pollfdevents成员是要测试的条件,而revents是内核要填充的,表示文件描述符当前的读写状态。

poll方法不再使用位图的方式传入文件描述符符,而是采用一个结构体的方式,因而在遍历每个文件描述符的poll方法的时候,就从结构体的数组中依次遍历,因此突破了select文件描述符的限制。

poll的缺点:
每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

每次调用select都需要在内核遍历传递进来的所有fd,查看有没有就绪的fd,这个开销在fd很多时也很大,效率随FD数目增加而线性下降

epoll

源码

include <sys/epoll.h>
// 创建 epollfd 的函数
int epoll_create(int size);
int epoll_create1(int flags);
// 对要监听的文件描述符的的增加修改删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 开始阻塞
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, 
    int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, 
    int timeout, const sigset_t *sigmask);
// epoll_wait 返回的时候,events表示发生的、
//时间 data则表示与文件描述符相关的信息,可以指向一个结构体,
//也可以是直接的文件描述符
typedef union epoll_data {
    void *ptr; // 常用这个
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
struct epoll_event
{
 //感兴趣的事件如EPOLLIN,EPOLLOUT,EPOLLPRI等
    uint32_t events;  
 //一般使用data的fd成员表示感兴趣的socket
    epoll_data_t data;
};

epoll_ctl() 这个函数是修改epoll关注描述符的。

  1. epfdepoll_create创建的文件描述符
  2. op表示要修改的方式:
    1. POLL_CTL_ADD:添加
    2. EPOLL_CTL_MOD:修改
    3. EPOLL_CTL_DEL:删除
  3. fd参数是要修改关注的文件描述符
  4. event则是要关注的事件了。

epoll_wait() 就是开始阻塞的函数了。

  1. events是已经分配好的epoll_event结构体。这个是一个空的,内核负责帮我们填充.events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存

  2. maxevents是传入要填充的数组的大小。

为什么高效

  1. 每次调用不需要传入所有的文件描述符

select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。

  1. 每次epoll_wait()被唤醒,不需要去遍历所有文件描述符

  2. epoll没有文件描述符数量上的限制。

epoll两种工作方式LT和ET

水平触发(LT)是epoll**缺省的工作方式,**支持阻塞和非阻塞文件描述符。当数据可读写的时候就唤醒epoll_wait,如果不对这个文件描述符作任何操作,内核还是会继续通知,所以,这种模式编程出错误可能性要小一点。传统的select/poll都使用这种模型。

边缘触发(ET)是高速工作方式,只支持非阻塞模式。当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。

LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

epoll的跟select和poll对比的优点:

支持一个进程打开大数目的socket描述符。它所支持的FD上限是最大可以打开文件的数目,这个数字一般远远于2048,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

select和poll每次调用都需要把所有要监听的fd重新拷贝到内核空间;epoll只在调用epoll_ctl时拷贝一次要监听的fd,调用epoll_wait时不需要每次把所有要监听的fd重复拷贝到内核空间。

IO效率不随FD数目增加而线性下降。传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,任一时间只有部分的socket是”活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行操作。这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用callback函数,其他idle状态socket则不会。

select,poll,epoll区别

请添加图片描述

零拷贝

零拷贝基本概念:

零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。

nio中epoll零拷贝原理解析:

请添加图片描述

mmap 通过内存映射,将文件映射到 内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。

零拷贝代码演示:

 /**
     * 零拷贝代码演示
     */
    private static void zeroCopyDemo() throws IOException {
        // 构建零拷贝的输入输出
        FileChannel inChannel = FileChannel.open(Paths.get("C:\\Users\\yojofly\\Desktop\\sps-plugins\\learn\\test.csv"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("C:\\Users\\yojofly\\Desktop\\sps-plugins\\learn\\test_cp.csv"),StandardOpenOption.READ,StandardOpenOption.CREATE,StandardOpenOption.WRITE);
        // 缓冲区初始化,可对比为文件Io读取时候的   new byte[1024]
        MappedByteBuffer inMapperBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMapperBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
        // 获取输入文件的字节长度limit
        byte[] bytes = new byte[inMapperBuffer.limit()];
        // 文件读入缓存中
        inMapperBuffer.get(bytes);
        // 从缓存中取出文件内容写入到新的文件
        outMapperBuffer.put(bytes);
        inChannel.close();
        outChannel.close();
    }

nio中channel,buffer,selector

Channel

Channel是数据传输的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel。
常见的Channel有:

FileChannel 文件传输
DatagramChannel(通常用于UDP)
SocketChannel(通常用于TCP)
ServerSocketChannel(通常用于TCP)

Buffer

内存缓存区,用于暂存从Channel中读取的数据、以及即将写入Channel中的数据。

常见的实现类Buffer:

ByteBuffer 字节

MappedByteBuffer
DirectByteBuffer
HeapByteBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
CharBuffer
从名字上可以看出,这些Buffer支持的是不同数据类型的缓冲区,如short、int等。

常用方法:

put: 写入数据

get:读取数据

flip:将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值

clear:方法,position将被设回0,limit被设置成 capacity的值

compact:方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面

mark :添加标记
reset:恢复到标记位置

通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。

内部结构

ByteBuffer的内部结构类似于一个数组,主要有3个重要属性:

capacity:代表buffer的容量。
position:一个读写指针,可以理解为读写到buffer中的哪个下标。
limit:读写限制,position不能超过limit。

当我们新建一个buffer时,它的内部结构如下:

在这里插入图片描述

写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态:

在这里插入图片描述

flip 动作发生后,position 切换为读取位置,limit 切换为读取限制:

在这里插入图片描述

读取 4 个字节后,状态变成:

在这里插入图片描述

clear 动作发生后,状态变成:

在这里插入图片描述

compact 方法,是把未读完的部分向前压缩,然后切换至写模式

在这里插入图片描述

Selector

在NIO出现之前,在开发服务器端程序时,会采用一种叫做“多线程版设计”的思路,即服务器端每当需要和一个新的客户端进行通信时,就会开启一个新的Socket连接(通过Socket就能读写数据了),并启动一个新的线程来为这个Socket服务(执行读写操作等)。

这种思路的弊端在于,如果客户端太多的话,那么服务器端就需要开启许多线程才能进行服务,这会带来以下问题:

内存占用高。
线程上下文切换成本高。
只适合连接数少的场景。
注意:线程并不是越多越好,因为CPU能并行处理的线程是有限的,一旦线程数量超出CPU能并行处理的数量,多出来的线程就需要等待CPU,而这就会导致线程上下文切换,从而带来额外开销

SelectionKey的四个常量

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

分别表示监听四种不同类型的事件:

  • Connect
  • Accept
  • Read
  • Write

selector常用方法:

open: 创建一个selector连接

select():阻塞到至少有一个通道在你注册的事件上就绪了。

select(long timeout):select()一样,除了最长会阻塞timeout毫秒(参数)。

selectNow():不会阻塞,不管什么通道就绪都立刻返回(此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零)。

selectedKeys():返回所有selector里注册的SelectionKey

代码示例:模拟Nio中网络中的客户端和服务端通信

SelectorClientDemo.java

package com.yojofly.server.learn.nio.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class SelectorClientDemo {
    static Selector selector;

    public static void main(String[] args) throws IOException {
        selector = Selector.open();
        SocketChannel socketChannel = SocketChannel.open();
        // 设置为非阻塞io
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost",8000));
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        while (true){
            selector.select(); //阻塞
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                if (selectionKey.isConnectable()){ // 连接事件
                    // 处理连接事件
                    handleConnect(selectionKey);
                }else if (selectionKey.isReadable()){ // 读事件
                    // 处理读事件
                    handleRead(selectionKey);
                }
            }
        }
    }

    private static void handleConnect(SelectionKey selectionKey) throws IOException{
        SocketChannel socketChannel  = (SocketChannel) selectionKey.channel();
        // 连接中断
        if (socketChannel.isConnectionPending()){
            socketChannel.finishConnect();
        }
        // 设置io非阻塞状态
        socketChannel.configureBlocking(false);
        socketChannel.write(ByteBuffer.wrap("hello server,i am nio client with accept".getBytes()));
        socketChannel.register(selector,SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey selectionKey) throws IOException{
        SocketChannel socketChannel  = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.out.println("client receive Msg:" +new String(byteBuffer.array()));
    }
}

SelectorServerDemo.java

package com.yojofly.server.learn.nio.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class SelectorServerDemo {
    static Selector selector;

    public static void main(String[] args) throws IOException {
        selector = Selector.open();
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        // 设置为非阻塞io
        socketChannel.configureBlocking(false);
        socketChannel.socket().bind(new InetSocketAddress(8000));
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            selector.select(); //阻塞
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                iterator.remove();
                if (selectionKey.isAcceptable()){ // 连接事件
                    // 处理连接事件
                    handleConnect(selectionKey);
                }else if (selectionKey.isReadable()){ // 读事件
                    // 处理读事件
                    handleRead(selectionKey);
                }
            }
        }
    }

    private static void handleConnect(SelectionKey selectionKey) throws IOException{
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.write(ByteBuffer.wrap("hello client,i am nio server with connect".getBytes()));
        socketChannel.register(selector,SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey selectionKey) throws IOException{
        SocketChannel socketChannel  = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.out.println("server receive Msg:" +new String(byteBuffer.array()));
    }
}

文章参考:

同步异步,阻塞非阻塞
https://blog.csdn.net/qq_39515350/article/details/120854214
io多路复用中详解:
https://blog.csdn.net/adminpd/article/details/124553590
io基础知识原理分析:
https://www.cnblogs.com/crazymakercircle/p/10225159.html
select,poll.epoll对比
https://blog.csdn.net/xx_yTm/article/details/54801977
select,poll,epoll源码分析
http://t.zoukankan.com/perfy576-p-8554734.html
nio中channel,buffer,selector的作用:
https://blog.csdn.net/lucas161543228/article/details/125172566

### 回答1: IO(输入输出)是指操作系统与硬件之间进行数据传输的过程。常见的IO操作包括读取和写入文件、网络数据传输等。 NIO (非阻塞IO)是Java中的一种IO编程方式,它支持非阻塞的数据传输,即程序在等待IO操作完成时不会阻塞。这样可以避免程序因等待IO而卡住的问题。NIO提供了异步通道、缓冲区等功能来支持高效的IO操作。 AIO (异步IO)是操作系统提供的另一种IO编程方式,它支持异步的数据传输。AIO允许程序在发起IO请求后立即返回,而不是等待IO操作完成。这样程序可以继续执行其他任务,避免因等待IO而阻塞。AIO在操作系统层面支持异步IO,而NIO是在Java语言层面支持异步IO. ### 回答2: IO,即Input/Output,是指计算机与外部设备之间进行数据传输的过程。传统的IO操作是同步的,即在进行IO操作时,程序会阻塞等待数据的读写完成。这种方式效率较低,因为程序在等待IO操作完成时无法做其他事情。 NIO,即New Input/Output,是Java在JDK1.4版本引入的一种新的IO模型。NIO可以实现非阻塞的IO操作,其核心是通过通道(Channel)和缓冲区(Buffer)来实现数据的读写。NIO通过选择器(Selector)实现了多路复用,一个线程可以同时处理多个通道的IO操作,实现了更高的效率。NIO适用于连接数较多且连接时间较短的场景,例如网络编程中的ServerSocketChannel和SocketChannel。 AIO,即Asynchronous Input/Output,是Java在JDK1.7版本引入的一种新的IO模型。AIO通过回调机制实现异步的IO操作,当IO操作完成后会触发回调函数的执行。AIO适用于连接数较少且连接时间较长的场景,例如网络编程中的AsynchronousServerSocketChannel和AsynchronousSocketChannel。相比于NIOAIO的优势是在IO操作完成前不需要阻塞线程,可以充分利用线程资源,并且编程模型更简单。 综上所述,IO是传统的同步IO模型,效率较低;NIO是一种非阻塞的IO模型,通过选择器实现了多路复用,效率较高;AIO是一种异步的IO模型,通过回调机制实现了非阻塞的IO操作,适用于连接时间较长的场景。在实际开发中,可以根据具体需求选择适合的IO模型来提高程序的性能和效率。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值