前言
以前對IO、NIO還算了解,也寫過Netty的專案。但是對底層的資料傳遞不是很瞭解,一直存有這方面的疑惑。但是由於有其他事情就被打斷了。前陣子因為想要了解volatile關鍵字的原理,學習了下JMM(Java記憶體模型),瞭解到物件資料是如何儲存的。後來又想知道Tomcat是如何傳遞Http報文的,原始碼翻著翻著就到了Socket,想來Socket還有些東西沒學清楚,就乾脆乘著興致查閱了不少資料。
這裡就以資料讀寫位置為中心,整理分享一下相關內容吧。
整體檢視
從“網際網路” 到“本機網路卡”
網路卡會判斷網路資料包是否是給本機的,如果是則接收,否則丟棄。它是如何判斷的?資料包中有目的地址,如果為本機IP地址,則接收下來。
網路卡的儲存空間
網路卡是有儲存空間的,不過很小,只有幾KB。它只能作為臨時緩衝用的,一般需要存入記憶體。
從“本機網路卡”到“核心空間”
網路卡會使用DMA把資料包寫入到核心空間中,這個過程不需要CPU干預。
DMA寫資料是以塊為單位的,也就是一堆位元組。
核心空間與使用者空間
記憶體分為兩大塊,使用者空間和核心空間。核心空間是歸屬於作業系統使用的,為了安全,使用者空間中的程式只能訪問分配給它的地址空間,一般不能訪問核心空間。
地址空間:也就是作業系統分配給程式的記憶體空間,它只能訪問自己的記憶體空間,不能干預其他程式。即指標只能在一定範圍內活動。地址空間是可以擴容的,這是後話了。
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)方向去看。
這跟擁塞控制無關,應該跟TCP滑動視窗有關。當接收方的接收視窗已滿的時候,傳送方不會再繼續傳送資料。
注:滑動視窗是緩衝佇列的一部分,相當於一個遊標。
3.資料傳送出去後,萬一丟失了呢,如果要重發資料從哪裡來?
實際上,當核心空間中傳送緩衝區的資料發出時,該資料並沒有立即從佇列中刪除,也就是說它還在傳送方的電腦裡。只有收到接收方的確認後,該資料才會被刪除。如果等待時間超過超時時間,則會重發資料。
注意:TCP協議實現是作業系統提供的,怎麼移動滑動視窗,怎麼保證可靠性,怎麼控制端到端的流量,怎麼防止網路擁塞,這些底層都已經是實現好的。
4.Socket建立連線的過程做了什麼,為什麼要建立連線?
很多人可能會疑惑,因為socket連線建立後,並沒有建立一條實際的通訊路徑。熟悉TCP/IP的都知道,TCP資料包到了網路層被封裝成IP資料包。這時候,資料包往哪條路走是不固定的,路由器會根據實際的網路情況進行路由。
建立連線到底都做了些什麼,我暫時也不是很瞭解。但是我已知曉的就有序號的協商。TCP接收或傳送的佇列中每個位元組資料都是要編號的,但是初始序號並不是0。建立連線的過程會確定雙方每對"傳送-接收"佇列的起始序號。
參考資料
2.Network Interface Controller