NIO以及AIO不适合基础不好的同学学习,需要熟练Java OOP编程,具有一定编程思维,熟悉java多线程编程,熟悉Java IO流编程,Java网络编程,还要熟悉常用Java设计模式 |
---|
BIO大部分人已经接触,简单介绍一些,重点会放在NIO的讲解 |
---|
- Java软件开发中,通信架构不可避免,不同系统或不同进程间数据交互,或者高并发下的通信场景下都需要网络通信技术,例如游戏
- 早期网络通信架构有一些缺陷,最让人恼火的就是基于
性能低下的同步阻塞式I/O通信BIO(底层基于IO)
- 随着互联网发展,通信性能要求变高,java在2002年开始支持
非阻塞式I/O通信技术NIO(底层基于缓冲区)
- 局域网内的通信
- 多系统间的底层消息传递机制
- 高并发下,大数据量的通信场景
- 游戏行业,手游服务端,大型网络游戏
- 规定使用何种通道或通信模式和架构进行数据传输。
Java共支持3中网络编程I/O模型BIO、NIO、AIO
不同业务场景和性能需求下,选择不同I/O模型
- 服务器实现模式为一个连接一个线程,当客户端有连接请求时,服务器就得启动一个线程处理。如果这个连接不做任何事,就会造成不必要线程开销。

- 适用于连接数较小且固定的架构,对服务器资源要求较高,并发局限于应用中,JDK1.4前唯一的选择,程序简单
- 一个线程处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器(选择器)上,多路复用器轮询到连接,如果当前轮询的连接有I/O请求就进行处理(开一个线程处理),如果没有请求,它不会等待,而BIO会一直阻塞等着

- 适用于连接数多且连接较短(轻操作)的架构,例如聊天服务器,弹幕系统,服务器间通讯等,编程较为复杂,JDK1.4开始支持
- 一个有效请求一个线程,客户端的I/O请求都由OS先完成了以后,再通知服务器应用去启动线程进行处理
- 适用于连接数多且连接较长(重操作)的架构,如相册服务器,充分调用OS参与并发操作,编程较为复杂,JDK7开始支持
零、Linux系统底层到JavaBIO/NIO
说白了,IO调用的都是底层Linux的方法,例如读方法read()需要传参fd(文件描述符),然后对文件进行读操作,如果socket是阻塞的,那么会有一个标识,我们调用read()时,需要人为帮他阻塞,如果是非阻塞的,我们调用read()时,不用帮他阻塞(当然还需要诸如select等内核帮助)。这些事JVM帮我们做了,它会通过BIO和NIO两种技术,帮我们完成阻塞和非阻塞的操作
1、IO和BIOLinux底层
1. 为什么IO是计算机的瓶颈
- 内存的时间单位是纳秒级别,而I/O(网卡,磁盘寻址,输入输出)是毫秒级别
- 1s = 1000ms =1000000vm= 1000000000ns(1毫秒=1000微秒=1000000纳秒)
- 所以差一点的内存比好一点的I/O设备快几十万倍,所以IO是计算机瓶颈
2. Socket是阻塞的么?每个accept连接都是新的文件对象么?
- Socket既可以阻塞,也可以非阻塞,每个Socket连接,都是一个新的文件对象,产生新的文件描述符
- 上面我虽然这么说,但是大家也不知道是不是真的,接下来看一下Linux源码,验证我上面的说法
- 通过Linux命令man 2 socket,可以看到,它给出了非阻塞的参数定义,也就是socket可以非阻塞


- 通过命令 man 2 bind,我们看example,实例程序,是C的代码,但大家应该能看懂
- 可见它main方法中,上来就调用了socket,返回值是int型的sfd,也就是文件描述符,然后bind绑定端口,listen开启监听(基于文件描述符),当开启监听后,需要accept开启连接,当开启连接后,生成全新文件描述符cfd
- 也就是说,每个新的连接,都生成一个新的文件对象


3. 什么是BIO呢?
- 把前面的东西理解了,就完全没问题了。Linux生成新的文件描述符,是因为Linux万物皆文件,而JAVA万物皆对象。
- 将Linux文件换成对象,就是一个Socket连接是一个对象,每个accept连接,又是一个新对象。所以每个accept连接互不冲突。
- BIO就是阻塞IO,也就是系统底层参数为默认的socket,上面我们说了,socket也可以非阻塞
- 也就是说,JAVA也不过是调用系统内核的东西,只不过JAVA还有JVM帮他,所以JAVA可以抽象为面向对象,实际还是JVM再调用Linux底层的东西。
所以你可以这样和面试官说,JAVA BIO就是通过JVM调用系统内核BIO的东西
,首先,开启一个socket连接,对应一个文件描述符,有了socket连接后,进行bind绑定,绑定到端口,然后开启Listen监听,监听指定文件描述符,然后开启accept连接,开启一个accept连接,会生成一个新的文件描述符,把文件概念缓存对象,就是Java的BIO
2、Linux底层如何阻塞和非阻塞
4. Linux如何完成阻塞?
- 当我们没有指定socket不阻塞后,就是Linux中没有指定SOCK_NONBLOCK参数为非阻塞,也就默认socket是一个阻塞的连接
- JAVA中直接使用socket套接字,accept连接时,就是阻塞的。也就是默认的,并不是说Socket是阻塞的。
3.Linux 内核底层的read读方法
,需要传参连接的文件描述符fd
,缓存区大小,文件总大小,我们知道,socket可以用SOCK_NONBLOCK参数
进行设置socket是阻塞的。规定生成阻塞的或非阻塞的socket连接文件描述符
,如果是阻塞的
,read方法执行前,如果没有可以读取的内容,会帮他卡住,也就是阻塞住

5. Linux如何完成非阻塞?
- 如何调用Linux的read()方法时,文件描述符fd是非阻塞的socket连接
- 那么它在read时,如果没有东西,立即返回结果,不进行阻塞
- 什么时候我们开心了想起来了,再去read()一下,如果此时有需要读的东西,就读。也就是我不等你,你来了,你等我一会。
- 非阻塞就是一种思想,假设它只开一个线程,进行监听,不断监听fd 6,当监听到后,有连接过来,它不断遍历,通过read()方法轮询fd 8 和 fd 9 ,有操作(读,写)过来,就进行处理,没有,就继续轮询,这样就不用阻塞了。

- 上面这种结构有很大缺陷,假设1000个客户端连接,当有一个连接突然要发送数据,我们可能需要调用999次read()方法,才能去处理
- 而最好能够减少和内核交互(read())的次数,并且及时知道有操作过来,把1000次询问,变成1次,可以节省大量资源。如果可以把文件描述符拿到,然后及时知道想读想写就好了。
- 所以Linux提供了新的系统调用,select,通过man 2 select命令查看,JAVA模型当中,对应selector选择器对象
- 首先它的参数nfds,表示可以传很多个文件描述符,而指针fd_set *readfds表示想读的文件描述符,writefds表示想写的文件描述符,轮询时将相应的文件描述符放进去。我们调用方,通过指针可以获取到想读的,想写的
- 它会轮询这些文件描述符,判断它们的操作状态,然后返回文件描述符的个数

2、JAVA 的 BIO和 NIO
javaBIO和NIO包括普通IO,都是对底层操作系统内核系统处理的封装,我们写的代码,都是一条条指令,调用操作系统指令,所以我们要先了解系统底层的逻辑和指令,然后看JAVA如何进行面向对象的封装
6. BIO一般搭配多线程使用,为什么?
- 假设服务端tomcat监听8080端口,文件描述符为6
- 当同时两个客户端,通过与服务端Linux内核三次握手,建立TCP连接后
- 双方开始分配资源,来处理连接,此时两个客户端各自分配好了资源
- 假设服务端只分配一个线程资源,但是连接不会建立一个,假设两个连接都建立完成,文件描述符分别为8和9
- 此时,tomcat只有一个线程,阻塞监听fd 8的读,但是此时fd 8 没有立即发送数据,或者发送的数据太大
- 那么fd 9就必须一直等着处理fd 8的线程执行结束,再来处理自己的请求。

- 而有了多线程,第一个线程处理fd 8,第二个线程处理fd 9就好了,但是如果两个客户端都不发数据,两个线程依然会阻塞住

7. JAVA NIO如何实现非阻塞?请结合系统底层原理阐述!
- BIO和NIO是阻塞IO和非阻塞IO的简写,是JAVA中或者说编程语言中的概念,底层调用的还是socket的read()方法
- 但是NIO和BIO不同之处在于,
NIO使用了操作系统的select系统调用
,它可以同时处理多个文件描述符
,轮询它们的操作状态
,然后返回
可以进行操作的文件描述符个数
。此方法执行一次轮询一次
,本身不阻塞
- 需要我们
将多个文件描述符传入
- 而
select的writefds和readfds两个指针,会保存想要读,写操作的文件描述符
,当然还有其它操作 - 最终,还会
返回需要处理操作的文件描述符的个数
- 但是select只是负责轮询,看看你给我的这些文件描述符,谁想写,谁想读,如果有新连接过来,不归我管
- 所以我们需要外部调用select的程序,自己实现这样功能,
JAVA通过Channel通道来处理整个IO请求,而对select的调用和封装,通过Selector完成
- 所以,
Java就是抽象了系统底层
- 首先启动socket,进行端口绑定,这里java抽象出了通道Channel的概念,
我们所有的NIO操作都通过Channel进行,包括启动socket服务端监听
- 然后需要
指定操作哪些文件描述符了
,java抽象了selector选择器
,我们需要将连接注册到select
,也就是文件描述符给select,并且java可以指定选择器轮询时,关注的操作有哪些,通过register()方法注册
时可以指定,比如下图OP_ACCEPT就是关注监听连接事件。注意只是注册,并没有调用底层select方法
- 通俗点讲,bind相当于启动一个socket连接文件描述符,绑定到Channel通道,然后socket连接文件描述符就需要保存起来,通过ss.refister()保存
知道有哪些文件描述符,就可以调用select了
,通过selector.select()调用操作系统select,然后可以得到select()方法返回值
,也就是需要操作的文件描述符的个数
,然后判断是否有需要操作
的东西,没查到就一直去调用select()
,直到有操作如果有操作,获取操作集,看看是什么操作,然后执行相应的操作
。假设是建立连接操作
- 我们知道
建立一个socket连接,会生成新的文件描述符
,而NIO都在通道中完成操作
,所以通过key.channel()可以拿到新的socket通道
,然后调用accept()生成socket连接
,获取SocketChannel
- 而我们前面也说过,
socket可以指定阻塞或非阻塞
,java通过sc.configureBlocking(false)
指定当前通道封装的socket连接,是非阻塞状态
- 然后
我们可以选择把它放到selector中
,sc.refister注册进去,关注读操作。 - 当前循环结束后,
下一次while(true),又会进行selector.select()的调用
,此时新加入的文件描述符,也在文件描述符集合中,作为select的参数,调用select,回到上面的第4步



