3.从 Buffer 中读取数据;
4.调用 clear() 方法或者 compact() 方法。
当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 **.flip() 方法将 Buffer 从写模式切换到读模式。**在读模式下,可以读取之前写入到 Buffer 的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
Buffer种类:
CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ByteBuffer、ShortBuffer、FloatBuffer
不同Buffer操作不同的基本数据类型:其底层都是利用数组实现的:例如ByteBuffer底层存储数据就是用的Byte[]:
public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>
{
final byte[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
}
接下来我们看下Buffer的一个使用案例:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class IO_Demo
{
public static void main(String[] args) throws Exception
{
String infile = "D:\\Users\\data.txt";
String outfile = "D:\\Users\\dataO.txt";
// 获取源文件和目标文件的输入输出流
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
// 获取输入输出通道
FileChannel fileChannelIn = fin.getChannel();
FileChannel fileChannelOut = fout.getChannel();
// 创建缓冲区,分配1K堆内存
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true)
{
// clear方法重设缓冲区,使它可以接受读入的数据
buffer.clear();
// 从输入通道中读取数据数据并写入buffer
int r = fileChannelIn.read(buffer);
// read方法返回读取的字节数,可能为零,如果该通道已到达流的末尾,则返回-1
if (r == -1)
{
break;
}
// flip方法将 buffer从写模式切换到读模式
buffer.flip();
// 从buffer中读取数据然后写入到输出通道中
fileChannelOut.write(buffer);
}
//关闭通道
fileChannelOut.close();
fileChannelIn.close();
fout.close();
fin.close();
}
}
首先我们看下Buffer.allocate初始化一个ByteBuffer
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
}
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
可以看到这里初始化了一个1024大小的字节数组,即初始化一个1024字节的byteBuffer用来存储数据。
记下来我们看下 buffer.clear();方法:
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
清空buffer的过程。初始化mark为负1.position为0 limit 等于最大容量。初始化buffer后,开始写入数据。通过inputChannle写入数据的过程稍后介绍、
当写完数据后,调用buffer的flip方法切换为读数据模式
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
当为读取模式时:limit 为写入数据的position,即写入的多少数据。那么就只有多少数据可以读:
比如在写入数据的时候,写入了1000个字节。那么在读取模式下:能读到最大数据即为1000 limit=1000 ,并初始化。第一个读取的数据源为0的位置开始读。
然后通过通道writeChannel写入数据
总结:
position 记录当前读取或者写入的位置,写模式下等于当前写入的单位数据数量,从写模式切换到读模式时,置为 0,在读的过程中等于当前读取单位数据的数量;
limit 代表最多能写入或者读取多少单位的数据,写模式下等于最大容量 capacity;从写模式切换到读模式时,等于position,然后再将 position 置为 0,所以,读模式下,limit 表示最大可读取的数据量,这个值与实际写入的数量相等。
capacity 表示 buffer 容量,创建时分配。
读写模式下:各属性值为下图所示:
接下来我们来看NIO第二个主要的组件Channel。
上面我们大概知道:Buffer是存储数据的缓冲区,Channel是用于传输数据的通道.
磁盘读写数据到内存,都是通过channel读写数据到Buffer。
引用 Java NIO 中权威的说法:通道是 I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。对于离开缓冲区的传输,需要输出的数据被置于一个缓冲区,然后写入通道。对于传回缓冲区的传输,一个通道将数据写入缓冲区中。
Channel 是一个对象,可以通过它读取和写入数据。可以把它看做 IO 中的流。但是它和流相比还有一些不同:
Channel 是双向的,既可以读又可以写,而流是单向的(所谓输入/输出流);
Channel 可以进行异步的读写;
对 Channel的读写必须通过 buffer 对象。
在 Java NIO 中 Channel 主要有如下几种类型:
- FileChannel:从文件读取数据的
- DatagramChannel:读写 UDP 网络协议数据
- SocketChannel:读写 TCP 网络协议数据
- ServerSocketChannel:可以监听 TCP 连接
接着上面的buffer使用案例,我们来看下
FileChannel fileChannelIn = fin.getChannel();
FileChannel fileChannelOut = fout.getChannel();
fileChannelIn.read(buffer);
fileChannelOut.write(buffer);
的源码分析:
fin.getChannel();fin是文件的输入流
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}
FileChannelImpl是FileChannel的实现类、调用FileChannerlImpl.open主要是打开一个FileChannel的通道,用于传输buffer数据。返回一个Filechannle的通道对象、
public static FileChannel open(FileDescriptor var0, String var1, boolean var2, boolean var3, Object var4) {
return new FileChannelImpl(var0, var1, var2, var3, false, var4);
}
private FileChannelImpl(FileDescriptor var1, String var2, boolean var3, boolean var4,
boolean var5, Object var6) {
this.fd = var1;
this.readable = var3;
this.writable = var4;
this.append = var5;
this.parent = var6;
this.path = var2;
this.nd = new FileDispatcherImpl(var5);
}
我们看下传入的参数:FileDescriptor文件描述符对象:文件描述符的主要实际用途是创建一个 FileInputStream 或 FileOutputStream 来包含它:在FileInputStream初始化的时候就会new FileDescriptor对象。并依附在流对象上。定义writable为true 应该是输入流,所以定义为channel为写入buffer的写入通道,由次我们可以看出。Channel是支持双向的、可以读也可以写、
public int read(ByteBuffer var1) throws IOException {
//判断当前通道确实是open的,并且是read通道
this.ensureOpen();
if (!this.readable) {
throw new NonReadableChannelException();
} else {
synchronized(this.positionLock) {
int var3 = 0;
int var4 = -1;
byte var5;
try {
//响应线程中断、
this.begin();
var4 = this.threads.add();
if (this.isOpen()) {
do {
var3 = IOUtil.read(this.fd, var1, -1L, this.nd);
} while(var3 == -3 && this.isOpen());
int var12 = IOStatus.normalize(var3);
return var12;
}
var5 = 0;
} finally {
this.threads.remove(var4);
this.end(var3 > 0);
assert IOStatus.check(var3);
}
return var5;
}
}
}
//IOUtil.read方法
static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
if (var1.isReadOnly()) {
throw new IllegalArgumentException("Read-only buffer");
} else if (var1 instanceof DirectBuffer) {
return readIntoNativeBuffer(var0, var1, var2, var4);
} else {
// 创建一个临时的ByteBuffer 对象
ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());
int var7;
try {
// 将数据读取到临时的buffer对象中去。
int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
var5.flip();
// 当var6不为-1说明还有数据
if (var6 > 0) {
// 将真实数据写入到临时buffer中去、
var1.put(var5);
}
var7 = var6;
} finally {
Util.offerFirstTemporaryDirectBuffer(var5);
}
return var7;
}
}
NIO 第三个核心对象 Selector 详解
通道和缓冲区的机制,使得 Java NIO 实现了同步非阻塞 IO 模式,在此种方式下,用户进程发起一个 IO 操作以后便可返回做其它事情,而无需阻塞地等待 IO 事件的就绪,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。
鉴于此,需要有一个机制来监管这些 IO 事件,如果一个 Channel 不能读写(返回 0),我们可以把这件事记下来,然后切换到其它就绪的连接(channel)继续进行读写。在 Java NIO 中,这个工作由 selector 来完成,这就是所谓的同步。
Selector 是一个对象,它可以接受多个 Channel 注册,监听各个 Channel 上发生的事件,并且能够根据事件情况决定 Channel 读写。这样,通过一个线程可以管理多个 Channel,从而避免为每个 Channel 创建一个线程,节约了系统资源。如果你的应用打开了多个连接(Channel),但每个连接的流量都很低,使用 Selector 就会很方便。
要使用 Selector,就需要向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪,这就是所说的轮询。一旦这个方法返回,线程就可以处理这些事件。
下面这幅图展示了一个线程处理 3 个 Channel 的情况:
接下来我们看下具体的调用例子:
/**
* server 端
*/
public class Server {
private ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
private Selector selector;
public Server() throws IOException{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置非阻塞模式
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8080));
System.out.println("listening on port 8080");
//打开 selector
this.selector = Selector.open();
//在 selector 注册感兴趣的事件
serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
}
private void start() throws Exception{
while(true){
//调用阻塞的select,等待 selector上注册的事件发生
this.selector.select();
//获取就绪事件
Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//先移除该事件,避免重复通知
iterator.remove();
// 新连接
if(selectionKey.isAcceptable()){
System.out.println("isAcceptable");
ServerSocketChannel server = (ServerSocketChannel)selectionKey.channel();
// 新注册channel
SocketChannel socketChannel = server.accept();
if(socketChannel==null){
continue;
}
//非阻塞模式
socketChannel.configureBlocking(false);
//注册读事件(服务端一般不注册 可写事件)
socketChannel.register(selector, SelectionKey.OP_READ);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
buffer.put("hi new channel".getBytes());
buffer.flip();
int writeBytes= socketChannel.write(buffer);
}
// 服务端关心的可读,意味着有数据从client传来了数据
if(selectionKey.isReadable()){
System.out.println("isReadable");
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
readBuffer.clear();
socketChannel.read(readBuffer);
readBuffer.flip();
String receiveData= Charset.forName("UTF-8").decode(readBuffer).toString();
System.out.println("receiveData:"+receiveData);
//这里将收到的数据发回给客户端
writeBuffer.clear();
writeBuffer.put(receiveData.getBytes());
writeBuffer.flip();
while(writeBuffer.hasRemaining()){
//防止写缓冲区满,需要检测是否完全写入
System.out.println("写入数据:"+socketChannel.write(writeBuffer));
}
}
}
}
}
public static void main(String[] args) throws Exception{
new Server().start();
}
}
创建Selector
通过 Selector.open()方法, 我们可以创建一个选择器
将 Channel 注册到Selector 中
我们需要将 Channel 注册到Selector 中,这样才能通过 Selector 监控 Channel :
//非阻塞模式
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注意, 如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的, 即channel.configureBlocking(false);
因为 Channel 必须要是非阻塞的, 因此 FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的.
因为 channel 是非阻塞的,因此当没有数据的时候会理解返回,因此 实际上 Selector 是不断的在轮询其注册的 channel 是否有数据就绪。
在使用 Channel.register()方法时, 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣, 这些事件有:
Connect, 连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT
Accept, 确认事件, 对应于SelectionKey.OP_ACCEPT
Read, 读事件, 对应于SelectionKey.OP_READ, 表示 buffer 可读.
Write, 写事件, 对应于SelectionKey.OP_WRITE, 表示 buffer 可写.
Selector 整体使用 流程
1、建立 ServerSocketChannel
2、通过 Selector.open() 打开一个 Selector.
3、将 Channel 注册到 Selector 中, 并设置需要监听的事件
4、循环:
1、调用 select() 方法
2、调用 selector.selectedKeys() 获取 就绪 Channel
3、迭代每个 selected key:
最后这里附上和前面对应的客户端的代码:
/**
* client 端