前言:做爲一名開發人員咱們常常會聽到HTTP協議、TCP/IP協議、UDP協議、Socket、Socket長鏈接、Socket鏈接池等字眼,然而它們之間的關係、區別及原理並非全部人都能理解清楚,這篇文章就從網絡協議基礎開始到Socket鏈接池,一步一步解釋他們之間的關係。linux
七層網絡模型程序員
首先從網絡通訊的分層模型講起:七層模型,亦稱OSI(Open System Interconnection)模型。自下往上分爲:物理層、據鏈路層、網絡層、傳輸層、會話層、表示層和應用層。全部有關通訊的都離不開它,下面這張圖片介紹了各層所對應的一些協議和硬件。數據庫
經過上圖,我知道IP協議對應於網絡層,TCP、UDP協議對應於傳輸層,而HTTP協議對應於應用層,OSI並無Socket,那什麼是Socket,後面咱們將結合代碼具體詳細介紹。編程
TCP和UDP鏈接json
關於傳輸層TCP、UDP協議可能咱們平時碰見的會比較多,有人說TCP是安全的,UDP是不安全的,UDP傳輸比TCP快,那爲何呢,咱們先從TCP的鏈接創建的過程開始分析,而後解釋UDP和TCP的區別。安全
TCP的三次握手和四次分手服務器
咱們知道TCP創建鏈接須要通過三次握手,而斷開鏈接須要通過四次分手,那三次握手和四次分手分別作了什麼和如何進行的。cookie
第一次握手:創建鏈接。客戶端發送鏈接請求報文段,將SYN位置爲1,Sequence Number爲x;而後,客戶端進入SYN_SEND狀態,等待服務器的確認;網絡
第二次握手:服務器收到客戶端的SYN報文段,須要對這個SYN報文段進行確認,設置Acknowledgment Number爲x+1(Sequence Number+1);同時,本身本身還要發送SYN請求信息,將SYN位置爲1,Sequence Number爲y;服務器端將上述全部信息放到一個報文段(即SYN+ACK報文段)中,一併發送給客戶端,此時服務器進入SYN_RECV狀態;架構
第三次握手:客戶端收到服務器的SYN+ACK報文段。而後將Acknowledgment Number設置爲y+1,向服務器發送ACK報文段,這個報文段發送完畢之後,客戶端和服務器端都進入ESTABLISHED狀態,完成TCP三次握手。
完成了三次握手,客戶端和服務器端就能夠開始傳送數據。以上就是TCP三次握手的整體介紹。通訊結束客戶端和服務端就斷開鏈接,須要通過四次分手確認。
第一次分手:主機1(可使客戶端,也能夠是服務器端),設置Sequence Number和Acknowledgment Number,向主機2發送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有數據要發送給主機2了;
第二次分手:主機2收到了主機1發送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number爲Sequence Number加1;主機1進入FIN_WAIT_2狀態;主機2告訴主機1,我「贊成」你的關閉請求;
第三次分手:主機2向主機1發送FIN報文段,請求關閉鏈接,同時主機2進入LAST_ACK狀態;
第四次分手:主機1收到主機2發送的FIN報文段,向主機2發送ACK報文段,而後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段之後,就關閉鏈接;此時,主機1等待2MSL後依然沒有收到回覆,則證實Server端已正常關閉,那好,主機1也能夠關閉鏈接了。
能夠看到一次tcp請求的創建及關閉至少進行7次通訊,這還不包過數據的通訊,而UDP不需3次握手和4次分手。
TCP和UDP的區別
關於傳輸層咱們會常常聽到一些問題
1.TCP服務器最大併發鏈接數是多少?
關於TCP服務器最大併發鏈接數有一種誤解就是「由於端口號上限爲65535,因此TCP服務器理論上的可承載的最大併發鏈接數也是65535」。首先須要理解一條TCP鏈接的組成部分:客戶端IP、客戶端端口、服務端IP、服務端端口。因此對於TCP服務端進程來講,他能夠同時鏈接的客戶端數量並不受限於可用端口號,理論上一個服務器的一個端口能創建的鏈接數是全球的IP數*每臺機器的端口數。實際併發鏈接數受限於linux可打開文件數,這個數是能夠配置的,能夠很是大,因此實際上受限於系統性能。經過#ulimit -n查看服務的最大文件句柄數,經過ulimit -n xxx 修改 xxx是你想要能打開的數量。也能夠經過修改系統參數:
2.爲何TIME_WAIT狀態還須要等2MSL後才能返回到CLOSED狀態?
這是由於雖然雙方都贊成關閉鏈接了,並且握手的4個報文也都協調和發送完畢,按理能夠直接回到CLOSED狀態(就比如從SYN_SEND狀態到ESTABLISH狀態那樣);可是由於咱們必需要假想網絡是不可靠的,你沒法保證你最後發送的ACK報文會必定被對方收到,所以對方處於LAST_ACK狀態下的Socket可能會由於超時未收到ACK報文,而重發FIN報文,因此這個TIME_WAIT狀態的做用就是用來重發可能丟失的ACK報文。
3.TIME_WAIT狀態還須要等2MSL後才能返回到CLOSED狀態會產生什麼問題
通訊雙方創建TCP鏈接後,主動關閉鏈接的一方就會進入TIME_WAIT狀態,TIME_WAIT狀態維持時間是兩個MSL時間長度,也就是在1-4分鐘,Windows操做系統就是4分鐘。進入TIME_WAIT狀態的通常狀況下是客戶端,一個TIME_WAIT狀態的鏈接就佔用了一個本地端口。一臺機器上端口號數量的上限是65536個,若是在同一臺機器上進行壓力測試模擬上萬的客戶請求,而且循環與服務端進行短鏈接通訊,那麼這臺機器將產生4000個左右的TIME_WAIT Socket,後續的短鏈接就會產生address already in use : connect的異常,若是使用Nginx做爲方向代理也須要考慮TIME_WAIT狀態,發現系統存在大量TIME_WAIT狀態的鏈接,經過調整內核參數解決。
編輯文件,加入如下內容:
而後執行 /sbin/sysctl -p 讓參數生效。
net.ipv4.tcp_syncookies = 1 表示開啓SYN Cookies。當出現SYN等待隊列溢出時,啓用cookies來處理,可防範少許SYN攻擊,默認爲0,表示關閉;
net.ipv4.tcp_tw_reuse = 1 表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉;
net.ipv4.tcp_tw_recycle = 1 表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉。
net.ipv4.tcp_fin_timeout 修改系統默認的TIMEOUT時間。
HTTP協議
關於TCP/IP和HTTP協議的關係,網絡有一段比較容易理解的介紹:「咱們在傳輸數據時,能夠只使用(傳輸層)TCP/IP協議,可是那樣的話,若是沒有應用層,便沒法識別數據內容。若是想要使傳輸的數據有意義,則必須使用到應用層協議。應用層協議有不少,好比HTTP、FTP、TELNET等,也能夠本身定義應用層協議。
HTTP協議即超文本傳送協議(Hypertext Transfer Protocol ),是Web聯網的基礎,也是手機聯網經常使用的協議之一,WEB使用HTTP協議做應用層協議,以封裝HTTP文本信息,而後使用TCP/IP作傳輸層協議將它發到網絡上。
因爲HTTP在每次請求結束後都會主動釋放鏈接,所以HTTP鏈接是一種「短鏈接」,要保持客戶端程序的在線狀態,須要不斷地向服務器發起鏈接請求。一般 的作法是即時不須要得到任何數據,客戶端也保持每隔一段固定的時間向服務器發送一次「保持鏈接」的請求,服務器在收到該請求後對客戶端進行回覆,代表知道 客戶端「在線」。若服務器長時間沒法收到客戶端的請求,則認爲客戶端「下線」,若客戶端長時間沒法收到服務器的回覆,則認爲網絡已經斷開。
下面是一個簡單的HTTP Post application/json數據內容的請求:
關於Socket(套接字)
如今咱們瞭解到TCP/IP只是一個協議棧,就像操做系統的運行機制同樣,必需要具體實現,同時還要提供對外的操做接口。就像操做系統會提供標準的編程接口,好比Win32編程接口同樣,TCP/IP也必須對外提供編程接口,這就是Socket。如今咱們知道,Socket跟TCP/IP並無必然的聯繫。Socket編程接口在設計的時候,就但願也能適應其餘的網絡協議。因此,Socket的出現只是能夠更方便的使用TCP/IP協議棧而已,其對TCP/IP進行了抽象,造成了幾個最基本的函數接口。好比create,listen,accept,connect,read和write等等。
不一樣語言都有對應的創建Socket服務端和客戶端的庫,下面舉例Nodejs如何建立服務端和客戶端:
服務端:
服務監聽9000端口
下面使用命令行發送http請求和telnet
注意到curl只處理了一次報文。
客戶端
所謂長鏈接,指在一個TCP鏈接上能夠連續發送多個數據包,在TCP鏈接保持期間,若是沒有數據包發送,須要雙方發檢測包以維持此鏈接(心跳包),通常須要本身作在線維持。 短鏈接是指通訊雙方有數據交互時,就創建一個TCP鏈接,數據發送完成後,則斷開此TCP鏈接。好比Http的,只是鏈接、請求、關閉,過程時間較短,服務器如果一段時間內沒有收到請求便可關閉鏈接。其實長鏈接是相對於一般的短鏈接而說的,也就是長時間保持客戶端與服務端的鏈接狀態。
一般的短鏈接操做步驟是:
鏈接→數據傳輸→關閉鏈接;
而長鏈接一般就是:
鏈接→數據傳輸→保持鏈接(心跳)→數據傳輸→保持鏈接(心跳)→……→關閉鏈接;
何時用長鏈接,短鏈接?
長鏈接多用於操做頻繁,點對點的通信,並且鏈接數不能太多狀況,。每一個TCP鏈接都須要三步握手,這須要時間,若是每一個操做都是先鏈接,再操做的話那麼處理 速度會下降不少,因此每一個操做完後都不斷開,次處理時直接發送數據包就OK了,不用創建TCP鏈接。例如:數據庫的鏈接用長鏈接, 若是用短鏈接頻繁的通訊會形成Socket錯誤,並且頻繁的Socket建立也是對資源的浪費。
什麼是心跳包爲何須要:
心跳包就是在客戶端和服務端間定時通知對方本身狀態的一個本身定義的命令字,按照必定的時間間隔發送,相似於心跳,因此叫作心跳包。網絡中的接收和發送數據都是使用Socket進行實現。可是若是此套接字已經斷開(好比一方斷網了),那發送數據和接收數據的時候就必定會有問題。但是如何判斷這個套接字是否還可使用呢?這個就須要在系統中建立心跳機制。其實TCP中已經爲咱們實現了一個叫作心跳的機制。若是你設置了心跳,那TCP就會在必定的時間(好比你設置的是3秒鐘)內發送你設置的次數的心跳(好比說2次),而且此信息不會影響你本身定義的協議。也能夠本身定義,所謂「心跳」就是定時發送一個自定義的結構體(心跳包或心跳幀),讓對方知道本身「在線」,以確保連接的有效性。
實現:
服務端:
服務端輸出結果:
客戶端代碼:
客戶端輸出結果:
若是想要使傳輸的數據有意義,則必須使用到應用層協議好比Http、Mqtt、Dubbo等。基於TCP協議上自定義本身的應用層的協議須要解決的幾個問題:
下面咱們就一塊兒來定義本身的協議,並編寫服務的和客戶端進行調用:
定義報文頭格式: length:000000000xxxx; xxxx表明數據的長度,總長度20,舉例子不嚴謹。
數據序列化方式:JSON。
服務端:
日誌打印:
客戶端
日誌打印:
這裏能夠看到一個客戶端在同一個時間內處理一個請求能夠很好的工做,可是想象這麼一個場景,若是同一時間內讓同一個客戶端去屢次調用服務端請求,發送屢次頭數據和內容數據,服務端的data事件收到的數據就很難區別哪些數據是哪次請求的,好比兩次頭數據同時到達服務端,服務端就會忽略其中一次,然後面的內容數據也不必定就對應於這個頭的。因此想複用長鏈接並能很好的高併發處理服務端請求,就須要鏈接池這種方式了。
Socket鏈接池
什麼是Socket鏈接池,池的概念能夠聯想到是一種資源的集合,因此Socket鏈接池,就是維護着必定數量Socket長鏈接的集合。它能自動檢測Socket長鏈接的有效性,剔除無效的鏈接,補充鏈接池的長鏈接的數量。從代碼層次上實際上是人爲實現這種功能的類,通常一個鏈接池包含下面幾個屬性:
場景: 一個請求過來,首先去資源池要求獲取一個長鏈接資源,若是空閒隊列裏面有長鏈接,就獲取到這個長鏈接Socket,並把這個Socket移到正在運行的長鏈接隊列。若是空閒隊列裏面沒有,且正在運行的隊列長度小於配置的鏈接池資源的數量,就新建一個長鏈接到正在運行的隊列去,若是正在運行的不下於配置的資源池長度,則這個請求進入到等待隊列去。當一個正在運行的Socket完成了請求,就從正在運行的隊列移到空閒的隊列,並觸發等待請求隊列去獲取空閒資源,若是有等待的狀況。
下面簡單介紹Node.js的一個通用鏈接池模塊:generic-pool。
主要文件目錄結構
下面鏈接池的使用,使用的協議是咱們以前自定義的協議。
日誌打印:
這裏看到前面兩個請求都創建了新的Socket鏈接 socket_pool 127.0.0.1 9000 connect,定時器結束後從新發起兩個請求就沒有創建新的Socket鏈接了,直接從鏈接池裏面獲取Socket鏈接資源。
發現主要的代碼就位於lib文件夾中的Pool.js
構造函數:
lib/Pool.js
能夠看到包含以前說的空閒的資源隊列,正在請求的資源隊列,正在等待的請求隊列等。
下面查看 Pool.acquire 方法
lib/Pool.js
上面的代碼就按種狀況一直走下到最終獲取到長鏈接的資源,其餘更多代碼你們能夠本身去深刻了解。
做者簡介:6年服務端開發經驗,負責過初創企業項目從零到高併發訪問的技術方案演變及開發部署,積累了必定平臺開發和高併發高可用經驗,如今負責公司微服務框架下網關層的架構及開發。