套接字底層原理使用 tcp 或 udp 時,又會廣泛使用到 socket(套接字)api,socket 原本是由 bsd unix 開發的,但是後來被移植到 windows 的 winsock 以及嵌入式系統中。應用程式利用 socket,可以設定對端的 ip 位址、埠號,並實現資料的接收和傳送:
下面我們分別以 tcp 和 udp 為例,詳細介紹 socket 的底層原理和相關 api 函式。我們先從較為複雜的 tcp 開始。
tcp 的服務端要先監聽乙個埠,一般是先呼叫 bind 函式,給這個 socket 賦予乙個 ip 位址和埠。
當服務端有了 ip 和埠號,就可以呼叫listen
函式進行監聽。在 tcp 的狀態圖裡面,有乙個listen
狀態,當呼叫這個函式之後,服務端就進入了這個狀態,這個時候客戶端就可以發起連線了。
在作業系統核心中,為每個 socket 維護兩個佇列。乙個是已經建立了連線的佇列,這時候連線三次握手已經完畢,處於established
狀態;乙個是還沒有完全建立連線的佇列,這個時候三次握手還沒完成,處於syn_rcv
的狀態。
接下來,服務端呼叫accept
函式,拿出乙個已經完成的連線進行處理。如果還沒有完成,就要等著。
在服務端等待的時候,客戶端可以通過connect
函式發起連線。先在引數中指明要連線的 ip 位址和埠號,然後開始發起三次握手,作業系統會給客戶端分配乙個臨時的埠。一旦握手成功,服務端的accept
就會返回另乙個 socket 用於傳輸資料。
這裡需要注意的是,負責監聽的 socket 和真正用來傳資料的 socket 是兩個,乙個叫作監聽 socket,乙個叫作已連線 socket。
連線建立成功之後,雙方開始通過read
和write
函式來讀寫資料,就像往乙個檔案流裡面寫東西一樣。
下面這個圖就是基於 tcp 協議的 socket api 函式呼叫過程:
說 tcp 的 socket 就是乙個檔案流,是非常準確的。因為,socket 在 linux 中就是以檔案的形式存在的。除此之外,還存在檔案描述符。寫入和讀出,也是通過檔案描述符。
注:如果你留心過 nginx 裡面 php-fpm 的配置,就會發現有兩種方式將 php 動態請求**給 php-fpm,一種是 ip 位址+埠號,例如:127.0.0.1:9000,一種是 socket 檔案,例如:unix:/run/php/php7.1-fpm.sock。這裡也可以表明,socket 在 linux 中確實以檔案形式存在,由於不需要建立額外的網路請求,所以後者效率更高,但是由於是本地檔案,所以不能跨機器訪問,如果 nginx 和 php-fpm 部署在不同的機器,只能通過前一種方式**請求。基於 udp 的 socket 程式設計要簡單一些,因為 udp 通訊無需建立連線,所以不需要三次握手,也就不需要呼叫
listen
和connect
函式,但是,udp 的的互動仍然需要 ip 和埠號,因而也需要bind
。udp 是沒有維護連線狀態的,因而不需要每對連線建立一組 socket,而是只要有乙個 socket,就能夠和多個客戶端通訊。也正是因為沒有連線狀態,每次通訊的時候,呼叫sendto
和recvfrom
,都可以傳入 ip 位址和埠。
伺服器如何提高併發量
我們以 web 請求為例,介紹如何讓伺服器同時處理更多請求,提高併發量。web 請求一般都是 http 請求,而 http 協議又是基於 tcp 的,所以,我們主要**如何讓伺服器同時處理更多 tcp 連線請求。
伺服器通常固定在某個本地埠上監聽,等待客戶端的連線請求。伺服器端 tcp 連線四元組中只有對端 ip 和對端埠(即客戶端ip和埠)是可變的,因此,最大 tcp 連線數 = 客戶端 ip 數 × 客戶端埠數。對 ipv4,客戶端的 ip 數最多為 2 的 32 次方,客戶端的埠數最多為 2 的 16 次方,也就是服務端單機最大 tcp 連線數,約為 2 的 48 次方。
當然,服務端最大併發 tcp 連線數遠不能達到理論上限。首先主要是檔案描述符限制,按照前面介紹的原理,socket 都是檔案,所以首先要通過ulimit
配置檔案描述符的數目;另乙個限制是記憶體,按上面的資料結構,每個 tcp 連線都要占用一定記憶體,作業系統是有限的。
當有新的請求進來,fork出乙個子程序,讓子程序處理該請求,提高併發量。
程序開銷太大,執行緒則輕量級的多,所以我們還可以通過在程序中建立新的執行緒來處理請求。
所謂多路 io 復用可以簡單理解為乙個執行緒維護多個 socket(前面多程序或多執行緒都是乙個程序或執行緒維護乙個 socket),這也有兩種實現方式:輪詢和事件通知。
因為 socket 在 linux 系統中以檔案描述符形式存在,所以我們把乙個執行緒維護的所有 socket 叫做檔案描述符集合,所謂輪詢就是呼叫核心的 select 函式監聽檔案描述符集合是否有變化,一旦有變化,就會依次檢視每個檔案描述符,對那些發生變化的檔案描述符進行讀寫操作,然後再呼叫select
函式監聽下一輪的變化。
顯然,輪詢的效率有點低,因為每次檔案描述符集合有變化,都要將全部 socket 輪詢一遍,這大大影響了系統能夠支撐的最大連線數。如果改成事件通知的方式,情況要好很多。所謂事件通知,就是某個檔案描述符發生變化,呼叫epoll
函式主動通知。這種方式使得監聽的 socket 資料增加的時候,效率不會大幅度降低,能夠同時監聽的 socket 的數目也非常多。上限就為系統定義的、程序開啟的最大檔案描述符個數。
因此,epoll
被稱為解決 c10k 問題的利器。
關於更底層的實現原理我們將留到後面講 nginx 的時候深入展開。
這個思路在業務領域不會有太大的問題,因為需求的變化實在是太快了,需要時時去應對。但在底層技術的發展上,我們就有可能遭到「短視」的報復,比如:這個資料長度不會超過16位吧,這個程式不可能使用到2023年吧。於是就有了千年蟲的問題,也有了 c10k 的問題。
c10k 就是 client 10000 問題,即「在同時連線到伺服器的客戶端數量超過 10000 個的環境中,即便硬體效能足夠, 依然無法正常提供服務」,簡而言之,就是單機1萬個併發連線問題。這個概念最早由 dan kegel 提出並發布於其個人站點( )。
當然,這個問題隨著技術的發展很快就解決了,現在大部分的個人電腦作業系統可以建立64位的程序,由於資料型別所帶來的程序數上限消失了,但是我們依然不能無限制的建立程序,因為隨著併發連線數的上公升會占用系統大量的記憶體,同樣會造成系統的不可用。
作業系統裡記憶體管理的主要作用是,程序請求記憶體的時候為其分配可用記憶體,程序釋放後**記憶體,並監控記憶體的使用狀況。為了提高記憶體的使用率,現代作業系統需要程式能夠共享記憶體,並且記憶體的限制對開發者透明,有些程式占用了記憶體空間,但不一定是一直使用的,這樣可以把這部分記憶體資料序列化到磁碟上,需要的時候再載入到記憶體裡,這樣記憶體資源永遠會給最需要的程式使用。於是程式設計師們發明了虛擬記憶體(virtual memory)。
虛擬記憶體技術支援程式訪問比物理記憶體大得多的記憶體空間,也使得多個程式共享記憶體更加高效。物理記憶體由 ram 晶元提供,虛擬記憶體則依靠透明的使用磁碟空間,使程式執行起來好像有了更大的記憶體空間。
但是問題依然存在,程序和執行緒的建立都需要消耗一定的記憶體,每建立乙個棧空間,都會產生記憶體開銷,當記憶體使用超過物理記憶體的時候,一部分資料就會持久化到磁碟上,隨之而來的就是效能的大幅度下降。
這就像銀行擠兌,人們把現金存入銀行,收取一定的利息,平時只有少數人去銀行取現,銀行會拿人們存的錢去做更有價值的投資。但是,如果大部分人都去銀行取現,銀行是沒有那麼多現金的。取不到錢的使用者,被門擋在外面的使用者,一定會去拉橫幅喊口號「最喜歡雙截棍柔中帶剛,不喜歡銀行就上少林武當」云云,於是銀行就處於不可用狀態了。現在的 p2p 理財也是乙個道理,投資者都去變現,無論是多麼良性的資產,一樣玩完。
當然,現在我們早已經突破了 c10k 這個瓶頸,具體的思路就是通過單個程序或執行緒服務於多個客戶端請求,通過非同步程式設計和事件觸發機制替換輪訓,io 採用非阻塞的方式,減少不必要的效能損耗,等等。
底層的相關技術包括epoll、kqueue、libevent
等,應用層面的解決方案包括openresty、golang、node.js
等,比如 openresty 的介紹中是這麼說的:
openresty 通過匯聚各種設計精良的 nginx 模組,從而將 nginx 有效地變成乙個強大的通用 web 應用平台。這樣,web 開發人員和系統工程師可以使用 lua 指令碼語言調動 nginx 支援的各種 c 以及 lua 模組,快速構造出足以勝任 c10k 乃至 c1000k 以上單機併發連線的高效能 web 應用系統。據說現在都去搞 c10m 了,你們怕不怕?
實際操作中,每個解決方案都不是那麼容易實現的,很多技術領域油光水滑的東西,放到線上,往往會出現各種各樣的問題和毛病。松本行弘先生介紹了乙個「最弱連線」的概念:
如果往兩端用力拉一條由很多環 (連線)組成的鎖鏈,其中最脆弱的乙個連線會先斷掉。因此,鎖鏈整體的強度取決於其中最脆弱的一環。10k 問題的情況也很相似。一台伺服器同時應付超過一萬個(或者更多)併發連線的情況,哪怕只有乙個要素沒有考慮到超過一萬個客戶端的情況,這個要素就會成為「最弱連線」,從而導致問題的發生。
每個做架構設計和技術實現的程式設計師,都應當考慮這個最弱連線問題。
網路七層協議 傳輸層協議
tcp 可靠 效率低 面向連線 syn 打算與對方建立連線 ack 確認 fin 打算與對方斷開連線 應用 ftp 21 telnet 23 ssh 22 smtp 25 httpd 80 httpds 443 dns 53 udp 不可靠 效率高 無連線 應用 tftp 60 dns 53 ntp...
網路七層協議 傳輸層協議
tcp 可靠 效率低 面向連線 應用 ftp 21 telnet 23 ssh 22 smtp 25 httpd 80 httpds 443 dns 53 udp 不可靠 效率高 無連線 應用 tftp 60 dns 53 ntp 123 acl 2000 rule deny premit sour...
傳輸層協議
流量控制 擁塞控制 1.鏈路層 處理與電纜 或其他傳輸介質 的物理介面實現 2.網路層 處理分組在網路中的活動,處理分組路由 3.運輸層 為兩台主機上的應用程式提供端到端的通訊 4.應用層 處理特定的應用程式的細節 絕大多數的網路應用程式都是客戶 伺服器模式 雙方都有乙個或多個協議進行執行 應用程式...