NIO教程 ——检视阅读
简介
NIO中的N可以理解为Non-blocking ,不单纯是New 。
不同点:
- 标准的IO编程接口是面向字节流和字符流的。而NIO是面向通道和缓冲区的,数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中。
- Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。
- NIO中有一个“slectors”的概念。selector可以检测多个通道的事件状态(例如:链接打开,数据到达)这样单线程就可以操作多个通道的数据。
概览
NIO包含下面3个核心的组件,Channel,Buffer和Selector组成了这个核心的API:
- Channels ——通道
- Buffers ——缓冲区
- Selectors ——选择器
通常来说NIO中的所有IO都是从Channel开始的。Channel和流有点类似。通过Channel,我们即可以从Channel把数据写到Buffer中,也可以把数据冲Buffer写入到Channel 。
有很多的Channel,Buffer类型。下面列举了主要的几种:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
正如你看到的,这些channel基于于UDP和TCP的网络IO,以及文件IO。 和这些类一起的还有其他一些比较有趣的接口,在本节中暂时不多介绍。为了简洁起见,我们会在必要的时候引入这些概念。 下面是核心的Buffer实现类的列表:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些Buffer涵盖了可以通过IO操作的基础类型:byte,short,int,long,float,double以及characters. NIO实际上还包含一种MappedBytesBuffer,一般用于和内存映射的文件。
选择器允许单线程操作多个通道。如果你的程序中有大量的链接,同时每个链接的IO带宽不高的话,这个特性将会非常有帮助。比如聊天服务器。 下面是一个单线程中Slector维护3个Channel的示意图:
要使用Selector的话,我们必须把Channel注册到Selector上,然后就可以调用Selector的select()方法。这个方法会进入阻塞,直到有一个channel的状态符合条件。当方法返回后,线程可以处理这些事件。
Java NIO Channel通道
Java NIO Channel通道和流非常相似,主要有以下3点区别:
- 通道可以读也可以写,流一般来说是单向的(只能读或者写)。
- 通道可以异步读写。
- 通道总是基于缓冲区Buffer来读写。
Channel的实现
下面列出Java NIO中最重要的集中Channel的实现:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel用于文件的数据读写。 DatagramChannel用于UDP的数据读写。 SocketChannel用于TCP的数据读写。 ServerSocketChannel允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel.
RandomAccessFile扩展:
RandomAccessFile(随机访问文件)类。该类是Java语言中功能最为丰富的文件访问类 。RandomAccessFile类支持“随机访问”方式,这里“随机”是指可以跳转到文件的任意位置处读写数据。在访问一个文件的时候,不必把文件从头读到尾,而是希望像访问一个数据库一样“随心所欲”地访问一个文件的某个部分,这时使用RandomAccessFile类就是最佳选择。
四种模式:R RW RWD RWS
r | 以只读的方式打开文本,也就意味着不能用write来操作文件 |
---|---|
rw | 读操作和写操作都是允许的 |
rws | 每当进行写操作,同步的刷新到磁盘,刷新内容和元数据 |
rwd | 每当进行写操作,同步的刷新到磁盘,刷新内容 |
RandomAccessFile的用处:
1、大型文本日志类文件的快速定位获取数据:
得益于seek的巧妙设计,我认为我们可以从超大的文本中快速定位我们的游标,例如每次存日志的时候,我们可以建立一个索引缓存,索引是日志的起始日期,value是文本的poiniter 也就是光标,这样我们可以快速定位某一个时间段的文本内容
2、并发读写
也是得益于seek的设计,我认为多线程可以轮流操作seek控制光标的位置,从未达到不同线程的并发写操作。
3、更方便的获取二进制文件
通过自带的读写转码(readDouble、writeLong等),我认为可以快速的完成字节码到字符的转换功能,对使用者来说比较友好。
RandomAccessFile参考
实例:
public class FileChannelTest {
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile("D:\\text\\1_loan.sql", "r");
//mode只有4中,如果不是读写的mode或者给的不是4种中的,就会报错。
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
try {
FileChannel fileChannel = file.getChannel();
FileChannel copyFileChannel = copyFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = fileChannel.read(byteBuffer);
while (read != -1) {
System.out.println("read:" + read);
//byteBuffer缓冲区切换为读模式
byteBuffer.flip();
copyFileChannel.write(byteBuffer);
//“清空”byteBuffer缓冲区,以满足后续写入操作
byteBuffer.clear();
//注意,每次读时都要返回读后的状态read值赋值给循环判断体read,否则会陷入死循环true
read = fileChannel.read(byteBuffer);
}
} finally {
file.close();
copyFile.close();
}
}
}
报错:
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "w");
//因为没有"w"的mode
Exception in thread "main" java.lang.IllegalArgumentException: Illegal mode "w" must be one of "r", "rw", "rws", or "rwd"
at java.io.RandomAccessFile.<init>(RandomAccessFile.java:221)
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
//因为没有"w"的权限
Exception in thread "main" java.nio.channels.NonWritableChannelException
at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:194)
at com.niotest.FileChannelTest.main(FileChannelTest.java:33)
NIO Buffer缓冲区
Java NIO Buffers用于和NIO Channel交互。正如你已经知道的,我们从channel中读取数据到buffers里,从buffer把数据写入到channels.
buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。
Buffer基本用法
利用Buffer读写数据,通常遵循四个步骤:
- 把数据写入buffer;
- 调用flip;
- 从Buffer中读取数据;
- 调用buffer.clear()或者buffer.compact()
当写入数据到buffer中时,buffer会记录已经写入的数据大小。当需要读数据时,通过flip()方法把buffer从写模式调整为读模式;在读模式下,可以读取所有已经写入的数据。
当读取完数据后,需要清空buffer,以满足后续写入操作。清空buffer有两种方式:调用clear()或compact()方法。clear会清空整个buffer,compact则只清空已读取的数据,未被读取的数据会被移动到buffer的开始位置,写入位置则紧跟着未读数据之后。
Buffer的容量,位置,上限(Buffer Capacity, Position and Limit)
buffer缓冲区实质上就是一块内存,用于写入数据,也供后续再次读取数据。这块内存被NIO Buffer管理,并提供一系列的方法用于更简单的操作这块内存。
一个Buffer有三个属性是必须掌握的,分别是:
- capacity容量
- position位置
- limit限制
position和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。
下面有张示例图,描诉了不同模式下position和limit的含义:
容量(Capacity)
作为一块内存,buffer有一个固定的大小,叫做capacity容量。也就是最多只能写入容量值的字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据。
位置(Position)
当写入数据到Buffer的时候需要中一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1.
当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。
上限(Limit)
在写模式,limit的含义是我们所能写入的最大数据量。它等同于buffer的容量。
一旦切换到读模式,limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。
数据读取的上限时buffer中已有的数据,也就是limit的位置(原写模式下position所指的位置)。
Buffer Types
Java NIO有如下具体的Buffer类型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
正如你看到的,Buffer的类型代表了不同数据类型,换句话说,Buffer中的数据可以是上述的基本类型;
分配一个Buffer(Allocating a Buffer)
为了获取一个Buffer对象,你必须先分配。每个Buffer实现类都有一个allocate()方法用于分配内存。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
CharBuffer charBuffer = CharBuffer.allocate(48);
写入数据到Buffer(Writing Data to a Buffer)
写数据到Buffer有两种方法:
- 从Channel中写数据到Buffer
- 手动写数据到Buffer,调用put方法
//从Channel中写数据到Buffer
int read = fileChannel.read(byteBuffer);
//调用put方法写
buf.put(3);
//把数据写到特定的位置
public ByteBuffer put(int i, byte x);
//把一个具体类型数据写入buffer
public ByteBuffer putInt(int x);
flip()——翻转
flip()方法可以把Buffer从写模式切换到读模式。调用flip方法会把position归零,并设置limit为之前的position的值。 也就是说,现在position代表的是读取位置,limit标示的是已写入的数据位置。
从Buffer读取数据(Reading Data from a Buffer)
从Buffer读数据也有两种方式。
- 从buffer读数据到channel。
- 从buffer直接读取数据,调用get方法。
//读取数据到channel的例子:
int bytesWritten = inChannel.write(buf);
//调用get读取数据的例子:
byte aByte = buf.get();
rewind()——倒带
Buffer.rewind()方法将position置为0,这样我们可以重复读取buffer中的数据。limit保持不变。
clear() and compact()
一旦我们从buffer中读取完数据,需要复用buffer为下次写数据做准备。只需要调用clear或compact方法。
clear方法会重置position为0,limit为capacity,也就是整个Buffer清空。实际上Buffer中数据并没有清空,我们只是把标记为修改了。(重新写入的时候这些存在的数据就会被新的数据覆盖)
如果Buffer还有一些数据没有读取完,调用clear就会导致这部分数据被“遗忘”,因为我们没有标记这部分数据未读。
针对这种情况,如果需要保留未读数据,那么可以使用compact()。 因此compact()和clear()的区别就在于对未读数据的处理,是保留这部分数据还是一起清空。
mark() and reset()
通过mark方法可以标记当前的position,通过reset来恢复mark的位置,这个非常像canva的save和restore:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals() and compareTo()
可以用eqauls和compareTo比较两个buffer
equals()
判断两个buffer相对,需满足:
- 类型相同
- buffer中剩余字节数相同
- 所有剩余字节相等
从上面的三个条件可以看出,equals只比较buffer中的部分内容,并不会去比较每一个元素。
compareTo()
compareTo也是比较buffer中的剩余元素,只不过这个方法适用于比较排序的:
NIO Scatter (分散)/ Gather(聚集)
——分散读和聚集写的场景。
Java NIO发布时内置了对scatter / gather的支持。scatter / gather是通过通道读写数据的两个概念。
Scattering read指的是从通道读取的操作能把数据写入多个buffer,也就是scatters代表了数据从一个channel到多个buffer的过程。
gathering write则正好相反,表示的是从多个buffer把数据写入到一个channel中。
Scatter/gather在有些场景下会非常有用,比如需要处理多份分开传输的数据。举例来说,假设一个消息包含了header和body,我们可能会把header和body保存在不同独立buffer中,这种分开处理header与body的做法会使开发更简明。
Scattering Reads
"scattering read"是把数据从单个Channel写入到多个buffer,下面是示意图:
观察代码可以发现,我们把多个buffer写在了一个数组中,然后把数组传递给channel.read()方法。read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后,接着写到下一个buffer中。
实际上,scattering read内部必须写满一个buffer后才会向后移动到下一个buffer,因此这并不适合消息大小会动态改变的部分,也就是说,如果你有一个header和body,并且header有一个固定的大小(比如128字节),这种情形下可以正常工作。
gathering Writes
"gathering write"把多个buffer的数据写入到同一个channel中.
传入一个buffer数组给write,内部会按顺序将数组内的内容写进channel,这里需要注意,写入的时候针对的是buffer中position到limit之间的数据。也就是如果buffer的容量是128字节,但它只包含了58字节数据,那么写入的时候只有58字节会真正写入。因此gathering write是可以适用于可变大小的message的,这和scattering reads不同。
NIO Channel to Channel Transfers通道传输接口
在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。这个特性得益于FileChannel包含的transferTo和transferFrom两个方法。
transferFrom()——目标channel用,参数为源数据channel。
transferFrom的参数position和count表示目标文件的写入位置和最多写入的数据量。如果通道源的数据小于count那么就传实际有的数据量。 另外,有些SocketChannel的实现在传输时只会传输哪些处于就绪状态的数据,即使SocketChannel后续会有更多可用数据。因此,这个传输过程可能不会传输整个的数据。
transferTo()——源数据用,参数为目标channel
SocketChannel的问题也存在于transferTo.SocketChannel的实现可能只在发送的buffer填充满后才发送,并结束。
实例:
public class ChannelTransferTest {
public static void main(String[] args) throws IOException {
RandomAccessFile fromfile = new RandomAccessFile("D:\\text\\1_loan.sql", "rw");
//mode只有4中,如果不是读写的mode或者给的不是4种中的,就会报错。
RandomAccessFile toFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "rw");
FileChannel fromfileChannel = fromfile.getChannel();
FileChannel toFileChannel = toFile.getChannel();
//==========================transferTo=================================
//transferTo方法把fromfileChannel数据传输到另一个toFileChannel
//long transferSize = fromfileChannel.transferTo(0, fromfileChannel.size(), toFileChannel);
//System.out.println(transferSize);
//=============================transferFrom==============================
//把数据从通道源传输到toFileChannel,相比通过buffer读写更加的便捷
long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size());
//参数position和count表示目标文件的写入位置和最多写入的数据量
//long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()-1000);
//如果通道源的数据小于count那么就传实际有的数据量。
//long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()+1000);
System.out.println(transferSize1);
}
}
NIO Selector选择器
Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
为什么使用Selector
用单线程处理多个channels的好处是我需要更少的线程来处理channel。实际上,你甚至可以用一个线程来处理所有的channels。从操作系统的角度来看,切换线程开销是比较昂贵的,并且每个线程都需要占用系统资源,因此暂用线程越少越好。
需要留意的是,现代操作系统和CPU在多任务处理上已经变得越来越好,所以多线程带来的影响也越来越小。如果一个CPU是多核的,如果不执行多任务反而是浪费了机器的性能。不过这些设计讨论是另外的话题了。简而言之,通过Selector我们可以实现单线程操作多个channel。
创建Selector
创建一个Selector可以通过Selector.open()方法:
Selector selector = Selector.open();
注册Channel到Selector上
先把Channel注册到Selector上,这个操作使用SelectableChannel的register()。SocketChannel等都有继承此抽象类。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel必须是非阻塞的。所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式。Socket channel可以正常使用。
注意register的第二个参数,这个参数是一个“关注集合”,代表我们关注的channel状态,有四种基础类型可供监听:
- Connect——连接就绪(连接成功后)
- Accept——可连接就绪(接受请求连接时)
- Read——读就绪
- Write——写就绪
一个channel触发了一个事件也可视作该事件处于就绪状态。因此当channel与server连接成功后,那么就是“连接就绪”状态。server channel接收请求连接时处于“可连接就绪”状态。channel有数据可读时处于“读就绪”状态。channel可以进行数据写入时处于“写就绪”状态。
上述的四种就绪状态用SelectionKey中的常量表示如下:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果对多个事件感兴趣可利用位的或运算结合多个常量,比如:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
在上一小节中,我们利用register方法把Channel注册到了Selectors上,这个方法的返回值是SelectionKeys,这个返回的对象包含了一些比较有价值的属性:
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
Interest Set
这个“关注集合”实际上就是我们希望处理的事件的集合,它的值就是注册时传入的参数,我们可以用按为与运算把每个事件取出来:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
Ready Set
"就绪集合"中的值是当前channel处于就绪的值,一般来说在调用了select方法后都会需要用到就绪状态
int readySet = selectionKey.readyOps();
从“就绪集合”中取值的操作类似于“关注集合”的操作,当然还有更简单的方法,SelectionKey提供了一系列返回值为boolean的的方法:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector
从SelectionKey操作Channel和Selector非常简单:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Objects
我们可以给一个SelectionKey附加一个Object,这样做一方面可以方便我们识别某个特定的channel,同时也增加了channel相关的附加信息。例如,可以把用于channel的buffer附加到SelectionKey上:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
附加对象的操作也可以在register的时候就执行:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
从Selector中选择channel
一旦我们向Selector注册了一个或多个channel后,就可以调用select来获取channel。select方法会返回所有处于就绪状态的channel。 select方法具体如下:
- int select()
- int select(long timeout)
- int selectNow()
select()方法在返回channel之前处于阻塞状态。 select(long timeout)和select做的事一样,不过他的阻塞有一个超时限制。
selectNow()不会阻塞,根据当前状态立刻返回合适的channel。
select()方法的返回值是一个int整形,代表有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪。举例来说,假设第一次调用select时正好有一个channel就绪,那么返回值是1,并且对这个channel做任何处理,接着再次调用select,此时恰好又有一个新的channel就绪,那么返回值还是1,现在我们一共有两个channel处于就绪,但是在每次调用select时只有一个channel是就绪的。
selectedKeys()
在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,这个操作通过调用selectedKeys()方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
还记得在register时的操作吧,我们register后的返回值就是SelectionKey实例,也就是我们现在通过selectedKeys()方法所返回的SelectionKey。
遍历这些SelectionKey可以通过如下方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
上述循环会迭代key集合,针对每个key我们单独判断他是处于何种就绪状态。
注意:keyIterater.remove()方法的调用,Selector本身并不会移除SelectionKey对象,这个操作需要我们手动执行。当下次channel处于就绪是,Selector仍然会把这些key再次加入进来。
SelectionKey.channel返回的channel实例需要强转为我们实际使用的具体的channel类型,例如ServerSocketChannel或SocketChannel.
wakeUp()
由于调用select而被阻塞的线程,可以通过调用Selector.wakeup()来唤醒即便此时已然没有channel处于就绪状态。具体操作是,在另外一个线程调用wakeup,被阻塞与select方法的线程就会立刻返回。
close()
当操作Selector完毕后,需要调用close方法。close的调用会关闭Selector并使相关的SelectionKey都无效。channel本身不会被关闭。
示例:首先打开一个Selector,然后注册channel,最后监听Selector的状态。
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1.获取通道
ServerSocketChannel server = ServerSocketChannel.open();
// 2.切换成非阻塞模式
server.configureBlocking(false);
// 3. 绑定连接
server.bind(new InetSocketAddress(6666));
// 4. 获取选择器
Selector selector = Selector.open();
// 4.1将通道注册到选择器上,指定接收“监听通道”事件
server.register(selector, SelectionKey.OP_ACCEPT);
// 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪
while (selector.select() > 0) {
// 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 获取已“就绪”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 接收事件就绪
if (selectionKey.isAcceptable()) {
// 8. 获取客户端的链接
SocketChannel client = server.accept();
// 8.1 切换成非阻塞状态
client.configureBlocking(false);
// 8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(监听读就绪事件)
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) { // 读事件就绪
// 9. 获取当前选择器读就绪状态的通道
SocketChannel client = (SocketChannel) selectionKey.channel();
// 9.1读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 9.2得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建)
FileChannel outChannel = FileChannel.open(Paths.get("2_loan.sql"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
while (client.read(buffer) > 0) {
// 在读之前都要切换成读模式
buffer.flip();
outChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("yeah,i know,i got your message!".getBytes());
byteBuffer.flip();
client.write(byteBuffer);
}
// 10. 取消选择键(已经处理过的事件,就应该取消掉了)
iterator.remove();
}
}
}
}
public class NIOClientTwo {
public static void main(String[] args) throws IOException {
// 1. 获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 1.1切换成非阻塞模式
socketChannel.configureBlocking(false);
// 1.2获取选择器
Selector selector = Selector.open();
// 1.3将通道注册到选择器中,获取服务端返回的数据
socketChannel.register(selector, SelectionKey.OP_READ);
// 2. 发送一张图片给服务端吧
FileChannel fileChannel = FileChannel.open(Paths.get("D:\\text\\1_loan.sql"), StandardOpenOption.READ);
// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.读取本地文件(图片),发送到服务器
while (fileChannel.read(buffer) != -1) {
// 在读之前都要切换成读模式
buffer.flip();
socketChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据
buffer.clear();
}
// 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪
while (selector.select() > 0) {
// 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 获取已“就绪”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 8. 读事件就绪
if (selectionKey.isReadable()) {
// 8.1得到对应的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
// 9. 知道服务端要返回响应的数据给客户端,客户端在这里接收
int readBytes = channel.read(responseBuffer);
if (readBytes > 0) {
// 切换读模式
responseBuffer.flip();
System.out.println(new String(responseBuffer.array(), 0, readBytes));
}
}
// 10. 取消选择键(已经处理过的事件,就应该取消掉了)
iterator.remove();
}
}
}
}
NIO FileChannel文件通道
Java NIO中的FileChannel是用于连接文件的通道。通过文件通道可以读、写文件的数据。Java NIO的FileChannel是相对标准Java IO API的可选接口。
FileChannel不可以设置为非阻塞模式,他只能在阻塞模式下运行。
打开文件通道
在使用FileChannel前必须打开通道,打开一个文件通道需要通过输入/输出流或者RandomAccessFile,下面是通过RandomAccessFile打开文件通道的案例:
RandomAccessFile aFile = new RandomAccessFile("D:\text\1_loan.sql", "rw");
FileChannel inChannel = aFile.getChannel();
从文件通道内读取数据
读取文件通道的数据可以通过read方法:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
首先开辟一个Buffer,从通道中读取的数据会写入Buffer内。接着就可以调用read方法,read的返回值代表有多少字节被写入了Buffer,返回-1则表示已经读取到文件结尾了。
向文件通道写入数据
写数据用write方法,入参是Buffer:
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()) {
channel.write(buf);
}
注意这里的write调用写在了wihle循环汇总,这是因为write不能保证有多少数据真实被写入,因此需要循环写入直到没有更多数据。
关闭通道
操作完毕后,需要把通道关闭:
channel.close();
FileChannel Position
当操作FileChannel的时候读和写都是基于特定起始位置的(position),获取当前的位置可以用FileChannel的position()方法,设置当前位置可以用带参数的position(long pos)方法。
//获取当前的位置
long position = fileChannel.position();
//设置当前位置为pos +123
fileChannel.position(pos +123);
假设我们把当前位置设置为文件结尾之后,那么当我们视图从通道中读取数据时就会发现返回值是-1,表示已经到达文件结尾了。 如果把当前位置设置为文件结尾之后,再向通道中写入数据,文件会自动扩展以便写入数据,但是这样会导致文件中出现类似空洞,即文件的一些位置是没有数据的。
FileChannel Size
size()方法可以返回FileChannel对应的文件的文件大小:
long fileSize = channel.size();
FileChannel Truncate
利用truncate方法可以截取指定长度的文件:
FileChannel truncateFile = fileChannel.truncate(1024);
FileChannel Force
force方法会把所有未写磁盘的数据都强制写入磁盘。这是因为在操作系统中出于性能考虑回把数据放入缓冲区,所以不能保证数据在调用write写入文件通道后就及时写到磁盘上了,除非手动调用force方法。 force方法需要一个布尔参数,代表是否把meta data也一并强制写入。
channel.force(true);
NIO SocketChannel套接字通道
在Java NIO体系中,SocketChannel是用于TCP网络连接的套接字接口,相当于Java网络编程中的Socket套接字接口。创建SocketChannel主要有两种方式,如下:
- 打开一个SocketChannel并连接网络上的一台服务器。
- 当ServerSocketChannel接收到一个连接请求时,会创建一个SocketChannel。
建立一个SocketChannel连接
打开一个SocketChannel可以这样操作:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
关闭一个SocketChannel连接
关闭一个SocketChannel只需要调用他的close方法,如下:
socketChannel.close();
从SocketChannel中读数据
从一个SocketChannel连接中读取数据,可以通过read()方法,如下:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
首先需要开辟一个Buffer。从SocketChannel中读取的数据将放到Buffer中。
接下来就是调用SocketChannel的read()方法.这个read()会把通道中的数据读到Buffer中。read()方法的返回值是一个int数据,代表此次有多少字节的数据被写入了Buffer中。如果返回的是-1,那么意味着通道内的数据已经读取完毕,到底了(链接关闭)。
向SocketChannel写数据
向SocketChannel中写入数据是通过write()方法,write也需要一个Buffer作为参数。下面看一下具体的示例:
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()) {
channel.write(buf);
}
非阻塞模式
我们可以把SocketChannel设置为non-blocking(非阻塞)模式。这样的话在调用connect(), read(), write()时都是异步的。
socketChannel.configureBlocking(false);
connect()
如果我们设置了一个SocketChannel是非阻塞的,那么调用connect()后,方法会在链接建立前就直接返回。为了检查当前链接是否建立成功,我们可以调用finishConnect(),如下:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
write()
在非阻塞模式下,调用write()方法不能确保方法返回后写入操作一定得到了执行。因此我们需要把write()调用放到循环内。这和前面在讲write()时是一样的,此处就不在代码演示。
read()
在非阻塞模式下,调用read()方法也不能确保方法返回后,确实读到了数据。因此我们需要自己检查的整型返回值,这个返回值会告诉我们实际读取了多少字节的数据。
Selector结合非阻塞模式
SocketChannel的非阻塞模式可以和Selector很好的协同工作。把一个或多个SocketChannel注册到一个Selector后,我们可以通过Selector指导哪些channels通道是处于可读,可写等等状态的。
NIO ServerSocketChannel服务端套接字通道
在Java NIO中,ServerSocketChannel是用于监听TCP链接请求的通道,正如Java网络编程中的ServerSocket一样。
ServerSocketChannel实现类位于java.nio.channels包下面。
void test() throws IOException {
//打开一个ServerSocketChannel我们需要调用他的open()方法
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
if (socketChannel.isConnected()) {
break;
}
}
//关闭一个ServerSocketChannel我们需要调用close()方法
serverSocketChannel.close();
}
监听链接
通过调用accept()方法,我们就开始监听端口上的请求连接。当accept()返回时,他会返回一个SocketChannel连接实例,实际上accept()是阻塞操作,他会阻塞带去线程知道返回一个连接; 很多时候我们是不满足于监听一个连接的,因此我们会把accept()的调用放到循环中,就像这样:
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
当然我们可以在循环体内加上合适的中断逻辑,而不是单纯的在while循环中写true,以此来结束循环监听;
非阻塞模式
实际上ServerSocketChannel是可以设置为非阻塞模式的。在非阻塞模式下,调用accept()函数会立刻返回,如果当前没有请求的链接,那么返回值为空null。因此我们需要手动检查返回的SocketChannel是否为空,例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();