介紹了jdk實現nio的關鍵Selector以及SelectableChannel,瞭解了它的原理,就明白了netty爲什麼是事件驅動模型:(netty極簡教程(四):Selector事件驅動以及SocketChannel
的使用,接下來將它的使用更深入一步, nio reactor模型演進以及聊天室的實現;
示例源碼: https://github.com/jsbintask22/netty-learning
nio server
對於io消耗而言,我們知道提升效率的關鍵在於服務端對於io的使用;而nio壓榨cpu的關鍵在於使用Selector
實現的reactor
事件模型以及多線程的加入時機:
單線程reactor模型
省略Selector以及ServerSocketChannel的獲取註冊; 將所有的操作至於reactor主線程
while (true) { // 1
if (selector.select(1000) == 0) { // 2
continue;
}
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); // 3
while (selectedKeys.hasNext()) {
SelectionKey selectionKey = selectedKeys.next();
SelectableChannel channel = selectionKey.channel();
if (selectionKey.isAcceptable()) { // 4
ServerSocketChannel server = (ServerSocketChannel) channel;
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(CLIENT_BUFFER_SIZE));
String serverGlobalInfo = "系統消息:用戶[" + client.getRemoteAddress() + "]上線了";
System.err.println(serverGlobalInfo);
forwardClientMsg(serverGlobalInfo, client); // 5
} else if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) channel;
SocketAddress remoteAddress = null;
try {
remoteAddress = client.getRemoteAddress();
String clientMsg = retrieveClientMsg(selectionKey);
if (clientMsg.equals("")) {
return;
}
System.err.println("收到用戶[" + remoteAddress + "]消息:" + clientMsg);
forwardClientMsg("[" + remoteAddress + "]:" + clientMsg, client); // 6
} catch (Exception e) {
String msg = "系統消息:" + remoteAddress + "下線了";
forwardClientMsg(msg, client);
System.err.println(msg);
selectionKey.cancel(); // 7
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
selectedKeys.remove();
}
}
- 開啓一個while循環,讓Selector不斷的詢問操作系統是否有對應的事件已經準備好
- Selector檢查事件(等待時間爲1s),如果沒有直接開啓下一次循環
- 獲取已經準備好的事件(
SelectionKey
),然後依次循環遍歷處理 - 如果是
Accept
事件,說明是ServerSocketChannel註冊的,說明新的連接已經建立好了,從中獲取新的連接並將新連接再次註冊到Selector - 註冊後,然後生成消息給其它Socket,表示有新用戶上線了
- 如果是
Read
事件,說明客戶端Socket有新的數據可讀取,讀取然後廣播該消息到其它所有客戶端 - 如果發生異常,表示該客戶端斷開連接了(粗略的處理),同樣廣播一條消息,並且將該Socket從Selector上註銷
讀取以及廣播消息方法如下:
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
int len = client.read(buffer);
if (len == 0) {
return "";
}
buffer.flip();
byte[] data = new byte[buffer.remaining()];
int index = 0;
while (len != index) {
data[index++] = buffer.get();
}
buffer.clear();
return new String(data, StandardCharsets.UTF_8);
Set<SelectionKey> allClient = selector.keys();
allClient.forEach(selectionKey -> {
SelectableChannel channel = selectionKey.channel();
if (!(channel instanceof ServerSocketChannel) && channel != client) { // 1
SocketChannel otherClient = (SocketChannel) channel;
try {
otherClient.write(ByteBuffer.wrap(clientMsg.getBytes(StandardCharsets.UTF_8)));
} catch (IOException e) {
e.printStackTrace();
}
}
});
從Selector上獲取所有註冊的Channel然後遍歷,如果不是ServerSocketChannel或者當前消息的Channel,就將消息發送出去.
以上,所有代碼放在同一線程中,對於單核cpu而言,相比於bio的Socket
編程,我們主要有一個方面的改進
- 雖然
accept
方法依然是阻塞的,可是我們已經知道了肯定會有新的連接進來,所以調用改方法不會再阻塞而是直接獲取一個新連接 - 對於
read
方法而言同樣如此,雖然該方法依然是一個阻塞的方法,可是我們已經知道了接下來調用必定會有有效數據,這樣cpu不用再進行等待 - 通過Selector在一個線程中便管理了多個Channel
而對於多核cpu而言,Selector雖然能夠有效規避accept和read的無用等待時間,可是它依然存在一些問題;
- 上面的操作關鍵在於Selector的
select
操作,該方法必須能夠快速循環調用,不宜和其它io讀取寫入放在一起 - channel的io(read和write)操作較爲耗時,不宜放到同一線程中處理
多線程reactor模型
基於上面的單線程問題考慮,我們可以將io操作放入線程池中處理:
- 將accept事件的廣播放入線程池中處理
- 將read事件的所有io操作放入線程池中處理
if (selectionKey.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) channel;
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(CLIENT_BUFFER_SIZE));
String serverGlobalInfo = "系統消息:用戶[" + client.getRemoteAddress() + "]上線了";
System.err.println(serverGlobalInfo);
executorService.submit(() -> { // 1
forwardClientMsg(serverGlobalInfo, client);
});
} else if (selectionKey.isReadable()) {
executorService.submit(() -> { // 2
SocketChannel client = (SocketChannel) channel;
SocketAddress remoteAddress = null;
try {
remoteAddress = client.getRemoteAddress();
String clientMsg = retrieveClientMsg(selectionKey);
if (clientMsg.equals("")) {
return;
}
System.err.println("收到用戶[" + remoteAddress + "]消息:" + clientMsg);
forwardClientMsg("[" + remoteAddress + "]:" + clientMsg, client);
} catch (Exception e) {
String msg = "系統消息:" + remoteAddress + "下線了";
forwardClientMsg(msg, client);
System.err.println(msg);
selectionKey.cancel();
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
}
selectedKeys.remove();
}
在 1與2處,我們加入了線程池處理,不再在reactor主線程中做任何io操作。 這便是reactor多線程模型
雖然模型2有效利用了多核cpu優勢,可是依然能夠找到瓶頸
- 雖然廣播消息是在一個獨立線程中,可是我們需要將Selector上註冊的所有的channel全部遍歷,如果Selector註冊了太多的channel,依舊會有效率問題
- 因爲Selector註冊了過多的Channel,所以在進行select選取時對於主線程而言依舊會有很多的循環操作,存在瓶頸
基於以上問題,我們可以考慮引入多個Selector
,這樣主Selector只負責讀取accept操作,而其他的io操作均有子Selector負責,這便是多Reactor多線程模型
多Reactor多線程模型
基於上面的思考,我們要在單Reactor多線程模型上主要需要以下操作
- 對於accept到的新連接不再放入主Selector,將其加入多個
子Selector
- 子Selector操作應該在異步線程中進行.
- 所有子Selector只進行read write操作
基於以上,會增加一個子Selector列表,並且將原來的accept以及讀取廣播分開;
private List<Selector> subSelector = new ArrayList<>(8);
定義一個包含8個子selector的列表並進行初始化
如圖,分別開啓了一個reactor主線程,以及8個子selector子線程,其中,主線程現在只進行accept然後添加至子selector
while (true) {
if (mainSelector.select(1000) == 0) {
continue;
}
Iterator<SelectionKey> selectedKeys = mainSelector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey selectionKey = selectedKeys.next();
SelectableChannel channel = selectionKey.channel();
if (selectionKey.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) channel;
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(subSelector.get(index++), SelectionKey.OP_READ, // 1
ByteBuffer.allocate(CLIENT_BUFFER_SIZE));
if (index == 8) { // 2
index = 0;
}
String serverGlobalInfo = "系統消息:用戶[" + client.getRemoteAddress() + "]上線了";
System.err.println(serverGlobalInfo);
forwardClientMsg(serverGlobalInfo, client);
}
}
selectedKeys.remove();
}
- 將新連接註冊至從Selector.
- 如果當前的selector已經全部添加了一遍則重新從第一個開始
所有的從Selector只進行io操作,並且本身已經在異步線程中運行
while (true) {
if (subSelector.select(1000) == 0) {
continue;
}
Iterator<SelectionKey> selectedKeys = subSelector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey selectionKey = selectedKeys.next();
SelectableChannel channel = selectionKey.channel();
if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) channel;
SocketAddress remoteAddress = null;
try {
remoteAddress = client.getRemoteAddress();
String clientMsg = retrieveClientMsg(selectionKey); // 1
if (clientMsg.equals("")) {
return;
}
System.err.println("收到用戶[" + remoteAddress + "]消息:" + clientMsg);
forwardClientMsg("[" + remoteAddress + "]:" + clientMsg, client); // 2
} catch (Exception e) {
String msg = "系統消息:" + remoteAddress + "下線了";
forwardClientMsg(msg, client);
System.err.println(msg);
selectionKey.cancel();
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
selectedKeys.remove();
}
- 讀取消息
-
廣播消息
啓動server,並且打開三個客戶端:
如圖所示,上線通知,消息轉發,下線通知成功, 主Selector與從Selector交互成功
netty線程模型思考
事實上,在netty的線程模型中,與上方的多Reactor多線程模型類似
,一個改進版的多路複用多Reactor模型; Reactor主從線程模型
- 一個主線程不斷輪詢進行accept操作,將channel註冊至子Selector
- 一個線程持有一個Selector
- 一個子Selector又可以管理多個channel
- 在斷開連接前,一個channel總是在同一個線程中進行io操作處理
基於以上思考,我們將在後面在netty源碼中進行一一驗證。