上篇基於Java Socket實現同步非阻塞通信中展示了非阻塞的聊天示例,ServerSocket#accept接收連接後,會創建一個不斷輪訓是否有讀寫數據的線程。不斷輪訓是很消耗CPU資源的,本篇基於Java NIO Selector的多路複用IO模型,將解決這一問題。
方法:Selector可監聽Channel的OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE狀態,然後分發給Handler處理器。關聯了Socketd的SocketChannel可以使用configureBlocking方法將自己設置爲非阻塞狀態。
示例代碼使用Selector來監聽SocketChannel中是否有數據可讀、ServerSocketChannel是否接收的新的連接請求。該檢測是阻塞的,當有事件可處理時才通知處理器去處理。因爲是聊天示例,所以需要對每個網絡連接關聯一個阻塞的發消息線程,平常掛起,有輸入時發送消息,但是對每個連接都不斷地輪訓Socket中是否有數據了。
多路複用IO模型代碼
Server.java
: IO模型的主要部分。如果Channel在向Selector註冊時附加了Buffer,當buffer中數據沒有處理完的話,下次select()還會選中該SocketChannel。如果事情處理完了,可以取消註冊,即使Selector中沒有註冊Channel,select()方法還是會阻塞的。
Client.java
: 客戶端是否阻塞都行,爲了練習NIO,我將客戶端寫成了使用Selector、非阻塞的形式。
ConsoleThread.java
發送消息的線程類。
IO不可中斷等待狀態線程的改寫
結束通信時需要關閉SocketChannel和發消息線程。如果在發消息線程中使用了
BufferedReader#readLine()
方法,當該方法阻塞時,是不被外部控制的,即使發消息線程調用interrupt方法,該線程也不會被中斷結束。
因此,需要將讀取操作改爲掛起可中斷的IO讀形式:先用不可阻塞的BufferedReader#ready()方法判斷數據是否準備好,若無則使用Thread.sleep(1000)方法將線程掛起1秒,否則讀取數據。詳細寫法見ConsoleThread.java代碼片段。
附I/O複用模型圖
package syncnonblocking;
import java.io.IOException;
import java.net.InetSocketAddress;
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 Server {
private static final int BUF_SIZE = 256;
public static void main(String[] args) {
try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();){
serverSocketChannel.configureBlocking(false); // non blocking
serverSocketChannel.socket().bind(new InetSocketAddress(13579));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(selector.select() > 0) { // blocking method. when a client request for accept, selector.selector() > 1 is true
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
switch (key.readyOps()) {
case SelectionKey.OP_ACCEPT: // accept from connection
ServerSocketChannel clientChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = clientChannel.accept();
socketChannel.configureBlocking(false);
// 如果註冊時添加了ByteBuffer,只要buf不空,OP_READ會被一直調用
ConsoleThread thread = new ConsoleThread(socketChannel);
thread.start();
socketChannel.register(selector, SelectionKey.OP_READ, thread);
break;
case SelectionKey.OP_READ: // read from a channel
ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(buffer);
buffer.flip();
byte[] tmp = new byte[buffer.limit() - buffer.position()];
buffer.get(tmp);
// buffer.clear();
String msg = new String(tmp);
if (msg.equals("exit")) {
channel.write(ByteBuffer.wrap("exit".getBytes()));
((ConsoleThread)key.attachment()).interrupt();
key.cancel();
} else {
System.out.println(msg);
}
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package syncnonblocking;
import java.io.IOException;
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;
import java.util.Set;
public class Client {
private static final int BUF_SIZE = 1024 * 2;
public static void main(String[] args) {
ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
try (SocketChannel clientChannel = SocketChannel.open();
Selector selector = Selector.open();) {
clientChannel.connect(new InetSocketAddress("127.0.0.1", 13579));
clientChannel.configureBlocking(false);
ConsoleThread thread = new ConsoleThread(clientChannel);;
clientChannel.register(selector, SelectionKey.OP_READ, thread);
thread.start();
boolean flag = true;
// 客戶端沒有必要使用Selector,我是爲了練習而使用的。或者P2P通信時可以這樣寫
while(flag && selector.select() > 0) { // 不要先select()再flag,因爲select會阻塞flag的判斷
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
SocketChannel readChannel = (SocketChannel) key.channel();
buf.clear();
readChannel.read(buf);
buf.flip();
byte[] tmp = new byte[buf.limit()];
buf.get(tmp);
buf.clear();
String msg = new String(tmp);
if (msg.equals("exit")) {
readChannel.write(ByteBuffer.wrap("exit".getBytes()));
((ConsoleThread)key.attachment()).interrupt();
key.cancel();
flag = false;
} else {
System.out.println(msg);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package syncnonblocking;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ConsoleThread extends Thread {
private SocketChannel socketChannel;
public ConsoleThread(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));) {
while (!isInterrupted()) {
if (reader.ready()) {
String msg = reader.readLine();
ByteBuffer buf = ByteBuffer.wrap(msg.getBytes());
socketChannel.write(buf);
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
// System.out.println("ConsoleThread is over.");
}
}
順帶擼一遍Java NIO包含什麼
- 通道Channel: FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel、AsynchronousFileChannel
- 緩衝Buffer:ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, FloatBuffer, LongBuffer, DoubleBuffer, MappedByteBUffer。有時需要注意數據的的大小端問題。
- 選擇器Selector:open、select方法、SelectionKey。
- Pipe管道
- Paths path: get, normalize
- Files: exies, createDirectory, copy, move, move, walkFileTree