【詳解】換一個角度看Socket的數據讀寫


前言

  以前對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。建立連接的過程會確定雙方每對"發送-接收"隊列的起始序號。

參考資料

1.How TCP Socket works

2.Network Interface Controller

3.Network Socket

4.How to find the socket buffer size of Linux?

5.system call about socket


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM