一、IO模型
IO在计算机中指Input/Output,也就是输⼊和输出。
(一)内核空间与用户空间
在计算机中,将空间分为内核空间(Kernel-space)和⽤户空间(User-space)。 在 Linux 系统中,内核模块运⾏在内核空间,对应的进程处于内核态;⽽⽤户程序运⾏在⽤户空间,对应的进程处于⽤户态。
内核空间:内核空间总是驻留在内存中,它是为操作系统的内核保留的。应⽤程序是不允许直接在该区域进⾏读写或直接调⽤内核代码定义的函数的。
⽤户空间:每个普通的⽤户进程都有⼀个单独的⽤户空间,处于⽤户态的进程不能访问内核空间中的数据,也不能直接调⽤内核函数的 ,因此要进⾏系统调⽤的时候,就要将进程切换到内核态才⾏。
在系统调用进行读写时,系统(应用程序)不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统kernel内核完成的。
(二)IO模型类型
I/O 模型简单的理解:就是⽤什么样的通道进⾏数据的发送和接收,很⼤程度上决定了程序通信的性能。
IO模型⼤致有如下⼏种:
同步IO(synchronous IO)
异步IO(asynchronous IO)
阻塞IO(bloking IO)
⾮阻塞IO(non-blocking IO)
多路复⽤IO(multiplexing IO)
信号驱动式IO(signal-driven IO) //在实际中并不常⽤
1、同步与异步IO
⾸先来解释同步和异步的概念,这两个概念与消息的通知机制有关。也就是同步与异步主要是从消息通知机制⻆度来说的。
同步(synchronous IO): 同步就是发起⼀个调⽤后,被调⽤者未处理完请求之前,调⽤不返回。
异步(asynchronous IO): 异步就是发起⼀个调⽤后,⽴刻得到被调⽤者的回应表示已接收到请求,但是被调⽤者并没有返回结果,此时我们可以处理其他的请求,被调⽤者通常依靠事件,回调等机制来通知调⽤者其返回结果。
所谓同步就是⼀个任务的完成需要依赖另外⼀个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是⼀种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持⼀致。
所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么⼯作,依 赖的任务也⽴即执⾏,只要⾃⼰完成了整个任务就算完成了。⾄于被依赖的任务最终是否真正完成,依赖它的任务⽆法确定,所以它是不可靠的任务序列。
同步和异步的区别最⼤在于异步调⽤者不需要等待处理结果,被调⽤者会通过回调等机制来通知调⽤者其返回结果。
2、阻塞与非阻塞IO
阻塞(bloking IO): 阻塞就是发起⼀个请求,调⽤者⼀直等待请求结果返回,也就是当前线程会被挂起,⽆法从事其他任务,只有当条件就绪才能继续。
⾮阻塞(non-blocking IO): ⾮阻塞就是发起⼀个请求,调⽤者不⽤⼀直等着结果返回,可以先去⼲其他事情。
⾮阻塞和阻塞的概念相对应,指在不能⽴刻得到结果之前,该函数不会阻塞当前线程,⽽会⽴刻返回。虽然表⾯上看⾮阻塞的⽅式可以明显的提⾼CPU的利⽤率,但是也带了另外⼀种后果就是系统的线程切换增加。增加的CPU执⾏时间能不能补偿系统的切换成本需要好好评估。
这里需要特殊注意一下,有⼈也许会把阻塞调⽤和同步调⽤等同起来,实际上它们是不同的:
(1)如果这个线程在等待当前函数返回时,仍在执⾏其他消息处理,那这种情况就叫做同步⾮阻塞;
(2)如果这个线程在等待当前函数返回时,没有执⾏其他消息处理,⽽是处于挂起等待状态,那这种情况就叫做同步阻塞;
所以同步的实现⽅式会有两种:同步阻塞、同步⾮阻塞;同理,异步也会有两种实现:异步阻塞、异步⾮阻塞;同步/异步关注的是消息通知的机制,⽽阻塞/⾮阻塞关注的是程序(线程)等待消息通知时的状态
3、IO多路复用
IO多路复⽤(multiplexing)是⼀种同步IO模型,实现⼀个线程可以监视多个⽂件句柄;⼀旦某个⽂件句柄就绪,就能够通知应⽤程序进⾏相应的读写操作;没有⽂件句柄就绪时会阻塞应⽤程序,交出cpu。多路是指⽹络连接,复⽤指的是同⼀个线程服务器端采⽤单线程通过select/epoll等系统调⽤获取fd列表,遍历有事件的fd进⾏accept/recv/send,使其能⽀持更多的并发连接请求。
fds = [listen_fd] // 伪代码描述 while(1) { // 通过内核获取有读写事件发⽣的fd,只要有⼀个则返回,⽆则阻塞 // 整个过程只在调⽤select、poll、epoll这些调⽤的时候才会阻塞,accept/recv是不会阻塞 for (fd in select(fds)) { if (fd == listen_fd) { client_fd = accept(listen_fd) fds.append(client_fd) } elseif (len = recv(fd) && len != -1) { // logic } } }
不同的操作系统中IO多路复⽤的⽅案:
Linux: select、poll、epoll
MacOS/FreeBSD: kqueue
Windows/Solaris: IOCP
select/poll/epoll之间的区别
select
|
pol | epoll | |
数据结构
|
bitmap | 数组 | 红黑树 |
最大连接数 | 1024 | 无上限 | 无上限 |
fd拷⻉
|
每次调⽤select拷⻉
|
每次调⽤poll拷⻉
|
fd⾸次调⽤epoll_ctl拷⻉,每次调⽤epoll_wait不拷⻉
|
⼯作效率
|
轮询:O(n) | 轮询:O(n) | 回调:O(1) |
(三)Java的IO模型
java共⽀持3种⽹络编程模型I/O模式:BIO、NIO、AIO。
Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为⼀个连接⼀个线程,即客户端有连接请求时服务器端就需要启动⼀个线程进⾏处理,如果这个连接不做任何事情会造成不必要的线程开销。
Java NIO:同步⾮阻塞,服务器实现模式为⼀个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复⽤器上,多路复⽤器轮询到连接有 I/O 请求就进⾏处理。
Java AIO(NIO.2):异步⾮阻塞, AIO 引⼊异步通道的概念,采⽤了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,⼀般适⽤于连接数较多且连接时间较⻓的应⽤。
BIO、NIO、AIO 使⽤场景分析:
1. BIO ⽅式适⽤于连接数⽬⽐较⼩且固定的架构,这种⽅式对服务器资源要求⽐较⾼,并发局限于应⽤中,JDK1.4 以前的唯⼀选择,但程序简单易理解。
2. NIO ⽅式适⽤于连接数⽬多且连接⽐较短(轻操作)的架构,⽐如聊天服务器,弹幕系统,服务器通讯等。编程⽐较复杂,JDK1.4 开始⽀持。
3. AIO ⽅式使⽤于连接数⽬多且连接⽐较⻓(重操作)的架构,⽐如相册服务器,充分调⽤ OS 参与并发操作,编程⽐较复杂,JDK7 开始⽀持。
二、BIO编程
(一)BIO说明
Java BIO 就是传统的 Java I/O 编程,其相关的类和接⼝在 java.io。其是同步阻塞的,服务器实现模式为⼀个连接⼀个线程,即客户端有连接请求时服务器端就需要启动⼀个线程进⾏处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。BIO ⽅式适⽤于连接数⽬⽐较⼩且固定的架构,这种⽅式对服务器资源要求⽐较⾼,并发局限于应⽤中,JDK1.4 以前的唯⼀选择,程序简单易理解。
采⽤ BIO 通信模型的服务端,通常由⼀个独⽴的 Acceptor 线程负责监听客户端的连接。我们⼀般通过在 while(true) 循环中服务端会调⽤ accept() ⽅法等待接收客户端的连接的⽅式监听请求,请求⼀旦接收到⼀个连接请求,就可以建⽴通信套接字在这个通信套接字上进⾏读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执⾏完成。
但是,BIO有个致命的问题,在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很⾼,除此之外,线程的切换成本也是很⾼的。尤其在 Linux 这样的操作系统中,线程本质上就是⼀个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。
伪异步IO:为了解决上面提到的BIO的致命问题,后来就有了采⽤线程池和任务队列可以实现⼀种叫做伪异步的 I/O 通信框架。当有新的客户端接⼊时,将客户端的 Socket 封装成⼀个Task(该任务实现java.lang.Runnable接⼝)投递到后端的线程池中进⾏处理,JDK 的线程池维护⼀个消息队列和 N 个活跃线程,对消息队列中的任务进⾏处理。由于线程池可以设置消息队列的⼤⼩和最⼤线程数,因此,它的资源占⽤是可控的,⽆论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
伪异步I/O通信框架采⽤了线程池实现,因此避免了为每个请求都创建⼀个独⽴线程造成的线程资源耗尽问题。不过因为它的底层任然是同步阻塞的BIO模型,因此⽆法从根本上解决问题。
(二)BIO编码
使⽤ BIO 模型编写⼀个服务器端,监听 6666 端⼝,当有客户端连接时,就启动⼀个线程与之通讯。同时要求使⽤线程池机制改善,可以连接多个客户端。服务器端可以接收客户端发送的数据( telnet ⽅式即可)。
public class BIOServer { public static void main(String[] args) throws Exception { //线程池机制 //思路 //1. 创建一个线程池 //2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法) ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); //创建ServerSocket ServerSocket serverSocket = new ServerSocket(6666); System.out.println("服务器启动了"); while (true) { System.out.println("线程信息id = " + Thread.currentThread().getId() + ",名字 = " + Thread.currentThread().getName()); //监听,等待客户端连接 System.out.println("等待连接...."); final Socket socket = serverSocket.accept(); System.out.println("连接到一个客户端"); //就创建一个线程,与之通讯(单独写一个方法) newCachedThreadPool.execute(new Runnable() { public void run() {//我们重写 //可以和客户端通讯 handler(socket); } }); } } //编写一个handler方法,和客户端通讯 public static void handler(Socket socket) { try { byte[] bytes = new byte[1024]; //通过socket获取输入流 InputStream inputStream = socket.getInputStream(); //循环的读取客户端发送的数据 while (true) { System.out.println("线程信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName()); System.out.println("read...."); int read = inputStream.read(bytes); if (read != -1) { System.out.println("客户端说:"+new String(bytes, 0, read));//输出客户端发送的数据 } else { break; } } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("关闭和client的连接"); try { socket.close(); } catch (Exception e) { e.printStackTrace(); } } } }
启动程序,输出结果:
服务器启动了 线程信息id = 1,名字 = main 等待连接....
使用telnet访问服务后输出结果:
服务器启动了 线程信息id = 1,名字 = main 等待连接.... 连接到一个客户端 线程信息id = 1,名字 = main 等待连接.... 线程信息id = 20名字 = pool-1-thread-1 read....
(三)BIO存在的问题
BIO存在如下问题:
1、每个请求都需要创建独⽴的线程,与对应的客户端进⾏数据 Read ,业务处理,数据 Write 。
2、当并发数较⼤时,需要创建⼤量线程来处理连接,系统资源占⽤较⼤。
3、连接建⽴后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
在活动连接数不是特别⾼(⼩于单机1000)的情况下,这种模型是⽐较不错的,可以让每⼀个连接专注于⾃⼰的 I/O 并且编程模型简单,也不⽤过多考虑系统的过载、限流等问题。线程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是,当⾯对⼗万甚⾄百万级连接的时候,传统的 BIO 模型是⽆能为⼒的。因此,我们需要⼀种更⾼效的 I/O 处理模型来应对更⾼的并发量。
三、NIO编程
(一)NIO简介及与BIO对比
1、NIO简介
NIO是⼀种同步⾮阻塞的I/O模型,在Java 1.4 中引⼊了NIO框架,对应 java.nio 包,提供了Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它⽀持⾯向缓冲的,基于通道的I/O操作⽅法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都⽀持阻塞和⾮阻塞两种模式。阻塞模式使⽤就像传统中的⽀持⼀样,⽐较简单,但是性能和可靠性都不好;⾮阻塞模式正好与之相反。对于低负载、低并发的应⽤程序,可以使⽤同步阻塞I/O来提升开发速率和更好的维护性;对于⾼负载、⾼并发的(⽹络)应⽤,应使⽤ NIO 的⾮阻塞模式来开发。
NIO 有三⼤核⼼部分: Channel (通道)、 Buffer (缓冲区)、 Selector (选择器)。
NIO 是⾯向缓冲区,或者⾯向块编程的。数据读取到⼀个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使⽤它可以提供⾮阻塞式的⾼伸缩性⽹络。NIO 是可以做到⽤⼀个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配 50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,⾮得分配 10000 个。HTTP 2.0 使⽤了多路复⽤的技术,做到同⼀个连接并发处理多个请求,⽽且并发请求的数量⽐HTTP 1.1 ⼤了好⼏个数量级。
2、NIO与BIO对比
(1)BIO是面向流,而NIO是面向缓冲区的,或者说BIO 以流的⽅式处理数据,⽽ NIO 以块的⽅式处理数据,块 I/O 的效率⽐流 I/O ⾼很多。
(2)BIO 是阻塞的, NIO 则是⾮阻塞的。
(3)BIO 基于字节流和字符流进⾏操作,⽽ NIO 基于 Channel (通道)和 Buffer (缓冲区)进⾏操作,数据总是从通道读取到缓冲区中,或者从缓冲区写⼊到通道中。 Selector (选择器)⽤于监听多个通道的事件(⽐如:连接请求,数据到达等),因此使⽤单个线程就可以监听多个客户端通道。
(二)NIO核心原理
1、每个 Channel 都会对应⼀个 Buffer 。
2、Selector 对应⼀个线程,⼀个线程对应多个 Channel (连接)。
3、该图反应了有三个 Channel 注册到该 Selector //程序
4、程序切换到哪个 Channel 是由事件决定的, Event 就是⼀个重要的概念。
5、Selector 会根据不同的事件,在各个通道上切换。
6、Buffer 就是⼀个内存块,底层是有⼀个数组。
7、数据的读取写⼊是通过 Buffer ,这个和 BIO , BIO 中要么是输⼊流,或者是输出流,不能双向,但是 NIO 的 Buffer 是可以读也可以写,需要 flip ⽅法切换 Channel 是双向的,可以返回底层操作系统的情况,⽐如 Linux ,底层的操作系统通道就是双向的。
(三)缓冲区(Buffer)
在NIO厍中,所有数据都是⽤缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写⼊数据时,写⼊到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进⾏操作。最常⽤的缓冲区是 ByteBuffer,⼀个 ByteBuffer 提供了⼀组功能⽤于操作 byte 数组。除了ByteBuffer,还有其他的⼀些缓冲区,事实上,每⼀种Java基本类型(除了Boolean类型)都对应有⼀种缓冲区。
在 NIO 中, Buffer 是⼀个顶层⽗类,它是⼀个抽象类,类的层级关系图:
Buffer 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:capacity(容量)、position(游标位置)、limit (末尾限定符),其中,position 和 limit 的意义依赖于当前 Buffer 是处于读模式还是写模式。capacity 的含义⽆论读写模式都是相同的。
Capacity (容量):
作为⼀个内存块,Buffer 有⼀个固定的⼤⼩,我们叫做 “capacity(容量)"。你最多只能向 Buffer 写⼊ capacity ⼤⼩的字节,⻓整数,字符等。⼀旦 Buffer 满了,你必须在继续写⼊数据之前清空它(读出数据,或清除数据)。
Position (游标位置):
当你开始向 Buffer 写⼊数据时,你必须知道数据将要写⼊的位置。position 的初始值为 0。当⼀个字节或⻓整数等类似数据类型被写⼊ Buffer 后,position 就会指向下⼀个将要写⼊数据的位置(根据数据类型⼤⼩计算)。position 的最⼤值是 capacity - 1。
当你需要从 Buffer 读出数据时,你也需要知道将要从什么位置开始读数据。在你调⽤ flip ⽅法将Buffer 从写模式转换为读模式时,position 被重新设置为 0。然后你从 position 指向的位置开始读取数据,接下来 position 指向下⼀个你要读取的位置。
Limit(末尾限定符):
在写模式下对⼀个Buffer的限制即你能将多少数据写⼊Buffer中。在写模式下,限制等同于Buffer的容量(capacity)。
当切换Buffer为读模式时,限制表示你最多能读取到多少数据。因此,当切换Buffer为读模式时,限制会被设置为写模式下的position值。换句话说,你能读到之前写⼊的所有数据(限制被设置为已写的字节数,在写模式下就是position)。
mark(标记):你可以调⽤ Buffer.mark() ⽅法在 Buffer 中标记给定位置。之后,你可以调⽤Buffer.reset() ⽅法重置回标记的这个位置。
Buffer 类相关⽅法⼀览:
最常⽤的⾃然是 ByteBuffer 类(⼆进制数据),该类的主要⽅法如下:
使用一个案例说明buffer
public class BasicBuffer { public static void main(String[] args) { IntBuffer intBuffer = IntBuffer.allocate(10); intBuffer.put(10); intBuffer.put(101); System.err.println("Write mode: "); System.err.println(" Capacity: " + intBuffer.capacity()); System.err.println(" Position: " + intBuffer.position()); System.err.println(" Limit: " + intBuffer.limit()); intBuffer.flip(); System.err.println("Read mode: "); System.err.println(" Capacity: " + intBuffer.capacity()); System.err.println(" Position: " + intBuffer.position()); System.err.println(" Limit: " + intBuffer.limit()); } }
上面代码中 ,首先写入两个 int 值, 此时 capacity = 10, position = 2, limit = 10,然后调用 flip 转换为读模式, 此时 capacity = 10, position = 0, limit = 2;
输出结果:
Write mode: Capacity: 10 Position: 2 Limit: 10 Read mode: Capacity: 10 Position: 0 Limit: 2
(四)通道(Channel)
1、简介
通常来说NIO中的所有IO都是从 Channel(通道) 开始的,类似于BIO中的Stream,但是BIO 中的 Stream 是单向的,例如 FileInputStream 对象只能进⾏读取数据的操作,⽽ NIO中的通道( Channel )是双向的,可以读操作,也可以写操作。
Channel 在 NIO 中是⼀个接⼝ public interface Channel extends Closeable{};常⽤的 Channel 类有FileChannel ⽤于⽂件的数据读写、DatagramChannel ⽤于 UDP 的数据读写、ServerSocketChannel 和 SocketChannel ⽤于 TCP 的数据读写(ServerSocketChannel 类似
以FileChannel为例,FileChannel 主要⽤来对本地⽂件进⾏ IO 操作,常⻅的⽅法有:
public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, longcount) ,从⽬标通道中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChanneltarget) ,把数据从当前通道复制给⽬标通道
2、代码实例:
(1)本地文件写数据
public class NIOFileChannel01 { public static void main(String[] args) throws Exception { String str = "hello,every one,every body"; //创建一个输出流 -> channel FileOutputStream fileOutputStream = new FileOutputStream("/Users/conglongli/Desktop/file01.txt"); //通过 fileOutputStream 获取对应的 FileChannel //这个 fileChannel 真实类型是 FileChannelImpl FileChannel fileChannel = fileOutputStream.getChannel(); //创建一个缓冲区 ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将 str 放入 byteBuffer byteBuffer.put(str.getBytes()); //对 byteBuffer 进行 flip byteBuffer.flip(); //将 byteBuffer 数据写入到 fileChannel fileChannel.write(byteBuffer); fileOutputStream.close(); } }
(2)本地文件读取
public class NIOFileChannel02 { public static void main(String[] args) throws Exception { //创建文件的输入流 File file = new File("/Users/conglongli/Desktop/file01.txt"); FileInputStream fileInputStream = new FileInputStream(file); //通过 fileInputStream 获取对应的 FileChannel -> 实际类型 FileChannelImpl FileChannel fileChannel = fileInputStream.getChannel(); //创建缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length()); //将通道的数据读入到 Buffer fileChannel.read(byteBuffer); //将 byteBuffer 的字节数据转成 String System.out.println(new String(byteBuffer.array())); fileInputStream.close(); } }
(3)使用一个Buffer完成读取和写入
public class NIOFileChannel03 { public static void main(String[] args) throws Exception { FileInputStream fileInputStream = new FileInputStream("/Users/conglongli/Desktop/file01.txt"); FileChannel fileChannel01 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("/Users/conglongli/Desktop/file05.txt"); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { //循环读取 //这里有一个重要的操作,一定不要忘了 /* public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; } */ byteBuffer.clear(); //清空 buffer int read = fileChannel01.read(byteBuffer); System.out.println("read = " + read); if (read == -1) { //表示读完 break; } //将 buffer 中的数据写入到 fileChannel02--2.txt byteBuffer.flip(); fileChannel02.write(byteBuffer); } //关闭相关的流 fileInputStream.close(); fileOutputStream.close(); } }
(4)直接使用transferFrom ⽅法读取和写入
public class NIOFileChannel04 { public static void main(String[] args) throws Exception { //创建相关流 FileInputStream fileInputStream = new FileInputStream("/Users/conglongli/Desktop/123.png"); FileOutputStream fileOutputStream = new FileOutputStream("/Users/conglongli/Desktop/456.png"); //获取各个流对应的 FileChannel FileChannel sourceCh = fileInputStream.getChannel(); FileChannel destCh = fileOutputStream.getChannel(); //使用 transferForm 完成拷贝 destCh.transferFrom(sourceCh, 0, sourceCh.size()); //关闭相关通道和流 sourceCh.close(); destCh.close(); fileInputStream.close(); fileOutputStream.close(); } }
3、关于 Buffer 和 Channel 的注意事项和细节
(1)ByteBuffer ⽀持类型化的 put 和 get , put 放⼊的是什么数据类型, get 就应该使⽤相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。
public class NIOByteBufferPutGet { public static void main(String[] args) { //创建一个 Buffer ByteBuffer buffer = ByteBuffer.allocate(64); //类型化方式放入数据 buffer.putInt(100); buffer.putLong(9); buffer.putChar('尚'); buffer.putShort((short) 4); //取出 buffer.flip(); System.out.println(); System.out.println(buffer.getInt()); System.out.println(buffer.getLong()); System.out.println(buffer.getInt()); //这里应该使用buffer.getChar() System.out.println(buffer.getShort()); } }
(2)可以使用buffer.asReadOnlyBuffer()将⼀个普通 Buffer 转成只读 Buffer
public class ReadOnlyBuffer { public static void main(String[] args) { //创建一个 buffer ByteBuffer buffer = ByteBuffer.allocate(64); for (int i = 0; i < 64; i++) { buffer.put((byte) i); } //读取 buffer.flip();//得到一个只读的 Buffer ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); System.out.println(readOnlyBuffer.getClass()); //读取 while (readOnlyBuffer.hasRemaining()) { System.out.println(readOnlyBuffer.get()); } readOnlyBuffer.put((byte) 100); //ReadOnlyBufferException } }
(3)NIO 还提供了 MappedByteBuffer ,可以让⽂件直接在内存(堆外的内存)中进⾏修改,⽽如何同步到⽂件由 NIO 来完成。
public class MappedByteBufferTest { public static void main(String[] args) throws Exception { RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/conglongli/Desktop/file01.txt", "rw"); //获取对应的通道 FileChannel channel = randomAccessFile.getChannel(); /** * 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式 * 参数 2:0:可以直接修改的起始位置 * 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存 * 可以直接修改的范围就是 0-5 * 实际类型 DirectByteBuffer */ MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); mappedByteBuffer.put(0, (byte) 'C'); mappedByteBuffer.put(3, (byte) '8'); //mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException randomAccessFile.close(); System.out.println("修改成功~~"); } }
(4)前⾯的读写操作,都是通过⼀个 Buffer 完成的, NIO 还⽀持通过多个 Buffer (即 Buffer 数组)完成读写操作,即 Scattering 和 Gathering
public class ScatteringAndGatheringTest { public static void main(String[] args) throws Exception { //使用 ServerSocketChannel 和 SocketChannel 网络 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); //绑定端口到 socket,并启动 serverSocketChannel.socket().bind(inetSocketAddress); //创建 buffer 数组 ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(5); byteBuffers[1] = ByteBuffer.allocate(3); //等客户端连接 (telnet) SocketChannel socketChannel = serverSocketChannel.accept(); int messageLength = 8; //假定从客户端接收 8 个字节 //循环的读取 while (true) { int byteRead = 0; while (byteRead < messageLength) { long l = socketChannel.read(byteBuffers); byteRead += 1; //累计读取的字节数 System.out.println("byteRead = " + byteRead); //使用流打印,看看当前的这个 buffer 的 position 和 limit Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println); } //将所有的 buffer 进行 flip Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip()); //将数据读出显示到客户端 long byteWirte = 0; while (byteWirte < messageLength) { long l = socketChannel.write(byteBuffers);// byteWirte += l; } //将所有的buffer进行clear Arrays.asList(byteBuffers).forEach(buffer -> { buffer.clear(); }); System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWirte + ", messagelength = " + messageLength); } } }
(五)选择器(Selector)
选择器⽤于使⽤单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提⾼系统效率选择器是有⽤的。NIO有选择器,⽽IO没有。
Java 的 NIO ,⽤⾮阻塞的 IO ⽅式。可以⽤⼀个线程,处理多个的客户端连接,就会使⽤到Selector (选择器)。Selector 能够检测多个注册的通道上是否有事件发⽣(注意:多个 Channel 以事件的⽅式可以注册到同⼀个 Selector ),如果有事件发⽣,便获取事件然后针对每个事件进⾏相应的处理。
选择器的好处是可以只⽤⼀个单线程去管理多个通道,也就是管理多个连接和请求。只有在连接/通道真正有读写事件发⽣时,才会进⾏读写,就⼤⼤地减少了系统开销,并且不必为每个连接都创建⼀个线程,不⽤去维护多个线程。同时也避免了多线程之间的上下⽂切换导致的开销。
选择器需要介绍四个类:Selector、SelectionKey、ServerSocketChannel、SocketChannel
1、Selector类
Selector 相关⽅法说明:
selector.select(); //阻塞
selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
selector.wakeup(); //唤醒 selector
selector.selectNow(); //不阻塞,⽴⻢返还
2、SelectionKey
SelectionKey ,表示 Selector 和⽹络通道的注册关系,共四种:
int OP_ACCEPT :有新的⽹络连接可以 accept ,值为 16
int OP_CONNECT :代表连接已经建⽴,值为 8
int OP_READ :代表读操作,值为 1
int OP_WRITE :代表写操作,值为 4
源码中:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
SelectionKey 相关⽅法:
3、ServerSocketChannel
ServerSocketChannel 在服务器端监听新的客户端 Socket 连接,ServerSocketChannel类似ServerSocket。
相关⽅法如下:
4、SocketChannel
SocketChannel,⽹络 IO 通道,具体负责进⾏读写操作。NIO 把缓冲区的数据写⼊通道,或者把通道⾥的数据读到缓冲区。SocketChannel 类似 Socket。
5、应用实例
服务端:
public class WebServer { public static void main(String[] args) { try { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000)); ssc.configureBlocking(false); Selector selector = Selector.open(); // 注册 channel,并且指定感兴趣的事件是 Accept ssc.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer readBuff = ByteBuffer.allocate(1024); ByteBuffer writeBuff = ByteBuffer.allocate(128); writeBuff.put("received".getBytes()); writeBuff.flip(); while (true) { int nReady = selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); if (key.isAcceptable()) { // 创建新的连接,并且把连接注册到selector上,而且, // 声明这个channel只对读操作感兴趣。 SocketChannel socketChannel = ssc.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); readBuff.clear(); socketChannel.read(readBuff); String msg = new String(readBuff.array(),0, readBuff.position(), "utf-8"); readBuff.flip(); System.out.println("received : " + msg); key.interestOps(SelectionKey.OP_WRITE); } else if (key.isWritable()) { writeBuff.rewind(); SocketChannel socketChannel = (SocketChannel) key.channel(); socketChannel.write(writeBuff); key.interestOps(SelectionKey.OP_READ); } } } } catch (IOException e) { e.printStackTrace(); } } }
客户端:
public class WebClient { public static void main(String[] args) throws IOException { try { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000)); ByteBuffer writeBuffer = ByteBuffer.allocate(32); ByteBuffer readBuffer = ByteBuffer.allocate(32); writeBuffer.put("hello111".getBytes()); writeBuffer.flip(); while (true) { writeBuffer.rewind(); socketChannel.write(writeBuffer); readBuffer.clear(); socketChannel.read(readBuffer); } } catch (IOException e) { } } }
启动两个客户端,一个客户端put值为hello111,一个客户端put值为hello111222,服务端输出结果:
(六)NIO与零拷贝
可以参看一下:NIO与零拷贝
四、AIO
JDK7 引⼊了 AsynchronousI/O ,即 AIO 。在进⾏ I/O 编程中,常⽤到两种模式: Reactor和 Proactor 。 Java 的 NIO 就是 Reactor ,当有事件触发时,服务器端得到通知,进⾏相应的处理。
AIO 即 NIO2.0 ,叫做异步不阻塞的 IO 。 AIO 引⼊异步通道的概念,采⽤了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,⼀般适⽤于连接数较多且连接时间较⻓的应⽤。
⽬前 AIO 还没有⼴泛应⽤, Netty 也是基于 NIO ,⽽不是 AIO。