一、Java BIO
- 传统java io编程,相关类和接口在java.io包中
- BIO(blocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要线程开销,可以通过线程池机制改善(实现多个客户连接服务器)

1. 何为同步阻塞
- 案例中,服务端会一直等待客户端消息,如果客户端没有进行消息的发送,服务端会一直阻塞
- 服务端按照任何方式接收消息(例如按行接收),客户端也必须按照相同方式发送(也得按行),否则服务端可能无法正常收到消息,或者直接抛出异常
- 服务端可以反复接收消息,客户端也可以反复发送消息
- 当服务端accept()方法接收到连接后,将会一直和他建立连接,直到再次执行accept()方法阻塞,才会与下一个客户端建立连接
- 所以如果想要服务端处理多个客户端请求,就需要引入线程,每一个客户端请求,都由一个线程处理
- 伪异步IO,采用线程池,避免为每个请求都创建一个独立线程造成线程资源耗尽的问题,但由于底层依然使用同步阻塞BIO模型,无法根本上解决问题
- 如果单个消息处理缓慢,或者服务器线程池中的全部线程都被阻塞,后续socket的IO消息都将在队列中排队,新的Socket请求将被拒绝,客户端会发生大量连接超时。
2. 端口转发思想:群聊功能实现
源码:src/main/java/bio/instand_messaging包下,大家自己看下就行了,很简单,了解基本思路即可 |
---|
实现一个客户端消息可以发送给所有客户端接收(群聊系统): |
---|

- 客户端登录功能:启动客户端登录,需要输入用户名和服务端ip地址
- 在线人数实时更新:客户端登录后,同步更新所有客户端联系人信息栏
- 离线人数更新:检测客户端下线后,同步更新所有客户端联系人信息栏
- 群聊:任意一个客户端消息,推送给当前所有客户端接收
- 私聊:选择某个客户端,点击私聊,发出信息可以被该客户端单独接收
- @消息:发出消息@某客户端,其它人都能知道
- 消息用户和消息时间点:服务端可以实时记录用户的消息时间点,进行消息的多路转发或选择
- 开启服务端

- idea开启客户端并发执行


- 开启3个客户端,分别为kk,小刘,小红(ip地址全写127.0.0.1)

- 随便使用一个客户端发送bbbb,所有人都能看见

- 选择一个人,发送@消息,其它人都可以看见

- 选中一个人,私聊

- 服务端,用容器保存已登录socket,根据容器,将消息推送给指定socket客户端
二、Java NIO
New IO(新IO)或Non Blocking IO(非阻塞IO),JDK1.4引入,支持面向缓冲区、基于通道的IO操作,NIO以更高效的方式进行文件读写操作 |
---|
- BIO,阻塞住,等着客户端消息,一个线程与客户端连接,会一直等着它完成,无论它是不是有IO操作,没有任何操作,也会一直占着线程
- NIO,通过选择器,不断轮询调度,谁有IO请求,就找个线程处理谁,如果客户端没有操作,不会浪费资源再它身上
- IO事件本身不阻塞,但获取IO事件的select()方法需要阻塞
- BIO会阻塞在IO操作上,NIO阻塞在事件获取上,没有事件就没有IO,从高层的角度看IO就不阻塞了
- NIO本质是延迟IO操作到真正发生IO的时候,而不是BIO那种,只要IO流打开就一直等待IO操作

NIO三大核心部分,NIO有很多类和组件,但3大核心构成了核心API,其它组件Pipe和FileLock,只是与三大核心组件共同使用的工具类 |
---|
- Channels通道
- Channel和IO中Stream(流)差不多一个等级,不过Stream是单向的,例如InputStream或OutputStream。
- Channel是双向的,既可以进行读操作,又可以进行写操作
- NIO中channel主要实现有FileChannel(文件IO)、DatagramChannel(UDP)、SocketChannel(TCP,Client)和ServerSocketChannel(TCP,Server)
- Buffers缓冲区
- 关键Buffer实现有:ByteBuffer(byte)、CharBuffer(char)、DoubleBuffer(double)、FloatBuffer(float)、IntBuffer(int)、LongBuffer(long)、ShortBuffer(short)
- Selectors选择器
- 单线程处理多个Channel,当你的应用打开多个通道,但每个连接流量都很低,使用Selector就很方便
- 例如聊天服务器中,使用Selector,得向Selector注册Channel,然后调用它的select()方法,这个方法会一直阻塞,直到某个注册的通道有事件就绪。
- 一旦这个方法返回,线程就可以处理这些事件
1. File通道
1. 概述
- 可以通过Channel读取和写入数据,就像水管一样,网络数据通过Channel读取和写入
- 通道(Channel)与流(Stream)不同,通道是双向的,流只能一个方向(InputStream或OutputStream子类),通道可以读、写或同时读写,通道是全双工的,比流更好地映射底层操作系统API
- Channel封装了对数据源的操作,通过channel可以操作数据源,但不必关心数据源的具体物理结构。这个数据源可能是多种的(文件,网络socket)
- 大多数应用中,channel与文件描述符或socket是一一对应的。channel用于在字节缓冲区和位于通道另一侧的
实体(通常是一个文件或套接字)
之间有效传输数据。也就是说,它是缓冲区和实体的通道,缓冲区-------channel--------实体
。

- 两个方法,isOpen()返回通道是否打开,true表示打开,close()关闭通道
- 与缓冲区不同,channel的API主要由接口指定。不同操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。因此,通道实现经常使用操作系统的本地代码。channel接口允许以一种受控且可移植的方式来访问底层I/O服务。
- channel是一个对象,可以通过它读取和写入数据,就像流,所有
数据都通过Buffer对象处理
,永远不会将字节直接写到通道中,相反,数据将写入包含一个或多个字节的缓冲区中
,读取字节时,也是将数据从通道读入缓冲区,再从缓冲区获取字节
channel可以异步地读写
- FileChannel(文件IO):从文件中读写数据
- DatagramChannel(UDP):能通过UDP读写网络中的数据
- SocketChannel(TCP Server):能通过TCP读写网络中的数据
- ServerSocketChannel(TCP Client):可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
2. FileChannel(文件IO)
提供针对文件的常用读写操作和scatter/gather操作(设置/获取),同时提供很多专用于文件的新方法 |
---|
描述(🍅:不常用/🏆:常用) | 方法 |
---|
🏆从Channel中读取数据到ByteBuffer | int read(ByteBuffer dst) |
🏆将Channel中的数据"分散"到ByteBuffer[] | long read(ByteBuffer[] dsts) |
🏆将 ByteBuffer 中的数据写入到Channel | int write(ByteBuffer src) |
🏆将ByteBuffer[] 中的数据"聚集"到Channel | long write(ByteBuffer[] srcs) |
🍅返回此通道的文件位置 | long position() |
🍅设置此通道的文件位置 | FileChannel position(long p) |
🍅返回此通道的文件的当前大小 | long size() |
🍅将此通道的文件截取为给定大小 | FileChannel truncate(long s) |
🍅强制将所有对此通道的文件更新写入到存储设备中 | void force(boolean metaData) |
- 将数据写入缓冲区
调用buffer.flip()反转读写模式
,上面都是将数据读入到缓冲区,这里转换,就可以直接写,而不用再建一个输出流- 从缓冲区读取数据
- 调用buffer.clear()或buffer.compact()清除缓冲区内容
1. 入门案例
使用FileChannel步骤1:打开FileChannel |
---|
- FileChannel无法直接打开,需要通过一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
使用FileChannel步骤2:读取数据,写同理 |
---|
- channel只是通道,读数据需要缓冲区Buffer,读到Buffer中
- FileChannel.read()方法,将数据从channel读取到Buffer中
- read()方法返回int值,表示读取多少字节到Buffer中,返回-1表示读完了
ByteBuffer buf = ByteBuffer.allocate(1024);
int byteCount = channel.read(buf);
案例,向E:/1.txt文件进行读写操作:src/main/java/nio/channel/FileChannelTest.java |
---|

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
public class FileChannelTest {
public static void main(String[] args) {
FileChannelTest fileChannelTest = new FileChannelTest();
try {
fileChannelTest.test();
fileChannelTest.test2();
} catch (IOException e) {
e.printStackTrace();
}
}
public void test() throws IOException{
System.out.println("===========================================数据写入开始================================");
RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");
FileChannel channel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
String str = new String("贼爽,这是NIO中FileChannel要写入的内容1123444alsdkfjlaksdf");
buf.clear();
buf.put(str.getBytes(StandardCharsets.UTF_8));
buf.flip();
System.out.println("buf.flip():切换读写模式,上面都是将数据读入到缓冲区,这里转换,就可以直接写,而不用再建一个输出流");
while(buf.hasRemaining()){
channel.write(buf);
}
buf.clear();
channel.close();
aFile.close();
System.out.println("===========================================数据写入完成================================");
}
public void test2() throws IOException {
System.out.println("===========================================数据读取开始================================");
RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");
FileChannel channel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int byteCount = 0;
while((byteCount = channel.read(buf))!=-1){
System.out.print("读取内容:"+new String(buf.array(),0,byteCount));
buf.flip();
System.out.println("buf.flip():切换读写模式,上面都是将数据读入到缓冲区,这里转换,就可以直接写,而不用再建一个输出流");
while(buf.hasRemaining()){
System.out.print(buf.get());
}
buf.clear();
}
aFile.close();
System.out.println("========读取数据完成=======");
}
}
2. 常用方法
1. position() 方法:当需要在FileChannel的某个特定位置进行数据读写,可通过position()方法获取FileChannel的当前位置 ,也可以调用position(long pos)方法设置FileChannel的当前位置 |
---|
- 如果将位置设置在文件结束符后面,然后试图从文件通道中读取数据,读方法将返回-1
- 如果是写数据,文件将撑大到当前位置并写入,可能导致文件空洞,导致磁盘上物理文件中写入的数据间有空隙
long pos = channel.position();
channel.position(pos+123);
long fileSize = channel.size();
3. truncate() 方法:截取一个文件,截取时,文件中指定长度的后面的部分将被删除 |
---|
channel.truncate(1024);
4. force() 方法:通道里尚未写入磁盘的数据强制写到磁盘上 |
---|
- 为了性能,操作系统会将数据缓存到内存,所以无法保证写入到FileChannel中的数据一定会即时写到磁盘上,只需要调用force()方法
- force()方法有一个boolean类型参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
5. transferTo()和transferFrom()方法 :如果两个通道中,有一个是FileChannel,那就可以直接将数据从一个channel传输到另一个channel |
---|
3. transferFrom()方法和transferTo()方法
transferFrom():表示从…转移,将其它通道的东西,搞自己这里来 |
---|
- 将数据从源通道传输到FileChannel中(JDK中解释:将字节从给定可读取字节通道传输到此通道的文件中)
transferTo():表示转移到…,将自己的东西,搞别人那 |
---|
1.将数据从FileChannel传输到其它channel中

public void test3()throws IOException{
RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("E:/2.txt", "rw");
FileChannel toChannel = bFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel,position,count);
aFile.close();
bFile.close();
System.out.println("E:/1.txt文件内容到E:/2.txt拷贝完成!!!!");
}
public void test4()throws IOException{
RandomAccessFile aFile = new RandomAccessFile("E:/1.txt", "rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("E:/3.txt", "rw");
FileChannel toChannel = bFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position,count,toChannel);
aFile.close();
bFile.close();
System.out.println("E:/1.txt文件内容到E:/3.txt拷贝完成!!!!");
}
2. Socket通道
1. 概述
- 可以运行非阻塞模式并且是可选择的,可以激活大程序(网络服务器和中间件组件)巨大的可升缩性和灵活性
- 再也没有为每个socket连接使用一个线程的必要了,避免管理大量线程所需的上下文交换开销。
- 借助新的NIO类,
一个或多个线程可以管理成百上千的活动socket连接。并且只有很少甚至没有性能损失。
- 所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了java.nio.channels.spi包中的AbstractSelectableChannel。
也就是说我们可以用一个Selector对象执行socket通道的就绪选择
DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,本身不传输数据。
通道
:连接I/O服务,并提供与该服务交换的方法,对某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议API
,但是java.net中已存在的socket通道都可以被大多数协议操作重复使用
- socket通道类,实例化时都会创建一个对等socket对象。都是java.net中的类(
DatagramChannel= = =>>>DatagramSocket
、SocketChannel = = =>>>Socket
、ServerSocketChannel= = = >>>ServerSocket
),它们已经被更新以识别通道,对等socket可以通过调用socket()方法
从一个通道上获取
。此外,这三个java.net类现有都有getChannel()方法。
依靠所有socket通道类的公有超级类SelectableChannel
。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。- 非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
- 设置或重新设置一个通道的阻塞模式是很简单的,只要
调用configureBlocking()方法即可,传递参数true则设为阻塞模式,参数值为false值设为非阻塞模式
。可以通过调用isBlocking()方法来判断某个socket通道当前处于哪种模式。
- AbstractSelectableChannel继承于SelectableChannel,其中configureBlocking()方法源码如下:如果你传true,表示阻塞模式,如果和当前模式一样,不做改变返回this,否则将当前模式改为true(阻塞)

