從 Socket 編程談談 IO 模型(三)

  • 2020 年 2 月 19 日
  • 筆記

【這是一猿小講的第 85 篇原創分享】

快過年啦,估計很多朋友已在摸魚的路上。而我為了兄弟們年後的追逐,卻在苦苦尋覓、規劃,導致文章更新晚了些,各位猿粉諒解。

上期分享,我們結合新春送祝福的場景,通過一坨坨的程式碼讓 BIO、NIO 編程過程呈現的淋漓盡致。

本期分享,通過畫幾張圖,再聊 IO 之 Socket 編程的哪些事兒(小猿舞劍,上期意在程式碼,這期意在圖)。

Socket 翻譯為插口、槽,名字很有意義,一旦插入網線進行連接,我們的程式碼便能夠通訊。

如圖示意,每個 Socket 都包含兩條線,也就是兩個流(輸入流和輸出流)。其實建立網路連接就類似電話系統,一端給另一端打電話(port 就像電話號碼),而且一直在監聽是不是通了,是不是說話啦。

了解過網路底層協議的都知道,通過使用 Socket,使得開發變得簡單了很多,因為 Socket 隱藏了大量的網路開發的細節。其實,對於 Java 程式而言,網路的 IO 就像操作順序文件的 IO 一樣。

如上圖示意,Socket 編程模型,倒是不複雜,我們拆開去說。

服務端編程步驟,其實圖說的已經很清晰,再絮叨兩句。

第一步:創建 ServerSocket 對象;

第二步:接受來自客戶端的連接請求並返回 Socket;

第三步:從 Socket 獲取輸入流/輸出流;

第三步:根據數據類型將原始輸入流/輸出流封裝為高級流(可選);

第四步:從流中接收/發送消息;

第五步:釋放資源。

客戶端與服務端編程步驟差不多,也不複雜。

客戶端編程步驟圖中也說的很清楚了,不過還是要簡單歸納一下。

第一步:創建 Socket;

第二步:從連接的 Socket 獲取輸入流和輸出流;

第三步:根據數據類型將原始輸入流/輸出流封裝為高級流(可選);

第四步:從流中接收/發送消息;

第五步:釋放資源。

Socket 編程模型總體來說很簡單,所以稍微花點功夫,輕輕鬆鬆就能照貓畫虎擼出能聊天的程式。

接下來我們再畫幾張圖,一起看看 Socket 編程的演進。

如圖所示,服務端首先通過 accept() 方法接受客戶端的鏈接,然後創建執行緒進行處理客戶端的請求,最後把響應發給客戶端。這種模型坊間稱之為傳統 BIO 編程模型圖。

聰明的你們會發現,這種模型,如果在客戶端訪問過多的情況下,服務端需要頻繁的啟動、銷毀執行緒,那麼勢必會有性能上的開銷;另外,執行緒數過多,也有可能會拖垮伺服器,於是就引入了執行緒池進行改進。

如上圖所示,服務端接收到請求之後,封裝成 Task 對象,然後交給執行緒池去執行任務,最後給客戶端響應。

通過引入執行緒池,來管理工作執行緒的數量,進而避免頻繁創建、銷毀執行緒帶來的開銷,在實際研發中若是並發量較小的應用,這種設計已經足矣,這種模型在坊間稱為偽非同步 IO 編程模型圖。

但是,這種模型,恰恰由於執行緒池限制了執行緒的數量,在高並發場景下,請求超過執行緒池的最大數量時,那麼就只能等待,直到執行緒池中的有空閑的執行緒才可以被複用。那麼,在網路較差、傳輸較大文件時,是不是就出現了鏈接超時?!

這或許就是 BIO(同步阻塞) 的劣勢吧,那該怎麼辦呢?於是就有了 NIO 編程模型。

如圖示意,NIO 利用了單執行緒管理一個Selector,而一個Selector管理多個 SocketChannel,也就是管理多個連接,那麼就不用為每個連接都創建一個執行緒,可以有效避免高並發情況下,頻繁執行緒切換等帶來的問題。

舉個貼近生活的場景,加深一下理解。當下豬肉的行情著實不錯,不妨就舉個養豬場的栗子。

養豬場有個看護豬圈的人,他每天要做的事兒,就是不停的檢查豬圈,如果有豬生病、有豬生仔、有豬跑出去等等,他就把相應的情況記錄下來,如果豬場的老闆想知道具體情況,只需要問看護豬圈的那個人就行啦。

容我抽象一下,看護豬圈的人就相當於 Selector,每個豬圈就相當於 SocketChannel,而豬場的老闆就相當於執行緒Thread,他可以通過一個Selector管理多個 SocketChannel,進而服務多個 Socket。

好了,今天就扯這麼多,沒有引入新知識點,一方面在於把前期的知識點系統的串一串;重點是為了,給找我請教的熱情粉絲,一個交代,後續有時間會把 Socket 往深里挖一挖,請大家期待。