換一個角度看Socket的資料讀寫
前言
以前對IO、NIO還算了解,也寫過Netty的專案。但是對底層的資料傳遞不是很瞭解,一直存有這方面的疑惑。但是由於有其他事情就被打斷了。前陣子因為想要了解volatile關鍵字的原理,學習了下JMM(Java記憶體模型),瞭解到物件資料是如何儲存的。後來又想知道Tomcat是如何傳遞Http報文的,原始碼翻著翻著就到了Socket,想來Socket還有些東西沒學清楚,就乾脆乘著興致查閱了不少資料。
這裡就以資料讀寫位置為中心,整理分享一下相關內容吧。
整體檢視
從“網際網路” 到“本機網絡卡”
網絡卡會判斷網路資料報是否是給本機的,如果是則接收,否則丟棄。它是如何判斷的?資料報中有目的地址,如果為本機IP地址,則接收下來。
網絡卡的儲存空間
網絡卡是有儲存空間的,不過很小,只有幾KB。它只能作為臨時緩衝用的,一般需要存入記憶體。
從“本機網絡卡”到“核心空間”
網絡卡會使用DMA把資料報寫入到核心空間中,這個過程不需要CPU干預。
核心空間與使用者空間
記憶體分為兩大塊,使用者空間和核心空間。核心空間是歸屬於作業系統使用的,為了安全,使用者空間中的程式只能訪問分配給它的地址空間,一般不能訪問核心空間。
地址空間:也就是作業系統分配給程序的記憶體空間,它只能訪問自己的記憶體空間,不能干預其他程序。即指標只能在一定範圍內活動。地址空間是可以擴容的,這是後話了。
Socket的讀寫佇列
每個Socket都在核心空間中都有與之相關聯的讀寫佇列(儲存空間),一個讀佇列,一個寫佇列。且讀佇列的大小一般要大於寫佇列。Socket要讀資料就從對應的讀佇列中讀,寫資料就寫到相應的寫佇列。
資料報如何正確地寫入到相關的Socket佇列中?
換句話說,如何知道資料報是歸屬於哪個socket。首先IP地址肯定有了,其次TCP/UDP資料報中就有"目的埠"的欄位,這自然就能對映到相關的Socket了,因為本機中的socket就是用佔用的埠來彼此區分的。
Linux如何檢視讀寫佇列大小
相關資訊在這兩個配置檔案中,內容依次是最小,預設,最大
/proc/sys/net/ipv4/tcp_rmem (讀佇列大小配置) /proc/sys/net/ipv4/tcp_wmem (寫佇列大小配置)
從“核心空間”到“使用者空間”
socket物件呼叫read方法,就是從核心空間中讀取資料到使用者空間。
系統呼叫
前面說了,使用者空間的程式一般是不能訪問核心空間的。但是程式要執行,有時候不得不訪問磁碟和網路資料。於是乎,作業系統就提供一些庫函式,使用者程式可以呼叫這些庫函式來間接使用作業系統的功能。
注:這裡與socket相關的操作都是系統呼叫
如果讀佇列沒有資料可讀會怎樣?
這取決於socket的mode,預設是阻塞的。也就是說,如果讀佇列中沒有資料可讀,那麼當前執行這個read函式的執行緒將被掛起,然後等到核心空間來資料的時候再喚醒這個執行緒開始讀資料,這就是同步阻塞。當然也有非阻塞式的,就是說,如果沒有資料可讀,執行執行緒不會被掛起,而是完成read函式,返回一個"-1"的錯誤碼。同步非阻塞,說的就是,反覆呼叫read函式直到成功。
待解決:核心空間如何喚醒這個執行緒,用的是什麼機制。
讀出來的資料放在哪裡?
一般,我們會分配一個空間來儲存,也就是建立一個byte陣列來快取讀取進來的資料。為什麼說是快取?因為我們使用socket肯定不是簡單的把資料讀出來,肯定還要進行下一步的處理,byte陣列只是用來暫時儲存資料的。
IO複用的思想
前面說的,不管是同步阻塞,還是同步非阻塞。根本上都是說,執行緒要等到可以讀寫的時候,才開始讀寫操作。這樣看來,這段等待的時間就算是浪費了。(不管你等待的方式是掛起,還是輪詢),IO複用的思想就是認為,這段等待的時間可以利用起來,去執行其他socket的IO操作(當然是滿足讀寫狀態的socket)。或者說,就是隻有你滿足讀寫條件後,你準備好後,我(也就是執行緒)才來處理你的讀寫操作,而不是我來了,還要等你梳妝打扮半小時才能出發。
select、poll、epoll等函式的使用
IO複用中,一個執行緒同時負責多個socket連線的讀寫。select、poll、epoll函式簡單地說,就是把滿足讀寫狀態的socket挑選出來。不同的是,它們挑選的方式不同而已。這裡由於博主涉獵不深,也就不展開介紹了。
FAQ 常見問題
說是常見問題,其實只是我個人想到的,看客可能會存在的疑惑。
1.Java的socket API與window或linux底層的socket API是什麼關係?
Java的socket是上層封裝的API,它使得不管什麼平臺,都能使用同一套API。它的底層實現還是c語言的庫函式。到底用哪個看執行環境,如果是window,那底層用的就是windows的socket api,否則就是linux的socket api。其實你裝JDK的時候就已經確定了,因為下jdk的時候就已經選擇了windows/linux。
2.如果讀佇列已滿,傳送方繼續傳送的資料會丟失嗎?
這就涉及到TCP的擁塞控制了,當佇列已滿的時候,新來的資料不會被確認。沒有確認收到的資料,它是會重新發的。讀者可以往擁塞控制(congestion control)方向去看。
參考資料
2.Network Interface Controller