2. ServerSocketChannel(TCP,Server)
ServerSocketChannel,对等socket对象是java.net.ServerSocket |
---|
基于通道的socket监听器(本身不传输数据,就是个监听器)
,和java.net.ServerSocket执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。- ServerSocketChannel
没有bind()方法
,必须取出对等的socket并使用它来绑定到一个端口开始监听连接
,我们使用ServerSocket的API来根据需要设置其他的socket选项 - 和java.net.ServerSocket一样,ServerSocketChannel也有accept()方法,一旦创建一个ServerSocketChannel并用对等socket绑定,就可以在其中一个上调用accept
- 如果选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象
- 如果选择在
ServerSocketChannel上调用accept()方法则会返回SocketChannel类型对象,这个对象可以在非阻塞模式下运行。
- 其它Socket的accept()方法会阻塞返回一个Socket对象,如果ServerSocketChannel以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null。检查连接但不阻塞的机制,充分体现可伸缩性,可选择性,降低复杂性
- 我们还可以使用一个选择器注册ServerSocketChannel对象以实现新链接到达时自动通知功能。
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.close()
ssc.configureBlocking(false);
- ServerSocketChannel上调用accept()方法监听新连接,当accept()方法返回时,会返回一个包含新连接的SocketChannel,
- 如果当前是阻塞模式,accept()方法会一直阻塞到有新连接到达,非阻塞模式,会直接自动向下执行
- 通常不仅仅只监听一个连接,一般while循环中调用accept()方法
SocketChannel sc = ssc.accept();
使用案例(非阻塞,阻塞的就是BIO了):src/main/java/nio/channel/ServerSocketChannelTest.java |
---|

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class ServerSocketChannelTest {
public static void main(String[] args) {
ServerSocketChannelTest serverSocketChannelTest = new ServerSocketChannelTest();
try {
serverSocketChannelTest.test();
} catch (Exception exception) {
exception.printStackTrace();
}
}
public void test() throws Exception {
int port = 8080;
ByteBuffer buffer = ByteBuffer.wrap("准备写的内容alkdsjfl234324k123sajdf".getBytes(StandardCharsets.UTF_8));
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(port));
ssc.configureBlocking(false);
while(true){
System.out.println("Waiting for connections");
SocketChannel sc = ssc.accept();
if(sc == null){
System.out.println("null,没有连接传入");
Thread.sleep(2000);
}else{
System.out.println("Incoming connection from:"+sc.socket().getRemoteSocketAddress());
buffer.rewind();
sc.write(buffer);
sc.close();
}
}
}
}
3. SocketChannel(TCP,Client)
SocketChannel,对等socket对象是java.net.Socket |
---|
- 连接到TCP网络套接字的通道,一种面向流连接sockets套接字的可选择通道,
SocketChannel是用来连接Socket套接字的
- 主要用来处理网络I/O的通道
- 基于TCP连接传输
- 实现可选通道,可以被多路复用
- 已存在的socket不能创建SocketChannel
- 提供open接口创建的Channel没有网络级联,需要用connect接口连接到指定地址
- 未进行连接的SocketChannel执行I/O操作时,会抛NotYetConnectedException
- SocketChannel支持两种I/O模式:阻塞式和非阻塞式
- SocketChannel支持异步关闭,当SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将直接返回-1,表示没有读取任何数据,如果SocketChannel在一个线程上write阻塞,另一个线程对SocketChannle调用shutdownWrite,则写阻塞线程叫抛出AsynchronousCloseException
- SocketChannel支持设定参数
- SO_SNDBUF 套接字发送缓冲区大小
- SO_RCVBUF 套接字接收缓冲区大小
- SO_KEEPALIVE 保活连接
- O_REUSEADDR 复用地址
- SO_LINGER 有数据传输时延缓关闭Channel(非阻塞模式下可用)
- TCP_NODELAY 禁用Nagle算法
创建SocketChannel两种方式(使用场景不同),一种直接建立TCP连接,另一种,不直接建立TCP连接,需要的时候调用connect进行连接 |
---|
SocketChannel sc = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
SocketChannel sc2 = SocketChannel.open();
sc2.connect(new InetSocketAddress("www.baidu.com", 80));
sc.isOpen();
sc.isConnected();
sc.isConnectionPending();
sc.finishConnect();
sc.configureBlocking(false);
sc
.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE)
.setOption(StandardSocketOptions.TCP_NODELAY,Boolean.TRUE);
sc.getOption(StandardSocketOptions.SO_KEEPALIVE);
sc.getOption(StandardSocketOptions.SO_RCVBUF);
4. DatagramChannel(UDP)
DatagramChannel,对等socket对象是DatagramChannel |
---|
SocketChannel模拟连接导向的流协议
(例如TCP/IP),而DatagramChannel模拟包导向的无连接协议
(例如UDP/IP)- 无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址。
- 也可以接收来自任意地址的数据包,每个到达的数据报都包含源地址(从哪来的)
打开DatagramChannel,下面例子,打开10086端口接收UDP数据包 |
---|
DatagramChannel dc = DatagramChannel.open();
dc.socket().bind(new InetSocketAddress(10086));
接收数据:receive()接收UDP包,SocketAddress可以获得发包的ip、端口等信息,用toString查看(格式:/127.0.0.1:57126) |
---|
ByteBuffer buffer = ByteBuffer.allocate(64);
buffer.clear();
SocketAddress receiveAddr = dc.receive(buffer);
System.out.println(receiveAddr.toString());
DatagramChannel dc = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes(StandardCharsets.UTF_8));
dc.send(sendBuffer,new InetSocketAddress("127.0.0.1",10086));
UDP不存在真正意义上的连接,这里的连接是向特定服务地址用read和write接收发送数据包 |
---|
dc.connect(new InetSocketAddress("127.0.0.1",10086));
int readSize = dc.read(sendBuffer);
dc.write(sendBuffer);
案例,模拟服务端和客户端:src/main/java/nio/channel/DatagramChannelTest.java |
---|
- 让发送端,发

