Java IO 基本概念

Java IO:即 Java 输入 / 输出系统。
区分 Java 的输入和输出:把自己当成程序, 当你从外边读数据到自己这里就用输入(InputStream/Reader), 向外边写数据就用输出(OutputStream/Writer)。
Stream:Java 中将数据的输入输出抽象为流,流是一组有顺序的,单向的,有起点和终点的数据集合,就像水流。按照流中的最小数据单元又分为字节流和字符流。

  1. 字节流:以 8 位(即 1 byte,8 bit)作为一个数据单元,数据流中最小的数据单元是字节。
  2. 字符流:以 16 位(即 1 char,2 byte,16 bit)作为一个数据单元,数据流中最小的数据单元是字符, Java 中的字符是 Unicode 编码,一个字符占用两个字节。

IO 的分类0
image.png
流式部分和非流式部分
Java 的 IO 主要包含两个部分:

1.流式部分

是 IO 的主体部分,也是本文介绍的重点, 流式部分根据流向分为输入流(InputStream/Reader)和输出流(OutputStream/Writer), 根据数据不同的操作单元,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),依据字节流和字符流,Java 定义了用来操作数据的抽象基类InputStream/OutputStream 和 Reader/Writer,再根据不同应用场景(或功能),在这两种抽象基类上基于数据载体或功能派上出很多子类,用来满足文件,网络,管道等不同场景的 IO 需求,从而形成了 Java 的基本 IO 体系。
下面是 Java IO 体系中常用的流类:

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutPutStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
字符串 StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 PrintStream PrintWriter
推回输入流 PushbackInputStream PushBackReader
特殊流 DataInputStream DataOutputStream

2.非流式部分

主要包含一些辅助流式部分的类,如: SerializablePermission 类、File 类、RandomAccessFile 类和 FileDescriptor 等;

3.InputStream

image.png
总结:

  1. InputStream 是所有的输入字节流的父类,它是一个抽象类。
  2. PushbackInputStream、DataInputStream 和 BufferedInput Stream都是处理流,他们的的父类是 FilterInputStream。
  3. ByteArrayInputStream、StringBufferInputStream、FileInputStream 是三种基本的介质流,它们分别从 Byte 数组、StringBuffer、和本地文件中读取数据。PipedInputStream 是从与其它线程共用的管道中读取数据。

InputStream 中的三个基本的读方法

  • abstract int read() :读取一个字节数据,并返回读到的数据,如果返回 -1,表示读到了输入流的末尾。
  • int read(byte[] b) :将数据读入一个字节数组,同时返回实际读取的字节数。如果返回-1,表示读到了输入流的末尾。
  • int read(byte[] b, int off, int len) :将数据读入一个字节数组,同时返回实际读取的字节数。如果返回 -1,表示读到了输入流的末尾。off 指定在数组 b 中存放数据的起始偏移位置;len 指定读取的最大字节数。

4.OutputStream

image.png
总结:

  1. OutputStream 是所有的输出字节流的父类,它是一个抽象类。
  2. ByteArrayOutputStream、FileOutputStream 是两种基本的介质流,它们分别向 Byte 数组、和本地文件中写入数据。
  3. PipedOutputStream 是向与其它线程共用的管道中写入数据。
  4. BufferedOutputStream、DataOutputStream 和 PrintStream 都是处理流,他们的的父类是 FilterOutputStream。

outputStream中的三个基本的写方法

  • abstract void write(int b):往输出流中写入一个字节。
  • void write(byte[] b) :往输出流中写入数组b中的所有字节。
  • void write(byte[] b, int?off, int?len) :往输出流中写入数组 b 中从偏移量 off 开始的 len 个字节的数据。

5.Reader

image.png
Reader 是所有的输入字符流的父类,它是一个抽象类。

  1. CharReader、StringReader 是两种基本的介质流,它们分别将 Char 数组、String 中读取数据。PipedReader 是从与其它线程共用的管道中读取数据。
  2. BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它 Reader 对象。
  3. FilterReader 是所有自定义具体装饰流的父类,其子类 PushbackReader 对 Reader 对象进行装饰,会增加一个行号。
  4. InputStreamReader 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。

Reader 基本的三个读方法(和字节流对应):

  • public int read() throws IOException; 读取一个字符,返回值为读取的字符。
  • public int read(char cbuf[]) throws IOException; 读取一系列字符到数组 cbuf[]中,返回值为实际读取的字符的数量。
  • public abstract int read(char cbuf[],int off,int len) throws IOException; 读取 len 个字符,从数组 cbuf[] 的下标 off 处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现。

