從抓包的角度分析connect函數的連接過程
這篇文章主要是從tcp連接建立的角度來分析客戶端程序如何利用connect函數和服務端程序建立tcp連接的,了解connect函數在建立連接的過程中底層協議棧做了哪些事情。
tcp三次握手
在正式介紹connect函數時,我們先來看一下tcp三次握手的過程,下面這個實驗是客戶端通過telnet遠程登錄服務端的例子,telnet協議是基於tcp協議,我們可以通過wireshark抓包工具看到客戶端和服務端之間三次握手的過程,12.1.1.1是客戶端的ip地址,12.1.1.2是服務端的ip地址。
下面是我們通過wireshark抓取到的tcp三次握手的數據包:
我們看到客戶端遠程登錄服務端時,首先發送了一個SYN報文,其中目標埠為23(遠程登錄telnet協議使用23埠),初始序號seq = 0,並設置自己的窗口rwnd = 4128(rwnd是一個對端通告的接收窗口,用於流量控制)。
然後服務端回復了一個SYN + ACK報文,初始序號seq = 0,ack = 1(在前一個包的seq基礎上加1),同時也設置自己的窗口rwnd = 4128。
然後客戶端收到服務端的SYN + ACK報文時,回復了一個ACK報文,表示確認建立tcp連接,序號為seq = 1,ack = 1(在前一個包的seq基礎上加1), 設置窗口rwnd = 4128,此時客戶端和服務端之間已經建立tcp連接。
connect函數
前面我們在介紹tcp三次握手的時候說過,客戶端在跟服務端建立tcp連接時,通常是由客戶端主動向目標服務端發起tcp連接建立請求,服務端被動接受tcp連接請求;同時服務端也會發起tcp連接建立請求,表示服務端希望和客戶端建立連接,然後客戶端會接受連接並發送一個確認,這樣雙方就已經建立好連接,可以開始通信。
這裡說明一下:可能有的小夥伴會感到疑惑,為啥服務端也要跟客戶端建立連接呢?其實這跟tcp採用全雙工通信的方式有關。對於全雙工通信,簡單來說就是兩端可以同時收發數據,如下圖所示:
我們再回到正題,那麼在網路編程中,肯定也有對應的函數做到跟上面一樣的事情,沒錯,就是connect(連接)。顧名思義,connect函數就是用於客戶端程序和服務端程序建立tcp連接的。
一般來說,客戶端使用connect函數跟服務端建立連接,肯定要指定一個ip地址和埠號(相當於客戶端的身份標識),要不然服務端都不知道你是誰?憑什麼跟你建立連接。同時還得指明服務端的ip地址和埠號,也就是說,你要跟誰建立連接。
connect函數原型:
參數說明:
sockfd:客戶端的套接字文件描述符
addr:要連接的套接字地址,這是一個傳入參數,指定了要連接的套接字地址信息(例如IP地址和埠號)
addrlen:是一個傳入參數,參數addr的大小,即sizeof(addr)
返回值說明:連接建立成功返回0,失敗返回-1並設置errno
connect函數在建立tcp連接的過程中用到了一個非常重要的隊列,那就是未決連接隊列,這個隊列用來管理tcp的連接,包括已完成三次握手的tcp連接和未完成三次握手的tcp連接,下面我們就來詳細介紹一下未決連接隊列。
未決連接隊列
未決連接隊列是指伺服器接收到客戶端的連接請求,但是尚未被處理(也就是未被accept,後面會說)的連接,可以理解為未決連接隊列是一個容器,這個容器存儲著這些尚未被處理的鏈接。
當一個客戶端進程使用 connect 函數發起請求後,伺服器進程就會收到連接請求,然後檢查未決連接隊列是否有空位,如果未決隊列滿了,就會拒絕連接,那麼客戶端調用的connect 函數返回失敗。
如果未決連接隊列有空位,就將該連接加入未決連接隊列。當 connect 函數成功返回後,表明tcp的「三次握手」連接已完成,此時accept函數獲取到一個客戶端連接並返回。
在上圖中,在未決連接隊列中又分為2個隊列:
未完成隊列(未決隊列):即客戶端已經發出SYN報文併到達伺服器,但是在tcp三次握手連接完成之前,這些套接字處於SYN_RCVD狀態,伺服器會將這些套接字加入到未完成隊列。
已完成隊列:即剛剛完成tcp三次握手的tcp連接,這些套接字處於ESTABLISHED狀態,伺服器會將這些套接字加入到已完成隊列。
我們來看一下連接建立的具體過程,如圖所示:
服務端首先調用listen函數監聽客戶端的連接請求,然後調用accept函數阻塞等待取出未決連接隊列中的客戶端連接,如果未決連接隊列一直為空,這意味著沒有客戶端和伺服器建立連接,那麼accept就會一直阻塞。
當客戶端一調用connect函數發起連接時,如果完成tcp三次握手,那麼accept函數會取出一個客戶端連接(注意:是已經建立好的連接)然後立即返回。
上面就是客戶端和服務端在網路中的狀態變遷的具體過程,前面我們在學習tcp三次握手的過程中還知道,服務端和客戶端在建立連接的時候會設置自己的一個接收緩衝區窗口rwnd的大小。
服務端在發送SYN + ACK數據報文時會設置並告知對方自己的接收緩衝區窗口大小,客戶端在發送ACK數據報文時也會設置並告知對方自己的接收緩衝區窗口大小。
注意,accept函數調用成功,返回的是一個已經完成tcp三次握手的客戶端連接。如果在三次握手的過程中(最後一步),服務端沒有接收到客戶端的ACK,則說明三次握手還沒有建立完成,accept函數依然會阻塞。
關於tcp三次握手連接建立的幾種狀態:SYN_SENT,SYN_RCVD,ESTABLISHED。
SYN_SENT:當客戶端調用connect函數向服務端發送SYN包時,客戶端就會進入SYN_SENT狀態,並且還會等待伺服器發送第二個SYN + ACK包,因此SYN_SENT狀態就是表示客戶端已經發送SYN包。
SYN_RCVD:當服務端接收到客戶端發送的SYN包並確認時,服務端就會進入SYN_RCVD狀態,這是tcp三次握手建立的一個很短暫的中間狀態,一般很難看到,SYN_RCVD狀態表示服務端已經確認收到客戶端發送的SYN包。
ESTABLISHED:該狀態表示tcp三次握手連接建立完成。
對於這兩個隊列需要注意幾點注意:
1.未完成隊列和已完成隊列的總和不超過listen函數的backlog參數的大小。listen函數的簽名如下:
2.一旦該連接的tcp三次握手完成,就會從未完成隊列加入到已完成隊列中
3.如果未決連接隊列已滿,當又接收到一個客戶端SYN時,服務端的tcp將會忽略該SYN,也就是不會理客戶端的SYN,但是服務端並不會發送RST報文,原因是:客戶端tcp可以重傳SYN,並期望在超時前未決連接隊列找到空位與服務端建立連接,這當然是我們所希望看到的。如果服務端直接發送一個RST的話,那麼客戶端的connect函數將會立即返回一個錯誤,而不會讓tcp有機會重傳SYN,顯然我們也並不希望這樣做。
但是不排除有些linux實現在未決連接隊列滿時,的確會發送RST。但是這種做法是不正確的,因此我們最好忽略這種情況,處理這種額外情況的代碼也會降低客戶端程序的健壯性。
connect函數出錯情況
由於connect函數是在建立tcp連接成功或失敗才返回,返回成功的情況本文上面已經介紹過了。這裡我們介紹connect函數返回失敗的幾種情況:
第一種
當客戶端發送了SYN報文後,沒有收到確認則返回ETIMEDOUT錯誤,值得注意的是,失敗一次並不會馬上返回ETIMEDOUT錯誤。即當你調用了connect函數,客戶端發送了一個SYN報文,沒有收到確認就等6s後再發一個SYN報文,還沒有收到就等24s再發一個(不同的linux系統設置的時間可能有所不同,這裡以BSD系統為主)。這個時間是累加的,如果總共等了75s後還是沒收到確認,那麼客戶端將返回ETIMEDOUT錯誤。
你能通過以下命令修改該值:
查看該值的命令是:
如果希望重啟後生效,將net.ipv4.tcp_syn_retries = 6放入/etc/sysctl.conf中。
這種情況一般是發生在服務端的可能性比較大,也就是服務端當前所處網路環境流量負載過高,網路擁塞了,然後服務端收到了客戶端的SYN報文卻來不及響應,或者發送的響應報文在網路傳輸過程中老是丟失,導致客戶端遲遲收不到確認,最後返回ETIMEDOUT錯誤。
我們可以簡單復現一下這種情況,這個實驗是基於CentOS系統進行的,具體過程如下所示:
1. 首先通過iptables -F把Centos上的防火牆規則清理掉,然後再通過iptables -I INPUT -p tcp --syn -i lo -j DROP命令把本地的所有SYN包都過濾掉(模擬服務端當前網路不穩定)。
執行以下命令:
2. 然後通過nc命令向本地的環回地址127.0.0.1發起tcp連接請求(相當於自己跟自己發起tcp連接),來模擬客戶端跟服務端發起tcp連接,但是伺服器端就是不響應,最後導致客戶端的tcp連接建立請求超時,並終止tcp連接。
3. 然後再通過tcpdump工具把客戶端和服務端建立tcp連接過程中的數據報都抓取下來,由於我們設置的伺服器偵聽埠號是10086,這裡我們可以通過tcpdump -i any port 10086命令來過濾所有網卡的10086埠的數據包。如上圖所示,localhost.39299代表客戶端,localhost.10086代表服務端,客戶端總共向服務端發送了6個SYN報文,這6個SYN包的間隔時間分別是1s,2s,4s,8s,16s,這些時間累積加起來總共為31s,其實客戶端在發送最後一個SYN報文時還等待了一段時間,然後才超時。也就是說,客戶端在發送了第一個SYN報文時,會設置了一個計時器並開始計時,在最後一個SYN報文還沒收到服務端的確認時,這個計時器就會超時,然後關閉tcp連接。
第二種
客戶端連接一個伺服器沒有偵聽的埠。
過程是:客戶端發送了一個SYN報文後,然後服務端回復了一個RST報文,說明這是一個異常的tcp連接,服務端發送了RST報文重置這個異常的tcp連接。
這種情況一般為拒絕連接請求,比如:客戶端想和服務端建立tcp連接,但是客戶端的連接請求中使用了一個不存在或沒有偵聽的埠(比如:這個埠超出65535的範圍),那麼服務端就可以發送RST報文段拒絕這個請求。
拒絕連接一般是由伺服器主動發起的,因為客戶端發起請求連接攜帶的目的埠,可能伺服器並沒有開啟LISTEN狀態。因此伺服器在收到這樣的報文段後會發送一個RST報文段,在這個報文里把RST和ACK都置為1,它確認了SYN報文段並同時重置了該tcp連接,然後伺服器等待另一個連接。客戶端在收到RST+ACK報文段後就會進入CLOSED狀態。
這裡以通過20000不存在的埠遠程登錄為例:
tcpdump抓取到的數據包如下:
通過分析tcpdump工具抓取的數據發現,RST報文段不攜帶數據。
第三種
如果客戶端調用connect函數向服務端發送了一個SYN報文,這個SYN報文在網路傳輸過程中經過某個路由器時,正好這個路由器出問題了,缺少到達目的地的路由,不能把這個SYN報文轉發給目的地址,那麼該路由器會丟棄這個SYN報文,並同時給客戶端發送一個Destination unreachable(主機不可達)的ICMP差錯報文。客戶端的linux內核會保存這個Destination unreachable的ICMP差錯報文,同時按第一種情況繼續發送SYN報文,如果在規定的時間超時後還沒收到服務端的響應報文,那麼linux內核會把保存的ICMP差錯報文作為EHOSTUNREACH或ENETUNREACH錯誤返回給客戶端的應用進程。
下面的這個實驗就是用來說明第三種情況,幫助理解,大家能看明白就行了,可以不用去做這個實驗,當然,有興趣的同學可以去模擬一下。
然後client遠程登錄server成功。
上圖中沒有指定telnet埠號,使用默認埠號23。
這是抓取到的數據包,client在遠程登錄server時,發起了SYN連接請求。
現在我們來模擬client設備出故障,刪除R1設備到server的路由信息
client再登錄server時就會失敗,我們從抓取到的數據包可以發現,client發送了一個SYN報文,然後R1設備收到這個SYN報文時,發現自己不能到達server,於是會把這個SYN報文丟棄掉,並向client發送了一個目標主機不可達的ICMP差錯報文,於是client發送了RST報文來關閉這條異常的tcp連接。
全文完。
文章由網友song投稿,經張小方修改部分內容和文字錯誤。其博客地址是:
學習知識不僅要知其然也要知其所以然,這是我想通過這篇文章傳達的一個理念,文中一步步的實驗探索體現了學習知識動手實踐的重要性,這是非常值得提倡的。感謝song的投稿,期待更多的朋友給我投稿。
歡迎關注公眾號『easyserverdev』。如果有任何技術或者職業方面的問題需要我提供幫助,可通過這個公眾號與我取得聯繫,此公眾號不僅分享高性能伺服器開發經驗和故事,同時也免費為廣大技術朋友提供技術答疑和職業解惑,您有任何問題都可以在微信公眾號直接留言,我會儘快回復您。
TAG:高性能伺服器開發 |