【死磕 NIO】— ServerSocketChannel 到底有什么缺陷?
大家好,我是大明哥,一個(gè)專注于【死磕 Java】的程序員。?
【死磕 Java 】系列為作者「chenssy」 傾情打造的 Java 系列文章,深入分析 Java 相關(guān)技術(shù)核心原理及源碼。?
死磕 Java :https://www.cmsblogs.com/group/1420041599311810560
上篇文章大明哥介紹了 SocketChannel 的核心原理及其源碼,這篇文章就來介紹如何使用 ServerSocketChannel,分析單獨(dú)使用 ServerSocketChannel 存在哪些問題。

阻塞模式
我們先看服務(wù)端方法:
public static void main(String[] args) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(100);
ServerSocketChannel serverSocket = ServerSocketChannel.open()
serverSocket.bind(new InetSocketAddress(8080));
List channels = new ArrayList<>();
while(true) {
SocketChannel sc = serverSocket.accept();
channels.add(sc);
for (SocketChannel asc :channels) {
asc.read(buffer);
buffer.flip();
ByteBufferUtil.debugAll(buffer);
buffer.clear();
}
}
}
首先新建一個(gè) ServerSocketChannel 類,同時(shí)綁定 8080 端口 然后 while(true)循環(huán)調(diào)用accept()來建立連接,同時(shí)將該 SocketChannel 加入到 List 集合中,該集合用來裝所有與服務(wù)端建立連接的 SocketChannel最后接受客戶端發(fā)送來的數(shù)據(jù),打印出來
現(xiàn)在我們來運(yùn)行下(這里就不寫客戶端程序了,就用 MAC 的 iTerm 來模擬即可)。需要開 2 個(gè)客戶端。
我們先打開 client-01,然后發(fā)送一條 “hi,i am client-01”

服務(wù)器運(yùn)行結(jié)果:

服務(wù)端準(zhǔn)確無誤打印出 client-01 發(fā)送過來的消息(hi,i am client-01)。這個(gè)時(shí)候你再發(fā)一條消息:“hi,i am client-11”,你會(huì)驚奇地發(fā)現(xiàn),服務(wù)端竟然不輸出客戶端發(fā)來的消息。這個(gè)時(shí)候你再啟動(dòng) client-02,神奇的事情發(fā)生了,服務(wù)端把 client-01 發(fā)送的消息(hi,i am client-11)給打印出來了:

為什么會(huì)出現(xiàn)這種神奇的現(xiàn)象?主要原因就是該 ServerSocketChannel 是阻塞模式,相關(guān)方法都會(huì)導(dǎo)致線程的阻塞,當(dāng) client-01 建立連接,第一次發(fā)送消息時(shí),服務(wù)端正常打印消息(hi,i am client-01),這時(shí)服務(wù)端又運(yùn)行到 accept(),注意這個(gè)方法是阻塞方法,如果沒有客戶端來建立連接,它會(huì)一直阻塞在這里,哪怕 client-01 再次發(fā)送消息(hi,i am client-11),服務(wù)端也不會(huì)打印。這時(shí) client-02 與服務(wù)端建立連接,服務(wù)端就不會(huì)阻塞,打印 client-01 第二次發(fā)來的消息(hi,i am client-11)。
所以,阻塞模式存在如下缺陷
單線程情況下,阻塞方法都會(huì)導(dǎo)致線程暫停 ServerSocketChannel.accept()會(huì)在沒有連接建立時(shí)讓線程暫停,即使有客戶端向服務(wù)端發(fā)送消息,服務(wù)單也接收不到直到有新客戶端連接服務(wù)端,不再阻塞在accept()方法上。SocketChannel.read()會(huì)在通道中沒有數(shù)據(jù)可讀時(shí)讓線程暫停,即使之后有新客戶端向服務(wù)端發(fā)起連接請(qǐng)求也接受不了,直到讀取完畢,不再阻塞在read()方法上
所以在單線程情況,服務(wù)端幾乎不可能正常工作。那多線程呢?多線程情況下,如果連接數(shù)過多,必然會(huì)導(dǎo)致 OOM,然后線程的上下文切換也會(huì)導(dǎo)致性能低下。
非阻塞模式
上面的阻塞模式幾乎導(dǎo)致整個(gè)服務(wù)端是可能使用的,我們是可以使用非阻塞模式來避免的。如下
public static void main(String[] args) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(100);
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.bind(new InetSocketAddress(8080));
List channels = new ArrayList<>();
while(true) {
// 非阻塞模式
serverSocket.configureBlocking(false);
SocketChannel sc = serverSocket.accept();
if (sc != null){
channels.add(sc);
}
for (SocketChannel asc :channels) {
asc.configureBlocking(false);
int size = asc.read(buffer);
if (size > 0) {
buffer.flip();
ByteBufferUtil.debugAll(buffer);
buffer.clear();
}
}
}
}
通過 ServerSocketChannel.configureBlocking(false)將 serverSocket 設(shè)置為非阻塞模式,這樣 serverSocket 在調(diào)用accept()方法時(shí)就不會(huì)阻塞了,如果沒有連接,則會(huì)返回 null通過 SocketChannel..configureBlocking(false)將 asc 設(shè)置為非阻塞模式,這 asc 在調(diào)用read()方法就不會(huì)阻塞了,如果沒有可讀數(shù)據(jù),它則會(huì)返回 -1。
非阻塞模式雖然不會(huì)影響業(yè)務(wù)的使用,但由于在 while(true) 循環(huán)里面,CPU 會(huì)一直處理運(yùn)行狀態(tài),占用和浪費(fèi) CPU 資源。
所以,采用這種 while(true) 循環(huán)的暴力方式根本就不適合業(yè)務(wù)使用,對(duì)于 SocketChannel 而言,我們希望他只擔(dān)任一個(gè)通道,傳傳數(shù)據(jù)的角色即可,不需再有額外的角色了,故而我們不能放任他們,需要對(duì)其進(jìn)行統(tǒng)一管理,既要有管理器,有連接來了,我就告訴你該建立連接了,有要讀的數(shù)據(jù),我就告訴你可以讀數(shù)據(jù)了,這樣 SocketChannel 是不是就很爽了。在 NIO 中,這個(gè)管理器稱之為 Selector。
