Selector(選擇器)
Selector能夠檢測多個註冊的通道上是否有事件發生(多個channel以事件的方式可以註冊到同一個Selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個鏈接和請求。
只有在連接通道真正有讀寫事件發生時,纔會進行讀寫,這就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程了。並且避免了多線程之間的上下文切換導致的開銷。
圖解說明:
-
多路複用器Selector可以同時併發處理多個客戶端連接
-
當線程從某客戶端 Socket 通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務
-
線程通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出
通道。
-
由於讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率,避免由於頻繁 I/O 阻塞導致的線程掛
起。
-
一個 I/O 線程可以併發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連接一線
程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。
Selector類相關方法
Selector類是一個抽象類,常用方法如下:
public static Selector open();//得到一個選擇器對象
public int select(long timeout);//監控所有註冊的通道,當其中有IO操作可以進行時,將對應的SelectionKey加入到內部集合中並返回,參數用來設置超時時間,即阻塞xx毫秒,在xx毫秒後返回
public Set<SelectionKey> selectedKeys();//從內部集合中得到所有的SelectionKey
public abstract int select() throws IOException;//阻塞
public abstract Selector wakeup();//喚醒Selector
public abstract int selectNow() throws IOException;//不阻塞,立馬返回
NIO非阻塞網絡編程關係梳理圖
圖解說明:
- Selector進行監聽select方法,返回有事件發生的通道的個數
- 當客戶端連接時,會通過ServerSocketChannel得到SocketChannel
- 將socketChannel註冊到Selector上(一個Selector上可以註冊多個SocketChannel)
- 註冊後返回一個SelectionKey,會和該selector關聯
- 利用連接到服務器上的SelectionKey來判斷事件類型
- 判斷了事件之後,如是讀、寫事件,則根據SelectionKey獲取SocketChannel,進行業務處理。
SelectionKey
SelectionKey表示selector和網絡通道的註冊關係,分爲四種:
-
SelectionKey.OP_ACCEPT —— 接收連接繼續事件,表示服務器監聽到了客戶連接,服務器可以接收這個連接了
-
SelectionKey.OP_CONNECT —— 連接就緒事件,表示客戶端與服務器的連接已經建立成功
-
SelectionKey.OP_READ —— 讀就緒事件,表示通道中已經有了可讀的數據,可以執行讀操作了(通道目前有數據,可以進行讀操作了)
-
SelectionKey.OP_WRITE —— 寫就緒事件,表示已經可以向通道寫數據了(通道目前可以用於寫操作)
SelectionKey中常用的方法:
public abstract Selector selector();//得到與該Selectionkey關聯的Selector對象
public abstract SelectableChannel channel();//得到與該Selectionkey關聯的通道
public final Object attachment();//得到與該SelectionKey關聯的共享數據
public abstract SelectionKey intersetOps(int ops);//設置或改變監聽事件
public final boolean isAcceptable();//是否可以連接
public final boolean isReadable();//是否可以讀
public final boolean isWriteable();//是否可以寫
ServerSocketChannel類
ServerSocketChannel在服務器端監聽新的客戶端Socket連接
ServerSocketChannel常用方法:
public static ServerSocketChannel open() throws IOException;//得到一個ServerSocketChannel通道
public final ServerSocketChannel bind(SocketAddress local) throws IOException;//設置服務器端口號
public final SelectableChannel configureBlocking(boolean block);//設置阻塞或非阻塞模式,false表示非阻塞模式
public abstract SocketChannel accept() throws IOException;//接受一個連接,返回代表這個連接的通道對象
public final SelectionKey register(Selector sel, int ops);//註冊一個選擇器並設置監聽事件
public final SelectionKey register(Selector sel, int ops,Object att);//註冊一個選擇器並設置監聽事件,最後一個參數可以設置共享數據
SocketChannel類
SocketChannel,網絡 IO 通道,具體負責進行讀寫操作。NIO 把緩衝區的數據寫入通道,或者把通道里的數據讀到緩衝區。
SocketChannel常用方法:
public static ServerSocketChannel open() throws IOException;//得到一個SocketChannel通道
public final SelectableChannel configureBlocking(boolean block);//設置阻塞或非阻塞模式,false表示非阻塞模式
public boolean connect(SocketAddress remote);//連接服務器
public boolean finishConnect();//如果connect連接失敗,那麼要通過這個方法完成連接操作
public int write(ByteBuffer src);//往通道里寫數據
public int read(ByteBuffer det);//從通道里讀數據
public final SelectionKey register(Selector sel, int ops,Object att);//註冊一個選擇器並設置監聽事件,最後一個參數可以設置共享數據
public final void close();//關閉通道
例子
Demo1:利用NIO,實現服務器端和客戶端之間的簡單數據通訊
服務器端
/**
* 2020/1/8 上午10:09
* 實現服務器端和客戶端之間的數據通訊(非阻塞)
*/
public class NIOServer {
private static String IP = "127.0.0.1";
private static int PORT = 6668;
public static void main(String[] args) throws Exception {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(1000L) == 0) {
continue;
}
//如果客戶端連接上了
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
SocketChannel channel = serverSocketChannel.accept();
System.out.println(" 客 戶 端 連 接 成 功 生 成 了 一 個 socketChannel " +
channel.hashCode());
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
socketChannel.read(buffer);
System.out.println("from 客戶端:" + new String(buffer.array()).trim());
}
iterator.remove();
}
}
}
}
selector輪詢事件,當客戶端連接上服務之後,即產生一個OP_ACCEPT事件,服務器輪詢到該事件後,獲取到當前客戶端連接的通道channel,併爲通道註冊Read事件。 selector隨後會輪詢到Read事件,便會執行Read事件的業務邏輯,比如向通道中寫數據。這樣客戶端就能讀取到通道中寫入的數據,即實現簡單的通訊。
客戶端
public class NIOClient {
private static String IP = "127.0.0.1";
private static int PORT = 6668;
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress inetAddress = new InetSocketAddress(IP, PORT);
if (!socketChannel.connect(inetAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因爲連接需要時間,客戶端不會阻塞,可以做其它工作..");
}
String str = "Hello";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(buffer);
//客戶端阻塞在這裏
System.in.read();
}
}
}
客戶端主要是執行連接服務器的操作
Demo2:實現一個簡單的服務器端與客戶端的羣聊程序
服務器端:
/**
* 2020/1/8 上午11:18
* 實現服務器端與客戶端之間的數據簡單通訊
* 服務器端:檢測用戶上線、離線 並實現消息轉發功能
*/
public class NIOChatServer {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
public NIOChatServer() throws Exception {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(6669));
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public static void main(String[] args) throws Exception {
NIOChatServer server = new NIOChatServer();
server.chat();
}
private void chat() throws Exception {
String user = null;
while (true) {
int connectNum = selector.select();
if (connectNum <= 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println(socketChannel.getRemoteAddress() + "已經上線");
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) {
readMsg(key);
}
iterator.remove();
}
}
}
private void readMsg(SelectionKey key) {
SocketChannel channel = null;
try {
channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len > 0) {
String content = new String(buffer.array(), 0, len);
System.out.println(content.trim());
sendMsgToOtherChannel(content,channel);
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + " 離線了..");
key.cancel();
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
//發送消息給其他客戶端
private void sendMsgToOtherChannel(String msg, SocketChannel self) throws IOException {
//獲取當前選擇器中發生的事件
Set<SelectionKey> keys = selector.keys();
for (SelectionKey key : keys) {
//每個事件與channel是對應的,如果獲取到的Channel不是當前客戶端端的channel,那麼可以認爲是其他客戶端,就可以發送消息
SelectableChannel targetChannel = key.channel();
if (targetChannel instanceof SocketChannel && targetChannel != self) {
SocketChannel socketChannel = (SocketChannel) targetChannel;
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
}
}
}
}
客戶端:
/**
* 2020/1/8 上午11:19
* 無阻塞地發送消息給其他所有用戶,同時可以接受其他用戶發送的消息(服務器轉發)
*/
public class NIOChatClient {
private SocketChannel socketChannel;
private Selector selector;
private String userName;
public NIOChatClient() throws Exception {
socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6669));
socketChannel.configureBlocking(false);
selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_READ);
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName + " is ok...");
}
public static void main(String[] args) throws Exception {
NIOChatClient chatClient = new NIOChatClient();
new Thread(() -> {
try {
while(true){
chatClient.readInfo();
Thread.sleep(3000);
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
chatClient.sendInfo(msg);
}
}
public void sendInfo(String info) throws IOException {
info = userName.concat("說").concat(info);
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}
public void readInfo() throws IOException {
while (true) {
int select = selector.select();
if (select > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len > 0) {
String msg = new String(buffer.array(), 0, len);
System.out.println(msg.trim());
}
}
iterator.remove();
}
}
}
}
}
注意:
1.當向通道中註冊SelectionKey.OP_READ事件後,如果客戶端又向緩存中write數據,下次輪詢時,則isReadable()=true;
2.當向通道中註冊SelectionKey.OP_WRITE事件後,如果不設置爲其他事件,這時你會發現當前輪詢線程中isWritable()一直爲ture(解決方式:write業務處理完之後,將通道註冊爲其他事件)