- 接收端开启后,会一直接收

import org.junit.Test;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class DatagramChannelTest {
@Test
public void sendDatagram() throws Exception{
DatagramChannel sendChannel = DatagramChannel.open();
while(true){
ByteBuffer buffer = ByteBuffer.wrap("我是要发送的UPD报文".getBytes(StandardCharsets.UTF_8));
sendChannel.send(buffer,new InetSocketAddress("127.0.0.2",9999));
System.out.println("发送完成");
Thread.sleep(1000);
}
}
@Test
public void receiveDatagram() throws Exception{
DatagramChannel receiveChannel = DatagramChannel.open();
InetSocketAddress receiveAddress = new InetSocketAddress(9999);
receiveChannel.bind(receiveAddress);
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
while(true){
receiveBuffer.clear();
SocketAddress socketAddress = receiveChannel.receive(receiveBuffer);
System.out.println(socketAddress.toString());
receiveBuffer.flip();
System.out.println(Charset.forName("UTF-8").decode(receiveBuffer));
}
}
}
3. 通道的Scantter(分散)/Gather(聚集)
- 用于描述从Channel中读取或写入到channel的操作
- 分散(Scatter):从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,channel将读取的数据“分散(scatter)”到多个Buffer中
- 聚集(gather):写入channel指写操作时将多个buffer的数据写入同一个channel,因此,channel将多个buffer中数据“聚集(gather)”后发送到channel
- scatter/gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体
分散实例:Scattering Reads是指从一个channel读取到多个buffer中 |
---|

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.read(bufferArray);
聚集实例:Gathering Writes是指数据从多个buffer写入到同一个channel |
---|

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.write(bufferArray);
4. Buffer
- Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入通道中的

- 缓冲区
本质上是一块可以写入数据,然后可以从中读取数据的内存
。这块内存被包装成NIO Buffer对象,提供一组方法方便访问内存块,实际就是个容器(数组) - NIO库中,所有数据都是用缓冲区处理的
- 读取数据时,直接读到缓冲区,写入数据时,也是写入缓冲区,任何时候访问NIO中数据,都将它放到缓冲区中,而面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中
- NIO中,所有缓冲区类型都继承抽象类Buffer,常用的就是ByteBuffer,对应byte数据类型,基本每一个基本数据类型都要一个具体Buffer类型与之对应(下面是不完全类图)

- 写入数据到buffer
- 调用flip()方法切换读写转态
- 从buffer中读取数据
- 调用clear()方法(彻底清空)或compact()方法(只清除已读数据,未读的移到开头,新数据可以继续插入),清缓存区
1. Buffer的三个属性和类型
Capacity:Buffer的固定大小值
,Buffer是一个内存块,只能往里面写capacity个byte、long、char等类型数据。一旦Buffer满了,需要将其清空(读数据或调用clear()或compact())才能继续写数据Positon
写数据到Buffer中时
:position表示写入数据的当前位置
,初始为0,当一个byte、long…等数据写到Buffer后,position会后移到下一个可插入数据的Buffer单元,position最大可到capacity-1(因为初始为0)读数据到Buffer时
,position表示读入数据的当前位置
,如position = 2时表示已经开始读入3个byte,或从第三个byte开始读取。通过ByteBuffer.flip()切换到读模式时position会被重置为0
,当Buffer从position读入数据后,position会下移到下一个可读入的数据Buffer单元
limit
写数据时
,limit表示可对Buffer最多写入多少个数据
。写模式下,limit等于Buffer的capacity读数据时
,limit表示Buffer最多有多少可读数据(not null的数据)
- position 和 limit 的含义取决于Buffer处在读模式还是写模式,但是capacity无论读还是写模式,含义都是一样

- 上图中,写模式,limit表示最多写多少,position,表示可写入的位置
- 当切换为读模式后,写模式position的值就是limit的值,表示这个地方就是可读区域的界限,而position会重新变为0,表示当前读数据的位置
Buffer的类型:这些Buffer类型代表不同的数据类型,就是可以通过char,short,int,long,float或double类型来操作缓冲区的字节 |
---|
- ByteBuffer
- MappedByteBuffer:内存映射字节缓存,比普通的更快
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
2. Buffer分配和读写数据以及常用方法
- Buffer对象的获取,需要先分配,每一个Buffer类都有一个allocate方法
- 假如分配48字节capacity的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
- 下面是分配一个可存储1024字符的CharBuffer
CharBuffer cuf = CharBuffer.allocate(1024);
- 从Channel写到Buffer
int bytesRead = inChannel.read(buf);
- 通过Buffer的put()方法写到Buffer里,put方法有很多重载,例如写一个数据到指定位置,或直接把一个字节数组写到Buffer
buf.put(127);
Buffer切换读写模式,调用flip()方法即可 |
---|
- flip方法将Buffer从当前模式切换到另一种模式(读->写,写->读)
- 调用flip()方法会对position和limit进行相应操作变换,例如写切换到读后,position置为0,limit置为之前的position表示可读的界限位置
buf.flip();
- 从Buffer读取数据到Channel
int bytesWritten = inChannel.write(buf);
- 使用get()方法从Buffer中读取数据;get方法和put一样,有很多重载,可以指定positon读取,或从Buffer中读取数据到字节数组
byte aByte = buf.get();
描述(🍅:不常用/🏆:常用) | 方法 |
---|
🏆重置读写指针,将position设回0,可以重读或重写Buffer中数据。limit不变,依然表示读写的界限 | rewind() |
🏆重置缓冲区,position置为0,limit设置为capacity,表面上Buffer被清空,实际上数据并没有清除 | clear() |
🏆清除已读数据,将未读数据拷贝到Buffer起始处,position置为当前未读数据后面(可以插入数据的位置),limit置为capacity,适用于,数据没读完,但是想先写点 | compact() |
🏆标记一个特定position位置,之后可通过Buffer.reset()方法恢复到这个position | mark() |
🏆恢复到一个mark()方法标记的特定position位置 | reset() |
3. 缓冲区操作
1. 缓冲区分片
NIO中,slice()方法创建一个子缓冲区 ,除了分配或包装一个缓冲区对象外,还能根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于现有缓冲区的一个视图窗口 |
---|
- 就是将一个缓冲区,分成多个部分,我们可以对每个单独的部分,进行操作
- 将一个缓冲区颗粒化,提高效率
下面是分区的例子:src/main/java/nio/buffer/BufferForExample.java |
---|

