# nio **Repository Path**: This-wang/nio ## Basic Information - **Project Name**: nio - **Description**: 【尚硅谷】Java NIO详细教程(一套带你掌握IO API/javanio) - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: https://www.bilibili.com/video/BV1E64y1h7Z4?p=1&vd_source=353e8b0a2cae8e4403f67e77ce2a4fe2 - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-11-08 - **Last Updated**: 2025-11-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Java IO模型系列教程-目录大纲 *** ## 目录 - [Java IO模型系列教程-目录大纲](#java-io模型系列教程-目录大纲) - [目录](#目录) - [前言](#前言) - [Java IO模型系列教程-第一章-操作系统的IO模型](#java-io模型系列教程-第一章-操作系统的io模型) - [五种IO模型:操作系统五种IO模型大全](#五种io模型操作系统五种io模型大全) - [一、IO模型简介](#一io模型简介) - [1.1 操作系统的内存简介](#11-操作系统的内存简介) - [1.1.1 操作系统的应用与内核](#111-操作系统的应用与内核) - [1.1.2 内核空间与用户空间](#112-内核空间与用户空间) - [1.1.3 CPU指令等级](#113-cpu指令等级) - [1.1.4 进程的内核态和用户态](#114-进程的内核态和用户态) - [1.2 IO的分类](#12-io的分类) - [1.2.1 网络IO和磁盘IO](#121-网络io和磁盘io) - [1.2.1 同步IO和异步IO](#121-同步io和异步io) - [1.2.2 阻塞IO和非阻塞IO](#122-阻塞io和非阻塞io) - [1.3 操作系统的五种IO模型](#13-操作系统的五种io模型) - [1.3.1 阻塞IO模型](#131-阻塞io模型) - [1.3.2 非阻塞IO模型](#132-非阻塞io模型) - [1.3.3 复用IO模型](#133-复用io模型) - [1.3.4 信号驱动IO模型](#134-信号驱动io模型) - [1.3.5 异步IO模型](#135-异步io模型) - [Java IO模型系列教程-第二章-Java的IO模型](#java-io模型系列教程-第二章-java的io模型) - [二、Java的IO模型](#二java的io模型) - [2.1 Java的IO模型支持](#21-java的io模型支持) - [2.2 BIO(blocking I/O)模型](#22-bioblocking-io模型) - [2.3 NIO(non-blocking I/O)模型](#23-nionon-blocking-io模型) - [2.3.1 Buffer(缓冲区)](#231-buffer缓冲区) - [2.3.2 Channel(通道)](#232-channel通道) - [2.3.3 Selector(选择器)](#233-selector选择器) - [2.3 AIO模型](#23-aio模型) - [Java IO模型系列教程-第三章-基于BIO编程](#java-io模型系列教程-第三章-基于bio编程) - [三、BIO编程](#三bio编程) - [3.1 BIO](#31-bio) - [3.2 文件IO编程](#32-文件io编程) - [3.3 网络IO编程](#33-网络io编程) - [3.3.1 UDP编程](#331-udp编程) - [3.3.2 TCP编程](#332-tcp编程) - [3.3.3 伪异步IO实现网络IO编程](#333-伪异步io实现网络io编程) - [Java IO模型系列教程-第四章-NIO核心组件之Buffer](#java-io模型系列教程-第四章-nio核心组件之buffer) - [四、NIO核心组件之Buffer](#四nio核心组件之buffer) - [4.1 Buffer相关子类](#41-buffer相关子类) - [4.2 Buffer中的属性](#42-buffer中的属性) - [4.3 Buffer相关方法](#43-buffer相关方法) - [4.3.1 Buffer的创建](#431-buffer的创建) - [4.3.2 Buffer的常用方法](#432-buffer的常用方法) - [4.3.2 Buffer的核心方法](#432-buffer的核心方法) - [4.3.3 只读Buffer](#433-只读buffer) - [4.3.4 直接内存与非直接内存](#434-直接内存与非直接内存) - [Java IO模型系列教程-第五章-NIO核心组件之Channel](#java-io模型系列教程-第五章-nio核心组件之channel) - [五、NIO核心组件之Channel](#五nio核心组件之channel) - [5.1 FileChannel](#51-filechannel) - [5.1.1 FileChannel的基本使用](#511-filechannel的基本使用) - [5.1.2 FileChannel的常用方法](#512-filechannel的常用方法) - [5.1.3 聚集和分散](#513-聚集和分散) - [5.2 DatagramChannel](#52-datagramchannel) - [5.2.1 DatagramChannel简介](#521-datagramchannel简介) - [5.2.2 DatagramChannel的获取](#522-datagramchannel的获取) - [5.2.3 DatagramChannel的常用方法](#523-datagramchannel的常用方法) - [5.3 SocketChannel与ServerSocketChannel](#53-socketchannel与serversocketchannel) - [5.3.1 Channel的简介](#531-channel的简介) - [5.3.2 Channel的获取](#532-channel的获取) - [5.3.3 Channel的常用方法](#533-channel的常用方法) - [Java IO模型系列教程-第六章-NIO核心组件之Selector](#java-io模型系列教程-第六章-nio核心组件之selector) - [六、Selector](#六selector) - [6.1 Selector的简介](#61-selector的简介) - [6.1.1 Seelctor概念](#611-seelctor概念) - [6.1.2 Channel的注册](#612-channel的注册) - [6.2 Selector的事件注册与监听](#62-selector的事件注册与监听) - [6.2.1 选择通道](#621-选择通道) - [6.2.2 事件注册与监听](#622-事件注册与监听) - [6.3 Selector其他方法](#63-selector其他方法) - [6.4 Selector综合案例](#64-selector综合案例) - [6.4.1 Selector改造多线程聊天案例](#641-selector改造多线程聊天案例) - [Java IO模型系列教程-第七章-NIO的其他API之Files、Path的使用](#java-io模型系列教程-第七章-nio的其他api之filespath的使用) - [七、Path、Files类的使用](#七pathfiles类的使用) - [7.1 Path](#71-path) - [7.1.1 Path简介](#711-path简介) - [7.1.2 Path相关方法](#712-path相关方法) - [7.2 Files类](#72-files类) - [7.2.1 Files类简介](#721-files类简介) - [7.2.2 Files类相关方法](#722-files类相关方法) - [Java IO模型系列教程-第八章-AIO编程的应用实例](#java-io模型系列教程-第八章-aio编程的应用实例) - [八、AIO编程](#八aio编程) - [8.1 AIO编程简介](#81-aio编程简介) - [8.1.1 AIO编程概述](#811-aio编程概述) - [8.1.2 AIO与其他IO的区别](#812-aio与其他io的区别) - [8.2 实现AIO编程](#82-实现aio编程) - [8.2.1 AIO基于文件编程](#821-aio基于文件编程) - [8.2.2 AIO基于网络编程](#822-aio基于网络编程) *** **鸣谢** **本系列教程参考了市面上大部分的IO模型文献,其中包括但不限于:CSDN/简书/掘金/知乎/B站/慕课等网站上许多博主的技术文献/视频,另外还参考了《NIO与Socket编程技术指南》、《Netty实战》等权威书籍,并摘抄许多有趣的案例;特别感谢《并发编程网》、《w3cschool》、《jenkov的教程网》等网站,本系列教程关于类的API使用大都来源于这写网站;当然其中也包含《JDK API文档》;** *** - **参考文献的部分链接(部分):** - jenkov的教程网: - 并发编程网: - w3cschool: - JDK 1.8 API: - 参考书籍:
*** ## 前言 *** **内容包括BIO、NIO、AIO等Java支持的三大IO模型编程案例,另外提供NIO的一些工具类的使用,Files/Path等,并附上完整案例代码,包括基于磁盘的BIO(FileInputStream/FileOutputStream/RandomAccessFile),基于网络的BIO(Socket/ServerSocket/DatagramSocket),NIO的三大组件:Buffer、Channel、Selector等,基于磁盘的NIO:FileChannel;基于网络的NIO:SocketChannel/ServerChannel/DatagramChannel等;基于AIO编程的AsynchronousFileChannel、AsynchronousServerSocketChannel、AsynchronousSocketChannel、CompletionHandler、Future等的应用实例。其中不乏有许多代码案例。** *** ## Java IO模型系列教程-第一章-操作系统的IO模型 ### 五种IO模型:操作系统五种IO模型大全 ### 一、IO模型简介 #### 1.1 操作系统的内存简介 ##### 1.1.1 操作系统的应用与内核 现代计算机是由硬件和操作系统组成,我们的应用程序要操作硬件(如往磁盘上写数据),就需要先与内核交互,然后再由内核与硬件交互; 操作系统可以划分为:内核与应用两部分; 内核提供进程管理、内存管理、网络等底层功能,封装了与硬件交互的接口,通过系统调用提供给上层应用使用。
##### 1.1.2 内核空间与用户空间 现在操作系统都是采用虚拟地址空间,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间(内核空间),也有访问底层硬件设备的所有权限。 为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。内核空间是操作系统内核访问的区域,独立于普通的应用程序,是受保护的内存空间。用户空间是普通应用程序可访问的内存区域。 针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
用户态的程序不能随意操作内核地址空间,即使用户的程序崩溃了,内核也不受影响。这样对操作系统具有一定的安全保护作用。 ##### 1.1.3 CPU指令等级 其实早期操作系统是不区分内核空间和用户空间的,但是应用程序能访问任意内存空间,如果程序不稳定常常把系统搞崩溃,比如清除操作系统的内存数据。后来觉得让应用程序随便访问内存太危险了,就按照CPU 指令的重要程度对指令进行了分级;
CPU指令分为四个级别:Ring0~Ring3,linux 只使用了 Ring0 和 Ring3 两个运行级别,进程运行Ring3级别的指令时运行在用户态,指令只访问用户空间,而运行在 Ring0级别时被称为运行在内核态,可以访问任意内存空间。 ##### 1.1.4 进程的内核态和用户态 当进程运行在内核空间时,它就处于内核态;当进程运行在用户空间时,它就处于用户态。 - 那什么时候运行再内核空间什么时候运行再用户空间呢? - 当我们需要进行IO操作时,如读写硬盘文件、读写网卡数据等,进程需要切换到内核态,否则无法进行这样的操作,无论是从内核态切换到用户态,还是从用户态切换到内核态,都需要进行一次上下文的切换。一般情况下,应用不能直接操作内核空间的数据,需要把内核态的数据拷贝到用户空间才能操作。 比如我们 Java 中需要新建一个线程,调用 start() 方法时,基于Hotspot Linux 的JVM 源码实现,最终是调pthread_create系统方法来创建的线程,这里会从用户态切换到内核态完成系统资源的分配,线程的创建。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态) > Tips:除了系统调用可以实现用户态到内核态的切换,软中断和硬中断也会切换用户态和内核态。 - 在内核态下:进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。 - 在用户态下:进程运行在用户地址空间中,被执行的代码要受到 CPU 的很多检查,比如:进程只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址。 *** #### 1.2 IO的分类 **通常用户进程中的一个完整IO分为两阶段:用户进程空间<- ->内核空间、内核空间<- ->设备空间(磁盘、网络等)。** ##### 1.2.1 网络IO和磁盘IO IO从读取数据的来源分为内存IO、 网络IO和磁盘IO三种,通常我们说的IO指的是后两者(因为内存IO的读写速度比网络IO和磁盘IO快的多)。 I/O按照设备来分的话,分为两种:一种是网络I/O,也就是通过网络进行数据的拉取和输出。一种是磁盘I/O,主要是对磁盘进行读写工作。 - **网络IO**:等待网络数据到达网卡→把网卡中的数据读取到内核缓冲区,然后从内核缓冲区复制数据到进程空间。 - **磁盘IO**:把数据从磁盘中读取到内核缓冲区,然后从内核缓冲区复制数据到进程空间。 > Tips:由于CPU和内存的速度远远高于外部设备(网卡,磁盘等)的速度,所以在IO编程中,存在速度严重不匹配的问题。 ##### 1.2.1 同步IO和异步IO - **同步**:A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。B在没有处理完A的请求时不能处理其他请求; - **异步**:A调用B,B的处理是异步的,B在接到请求后先告诉A我已经接到请求了,然后异步去处理,处理完之后通过回调等方式再通知A。B在处理A请求的同时,也可以接着处理其他人发送过来的请求; 同步和异步最大的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。 ##### 1.2.2 阻塞IO和非阻塞IO - **阻塞**:A调用B,A一直等着B的返回,别的事情什么也不干。 - **非阻塞**:A调用B,A不用一直等着B的返回,先去忙别的事情了。 阻塞和非阻塞最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待。阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。 > Tips:同步和异步强调的是被调用方(B–操作系统),阻塞和非阻塞强调的是调用方(A–应用程序);
- 优点:开发相对简单,在阻塞期间,用户线程被挂起,挂起期间不会占用CPU资源; - 缺点: - 1)连接利用率不高,内核如果没有响应数据,则该连接一直处于阻塞状态,占用连接资源 - 2)一个线程维护一个IO资源,当用大量并发请求时,需要创建等价的线程来处理请求,不适合用于高并发场景; *** #### 1.3 操作系统的五种IO模型 ##### 1.3.1 阻塞IO模型 阻塞IO就是当应用A发起读取数据申请时,在内核数据没有准备好之前,应用A会一直处于等待数据状态,直到内核把数据准备好了交给应用A才结束。 > Tips:我们之前所学过的所有的套接字,默认都是阻塞方式。 ##### 1.3.2 非阻塞IO模型 非阻塞IO就是当应用A发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用A(返回错误码等),不会让A在这里等待。一旦内核中的数据准备好了,并且又再次收到了A的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
- 优点:每次发起IO调用去内核获取数据时,在内核等待数据的过程中可以立即返回,用户线程不会被阻塞,实时性较好; - 缺点: - 1)当用户线程A没有获取到数据时,不断轮询内核,查看是否有新的数据,占用大量CPU时间,效率不高; - 2)和阻塞IO一样,一个线程维护一个IO资源,当用大量并发请求时,需要创建等价的线程来处理请求,不适合用于高并发场景; ##### 1.3.3 复用IO模型 如果在并发的环境下,可能会N个人向应用B发送消息,这种情况下我们的应用就必须创建多个线程去接收N个人发送过来的请求,每个请求都是一个独立的线程来处理;在并发量呈线性增长时,我们需要创建的线程数也随之而然的激增;
这种情况下应用B就需要创建N个线程去读取数据,同时又因为应用线程是不知道什么时候会有数据读取,为了保证消息能及时读取到,那么这些线程自己必须不断的向内核发送请求来读取数据(非阻塞式); 这么多的线程不断请求数据,先不说服务器能不能扛得住这么多线程,就算扛得住那么很明显这种方式是不是太浪费资源了,线程是我们操作系统的宝贵资源,大量的线程用来去读取数据了,那么就意味着能做其它事情的线程就会少。 **后来,有人就提出了一个思路,能不能提供一种方式,可以由一个线程监控多个网络请求(linux系统把所有网络请求以一个fd来标识,我们后面将称为fd即文件描述符),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。**
IO复用模型的思路就是系统提供了一种函数(select/poll/epoll)可以同时监控多个fd的操作,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,如果select监听的fd都没有可读数据,select调用进程会被阻塞;而只要有任何一个fd准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应的线程此时再发起请求去读取内核中准备好的数据; > Tips:在IO复用模型下,允许单线程内处理多个IO请求; Linux中IO复用的实现方式主要有select,poll和epoll 1)select(时间复杂度O(n)) - `select`:线性轮询扫描所有的fd,不管他们是否活跃,监听的IO最大连接数不能多于FD_ SIZE(32位操作系统1024,64位操作系统2048)。 > Tips:select方式仅仅知道有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),用户线程只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。 2)poll(时间复杂度O(n)) - `poll`:原理和select相似,poll底层需要分配一个pollfd结构数组,维护在内核中,它没有数量限制,但IO数量大,扫描线性性能下降。 3)epoll (时间复杂度O(1)) - `epoll` :用于代替poll和select,没有大小限制。epoll采用事件驱动代替了轮询,epoll会把哪个流发生了怎样的I/O事件通知用户线程,所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时用户线程对这些流的操作都是有意义的。(复杂度降低到了O(1)),另外epoll模型采用mmap内存映射实现内核与用户空间的消息传递,减少用户态和内核态数据传输的开销,epoll模型在Linux2.6后内核支持。 select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符准备就绪,能够通知程序进行相应的读写操作。**但select,poll,epoll**本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写(一个个的处理),也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 > Tips:epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现 4)IO复用模型小结 - 关于IO复用模型,下面这个例子可以很好的说明IO复用模型的原理: 某教室有10名学生和1名老师,这些学生上课会不停的提问,所以一个老师处理不了这么多的问题。那么学校为每个学生都配一名老师,也就是这个教室目前有10名老师。此后,只要有新的转校生,那么就会为这个学生专门分配一个老师,因为转校生也喜欢提问题。如果把以上例子中的学生比作客户端,那么老师就是负责进行数据交换的服务端。则该例子可以比作是多进程的方式。 后来有一天,来了一位具有超能力的老师,这位老师回答问题非常迅速,并且可以应对所有的问题。而这位老师采用的方式是学生提问前必须先举手,确认举手学生后在回答问题。则现在的情况就是IO复用。 - **IO复用模型的优点**:系统不必创建和维护大量的线程,只使用一个或几个线程来监听select选择器的操作,而一个选择器可同时处理成千上万个连接,大大减少了系统的开销; - **IO复用模型的缺点**:select本质上还是同步阻塞模式; 总结: 复用IO的基本思路就是通过select或poll、epoll来监控多fd ,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。复用IO模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。 ##### 1.3.4 信号驱动IO模型 当进程发起一个IO操作,系统调用sigaction执行一个信号处理函数,该函数向内核注册一个信号处理函数(回调函数),然后进程返回,并且不阻塞当前进程;当内核数据准备好时,内核使用信号(SIGIO)通知应用线程调用recvfrom来读取数据(运行回调函数)。 信号驱动IO它也可以看成是一种**异步非阻塞IO**
我们说信号驱动IO模型是一种异步非阻塞IO模型,指的是用户线程去内核空间请求数据时,直接注册一个信号处理函数,然后用户线程返回(异步),而内核空间接收到请求后,开始处理(此时并不会阻塞,内核空间可以同时接收多个请求,注册多个信号处理函数); 但是,等到内核空间读取到数据之后,应用线程需要将数据从内核空间拷贝到用户空间,**此时是用户线程是阻塞的**;也就是说:应用程序将数据从内核态拷贝到用户态的过程是阻塞等待的,这是和异步IO的本质区别; ##### 1.3.5 异步IO模型 在前面几种IO模型中,应用线程要获取数据总是先发送请求到内核,然后进行如下处理: - 1)阻塞IO:应用线程等待内核响应数据,期间什么都不能做 - 2)非阻塞IO:应用线程立即响应,可以去处理其他事情,但需要不断轮询内核去获取数据 - 3)复用IO:采用IO复用机制,请求都先交给select函数,由应用线程调用select函数来轮询所有的请求,当有请求需要获取数据时,应用线程再去内核获取数据; - 4)信号驱动IO:系统注册一个信号处理函数(回调函数),然后应用线程返回(不阻塞);当内核中准备好数据后,应用线程需要把内核中的数据拷贝到用户空间,**此时用户线程是阻塞的**; 在以上4种IO模型中,每次要去读取数据时都是事先发送请求询问内核是否有可读数据,然后再发起真正的读取数据请求; 在异步IO模型中,应用只需要向内核发送一个请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间(而信号驱动是告诉应用程序何时可以开始拷贝数据),异步IO模型真正的做到了完完全全的非阻塞; > Tips:异步IO模型和前面模型最大的区别是:前4个都是阻塞的,需要自己把用户准备好的数据,去内核拷贝到用户空间。而全异步不同,用户线程完全不需要关心实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据,它是最理想的模型。 *** ## Java IO模型系列教程-第二章-Java的IO模型 ### 二、Java的IO模型 #### 2.1 Java的IO模型支持 Java共支持3种网络编程模型/IO模式,分别是: - **BIO(同步阻塞IO)** - **NIO(同步非阻塞IO)** - **AIO(异步非阻塞IO)** 我们可以根据不同的业务场景来决定选择不同I/O处理模型; *** #### 2.2 BIO(blocking I/O)模型 - `BIO(blocking I/O)`:**也叫同步阻塞IO**,在JDK1.4之前,我们建立网络连接的时候采用的是 BIO 模式。 阻塞 IO(BIO)是最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象,直至有可供读取的数据或者数据能够写入。 在BIO模式中,服务器会为每个客户端的请求创建一个对应的线程来处理,由该线程单独负责处理一个客户请求,如果这个连接不做任何事情会造成不必要的线程开销,为此我们可以通过线程池机制改善性能;
虽然使用线程池可以一定程度上改善BIO的性能,但依旧无法冲本质上解决BIO同步阻塞的的问题;如果连接大多是长连接,则会导致连接无法释放,新的请求将无法得到处理,另外,BIO这种一个请求对应一个线程的方式在应对高并发的情况下,服务器必须也要创建同等量的线程来处理客户端的请求,这样对系统的消耗是非常大的; - BIO优点: - 实现简单,IO模式适用于连接数目比较小且固定的架构,是JDK1.4以前的唯一选择; - BIO缺点: - 1)每个请求都需要创建独立的线程来处理,当连接数较大时,需要创建大量的线程来处理 - 2)一个线程只能处理一个请求,连接建立后,如果当前线程暂时没有数据可读,那么该线程就一直阻塞在读操作,并不能处理其他事情,造成性能浪费; *** #### 2.3 NIO(non-blocking I/O)模型 NIO是从JDK1.4版本开始引入的一个新的IO API,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。 BIO是同步阻塞IO,同步:即在同一时间点只能同时处理一个客户端连接,阻塞:即当调用方法获取数据时,如果没有可用的数据将会阻塞当前线程; NIO则是同步非阻塞IO,NIO中有三大组件,分别是:Channel(通道),Buffer(缓冲区),Selector(选择器);当有客户端连接时,服务器可以获取与该客户端的连接Channel(通道),所有的通道都会被注册到Selector(选择器)上,当Channel上有读写数据时将会被Selector侦测到,服务器只需要派发一个线程来处理Selector上的事件即可;当前Channel如果没有读写数据时,Selector并不会一直阻塞的等待Channel的数据返回,而是轮询式的侦测所有的Channel,这是NIO非阻塞的核心; 另外,客户端的连接都变成了Channel,这些Channel都注册到了Selector中,服务器再也不需要为每一个连接来创建一个独立的线程为之服务了;这也是NIO能够应对高并发的核心之一; NIO(non-blocking IO):也叫同步非阻塞IO,由于BIO的各种弊端,JDK1.4从开始提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。 在NIO模型中,每个请求都会有一个与服务器做数据交互的通道(Channel),所有的通道都被注册到一个选择器中(selector),当需要与服务器做数据交互时,数据通过管道写入到一个缓冲区(Buffer)中,服务器通过往缓冲区中读取数据,如果当前通道没有数据时,就什么都不会获取,而不是保持线程阻塞,直至数据变的可以读取之前,该线程可以继续做其他的事情。
> 在Java NIO有三大核心部分:Buffer(缓冲区)、Channel(通道)、Selector(选择器) ; ##### 2.3.1 Buffer(缓冲区) **Buffer本质上**就是一块存储数据的内存,我们可以在这一块内存中进行读写操作,这与我们之前的数组非常类似。与数组不同的是,这块内存被封装成Buffer对象,并根据不同的数据类型提供有不同的Buffer子类。Java对Buffer提供了更多的API,使得Buffer功能更加强大; - Java中常见的Buffer如下: - CharBuffer - DoubleBuffer - IntBuffer - LongBuffer - ByteBuffer - ShortBuffer - FloatBuffer > Tips:以上Buffer都继承与Buffer抽象类,StringBuffer和以上的Buffer并不是同一类的,没有继承与NIO包下的Buffer接口; ##### 2.3.2 Channel(通道) Java NIO的通道类似流,都是用于传输数据的。但通过又与流有些不同;流的数据走向是单向的,分为输入流(只能读取数据),输出流(只能写出数据),但NIO中的通道不一样,**通道既可以写数据到Buffer,又可以从Buffer中读取数据;** **另外流的操作对象是数组,而通道的操作对象是Buffer;** - Java中常见的Channel如下: - FileChannel:用于文件 I/O 编程 - SocketChannel、ServerSocketChannel:用于 TCP I/O 编程 - DatagramChannel:用于 UDP I/O 编程 ##### 2.3.3 Selector(选择器) Selector选择器,也叫多路复用器;**NIO中实现非阻塞 I/O 的核心对象就是 Selector**。当一个连接创建后,不需要创建一个线程来处理这个来连接,这个连接(管道)会被注册到选择器上,选择器可以检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。 *** #### 2.3 AIO模型 - `AIO` :AIO也叫**异步非阻塞**,JDK1.7之后的新特性,AIO引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 与NIO模型不同,读写操作为例,只需直接调用read和write的API即可,这方法都是异步的对于读操作:当有流可读是,系统会将可读的流传入到read方法的缓冲区,并通知应用程序读写都是异步的,完成之后会主动调用回调函数 AIO需要操作系统的支持,在Linux内核2.6版本之后增加了对真正异步IO的实现。Java从JDK1.7之后支持AIO,JDK1.7新增一些与文件/网络IO相关的一些API,称之为NIO2.0或者称之为AIO(Asynchronous IO)。AIO最大的特征提供了异步功能,对于socket网络通信和文件IO都是起作用的。 目前 AIO 还没有广泛应用,Netty也是基于NIO,而不是AIO。 *** - BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解; - NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持; - AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如文件服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 *** ## Java IO模型系列教程-第三章-基于BIO编程 ### 三、BIO编程 #### 3.1 BIO BIO是同步阻塞IO,Java中的BIO编程分为传统的文件IO编程与网络IO编程。文件IO编程相关类包在`java.io`包中,网络IO编程相关类在java.net包下;文件IO关联的是某个文件,从文件中读取数据;网络IO则关联的是某个网络连接,从网络上读取数据;但两者都是属于BIO模型(同步阻塞); *** #### 3.2 文件IO编程 我们之前学过的IO流中四个顶层父类:InputStream、OutputStream、Reader、Writer都是属于BIO,即同步阻塞式IO - **同步**: 当调用read或write方法时,FileInputStream/FileOutputStream的处理是同步的,在处理完read/write方法之前并不能处理其他业务 - **阻塞**: 当没有数据可读时/可写,当前线程发生阻塞,不能处理其他事情 > Tips:FileXxxStream关联的是某个文件,如果文件中没有数据可读(读取到文件末尾)则会返回-1,因此看不到阻塞效果; - 示例代码: ```java package com.bio.file; import java.io.FileInputStream; import java.io.FileOutputStream; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_传统文件IO { public static void main(String[] args) throws Exception { FileInputStream fis = new FileInputStream(""); FileOutputStream fos = new FileOutputStream(""); /* 同步: 当调用read或write方法时,FileInputStream/FileOutputStream的处理是同步的,在处理完read/write方法之前并不能处理其他业务 阻塞: 当没有数据可读时/可写,当前线程发生阻塞,不能处理其他事情 */ } } ``` 我们将文件流换成标准输入流测试 - 测试代码: ```java package com.bio.file; import java.io.InputStream; import java.util.Scanner; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_标准输入流 { public static void main(String[] args) throws Exception { // 获取到标准输入流(指向的是控制台) InputStream in = System.in; // 创建一个扫描器,并且从从控制台扫描数据 Scanner scanner = new Scanner(in); // 如果控制台没有数据输入,则阻塞 String str = scanner.nextLine(); System.out.println("str: " + str); } } ``` 当控制台没有数据输入时,将会一直阻塞下去,用户线程(main线程)什么事情都不能干;并且nextLine()方法的处理是同步的,这就意味着,在执行nextLine()方法的同时不能执行其他代码; *** #### 3.3 网络IO编程 Java中的网络IO编程分为**UDP**编程和**TCP**编程; TCP和UDP的特点: - UDP: - 1)面向无连接,资源消耗小,传输速度高 - 3)不保证数据的完整性,可靠性低,安全性低 - TCP: - 1)面向连接,每次连接都需要三次握手,每次断开连接都需要四次挥手,资源消耗大,传输速度低 - 2)保证数据的完整性,可靠性高,安全性高 ##### 3.3.1 UDP编程 在UDP编程中,分为数据的发送端和接收端,Java在UDP编程中,提供有两个类,分别为`DatagramPacket`和`DatagramSocket`,其中DatagramPacket用于封装一个UDP报文,DatagramSocket则是用于发送UDP报文; 1)**UDP相关API** - **DatagramPacket构造方法:** - `public DatagramPacket(byte[] buf, int length, InetAddress address, int port)`:创建一个数据包对象 - `buf`:要发送的内容 - `length`:要发送的内容长度,单位字节 - `address`:接收端的ip地址 - `port`:接收端口号 - `public DatagramPacket(byte buf[], int length)`:创建一个数据包对象 - **其他方法:** - `public synchronized int getLength()`:获取此UDP数据包载荷的数据长度(单位字节) - `public synchronized int getPort()`:获取此UDP数据包的目的端口号 - `public synchronized byte[] getData()` :获取此UDP数据包的载荷部分(数据) - **DatagramSocket构造方法:** - `public DatagramSocket(int port)`:通过端口构建一个发送端/接收端 - **其他方法:** - `public void send(DatagramPacket p)`:发送一个UDP数据包 - `public synchronized void receive(DatagramPacket p)`:接收一个UDP数据包 - `public void close()`:释放该Socket占用的资源 2)**UDP案例测试** - 发送端测试代码: ```java package com.bio.net.udp; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * @author lscl * @version 1.0 * @intro: 发送端 */ public class Demo01_Sender { public static void main(String[] args) throws Exception { // 套接字(用于发送UDP报文) DatagramSocket socket = new DatagramSocket(); String str = "hello"; // 准备一个UDP数据包 DatagramPacket packet = new DatagramPacket( str.getBytes(), str.getBytes().length, InetAddress.getLocalHost(), 9999 ); // 发送数据包 socket.send(packet); socket.close(); } } ``` - 接收端测试代码: ```java package com.bio.net.udp; import java.net.DatagramPacket; import java.net.DatagramSocket; /** * @author lscl * @version 1.0 * @intro: 接收端 */ public class Demo02_Receive { public static void main(String[] args) throws Exception { // 创建一个套接字,接收UDP报文 DatagramSocket socket = new DatagramSocket(9999); byte[] bytes = new byte[1024]; // 创建一个UDP报文,用于存储UDP报文的数据 DatagramPacket packet = new DatagramPacket(bytes, bytes.length); System.out.println("正在接收数据..."); /* 同步: socket.receive方法的处理是同步的,最多只能同时处理一个UDP请求,无法同时处理多个 阻塞: socket调用receive方法时,如果还未接收到UDP报文,那么将阻塞 */ socket.receive(packet); // 接收UDP报文 System.out.println("成功接收数据: "); // 获取接收数据的大小 int len = packet.getLength(); System.out.println("已经接收到:" + len + "个字节"); // 转换为字符串打印 System.out.println(new String(bytes, 0, len)); socket.close(); } } ``` 3)**UDP实现网络图片传递** - 发送端: ```java package com.bio.net.udp02; import java.io.FileInputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_Sender { public static void main(String[] args) throws Exception { // 关联一个本地文件 FileInputStream fis = new FileInputStream("100.jpg"); // 创建一个套接字 DatagramSocket socket = new DatagramSocket(); // 准备一个字节数组存储读取到的文件数据 byte[] data = new byte[8192]; // 创建一个UDP数据包 DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 9999); while (true) { // 从文件中读取数据 int len = fis.read(data); // 将读取到的数据设置到UDP报文中 packet.setData(data); // 设置文件输入流读取到的实际有效字节个数 packet.setLength(len == -1 ? 0 : len); // 发送报文 socket.send(packet); if (len == -1) { break; } } socket.close(); } } ``` - 接收端: ```java package com.bio.net.udp02; import java.io.FileOutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_Receive { public static void main(String[] args) throws Exception { // 创建一个套接字(用于接收UDP报文) DatagramSocket socket = new DatagramSocket(9999); byte[] data = new byte[8192]; // 创建一个UDP数据包 DatagramPacket packet = new DatagramPacket(data, data.length); // 创建一个文件输出流 FileOutputStream fos = new FileOutputStream("000.jpg"); // 死循环监听报文 while (true) { /* 同步: socket.receive方法的处理是同步的,最多只能同时处理一个UDP请求,无法同时处理多个 阻塞: socket调用receive方法时,如果还未接收到UDP报文,那么将阻塞 */ socket.receive(packet); if (packet.getLength() == 0) { // 代表读取到了末尾 break; } // 写出到指定文件 fos.write(packet.getData(), 0, packet.getLength()); } fos.close(); socket.close(); } } ``` 我们知道UDP是面向无连接的通讯协议,并且传递数据是不安全的(UDP由于报文的设计在网络数据传递时,如果出现丢包现象那么将无法补发数据),因此在UDP传递完了文件后,文件大小会比原文件要小,这是由于UDP在传输过程中丢包所导致的:
这是拷贝之后的文件(出现一些瑕疵):
##### 3.3.2 TCP编程 在TCP通信中,分为数据的发送端(客户端)和接收端(服务器),当建立连接成功后(三次握手),才可以进行数据的发送; 在Java中,提供了两个类用于实现TCP通信程序: - 1)客户端:`java.net.Socket` 类表示;用于与服务器端建立连接,向服务器端发送数据报文等; - 2)服务端:`java.net.ServerSocket` 类表示;用于与客户端的交互; 1)**TCP相关API** - Socket构造方法: `public Socket(String host, int port)` :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为本机地址。 - 成员方法: - `public InputStream getInputStream()` : 返回此套接字的输入流。关闭生成的InputStream也将关闭相关的Socket。 - `public OutputStream getOutputStream()` : 返回此套接字的输出流。关闭生成的OutputStream也将关闭相关的Socket。 - `public void close()` :关闭此套接字。关闭此socket也将关闭相关的InputStream和OutputStream 。 - `public void shutdownOutput()` : 禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。 *** - ServerSocket构造方法: - `public ServerSocket(int port)` :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。 - 成员方法: - `public Socket accept()` :监听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。 2)**TCP案例测试** - 服务器: ```java package com.bio.net.tcp; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_Server { public static void main(String[] args) throws Exception { // 创建一个服务器 ServerSocket serverSocket = new ServerSocket(6666); System.out.println("等待客户端的连接....."); /* 同步: accept方法只能同时处理一个客户端的连接,不能同时处理多个客户端的连接,在没有处理完客户端连接时当前线程什么都干不了 阻塞: 如果没有客户端来连接,accept方法会一直阻塞当前线程 */ Socket socket = serverSocket.accept(); // 接收到一个客户端 System.out.println("客户端连接成功!"); // InputStream和OutputStream都是BIO // 获取与客户端的输入流(用于读取客户端发送过来的数据) InputStream in = socket.getInputStream(); // 获取与客户端的输出流(用于往客户端写出数据) OutputStream out = socket.getOutputStream(); byte[] data = new byte[1024]; // 读取客户端发送过来的数据 int len = in.read(data); System.out.println("客户端的数据: " + new String(data, 0, len)); // 往客户端写出数据 out.write("请问您需要点什么呢?".getBytes()); // 读取客户端发送过来的数据 len = in.read(data); System.out.println("客户端的数据: " + new String(data, 0, len)); // 往客户端写出数据 out.write("好的".getBytes()); // 关闭流 socket.close(); serverSocket.close(); } } ``` - 客户端: ```java package com.bio.net.tcp; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.channels.SocketChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_Client { public static void main(String[] args) throws Exception{ // 创建一个客户端 Socket socket = new Socket("127.0.0.1", 6666); // 获取与服务器的输入流(用于读取服务器的数据) InputStream in = socket.getInputStream(); // 获取与服务器的输出流(用于往服务器写出数据) OutputStream out = socket.getOutputStream(); // 往客户端写出数据 out.write("你好,请问在吗?".getBytes()); byte[] data = new byte[1024]; // 读取服务器发送过来的数据 int len = in.read(data); System.out.println("服务器的数据: " + new String(data, 0, len)); // 往客户端写出数据 out.write("来一份南昌拌粉~".getBytes()); // 读取服务器发送过来的数据 len = in.read(data); System.out.println("服务器的数据: " + new String(data, 0, len)); socket.close(); } } ``` 3)**TCP实现网络图片传递** - 服务器: ```java package com.bio.net.tcp02; import java.io.FileOutputStream; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_Server { public static void main(String[] args) throws Exception{ // 创建一个服务器 ServerSocket serverSocket = new ServerSocket(6666); // 接收一个客户端 Socket socket = serverSocket.accept(); // 获取与服务器的输入流 InputStream in = socket.getInputStream(); // 将读取到的数据写出到磁盘中 FileOutputStream fos = new FileOutputStream("000.jpg"); byte[] data=new byte[8192]; int len; while ((len=in.read(data))!=-1){ fos.write(data,0,len); } socket.close(); fos.close(); serverSocket.close(); } } ``` - 客户端: ```java package com.bio.net.tcp02; import java.io.FileInputStream; import java.io.OutputStream; import java.net.Socket; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_Client { public static void main(String[] args) throws Exception { // 创建一个套接字 Socket socket = new Socket("127.0.0.1", 6666); // 获取与服务器的输出流 OutputStream out = socket.getOutputStream(); FileInputStream fis = new FileInputStream("100.jpg"); byte[] data = new byte[8192]; int len; while ((len = fis.read(data)) != -1) { out.write(data,0,len); } fis.close(); socket.close(); } } ``` TCP是面向连接的通讯协议,并且传递数据是安全的(TCP的报文中有序列号,确认号等标志位,如果在网络数据传递过程中出现丢包可以侦测到丢失的数据,然后数据重发),因此在使用TCP传递数据时,不会出现丢包等情况;但TCP的传输速度将不如UDP协议; ##### 3.3.3 伪异步IO实现网络IO编程 我们之前使用的网络编程都是属于同步阻塞式的;其中同步指的是同时最多只能处理一个请求,阻塞指的是当调用某方法时,方法还未获取到数据时当前线程将会处于阻塞状态;我们可以利用多线程来实现一个伪异步网络编程,即可以"同时"处理多个请求; 多线程实现TCP网络聊天案例: - 客户端: ```java package com.bio.net.tcp03; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_Client { public static void main(String[] args) throws Exception { // 获取到一个新的客户端 Socket socket = new Socket("127.0.0.1", 8888); // 读取客户端数据 InputStream is = socket.getInputStream(); // 缓冲字符输入流(读取客户端数据更加方便) BufferedReader br = new BufferedReader(new InputStreamReader(is)); // 读线程 new Thread() { @Override public void run() { try { String info; while (true) { // 死循环读取客户端的信息 info = br.readLine(); if (info.equals("end")) { socket.close(); // 退出应用程序 System.exit(0); System.out.println("bye bye...."); break; } else { System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info); } } } catch (IOException e) { e.printStackTrace(); } } }.start(); // 给客户端写回数据 OutputStream os = socket.getOutputStream(); // 缓冲字符输出流(往客户端写数据更加方便) BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); // 写线程 new Thread() { @Override public void run() { try { Scanner scanner = new Scanner(System.in); String info; while (true) { // 获取键盘输入的信息 info = scanner.nextLine(); // 往客户端写数据 bw.write(info); bw.newLine(); bw.flush(); } } catch (IOException e) { e.printStackTrace(); } } }.start(); } } ``` - 服务器: ```java package com.bio.net.tcp03; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; /** * @author lscl * @version 1.0 * @intro: 多线程实现服务器 */ public class Demo02_Server { public static void main(String[] args) throws Exception { // 创建一台服务器 ServerSocket serverSocket = new ServerSocket(8888); while (true) { // 获取到一个新的客户端 Socket socket = serverSocket.accept(); // 开启任务线程执行任务(有读写任务) ServerTaskThread task = new ServerTaskThread(socket); task.start(); } } } ``` - 服务器任务线程: ```java package com.bio.net.tcp03; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * @author lscl * @version 1.0 * @intro: */ public class ServerTaskThread extends Thread{ private Socket socket; public ServerTaskThread(Socket socket){ this.socket=socket; } @Override public void run() { try { System.out.println(socket.getInetAddress().getHostAddress() + "已与您连接成功"); // 开启读线程 ReaderThread readerThread = new ReaderThread(socket.getInputStream()); readerThread.start(); // 开启写线程 WriterThread writerThread = new WriterThread(socket.getOutputStream()); writerThread.start(); } catch (IOException exception) { exception.printStackTrace(); } } class ReaderThread extends Thread{ private InputStream in; public ReaderThread(InputStream in){ this.in=in; } // 读线程 @Override public void run() { while (true){ // 缓冲字符输入流(读取客户端数据更加方便) BufferedReader br = new BufferedReader(new InputStreamReader(in)); try { String info; while (true) { // 死循环读取客户端的信息 info = br.readLine(); if (info.equals("end")) { // 关闭与客户端的连接 socket.close(); System.out.println(socket.getInetAddress().getHostAddress() + "已与您断开连接: "); break; } else { System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info); } } } catch (IOException e) { e.printStackTrace(); } } } } // 写线程 class WriterThread extends Thread{ private OutputStream out; public WriterThread(OutputStream out){ this.out=out; } @Override public void run() { while (true){ // 缓冲字符输出流(往客户端写数据更加方便) BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out)); try { Scanner scanner = new Scanner(System.in); String info; while (true) { // 获取键盘输入的信息 info = scanner.nextLine(); // 往客户端写数据 bw.write(info); bw.newLine(); bw.flush(); } } catch (IOException e) { e.printStackTrace(); } } } } } ``` 虽然我们服务器处理改为了多线程,但是服务器端的控制台只有一个,当多个线程争抢控制台输入的数据时,最终只会有一个线程抢到了控制台输入的值,也只有一个客户端能收到这个值; 另外还有一点是服务器每接收到一个客户端请求时都会开启一个一个线程来处理这个请求(这个线程里面又开启两个线程分别处理读/写),这种请求与线程一对一的处理显然是比较低效的;再者当客户端断开连接都好不容易创建出来的线程又被销毁了,造成性能浪费; *** ## Java IO模型系列教程-第四章-NIO核心组件之Buffer ### 四、NIO核心组件之Buffer NIO是从JDK1.4版本开始引入的一个新的IO API,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。 - BIO是同步阻塞IO,同步:即在同一时间点只能同时处理一个客户端连接,阻塞:即当调用方法获取数据时,如果没有可用的数据将会阻塞当前线程; - NIO则是同步非阻塞IO,NIO中有三大组件,分别是:Channel(通道),Buffer(缓冲区),Selector(选择器);当有客户端连接时,服务器可以获取与该客户端的连接Channel(通道),所有的通道都会被注册到Selector(选择器)上,当Channel上有读写数据时将会被Selector侦测到,服务器只需要派发一个线程来处理Selector上的事件即可;当前Channel如果没有读写数据时,Selector并不会一直阻塞的等待Channel的数据返回,而是轮询式的侦测所有的Channel,这是NIO非阻塞的核心; 另外,客户端的连接都变成了Channel,这些Channel都注册到了Selector中,服务器再也不需要为每一个连接来创建一个独立的线程为之服务了;这也是NIO能够应对高并发的核心之一; *** #### 4.1 Buffer相关子类 Buffer本质上就是一块存储数据的内存,我们可以在这一块内存中进行读写操作。这与我们之前的数组非常类似,与数组不同的是,这块内存被封装成Buffer对象,并根据不同的数据类型提供有不同的Buffer子类。Java对Buffer提供了更多的API,使得Buffer功能更加强大; - Java中常见的Buffer如下: - CharBuffer - DoubleBuffer - IntBuffer - LongBuffer - ByteBuffer - ShortBuffer - FloatBuffer > Tips:以上Buffer都继承与Buffer抽象类,StringBuffer和以上的Buffer并不是同一类的,没有继承与NIO包下的Buffer接口; *** #### 4.2 Buffer中的属性 Buffer是一块内存区域,这个块内存是有固定大小的,我们可以对这块内存区域进行读写操作;每一个Buffe都具备如下的属性; - **capacity(容量)**:在创建Buffer时需要指定Buffer的大小(容量),一旦创建后此Buffer容量不可修改; - **position(位置)**:每次对Buffer进行读或写操作时,position都会自动递增,position表示下一个要读取或写入的数据索引。position不可大于limit; - **limit(限制)**:表示Buffer可操作的数据区域,limit之后的数据不能读写;limit与capacity的关系为:limit<=capacity;默认情况下: - Buffer在写入模式中,limit=capacity - Buffer在读取模式中,limit=position capacity/position/limit示意图:
当往Buffer中添加数据时,position往后移动,capacity和limit保持不变:
mark和reset - **mark(标记)/reset(重置)**:标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个position;
mark/position/limit/capacity之间的关系:mark <= position <= limit <= capacity *** #### 4.3 Buffer相关方法 ##### 4.3.1 Buffer的创建 - Buffer的创建: - `public static ByteBuffer wrap(byte[] array)`:通过一个字节数组构建Buffer,通过该方法创建的Buffer,**不会造成position的位移,position依旧是0;** - `public static ByteBuffer allocateDirect(int capacity)`:分配一个capacity大小的Buffer,该Buffer是基于直接内存; - `public static ByteBuffer allocate(int capacity)`:分配一个capacity大小的Buffer,该Buffer是基于非直接内存; *** - Buffer的常用方法: - `int capacity()`:返回 Buffer 的 capacity 大小 - `int position()`:返回Buffer的当前位置position - `Buffer position(int n)`:将设置Buffer的当前position为 n,并返回修改后的 Buffer 对象 - `int limit()`:返回 Buffer的limit位置 - `Buffer limit(int n)`:设置新的limit位置,并返回一个具有新 limit 的缓冲区对象 - `Buffer mark()`:设置一个mark标记,为当前的position值; - `Buffer reset()`:将位置 position 转到以前设置的 mark 所在的位置 - `boolean hasRemaining()`:判断Buffer中是否还有元素;position是否超过了limit; - `int remaining()`:返回 position 和 limit 之间的元素个数 - `Buffer rewind()`:将位置设为为 0, 取消设置的 mark - `public byte[] array()`:返回该Buffer的字节数组;**注意,此方法不是去读取,而是将Buffer中的数据返回。该方法不会造成position的位移** - `Buffer flip()`:**该方法将limit设置为position,然后将positon复位到0;此操作就是为了读取buffer做准备;因此我们称该方法将buffer切换为读模式;** - `Buffer clear()`:**将position复位到0,将limit设置为capacity;所以说此方法将buffer切换为写模式;** - `public ByteBuffer compact()`: - 案例1-使用allocate创建一个Buffer,使用put添加数据: ```java @Test public void test2() { ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes()); System.out.println("-----------wrap------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 5 System.out.println(buffer.capacity()); // 5 byte[] data = new byte[buffer.limit()]; // 从Buffer中读取数据 buffer.get(data); System.out.println("-----------get------------"); System.out.println(buffer.position()); // 5 System.out.println(buffer.limit()); // 5 System.out.println(buffer.capacity()); // 5 System.out.println("-----------println------------"); System.out.println(new String(data, 0, data.length)); // hello } ``` > Tips:使用wrap创建一个有数据的Buffer时,position的位置依旧是0; ##### 4.3.2 Buffer的常用方法 - 案例1-测试其他方法: ```java package com.dfbz.buffer; import org.junit.Test; import java.nio.ByteBuffer; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_Buffer常用方法 { @Test public void test1() { // 分配一个8字节大小的Buffer缓冲区 ByteBuffer buffer = ByteBuffer.allocate(8); System.out.println("-----------allocate------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // 往Buffer中添加数据 System.out.println("----------put(abcd)-------------"); buffer.put("abcd".getBytes()); System.out.println(buffer.position()); // 4 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // 将position=4这个位置打一个标记 buffer.mark(); // 再往Buffer中添加数据 System.out.println("----------put(ef)----mark-------------"); buffer.put("ef".getBytes()); System.out.println(buffer.position()); // 6 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // position与limit之间的元素个数 System.out.println("【remaining】--->" + buffer.remaining()); // 2 // 重新回到之前标记的位置(4) System.out.println("-----------reset------------"); buffer.reset(); System.out.println(buffer.position()); // 4 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // position与limit之间的元素个数 System.out.println("【remaining】--->" + buffer.remaining()); // 4 // 将position设置为0,并取消之前设置过的mark System.out.println("----------rewind-------------"); buffer.rewind(); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 } } ``` - 案例2-测试`hasRemaining()`方法: ```java @Test public void test2() { // 分配一个5字节大小的Buffer缓冲区 ByteBuffer buffer = ByteBuffer.allocate(5); // 0(position) < 8(limit) = (true) System.out.println("【hasRemaining】--->" + buffer.hasRemaining()); // 往Buffer中添加数据 buffer.put("abc".getBytes()); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 5 System.out.println(buffer.capacity()); // 5 System.out.println("-----------------------"); // 3(position) < 5(limit) = (true) System.out.println("【hasRemaining】--->" + buffer.hasRemaining()); // 添加数据 buffer.put("def".getBytes()); // 出现异常 java.nio.BufferOverflowException } ``` ##### 4.3.2 Buffer的核心方法 1)**flip方法** Buffer是一块内存区域,我们既可以往里面添加数据(put),也可以获取Buffer里面的数据(get);但是不管是读或者 - `Buffer flip()`:**该方法将limit设置为position,然后将positon复位到0;此操作就是为了读取buffer做准备;因此我们称该方法将buffer切换为读模式;** 案例1-Buffer读取数据问题: ```java package com.dfbz.demo; import org.junit.Test; import java.nio.ByteBuffer; /** * @author lscl * @version 1.0 * @intro: */ public class Demo03_flip方法 { @Test public void test1() throws Exception { // 创建一个容量为8的Buffer ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put("abc".getBytes()); System.out.println("-----------put(abc)------------"); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // 准备一个字节数组 byte[] data=new byte[buffer.limit()]; // 将数据读取到字节数组 buffer.get(data); // 出现异常BufferUnderflowException System.out.println(new String(data,0,data.length)); } } ``` 运行代码,出现Buffer溢出现象:
为什么会这样呢?原因是:不管是往Buffer中写入数据,还是读取数据,都会造成position的位移; - 测试代码: ```java @Test public void test2() throws Exception { // 创建一个容量为8的Buffer ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put("abc".getBytes()); System.out.println("-----------put(abc)------------"); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 System.out.println("-----------get-------------"); // 准备一个字节数组 byte[] data = new byte[3]; // 将数据读取到字节数组 buffer.get(data); System.out.println(buffer.position()); // 6 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 System.out.println("------------------------"); System.out.println(new String(data,0,data.length)); // 空 } ``` 画图分析:
测试代码: ```java @Test public void test3() throws Exception { // 创建一个容量为8的Buffer ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put("abc".getBytes()); System.out.println("-----------put(abc)------------"); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // 将position的位置设置为0 buffer.position(0); System.out.println("-----------get-------------"); // 准备一个字节数组 byte[] data = new byte[3]; // 将数据读取到字节数组 buffer.get(data); System.out.println(buffer.position()); // 6 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 System.out.println("------------------------"); System.out.println(new String(data, 0, data.length)); // abc } ``` 执行程序:
将position设置为了0之后,发现可以正常读取,Buffer中帮我们提供了`flip`方法,该方法可以将Buffer的position复位到0,同时将Buffer中的limit设置为上一次的position位置,这样可以为读数据做准备,也可以限制读取的范围,防止读取到无效的数据; - Buffer flip():该方法将limit设置为position,然后将positon复位到0;此操作就是为了读取buffer做准备;因此我们称该方法将buffer切换为读模式; 示意图:
- flip方法测试: ```java @Test public void test4() throws Exception { // 创建一个容量为8的Buffer ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put("abc".getBytes()); System.out.println("-----------put(abc)------------"); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // 将limit设置为position(3),将position复位到0 buffer.flip(); System.out.println("-----------flip------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 3 System.out.println(buffer.capacity()); // 8 System.out.println("-----------get-------------"); // 创建一个大小为3的字节数组 byte[] data = new byte[buffer.limit()]; // 将数据读取到字节数组 buffer.get(data); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 3 System.out.println(buffer.capacity()); // 8 System.out.println("------------------------"); System.out.println(new String(data, 0, data.length)); // abc } ``` 2)**clear方法** - `Buffer clear()`:将position复位到0,将limit设置为capacity;所以说此方法将buffer切换为写模式; 测试代码: ```java package com.dfbz.demo; import org.junit.Test; import java.nio.ByteBuffer; /** * @author lscl * @version 1.0 * @intro: */ public class Demo04_clear方法 { @Test public void test1() throws Exception { // 创建一个容量为8的Buffer ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put("abc".getBytes()); System.out.println("-----------put(abc)------------"); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // limit=3,position=0 buffer.flip(); System.out.println("-----------flip------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 3 System.out.println(buffer.capacity()); // 8 byte[] data=new byte[buffer.limit()]; buffer.get(data); System.out.println(new String(data,0,data.length)); // abc System.out.println("-----------get------------"); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 3 System.out.println(buffer.capacity()); // 8 // limit=capacity(8),position=0 buffer.clear(); System.out.println("-----------clear------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 } } ``` 需要注意的是,clear方法只是将limit设置为capacity,并且将position设置为0,并不会将Buffer中的数据清空; - 测试代码: ```java @Test public void test2(){ // 创建Buffer ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put("abc".getBytes()); System.out.println("-----------put(abc)------------"); System.out.println(buffer.position()); // 3 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 // position=0,limit=8 buffer.clear(); System.out.println("-----------clear------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 8 System.out.println(buffer.capacity()); // 8 System.out.println("--clear方法只是将position设置为了0,并不代表Buffer中的数据被清空--"); System.out.println((char)buffer.get(0)); // a System.out.println((char)buffer.get(1)); // b System.out.println((char)buffer.get(2)); // c } ``` 示意图:
3)**compact方法** 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么可以使用compact()方法。 - `public ByteBuffer compact()`:compact方法做如下三件事情 - 1)将未读的数据拷贝到Buffer的起始位置(后续还可以读取到这些数据) - 2)然后将position设到最后一个未读元素正后面(保证对Buffer写数据时不会覆盖之前未读的数据) - 3)limit设置位capacity(可写的数据范围是整个Buffer的大小) 示意图:
测试代码: ```java package com.dfbz.buffer; import org.junit.Test; import java.nio.ByteBuffer; /** * @author lscl * @version 1.0 * @intro: */ public class Demo05_compact方法 { @Test public void test() { // 创建Buffer ByteBuffer buffer = ByteBuffer.allocate(8); buffer.put("abcde".getBytes()); System.out.println("-----------put(abcde)------------"); System.out.println(buffer); // [pos=5 lim=8 cap=8] // limit=position,position=0 buffer.flip(); System.out.println("--------get---------"); byte[] data = new byte[2]; buffer.get(data); // 进行数据读取 System.out.println(buffer); // [pos=2 lim=5 cap=8] /* limit=capacity=8 1) 将未读的数据拷贝到起始处 abcde(原始数据), cde(未读的数据) 2) position设置到最后一个未读的元素的正后面(position=3,保证下次要写入的时候不会覆盖之前未读的数据) 3) limit=capacity=8 */ buffer.compact(); System.out.println("--------compact---------"); System.out.println(buffer); // [pos=3 lim=8 cap=8] System.out.println(new String(buffer.array(), 0, buffer.array().length)); // cdede } } ``` 4)**array方法** - `public byte[] array()`:返回该Buffer的字节数组;注意,此方法不是去读取,而是将Buffer中的数据返回。该方法不会造成position的位移 案例1-使用get读取Buffer中的数据: ```java package com.dfbz.buffer; import org.junit.Test; import java.nio.ByteBuffer; /** * @author lscl * @version 1.0 * @intro: */ public class Demo06_array { @Test public void test1() { // 创建Buffer ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes()); System.out.println("----------wrap('hello')-------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 5 System.out.println(buffer.capacity()); // 5 byte[] data = new byte[buffer.limit()]; buffer.get(data); System.out.println(new String(data,0,data.length)); System.out.println("----------get-------------"); System.out.println(buffer.position()); // 5 System.out.println(buffer.limit()); // 5 System.out.println(buffer.capacity()); // 5 } } ``` 案例2-使用array获取Buffer中的数据: ```java @Test public void test2() { // 创建Buffer ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes()); System.out.println("----------wrap('hello')-------------"); System.out.println(buffer.position()); // 0 System.out.println(buffer.limit()); // 5 System.out.println(buffer.capacity()); // 5 // 获取Buffer中的数据,不会造成position的位移 byte[] data = buffer.array(); System.out.println(new String(data,0,data.length)); System.out.println("----------get-------------"); System.out.println(buffer.position()); // 5 System.out.println(buffer.limit()); // 5 System.out.println(buffer.capacity()); // 5 } ``` ##### 4.3.3 只读Buffer 默认情况下通过方法分配的缓冲区都是非只读缓冲区,即可读,又可写;Java提供API将一块常规Buffer转换为只读Buffer。只读Buffer与原Buffer共享数据,只不过它是只读的。**如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化** - `public ByteBuffer asReadOnlyBuffer()`:将Buffer转换为只读Buffer 测试代码: ```java package com.dfbz.demo; import org.junit.Test; import java.nio.ByteBuffer; /** * @author lscl * @version 1.0 * @intro: */ public class Demo04_只读Buffer { @Test public void test1() { // 分配一个Buffer(默认是非只读Buffer) ByteBuffer buffer = ByteBuffer.allocate(8); System.out.println("只读Buffer: " + buffer.isReadOnly()); // false System.out.println("------------"); // 往Buffer中添加数据 for (int i = 0; i < 8; i++) { buffer.put((byte) i); } // 将Buffer转换为只读Buffer ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); System.out.println("只读Buffer: " + readOnlyBuffer.isReadOnly()); // true System.out.println(readOnlyBuffer.position()); // 8 System.out.println(readOnlyBuffer.limit()); // 8 System.out.println(readOnlyBuffer.capacity()); // 8 System.out.println("---------------"); for (int i = 0; i < 8; i++) { System.out.println(readOnlyBuffer.get(i)); } System.out.println("-------------------"); // 改变原缓冲区的内容 for (int i = 0; i < 8; i++) { byte b = buffer.get(i); b *= 10; buffer.put(i, b); } // 只读Buffer中的内容也随之改变 for (int i = 0; i < 8; i++) { System.out.println(readOnlyBuffer.get(i)); } System.out.println("-------------------"); } } ``` ##### 4.3.4 直接内存与非直接内存 - 直接内存(系统内存):概指系统内存,而非堆内存; - 非直接内存(堆内存): 也可以称之为堆内存,运行JVM都会预先OLAL;SAWWWW分配一定内存,我们把JVM管理的这些内存称为堆内存(非操作系统直接内存),JVM会对这些内存空间的分配和回收进行管理。 直接内存操作流程:
非直接内存操作流程:
直接内存它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。很显然,直接内存会具有更高的效率。 Buffer中提供`allocateDirect`方法可以创建直接内存,但它比申请普通的堆内存需要耗费更高的性能。另外,由于创建的是直接内存,不在属于JVM内存,因此他不会占用JVM的应用程序内存;因此,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。 - `public static ByteBuffer allocateDirect(int capacity)`:创建一个指定容量大小的直接内存Buffer; - `public boolean isDirect()`:判断当前Buffer是否是基于直接内存; 测试代码: ```java package com.dfbz.demo; import org.junit.Test; import java.nio.ByteBuffer; /** * @author lscl * @version 1.0 * @intro: */ public class Demo04_直接内存与非直接内存 { @Test public void test1() throws Exception { // 直接内存 ByteBuffer buffer_01 = ByteBuffer.allocateDirect(8); // 非直接内存 ByteBuffer buffer_02 = ByteBuffer.allocate(8); System.out.println(buffer_01.isDirect()); // true System.out.println(buffer_02.isDirect()); // false } } ``` - 测试创建直接内存与非直接内存花费时间: ```java @Test public void test2() throws Exception { long startTime = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { ByteBuffer buffer = ByteBuffer.allocateDirect(102400); // 5167 // ByteBuffer buffer = ByteBuffer.allocate(102400); // 1268 } long endTime = System.currentTimeMillis(); System.out.println("花费时间: " + (endTime - startTime)); } ``` *** ## Java IO模型系列教程-第五章-NIO核心组件之Channel ### 五、NIO核心组件之Channel Java NIO的通道类似流,都是用于传输数据的。但通过又与流有些不同;**流的数据走向是单向的**,分为输入流(只能读取数据),输出流(只能写出数据),但NIO中的通道不一样,**通道既可以写数据到Buffer,又可以从Buffer中读取数据;** **另外流的操作对象是数组,而通道的操作对象是Buffer;** - Java中常见的Channel如下: - `FileChannel`:用于文件 I/O 编程 - `SocketChannel`、`ServerSocketChannel`:用于 TCP I/O 编程 - `DatagramChannel`:用于 UDP I/O 编程 *** #### 5.1 FileChannel FileChannel是用于文件I/O编程的管道类;通过FileInputStream和FileOutputStream可以获取一个关联文件的Channel,即FileChannel; - `public FileChannel getChannel()`:获取该流的Channel; 示例代码: ```java // 通过输入流获取Channel,该Channel只能读取数据 FileChannel inChannel = new FileInputStream("").getChannel(); // 通过输出流获取Channel,该Channel只能写出数据 FileChannel outChannel = new FileOutputStream("").getChannel(); ``` **我们可以读取Channel中的数据,也可以往Channel中写数据,Channel是可读可写的;但FileChannel最终需要将数据写入到对应的输入/输出流,因为流是有顺序的,输入流只能读取数据,而输出流只能写出数据,因此使用FileInputStream/FileOutputStream获取到的FileChannel只能读或写;** ##### 5.1.1 FileChannel的基本使用 - `int read(ByteBuffer dst)`:从Channel中读取数据,写入到Buffer中,返回读取到的有效字节个数,读取到末尾返回-1 - `int write(ByteBuffer src)`:从Buffer中读取数据,写入到Channel中,返回写出的有效字节个数,如果没有数据写出返回0 案例1:通过Channel输出数据 ```java package com.dfbz.channel.fileChannel; import org.junit.Test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_FileChannel的基本使用 { @Test public void writer() throws Exception { // 1. 创建一个输出流 FileOutputStream fos = new FileOutputStream("001.txt"); // 2. 通过输出流获取Channel FileChannel channel = fos.getChannel(); // 3. 创建一个Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("hello".getBytes()); // 4. 切换读模式(limit=position=5;position=0) buffer.flip(); // 5. 将Buffer的数据读出来,写入到Channel中 int len = channel.write(buffer); // System.out.println("写出的有效字节个数: " + len); // 5 len = channel.write(buffer); System.out.println("写出的有效字节个数: " + len); // 0 channel.close(); fos.close(); } } ``` **注意:channel.write()方法是将数据从Buffer中读取出来,然后写入到Channel中,这对Buffer本质上是一次读操作,我们对Buffer的任何读写操作都会造成Buffer中的position位移;** 再次测试: ```java @Test public void writer_02() throws Exception { // 1. 创建一个输出流 FileOutputStream fos = new FileOutputStream("001.txt"); // 2. 通过输出流获取Channel FileChannel channel = fos.getChannel(); // 3. 创建一个Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); System.err.println(buffer); // position=0,limit=capacity=1024 buffer.put("hello".getBytes()); System.err.println(buffer); // position=5,limit=capacity=1024 // 4. 切换读模式(limit=position=5;position=0) buffer.flip(); System.err.println(buffer); // position=0,limit=5,capacity=1024 // 5. 将Buffer的数据读出来(也会造成position的位移),写入到Channel中 int len = channel.write(buffer); // System.out.println("写出的有效字节个数: " + len); // 5 len = channel.write(buffer); System.out.println("写出的有效字节个数: " + len); // 0 System.err.println(buffer); // position=5,limit=5,capacity=1024 channel.close(); fos.close(); } ``` 执行程序,查看控制台:
案例2:通过Channel读取数据 ```java @Test public void reader() throws Exception { // 1. 创建一个输入流 FileInputStream fis = new FileInputStream("001.txt"); // 2. 通过输入流获取Channel FileChannel channel = fis.getChannel(); // 3. 创建一个Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); // 4. 从Channel中读取数据,写入到Buffer中 int len = channel.read(buffer); System.out.println("读取到的有效字节个数: " + len); // 5 len = channel.read(buffer); System.out.println("读取到的有效字节个数: " + len); // -1 // limit=position,position=0 buffer.flip(); byte[] data = new byte[buffer.limit()]; buffer.get(data); // 从Buffer中读取数据 System.out.println(new String(data, 0, data.length)); channel.close(); fis.close(); } ``` **注意:和write方法一样,channel调用read方法是将数据从Channel读取出来,往Buffer中写入,这对Buffer来说本质上是一种写的操作,我们对Buffer的任何读写操作都会造成Buffer中的position位移;** 再次测试: ```java @Test public void reader_01() throws Exception { // 1. 创建一个输入流 FileInputStream fis = new FileInputStream("001.txt"); // 文件内容: hello // 2. 通过输入流获取Channel FileChannel channel = fis.getChannel(); // 3. 创建一个Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); System.err.println(buffer); // position=0,limit=capacity=1024 // 4. 从Channel中读取数据,写入到Buffer中(会造成position的位移) channel.read(buffer); System.err.println(buffer); // position=5,limit=capacity=1024 // limit=position=5,position=0,capacity=1024 buffer.flip(); System.err.println(buffer); // position=0,limit=5,capacity=1024 byte[] data = new byte[buffer.limit()]; // 从Buffer中读取数据 buffer.get(data); System.err.println(buffer); // position=5,limit=5,capacity=1024 System.out.println(new String(data, 0, data.length)); channel.close(); fis.close(); } ``` 执行程序,查看控制台:
案例3-使用FileChannel拷贝文件: ```java @Test public void copy() throws Exception { FileInputStream fis = new FileInputStream("100.png"); FileOutputStream fos = new FileOutputStream("200.png"); FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取Channel中的数据,写入到Buffer中 while (inChannel.read(buffer) != -1) { // limit=position,position=0 buffer.flip(); // 从Buffer中读取数据,写入Channel中 outChannel.write(buffer); // limit=capacity,position=0 buffer.clear(); } inChannel.close(); outChannel.close(); fis.close(); fos.close(); } ``` ##### 5.1.2 FileChannel的常用方法 - `FileChannel truncate(long s)`:将此通道的文件截取为给定大小 - `long size()`:返回此通道的文件的当前大小 - `long position()`:返回该Channel目前所在的文件位置; - `FileChannel position(long p)`:设置该Channel目前所在的文件位置; - `public void force(boolean metaData)`:将当前Channel中的数据强制写入到磁盘中 - `public long transferFrom(ReadableByteChannel src, long position, long count)`:从src的position位置开始读取,读取count个字节到当前Channel中 - `public long transferTo(long position, long count,WritableByteChannel target)`:从当前Channel的position位置开始读取,读取count个字节到target中 - `long write(ByteBuffer[] srcs)` 将ByteBuffer[]到中的数据全部写入(聚集)到 Channel - `long read(ByteBuffer[] dsts)` 将Channel到中的数据读取出来,然后全部写入(分散)到ByteBuffer[] *** 1)**truncate** - `FileChannel truncate(long s)`:将此通道的文件截取为给定大小; 可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。 ```java @Test public void truncate() throws Exception { FileInputStream stream = new FileInputStream("001.txt"); // 文件内容: hello // FileOutputStream stream = new FileOutputStream("001.txt"); // 文件内容: hello // 创建流(可读可写) // RandomAccessFile stream = new RandomAccessFile("001.txt", "rw"); // 文件内容: hello // 获取Channel FileChannel channel = stream.getChannel(); // 将Channel的文件截取为2 channel.truncate(2); // 使用FileInputStream出现: java.nio.channels.NonWritableChannelException ByteBuffer buffer = ByteBuffer.allocate(10); channel.read(buffer); // 使用FileOutputStream出现: java.nio.channels.NonReadableChannelException System.out.println(new String(buffer.array(), 0, buffer.array().length)); // he stream.close(); channel.close(); } ``` > Tips:FileInputStream只能读取数据,因此使用FileInputStream获取的FileChannel也只能读取数据;同理FileOutputStream只能写出数据,使用FileOutputStream获取FileChannel也只能写出数据;因此上述案例中使用RandomAccessFile,即可读又可写; 2)s**ize** FileChannel实例的size()方法将返回该实例所关联文件的大小。 - `long size()`:返回此通道的文件的当前大小 ```java @Test public void size() throws Exception { FileInputStream fis = new FileInputStream("001.txt"); FileChannel channel = fis.getChannel(); System.out.println(channel.size()); // 5 } ``` 3)**position** - `long position()`:返回该Channel目前所在的文件位置; 有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取FileChannel的当前位置。也可以通过调用position(long pos)方法设置FileChannel的当前位置。 - 测试position: ```java package com.dfbz.channel; import org.junit.Test; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_常用方法 { @Test public void position() throws Exception { FileInputStream fis = new FileInputStream("001.txt"); FileChannel channel = fis.getChannel(); // 获取此Channel的读写位置(默认为0) System.out.println(channel.position()); // 0 ByteBuffer buffer = ByteBuffer.allocate(1024); // 从Channel中读取数据到buffer中,读取了5个有效字节 channel.read(buffer); System.out.println("buffer中的内容: " + new String(buffer.array(), 0, buffer.array().length)); System.out.println("---------------------"); // limit=capacity,position=0 buffer.clear(); System.out.println(channel.position()); // 5 // 将channel的读取位置设置为2 channel.position(2); channel.read(buffer); System.out.println("buffer中的内容: " + new String(buffer.array(), 0, buffer.array().length)); } } ``` > Tips:如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1 —— 文件结束标志。 **如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。** - 测试position-2: ```java @Test public void position2() throws Exception { // 创建流(可读可写) RandomAccessFile raf = new RandomAccessFile("001.txt", "rw"); // 文件内容: hello // 获取Channel FileChannel channel = raf.getChannel(); // 将position设置为size+10(空隙) channel.position(channel.size()+10); // 往Channel写出数据 channel.write(ByteBuffer.wrap("abc".getBytes())); raf.close(); channel.close(); } ``` 001.txt文件内容如下:
4)**force** - `public void force(boolean metaData)`:将当前Channel中的数据强制写入到磁盘中 FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。 另外,force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上 - 测试代码: ```java @Test public void force() throws Exception { // 创建流(可读可写) RandomAccessFile raf = new RandomAccessFile("001.txt", "rw"); // 文件内容: hello // 获取Channel FileChannel channel = raf.getChannel(); channel.write(ByteBuffer.wrap("Hello Everyone!".getBytes())); /* 将内存中的数据强制写入到磁盘中 true: 将文件元信息(权限信息等)写入磁盘 false: 不写入文件元信息到磁盘 */ channel.force(true); raf.close(); channel.close(); } ``` 5)**transferFrom** - `public long transferFrom(ReadableByteChannel src, long position, long count)`:从src的position位置开始读取,读取count个字节到当前Channel中 FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中 ```java @Test public void transferFrom() throws Exception { RandomAccessFile fromFile = new RandomAccessFile("001.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("002.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); // 从fromChannel的position位置开始读取,读取count个字节到toChannel中 toChannel.transferFrom(fromChannel, position, count); } ``` 6)**transferTo** - `public long transferTo(long position, long count,WritableByteChannel target)`:从当前Channel的position位置开始读取,读取count个字节到target中 transferTo()方法将数据从FileChannel传输到其他的channel中 ```java @Test public void transferTo() throws Exception { RandomAccessFile fromFile = new RandomAccessFile("001.txt", "rw"); FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("002.txt", "rw"); FileChannel toChannel = toFile.getChannel(); long position = 0; long count = fromChannel.size(); // 从fromChannel的position位置开始读取,读取count到toChannel中 fromChannel.transferTo(position, count, toChannel); } ``` ##### 5.1.3 聚集和分散 - 聚集(gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。 - 分散(scatter):从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。 - `long write(ByteBuffer[] srcs)` 将ByteBuffer[]到中的数据全部写入(聚集)到 Channel - `long read(ByteBuffer[] dsts)` 将Channel到中的数据读取出来,然后全部写入(分散)到ByteBuffer[] *** - Gathering Writes(聚集):是指数据从多个buffer写入到同一个channel。如下图描述:
- Scattering Reads(分散):是指数据从一个channel读取到多个buffer中。如下图描述:
- 测试代码: ```java package com.dfbz.channel; import org.junit.Test; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo03_聚集和分散 { // 聚集 @Test public void gather() throws IOException { RandomAccessFile raf = new RandomAccessFile("004.txt", "rw"); FileChannel channel = raf.getChannel(); ByteBuffer header = ByteBuffer.wrap("{'content-Type':'application/json'}".getBytes()); ByteBuffer body = ByteBuffer.wrap("{'username':'admin','password':'123'}".getBytes()); ByteBuffer[] bufferArray = {header, body}; // 将多个Buffer聚集到一起写入到channel中 channel.write(bufferArray); channel.close(); raf.close(); } // 分散 @Test public void test2() throws IOException { RandomAccessFile raf = new RandomAccessFile("004.txt", "rw"); FileChannel channel = raf.getChannel(); ByteBuffer header = ByteBuffer.allocate("{'content-Type':'application/json'}".getBytes().length); ByteBuffer body = ByteBuffer.allocate("{'username':'admin','password':'123'}".getBytes().length); ByteBuffer[] bufferArray = {header, body}; // 将channel中的数据分散读取,然后逐个写入到每个Buffer中 channel.read(bufferArray); for (ByteBuffer buffer : bufferArray) { System.out.println(new String(buffer.array(),0,buffer.array().length)); } channel.close(); raf.close(); } } ``` *** #### 5.2 DatagramChannel ##### 5.2.1 DatagramChannel简介 DatagramChannel是用于UDP编程的Channel;获取到了DatagramChannel之后,可以使用Channel直接发送Buffer数据;因为UDP是无连接的网络协议,因此使用DatagramChannel发送的Buffer数据在发送时都会被封装成UDP报文,并且存在UDP协议的特性; - **发送和接收报文:** 在使用DatagramChannel发送数据时,必须通过InetSocketAddress类来指定接收端的地址和端口;而在接收数据时,接收端的Channel必须先通过InetSocketAddress类绑定一个地址和端口; - **读取和写入数据:** DatagramChannel不仅可以发送报文和接收报文,还可以读取DatagreamChannel中的数据,或往DatagreamChannel中写入数据;与发送和接收不同,在使用DatagramChannel发送或接收时,DatagramChannel充当一个接收器/发送器的角色,**自己本身并不存储那些数据**;而是将数据接收到一个Buffer中,而在使用DatagramChannel读取/写入数据时,数据可以从Buffer中读取到Channel中,也可以从Channel写出到Buffer; ##### 5.2.2 DatagramChannel的获取 在Java NIO中,我们可以通过DatagreamChannel来直接打开一个Channel,也可以DatagreamSocket可以获取到一个属于该Socket的Channel,**但该Socket必须是由Channel获取的Socke**t;这两种方式获取到的Channel是同一个; - DatagreamChannel方法: - `public static DatagramChannel open()` :打开一个基于UDP协议的Channel管道; - `public DatagramSocket socket()`:通过Channel获取一个Socket; - DatagreamSocket方法: - `public DatagramChannel getChannel()`:获取该Socket的Channel管道; 测试代码: ```java package com.dfbz.channel.datagramChannel; import org.junit.Test; import java.net.DatagramSocket; import java.nio.channels.DatagramChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_DatagramChannel的获取 { @Test public void test() throws Exception { // 通过DatagramChannel打开一个Channel DatagramChannel channel = DatagramChannel.open(); // 通过channel也可以获取一个socket DatagramSocket socket = channel.socket(); // 通过socket也可以获取Channel DatagramChannel channel2 = socket.getChannel(); System.out.println(channel2.getClass()); // class sun.nio.ch.DatagramChannelImpl System.out.println(channel == channel2); // true } @Test public void test2() throws Exception { DatagramSocket socket = new DatagramSocket(9999); // 不能通过DatagramSocket来获取channel DatagramChannel channel = socket.getChannel(); System.out.println(channel); // null } } ``` ##### 5.2.3 DatagramChannel的常用方法 - `public DatagramChannel bind(SocketAddress local)`:绑定当前客户端的地址和端口,其他Channel向当前Channel发送数据时指定的地址。**在接收UDP报文时,必须先绑定;** - `public SocketAddress receive(ByteBuffer dst)`:接收UDP报文,将接收到的UDP报文赋值给dst; - `public int send(ByteBuffer src, SocketAddress target)`:发送一个UDP报文;并指定报文要发送的地址和端口 - `public DatagramChannel connect(SocketAddress remote)`:用于连接其他的DatagramChannel ,DatagramChannel之间建立连接后可以相互读取/写入数据; > Tips:由于UDP是无连接的,使用connect方法连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据。因此即使是连接的地址不存在,也不会报错; 1)**发送和接收** 测试代码: ```java package com.dfbz.channel.datagramChannel; import org.junit.Test; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_发送和接收 { /** * 发送端 * * @throws Exception */ @Test public void sender() throws Exception { // 通过DatagramChannel打开一个Channel DatagramChannel channel = DatagramChannel.open(); // 往该Channel写入数据 channel.send(ByteBuffer.wrap("hello".getBytes()), new InetSocketAddress("127.0.0.1", 9999)); channel.close(); } /** * 接收端 * * @throws Exception */ @Test public void receive() throws Exception { // 通过DatagramChannel打开一个Channel DatagramChannel channel = DatagramChannel.open(); // 绑定地址(用于接收该地址发送过来的数据) channel.bind(new InetSocketAddress("127.0.0.1", 9999)); ByteBuffer buffer = ByteBuffer.allocate(1024); // 使用Buffer接收报文 channel.receive(buffer); System.out.println(new String(buffer.array(), 0, buffer.array().length)); } } ``` 2)**读取和写入**
- 读取端: ```java package com.dfbz.channel.datagramChannel; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo03_读取端 { public static void main(String[] args) throws Exception { // 获取Channel DatagramChannel channel = DatagramChannel.open(); /* 绑定一个地址 1) 用于接收该地址发送的UDP报文 2) 用于其他DatagramChannel与当前Channel建立连接(逻辑连接),待会可以使用Channel从127.0.0.1的9999端口读取数据 */ channel.bind(new InetSocketAddress("127.0.0.1", 9999)); /* 连接一个地址: 1) 建立一个UDP逻辑连接,如果需要读取127.0.0.2主机的数据则必须建立逻辑连接 2) 建立好逻辑连接后,可以使用channel向127.0.0.2主机写入数据 */ channel.connect(new InetSocketAddress("127.0.0.2", 9999)); System.out.println("开始接收数据: "); System.out.println("----------------"); // 创建Buffer,用读取Channel中的数据 ByteBuffer readBuffer = ByteBuffer.allocate(1024); // position=0,limit=1024,capacity=1024 while (true) { // 将数据从Channel中读取出来,写入到readBuffer中 channel.read(readBuffer); // 预备下次一次从Channel读出数据写入到Buffer中[position=0,limit=capacity] readBuffer.clear(); System.out.println("接收到来自【" + channel.getRemoteAddress() + "】的数据: " + new String(readBuffer.array(), 0, readBuffer.array().length)); } } } ``` - 写入端: ```java package com.dfbz.channel.datagramChannel; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.util.Scanner; /** * @author lscl * @version 1.0 * @intro: */ public class Demo04_写入端 { public static void main(String[] args) throws Exception { // 获取Channel DatagramChannel channel = DatagramChannel.open(); /* 绑定一个地址 1) 用于接收该地址发送的UDP报文 2) 用于其他DatagramChannel与当前Channel建立连接(逻辑连接),待会可以使用Channel从127.0.0.2的9999端口读取数据 */ channel.bind(new InetSocketAddress("127.0.0.2", 9999)); // 连接一个地址: 与指定的地址建立逻辑连接,用于向这个地址发送数据,待会可以使用Channel向写入数据到127.0.0.1的9999端口 channel.connect(new InetSocketAddress("127.0.0.1", 9999)); // 创建Buffer ByteBuffer writeBuffer = ByteBuffer.allocate(1024); // position=0,limit=1024,capacity=1024 // 获取一个扫描器 Scanner scanner = new Scanner(System.in); while (true) { System.out.println("请输入数据: "); // 接收控制台输入的数据 String str = scanner.nextLine(); // 将控制台输入的数据添加到buffer中 writeBuffer.put(str.getBytes()); // buffer的position会进行位移 // limit=position,position=0 writeBuffer.flip(); // 从Buffer中读取数据出来,往Channel中写入数据 // buffer的position会进行位移 channel.write(writeBuffer); // position=0,limit=capacity(预备下一次写入) writeBuffer.clear(); System.out.println("使用Channel向【" + channel.getRemoteAddress() + "】发送了数据: " + str); } } } ``` 使用一个Channel进行读取和写入: ```java package com.dfbz.channel.datagramChannel; import org.junit.Test; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo05_读取和写入 { /** * 读取/写入数据 * * @throws Exception */ @Test public void readerAndWriter() throws Exception { // 获取Channel DatagramChannel channel = DatagramChannel.open(); /* 绑定一个端口(本机) 1) 接收127.0.0.1主机9999端口发送的UDP报文 2) 用于其他DatagramChannel与当前Channel建立连接(逻辑连接),待会可以使用Channel从127.0.0.1的9999端口读取数据 */ channel.bind(new InetSocketAddress("127.0.0.1", 9999)); /* 连接一个地址: 1) 建立一个UDP逻辑连接,如果需要读取127.0.0.2主机的数据则必须建立逻辑连接 2) 建立好逻辑连接后,可以使用channel向127.0.0.2主机写入数据 */ channel.connect(new InetSocketAddress("127.0.0.1", 9999)); // 创建Buffer,用于往Channel写入数据 ByteBuffer writeBuffer = ByteBuffer.wrap("hello".getBytes()); // position=0,limit=5,capacity=5 // 创建Buffer,用读取Channel中的数据 ByteBuffer readBuffer = ByteBuffer.allocate(1024); // position=0,limit=1024,capacity=1024 while (true) { // 将数据从writeBuffer中读取出来,写入到Channel中 channel.write(writeBuffer); // writeBuffer:[position=5,limit=5,capacity=5] // 切换写模式读模式,writeBuffer:[position=0,limit=5,capacity=5] writeBuffer.flip(); // 将数据从Channel中读取出来,写入到readBuffer中 channel.read(readBuffer); // readBuffer:[position=5,limit=1024,capacity=1024] // 切换写模式,readBuffer:[position=0,limit=1024,capacity=1024] readBuffer.clear(); System.out.println("Channel的数据: " + new String(readBuffer.array(), 0, readBuffer.array().length)); Thread.sleep(1000); } } } ``` *** #### 5.3 SocketChannel与ServerSocketChannel ##### 5.3.1 Channel的简介 BIO中Socket编程的两个核心类分别为:Socket(代表客户端)和ServerSocket(代表服务器端),通过ServerSocket的accept可以接收一个客户端的Socket; 在NIO中,提供有SocketChannel和ServerSocketChannel,分别代表客户端和服务端;底层依旧采用TCP协议进行数据的网络传输,同时这些Channel还支持非阻塞方式运行,这一点与原生的Socket/ServerSocket有很大的不同;例如ServerSocketChannel在接收一个客户端时,如果还未有客户端来连接服务端,那么accept会返回null,而不是将当前线程阻塞; ##### 5.3.2 Channel的获取 通过ServerSocketChannel也可以来获取一个SocketChannel;也可以和DatagramChannel一样,通过open方法来打开一个管道;并且通过Socket可以获取SocketChannel,通过ServerSocket可以获取ServerSocketChannel; 和DatagramChannel一样,虽然通过Socket可以获取Channel,但该Socket必须是由Channel获取的Socket;因为原生的Socket的getChannel()方法永远返回的是null;
- 测试代码: ```java package com.dfbz.channel.socketChannel; import org.junit.Test; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_Channel的获取 { @Test public void test1() throws Exception { // 获取socketChannel SocketChannel socketChannel = SocketChannel.open(); // 通过channel获取socket Socket socket = socketChannel.socket(); // 通过socket也可以获取对应的Channel System.out.println(socket.getChannel() == socketChannel); // true System.out.println(socket.getClass()); // class sun.nio.ch.SocketAdaptor(并不是一个原生的Socket对象) // serverSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 通过channel获取socket ServerSocket serverSocket = serverSocketChannel.socket(); // 通过socket也可以获取对应的Channel System.out.println(serverSocket.getChannel() == serverSocketChannel); // true System.out.println(serverSocket.getClass()); // class sun.nio.ch.ServerSocketAdaptor(并不是一个原生的ServerSocket对象) } @Test public void test2() throws Exception { // 原生的socket并不能获取Channel Socket socket = new Socket(); SocketChannel socketChannel = socket.getChannel(); System.out.println(socketChannel); // null // 原生的ServerSocket并不能获取Channel ServerSocket serverSocket = new ServerSocket(); ServerSocketChannel serverSocketChannel = serverSocket.getChannel(); System.out.println(serverSocketChannel); // null } @Test public void test3() throws Exception { // serverSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 接收到一个socketChannel(客户端) SocketChannel socketChannel = serverSocketChannel.accept(); } } ``` ##### 5.3.3 Channel的常用方法 - ServerSocketChannel: - `public static ServerSocketChannel open()`:获取一个ServerSocketChannel - `public ServerSocket socket()`:通过当前Channel获取Socket - `public ServerSocketChannel bind(SocketAddress local)`:绑定一个地址,用于客户端(SocketChannel)来连接 - `public SocketChannel accept()`:接收一个客户端(SocketChannel);默认情况下,如果没有客户端来连接,那么accept会使得当前线程一直处于等待状态; - `public SelectableChannel configureBlocking(boolean block)`:将当前Channel设置为非阻塞模式;默认为true(阻塞模式) - `public boolean isBlocking()`:判断当前Channel是否为非阻塞模式;默认true(阻塞模式) - SocketChannel: - `public static SocketChannel open()` :获取一个SocketChannel - `public Socket socket()`:通过当前Channel获取Socket; - `public SocketChannel bind(SocketAddress local)`:绑定一个地址,默认是本机地址,SocketChannel的该方法没有意义,因为SocketChannel是一个客户端,用于连接ServerSocketChannel,通过ServerSocketChannel可以获取客户端的地址,默认情况下SocketChannel为本机地址,并随机分配一个端口; - `public boolean connect(SocketAddress remote)`:用于连接服务端 - `public int read(ByteBuffer dst)`:读取Channel中的数据到Buffer中 - `public int write(ByteBuffer src)`:将Buffer中的数据写入到Channel中; - `public SelectableChannel configureBlocking(boolean block)`:将当前Channel设置为非阻塞模式;默认为true(阻塞模式) - `public boolean isBlocking()`:判断当前Channel是否为非阻塞模式;默认true(阻塞模式) 1)**基本读写** - 服务端: ```java package com.dfbz.channel.socketChannel和serverSocketChannel; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_基本演示_服务端 { public static void main(String[] args) throws Exception{ // 创建一个服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); System.out.println("等待客户端连接...."); // 绑定一个地址 serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9999)); /* 接收一个客户端(如果没有客户端来连接则会造成阻塞) 接收到了与客户端交互的Channel后,通过SocketChannel既可以向客户端写出数据,又可以读取来自客户端发送的数据 */ SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端【" + socketChannel.getRemoteAddress() + "】连接成功成功...."); // 准备一个Buffer ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes()); // 往客户端写入数据 socketChannel.write(buffer); socketChannel.close(); serverSocketChannel.close(); } } ``` - 客户端: ```java package com.dfbz.channel.socketChannel和serverSocketChannel; import org.junit.Test; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; /** * @author lscl * @version 1.0 * @intro: */ public class Demo03_基本演示_客户端 { @Test public void server() throws Exception { SocketChannel socketChannel = SocketChannel.open(); // 连接到一个地址(如果该地址不存在,则默认情况下会阻塞,等到一定时间后抛出异常) socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999)); // 准备一个Buffer用于接收服务端的数据 ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取服务端的数据,返回数据的长度 int len = socketChannel.read(buffer); System.out.println("数据长度: " + len); System.out.println(new String(buffer.array(), 0, buffer.array().length)); } } ``` 2)**拷贝图片案例** 3)**在线聊天案例** *** ## Java IO模型系列教程-第六章-NIO核心组件之Selector ### 六、Selector #### 6.1 Selector的简介 ##### 6.1.1 Seelctor概念 Selector选择器,也叫多路复用器;NIO中实现非阻塞 I/O 的核心对象就是 Selector。当一个连接创建后,不需要创建一个线程来处理这个来连接,这个连接(管道)会被注册到选择器上,选择器可以检查一个或多个 NIO 通道(轮询),并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。
**需要注意的是:不是所有的 Channel 都可以被 Selector 复用的。能被注册到Selector的Channel必须要实现SelectableChannel类;比如FileChannel就不支持被注册到Selector。另外,被注册到Selector的Channel都必须设置为非阻塞模式;** ##### 6.1.2 Channel的注册 Channel注册到Selector后,由Selector来轮询监听所有被注册到该Selector的Channel所触发的事件,这个时候我们就需要关心Selector应该监听Channel触发的哪些事件; - Selector能注册的监听事件如下: - SelectionKey.OP_READ(1):读事件,当被轮询的Channel读缓冲区有数据可读时触发。 - SelectionKey.OP_WRITE(4):可写事件,当被轮询的Channel写缓冲区有空闲空间时触发。一般情况下写缓冲区都有空闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不断就绪浪费 CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很可能满,注册该操作类型就很有必要,同时注意写完后取消注册。 - SelectionKey.OP_CONNECT(8):连接事件,当被轮询到Channel成功连接到其他服务器时触发; - SelectionKey.OP_ACCEPT(16):接收事件,当被轮询到Channel接受到新的连接时触发; > Tips:可写事件比较特殊,一般我们不注册写事件,写操作的就绪条件为底层缓冲区有空闲空间,而写缓冲区绝大部分时间都是有空闲空间的;因此写操作一直是就绪状态的,所以,只有当你确实有数据要写时再注册写操作,并在写完以后马上取消注册。 *** #### 6.2 Selector的事件注册与监听 ##### 6.2.1 选择通道 一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。**select()方法会返回已经准备就绪的通道数量;** - int select():获取上一次调用select()方法后,进入就绪通道(包括连接就绪/接受就绪/读就绪/写就绪等)的数量;该方法会阻塞到至少有一个通道在你注册的事件上就绪了。 - int select(long timeout):和select()方法一样,阻塞timeout毫秒后还没有就绪的事件暂时释放一下线程(返回0),接着进行下一次阻塞 - int selectNow():不会阻塞,不管什么是否有事件就绪都立刻返回(返回0) 测试代码: ```java package com.dfbz.selector; import org.junit.Test; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; public class Demo01_count方法 { @Test public void server() throws Exception { // 创建一个服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 绑定地址 serverSocketChannel.bind(new InetSocketAddress(9999)); // 设置为非阻塞模式 serverSocketChannel.configureBlocking(false); // 获取一个选择器 Selector selector = Selector.open(); /* 注册事件: * 读 : SelectionKey.OP_READ (1) * 写 : SelectionKey.OP_WRITE (4) * 连接 : SelectionKey.OP_CONNECT (8) * 接收 : SelectionKey.OP_ACCEPT (16) */ serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { /* 获取有多少通道已经就绪(包括连接就绪/接受就绪/读就绪/写就绪等) select方法将会一直阻塞到至少要有一个通道在注册的事件上就绪(返回1),之后将会一直轮询调用select方法获取就绪的事件数量 */ int count = selector.select(); System.out.println("count: " + count); Thread.sleep(1000); } } @Test public void client() throws Exception { SocketChannel socketChannel = SocketChannel.open(); // 连接服务器 socketChannel.connect(new InetSocketAddress(9999)); System.out.println("连接成功"); } } ``` ##### 6.2.2 事件注册与监听 我们前面了解到,Selector能注册的监听事件有四种(接收/连接/读/写),当Selector监听到某个事件后可以通过selectedKeys()方法获取那些活跃的事件; **并不是所有的Channel都支持注册所有的事件**,下表描述各种 Channel 允许注册的操作类型,Y 表示允许注册,其中服务器 SocketChannel 指由服务器 ServerSocketChannel.accept()返回的对象。 |项目 |OP_READ |OP_WRITE |OP_CONNECT |OP_ACCEPT| |---|---|---|---|---| |服务器 ServerSocketChannel | | | |Y| |服务器 SocketChannel |Y |Y | || |客户端 SocketChannel |Y |Y |Y || - `public Set selectedKeys()`:获取Selector监听到的事件集合。一个SelectionKey对象代表一个监听事件; SelectionKey监听具体事件的方法: - `public boolean isAcceptable()`:监听接收事件,当客户端连接到服务端时,服务端的接收事件将被触发; - `public boolean isConnectable()`:监听连接事件,当客户端连接到服务端时,客户端的连接事件将被触发; - `public boolean isReadable()`:监听读事件,当客户端向服务端写出数据时,服务端的SocketChannel将触发可读数据; - `public boolean isWritable()`:监听写事件,当被轮询的Channel写缓冲区有空闲空间时触发(一般情况下都会触发) - `public boolean isValid()`:判断当前这个通道是否是有效的; 1)**监听Accept事件** **只有ServerSocketChannel才可以注册Accept事件,因为只有ServerSocketChannel才可以接收其他Channel的请求;** - 服务端: ```java package com.dfbz.selector; import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.util.Iterator; import java.util.Set; public class Demo02_监听接收事件_服务端 { public static void main(String[] args) throws Exception { // 创建一个服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 绑定地址 serverSocketChannel.bind(new InetSocketAddress(9999)); // 设置为非阻塞模式 serverSocketChannel.configureBlocking(false); // 获取一个选择器 Selector selector = Selector.open(); /* 注册事件: * 读 : SelectionKey.OP_READ (1) * 写 : SelectionKey.OP_WRITE (4) * 连接 : SelectionKey.OP_CONNECT (8) * 接收 : SelectionKey.OP_ACCEPT (16): 是否有新的连接 */ serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 获取有多少通道已经准备就绪 int count = selector.select(); System.out.println(count); // 获取那些已经准备就绪的事件 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()) { // 迭代获取每一个事件 SelectionKey event = iterator.next(); if (event.isAcceptable()) { // 接收就绪事件(当有客户端连接到服务器时触发) System.out.println("isAcceptable"); } } Thread.sleep(1000); } } } ``` - 客户端: ```java package com.dfbz.selector; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; public class Demo03_监听接收事件_客户端 { public static void main(String[] args) throws Exception{ SocketChannel socketChannel = SocketChannel.open(); // 连接服务器 socketChannel.connect(new InetSocketAddress(9999)); } } ``` 再上述案例中,当服务器端的Selector监听到了接收事件时(客户端连接服务器时),select方法将会返回1,并且服务端通过调用Selector的selectedKeys()方法可以获取监听到的具体事件;这里存在两个问题: - 1)当Selector被激活后(第一次监听到事件),Selector将会一直轮询select方法,如果没有新的事件准备就绪则返回0,有事件则f返回触发事件的数量;如果select()方法返回0,说明没有新的就绪事件,本次循环应该结束; - 2)调用selectedKeys()方法后,可以获取Selector监听到的事件,获取到事件后,我们可以通过一系列方法来判断到底触发的是什么事件,然后做具体的逻辑处理;**但是逻辑处理完毕后,事件依旧存在**,不会被移除。也就是说,下次调用selectedKeys()方法获取Selector监听到的事件时,上一次依旧处理的事件仍然存在,因此我们在处理完事件后,必须将事件移除; 服务端改进代码: ```java package com.dfbz.selector; import java.net.InetSocketAddress; 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 Demo02_监听接收事件_服务端 { public static void main(String[] args) throws Exception { // 创建一个服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 绑定地址 serverSocketChannel.bind(new InetSocketAddress(9999)); // 设置为非阻塞模式 serverSocketChannel.configureBlocking(false); // 获取一个选择器 Selector selector = Selector.open(); /* 注册事件: * 读 : SelectionKey.OP_READ (1) * 写 : SelectionKey.OP_WRITE (4) * 连接 : SelectionKey.OP_CONNECT (8) * 接收 : SelectionKey.OP_ACCEPT (16): 是否有新的连接 */ serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 获取有多少通道已经准备就绪 int count = selector.select(); System.out.println(count); /*if (count == 0) { // 代表还没有准备就绪的事件 continue; }*/ // 获取那些已经准备就绪的事件 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()) { // 迭代获取每一个事件 SelectionKey event = iterator.next(); if (event.isAcceptable()) { // 接收就绪事件(当有客户端连接到服务器时触发) System.out.println("isAcceptable"); // 接收客户端(如果不接收,那么selector将一直监听到serverSocketChannel的接收事件) SocketChannel socketChannel = serverSocketChannel.accept(); } /* 处理完事件后记得要移除事件,否则该一直存在selectedKeys集合中 如果不移除,下次调用selectedKeys()方法时,即使没有触发该事件也能获取到该事件 */ iterator.remove(); } Thread.sleep(1000); } } } ``` 2)**监听Connect事件** **只有客户端的SocketChannel才可以监听Connect事件,因为只有客户端的SocketChanel才可以连接其他Channel(连接服务端);** - 示例代码-服务端: ```java package com.dfbz.selector; import java.net.InetSocketAddress; import java.nio.channels.ServerSocketChannel; public class Demo04_监听连接事件_服务端 { public static void main(String[] args) throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8888)); while (true) { serverSocketChannel.accept(); } } } ``` - 示例代码-客户端: ```java package com.dfbz.selector; import java.net.InetSocketAddress; 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 Demo05_监听连接事件_客户端 { public static void main(String[] args) throws Exception { // 获取一个客户端的socketChannel SocketChannel socketChannel = SocketChannel.open(); // 设置为非阻塞面模式 socketChannel.configureBlocking(false); // 获取一个Selector选择器 Selector selector = Selector.open(); socketChannel.register(selector, SelectionKey.OP_CONNECT); // 开启一个新的线程来开启监听任务 new Thread() { @Override public void run() { try { while (true) { // 当selector监听到具体的事件后 int count = selector.select(); if (count == 0) { continue; } // 获取监听的事件集合 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); // 迭代遍历每一个事件 while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 判断是否是触发连接事件 if (selectionKey.isConnectable()) { System.out.println("isConnectable"); } // 移除事件 iterator.remove(); } Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } } }.start(); // 连接服务器(触发连接事件) socketChannel.connect(new InetSocketAddress(9999)); } } ``` 3)**监听Read事件** **客户端和服务端的SocketChannel都可以监听Read事件,但ServerSocketChannel不可以监听读事件;** 1)**监听服务端的SocketChannel** - 监听服务端的SocketChannel-读事件-服务端代码: ```java package com.dfbz.selector; 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 Demo06_监听服务端的Read事件_服务端 { public static void main(String[] args) throws Exception { // 创建一个服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(7777)); System.out.println("等待客户端连接..."); // 接收到一个客户端 SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端【" + socketChannel.getRemoteAddress() + "】连接成功"); // 将客户端的channel设置为非阻塞模式(需要注册到Selector) socketChannel.configureBlocking(false); // 创建一个Selector选择器 Selector selector = Selector.open(); // 将客户的Channel注册到选择器上,并让选择器监听该socketChanel的读事件(当客户端发送数据来时将触发) socketChannel.register(selector, SelectionKey.OP_READ); // 创建一个buffer用来接收客户端发送过来的数据 ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { // 是否有事件就绪 int count = selector.select(); if (count == 0) { continue; } // 获取触发的事件 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); // 迭代每个事件 while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 判断是否触发可读事件 if (selectionKey.isReadable()) { System.out.println("触发读事件啦,客户端发送过来的数据是: "); socketChannel.read(buffer); buffer.clear(); System.out.println(new String(buffer.array(), 0, buffer.array().length)); } iterator.remove(); } } } } ``` - 监听服务端的SocketChannel-读事件-客户端代码: ```java package com.dfbz.selector; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Scanner; public class Demo07_监听客户端的Read事件_客户端 { public static void main(String[] args) throws Exception { // 获取一个客户端的socketChannel SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress(7777)); Scanner scanner = new Scanner(System.in); ByteBuffer buffer = ByteBuffer.allocate(1024); while (true){ String str = scanner.nextLine(); buffer.put(str.getBytes()); // limit=position,position=0 buffer.flip(); // 将数据写道服务器的SocketChannel(服务器的SocketChannel触发读事件) socketChannel.write(buffer); // socketChannel会把数据从buffer中读出来,造成position位移 // position=0,limit=capacity buffer.clear(); } } } ``` 2)**监听客户端的SocketChannel** - 监听客户端的SocketChannel-读事件-服务端代码: ```java package com.dfbz.selector; 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.Scanner; import java.util.Set; public class Demo08_监听服务端的Read事件_服务端 { public static void main(String[] args) throws Exception { // 创建一个服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(7777)); System.out.println("等待客户端连接..."); // 接收到一个客户端 SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端【" + socketChannel.getRemoteAddress() + "】连接成功"); // 创建Buffer用于存储要发送给客户端的数据 ByteBuffer buffer = ByteBuffer.allocate(1024); Scanner scanner = new Scanner(System.in); while (true){ String str = scanner.nextLine(); buffer.put(str.getBytes()); // limit=position,position=0 buffer.flip(); // 将数据写道客户端的SocketChannel(客户端的SocketChannel触发读事件) socketChannel.write(buffer); // socketChannel会把数据从buffer中读出来,造成position位移 // position=0,limit=capacity buffer.clear(); } } } ``` - 监听客户端的SocketChannel-读事件-客户端代码: ```java package com.dfbz.selector; 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 Demo09_监听客户端的Read事件_客户端 { public static void main(String[] args) throws Exception { // 获取一个客户端的socketChannel SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress(7777)); socketChannel.configureBlocking(false); // 创建选择器 Selector selector = Selector.open(); // 注册读事件(当服务端有数据写到客户端时触发) socketChannel.register(selector, SelectionKey.OP_READ); ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { // 是否有事件就绪 int count = selector.select(); if (count == 0) { continue; } // 获取触发的事件 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); // 迭代每个事件 while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 判断是否触发可读事件 if (selectionKey.isReadable()) { System.out.println("触发读事件啦,服务端发送过来的数据是: "); socketChannel.read(buffer); buffer.clear(); System.out.println(new String(buffer.array(), 0, buffer.array().length)); } iterator.remove(); } } } } ``` 4)**监听Write事件** 服务端和客户端的SocketChannel都可以注册写事件,当SocketChannel写就绪时触发该事件,默认情况下,写一直都处于就绪状态,因此一旦SocketChannel监听了写事件,Selector的select将永远返回1(事件永远准备就绪); ```java package com.dfbz.selector; import java.net.InetSocketAddress; 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 Demo10_监听服务端的Write事件_服务端 { public static void main(String[] args) throws Exception { // 创建一个服务器 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(6666)); System.out.println("等待客户端连接..."); // 接收到一个客户端 SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端【" + socketChannel.getRemoteAddress() + "】连接成功"); // 将客户端的channel设置为非阻塞模式(需要注册到Selector) socketChannel.configureBlocking(false); // 创建一个Selector选择器 Selector selector = Selector.open(); // 将socketChannel注册到Selector上,并监听写事件 socketChannel.register(selector, SelectionKey.OP_WRITE); while (true) { // 是否有事件就绪 int count = selector.select(); if (count == 0) { continue; } // 获取触发的事件 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); // 迭代每个事件 while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 判断是否触发可读事件(默认情况下一直是处于可写状态) if (selectionKey.isWritable()) { System.out.println("isWritable"); } iterator.remove(); Thread.sleep(1000); } } } } ``` - 客户端: ```java package com.dfbz.selector; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; public class Demo11_监听服务端的Write事件_客户端 { public static void main(String[] args) throws Exception{ SocketChannel socketChannel = SocketChannel.open(); // 连接服务器(服务器的接收事件将要触发) socketChannel.connect(new InetSocketAddress(6666)); } } ``` > Tips:客户端的SocketChannel也可以监听写事件,代码和服务端的SocketChannel一致,这里就不举例了; *** #### 6.3 Selector其他方法 - Selector常用方法: - `public abstract Set selectedKeys()` - `public abstract Set keys()` - `public abstract void close()` - `public abstract boolean isOpen()` - `public abstract Selector wakeup()` - `public abstract SelectorProvider provider()` - SelectionKey常用方法: - `public abstract SelectableChannel channel()` - `public abstract int interestOps()` - `public abstract SelectionKey interestOps(int ops)` - `public abstract int readyOps()` - `public abstract void cancel()` - `public final Object attach(Object ob)` - `public final Object attachment()` - `public abstract Selector selector()` *** #### 6.4 Selector综合案例 Selector正是Java NIO实现IO多路复用的核心组件,当Channel都被注册到Selector后,服务端只需要一个线程来轮询查看注册到Selector的哪些Channel,如果有准备就绪的事件将会执行;这样一来就可以解决我们传统情况下的一个客户端对应一个线程的IO模型了,大大减少服务端的开销; ##### 6.4.1 Selector改造多线程聊天案例 1)**服务端代码:** ```java package com.dfbz.selector; 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 Test1_selector改造网络聊天_服务端 { public static void main(String[] args) throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9999)); Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("等待客户端连接...."); ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { if (selector.select() == 0) { continue; } Set keys = selector.selectedKeys(); Iterator iterator = keys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); if (selectionKey.isAcceptable()) { // 代表有客户端连接了 SocketChannel socketChannel = serverSocketChannel.accept(); // 获取触发连接事件的那个客户端 socketChannel.configureBlocking(false); // 注册读事件到Selector上 socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("客户端【" + socketChannel.getRemoteAddress() + "】连接成功.."); } else if (selectionKey.isReadable()) { // 触发读事件,代表客户端发送信息到服务端了 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); // 获取触发连接事件的那个客户端 socketChannel.read(buffer); // limit=position,position=0 buffer.flip(); System.out.println("客户端【" + socketChannel.getRemoteAddress() + "】的数据: " + new String(buffer.array(), 0, buffer.array().length)); } // 处理完逻辑后移除事件 iterator.remove(); } } } } ``` 2)**客户端代码:** ```java package com.dfbz.selector; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Scanner; public class Test2_selector改造网络聊天_客户端 { public static void main(String[] args) throws Exception { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999)); // 写线程 new Thread() { @Override public void run() { try { Scanner scanner = new Scanner(System.in); // 用于封装要发送给客户端的数据 ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { String str = scanner.nextLine(); buffer.put(str.getBytes()); // limit=position, position=0 buffer.flip(); socketChannel.write(buffer); // position=0,limit=capacity buffer.clear(); } } catch (IOException exception) { exception.printStackTrace(); } } }.start(); } } ``` 运行效果:
*** ## Java IO模型系列教程-第七章-NIO的其他API之Files、Path的使用 ### 七、Path、Files类的使用 #### 7.1 Path ##### 7.1.1 Path简介 Path是一个接口,它用来表示文件系统的路径,可以指向文件或文件夹。也有相对路径和绝对路径之分。Path是在Java 7中新添加进来的。Path接口在java.nio.file包下,所以全称是java.nio.file.Path;Path对象中存在很多与路径相关的功能方法,如获取根路径、获取父路径、获取文件名、拼接路径、返回绝对路径等操作; 在很多方面,java.nio.file.Path接口和java.io.File有相似性,但也有一些细微的差别,Path用起来比File类要方便的多。在很多情况下,可以用Path来代替File类,另外一个Path对象也可以直接转换File对象,一个File对象也可以直接转换为Path对象。 ##### 7.1.2 Path相关方法 - java.nio.file.Paths: - `static Path get(String first,String... more)`:通过连接给定的字符串创建一个Path对象 1)**Path路径相关方法** - `Path toAbsolutePath()` :返回表示此路径的绝对路径的 Path对象。 - `Path toRealPath(LinkOption... options)` :返回现有文件的真实路径。 - `boolean isAbsolute()` :告诉这条路是否是绝对的。 - `Path normalize()`:返回一个路径,该路径是冗余名称元素的消除。 示例代码: ```java package com.dfbz.path; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_Path路径相关方法 { public static void main(String[] args) throws IOException { Path path = Paths.get("./001.txt"); // .\001.txt System.out.println(path); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\.\001.txt System.out.println(path.toAbsolutePath()); // 绝对路径包含相对路径中的那个"./" // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\001.txt System.out.println(path.toRealPath()); // 真实路径不包含相对路径的那个"./" // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\001.txt System.out.println(path.toAbsolutePath().normalize()); // 路径优化可以把"./"去掉 } } ``` 2)**Path获取相关方法** - `Path getName(int index)` :返回此路径的名称元素作为 Path对象(文件所在盘符不算元素)。 - `int getNameCount()` :返回路径中的名称元素的数量(文件所在盘符不算元素)。 - `Path getFileName()`:将此路径表示的文件或目录的名称返回为Path对象。 - `Path getParent()`:返回父路径,如果此路径没有父返回null,如:相对路径 - `Path getRoot()`:返回此路径的根(盘符)作为Path对象,如果此路径没有根返回null,如:相对路径 - `Path subpath(int beginIndex, int endIndex)`:截取该路径的beginIndex(含)索引到endIndex(不含)索引的元素 - `Iterator iterator()`:返回此路径的名称元素的迭代器。 另外Path继承了Iterator接口,任意的Path对象都可以使用foreach迭代; 示例代码: ```java package com.dfbz.path; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_Path获取相关方法 { public static void main(String[] args) throws IOException { Path path = Paths.get("./001.txt"); Path absPath = path.toAbsolutePath(); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\.\001.txt System.out.println(absPath); // BaiduNetDiskWorkspace\workspace System.out.println(absPath.subpath(0,2)); // workspace\IO System.out.println(absPath.subpath(1,3)); // workspace\IO\NIO\. System.out.println(absPath.subpath(1,5)); // workspace\IO\NIO\.\001.txt System.out.println(absPath.subpath(1,6)); } public static void test2(String[] args) throws IOException { Path path = Paths.get("./001.txt"); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\.\001.txt Path absPath = path.toAbsolutePath(); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\001.txt Path realPath = path.toRealPath(); /* 6("./"也算一个元素,注意该方法并没有纳入盘符元素) ["BaiduNetDiskWorkspace","workspace","IO","NIO",".","001.txt"] */ System.out.println(absPath.getNameCount()); /* 5(注意该方法并没有纳入盘符元素) ["BaiduNetDiskWorkspace","workspace","IO","NIO","001.txt"] */ System.out.println(realPath.getNameCount()); System.out.println("-----------------------"); for (Path temp : absPath) { System.out.println(temp); } System.out.println("-----------------------"); for (Path temp : realPath) { System.out.println(temp); } System.out.println("------------------------"); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\. System.out.println(absPath.getParent()); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO System.out.println(realPath.getParent()); // E:\ System.out.println(absPath.getRoot()); // E:\ System.out.println(realPath.getRoot()); } public static void test() throws IOException { Path path = Paths.get("./001.txt"); // .\001.txt System.out.println(path); // 001.txt System.out.println(path.getFileName()); // . System.out.println(path.getParent()); // null System.out.println(path.getRoot()); // 获取的是文件的盘符,相对路径中没有盘符,所以返回null // 2 System.out.println(path.getNameCount()); // . System.out.println(path.getName(0)); // 001.txt System.out.println(path.getName(1)); } } ``` 3)**Path比较相关方法** - `boolean endsWith(Path other)`:测试此路径是否以给定的路径结束。 - `boolean endsWith(String other)`:测试此路径是否以给定的路径结束。 - `boolean startsWith(Path other)` :测试此路径是否以给定的路径开始。 - `boolean startsWith(String other)` :测试此路径是否以给定的路径开始。 **XxxWith方法比较的是是否以指定"元素"开始或者结束,而不是比较是否以指定字符串开始或结束;首先这个字符串得是一个元素!** 示例代码: ```java package com.dfbz.path; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; /** * @author lscl * @version 1.0 * @intro: */ public class Demo03_Path比较相关方法 { public static void main(String[] args) throws IOException { Path path = Paths.get("E:\\BaiduNetDiskWorkspace\\workspace\\IO\\NIO\\001.txt"); System.out.println(path.startsWith("E")); // false,E并不是一个元素 System.out.println(path.startsWith("E:")); // false,E并不是一个元素 System.out.println(path.endsWith("txt")); // false,txt并不是一个元素 System.out.println("---------------"); System.out.println(path.startsWith("E:\\")); // true System.out.println(path.startsWith("E:\\BaiduNet")); // false System.out.println(path.startsWith("E:\\BaiduNetDiskWorkspace")); // true System.out.println(path.startsWith("E:\\BaiduNetDiskWorkspace\\")); // true System.out.println("-----------------------"); System.out.println(path.endsWith("001.txt")); // true System.out.println(path.endsWith("O\\001.txt")); // false System.out.println(path.endsWith("NIO\\001.txt")); // true System.out.println(path.endsWith("\\NIO\\001.txt")); // false } } ``` 4)**Path拼接相关方法** - `Path resolve(Path other)`:返回连接this和other获取的路径 - `Path resolve(String other)`:返回连接this和other获取的路径 - `Path resolveSibling(String other)`:返回连接this的父路径和other获取的路径,如果是想对路径则直接将this替换为other - `Path resolveSibling(String other)`:返回连接this的父路径和other获取的路径,如果是想对路径则直接将this替换为other 示例代码: ```java package com.dfbz.path; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; /** * @author lscl * @version 1.0 * @intro: */ public class Demo04_Path拼接相关方法 { public static void main(String[] args) throws IOException { Path path1 = Paths.get("E:\\BaiduNetDiskWorkspace\\workspace\\IO\\NIO\\001.txt"); Path path2 = Paths.get("001.txt"); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\001.txt\002.txt System.out.println(path1.resolve("002.txt")); // 001.txt\002.txt System.out.println(path2.resolve("002.txt")); System.out.println("---------------------------------"); // E:\BaiduNetDiskWorkspace\workspace\IO\NIO\002.txt System.out.println(path1.resolveSibling("002.txt")); // 002.txt System.out.println(path2.resolveSibling("002.txt")); } } ``` 5)**File与Path的转换** - java.nio.file.Path: - `File toFile()` :返回表示此路径的File对象。 - String toString() :返回此路径的字符串表示形式。 - `URI toUri()` :返回一个URI来表示此路径。 - `static Path of(String first, String... more)`:构建一个Path对象; - java.io.File: - `Path toPath()`:将该File转换为Path对象 示例代码: ```java package com.dfbz.path; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; /** * @author lscl * @version 1.0 * @intro: */ public class Demo05_File与Path的转换 { public static void main(String[] args) { // 构建一个新的Path Path path = Path.of("001.txt"); System.out.println(path); // 001.txt // 通过多级目录来构建Path System.out.println(Path.of("aaa", "bbb", "000.txt")); // aaa\bbb\000.txt } public static void test3(String[] args) throws URISyntaxException { URI uri = new URI("001.txt"); // 通过URI来构建File File file = new File(uri); System.out.println(file); // 通过uri来构建Path Path path = Paths.get(uri); System.out.println(path); } public static void test2(String[] args) { File file = new File("001.txt"); // File转Path Path path = file.toPath(); System.out.println(path); // File转RRI URI uri = file.toURI(); System.out.println(uri); } public static void test(String[] args) throws IOException { Path path = Paths.get("001.txt"); // Path转换为File File file = path.toFile(); System.out.println(file); // Path转换为URI URI uri = path.toUri(); System.out.println(uri); } } ``` *** #### 7.2 Files类 ##### 7.2.1 Files类简介 Files类是一个强大的文件处理类,它可以帮助我们类似与一个文件操作的工具类,可以使得普通文件操作变得快捷; ##### 7.2.2 Files类相关方法 1)**文件的操作** - 文件/文件夹的判断方法: |返回值|方法名|说明| |:---:|:---:|:---:| |`long`|`size(Path path)`|返回文件大小| |`boolean`|`isDirectory(Path path)`|是否是文件夹| |`boolean`|`isExecutable(Path path)`|是否是可执行文件| |`boolean`|`isHidden(Path path)`|是否是隐藏的| |`boolean`|`exists(Path path)`|该文件/文件夹是否存在| |`boolean`|`notExists(Path path)`|是否不存在| |`boolean`|`isReadable(Path path)`|是否可读| |`boolean`|`isWritable(Path path)`|是否可写| - 移动文件/文件夹: |返回值|方法名|说明| |:---:|:---:|:---:| |`Path`|`Files.move(Path src, Path target)`|剪切,如果目标已存在,会报错| |`Path`|`public static Path move(Path source, Path target, CopyOption... options)`|如果目标已存在,会替换| - 复制文件/文件夹: |返回值|方法名|说明| |:---:|:---:|:---:| |`Path`|`Files.copy(Path src, Path target)`|复制文件,如果存在同名的目标文件,会报错。| |`Path`|`public static Path copy(Path source, Path target, CopyOption... options)`|如果存在同名的目标文件,会替换只能是文件,不能是文件夹(只能复制空的文件夹)| - 删除文件/目录: |返回值|方法名|说明| |:---:|:---:|:---:| |`Path`|`Files.delete(Path path)`|删除文件、空目录。如果不存在,会报错| |`Path`|`Files.deleteIfExists(Path path)`|存在才删除,不存在时不会报错。| - 创建文件/目录: |返回值|方法名|说明| |:---:|:---:|:---:| |`Path`|`createFile(Path filePath)`|创建文件,只能是文件,不能是文件夹。如果已存在同名文件,会报错。| |`Path`|`createDirectory(Path dirPath)`|创建文件夹。如果已存在同名文件夹,会报错| - 示例代码: ```java package com.dfbz.files; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_文件的操作 { /** * 文件/文件夹的判断 * * @param args * @throws Exception */ public static void main(String[] args) throws Exception { // 是否是文件夹 System.out.println(Files.isDirectory(Path.of("001"))); // 文件或文件夹是否存在 System.out.println(Files.exists(Path.of("001"))); // 文件或文件夹是否是隐藏 System.out.println(Files.isHidden(Path.of("001"))); // 文件或文件夹是否是可读 System.out.println(Files.isReadable(Path.of("001"))); // 文件或文件夹是否是可写 System.out.println(Files.isWritable(Path.of("001"))); // 文件或文件夹是否是可执行 System.out.println(Files.isExecutable(Path.of("001"))); // 返回文件的大小(文件夹将会直接返回0) long size = Files.size(Path.of("001.txt")); System.out.println(size); } /** * 移动文件/文件夹 * * @param args * @throws Exception */ public static void test4(String[] args) throws Exception { // 将001.txt移动到000目录下,并命名为aaa.txt // Files.move(Path.of("000.txt"),Path.of("./000/aaa.txt")); // 相当于剪切操作 Files.move(Path.of("000"), Path.of("001")); } /** * 复制文件/文件夹 * * @param args * @throws Exception */ public static void test3(String[] args) throws Exception { // 只能复制空的文件夹,即使文件中存在很多文件(相当于拷贝文件夹) // Files.copy(Path.of("000"),Path.of("001")); // 将001.txt复制到000目录下,并命名为bbb.txt // Files.copy(Path.of("001.txt"),Path.of("./000/bbb.txt")); FileOutputStream fos = new FileOutputStream("003.txt"); // 将001.txt中的字节复制到指定的输出流中 Files.copy(Path.of("001.txt"), fos); fos.close(); } /** * 删除文件/目录 * * @param args * @throws IOException */ public static void test2(String[] args) throws IOException { // 删除文件或文件夹,该文件或文件夹必须存在(删除文件夹时,文件夹必须是空的,不能包含其他文件或文件夹) // Files.delete(Path.of("000")); // 如果该文件或文件夹存在就删除 Files.deleteIfExists(Path.of("000")); } /** * 创建文件/目录 * * @param args * @throws Exception */ public static void test1(String[] args) throws Exception { // 创建目录(该方法不能创建多级目录) Files.createDirectory(Path.of("000")); // 创建多级目录 Files.createDirectories(Path.of("111\\222\\333")); // 创建文件(该文件不能存在) Files.createFile(Path.of("002.txt")); } } ``` 2)**读取和写入** - 读取相关方法: |返回值|方法名|说明| |:---:|:---:|:---:| |`String`|`String Files.readString(Path path)`|读取所有文本,以String形式返回。会读取换行符。只能是文本文件| |`String`|`Files.readString(Path path, Charset charset)`|可指定解码字符集| |`List`|`Files.readAllLines(Path path)`|读取所有的行,以List形式返回,一行就是一个String类型的元素。只能是文本文件| |`List`|`Files.readAllLines(Path path, Charset charset)`|可指定解码字符集| |`byte[]`|`Files.readAllBytes(Path path)`|读取文件所有数据,以字节数组形式返回。文件可以是任意类型。| - 写入相关方法: |返回值|方法名|说明| |:---:|:---:|:---:| |`Path`|`Files.write(Path path, byte[] bytes)`|写入一个byte[]| |`Path`|`Files.writeString(Path path, CharSequence cs)`|写入一个字符序列,可以是String、StringBuffer、StringBuilder、Char[]| |`Path`|`Files.writeString(Path path, CharSequence cs, Charset charset)`|指定编码字符集| - 示例代码: ```java package com.dfbz.files; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.List; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01 { public static void main(String[] args) throws Exception { // 写出字符串 Files.writeString(Path.of("001.txt"), "你好"); // 按照指定的编码表写出字符串 Files.writeString(Path.of("001.txt"), "UTF-8"); // 追加内容 Files.writeString(Path.of("001.txt"), "UTF-8", StandardOpenOption.APPEND); } /** * 写字节 * * @param args * @throws Exception */ public static void test2(String[] args) throws Exception { // 往文件中写入字节 byte[] data = {97, 98, 99}; Files.write(Path.of("001.txt"), data); // 往文件中写内容 List lines = Arrays.asList("你好", "我好", "大家好"); Files.write(Path.of("001.txt"), lines); // 追加内容 Files.write(Path.of("001.txt"), "我是追加的内容".getBytes(), StandardOpenOption.APPEND); } /** * 读取数据 * * @param args * @throws IOException */ public static void test(String[] args) throws IOException { // 读取数据 byte[] data = Files.readAllBytes(Paths.get("001.txt")); System.out.println(new String(data)); System.out.println("------------------"); // 使用默认的编码表读取字符串 String str = Files.readString(Paths.get("001.txt")); System.out.println(str); System.out.println("------------------"); // 指定编码表读取字符串 String str2 = Files.readString(Paths.get("001.txt"), Charset.forName("UTF-8")); System.out.println(str2); System.out.println("-----------------------"); // 按行读取数据,每一行数据都是一个字符串 List lines = Files.readAllLines(Path.of("001.txt")); for (String line : lines) { System.out.println(line); } } } ``` 3)**文件与流** |返回值|方法名|说明| |:---:|:---:|:---:| |`BufferedReader`|`Files.newBufferedReader(Path path)`|可指定解码字符集| |`BufferedReader`|`Files.newBufferedReader(Path path,Charset charset)`|| |`BufferedWriter`|`Files.newBufferedWriter(Path path)`|| |`BufferedWriter`|`Files.newBufferedWriter(Path path, Charset charset)`|可指定编码字符集| |`InputStream`|`Files.newInputStream(Path path)`|| |`OutputStream`|`Files.newOutputStream(Path path)`|| |`SeekableByteChannel`|`newByteChannel(Path path, OpenOption... options)`|获取Channel| - 示例代码: ```java package com.dfbz.files; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; /** * @author lscl * @version 1.0 * @intro: */ public class Demo03_文件与流 { /** * 通过Path获取Channel * * @param args * @throws Exception */ public static void main(String[] args) throws Exception { // 通过Path构建Channel SeekableByteChannel channel = Files.newByteChannel(Path.of("001.txt"), // 设置可读 StandardOpenOption.READ, // 设置可写 StandardOpenOption.WRITE, // 设置追加 StandardOpenOption.APPEND ); channel.write(ByteBuffer.wrap("hello".getBytes())); channel.close(); } /** * 通过Path获取流 * * @param args * @throws Exception */ public static void test(String[] args) throws Exception { InputStream is = Files.newInputStream(Path.of("001.txt")); OutputStream os = Files.newOutputStream(Path.of("004.txt")); BufferedReader br1 = Files.newBufferedReader(Path.of("005.txt")); BufferedReader br2 = Files.newBufferedReader(Path.of("005.txt"), Charset.forName("UTF-8")); BufferedWriter bw1 = Files.newBufferedWriter(Path.of("006.txt")); BufferedWriter bw2 = Files.newBufferedWriter(Path.of("006.txt"), Charset.forName("GBK")); } } ``` *** ## Java IO模型系列教程-第八章-AIO编程的应用实例 ### 八、AIO编程 #### 8.1 AIO编程简介 ##### 8.1.1 AIO编程概述 AIO也叫异步非阻塞,JDK1.7之后的新特性,AIO引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 与NIO模型不同,读写操作为例,只需直接调用read和write的API即可,这方法都是异步的对于读操作,当有流可读时,系统会将可读的流传入到read方法的缓冲区,并通知应用程序读写都是异步的,完成之后会主动调用回调函数; AIO需要操作系统的支持,在Linux内核2.6版本之后增加了对真正异步IO的实现。Java从JDK1.7之后支持AIO,JDK1.7新增一些与文件/网络IO相关的一些API,称之为NIO2.0或者称之为AIO(Asynchronous IO)。AIO最大的特征提供了异步功能,对于socket网络通信和文件IO都是起作用的。 ##### 8.1.2 AIO与其他IO的区别 AIO是异步非阻塞IO,AIO实现了真正意义上的异步处理。 - 异步:当在处理一个请求的同时,还可以同时处理其他请求,并且当请求处理完毕时(开始响应数据),基于事件回调的方式将数据响应给调用者,调用者不需要再次来到提供者这边读取数据,而是直接读取调用者中用户空间中的数据,完完全全的做到了异步处理; - 非阻塞:在AIO中,调用任何的请求连接、读取提供者数据等方法都不会造成当前线程阻塞,而是将请求告诉给了服务提供者,自己可以去处理其他任务;服务提供者接收到请求后,实现自身的业务逻辑处理,然后将数据数据发送给调用者; BIO(同步阻塞):
NIO(同步非阻塞):
AIO(同步非阻塞):
- BIO同步阻塞:到理发店理发,理发店人多,就一直等理发师,知道轮到自己理发 - NIO同步非阻塞:到理发店理发,发现理发店人很多,就先告诉理发师说一会再来,自己先去干其它事情,一会再回来看看是否轮到了自己 - AIO异步非阻塞:给理发师打电话,让理发师上门服务,自己干其它事情,理发师自己来家里理发 *** #### 8.2 实现AIO编程 在Java中,实现AIO编程的主要有AsynchronousFileChannel、AsynchronousServerSocketChannel、AsynchronousSocketChannel等三个异步通道类;使用read、accept等阻塞方法时,异步通道则是基于事件回调的方式来处理阻塞问题; 即:当调用阻塞方法时不再阻塞当前线程,并且当数据提供者响应数据时将,数据请求方将会自动将操作系统内核所获取到的数据复制到用户空间,供用户提供者使用; ##### 8.2.1 AIO基于文件编程 1)**基于Future-读** - 示例代码: ```java package com.aio.demo01_asynchronousFileChannel; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousFileChannel; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.concurrent.Future; /** * @author lscl * @version 1.0 * @intro: */ public class Demo01_通过Future读取数据 { public static void main(String[] args) throws Exception { Path path = Paths.get("001.txt"); // 创建一个异步的FileChannel对象,设置为读模式 AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); // 创建Buffer来接收AsynchronousFileChannel的数据 ByteBuffer buffer = ByteBuffer.allocate(1024); // 进行异步读取 Future future = fileChannel.read(buffer, 0); /* 如果isDone()返回true,代表内核空间已经准备好了数据,并且已经将内核的数据拷贝到用户空间了 返回false,则代表内核空间暂时没有数据,或者数据还没有从内核拷贝到用户空间 */ while (!future.isDone()) ; // limit=position,position=0 buffer.flip(); System.out.println(new String(buffer.array())); // position=0,limit=capacity buffer.clear(); // 可以获取到读取的字节数 Integer count = future.get(); System.out.println("读取完毕,共读取到【" + count + "】个字节"); } } ``` 2)**基于CompletionHandler-读** - 示例代码: ```java package com.aio.demo01_asynchronousFileChannel; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousFileChannel; import java.nio.channels.CompletionHandler; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; /** * @author lscl * @version 1.0 * @intro: */ public class Demo02_通过CompletionHandler读取数据 { public static void main(String[] args) throws Exception { Path path = Paths.get("001.txt"); // 创建一个异步的FileChannel对象,设置为读模式 AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); ByteBuffer buffer = ByteBuffer.allocate(1024); // 进行异步读取 fileChannel.read(buffer, 0, buffer, new CompletionHandler() { /** * 读取完毕时调用的方法 * @param result: 读取的字节数量 * @param attachment: 附着的信息,该参数等于fileChannel调用read时传递的buffer(第三个参数的buffer) */ @Override public void completed(Integer result, ByteBuffer attachment) { // true System.out.println(buffer == attachment); // limit=position,position=0 buffer.flip(); System.out.println(new String(buffer.array())); // position=0,limit=capacity buffer.clear(); } /** * 读取失败时调用的方法 * @param exc: 出现的异常 * @param attachment: 外面传递的那个buffer对象 */ @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } }); System.out.println("程序执行完毕..."); } } ``` 3)**基于Future-写** - 示例代码: ```java package com.aio.demo01_asynchronousFileChannel; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousFileChannel; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.concurrent.Future; /** * @author lscl * @version 1.0 * @intro: */ public class Demo03_通过Future写取数据 { public static void main(String[] args) throws Exception { Path path = Paths.get("001.txt"); // 创建一个AsynchronousFileChannel,对与该类来说,每次运行不会清空文件,而是每次运行都会把本次的内容覆盖文件之前的内容 AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( path, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING // 如果文件里面有内容那么就清空数据 ); ByteBuffer buffer = ByteBuffer.wrap("abc".getBytes()); Future future = fileChannel.write(buffer, 0); System.out.println("写出【" + future.get() + "】个字节"); fileChannel.close(); } } ``` 4)**基于CompletionHandler-写** - 示例代码: ```java package com.aio.demo01_asynchronousFileChannel; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousFileChannel; import java.nio.channels.CompletionHandler; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; /** * @author lscl * @version 1.0 * @intro: */ public class Demo04_通过CompletionHandler写取数据 { public static void main(String[] args) throws Exception { Path path = Paths.get("001.txt"); // 创建一个AsynchronousFileChannel,对与该类来说,每次运行不会清空文件,而是每次运行都会把本次的内容覆盖文件之前的内容 AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open( path, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING // 如果文件里面有内容那么就清空数据 ); ByteBuffer buffer = ByteBuffer.wrap("abc".getBytes()); fileChannel.write(buffer, 0, null, new CompletionHandler() { @Override public void completed(Integer result, ByteBuffer attachment) { System.out.println("写出【" + result + "】个字节"); } @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } }); System.out.println("写入完毕...."); // 让程序不要这么快结束(等待completed回调函数执行) Thread.sleep(100); } } ``` ##### 8.2.2 AIO基于网络编程 1)**服务端** - 示例代码: ```java package com.aio.demo01_asynchronousFileChannel; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; /** * @author lscl * @version 1.0 * @intro: */ public class Demo05_基于服务器的AIO通信模式_服务端 { public static void main(String[] args) throws Exception { // 创建一个异步的ServerSocketChannel AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(); // 绑定IP和端口 serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8888)); /* 接收一个客户端 该方法不会阻塞当前线程,而是基于事件回调的形式,当接收到客户端时执行回调函数把接收到的客户端传递进去 */ serverSocketChannel.accept(null, new CompletionHandler() { /** * 接收成功执行的回调 * @param socketChannel: 客户的Channel * @param attachment: 添加的附着信息 */ @Override public void completed(AsynchronousSocketChannel socketChannel, Object attachment) { ByteBuffer buffer = ByteBuffer.allocate(1024); /* 使用AsynchronousSocketChannel进行读取 该方法并不会阻塞当前线程,而是基于事件回调的形式,当客户端有数据可读时,触发回调函数 */ socketChannel.read(buffer, null, new CompletionHandler<>() { @Override public void completed(Integer result, Object attachment) { // limit=position,position=0 buffer.flip(); try { System.out.println("接收到了来自客户端【" + socketChannel.getRemoteAddress() + "】发送的信息: " + new String(buffer.array())); } catch (IOException exception) { exception.printStackTrace(); } // position=0,limit=capacity buffer.clear(); } @Override public void failed(Throwable exc, Object attachment) { } }); } /** * 接收失败的回调 * @param exc * @param attachment */ @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); // 让程序不会结束 System.in.read(); } } ``` 2)**客户端** - 示例代码: ```java package com.aio.demo01_asynchronousFileChannel; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; /** * @author lscl * @version 1.0 * @intro: */ public class Demo06_基于服务器的AIO通信模式_客户端 { public static void main(String[] args) throws Exception { // 获取一个异步的SocketChannel AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open(); /* 连接到服务器 该方法不会阻塞, */ socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888), null, new CompletionHandler() { /** * 连接服务器成功执行的方法 * @param result * @param attachment */ @Override public void completed(Void result, Object attachment) { ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("hello".getBytes()); // limit=position,position=0 buffer.flip(); // 将数据发送到服务器 socketChannel.write(buffer); // position=0,limit=capacity buffer.clear(); } @Override public void failed(Throwable exc, Object attachment) { } }); // 让程序不会结束 System.in.read(); } } ``` ***