6.Writer

image.png
Writer 是所有的输出字符流的父类,它是一个抽象类。

  1. CharArrayWriter、StringWriter 是两种基本的介质流,它们分别向 Char 数组、String 中写入数据。PipedWriter 是向与其它线程共用的管道中写入数据。
  2. BufferedWriter 是一个装饰器为 Writer 提供缓冲功能。
  3. PrintWriter 和 PrintStream 极其类似,功能和使用也非常相似。
  4. OutputStreamWriter 是 OutputStream 到 Writer 转换的桥梁,它的子类 FileWriter 其实就是一个实现此功能的具体类。

writer 的主要写方法:

  • public void write(int c) throws IOException; //写单个字符
  • public void write(char cbuf[]) throws IOException; //将字符数组 cbuf[] 写到输出流 。
  • public abstract void write(char cbuf[],int off,int len) throws IOException; //将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流 。
  • public void write(String str) throws IOException; //将字符串str中的字符写入输出流 。
  • public void write(String str,int off,int len) throws IOException; //将字符串 str 中从索引 off 开始处的 len 个字符写入输出流 。

7.实例

字节流

  1. public class TestStream {
  2. /**
  3. * 文件流
  4. * @throws Exception
  5. */
  6. @Test
  7. public void testWrite() throws Exception {
  8. File file = new File("D:\\work\\学习\\study.txt");
  9. String str = "test write";
  10. //1.建立通道
  11. FileOutputStream fos = new FileOutputStream(file);
  12. //2.写数据
  13. fos.write(str.getBytes());
  14. //3.关闭通道
  15. fos.close();
  16. }
  17. /**
  18. * FileOutputStream 字节输出流
  19. * @throws Exception
  20. */
  21. @Test
  22. public void testWrite2() throws Exception {
  23. File file = new File("D:\\work\\学习\\study.txt");
  24. //追加文件
  25. FileOutputStream fos = new FileOutputStream(file, true);
  26. String str = "\r\n中国人";
  27. //2.写数据
  28. fos.write(str.getBytes());
  29. //3.关闭通道
  30. fos.close();
  31. }
  32. /**
  33. * FileOutputStream 字节输出流
  34. * @throws Exception
  35. */
  36. @Test
  37. public void testRead() throws Exception {
  38. File file = new File("D:\\work\\学习\\study.txt");
  39. //1建立管道
  40. FileInputStream is = new FileInputStream(file);
  41. while(is.read() != -1) {
  42. System.out.print((char)is.read());
  43. }
  44. //3.关闭通道
  45. is.close();
  46. }
  47. /**
  48. * 测试copy文件
  49. * @throws Exception
  50. */
  51. @Test
  52. public void copyfile() throws Exception {
  53. //建立读取视频的管道
  54. FileInputStream fis = new FileInputStream("D:\\work\\学习\\io\\from\\test.wmv");
  55. BufferedInputStream bis = new BufferedInputStream(fis); //包装成字符流
  56. //创建用于接收视频的文件
  57. File file = new File("D:\\work\\学习\\io\\to\\test.wmv");
  58. if(!file.exists()) {
  59. boolean flag = file.createNewFile();
  60. if(flag) {
  61. System.out.println("创建文件成功");
  62. }else {
  63. System.out.println("创建文件失败");
  64. }
  65. }
  66. //建立写入视频的管道
  67. FileOutputStream fos = new FileOutputStream(file);
  68. BufferedOutputStream bos = new BufferedOutputStream(fos); //包装成字符流
  69. //读取数据 设置缓存区大小
  70. byte[] buf = new byte[1024];
  71. int len = 0;
  72. while((len = bis.read(buf)) != -1){
  73. bos.write(buf, 0, len);
  74. }
  75. //关闭流 先关外部管道 再关内部管道
  76. bis.close();
  77. fis.close();
  78. fos.close();
  79. bos.close();
  80. }
  81. }

