- NIO概述
- NIO-Channel
- FileChannel
- 打开FileChannel
- 从FileChannel读取数
- 向FileChannel写数
- FileChannel的transferTo和 transferFro
- DatagramChannel
- 打开DatagramChannel
- 接受数据
- 发送数据
- 连接
- 实例
- SocketChannel
- 创建SocketChannel
- 连接校验
- 读写模式
- ServerSocketChannel
- 打开ServerSocketChannel
- 监听新连接
- NIO-Buffer
- Buffer通常的操作
- Buffer的capacity、position和limit
- Buffer分配
- 向Buffer中写数据
- 从Buffer中读取数据
- NIO-缓冲区
- 只读缓冲区
- 直接缓冲区
- 内存映射文件io
- NIO-Selector
- NIO-Pipe
IO的操作方式分为:同步阻塞BIO、同步非阻塞NIO、异步非阻塞AIO
NIO由Channel、Buffer、Selector核心部分组成,Pipe和FileLock是与三个核心组件共同使用的工具类
NIO-ChannelNIO中的Channel的主要实现有:FileChannel、DatagramChannel、 SocketChannel和ServerSocketChannel,分别可以对应文件IO、UDP和TCP Server和TCP Client。
Channel既可以读取数据又可以写入数据,Channel中的数据总要先读到一个Buffer,或者总是要从一个Buffer中写入。
FileChannel 打开FileChannel在使用FileChannel之前必须先打开它。但是直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个 FileChannel实例
RandomAccessFile aFile = new RandomAccessFile("01.txt", "rw");
FileChannel inChannel = aFile.getChannel()
从FileChannel读取数
首先分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。然后调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read() 方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1表示到了文件末尾。
// 创建Buffer ByteBuffer buf = ByteBuffer.allocate(1024); // 读取数据到buffer中 int byteRead = channel.read(buf);向FileChannel写数
FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法直到Buffer中已经没有尚未写入通道的字节。
// 打开FileChannel
RandomAccessFile aFile = new RandomAccessFile("02.txt", "rw");
FileChannel channel = aFile.getChannel();
// 创建buffer对象
ByteBuffer buffer = ByteBuffer.allocate(1024);
String newData = "data czs";
buffer.clear();
// 写入数据
buffer.put(newData.getBytes());
// 读写模式转化
buffer.flip();
// FileChannel最终实现
while (buffer.hasRemaining()) {
channel.write(buffer);
}
// 关闭channel
channel.close();
FileChannel的transferTo和 transferFro
可以将数据从源通道传输到FileChannel中
// 创建二个FileChannel
RandomAccessFile aFile = new RandomAccessFile("01.txt", "rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("02.txt", "rw");
FileChannel toChannel = bFile.getChannel();
// from写入到to
long position = 0;
long size = fromChannel.size();
toChannel.transferFrom(fromChannel, position, size);
// from写入到to
long position = 0;
long size = fromChannel.size();
fromChannel.transferTo(position,size,toChannel);
aFile.close();;
bFile.close();
DatagramChannel
通过UDP读写网络中的数据,DatagramChannel可以发送单独的数据报给不同的目的地址。同样DatagramChannel对象也可以接收来自任意地址的数据包。 每个到达的数据报都含有关于它来自何处的信息
打开DatagramChannelDatagramChannel server = DatagramChannel.open(); server.socket().bind(new InetSocketAddress(8080));接受数据
ByteBuffer receiveBuffer = ByteBuffer.allocate(64); receiveBuffer.clear(); SocketAddress receiveAddr = server.receive(receiveBuffer);发送数据
DatagramChannel server = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
server.send(sendBuffer, new InetSocketAddress("127.0.0.1",8080));
连接
UDP不存在真正意义上的连接,这里的连接是向特定服务地址用read和write接收发送数据包。
client.connect(new InetSocketAddress("127.0.0.1",8080));
int readSize= client.read(sendBuffer);
server.write(sendBuffer);
实例
// Datagram 连接 read和write
@Test
public void testConnect() throws IOException {
// 打开DatagramChannel
DatagramChannel connChannel = DatagramChannel.open();
// 绑定
connChannel.bind(new InetSocketAddress(9999));
// 连接
connChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
// write
ByteBuffer buffer = ByteBuffer.wrap("发送hello world".getBytes(StandardCharsets.UTF_8));
connChannel.write(buffer);
// read
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
while (true) {
readBuffer.clear();
connChannel.read(readBuffer);
readBuffer.flip();
System.out.println(StandardCharsets.UTF_8.decode(readBuffer));
}
}
SocketChannel
通过TCP读写网络中的数据
SocketChannel就是NIO对于非阻塞socket操作的支持的组件,其在socket上封装了一层,主要是支持了非阻塞的读写。同时改进了传统的单向流 API,,Channel 同时支持读写。
创建SocketChannel方式一
SocketChannel socketChannel = SocketChannel.open(newInetSocketAddress("www.baidu.com", 80))
方式二
SocketChannel socketChanne2 = SocketChannel.open();
socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80))
连接校验
socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态 socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接 socketChannel.isConnectionPending(); //测试 SocketChannel 是否正在进行连接 socketChannel.finishConnect(); //校验正在进行套接字连接的 SocketChannel是否已经完成连接读写模式
SocketChannel支持阻塞和非阻塞两种模式,false 表示非阻塞,true 表示阻塞。
socketChannel.configureBlocking(false);ServerSocketChannel
监听新TCP连接,像WEB服务器一样,对每一个新连接创建一个SocketChannel
打开ServerSocketChannel通过调用ServerSocketChannel.open()方法来打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();监听新连接
通过ServerSocketChannel.accept()方法监听新进的连接。当accept()方法返回时候,它返回一个包含新进来的连接的SocketChannel。因此,accept()方法会一直阻塞到有新连接到达,可以设置非阻塞模式,在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
// 设置阻塞非阻塞模式
socketChannel.configureBlocking(false);
// 读操作
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("over");
NIO-Buffer
NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。
Buffer通常的操作第一步:写入数据到Buffer
第二步:调用flip方法
第三步:从Buffer中读取数据
第四步:调用clear方法或者compact方法
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer的capacity、position和limitcapacity:作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”。只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空才能继续写数据往里写数据
position:写数据到Buffer中时,position表示写入数据的当前位置,position的初始值为0。当一个byte、long等数据写到Buffer后, position会向下移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1(因为 position 的初始值为 0)。读数据到 Buffer 中时,position表示读入数据的当前位置,如position=2时表示已开始读入了3个 byte,或从第3个 byte开始读取。通过ByteBuffer.flip()切换到读模式时position会被重置为0,当 Buffer从position读入数据后,position会下移到下一个可读入的数据Buffer单元。
limit:写数据时,limit表示可对Buffer最多写入多少个数据。写模式下,limit等于Buffer的capacity。读数据时,limit表示Buffer里有多少可读数据(not null 的数据),因此能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)。
flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等(现在能读取多少个 byte、char 等)。
Buffer分配要想获得一个Buffer对象首先要进行分配。 每一个Buffer类都有一个allocate方法。
ByteBuffer buffer = ByteBuffer.allocate(1024);向Buffer中写数据
方式一:从Channel中读取数据到Buffer
int bytesRead = inChannel.read(buf);
方式二:通过Buffer的put方法写到Buffer里
buf.put("hello world");
从Buffer中读取数据
方式一:从Buffer写入数据到Channel
int bytesWritten = inChannel.write(buf);
方式二:通过Buffer的get方法读取数据
byte aByte = buf.get();NIO-缓冲区
在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。调用slice()方法可以创建一个子缓冲区
只读缓冲区只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的asReadonlyBuffer()方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化
// 只读缓冲区
@Test
public void buffer02() {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
// 创建只读缓冲区
ByteBuffer readonlyBuffer = buffer.asReadOnlyBuffer();
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.get(i);
buffer.put(i, b);
}
readOnlyBuffer.flip();
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
}
直接缓冲区
直接缓冲区是为加快I/O速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后), 尝试避免将缓冲区的内容拷贝到一个中间缓冲区中或者从一个中间缓冲区中拷贝数据。 要分配直接缓冲区,需要调用allocateDirect()方法,而不是allocate()方法,使用方式与普通缓冲区并无区别。
// 直接缓冲区
@Test
public void buffer03() throws IOException {
String infile = "01.txt";
FileInputStream fin = new FileInputStream(infile);
FileChannel finChannel = fin.getChannel();
String outfile = "02.txt";
FileOutputStream fout = new FileOutputStream(outfile);
FileChannel foutChannel = fout.getChannel();
// 创建直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
buffer.clear();
int read = finChannel.read(buffer);
if (read == -1) break;
buffer.flip();
foutChannel.write(buffer);
}
}
内存映射文件io
内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快的多。内存映射文件I/O是通过使文件中的数据出现为内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。 一般来说,只有文件中实际读取或者写入的部分才会映射到内存中
static private final int start = 0;
static private final int size = 1024;
// 内存映射文件io
@Test
public void buffer04() throws IOException {
RandomAccessFile raf = new RandomAccessFile("01.txt", "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);
mbb.put(0, (byte) 97);
mbb.put(1024, (byte) 122);
raf.close();
}
NIO-Selector
Selector运行单线程处理多个Channel,如果应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件, 事件的例子有如新的连接进来、数据接收等。
public static void main(String[] args) throws IOException {
// 创建selector
Selector selector = Selector.open();
// 注册channel到selector
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 非阻塞
serverSocketChannel.configureBlocking(false);
// 绑定连接
serverSocketChannel.bind(new InetSocketAddress(8080));
// 将通道注册到选择器上 并制定监听事件为:“接收”事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 轮询查询就绪操作
Set selectionKeys = selector.selectedKeys();
// 遍历集合
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 判断key就绪状态操作
if (key.isAcceptable()) {
} else if (key.isConnectable()) {
} else if (key.isReadable()) {
} else if (key.isWritable()) {
}
iterator.remove();
}
}
NIO-Pipe
Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取
public static void main(String[] args) throws IOException {
// 1 获取管道
Pipe pipe = Pipe.open();
// 2 获取sink通道
Pipe.SinkChannel sinkChannel = pipe.sink();
// 3 创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("czs".getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
// 4 写入数据
sinkChannel.write(byteBuffer);
// 5 获取source通道
Pipe.SourceChannel sourceChannel = pipe.source();
// 6 创建缓冲区,读取数据
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
// readBuffer.flip();
int read = sourceChannel.read(readBuffer);
System.out.println(new String(readBuffer.array(), 0, read));
// 7 关闭
sourceChannel.close();
sinkChannel.close();
}



