文中示例代码:https://github.com/q200737056/Java-Course
一、概述
Java NIO是java 1.4之后新出的一套IO接口,这里的新是相对于原有标准的Java IO和Java Networking接口。NIO提供了一种完全不同的操作方式。NIO中的N可以理解为Non-blocking(非阻塞)。
同步(Sync)vs 异步(Async)
- 同步,发出一个功能调用时,在没有得到结果之前,不会执行后续操作。
- 异步,发出一个功能调用时,会立即返回,不管有没有得到结果。继续执行后续操作。一般通过状态、通知和回调来通知调用者。
阻塞 vs 非阻塞
- 阻塞,调用结果返回之前,当前线程会被阻塞。只有在得到结果之后才会返回。
- 非阻塞,不能立刻得到调用结果时,当前线程不会被阻塞。
其实同步异步与阻塞非阻塞区别是关注的角度不同,同步和异步关注调用后是否等待结果返回。而阻塞和非阻塞关注的是调用时当前的线程是否阻塞。
NIO vs IO
- IO是面向流的,而NIO是面向缓冲区的。
- IO的各种流都是阻塞的。这意味着一个线程一旦调用了read(),write()方法,那么该线程就被阻塞住了;NIO的非阻塞模式使得线程可以通过channel来读数据,并且返回当前已有的数据,或者返回空的(可能没有数据)。这样一来线程不会被阻塞住,它可以继续向下执行。
NIO包含3个核心的组件
- Channel通道。如FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel。
- Buffer缓冲区。如ByteBuffer,CharBuffer,IntBuffer,MappedByteBuffer。
- Selector多路复用器。
二、Channel,Buffer,Selector
Channel通道和流非常相似,主要有以下几点区别
- 通道可以读也可以写,流一般来说是单向的(只能读或者写)。
- 通道可以异步读写。
- 通道总是基于缓冲区Buffer来读写。
通过一个FileChannel的例子来了解如何使用
RandomAccessFile file = new RandomAccessFile("d:/test.txt", "rw");
FileChannel inChannel = file.getChannel();
//开辟一块48字节的缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
//从通道中写入到缓冲区
int bytesRead = inChannel.read(buf);
while(bytesRead != -1) {
//写模式转读模式
buf.flip();
//如果position与limit上限之间有元素,则一个一个读取
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
//清空数据,准备下次写入,实际上只是重置了位置标识
buf.clear();
//继续读
bytesRead = inChannel.read(buf);
}
file.close();
Buffer最重要的3个属性
- 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主要的几个方法介绍
flip()
flip方法可以把Buffer从写模式切换到读模式。会把position归零,并设置limit为之前的position的值。
rewind()
rewind方法将position设置为0,limit保持不变,这样我们可以重复读取buffer中的数据。
clear() 与compact()
clear方法会重置position为0,limit为capacity,也就是把整个Buffer清空,为下次写入做好准备。实际上Buffer中数据并没有清空,我们只是把标记重置了。
compact方法保留未读数据,即重置position为limit-原来的position,limit为capacity,清空了已读部分。
mark()与reset()
通过mark方法可以标记当前的position,通过reset来恢复到原来的position位置。
Selector多路复用器实现了可以用单线程来处理多个channel。对高并发来说大大减少了对线程的开销。
如何使用Selector
创建Selector。
Selector selector = Selector.open();
-
注册Channel到Selector上。Channel必须是非阻塞的。FileChannel不能切换为非阻塞模式,所以不适用。
SocketChannel channel= SocketChannel.open(); channel.configureBlocking(false);SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
register方法第二个参数为关注集合。- SelectionKey.OP_CONNECT(1 << 3)连接就绪状态
- SelectionKey.OP_ACCEPT(1 << 4)可连接就绪状态
- SelectionKey.OP_READ(1 << 0)读就绪状态
- SelectionKey.OP_WRITE(1 << 2)写就绪状态
我们发现这些状态值都使用了位运算,所以想监听多个状态时,可使用位或运算结合多个常量。
SelectionKey.OP_READ | SelectionKey.OP_WRITE
。取出来的时候可以按位与运算。int val = key.interestOps();int op_accept = val & SelectionKey.OP_ACCEPT;
register重载方法还有第3个参数。可以给SelectionKey附加一个Object对象,这样做一方面可以方便我们识别某个特定的channel,同时也增加了channel相关的附加信息。channel.register(selector, SelectionKey.OP_READ, obj);
,也可以调用SelectionKey对象的attach方法添加。key.attach(theObject);
,取出附加信息Object obj= key.attachment();
- 调用selector.select()阻塞等待,select方法底层实现是一个轮询,跟操作系统有关。select方法会返回所有处于就绪状态的channel。
- 返回了有channel就绪之后,获取SelectionKey集合。
Set
,遍历SelectionKey集合,根据各个就绪状态,进行相应的处理。selectedKeys = selector.selectedKeys();
三、NIO套接字通道简单示例
服务端代码
public class HelloServer {
private String name = "";
private Selector selector;
//开出一块1024字节的缓冲区
private ByteBuffer buffer = ByteBuffer.allocate(1024);
private CharsetDecoder decoder = Charset.forName("GB2312").newDecoder();
private CharsetEncoder encoder = Charset.forName("GB2312").newEncoder();
public HelloServer(int port) throws IOException {
selector = this.getSelector(port);
}
private Selector getSelector(int port) throws IOException {
//获得一个ServerSocketChannel通道
ServerSocketChannel server = ServerSocketChannel.open();
// 设置通道为非阻塞
server.configureBlocking(false);
// 绑定端口
server.socket().bind(new InetSocketAddress(port));
// 创建多路复用器
Selector sel = Selector.open();
//将该通道绑定到Selector,并为该通道注册SelectionKey.OP_ACCEPT事件(可连 接事件监听)
//当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
server.register(sel, SelectionKey.OP_ACCEPT);
return sel;
}
public void listen() {
System.out.println("服务端启动成功!");
try {
while(true) {
//当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selectedKey集合的迭代器
Iterator iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 必须手工移除,以防重复处理
iter.remove();
process(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 处理事件
private void process(SelectionKey key) throws IOException {
if (key.isAcceptable()) { // 接收请求
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
//设置非阻塞模式
channel.configureBlocking(false);
//在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读就绪状态。
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 读信息
// 服务器可读取消息,得到事件发生的SocketChannel通道
SocketChannel channel = (SocketChannel) key.channel();
//从Channel写到Buffer
int count = channel.read(buffer);
if (count > 0) {
//将Buffer从写模式切换到读模式
buffer.flip();
//解码
CharBuffer charBuffer = decoder.decode(buffer);
name = charBuffer.toString();
System.out.println("收到客户端信息:"+name);
//给通道设置写就绪
SelectionKey sKey = channel.register(selector,
SelectionKey.OP_WRITE);
sKey.attach(name);//附加name数据
} else {
channel.close();
}
//清空缓冲区
buffer.clear();
} else if (key.isWritable()) { // 写事件
SocketChannel channel = (SocketChannel) key.channel();
//获取附加的name
String name = (String) key.attachment();
System.out.println("向客户端发送消息:"+"Hello! " + name);
// 编码
ByteBuffer block = encoder.encode(CharBuffer.wrap("Hello! " + name));
//从Buffer读取数据到Channel
channel.write(block);
//关闭客户端通道
channel.close();
}
}
public static void main(String[] args) throws IOException {
//单线程 管理多个SocketChannel通道
HelloServer server = new HelloServer(8888);
server.listen();
}
}
客户端代码
public class HelloClient {
private InetSocketAddress ip = new InetSocketAddress("localhost", 8888);
private CharsetDecoder decoder = Charset.forName("GB2312").newDecoder();
private CharsetEncoder encoder = Charset.forName("GB2312").newEncoder();
class Message implements Runnable {
private String name;
private String msg = "";
public Message(String name) {
this.name = name;
}
public void run() {
try {
long start = System.currentTimeMillis();
//打开SocketChannel通道
SocketChannel client = SocketChannel.open();
//设置为非阻塞模式
client.configureBlocking(false);
// 创建多路复用器
Selector selector = Selector.open();
//注册连接就绪状态
client.register(selector, SelectionKey.OP_CONNECT);
//去连接
client.connect(ip);
//创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
_FOR: while(true) {
selector.select();
// 获得selectedKey集合的迭代器
Iterator iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 必须手工移除,以防重复处理
iter.remove();
if (key.isConnectable()) { // 连接事件监听
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending())
channel.finishConnect();
//向服务器发送数据
System.out.println("向服务端发送消息:"+name);
channel.write(encoder.encode(CharBuffer.wrap(name)));
//在和服务端连接成功之后,给通道设置读就绪状态
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 可读事件监听
SocketChannel channel = (SocketChannel) key.channel();
//从Channel写到Buffer
int count = channel.read(buffer);
if (count > 0) {
//将Buffer从写模式切换到读模式
buffer.flip();
CharBuffer charBuffer = decoder.decode(buffer);
msg = charBuffer.toString();
System.out.println("收到服务端信息:"+msg);
//清空整个缓存,compact()方法只会清除已经读过的数据
buffer.clear();
} else {
System.out.println(name+"没数据可读,关闭客户端");
client.close();
break _FOR;
}
}
}
}
double last = (System.currentTimeMillis() - start) * 1.0 / 1000;
System.out.println(name+"使用时间 :" + last + "s.");
msg = "";
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
HelloClient cli = new HelloClient();
for (int index = 0; index < 10; index++) {
System.out.println("开启客户端"+index);
new Thread(cli.new Message("client[" + index + "]").start();
}
}
}
四、异步文件通道
Java7中新增了AsynchronousFileChannel异步文件通道,使得数据可以进行异步读写。
以下简单例子
public class AIOTest {
public static void main(String[] args) throws IOException {
AIOTest test = new AIOTest();
test.writeTest("d:/1.txt");
test.readTest("d:/1.txt");
}
public void readTest(String filename) throws IOException{
Path path = Paths.get(filename);
AsynchronousFileChannel fileChannel =
AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
int position = 0;
Future operation = fileChannel.read(buffer, position);
//是否操作完成
while(!operation.isDone());
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();
}
public void writeTest(String filename) throws IOException{
Path path = Paths.get(filename);
if(!Files.exists(path)){
Files.createFile(path);
}
AsynchronousFileChannel fileChannel =
AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
int position = 0;
//是否操作完成
buffer.put("112345678".getBytes());
buffer.flip();
Future operation = fileChannel.write(buffer, position);
buffer.clear();
while(!operation.isDone());
System.out.println("Write done");
}
}