字符流:

  1. /**
  2. * @author meikb
  3. * @date 2018年1月8日
  4. */
  5. public class TestFileder {
  6. public static void main(String[] args) {
  7. //readFile();
  8. writeFile();
  9. }
  10. /**
  11. * 读取文件内容
  12. */
  13. public static void readFile() {
  14. try {
  15. BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("E:\\txt\\mkb.txt"), "UTF-8"));
  16. String line = null;
  17. // 每次读取一行内容,循环读取,读到文件末尾结束
  18. while ((line = br.readLine()) != null) {
  19. System.out.println(line);
  20. }
  21. // 关闭I/O流
  22. br.close();
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. /**
  28. * 读取文件内容
  29. */
  30. public static void writeFile() {
  31. try {
  32. FileOutputStream fos = new FileOutputStream("E:\\txt\\mkb.txt", true);
  33. OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
  34. osw.write("小红帽");
  35. BufferedWriter br = new BufferedWriter(osw);
  36. br.write("白雪公主");
  37. PrintWriter pw = new PrintWriter(fos);
  38. pw.println("总一条蜿蜒在童话镇里七彩的河");
  39. pw.flush();
  40. br.close(); //字符流采用缓冲区 必须关闭才能输出
  41. pw.close();
  42. } catch (Exception e) {
  43. e.printStackTrace();
  44. }
  45. }
  46. }

BIO

server

  1. public class BioServer {
  2. public static void main(String[] args) throws Exception{
  3. ServerSocket serverSocket = new ServerSocket(8181);
  4. while (true){
  5. //等待接受连接,这个方法是阻塞的
  6. System.out.println("等待连接");
  7. Socket socket = serverSocket.accept();
  8. new Thread(new Runnable() {
  9. @Override
  10. public void run() {
  11. try {
  12. InputStream inputStream = socket.getInputStream();
  13. byte[] bufArr = new byte[2048];
  14. while (true){
  15. int length = inputStream.read(bufArr);
  16. // 如果不判断流结束,上面的read()读不到数据会一直堵塞
  17. if(length == -1){
  18. break;
  19. }
  20. System.out.println("接受到消息" + new String(bufArr));
  21. }
  22. }catch (Exception e){
  23. }
  24. }
  25. }).start();
  26. }
  27. }
  28. }

client

  1. public class BioClient {
  2. public static void main(String[] args) throws Exception{
  3. Socket serverSocket = new Socket("localhost", 8181);
  4. OutputStream outputStream = serverSocket.getOutputStream();
  5. Scanner scanner = new Scanner(System.in);
  6. System.out.println("请输入:");
  7. while (scanner.hasNextLine()){
  8. String msg = scanner.nextLine();
  9. outputStream.write(msg.getBytes());
  10. }
  11. }
  12. }

BIO: accept 和 read方法都是阻塞方法,通过 new thread 可以解决问题,但是不可能所有的socket都新建一个线程去执行。

Java NIO

I/O介绍

网络I/O:本质是socket读取

磁盘I/O:每次I/O,都经由两个阶段:
第一步:将数据先从磁盘文件先加载至内核内存空间(缓冲区),等待数据准备完成时间较长。
第二步:将数据从内核缓冲区复制到用户空间进程的内存中,时间较短

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

文件描述符 FD

文件描述符是一个用于表述指向文件的引用的抽象化概念。在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。

在linux中一切皆文件,当客户端发起连接请求的时候,会在指定的目录下生产,一个文件,来记录这个进程。

select

1、最大并发数限制,因为一个进程所打开的 FD(文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024/2048 ,因此Select模型的最大并发数就被相应限制了。
2、效率问题,select每次调用都会线性扫描全部的FD集合,这样效率就会呈现线性下降,把FD_SETSIZE改大的后果就是,都超时了。
3、内核 / 用户空间内存拷贝问题,如何让内核把FD消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法,在FD非常多的时候,非常的耗费时间。
总结:1、连接数受限 2、查找配对速度慢 3、数据由内核拷贝到用户态消耗时间

epoll

1、Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max查看。
2、效率提升,Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中, Epoll的效率就会远远高于select和poll。
3、内存共享, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。
总结:把描述符列表的管理交由内核负责,一旦有某种事件发生,内核把发生事件的描述符列表通知给进程,避免轮询减少系统开销

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的文件描述符集合,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的文件描述符集合就行了。当描述符多的时候也只是会占用较多的内存而已,而不会造成占用大量cpu时间

Buffer

buffer 本身就是一块内存,实际上就是一个数组,数据的读写都是通过 buffer 来实现的。
java 中 8 种基本数据类型都有各自对应的 buffer 类型(除 boolean 外),如 IntBuffer、CharBuffer、ByteBuffer、ShortBuffer、LongBuffer 等。

image.png

capacity:最大容量,永远不可能是负数,并且是不会变化的
limit:限制,永远不可能是负数,并且不会大于 capacity
position:写一个读或者写的位置,永远不可能是负数,并且不会大于 limit

  • buffer.put():往此 buffer 中放置元素(往数组中写)
  • buffer.get():往此 buffer 中取出元素(往数组中读)

Channel

所有数据读写都是通过 buffer 来进行的,永远不会出现直接在 channel 中直接写入、读取数据,与 stream 不同的是,channel 是双向的,而 stream 可能是 inputSteram 或 outputStream,而 channel 打开后可以读又可以写

  • channel.read(buffer):从通道中读取数据写到 buffer 中,对于 buffer 来说是写操作
  • channel.write(buffer):从 buffer 中读取数据写到到 channel 中,对于 buffer 来说是读操作

什么是阻塞、什么是同步?

阻塞:用户程序向操作系统提出IO请求,操作系统对于用户程序的请求是否立即返回,如果立即返回就是非阻塞的,如果不是立即返回的就是阻塞的;
同步:用户程序的读写操作是否暂停用户程序,如果用户程序停下手头的工作去忙活读写操作就是同步的,如果不用停下用户程序,操作系统就可以帮忙读写那就是异步的;

阻塞 IO 和非阻塞 IO 的区别就在于:应用程序的调用是否立即返回!
同步 IO 和异步 IO 的区别就在于:数据拷贝的时候进程是否阻塞!

IO的几种类型

1.同步阻塞IO(BIO):
  用户程序向操作系统发送请求后,操作系统一直不返回所以一直堵塞,当操作系统将数据准备好以后返回,然后用户程序将数据写入socket空间或者将数据读出socket空间;

2,同步非阻塞IO(NIO)//NIO有两种,还有一种是IO多路复用
  用户程序向操作系统发送请求后,操作体统立即返回(非阻塞),返回之后用户程序可以继续进行自己的事,用户线程要定时轮询检查数据是否就绪,当数据就绪后,用户线程将数据从用户空间写入socket空间,或从socket空间读取数据到用户空间(同步)。

3.IO多路复用
  用户程序向操作系统发送请求后,操作系统立即返回(非阻塞),将socket连接及关注事件注册到selector(多路复用器,os级别线程)上,selector循环遍历socket连接,看是否有关注数据就绪,如果连接有数据就绪后,就通知应用程序,建立线程进行数据读写。同BIO对比,NIO中线程处理的都是有效连接(数据就绪),且一个线程可以分管处理多个连接上的就绪数据,节省线程资源开销。
上一个NIO轮询检查数据是否就绪,多路复用就是将数据是否就绪的问题交给了操作系统,当数据准备就绪后通知用户程序建立连接进行想要的IO操作;

3.异步非阻塞(AIO):
  用户程序向操作系统发送请求后,操作系统立即返回(非阻塞),当操作系统将数据的读写操作完成后才会通知用户程序,这里面的读写操作也都是OS进行的不会停止用户程序(异步);
**

NIO

server

  1. public class NioServer {
  2. static SelectorProvider provider = SelectorProvider.provider();
  3. static Selector selector = null;
  4. static ServerSocketChannel server = null;
  5. private static void accept() throws IOException {
  6. SocketChannel channel = null;
  7. try {
  8. channel = server.accept(); // 接受连接
  9. channel.configureBlocking(false); // 非阻塞模式
  10. channel.register(selector, SelectionKey.OP_READ, null); // 监听读就绪
  11. } catch (IOException e) {
  12. if (channel != null)
  13. channel.close();
  14. }
  15. }
  16. private static int read(SocketChannel channel) throws IOException {
  17. try {
  18. ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配HeapByteBuffer
  19. int len = channel.read(buffer); // 直到没有数据 || buffer满
  20. if (len > 0)
  21. System.out.println(new String(buffer.array(), 0, len, Charset.forName("UTF-8"))); // buffer.array():取HeapByteBuffer中的原始byte[]
  22. return len;
  23. } catch (IOException e) {
  24. if (channel != null)
  25. channel.close();
  26. return -1;
  27. }
  28. }
  29. private static void write(SocketChannel channel, String msg) throws IOException {
  30. try {
  31. byte[] bytes = msg.getBytes(Charset.forName("UTF-8"));
  32. ByteBuffer buffer = ByteBuffer.allocate(bytes.length); // 分配HeapByteBuffer
  33. buffer.put(bytes);
  34. buffer.flip(); // 切换为读模式
  35. channel.write(buffer);
  36. } catch (IOException e) {
  37. if (channel != null)
  38. channel.close();
  39. }
  40. }
  41. public static void main(String[] args) throws IOException {
  42. try {
  43. selector = provider.openSelector();
  44. server = provider.openServerSocketChannel();
  45. server.configureBlocking(false); // 非阻塞模式
  46. SelectionKey key = server.register(selector, 0, null); // 注册
  47. if (server.bind(new InetSocketAddress(8888)).socket().isBound()) // 绑定成功
  48. key.interestOps(SelectionKey.OP_ACCEPT); // 监听连接请求
  49. while (true) {
  50. selector.select(); // 监听就绪事件
  51. Iterator<SelectionKey> it = selector.selectedKeys().iterator();
  52. while (it.hasNext()) {
  53. key = it.next();
  54. it.remove(); // 从已选择键集中移除key
  55. if (key.isAcceptable()) { // 连接请求到来
  56. System.out.println("accept...");
  57. accept();
  58. } else {
  59. SocketChannel channel = (SocketChannel) key.channel();
  60. if (key.isWritable()) { // 写就绪
  61. System.out.println("write...");
  62. write(channel, "Hello NioClient!");
  63. key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); // 取消写就绪,否则会一直触发写就绪(写就绪为代码触发)
  64. key.channel().close(); // 关闭channel(key将失效)
  65. }
  66. if (key.isValid() && key.isReadable()) { // key有效(避免在写就绪时关闭了channel或者取消了key) && 读就绪
  67. System.out.println("read...");
  68. int len = read(channel);
  69. if (len >= 0)
  70. key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); // 写就绪,准备写数据
  71. else if (len < 0) // 客户端已关闭socket
  72. channel.close(); // 关闭channel(key将失效)
  73. }
  74. }
  75. }
  76. }
  77. } finally {
  78. if (server != null)
  79. server.close();
  80. if (selector != null)
  81. selector.close();
  82. }
  83. }
  84. }

