java 1.4版本推出了一种新型的IO API,与原来的IO具有相同的作用和目的;可代替标准java IO,只是实现的方式不一样,NIO是面向缓冲区、基于通道的IO操作;通过NIO可以提高对文件的读写操作。基于这种优势,现在使用NIO的场景越来愈多,很多主流行的框架都使用到了NIO技术,如Tomcat、Netty、Jetty等;所以学习和掌握NIO技术已经是一个java开发的必备技能了。
1.1: 阻塞IO通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的 Server/Client 模式会基于 TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来新的问题,如果线程池中有 100 个线程,而有100 个用户都在进行大文件下载,会导致第 101 个用户的请求无法及时处理,即便第101 个用户只想请求一个几 KB 大小的页面。
1.2:非阻塞IONIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件。当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。
1.3:阻塞IO与非阻塞的对比 非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的.区别是阻塞的 IO 会阻塞在 IO 操作上, NIO 阻塞在事件获取上,没有事件就没有 IO, 从高层次看 IO 就不阻塞了.也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是select()阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢?NIO 的本质是延迟 IO 操作到真正发生 IO 的时候,而不是以前的只要 IO 流打开了就一直等待 IO 操作。
Java NIO 由以下几个核心部分组成:
- Channels(通道)
- Buffers(缓冲区)
- Selectors(选择器)
虽然 Java NIO 中除此之外还有很多类和组件,但 Channel,Buffer 和 Selector 构成了核心的 API。其它组件,如 Pipe 和 FileLock,只不过是与三个核心组件共同使用的工具类。
1.4.1:Channel首先说一下 Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream.而Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。NIO 中的 Channel 的主要实现有:FileChannel、DatagramChannel、SocketChannel 和 ServerSocketChannel,这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。
1.4.2: BufferNIO 中的关键 Buffer 实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。
1.4.3:SelectorSelector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。要使用Selector, 得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如:新的连接进来、数据接收等。
1.4.4 :主要核心原理首先获取用于连接IO设备的通道channel以及用于容纳数据的缓冲区,利用选择器Selector监控多个Channel的IO状况(多路复用),然后操作缓冲区,对数据进行处理。NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道,即一个单独的线程现在可以管理多个输入和输出通道。
2、JavaNIO的Channel 2.1:Channel概述(负责数据的运输) channel表示到IO设备(如:文件、套接字)的连接,即用于源节点与目标节点的连接,在java NIO中channel本身不负责存储数据,主要是配合缓冲区,负责数据的传输。
NIO 中通过 channel 封装了对数据源的操作,通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络 socket。在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
2.2:Channel的实现下面是 Java NIO 中最重要的 Channel 的实现:
- FileChannel 从文件中读写数据。
- DatagramChannel 能通过 UDP 读写网络中的数据。
- SocketChannel能通过 TCP 读写网络中的数据。
- ServerSocketChannel可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel。
FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。
1、 打开 FileChannel
在使用 FileChannel 之前,必须先打开它。但是,我们无法直接打开一个
FileChannel,需要通过使用一个 InputStream、OutputStream 或
RandomAccessFile 来获取一个 FileChannel 实例。
2、从 FileChannel 读取数据
调用多个 read()从 FileChannel 中读取数据。
3、向 FileChannel 写数据
使用 FileChannel.write()方法向 FileChannel 写数据,该方法的参数是一个 Buffer。
public class FileChannelDemo {
public static void main(String[] args) throws IOException {
RandomAccessFile aFile = new
RandomAccessFile("d:\atguigu\01.txt", "rw");
FileChannel inChannel = aFile.getChannel();
String newData = "New String to write to file..." +
System.currentTimeMillis();
ByteBuffer buf1 = ByteBuffer.allocate(48);
buf1.clear();
buf1.put(newData.getBytes());
buf1.flip();
while(buf1.hasRemaining()) {
inChannel.write(buf1);
}
inChannel.close();
} }
注意: FileChannel.write()是在 while 循环中调用的。因为无法保证 write()方法一次能向 FileChannel 写入多少字节,因此需要重复调用 write()方法,直到 Buffer 中已经没有尚未写入通道的字节。
4、 关闭 FileChannel
用完 FileChannel 后必须将其关闭。
5、 FileChannel 的 position 方法
有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置。也可以通过调用 position(long pos)方法设置 FileChannel 的当前位置。
这里有两个例子:
long pos = channel.position(); channel.position(pos +123);
如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1 (文件结束标志)。
如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。
6、FileChannel 的 size 方法
FileChannel 实例的 size()方法将返回该实例所关联文件的大小。如:
long fileSize = channel.size();
7、 FileChannel 的 truncate 方法
可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。如:
channel.truncate(1024);
这个例子截取文件的前 1024 个字节。
8、FileChannel 的 force 方法
FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,需要调用 force()方法。force()方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
9、 FileChannel 的 transferTo 和 transferFrom 方法
通道之间的数据传输:如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个 channel。
transferFrom()方法: FileChannel 的 transferFrom()方法可以将数据从源通道传输到 FileChannel 中(注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节道传输到此通道的文件中)。transferTo()方法则相反。
下面是一个 FileChannel 完成文件间的复制的例子:
a.txt文件
b.txt文件此时为空
public class FileCopy {
public static void main(String[] args) throws Exception{
RandomAccessFile filea= new RandomAccessFile("D:\测试数据\a.txt", "rw");
RandomAccessFile fileb= new RandomAccessFile("D:\测试数据\b.txt", "rw");
FileChannel fromChannel = filea.getChannel();
FileChannel toChannel = fileb.getChannel();
long position = 0;
long count = fromChannel.size();
System.out.println(count);
// toChannel.transferFrom(fromChannel,position,count);
fromChannel.transferTo(position, count, toChannel);
filea.close();
fileb.close();
System.out.println("操作结束");
}
}
成功将a.txt中的数据复制给了b.txt
下面是一个使用 FileChannel 读取数据到 Buffer 中的示例:
package com.zjw;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class JavaNIO {
public static void main(String[] args) throws Exception {
// 文件编码是utf8,需要用utf8解码
Charset charset = Charset.forName("utf-8");
CharsetDecoder decoder = charset.newDecoder();
RandomAccessFile raFile = new RandomAccessFile("D:\测试数据\a.txt", "rw");
FileChannel fChannel = raFile.getChannel();
ByteBuffer bBuf = ByteBuffer.allocate(32); // 缓存大小设置为32个字节。仅仅是测试用。
CharBuffer cBuf = CharBuffer.allocate(32);
int bytesRead = fChannel.read(bBuf); // 从文件通道读取字节到buffer.
char[] tmp = null; // 临时存放转码后的字符
byte[] remainByte = null;// 存放decode操作后未处理完的字节。decode仅仅转码尽可能多的字节,此次转码不了的字节需要缓存,下次再转
int leftNum = 0; // 未转码的字节数
while (bytesRead != -1) {
bBuf.flip(); // 切换buffer从写模式到读模式
decoder.decode(bBuf, cBuf, true); // 以utf8编码转换ByteBuffer到CharBuffer
cBuf.flip(); // 切换buffer从写模式到读模式
remainByte = null;
leftNum = bBuf.limit() - bBuf.position();
if (leftNum > 0) { // 记录未转换完的字节
remainByte = new byte[leftNum];
bBuf.get(remainByte, 0, leftNum);
}
// 输出已转换的字符
tmp = new char[cBuf.length()];
while (cBuf.hasRemaining()) {
cBuf.get(tmp);
System.out.print(new String(tmp));
}
bBuf.clear(); // 切换buffer从读模式到写模式
cBuf.clear(); // 切换buffer从读模式到写模式
if (remainByte != null) {
bBuf.put(remainByte); // 将未转换完的字节写入bBuf,与下次读取的byte一起转换
}
bytesRead = fChannel.read(bBuf);
}
raFile.close();
}
}
2.3:Socket 通道
1、 新的 socket 通道类可以运行阻塞模式与非阻塞模式并且它们是可被selector(选择器)选择的。所有的 socket 通道类(DatagramChannel、SocketChannel 和ServerSocketChannel)都继承了位于 java.nio.channels.spi 包中的 AbstractSelectableChannel。
2、 请注意 DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。
3、 全部 socket 通道类(DatagramChannel、SocketChannel 和
ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。这些是我们所熟悉的来自 java.net 的类(Socket、ServerSocket 和 DatagramSocket),它们已经被更新以识别通道。对等 socket 可以通过调用 socket( )方法从一个通道上获取。此外,这三个 java.net 类现在都有 getChannel( )方法。
4、 设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking( )方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。可以通过调用 isBlocking( )方法来判断某个 socket 通道当前处于哪种模式。
ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的
java.net.ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
1、打开 ServerSocketChannel
通过调用 ServerSocketChannel.open() 方法来打开 ServerSocketChannel.
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
2、监听新的连接
通过 ServerSocketChannel.accept() 方法监听新进的连接。当 accept()方法返回时候,它返回一个包含新进来的连接的 SocketChannel。如果将ServerSocketChannel设置为阻塞状态(默认为阻塞), accept()方法会一直阻塞到有新连接到达,否则它将直接返回null。通常不会仅仅只监听一个连接,在 while 循环中调用 accept()方法. 如下面的例子:
3、关闭 ServerSocketChannel
通过调用 ServerSocketChannel.close() 方法来关闭 ServerSocketChannel.
serverSocketChannel.close();
以下代码演示了如何使用一个非阻塞的 accept( )方法来实现客户端与服务器端简单通信:
客户端:
public class WebClient {
public static void main(String[] args) throws IOException {
//1.通过SocketChannel的open()方法创建一个SocketChannel对象
SocketChannel socketChannel = SocketChannel.open();
//2.连接到远程服务器(连接此通道的socket)
socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));
// 3.创建写数据缓存区对象
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello WebServer this is from WebClient".getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
//创建读数据缓存区对象
ByteBuffer readBuffer = ByteBuffer.allocate(128);
socketChannel.read(readBuffer);
//String 字符串常量,不可变;StringBuffer 字符串变量(线程安全),可变;StringBuilder 字符串变量(非线程安全),可变
StringBuilder stringBuffer = new StringBuilder();
//4.将Buffer从写模式变为可读模式
readBuffer.flip();
while (readBuffer.hasRemaining()) {
stringBuffer.append((char) readBuffer.get());
}
System.out.println("从服务端接收到的数据:" + stringBuffer);
socketChannel.close();
}
}
服务端:
public class WebServer {
public static void main(String[] args) throws Exception{
//1.通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象,open方法的作用:打开套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//以阻塞的模式运行
ssc.configureBlocking(false);
//2.通过ServerSocketChannel绑定ip地址和port(端口号)
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));
while (true) {
//通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
SocketChannel socketChannel = ssc.accept();
if (socketChannel == null) {
System.out.println("此时还没有Sock接入");
Thread.sleep(1000);
}
else {
//3.创建写数据的缓存区对象
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello WebClient this is from WebServer".getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
//创建读数据的缓存区对象
ByteBuffer readBuffer = ByteBuffer.allocate(128);
//读取缓存区数据
socketChannel.read(readBuffer);
StringBuilder stringBuffer = new StringBuilder();
//4.将Buffer从写模式变为可读模式
readBuffer.flip();
while (readBuffer.hasRemaining()) {
stringBuffer.append((char) readBuffer.get());
}
System.out.println("从客户端接收到的数据:" + stringBuffer);
}
}
}
}
多次运行客服端:
2.3.2:SockChannel的使用SocketChannel 特征:
(1)对于已经存在的 socket 不能创建 SocketChannel。
(2)SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联,需要使
用 connect 接口连接到指定地址。
(3)未进行连接的 SocketChannle 执行 I/O 操作时,会抛出
NotYetConnectedException。
(4)SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式。
(5)SocketChannel 支持异步关闭。如果 SocketChannel 在一个线程上 read 阻塞,另
一个线程对该 SocketChannel 调用 shutdownInput,则读阻塞的线程将返回-1 表示没有
读取任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该
SocketChannel 调用 shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException。
(6)SocketChannel 可以通过socketChannel.setOption()方法设定参数,
- SO_SNDBUF 套接字发送缓冲区大小
- SO_RCVBUF 套接字接收缓冲区大小
- SO_KEEPALIVE 保活连接
- O_REUSEADDR 复用地址
- SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)
- TCP_NODELAY 禁用 Nagle 算法
SocketChannel 的使用
1、创建 SocketChannel
方式一:
SocketChannel socketChannel = SocketChannel.open(new
InetSocketAddress("www.baidu.com", 80));
方式二:
//直接使用有参 open api 或者使用无参 open api,
//但是在无参 open 只是创建了一个SocketChannel 对象,并没有进行实质的 tcp 连接。
SocketChannel socketChanne2 = SocketChannel.open();
socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));
2、连接校验
socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态 socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接 //测试 SocketChannel 是否正在进行连接 socketChannel.isConnectionPending(); //校验正在进行套接字连接的 SocketChannel是否已经完成连接 socketChannel.finishConnect();
3、设置阻塞模式
前面提到 SocketChannel 支持阻塞和非阻塞两种模式:
//false 表示非阻塞,true 表示阻塞,默认为false socketChannel.configureBlocking(false);
4、读写
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("www.baidu.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");
2.3.3:Scatter/Gather
分散(scatter): 从 Channel 中读取是指在读操作时将读取的数据写入多个 buffer
中。因此,Channel 将从 Channel 中读取的数据“分散(scatter)”到多个 Buffer中。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
read()方法按照 buffer 在数组中的顺序将从 channel 中读取的数据写入到 buffer,当一个 buffer 被写满后,channel 紧接着向另一个 buffer 中写。Scattering Reads 在移动下一个 buffer 前,必须填满当前的 buffer,这也意味着它不适用于动态消息(注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads 才能正常工作。
聚集(gather): 写入 Channel 是指在写操作时将多个 buffer 的数据写入同一个Channel,因此,Channel 将多个 Buffer 中的数据“聚集(gather)”后发送到Channel。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
buffers 数组是 write()方法的入参,write()方法会按照 buffer 在数组中的顺序,将数
据写入到 channel,注意只有 position 和 limit 之间的数据才会被写入。因此,如果
一个 buffer 的容量为 128byte,但是仅仅包含 58byte 的数据,那么这 58byte 的数
据将被写入到 channel 中。因此与 Scattering Reads 相反,Gathering Writes 能较
好的处理动态消息。



