Java NIO对于数据的处理是使用管道(Channel)和缓冲区(Buffer)来进行处理的,可以从管道读取数据到缓冲区,也可以从缓冲区读取数据写入到管道。
1、分散(scatter)读取
从一个管道读取数据写入到多个缓冲区
从管道读取数据写入数据到一个缓冲区,一个缓冲区满了之后再去写入到另一个缓冲区
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
2、聚合(gather)写入
从多个缓冲区读取数据写入到一个管道
按顺序读取buffer数组的数据写入到管道,写入的数据在buffer中的位置不能超过position和limit之间这个限制
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into channel
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
3、从一个管道传输到另一个管道
(1) transferFrom()可以从其它管道传输数据到FileChannel
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
position指的是从目标文件的哪个位置写入数据,count指的是可传输的最大字节数
SockectChannel会传输此时已经准备好的数据到另一个Channel,并不一定会传输请求的所有数据(count个字节的数据)
(2) TransferTo()可以从一个FileChannel的数据到另一个其它的管道
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
SocketChannel的问题和上面的TransferFrom()方法提到的问题一样
4、选择器(Selector)
使用选择器可以做到一个线程管理多个管道
//创建选择器
Selector selector = Selector.open();
//注册Channel到Selector
SocketChannel channel = SocketChannel.open();
//channel注册到Selector里面需要是非阻塞的
channel.configureBlocking(false);
//注册到Selector中,设置的监听的触发事件为可以读取数据
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
//兴趣集
int interestSet = key.interestOps();
boolean isInterestedInAccept = SelectionKey.OP_ACCEPT == (interestSet & SelectionKey.OP_ACCEPT);
boolean isInterestedInRead = SelectionKey.OP_READ == (interestSet & SelectionKey.OP_READ);
//获取就绪集
int readySet = key.readyOps();
//判断是否就绪的方法
//管道是否已经准备就绪接收传入的连接
key.isAcceptable();
//管道是否已经与一台服务器成功建立连接
key.isConnectable();
//管道是否已经有数据已经准备就绪可以读取
key.isReadable();
//管道是否已经准备就绪可以写入数据
key.isWritable();
//获取管道
Channel channel1 = key.channel();
//获取选择器
Selector selector1 = key.selector();
//附加对象或者一些信息到SelectionKey
key.attach("");
Object attachedObj = key.attachment();
//在注册的时候附加对象
SelectionKey key1 = channel.register(selector, SelectionKey.OP_READ, "");
//通过获取到的已选择的键集来访问已经就绪的管道
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key2 = keyIterator.next();
if (key2.isAcceptable()) {
//一个连接已经被一个ServerSocketChannel接收了
} else if (key2.isConnectable()) {
//已经和一个远程服务器建立了连接
} else if (key2.isReadable()) {
//一个管道的数据已经准备就绪可以进行读取
} else if (key2.isWritable()) {
//一个管道已经准备就绪可以进行写入数据
}
keyIterator.remove();
}
//如果线程1调用了select()方法进入阻塞的状态,即使是没有管道是准备就绪的,线程2可以通过调用wakeup()方法来使得线程1可以离开select()方法的阻塞状态
selector.wakeup();
//调用该方法可以关闭选择器和使SelectionKey实例无效,但是管道自身不会被关闭
selector.close();
5、FileChannel
被用于连接到一个文件,可以从文件读取数据,也可以写入数据到文件里。这个管道运行的时候是阻塞模式的。
6、SocketChannel
//创建一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
//读取管道数据到缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
//从缓冲区读取数据写入到管道
String newData = "New String to write to file..." + System.currentTimeMillis();
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
socketChannel.write(buf);
}
//设置为非阻塞模式
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
//判断是否和服务器已经建立连接
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
socketChannel.close();
7、ServerSocketChannel
//打开一个管道,能够监听接入的TCP连接
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
//监听接入的连接,返回一个带有连接的Channel,是阻塞模式,需要等待有连接到达才返回
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
//设置为非阻塞模式,立即返回,不管是否有连接接入,使用前需要校验返回的Channel是否为null
serverSocketChannel.configureBlocking(false);
//关闭管道
serverSocketChannel.close();
8、DatagramChannel
DatagramChannel能够发送和接收UDP的数据包。
//打开一个DatagramChannel
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
//从收到的数据包中复制数据到给定的缓冲区,如果数据包的内容量大于缓冲区的容量,超出的部分将会被忽略掉
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
//发送数据到指定的网址
String newData = "New String to write to file..."+System.currentTimeMillis();
ByteBuffer buf1 = ByteBuffer.allocate(48);
buf1.clear();
buf1.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf1,new InetSocketAddress("localhost",80));
//连接到特定的地址
channel.connect(new InetSocketAddress("localhost",80));
int bytesRead1 = channel.read(buf);
int bytesWritten1 = channel.write(buf);
9、Java NIO Pipe
一个Java NIO Pipe在两个线程之间传输数据的数据连接方式,一个Pie包含一个源管道和一个接收管道,线程A可以写数据到接收管道,这些数据能够从源管道读取出来
10、Java NIO适用的情况
可以通过Java NIO来实现一个线程或者比较少的线程来管理多个Channel。如果需要管理成千上万的连接,而每一个连接只是发送一小部分数据,例如一个聊天服务器,这种比较适合用NIO。如果是需要保持和一些计算机的许多开放连接,例如P2P网络,这种情况下使用一个线程来管理多个出站连接是比较适合的,下面是一个线程管理多个连接的图:
如果是只有少量的连接,但是一次传输的数据量却很大,传统的IO实现会比较适合,下面传统IO的设计图: