各位騷年好,我又來了.

上次寫文章是啥時候? 忘了,也懶得翻記錄,總之,很久沒寫知乎文章了其實吧,不是很願意把這個所謂的碎碎念記下的一些筆記(很多還是從其他地方抄的,做了一些修改和補充)叫做所謂的「文章」.感覺是對這兩個字的侮辱. 或許叫做學習筆記,可能更合適,不過先這麼稱呼吧,總比「震驚,26歲大叔居然對Netty做出這樣的事情...」這類喪心病狂的標題好一些.

回想自己年輕的時候,在那本科的青蔥歲月里,我還是一個非常英俊,頭髮茂密的少年.追求者排成了一條長龍,從杭州市,一直排到了銀河系外(此處是幻想).

然而,作為一個要在計算機領域有所建樹的有志青年(此處是痴心妄想!),「兩耳不聞窗外事,一心只敲心中碼」.在每一個炎熱的中午,專註地和大學室友構思著一件驚天動地,百思不解的大事:「午飯去哪個食堂吃飯,吃些什麼!」然後回來繼續做著我們的「網路編程」大作業:「利用windows C socket ,自定通訊協議.完成一個即時通訊工具,按照能支撐的並發連接數量,評定最終成績」.

好傢夥,我終於可以和「騰訊」一決高下了!(不知天高地厚)

中間過程略過不提,最終是採用了windows的重疊非同步IO埠模型(windows平台的IOCP),在Acer電腦(4G內存,i3雙核處理器上支持2萬個並發連接),整個組全部拿到了優秀評級.

這樣就說明自己真的優秀了嗎? 其實自己做的事情真沒多少,只是定義了一些通訊協議,根據編程模型,調用了windows提供的底層API,並做了一些比較繁瑣的調用和事件處理操作.平時工作如果叫做搬磚的話,那麼這個最多就是「花式搬磚」.

不過,就這個「調用windows C socket API」這個操作,說起來簡單,調用過程還是相當繁瑣的.大量的樣板代碼.(當初的代碼不知道哪裡去了,不然可以貼上來一段).在C/C++領域,不知道是否有成型的通訊框架(很久沒接觸這塊了),但是好在Java對這些繁瑣的操作做了封裝,實現了Netty框架.對外屏蔽了很多繁瑣的操作,讓一般開發者不用關心底層的介面調用.

我相信很多騷年應該都會聽說過Netty(就算沒有真正使用過).特別是去網上搜索「高並發網路編程 Java」,然後你就會搜索到Netty,隨著Dive into 的過程,哈哈哈,你就會像我一樣,去某個地方記一下筆記

進入正題:

先介紹一下Socket先生,它是無人不知,無人不曉,統領TCP和UDP兩位小弟,在網路界里馳騁風雲.典型的高富帥人物.在計算機的世界裡,兩個的非同一個主機進程之間(IP地址+埠標記一個進程,我們可以稱做為「端點Point」)想要進行友好溝通,必須勞煩Socket進行消息傳達工作.對於我們要傳達的消息,我們需要告訴socket,對方的埠號,以及IP地址(也就是傳說中的「點對點通信」)

在服務端的樣板代碼會是這麼個樣子

ServerSocket serverSocket = new ServerSocket(1111);//這裡的1111是服務端的socket監聽埠號
Socket clientSocket = serverSocket.accept();//等待客戶端向服務端發起請求
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true);
String request,response;
while (((request= in.readLine())!=null)){//不斷循環等待客戶端發送的消息
if("Done".equals(request)){
break;
}
response = request;
out.println(response);//將客戶端發來的消息原封不動地發送回去
}

啟動上面的代碼,我們可以用命令工具 telnet做一個小測試:

其中紅色的helloWorld是客戶端(也就是這個命令行界面)發送給上述的服務端的消息

其中綠色的helloWorld是服務端返回給該客戶端的消息

這個時候我們再起一個客戶端,進行同樣的操作,這個時候新的客戶端不管發什麼消息,都將得不到響應,問題在於上述代碼中等待監聽的 serverSocket.accept()在收到一個客戶端的連接後,將進入到後續代碼的while (((request= in.readLine())!=null))中,循環等待這個客戶端發送的信息,不在理會新的連接請求(也無法理會,應為監聽代碼accpet已經執行過去了)

這個時候,我們來改造一下,改成下面的代碼:

ServerSocket serverSocket = new ServerSocket(1111);
while (true) {
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String request, response;
while (((request = in.readLine()) != null)) {
if ("Done".equals(request)) {
break;
}
response = request;
out.println(response);
}
}