client

  1. public class NioClient {
  2. static SelectorProvider provider = SelectorProvider.provider();
  3. static Selector selector = null;
  4. static SocketChannel client = null;
  5. static boolean close = false;
  6. private static void write(String msg) throws IOException {
  7. byte[] bytes = msg.getBytes(Charset.forName("UTF-8"));
  8. ByteBuffer buffer = ByteBuffer.allocate(bytes.length); // 建立HeapByteBuffer(DirectByteBuffer以后有机会再讨论)
  9. buffer.put(bytes);
  10. buffer.flip(); // 切换为读模式
  11. client.write(buffer);
  12. }
  13. private static int read() throws IOException {
  14. ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配HeapByteBuffer
  15. int len = client.read(buffer); // 直到没有数据 || buffer满
  16. if (len > 0)
  17. System.out.println(new String(buffer.array(), 0, len, Charset.forName("UTF-8"))); // buffer.array():取HeapByteBuffer中的原始byte[]
  18. return len;
  19. }
  20. public static void main(String[] args) throws IOException {
  21. try {
  22. selector = provider.openSelector();
  23. client = provider.openSocketChannel();
  24. client.configureBlocking(false); // 非阻塞模式
  25. SelectionKey key = client.register(selector, 0, null); // 注册
  26. if (client.connect(new InetSocketAddress("127.0.0.1", 8888))) { // 连接成功(很难)
  27. System.out.println("connected...");
  28. key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 监听读就绪和写就绪(准备写数据)
  29. } else // 连接失败(正常情况下)
  30. key.interestOps(SelectionKey.OP_CONNECT); // 监听连接就绪
  31. while (!close) {
  32. selector.select(); // 监听就绪事件
  33. Iterator<SelectionKey> it = selector.selectedKeys().iterator();
  34. while (it.hasNext()) {
  35. key = it.next();
  36. it.remove(); // 从已选择键集移除key
  37. if (key.isConnectable()) { // 连接就绪
  38. client.finishConnect(); // 完成连接
  39. System.out.println("connected...");
  40. key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT); // 取消监听连接就绪(否则selector会不断提醒连接就绪)
  41. key.interestOps(key.interestOps() | SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 监听读就绪和写就绪
  42. } else {
  43. if (key.isWritable()) { // 写就绪
  44. System.out.println("write...");
  45. write("Hello NioServer!");
  46. key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); // 取消写就绪,否则会一直触发写就绪(写就绪为代码触发)
  47. }
  48. if (key.isValid() && key.isReadable()) { // key有效(避免在写就绪时关闭了channel或者取消了key) && 读就绪
  49. System.out.println("read...");
  50. if (read() < 0) // 服务器已关闭socket
  51. close = true; // 退出循环
  52. }
  53. }
  54. }
  55. }
  56. } finally {
  57. if (client != null)
  58. client.close();
  59. if (selector != null)
  60. selector.close();
  61. }
  62. }
  63. }

NIO原理图

使用select 原理图

image.png

使用epoll原理图

image.png