传统的BIO方式是基于流进行读写的,而且是阻塞的,整体性能比较差。为了提高I/O性能,JDK1.4引入NIO,他弥补了原来bio的不足,在标准java代码中提供了高速、面向块的I/O。
理解NIO 先从NIO三个核心部分。
通道(channel)
通道是对BIO中流的模拟,到任何目的地的所有数据都必须通过一个通道对象。通道是一个双向的,他比流更好地反映了底层操作系统的真实情况。
主要有以下通道:
- FileChannel 从文件中读写数据。
- DatagramChannel 能通过UDP读写网络中的数据。
- SocketChannel 能通过TCP读写网络中的数据。
- ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
缓冲区(buffer)
尽管通道用于读写数据,但是我们却并不直接操作通道进行读写,而是通过缓冲区完成。
数据是从通道读入缓冲区,从缓冲区写入到通道中的。缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。
在使用缓冲区进行读操作时,我们需要注意是否真的读取了整个缓冲区的全部内容或者发送的数据能否仅使用一次接收方的缓存就可以完整的被接收,所以我们尽量使用以下的代码段
public static void main(String args[]) throws IOException{
RandomAccessFile aFile = new RandomAccessFile("data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//Buffer的分配
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //从Buffer中读取数据
while (bytesRead != -1) {
//flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); //get()方法从Buffer中读取数据的例子
}
//一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。
//如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。
//compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
}
读模式 | 写模式 | |
limit | 当切换Buffer到读模式时,limit会被设置成写模式下的position值。 换句话说,你能读到之前写入的所有数据 | 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。 |
position | 当读取数据时,也是从某个特定位置读。 当将Buffer从写模式切换到读模式, position会被重置为0. 当从Buffer的 position处读取数据时,position向前 移动到下一个可读的位置。 | 当你写数据到Buffer中时,position表示当前的位置。 初始的position值为0.当一个byte、long等数据 写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。 position最大可为capacity – 1. |
选择器(selector)
选择器是NIO中能够检测到一到多个NIO通道,并能够知晓通道是否为诸多读写事件做好准备的组件,他可以以一个单线程管理多个channel,从而管理多个网络连接。
- 管道的注册
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,
Selectionkey.OP_READ);
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。有以下四个常量:
SelectionKey.OP_CONNECT SelectionKey.OP_ACCEPT SelectionKey.OP_READ SelectionKey.OP_WRITE
- selectedKeys
一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。如下所示:
1 | Set selectedKeys = selector.selectedKeys(); |
当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。
可以遍历这个已选择的键集合来访问就绪的通道。如下:
Set selectedKeys = selector.selectedKeys();
Iterator 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();
}
这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。
注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。
SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。
最后。
一个完整的实例:
客户端:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NioClient {
private Selector selector;
private BufferedReader clientInput = new BufferedReader(new InputStreamReader(System.in));
public void init() throws Exception {
// 创建选择器
this.selector = Selector.open();
// 创建socketChannel
SocketChannel scChannel = SocketChannel.open();
//设置为非阻塞 。
//与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式
scChannel.configureBlocking(false);
scChannel.connect(new InetSocketAddress("localhost", 54132));
//注册
scChannel.register(selector, SelectionKey.OP_CONNECT);
}
public void start() throws Exception {
//通过两次循环,避免了在迭代过程中新增的通道
while(true) {
// 下面方法会阻塞,直到至少有一个已注册的事件发生。
selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()) {
SelectionKey key = it.next();
// 先从集合中移除,避免重复处理
it.remove();
// 判断是那种兴趣事件.
if(key.isConnectable()) {
// 连接事件
connect(key);
} else if(key.isReadable() ) {
// 读事件
read(key);
}
}
}
}
private void read(SelectionKey key) throws Exception {
SocketChannel channel = (SocketChannel) key.channel();
//创建读取时用的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据 ,这里以一种简单的方式接收,如果确保安全性,则按照之前提到的方式。
channel.read(buffer);
String responseRes = new String(buffer.array()).trim();
// 读取客户端的下一条请求。
String nextRequest = clientInput.readLine();
ByteBuffer sendBuffer = ByteBuffer.wrap(nextRequest.getBytes());
//发送
channel.write(sendBuffer);
}
private void connect(SelectionKey key) throws Exception {
SocketChannel channel = (SocketChannel) key.channel();
// 正在连接?
if(channel.isConnectionPending()) {
// 完成链接?
if(channel.finishConnect()) {
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
String request = clientInput.readLine();
channel.write(ByteBuffer.wrap(request.getBytes()));
} else {
key.cancel();
}
}
}
public static void main(String[] args) {
NioClient client = new NioClient();
try {
client.init();
client.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务器端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NioServer {
private Selector selector;
public void init() throws IOException {
selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
//绑定端口
InetSocketAddress address = new InetSocketAddress(54132);
serverSocket.bind(address);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public void start() throws Exception {
while(true) {
selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()) {
SelectionKey key = it.next();
it.remove();
//客户端请求链接事件
if(key.isAcceptable()) {
accept(key);
} else if(key.isReadable() ) {
read(key);
}
}
}
}
private void read(SelectionKey key) throws Exception {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
String responseRes = new String(buffer.array()).trim();
ByteBuffer sendBuffer = ByteBuffer.wrap("请求收到".getBytes());
channel.write(sendBuffer);
}
private void accept(SelectionKey key) throws Exception {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
public static void main(String[] args) {
NioServer nioServer =new NioServer();
try {
nioServer.init();
nioServer.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}