這麼做還是存在問題,當一個客戶端連接進來,並且不終端連接的情況下,代碼將始終處在第二個wihle循環中,不斷等待這個客戶端發送數據,而沒有機會在此執行到accept方法,除非這個客戶端終止以後,下一個客戶端才能被處理.

綜上,上述的情況,一次只能接受一個客戶端請求,那麼,我有辦法同時接受多個客戶端請求嗎?

有! 為每一個連接進來的客戶端創建一個單獨的線程來響應處理. 大概的代碼是這樣子的

ServerSocket serverSocket = new ServerSocket(1111);
while(true) {
Socket clientSocket = serverSocket.accept();
//為每一個客戶端連接創建一個線程處理,主線程只負責接受客戶端的連接請求
Runnable connectionHandler = new Runnable() {
@Override
public void run() {
try {
myProcess(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
};
new Thread(connectionHandler).start();
}

好了,上述代碼可以同時處理接受多個客戶端的連接了. 世界大和諧!!!

哈哈,開什麼玩笑,如果就這麼簡單,我特么寫這個流水賬幹什麼.

在Java的世界裡,新創建一個線程默認會分配64KB以上的內存(大概值),假設一個伺服器有16GB內存,拋開其他一切因素,最多可以創建16*1024*1024/64= 262144個線程

等等,你真的打算就通過開線程來粗暴地完成多個客戶端的連接工作嗎? 我覺得你計算機的老師會把你打斷狗腿.先不說完全開不出這麼多線程,大量的線程在cpu上執行調度,需要進行上下文切換,但線程數量很大事,CPU的主要任務將集中在線程調度的工作上,而不再有精力去執行線程應該完成的任務.(強烈建議沒有學過操作系統的騷年們,去系統學一下操作系統)

「讓我們考慮一下這種方案的影響。第一,在任何時候都可能有大量的線程處於休眠狀態,只是等待輸入或者輸出數據就緒,這可能算是一種資源浪費。第二,需要為每個線程的調用棧都分配內存,其默認值大小區間為64 KB到1 MB,具體取決於操作系統。第三,即使Java虛擬機(JVM)在物理上可以支持非常大數量的線程,但是遠在到達該極限之前,上下文切換所帶來的開銷就會帶來麻煩,例如,在達到10 000個連接的時候」(摘錄來自: [美] Norman Maurer Marvin Allen Wolfthal. 「Netty實戰。」 iBooks. )

阻塞I/O(摘抄自Netty實戰)

如果思考上面的代碼,實際上是因為阻塞的IO操作(阻塞等待客戶端連接進來,和發送消息),所以需要用每一個線程來包裝每一個阻塞操作,使得「阻塞等待」在線程之間編程了非阻塞的非同步操作,然而每一個線程內部,還是阻塞等待的狀態.那麼如果將阻塞的I/O改成為非阻塞的I/O也就能避免創建過多線程的情況. 這個時候,我們有請NIO上台表演一下.

來,直接把寶貝掏出來給你們看

public static void main(String[] args) throws IOException {
Selector selector = Selector.open();//創建一個NIO的Selector
ServerSocketChannel serverSocket = ServerSocketChannel.open();//創建serverChannel,就是創建了一個NIO版的Socket
serverSocket.bind(new InetSocketAddress("localhost", 1111));//將該socket綁定到本地1111埠進行監聽
serverSocket.configureBlocking(false);//置通道為非阻塞模式
serverSocket.register(selector, SelectionKey.OP_ACCEPT);//註冊對 Accept操作的訂閱
ByteBuffer buffer = ByteBuffer.allocate(256);

while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();//得到已經I/O就緒的key
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {//如果是客戶端新的連接
register(selector, serverSocket);
}
if (key.isReadable()) {//如果是已連接客戶端的新數據
answerWithEcho(buffer, key);
}
iter.remove();
}
}
}

public static void register(Selector selector, ServerSocketChannel serverSocket) throws IOException {
SocketChannel client = serverSocket.accept();//為客戶端新的連接請求創建socket
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);//為這個客戶端socket訂閱"數據就緒"(有新的數據發過來,就是一個"數據就緒")
}

private static void answerWithEcho(ByteBuffer buffer, SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
client.read(buffer);
if ("DONE".equals(new String(buffer.array()).trim())) {
client.close();
System.out.println("Closed");
}
buffer.flip();
client.write(buffer);//將客戶端的數據原樣返回客戶端
buffer.clear();
}

上述代碼用一幅圖來描述的話,就是下面這個樣子

使用非阻塞NIO

上述模型是使用Java的NIO的改造,對於大量的並發可以使用更少的線程,從而達到更好的性能.對於NIO不了解的同學,可以移步我的另一篇學習筆記 玩轉Java NIO

預計下次更新時間為本周末


推薦閱讀:
相关文章