import org.junit.Test;
import java.nio.ByteBuffer;
public class BufferForExample {
@Test
public void test1() throws Exception{
ByteBuffer buffer = ByteBuffer.allocate(10);
for(int i = 0;i<buffer.capacity();i++){
buffer.put((byte)i);
}
System.out.println("===========================输出整个缓冲区数据=============================");
buffer.position(0);
buffer.limit(buffer.capacity());
while(buffer.remaining()>0){
System.out.print(buffer.get()+" ");
}
System.out.println();
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
System.out.println("===========================改变子缓冲区内容,子缓冲区每个数据都*10:(3~7(不含7))=============================");
for(int i = 0;i < slice.capacity();i++){
byte b = slice.get(i);
b *= 10;
slice.put(i,b);
}
buffer.position(0);
buffer.limit(buffer.capacity());
System.out.println("===========================输出整个缓冲区数据=============================");
while(buffer.remaining()>0){
System.out.print(buffer.get()+" ");
}
}
}
2. 只读缓冲区
将缓冲区搞成只读的 ,只能读取,不能写入。可以通过调用缓冲区asReadOnlyBuffer()方法,将任何常规缓冲区转换为只读缓冲区 ,这个方法返回一个与原缓冲区完全相同的缓冲区 ,并与原缓冲区共享数据 ,只不过它是只读的 。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发送变化 |
---|
例子,只能读这个就不测试了,没有读的相关方法,测一测原缓冲区改变,只读缓冲区跟着变 |
---|

@Test
public void test2() throws Exception{
ByteBuffer buffer = ByteBuffer.allocate(10);
for(int i = 0;i<buffer.capacity();i++){
buffer.put((byte)i);
}
System.out.println("===========================输出整个缓冲区数据=============================");
System.out.println(Arrays.toString(buffer.array()));
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println("===========================原缓冲区下标为3位置的值改为7777=============================");
buffer.put(3,(byte)7777);
System.out.println("===========================输出只读缓冲区内容=============================");
for(int i = 0;i<buffer.capacity();i++){
System.out.print(readOnlyBuffer.get(i)+" ");
}
}
3. 直接缓冲区
用来加速I/O速度 ,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作 。它会在每一次调用底层操作系统本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中或从一个中间缓冲区中拷贝数据。要分配直接缓冲区 ,需要调用allocateDirect()方法而不是allocate()方法 ,使用 方法与普通缓冲区无区别 |
---|
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
ByteBuffer buffer = ByteBuffer.allocate(1024);
4. 内存映射文件I/O
读和写文件数据的方法,可以比常规的基于流或基于通道的I/O块的多 。并不是将整个文件读到内存,只有文件中实际读取或写入的部分才会映射到内存中 |
---|
//内存映射字节缓存MappedByteBuffer
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);
static private final int start = 0;
static private final int size = 1024;
@Test
public void test3() throws Exception{
RandomAccessFile raf = new RandomAccessFile("E:/1.txt","rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);
mbb.put(0,(byte)97);
mbb.put(1023,(byte)122);
raf.close();
}
5. Selector
注意: 与Selector一起使用,Channel必须处于非阻塞模式下,否则抛出IllegalBlockingModeException,而FileChannel不能切换到非阻塞模式,所以不能配合Selector使用,而套接字socket相关的通道都可以 |
---|
- 又名多路复用器,Java NIO核心组件之一,用于检查一个或多个NIO Channel的状态是否处于可读、可写
- 实现单线程管理多个channels,也就是单线程可以管理多个网络链接

- 如上图所示,使用更少的线程管理多个通道,对比使用多个线程的方式,避免了线程上下文切换带来的开销
SelectableChannel可选择通道(一个抽象类) |
---|
- 不是所有Channel都可以被Selector复用,比如FileChannel
判断一个Channel能被Selector复用
,前提是,channel是否继承SelectableChannel抽象类
,如果继承了,就可以被复用,否则不能。- SelectableChannel提供实现通道可选择性所需的公共方法,所有支持就绪检查的通道类的父类,所有socket通道都继承了SelectableChannel类,都是可选择的。包括从管道(Pipe,后面讲)对象中获得的通道,但是FileChannel,没继承SelectableChannel,所以不可选
一个通道可以被注册到多个选择器上,但是每个选择器只能被注册一次
,通道和选择器之间的关系,使用注册方式完成。SelectableChannel可以被注册到Selector对象上,注册时,需要指定通道的哪些操作,Selector感兴趣(只监听感兴趣的操作)
。

- 使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器,第一个参数,指定通道注册的选择器,第二个参数,指定选择器感兴趣的操作
- 选择器感兴趣的操作
- SelectionKey.OP_READ:可读
- SelectionKey.OP_WRITE:可写
- SelectionKey.OP_CONNECT:连接
- SelectionKey.OP_ACCEPT:接收
- 如果Selector对通道的多个操作感兴趣,可以用"位或"操作符实现,例如:
int key = SelectionKey.OP_READ|SelectionKey.OP_WRITE;
- 配置如上两个关注点后,当通道已经准备读或者写时,选择器就会监听到,然后做出反应
- 选择器查询的不是通道的操作,而是通道的某种就绪状态(准备好读,或准备好写等等)
- OP_ACCEPT:接收就绪状态
- OP_READ:读就绪状态
- OP_WRITE:写就绪状态
一个通道,并不是一定支持所有4中操作,比如ServerSocketChannel支持Accept连接操作,SocketChannle却不支持
通道的validOps()方法,可以获取通道下所有支持的操作集合
- Channel注册之后,一旦通道处于某种就绪状态,就可以被选择器查询到,
需要使用Selector的select()方法完成
,select方法的作用是对感兴趣的通道操作,进行就绪状态查询 Selector会轮询注册的Channel,并监听对每个通道感兴趣的状态,一旦有感兴趣的操作就绪,就会被Selector选中放入选择键集合中
- 一个选择键,首先是包含了注册在Selector的通道操作的类型,比如SelectionKey.OP_READ.也包含了特定的通道与特定的选择器之间的注册关系。
NIO编程,就是根据对应选择键进行不同业务逻辑处理
1. 基本使用方法
Selector selector = Selector.open();
@Test
public void test()throws Exception{
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(9999));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
- Selector的select()方法,可以查询出已经就绪的通道操作,这些就绪状态集合,存储在一个元素是SelectionKey对象的Set集合中,select()方法有多个重载
- select():阻塞到至少有一个通道在你注册的事件上就绪了
- select(long timeout):和select相同,但是最长阻塞时间为timeout毫秒
- selectNow():非阻塞,只要有通道就绪就立刻返回
- select方法返回值类型为int类型,表示当前有多少通道已就绪(不包括以前select方法统计的,只统计上次select执行之后,到本次select执行之间的),只要返回值不是0,我们就可以通过Selector中的selectedKeys()方法,迭代选择键集合,根据就绪操作类型,完成对应操作
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isAcceptable()){
}else if(key.isConnectable()){
}else if(key.isReadable()){
}else if(key.isWritable()){
}
iterator.remove();
}
停止选择方法:选择器执行选择的过程中,系统底层会依次询问每个通道是否已经就绪,这个过程可能造成调用线程进入阻塞状态,我们有以下方法唤醒在select()方法中阻塞的线程 |
---|
- wakeup()方法:通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回,强制选择器上的第一个还没返回的选择操作立即返回,如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回
- close()方法:通过close()方法关闭Selector,可以让任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有键都将被取消,但是Channel本身不会被关闭
2. 模拟服务端/客户端实例


