前言
以前對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
