一、I/O 模型
为什么有IO模型:因为用户空间无法直接从内核空间拷贝数据,而是由内核空间来操作,而IO模型就是解决谁触发拷贝、
1、异步与同步
-
同步:如果有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行;
-
异步:如果有多个任务或者事件发生,这些事件可以并发地执行,一个事件或者任务的执行不会导致整个流程的暂时等待;
2、阻塞与非阻塞
-
阻塞:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;
-
非阻塞:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待;
阻塞和非阻塞的区别:关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息
3、阻塞IO与非阻塞IO
-
IO操作包括:对硬盘的读写、对socket的读写以及外设的读写;
-
完整的IO请求操作包括两个阶段:
- 查看数据是否就绪;
- 进行数据拷贝(内核将数据拷贝到用户线程);
-
阻塞(blocking IO)和非阻塞(non-blocking IO)的区别:其区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
4、异步IO与同步IO
4.1、概述
- 同步IO即如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞
- 异步IO为如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞
4.2、同步IO和异步IO模型是针对用户线程和内核的交互来说的:
- 同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;
- 异步IO:只有IO请求操作的发出是由用户线程来进行的,IO 操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞
4.3、同步IO和异步IO关键区别
同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成,所以说"异步IO"必须要有操作系统的底层支持;
4.4、“同步IO和异步IO”与“阻塞IO和非阻塞IO”是不同的两组概念
阻塞IO和非阻塞IO是反映在当用户请求IO操作时,如果数据没有就绪,是用户线程一直等待数据就绪,还是会收到一个标志信息这一点上面的;
也就是说:阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的;
同步、异步是描述被调用方的;
阻塞、非阻塞是描述调用方的;
同步不一定阻塞、异步也不一定非阻塞
5、五种IO模型
5.1、同步阻塞IO模型
最传统的一种 IO模型,即在读写数据过程中会发生阻塞现象
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态
典型的例子:data = socket.read();
5.2、同步非阻塞IO模型
用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果
- 一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回;
- 在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU;
5.3、多路复用IO模型
5.3.1、IO多路复用
多路复用IO模型是目前使用得比较多的模型;Java NIO 实际上就是多路复用IO
- Linux 提供 select、poll、epoll,进程通过讲一个或者多个 fd 传递给 select、poll、epoll 系统调用,阻塞在 select 操作(这个是内核级别的调用)上,这样的话,可以同时监听多个 fd 是否处于就绪状态
- 多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作
- 在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用
- 多路复用IO为何比非阻塞IO模型的效率高:是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多
- 注意点:多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询
IO多路复用技术
- IO多路复用技术通过把多个IO阻塞复用到同一个select的阻塞上,从而使得系统在单线程的清下可以同时处理多个客户端请求;
- 与传统的多线程/多进程模型比,IO多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统维护的工作量。
- IO多路复用技术应用场景:
- 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字;
- 服务器需要同时处理多种网络协议的套接字;
5.3.2、IO多路复用模型select、poll、epoll
I/O多路复用模型会用到select、poll、epoll
函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作;其中epoll
是Linux所特有,而select
则应该是POSIX所规定,一般操作系统均有实现;
(1)select:
-
基本原理:select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低;
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,这样所带来的缺点是:
- select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024;
- 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低;
- 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
(2)poll:
-
基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历
-
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义;
- poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd;
(3)epoll:
epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次;是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制
-
基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
-
优点:
- 没有最大并发连接的限制,能打开的FD的上限远大于1024;
- 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll;
- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销
-
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger),LT模式是默认模式,LT模式与ET模式的区别如下:
- LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件;
- ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件
5.4、信号驱动 IO 模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作
5.5、异步 IO 模型
- 最理想的IO模型
- 在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成,用户线程中不需要再次调用IO函数进行具体的读写
- 与信号驱动 IO 模型相比:
- 在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;
- 在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作
- 注意:异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO
5.6、总结:
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO;因为:前面四种模型中IO操作的第2个阶段都会引起用户线程阻塞,也就是说内核进行数据拷贝的过程都会让用户线程阻塞
6、高性能IO设计模式
6.1、Reactor
具体可以参考文档:《Scalable IO in Java》,作者是:Doug Lea
6.1.1、概念
Reactor 是一种和 IO 相关的设计模式
- 一种事件驱动模型
- 处理多个输入;
- 采用多路复用将事件分发给相应的Handler处理
Reactor实际上采用了分而治之和事件驱动的思想:
- 分而治之:一个连接里完整的网络处理过程一般分为 accept、read、decode、process、encode、send这几步。而Reactor模式将每个步骤映射为一个Task,服务端线程执行的最小逻辑单元不再是一个完整的网络请求,而是 Task,且采用非阻塞方式执行;
- 事件驱动:每个Task 对应特定的网络事件,当Task 准备就绪时,Reactor 收到对应的网络事件通知,并将Task 分发给绑定了对应网络事件的 Handler 执行
总结概念如下:
Reactor模式
就是指一个或多个事件输入同时传递给服务处理器(Reactor),服务处理器负责监听各事件的状态,当任意一个事件准备就绪时,服务处理器收到该事件通知,并将事件发送给绑定了该对应网络事件的事件处理器(Handler)执行
Reactor模式的核心组成部分包括Reactor和处理资源池,其中Reactor负责监听和分配事件,处理资源池负责处理事件
Reactor模式中角色:
- Reactor:负责响应事件,将事件分发绑定了该事件的Handler处理;
- Handler:事件处理器,绑定了某类事件,负责执行对应事件的任务对事件进行处理;
- Acceptor:Handler的一种,绑定了 connect 事件,当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理
6.1.2、Reactor模式的三种实现
-
Reactor单线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tdywXXB5-1580024645081)(image/Reactor单线程.png)]
以上的
select,accept,read,send
是标准I/O复用模型的网络编程API,dispatch和"业务处理"是需要完成的操作;方案具体操作步骤:
- Reactor对象通过select监控连接事件,收到事件后通过dispatch进行分发;
- 果是连接建立的事件,则交由 Acceptor 通过accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理;
- 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler来响应;
- Handler 会完成 read -> 业务处理 -> send 的完整业务流程
单线程方案的优点:模型简单,没有多线程,进程通信,竞争的问题,全部都在一个线程中完成;
单线程方案的缺点:
- 只有一个进程,无法发挥多核 CPU的性能,只能采取部署多个系统来利用多核CPU,但这样会带来运维复杂度;
- Handler 在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈
应用场景:其在实践中应用场景不多,只适用于业务处理非常快速的场景,比如Redis;
Reactor单线程在NIO中使用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-86UTRsGs-1580024645082)(image/Reactor单线程-NIO流程图.png)]
-
Reactor多线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-alOUbTgR-1580024645082)(image/Reactor多线程.png)]
方案步骤:
- 主线程中,Reactor对象通过select 监听连接事件,收到事件后通过 dispatch进行分发;
- 如果是连接建立的事件,则由Acceptor处理,Acceptor通过 accept接受连接,并创建一个 Handler 来处理连接后续的各种事件;
- 如果不是连接建立事件,则Reactor会调用连接对应的Handler来进行相应;
- Handler 只负责响应事件,不进行业务处理,Handler 通过 read 读取到数据后,会发给 processor 进行业务处理;
- Processor 会在独立的子线程中完成真正的 业务处理,然后将响应结果发给主进程的 Handler处理,Handler 收到响应后通过 send 将响应结果返回给 client;
多线程方案优点: 能够充分利用多核多 CPU的处理能力;
多线程方案缺点:多线程数据共享和访问比较复杂;Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈
-
多Reactor多线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6fsFLopT-1580024645082)(image/多Reactor多线程.png)]
方案步骤:
- 主进程中mainReactor对象通过 select监控连接建立事件,收到事件后通过 Acceptor接收,将新的连接分配给某个子进程;
- 子进程中的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个 Handler 用于处理连接的各种事件;
- 当有新的事件发生时,subReactor 会调用里连接对应的 Handler 来响应;
- Handler完成
read -> 业务处理 -> send
的完整业务流程;
特点:
- 主进程和子进程的职责非常明确,主进程只负责接收新连接,子进程负责完成后续的业务处理;
- 主进程和子进程的交互很简单,主进程只需要把新的连接传递给子进程,子进程无需返回数据;
- 子进程之间是相互独立的,无需同步共享之类的处理(这里仅限于网络模型相关的 select,read,send等无须同步共享,"业务处理"还是有可能需要同步共享的
6.2、Proactor
前摄器模式,其实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响)
Proactor主动器模式包含如下角色
- Handle 句柄;用来标识socket连接或是打开文件;
- Asynchronous Operation Processor:异步操作处理器;负责执行异步操作,一般由操作系统内核实现;
- Asynchronous Operation:异步操作
- Completion Event Queue:完成事件队列;异步操作完成的结果放到队列中等待后续使用
- Proactor:主动器;为应用程序进程提供事件循环;从完成事件队列中取出异步操作的结果,分发调用相应的后续处理逻辑;
- Completion Handler:完成事件接口;一般是由回调函数组成的接口;
- Concrete Completion Handler:完成事件处理逻辑;实现接口定义特定的应用处理逻辑;
业务流程:
- 应用程序启动,调用异步操作处理器提供的异步操作接口函数,调用之后应用程序和异步操作处理就独立运行;应用程序可以调用新的异步操作,而其它操作可以并发进行;
- 应用程序启动Proactor主动器,进行无限的事件循环,等待完成事件到来;
- 异步操作处理器执行异步操作,完成后将结果放入到完成事件队列;
- 主动器从完成事件队列中取出结果,分发到相应的完成事件回调函数处理逻辑中;
Proactor性能更高,能够处理耗时长的并发场景;适用于异步接收和同时处理多个服务请求的事件驱动程序的场景;
Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;
7、Java中IO实现
7.1、BIO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ekZgzpbU-1580024645084)(image/Java-BIO通信模型图.png)]
-
采用BIO通信模型的服务端,由一个独立的 Acceptor现场负责监听客户端连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回给应答给客户端,线程销毁.这是典型的一请求一应答通信模型。
-
该模型最大的问题是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈
1:1
的正比关系.由于线程是Java虚拟机的非常宝贵的系统资源,当线程数膨胀后,系统的性能急剧下降,系统可能会发生线程堆栈溢出,创建线程失败等。
7.2、伪异步IO
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMgqGLr3-1580024645084)(image/Java-伪异步IO通信模型图.png)]
-
采用线程池和任务队列可以实现伪异步IO
-
当有新的客户端接入时,将客户端的Socket封装成一个Task(其实现Runnable接口)传递给后端的线程池中处理,JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。
-
由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出;
-
但是由于其底层通信依然采用同步阻塞模型,无法从根本上解决问题
7.3、NIO-非阻塞IO
-
jdk1.4引入NIO,弥补了原来阻塞IO的不足,具体参考Java-NIO
-
NIO相对其他IO来说,优点:
- 客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞;
- SocketChannel 的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他链路;
- 线程模型的优化;
7.4、AIO-异步IO
异步的套接字通道时真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动I/O(AIO)
- JDK7中新增了一些与文件(网络)I/O相关的一些api。这些API被称为NIO.2,或称为AIO(Asynchronous I/O);
- 服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理;
- AIO最大的一个特性就是异步能力,这种能力对socket与文件I/O都起作用。AIO其实是一种在读写操作结束之前允许进行其他操作的I/O处理;
- jdk7主要增加了三个新的异步通道:
- AsynchronousFileChannel: 用于文件异步读写;
- AsynchronousSocketChannel: 客户端异步socket;
- AsynchronousServerSocketChannel: 服务器异步socket
- AIO的实施需充分调用OS参与,IO需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显
- 将来式读取:用现有的Java.util.concurrent技术声明一个Future,用来保存异步操作的处理结果。通常用Future get()方法(带或不带超时参数)在异步IO操作完成时获取其结果
- 回调式异步读取:回调式所采用的事件处理技术类似于Swing UI编程采用的机制
7.5、不同IO对比
对比参数 | 同步IO(BIO) | 伪异步IO | NIO | AIO |
---|---|---|---|---|
客户端个数 | 1:1 | M:N(其中M可以大于N) | M:1(1个IO线程处理多个客户端连接) | M:0(不需要启动额外的IO线程,被动回调) |
IO类型(阻塞) | 阻塞IO | 阻塞IO | 非阻塞IO | 非阻塞IO |
IO类型(同步) | 同步IO | 同步IO | 同步IO(IO多路复用) | 异步IO |
API难度 | 简单 | 简单 | 非常复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
二、Java-IO流
1、输入与输出-数据源和目标媒介
1.1、Java IO关注的是从原始数据源的读取以及输出原始数据到目标媒介: 数据源–> 程序 --> 目标媒介
1.2、一个程序需要InputStream或者Reader从数据源读取数据,需要 OutputStream或者Writer将数据写入到目标媒介中;
InputStream和Reader与数据源相关联,OutputStream和Writer与目标媒介相关联
1.3、Java IO操作类都在包 java.io 下,大概有将近 80 个类,但是这些类大概可以分成四组,分别如下:
- 基于字节操作的 I/O 接口:InputStream 和 OutputStream
- 基于字符操作的 I/O 接口:Writer 和 Reader
- 基于磁盘操作的 I/O 接口:File
- 基于网络操作的 I/O 接口:Socket
前两组主要是根据传输数据的数据格式,后两组主要是根据传输数据的方式I/O 的核心问题要么是数据格式影响 I/O 操作,要么是传输方式影响 I/O 操作
==> "数据格式"和"传输方式"是影响效率最关键的因素了
2、文件
一种常用的数据源或者存储数据的媒介
2.1、读文件:
- 如果你需要在不同端之间读取文件,你可以根据该文件是二进制文件还是文本文件来选择使用 FileInputStream 或者 FileReader;
- 如果你需要跳跃式地读取文件其中的某些部分,可以使用 RandomAccessFile
2.2、写文件:
如果你需要在不同端之间进行文件的写入,你可以根据你要写入的数据是二进制型数据还是字符型数据选用FileOutputStream 或者 FileWriter
2.3、随机存取文件:通过 RandomAccessFile 对文件进行随机存取
随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序
2.4、文件和目录信息的获取:
通过 File 类可以获取文件和目录的信息
3、管道
为运行在同一个JVM中的两个线程提供了通信的能力,所以管道也可以作为"数据源以及目标媒介"
3.1、不能利用管道与不同的JVM中的线程通信(不同的进程),其与 Unix/Linux 系统的管道不一致
3.2、创建管道:
- 通过Java IO 中的PipedOutputStream和PipedInputStream创建管道
- 一个PipedInputStream流应该和一个PipedOutputStream流相关联.
- 一个线程通过PipedOutputStream写入的数据可以被另一个线程通过相关联的PipedInputStream读取出来
3.3、例子:可以使用两个管道共有的connect()方法使之相关联
public class PipedExample {
public static void main(String[] args) throws IOException {
final PipedOutputStream out = new PipedOutputStream();
final PipedInputStream in = new PipedInputStream(out);
// in.connect(out); 可以使用两个管道共有的connect()方法使之相关联
Thread thread1 = new Thread(new Runnable() {
public void run() {
try {
out.write("Hello,piped output".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
try {
int data = in.read();
while(data != -1){
System.out.println((char)data);
data = in.read();
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
out.close();
in.close();
}
}
3.4、管道与线程:
当使用两个相关联的管道流时,务必将它们分配给不同的线程。read()方法和write()方法调用时会导致流阻塞,这意味着如果你尝试在一个线程中同时进行读和写,可能会导致线程死锁;
4、网络
4.1、当两个进程之间建立了网络连接之后,他们通信的方式如同操作文件一样:
利用 InputStream 读取数据,利用 OutputStream 写入数据,Java 网络 API 用来在不同进程之间建立网络连接,而 Java IO 则用来在建立了连接之后的进程之间交换数据
5、字节和字符数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TppHKyqe-1580024645084)(image/Java-IO层次结构.png)]
5.1、从InputStream或者Reader中读入数组
用 ByteArrayInputStream或者CharArrayReader封装字节或者字符数组从数组中读取数据:
5.2、从OutputStream或者Writer中写数组:
把数据写到ByteArrayOutputStream或者CharArrayWriter 中
5.3、基于字节流的操作层次结构
InputStream:
ByteArrayInputStream
FileInputStream
SocketInputStream
FilterInputStream
InflaterInputStream <- ZipInputStream
BufferedInputStream
DataInputStream
ObjectInputStream
PipedInputStream
OutputStream:与上述 InputStream 类似
5.4、Writer/Reader相关类层次结构
Writer:
OutputStreamWriter
FileWriter
BufferedWriter
StringWriter
PipedWriter
PrintWriter
CharArrayWriter
Reader:类似 Writer
5.5、InputStreamReader-字节流与字符流转换接口
是字节到字符的转化桥梁,InputStream到Reader的过程要指定编码字符集,否则将采用操作系统默认字符集,很可能会出现乱码问题.StreamDecoder正是完成字节到字符的解码的实现类
6、标准输出
System.in
、System.out
、System.err
6.1、JVM 启动的时候通过Java运行时初始化这3个流,所以你不需要初始化它们
6.2、System.in:
一个典型的连接控制台程序和键盘输入的 InputStream 流
6.3、System.out:
System.out 是一个 PrintStream 流.System.out一般会把你写到其中的数据输出到控制台上
6.4、System.err:
System.err 是一个 PrintStream 流.System.err 与 System.out 的运行方式类似,但它更多的是用于打印错误文本
6.5、替换系统流:
System.in、System.out、System.err这3个流是java.lang.System类中的静态成员,并且已经预先在JVM启动的时候初始化完成,你依然可以更改它们;要把一个新的InputStream设置给System.in或者一个新的OutputStream设置给System.out或者System.err;可以使用 System.setIn(), System.setOut(), System.setErr()方法设置新的系统流
注意:请记住,务必在JVM关闭之前冲刷System.out(译者注:调用flush()),确保System.out把数据输出到了文件中
7、流-仅仅只是一个连续的数据流
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BgWNJ1U0-1580024645084)(image/IO流链接.png)]
7.1、流和数组不一样,不能通过索引读写数据.在流中,你也不能像数组那样前后移动读取数据,除非使用 RandomAccessFile 处理文件.
7.2、InputStream:java.io.InputStream 类是所有 Java IO 输入流的基类
- 如果你需要将读过的数据推回到流中,你必须使用 PushbackInputStream,这意味着你的流变量只能是这个类型,否则在代码中就不能调用 PushbackInputStream的unread()方法;
- 通常使用输入流中的read()方法读取数据.read()方法返回一个整数,代表了读取到的字节的内容,当达到流末尾没有更多数据可以读取的时候,read()方法返回-1
注意:InputStream的read()方法返回一个字节,意味着这个返回值的范围在0到255之间
7.3、OutputStream:java.io.OutputStream 是 Java IO 中所有输出流的基类
7.4、组合流:将流整合起来以便实现更高级的输入和输出操作
InputStream input = new BufferedInputStream(new FileInputStream("c:\data\input-file.txt"));
一个流要么是输入流,要么是输出流,不可能同时是输入流、输出流。因为InputStream
和OutputStream
是抽象类,而Java中对于类只能单继承,不能继承多个类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pSPP6tJI-1580024645085)(image/Java-IO流.png)]
8、Reader、Writer
8.1、Reader类:是Java IO 中所有Reader的基类
- 子类包括BufferedReader,PushbackReader,InputStreamReader,StringReader和其他Reader
- Reader的read()方法返回一个字符,意味着这个返回值的范围在0到65535之间
- 整合 Reader 与 InputStream:
如果你有一个InputStream输入流,并且想从其中读取字符,可以把这个InputStream包装到InputStreamReader中Reader reader = new InputStreamReader(inputStream);在构造函数中可以指定解码方式
8.2、Writer类:是Java IO中所有Writer的基类,子类包括BufferedWriter和PrintWriter等等
- 整合 Writer 和 OutputStream:
一个Writer可以和一个 OutputStream 相结合。把OutputStream包装到OutputStreamWriter中,所有写入到OutputStreamWriter的字符都将会传递给OutputStream;
Writer writer = new OutputStreamWriter(outputStream);
8.3、整合Reader和Writer
以通过将 Reader 包装到 BufferedReader、Writer 包装到 BufferedWriter 中实现缓冲
9、流的使用
-
将字节流转换成字符缓冲流
// 读取文件,读取文件注意编码问题 BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("D:/1.txt"))); // 获取控制台输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
-
获取控制台输入
- 用Scanner包装System.in,Scanner取得的输入以space, tab, enter 键为结束符
Scanner scan = new Scanner(System.in); String read = scan.nextLine();
- 使用BufferedReader取得含空格的输入:取得包含space在内的输入
BufferedReader br = new BufferedReader(new InputStreamReader(System.in )); br.readLine();
- 用Scanner包装System.in,Scanner取得的输入以space, tab, enter 键为结束符
三、Java NIO
1、Java NIO 概述
1.1、核心概念:
NIO:非阻塞IO
- Channels
- Buffers
- Selectors
1.2、Channel 和 Buffer:
- 基本上,所有的IO在NIO中都从一个Channel开始,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中
- Channel的几种实现:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel - Buffer的实现:这些Buffer覆盖了你能通过IO发送的基本数据类型:byte、short、int、long、float、double和char
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer
1.3、Selector:Selector 允许单线程处理多个Channel
要使用Selector得向Selector注册Channel,然后调用它的select()方法个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收;
2、Channel
用于源节点与目标节点的连接。在 Java NIO 中负责缓冲区中数据的传输,本身不存储数据
2.1、与流类似,但有所不同:不能直接访问数据,可以与 Buffer 进行交互.
- 既可以从通道中读取数据,又可以写数据到通道.但流的读写通常是单向的
- 通道可以异步地读写;
- 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入;
2.2、Java NIO 中最重要的通道的实现
- FileChannel:从文件中读写数据,一般从流中获取 Channel,不能切换成非阻塞模式,不能用于Selector
- DatagramChannel:能通过UDP读写网络中的数据.
- SocketChannel:能通过TCP读写网络中的数据.
- ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样.对每一个新进来的连接都会创建一个 SocketChannel
2.3、通道的获取:
- Java 针对支持通道的类都提供了 getChannel() 的方法;
输入输出流
网络IO - 在JDK7中NIO2.0 针对各个通道提供了静态的方法 open()
- 在JDK7中NIO2.0 的 Files 工具类的 newByteChannel()
2.4、通道之间的数据传输:
transferFrom()
transferTo()
2.5、字符集:
(1).编码:字符串 --> 字节数组
(2).解码:字节数组 --> 字符串
查看支持的字符集:
Charset.availableCharsets();
A.定义字符集:Charset cs = Charset.forName("UTF-8");
B.获取编码器:
CharsetEncoder ce = cs.newEncoder();
ByteBuffer bf = ce.encode(CharBuffer)
C.获取解码器:
CharsetDecoder cd = cs.newDecoder();
CharBuffer cf = cd.decode(ByteBuffer);
3、Buffer
用于和NIO通道进行交互,缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存,底层实现是数组;一般读写都要通过channel和buffer,需要从channel中读取数据到buffer中,或者将数据从buffer中写到channel中
方法名称 | 方法描述 |
---|---|
Buffer clear() | 清空缓冲区并返回对缓冲区的引用,但是数据还存在 |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置充值为0 |
int capacity() | 返回Buffer 的capacity大小 |
boolean hasRemaining() | 判断缓冲区中是否还有元素 |
int limit() | 返回Buffer 的界限(limit) 的位置 |
Buffer.limit(int n) | 将设置缓冲区界限为n, 并返回一个具有新limit的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置position |
Buffer position(int n) | 将设置缓冲区的当前位置为n,并返回修改后的Buffer对象 |
int remaining() | 返回position 和limit 之间的元素个数 |
Buffer reset() | 将位置position 转到以前设置的mark 所在的位置 |
Buffer rewind() | 将位置设为为0,取消设置的mark |
3.1、Buffer 的基本用法
获取缓冲区的方法:(例子)ByteBuffer.allocate()
-
使用Buffer读写数据一般遵循以下四个步骤
- ①、写入数据到 Buffer
- ②、调用flip()方法
- ③、从 Buffer 中读取数据
- ④、调用clear()方法或者compact()方法
-
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式;在读模式下,可以读取之前写入到buffer的所有数据;
-
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面;
-
Buffer 的capacity,position和limit: 关键属性
- (1)缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存
- (2)Buffer 对象的四个属性:
capacity、position、limit、mark;其中position和limit的含义取决于 Buffer 处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的 - (3)capacity:最大存储数据的容量,一旦声明不能改变,且不会为负数;作为一个内存块,Buffer 有一个固定的大小值,也叫"capacity".你只能往里写capacity个 byte、long,char 等类型;一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据
- (4)position:
- ①、当你写数据到 Buffer 中时,position表示当前的位置,初始的position值为0,当一个 byte、long 等数据写到 Buffer 后,position会向前移动到下一个可插入数据的 Buffer 单元。position最大可为capacity – 1。永远不会大于limit
- ②、当读取数据时,也是从某个特定位置读.当将Buffer从写模式切换到读模式,position会被重置为0. 当从 Buffer的position处读取数据时,position向前移动到下一个可读的位置
- (5)limit:
- ①、在写模式下,Buffer 的limit表示你最多能往Buffer里写多少数据,永远不会超过capacity。写模式下,limit等于Buffer的capacity
- ②、当切换Buffer到读模式时,limit表示你最多能读到多少数据,当切换Buffer到读模式时,limit会被设置成写模式下的position值
- (6)mark:标记,表示记录当前position的位置,可以通过reset() 恢复到 mark 的位置;
它们之间的关系:
0 <= mark <= position <= limit <= capacity
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XUREcf3X-1580024645086)(image/NIO-Buffer属性变化.png)]
3.3、Buffer 的分配
因为JDK中所有跟基本数据类型相关的Buffer都是抽象类,不能直接通过new来创建,每一个 Buffer 类都有一个allocate方法。下面是一个分配48字节capacity的 ByteBuffer 的例子:
ByteBuffer buf = ByteBuffer.allocate(48);
3.4、向 Buffer 中写数据
- 从 Channel 写到 Buffer:
int bytesRead = inChannel.read(buf); //read into buffer
- 通过 Buffer 的put()方法写到 Buffer 里:buf.put(127);
3.5、flip()方法
flip方法将 Buffer 从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值,即position现在用于标记读的位置,limit表示之前写进了多少个 byte、char,现在能读取多少个 byte、char 等
3.6、从 Buffer 中读取数据
- 从 Buffer 读取数据到 Channel:int bytesWritten = inChannel.write(buf);
- 使用get()方法从 Buffer 中读取数据 byte aByte = buf.get();
3.7、rewind()方法
Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)
3.8、clear()与compact()方法
- 一旦读完Buffer中的数据,需要让Buffer准备好再次被写入.可以通过clear()或compact()方法来完成
- 如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值.换句话说,Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据
- 如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有;
- 如果 Buffer 中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法
- compact()方法将所有未读的数据拷贝到Buffer起始处.然后将position设到最后一个未读元素正后面.limit属性依然像clear()方法一样,设置成capacity.现在Buffer准备好写数据了,但是不会覆盖未读的数据
3.9、更多方法
-
Buffer.mark():通过调用 Buffer.mark()方法,可以标记 Buffer 中的一个特定position。之后可以通过调用 Buffer.reset()方法恢复到这个position
-
Buffer.slice():与原有Buffer共享底层数组
3.10、equals()与compareTo()方法
- equals(),当满足下列条件时,表示两个Buffer相等:
有相同的类型(byte、char、int等)。
Buffer 中剩余的 byte、char 等的个数相等。
Buffer 中所有剩余的 byte、char 等都相同。
==> 实际上,它只比较Buffer中的剩余元素(剩余元素是从 position到limit之间的元素) - compareTo()方法:
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer"小于"另一个Buffer:第一个不相等的元素小于另一个Buffer中对应的元素;所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)
3.11、只读Buffer
一般是具体的实现buffer后面带上一个R。
可以随时将一个普通Buffer调用asReadOnlyBuffer返回一个只读Buffer,但不能将一个只读Buffer转换为读写Buffer
3.11、直接缓冲区(DirectBuffer)
- 直接缓冲区是通过调用此类的
allocateDirect()
工厂方法来创建的,此方法返回的缓冲区进行分配取消分配所需成本通常要高于非直接缓冲区。 - 直接缓冲对象是在java中的,但是其并不是在堆中分配的,主要是通过Buffer类中的成员变量:address,指向的是堆外内存的地址;
- 建议将直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。一般情况下仅在直接缓冲区能带来明显好处时分配它们;
- 直接缓冲区还可以通过 FileChannel 的map() 方法将文件区域直接映射到内存中来创建。该方法返回 MappedByteBuffer
- 判断是直接缓冲区还是间接缓冲区,可以通过调用 isDirect() 方法来确定;
4、Scatter/Gather
4.1、概述
Scatter/Gather 用于描述从 Channel 中读取或者写入到 Channel 的操作
- Scatter(分散)从 Channel 中读取是指在读操作时将读取的数据写入多个 Buffer 中,Channel 将从 Channel 中读取的数据"分散(scatter)"到多个 Buffer 中
- Gather(聚集)写入 Channel 是指在写操作时将多个 Buffer 的数据写入同一个 Channel,Channel 将多个 Buffer 中的数据"聚集(gather)"后发送到 Channel
- Scatter/Gather 经常用于需要将传输的数据分开处理的场合
4.2、Scattering Reads
Scattering Reads:是指数据从一个channel读取到多个buffer中
(1).代码示例:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
// 注意buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
//read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,
//当一个buffer被写满后,channel紧接着向另一个buffer中写
注意:Scattering Reads 在移动下一个 Buffer 前,必须填满当前的buffer,这也意味着它不适用于动态消息
4.3、Gathering Writes
是指数据从多个buffer写入到同一个channel
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
// buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,
// 注意只有position和limit之间的数据才会被写入
5、通道之间的数据传输
在 Java NIO 中,如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另外一个channel
- transferFrom():数据从源通道传输到 FileChannel 中
注意:在SoketChannel的实现中,SocketChannel 只会传输此刻准备好的数据
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
/**
* 从position处开始向目标文件写入数据
* count表示最多传输的字节数
* 如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数
*/
toChannel.transferFrom(position, count, fromChannel);
- transferTo():将数据从FileChannel传输到其他的channel中
6、Selector
是 Java NIO 中能够检测一到多个 NIO 通道,并能够知晓通道是否为诸如读写事件做好准备的组件,这样一个单独的线程可以管理多个channel,从而管理多个网络连接
6.1、为什么使用 Selector
可以只用一个线程处理所有的通道,使用Selector能够处理多个通道;
6.2、Selector 的创建
Selector selector = Selector.open();
上面返回的Selector真正的实现类是:sun.nio.ch.KQueueSelectorImpl
6.3、向 Selector 注册通道
为了将Channel和Selector配合使用,必须将channel注册到selector上;通过 SelectableChannel.register()方法来实现
/**
* 与Selector一起使用时,Channel必须处于非阻塞模式下
* 这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式.而套接字通道都可以
*/
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
-
register()方法的第二个参数:是在通过Selector监听Channel时对什么事件感兴趣,可以监听四种不同类型的事件:
- Connect == SelectionKey.OP_CONNECT:连接就绪
- Accept == SelectionKey.OP_ACCEPT:接受就绪
- Read == SelectionKey.OP_READ:读就绪
- Writer == SelectionKey.OP_WRITE:写就绪
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
6.4、SelectionKey
- interest集合:你所选择的感兴趣的事件集合
int interestSet = selectionKey.interestOps();
//用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
- ready集合是通道已经准备就绪的操作的集合
在一次选择(Selection)之后,你会首先访问这个ready set
int readySet = selectionKey.readyOps();
检测channel中什么事件或操作已经就绪
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable(); - Channel + Selector:从SelectionKey访问Channel和Selector很简单
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector(); - 附加的对象:可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
6.5、基本使用
服务端:NioServer
public class NioServer {
private static Map<String, SocketChannel> clientMap = new ConcurrentHashMap<>();
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(8899);
serverSocket.bind(address);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
selector.select();
Set<SelectionKey> keySet = selector.selectedKeys();
keySet.forEach(selectionKey -> {
final SocketChannel client;
try {
if (selectionKey.isAcceptable()) {
ServerSocketChannel server =
(ServerSocketChannel) selectionKey.channel();
client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
String key = "【" + UUID.randomUUID().toString() + "】";
clientMap.put(key, client);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);
if (count > 0) {
readBuffer.flip();
Charset charset = Charset.forName("UTF-8");
String receiveMessage =
String.valueOf(charset.decode(readBuffer).array());
System.out.println(client + ": " + receiveMessage);
String senderKey = null;
for (Map.Entry<String, SocketChannel> entry
: clientMap.entrySet()) {
if (client == entry.getValue()) {
senderKey = entry.getKey();
break;
}
}
for (Map.Entry<String, SocketChannel> entry
: clientMap.entrySet()) {
SocketChannel value = entry.getValue();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((senderKey + ": " + receiveMessage).getBytes());
writeBuffer.flip();
value.write(writeBuffer);
}
}
}
} catch (IOException e) {
}
});
keySet.clear();
} catch (Exception e) {
}
}
}
}
客户端:NioClient
public class NioClient {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8899));
while (true) {
selector.select();
Set<SelectionKey> keySet = selector.selectedKeys();
for (SelectionKey selectionKey : keySet) {
if (selectionKey.isConnectable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
if (client.isConnectionPending()) {
client.finishConnect();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((LocalDateTime.now() + " 连接成功").getBytes());
writeBuffer.flip();
client.write(writeBuffer);
ExecutorService executorService =
Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
executorService.submit(() -> {
while (true) {
writeBuffer.clear();
InputStreamReader input = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(input);
String message = br.readLine();
writeBuffer.put(message.getBytes());
writeBuffer.flip();
client.write(writeBuffer);
}
});
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);
if (count > 0) {
String receivedMessage = new String(readBuffer.array(), 0, count);
System.out.println(receivedMessage);
}
}
}
keySet.clear();
}
} catch (Exception e) {
}
}
}
7、NIO序列图
7.1、服务端通信序列图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7L2bhqW-1580024645087)(image/NIO服务端通信序列图.jpg)]
- 1、打开ServerSocketChannel,用于监听客户端的连接,是所有客户端连接的父管道;
- 2、绑定监听端口,设置连接为非阻塞模式;
- 3、创建Reactor线程,创建多路复用器并启动线程;
- 4、将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件;
- 5、多路复用器在线程run方法的无限循环体内轮询准备就绪的Key;
- 6、多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP的三次握手,建立物理链路;
- 7、设置客户端链路为非阻塞模式;
- 8、将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,读取客户端发送的网络消息;
- 9、异步读取客户端请求消息到缓冲区;
- 10、对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排;
- 11、将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端;
7.2、客户端通信序列图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pMOTPOuP-1580024645088)(image/NIO客户端通信序列图.jpg)]
四、IO与NIO面试相关
1、文件拷贝实现方式
主要关注以下几点:
- 不同的copy方式,底层机制有什么区别?
- 为什么零拷贝可能有性能优势?
- Buffer的分类与使用;
- Direct Buffer对垃圾收集有哪些方面的影响
1.1、不同的拷贝方式底层机制的实现
-
关于两个概念:用户态空间(User Space)和内核态空间(Kernel Space)这是操作系统层面的概念,操作系统内核、硬件驱动等运行在内核状态空间,具有相对高的特权;而用户态空间,则给普通应用和服务使用
-
基于流读写:当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时先将内核态数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存中读取到用户缓存,这种方式会带来一定的额外开销,可能会降低IO效率
-
基于NIO:基于NIO的transfer的实现方式,在Linux和Unix上,则会使用零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的拷贝,进而可能提高应用拷贝性能
1.2、Files.copy 方法
最终实现是本地方法实现的UnixCopyFile.c,其内部明确说明了只是简单的用户态空间拷贝,所以该方法不是利用transfer来实现的,而是本地技术实现的用户态拷贝
1.3、基于流的读写
public static void copyFileByStream(File source, File dest) throws Exception {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
}
}
1.4、基于NIO实现
- 基于基本NIO操作实现
public static void main(String[] args) throws Exception {
FileInputStream in = new FileInputStream("temp/test1.txt");
FileOutputStream out = new FileOutputStream("temp/test1-copy.txt");
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = inChannel.read(buffer);
if (read == -1) {
return;
}
buffer.flip();
outChannel.write(buffer);
}
- 基于Nio的transferTo或者transferFrom
// 使用java nio 的transferTo 或 transferFrom来实现,其copy速度相对来说更快点,因为其更能利用现代操作系统底层机制,避免不必要的拷贝和上下文切换
public static void copyFileByChannel(File source, File dest) throws Exception {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel()) {
for (long count = sourceChannel.size(); count
> 0; ) {
long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
- 基于MappdByteBuffer实现,中间使用了编码
public static void copyFileByMappedByteBuffer(String source, String dest) throws Exception {
RandomAccessFile input = new RandomAccessFile(source, "r");
RandomAccessFile output = new RandomAccessFile(dest, "rw");
long length = new File(source).length();
FileChannel inputChannel = input.getChannel();
FileChannel outputChannel = output.getChannel();
MappedByteBuffer inputData = inputChannel.map(FileChannel.MapMode.READ_ONLY, 0, length);
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharsetEncoder encoder = charset.newEncoder();
CharBuffer charBuffer = decoder.decode(inputData);
ByteBuffer outputData = encoder.encode(charBuffer);
outputChannel.write(outputData);
}
1.4、如何提高拷贝效率
- 在程序中,使用缓存机制,合理减少IO次数;
- 使用transfer等机制,减少上下文切换和额外IO操作;
- 尽量减少不必要的转换过程,比如编解码;对象序列化与反序列化;
2、DirectBuffer 与 MappedByteBuffer
2.1、概述
- DirectBuffer:其定义了isDirect方法,返回当前buffer是不是Direct类型。因为Java提供了堆内和堆外(Direct)Buffer,我们可以以他的allocat 或者 allocatDirect方法直接创建;
- MappedByteBuffer:将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时直接将操作这块文件数据,省去了将数据从内核空间向用户空间传输的损耗;可以使用FileChannel.map创建,本质上也是DirectBuffer;
在实际使用时,Java会对DirectBuffer仅做本地iO操作,对于很多大数据量的IO密集操作,可能会带来非常大的优势:
- DirectBuffer生命周期内内存地址都不会再发生改变,进而内核可以安全的对其进行访问,很对IO操作很搞笑;
- 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高;
但是值得注意的是,DirectBuffer创建和销毁过程中,都会比一般的堆内存Buffer增加部分开销,通常建议用于长期使用、数据较大的场景
因为DirectBuffer不在堆上,所以其参数设置大小可以用如下参数:-XX:MaxDirectMemorySize=512M
;意味着在计算Java可以使用的内存大小的时候,不能只考虑堆的需要,还有DirectBuffer等一系列堆外因素,
如果出现内存不足,堆外内存占用也是一种可能性;
另外,大多数垃圾收集过程中,都不会主动收集DirectBuffer,它的垃圾收集过程,是基于Cleaner和幻象引用机制,其本身不是public类型,内部实现了一个Deallocator负责销毁的逻辑,对它的销毁往往需要到FullGC的时候,使用不当的话很容易引起OOM
关于DirectBuffer的回收,注意以下几点:
- 在应用程序中,显示调用System.gc()来强制触发;
- 在大量使用DirectBuffer的部分框架中,框架自己在程序中调用释放方法,Netty的实现即如此;
- 重复使用DirectBuffer
2.2、跟踪与诊断DirectBuffer内存占用
通常的垃圾收集日志等记录,并不包含Directbuffer等信息,在JDK8之后的版本,可以使用native memory tracking特性进行诊断:-XX:NativeMemoryTracking={summary|detail}
注意激活NMT通常都会导致JVM出现5%~10%性能下降
// 打印 NMT信息
jcmd <pid> VM.native_memory detail
// 进行baseline,以对比分配内存变化
jcmd <pid> VM.native_memory baseline
// 进行baseline,以对比分配内存变化
jcmd <pid> VM.native_memory detail.diff
3、使用Java读取大文件
-
(1)文件流边读边用,使用文件流的read()方法每次读取指定长度的数据到内存中,具体代码如下
public static void readMethod1(String filePath) throws Exception{ BufferedInputStream reader = new BufferedInputStream(new FileInputStream(filePath)); int bytes = -1; do { byte[] byteArray = new byte[8192]; bytes = reader.read(byteArray); if (bytes != -1) { String s = new String(byteArray); System.out.println(s); } } while (bytes > 0); reader.close(); }
-
(2)对大文件建立NIO的FileChannel,每次调用read()方法时会先将文件数据读取到已分配的固定长度的java.nio.ByteBuffer,接着从中获取读取的数据。这种方式比传统的流方式要快点
public static void fileChannelMethod(String filePath) throws Exception { FileInputStream in = new FileInputStream(filePath); ByteBuffer byteBuffer = ByteBuffer.allocate(65535); FileChannel fileChannel = in.getChannel(); int b = -1; do { b = fileChannel.read(byteBuffer); if (b != -1) { byte[] array = new byte[b]; byteBuffer.flip(); byteBuffer.get(array); byteBuffer.clear(); System.out.println(new String(array)); } } while (b > 0); in.close(); fileChannel.close(); }
-
(3)内存文件映射,就是把文件内容映射到虚拟内存的一块区域中,从而可以直接操作内存当中的数据而无需每次都通过IO去物理硬盘读取文件,
public static void memoryMappingMethod(String filePath) throws Exception { FileInputStream in = new FileInputStream(filePath); FileChannel fileChannel = in.getChannel(); MappedByteBuffer mapperBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); boolean end = false; do { int limit = mapperBuffer.limit(); int position = mapperBuffer.position(); if (position >= limit) { end = true; } int maxSize = 2048; if (limit - position < maxSize) { maxSize = limit - position; } byte[] array = new byte[maxSize]; mapperBuffer.get(array); System.out.println(new String(array)); } while (!end); in.close(); fileChannel.close(); }
这种方式存在致命问题,就是无法读取超大文件(大于Integer.Max_value),因为 FileChannel的map方法中 size 参数会有大小限制,源码中发现该参数值大于 Integer.MAX_VALUE 时会直接抛出 IllegalArgumentException(“Size exceeds Integer.MAX_VALUE”) 异常,所以对于特别大的文件其依然不适合。
// sun.nio.ch.FileChannelImpl#map public MappedByteBuffer map(MapMode var1, long var2, long var4) throws IOException { this.ensureOpen(); if (var1 == null) { throw new NullPointerException("Mode is null"); } else if (var2 < 0L) { throw new IllegalArgumentException("Negative position"); } else if (var4 < 0L) { throw new IllegalArgumentException("Negative size"); } else if (var2 + var4 < 0L) { throw new IllegalArgumentException("Position + size overflow"); } else if (var4 > 2147483647L) { throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE"); } else { .... } }
本质上是由于 java.nio.MappedByteBuffer 直接继承自 java.nio.ByteBuffer ,而 ByteBuffer 的索引是 int 类型的,所以 MappedByteBuffer 也只能最大索引到 Integer.MAX_VALUE 的位置,所以 FileChannel 的 map 方法会做参数合法性检查。
4、NIO消息传输错误
4.1、存在问题的情况
- 多消息粘包:
- 单消息不完整:接收端buffer容量不够
- 消息到达提醒重复触发(读消息时未设置取消监听)
4.2、如何解决
- 数据传输加上开始结束标记
- 数据传输使用固定头部的方案;
- 混合方案:固定头、数据加密、数据描述
5、关于BIO、NIO等现场问题
基于BIO实现的Server端,当建立了100个连接时,会有多少个线程?如果基于NIO,又会是多少个线程? 为什么?
BIO由于不是NIO那样的事件机制,在连接的IO读取上,无论是否真的有读/写发生,都需要阻塞住当前的线程,对于基于BIO实现的Server端,通常的实现方法都是用一个线程去accept连接,当连接建立后,将这个连接的IO读写放到一个专门的处理线程,所以当建立100个连接时,通常会产生1个Accept线程 + 100个处理线程。
NIO通过事件来触发,这样就可以实现在有需要读/写的时候才处理,不用阻塞当前线程,NIO在处理IO的读写时,当从网卡缓冲区读或写入缓冲区时,这个过程是串行的,所以用太多线程处理IO事件其实也没什么意义,连接事件由于通常处理比较快,用1个线程去处理就可以,IO事件呢,通常会采用cpu core数+1或cpu core数 * 2,这个的原因是IO线程通常除了从缓冲区读写外,还会做些比较轻量的例如解析协议头等,这些是可以并发的,为什么不只用1个线程处理,是因为当并发的IO事件非常多时,1个线程的效率不足以发挥出多core的CPU的能力,从而导致这个地方成为瓶颈,这种在分布式cache类型的场景里会比较明显,按照这个,也就更容易理解为什么在基于Netty等写程序时,不要在IO线程里直接做过多动作,而应该把这些动作转移到另外的线程池里去处理,就是为了能保持好IO事件能被高效处理
6、Channel和Scoket区别
Socket、SocketChannel二者的实质都是一样的,都是为了实现客户端与服务器端的连接而存在的。
- 所属包不同:Socket在java.net包中,而SocketChannel在java.nio包中;
- 异步方式不同:Socket是阻塞连接(当然我们可以自己实现非阻塞),SocketChannel可以设置非阻塞连接;
- 性能不同:一般来说使用SocketChannel会有更好的性能。其实,Socket实际应该比SocketChannel更高效,不过由于使用者设计等原因,效率反而比直接使用SocketChannel低;
- 使用方式不同:
- Socket、ServerSocket类可以传入不同参数直接实例化对象并绑定ip和端口:
Socket socket = new Socket("127.0.0.1", "8000"); ServerSocket serverSocket = new ServerSocket("8000")
- 而SocketChannel、ServerSocketChannel类需要借助Selector类控制
- Socket、ServerSocket类可以传入不同参数直接实例化对象并绑定ip和端口:
7、Java中怎么快速把InputStream转化为String
7.1、使用 Apachecommons包的工具类 IOUtils
StringWriter writer = new StringWriter();
IOUtils.copy(in, writer, encoding);
String str = writer.toString();
// 或者
String str = IOUtils.toString(in, encoding);
7.2、使用guava
CharStreams.toString(new InputStreamReader(in, encoding));
7.3、使用Scanner
Scanner scanner = new Scanner(in).useDelimiter("\A");
String str = scanner.hasNext() ? scanner.next() : "";
7.4、使用Stream API
String str = new BufferedReader(new InputStreamReader(in)).lines().collect(Collectors.joining("
"));
String str = new BufferedReader(new InputStreamReader(in)).lines().parallel().collect(Collectors.joining("
"));
7.5、使用InputStreamReader and StringBuilder
final int bufferSize = 1024;
final char[] buffer = new char[bufferSize];
final StringBuilder sb = new StringBuilder();
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
int charRead;
while ( (charRead = reader.read(buffer, 0, buffer.length)) > 0) {
sb.append(buffer, 0 , charRead);
}
return sb.toString();