@Test
public void server() throws Exception{
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1",8000));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector,SelectionKey.OP_ACCEPT);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("received".getBytes());
writeBuffer.flip();
while(true){
int nReady = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()){
SelectionKey key = it.next();
it.remove();
if(key.isAcceptable()){
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("isAcceptable,监听到连接就绪操作");
}else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuffer.clear();
socketChannel.read(readBuffer);
readBuffer.flip();
System.out.println("received 监听到读就绪操作,读到内容如下: " + new String(readBuffer.array()));
key.interestOps(SelectionKey.OP_WRITE);
}else if (key.isWritable()) {
readBuffer.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(readBuffer);
key.interestOps(SelectionKey.OP_READ);
System.out.println("isWritable 监听到写就绪操作,将读到内容写回去");
}
}
}
}
@Test
public void ClientDemo() throws Exception{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
writeBuffer.put("hello".getBytes());
writeBuffer.flip();
while (true) {
writeBuffer.rewind();
socketChannel.write(writeBuffer);
readBuffer.clear();
socketChannel.read(readBuffer);
readBuffer.flip();
System.out.println("服务端写过来的东西:"+new String(readBuffer.array(),0,readBuffer.limit()));
Thread.sleep(500);
}
}
6. Pipe和sink、source通道
- NIO管道是两个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

Pipe pipe = Pipe.open();
写入管道:需要sink通道(SinkChannel)然后调用sink通道的write()方法写入 |
---|
Pipe.SinkChannel sinkChannel = pipe.sink();
String newData = "New String to write to file..."+System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()){
sinkChannel.write(buf);
}
sinkChannel.close();
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);
while(bytesRead != -1){
System.out.print(new String(buf.array(),0,bytesRead));
sourceChannel.read(buf);
}
sourceChannel.close();
7. FileLock
- OS中很常见,多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题,给文件加一个锁,同一时间,只能有一个程序修改此文件,或多个程序只能读此文件但不能修改,解决同步问题。
文件锁是进程级别的,不是线程级别
。可以解决多个进程间并发访问,修改同一个文件问题,但不能解决一个进程内的多个线程并发同步问题。- 文件锁是当前程序所属JVM实例持有,一旦获取文件锁(对文件加锁),需要调用release()或关闭对应的FileChannel对象,或JVM退出才会释放这个锁
- 一旦某个进程(例如JVM实例)对某个文件加锁,释放锁之前,此进程不能再对此文件加锁,就是锁不重叠(进程级别不能重复在同一个文件上获取锁)
- 排它锁(独占锁):对文件
加排它锁后
,该进程可以对此文件进行读写,该进程独占此文件
,其他进程不能读写此文件,直到该进程释放文件锁。 - 共享锁:
某个进程对文件加共享锁后
,其他进程也可以访问此文件,但这些进程(包括自己)只能读,不能写,线程是安全的。只要还有一个进程持有共享锁,此文件只能读,不能写
。
- lock():对整个文件加锁,默认排它锁,阻塞式的,如果没有获取文件锁,会一直阻塞当前线程,直到获取文件锁
- lock(long position,long size,boolean shared):自定义加锁,前两个参数指定要加锁部分(可以只对文件部分内容加锁),第三个参数指定是否使用共享锁
- tryLock():lock()的升级版,尝试对整个文件加锁,默认排它锁,非阻塞式的,尝试获取文件锁,成功就返回锁对象,不成功就返回null,不会阻塞当前线程
- tryLock(long position,long size,boolean shared):自定义加锁
- 共享锁时,其它线程如果试图写此文件,会抛异常
FileLock的两个方法,某些OS(操作系统)上,对某个文件加锁后,不能对此文件使用通道映射 |
---|
- boolean isShared() :此文件是否是共享锁
- boolean isValid() :此文件锁是否还有效
FileChannel channel = new FileOutputStream("E:/1.txt").getChannel();
FileLock lock = channel.lock();
lock.release();
案例:src/main/java/nio/file_lock/FileLockTest.java |
---|

@Test
public void test1() throws Exception{
String input = "FileLockTest要写的内容";
System.out.println("input=====>>>>"+input);
ByteBuffer buffer = ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8));
String filePath = "E:/1.txt";
Path path = Paths.get(filePath);
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
long size = fileChannel.size();
fileChannel.position(size-1);
FileLock lock = fileChannel.lock();
System.out.println("是否是共享锁:"+lock.isShared());
fileChannel.write(buffer);
fileChannel.close();
System.out.println("写入完成!!!");
}
8. Path
Java Path接口是Java NIO更新的一部分,同Java NIO一起,包括在Java 6 和Java 7及以上版本中 |
---|
- 但是Java Path接口是在Java 7中添加到Java NIO中的,Path接口位于java.nio.file包中
- Java Path实例表示文件系统中的路径,可以指向文件或目录,路径绝对路径,相对路径都可以
- java.nio.file.Path接口类似于java.io.File类,但还是有差别的,许多情况下,可以使用Path接口替换File类的使用
创建Path(绝对路径):Paths.get()创建路径实例,静态工厂设计模式 |
---|
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("E:/1.txt");
创建Path(相对路径):Paths.get(basePath,relativePath)创建相对路径实例 |
---|
Path projects = Paths.get("E:/", "projects");
Path path1 = Paths.get("E:/", "projects/1.txt");
- 标准化,意味着移除所有路径字符串中间的.和…代码,并解析路径字符串所有引用的路径
String originalPath = "E:/projects/../aa-project";
Path path2 = Paths.get(originalPath);
System.out.println(path2);
Path normalize = path2.normalize();
System.out.println(normalize);
9. Files
Java NIO Files类(java.nio.file.Files) |
---|
- 提供几种操作文件系统中文件的方法,与java.nio.file.Path实例一起工作
Files.createDirectory():根据Path实例创建目录 |
---|
Path path = Paths.get("d:\\sgg");
try {
Path newDir = Files.createDirectory(path);
} catch(FileAlreadyExistsException e){
} catch (IOException e) {
e.printStackTrace();
}
Path sourcePath = Paths.get("E:\\test\\01.txt");
Path destinationPath = Paths.get("E:\\test\\002.txt");
try {
Files.copy(sourcePath, destinationPath);
Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING);
} catch(FileAlreadyExistsException e) {
} catch (IOException e) {
e.printStackTrace();
}
Files.move():移动文件,并且还会根据移动到的目标路径,改名 |
---|
Path sourcePath = Paths.get("d:\\test\\01.txt");
Path destinationPath = Paths.get("d:\\test\\001.txt");
try {
Files.move(sourcePath, destinationPath,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
Path path = Paths.get("d:\\test\\001.txt");
try {
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
Files.walkFileTree():递归遍历目录树功能,Path实例(要遍历的目录)和FileVisitor(遍历期间被调用)为参数 |
---|
- FileVisitor 是一个接口,必须自己实现 FileVisitor 接口,并将实现的实例传递给walkFileTree()方法。在目录遍历过程中,您的 FileVisitor 实现的每个方法都将被调用。如果不需要实现所有这些方法,那么可以扩展 SimpleFileVisitor 类,它包含FileVisitor 接口中所有方法的默认实现。
- FileVisitor 接口的方法中,每个都返回一个 FileVisitResult 枚举实例。FileVisitResult 枚举包含以下四个选项:
- CONTINUE 继续
- TERMINATE 终止
- SKIP_SIBLING 跳过同级
- SKIP_SUBTREE 跳过子级
Path rootPath = Paths.get("d:\\test");
String fileToFind = File.separator + "001.txt";
try {
Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws
IOException {
String fileString = file.toAbsolutePath().toString();
if(fileString.endsWith(fileToFind)){
System.out.println("file found at path: " + file.toAbsolutePath());
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
});
} catch(IOException e){
e.printStackTrace();
}
三、JAVA AIO
- Java 7中,Java NIO中加入AsynchronousFileChannel,可以异步地将数据写入文件
创建AsynchronousFileChannel:同样通过静态方法open()创建 |
---|
@Test
public void test1() throws Exception{
Path path = Paths.get("E:/i.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ,StandardOpenOption.WRITE);
}
1. 读取AsynchronousFileChannel数据
方法一:通过read()方法读取数据,返回Future对象,利用Future |
---|

@Test
public void test2() throws Exception{
Path path = Paths.get("E:/1.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
Future<Integer> operation = asynchronousFileChannel.read(buffer, position);
while(!operation.isDone());
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();
asynchronousFileChannel.close();
}
方法二:通过read()方法配合CompletionHandler内部类 |
---|

@Test
public void test3() throws Exception{
Path path = Paths.get("E:/1.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
asynchronousFileChannel.read(buffer, position, buffer,new CompletionHandler<Integer,ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("result = "+result);
attachment.flip();
byte[] data = new byte[attachment.limit()];
attachment.get(data);
System.out.println(new String(data));
attachment.clear();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
}
});
asynchronousFileChannel.close();
}
2. 写数据

@Test
public void test4() throws Exception{
Path path = Paths.get("E:/1.txt");
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
buffer.put("要写入的内容AIO写数据到AIO通道,Future".getBytes());
buffer.flip();
Future<Integer> operation = asynchronousFileChannel.write(buffer, position);
buffer.clear();
while(!operation.isDone());
System.out.println("Write over");
}
方法二:通过write()方法配合CompletionHandler内部类 |
---|

@Test
public void test5() throws Exception{
Path path = Paths.get("E:/1.txt");
if(!Files.exists(path)){
Files.createFile(path);
}
AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
buffer.put("AIO通道通过CompletionHandler写".getBytes());
buffer.flip();
asynchronousFileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("bytes written:"+result);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("Write Failed");
exc.printStackTrace();
}
});
asynchronousFileChannel.close();
}
四、字符集Charset
java中使用Charset表示字符集编码对象,常用静态方法如下: |
---|
描述(🍅:普通/🏆:静态) | 方法 |
---|
🏆通过编码类型获得 Charset 对象 | public static Charset forName(String charsetName) |
🏆获得系统支持的所有编码方式 | public static SortedMap<String,Charset> availableCharsets() |
🏆获得虚拟机默认的编码方式 | public static Charset defaultCharset() |
🏆判断是否支持该编码类型 | public static boolean isSupported(String charsetName) |
🍅获得 Charset 对象的编码类型(String) | public final String name() |
🍅获得编码器对象 | public abstract CharsetEncoder newEncoder() |
🍅获得解码器对象 | public abstract CharsetDecoder newDecoder() |
@Test
public void charSetEncoderAndDecoder() throws CharacterCodingException {
Charset charset=Charset.forName("UTF-8");
CharsetEncoder charsetEncoder=charset.newEncoder();
CharsetDecoder charsetDecoder=charset.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("字符集编码解码");
charBuffer.flip();
ByteBuffer byteBuffer = charsetEncoder.encode(charBuffer);
System.out.println("编码后:");
for (int i = 0;i < byteBuffer.limit();i++) {
System.out.println(byteBuffer.get());
}
byteBuffer.flip();
CharBuffer charBuffer1=charsetDecoder.decode(byteBuffer);
System.out.println("解码后:");
System.out.println(charBuffer1.toString());
System.out.println("指定其他格式解码:");
Charset charset1=Charset.forName("GBK");
byteBuffer.flip();
CharBuffer charBuffer2 =charset1.decode(byteBuffer);
System.out.println(charBuffer2.toString());
Map<String ,Charset> map = Charset.availableCharsets();
Set<Map.Entry<String,Charset>> set = map.entrySet();
for (Map.Entry<String,Charset> entry: set) {
System.out.println(entry.getKey() + "="+entry.getValue().toString());
}
}
五、Java NIO 聊天室综合案例
源码位置:src/main/java/nio_chat包下 |
---|



1. 服务端
- 通过选择器管理通道,当有客户端连接就绪,我们处理连接,给客户端输出“欢迎进入聊天室,请注意隐私安全”
- 当处理读就绪时(客户端发送消息),我们将其读出,然后广播给其它通道

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class ChatServer {
public void startServer() throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel =ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器已经启动成功了");
for(;;) {
int readChannels = selector.select();
if(readChannels == 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if(selectionKey.isAcceptable()) {
acceptOperator(serverSocketChannel,selector);
}
if(selectionKey.isReadable()) {
readOperator(selector,selectionKey);
}
}
}
}
private void readOperator(Selector selector, SelectionKey selectionKey)
throws IOException {
SocketChannel socketChannel =
(SocketChannel)selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readLength = socketChannel.read(byteBuffer);
String message = "";
if(readLength >0) {
byteBuffer.flip();
message += Charset.forName("UTF-8").decode(byteBuffer);
}
socketChannel.register(selector,SelectionKey.OP_READ);
if(message.length()>0) {
System.out.println(message);
castOtherClient(message,selector,socketChannel);
}
}
private void castOtherClient(String message, Selector selector, SocketChannel socketChannel) throws IOException {
Set<SelectionKey> selectionKeySet = selector.keys();
for(SelectionKey selectionKey : selectionKeySet) {
Channel tarChannel = selectionKey.channel();
if(tarChannel instanceof SocketChannel && tarChannel != socketChannel) {
((SocketChannel)tarChannel).write(Charset.forName("UTF-8").encode(message));
}
}
}
private void acceptOperator(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
socketChannel.write(Charset.forName("UTF-8").encode("欢迎进入聊天室,请注意隐私安全"));
}
public static void main(String[] args) {
try {
new ChatServer().startServer();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. 客户端
- 选择器监听读就绪
- 写的时候,直接给服务端写就好,服务端的选择器,监听读就绪后会处理
线程类:每个客户端需要一个线程处理,注意,客户端不是在我们服务器上运行的,而是用户电脑上,我们这里用一台电脑模拟 |
---|

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class ClientThread implements Runnable {
private Selector selector;
public ClientThread(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
try {
for(;;) {
int readChannels = selector.select();
if(readChannels == 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if(selectionKey.isReadable()) {
readOperator(selector,selectionKey);
}
}
}
}catch(Exception e) {
}
}
private void readOperator(Selector selector, SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel =
(SocketChannel)selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readLength = socketChannel.read(byteBuffer);
String message = "";
if(readLength >0) {
byteBuffer.flip();
message += Charset.forName("UTF-8").decode(byteBuffer);
}
socketChannel.register(selector, SelectionKey.OP_READ);
if(message.length()>0) {
System.out.println(message);
}
}
}

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;
public class ChatClient {
public void startClient(String name) throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
Selector selector = Selector.open();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
new Thread(new ClientThread(selector)).start();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()) {
String msg = scanner.nextLine();
if(msg.length()>0) {
socketChannel.write(Charset.forName("UTF-8").encode(name +" : " +msg));
}
}
}
public static void main(String[] args) throws IOException {
new ChatClient().startClient("爱国者");
}
}
3. 总结
- BIO每次连接,就需要启动一个线程,全程对其进行处理,尽管这个客户端连上后,什么也不干
- 但是NIO只需要一个选择器,和相关通道
- 当监听到感兴趣的就绪操作,才会对其进行处理,而不是一个线程全程连接客户端