從TCP到Socket,徹底理解網路程式設計是怎麼回事
進行程式開發的同學,無論Web前端開發、Web後端開發,還是搜尋引擎和大資料,幾乎所有的開發領域都會涉及到網路程式設計。比如我們進行Web服務端開發,除了Web協議本身依賴網路外,通常還需要連線資料庫,而資料庫連線通常是通過網路連線資料庫伺服器,或者資料庫叢集,如果負載太高還要搞個快取叢集。
我們在上學的時候基本學了網路程式設計和網路協議。但兩者之間的具體關係可能有些摸不到頭腦。這裡我們首先重點介紹2個概念,一個概念是網路程式設計,另外一個是協議。
我們知道網路協議是一個分層的協議族,也就是是有一組協議構成,從下往上各自負責各自的功能。那什麼是協議呢?協議的字面意思是共同計議,商議。簡單的理解其實就是多方進行溝通的規定。而網路協議其實就是在網路中多個計算節點進行互動、溝通的規定。如果根我們日常生活對比的話,協議可以理解為語言,比如漢語普通話。兩個人交流如果都用不通話,那麼彼此都能理解對方表達的意圖。例如,一個人用四川話,而另外一個用浙江話,那溝通起來估計幾乎不太可能。網路協議也是一樣的,通過對資料格式的規範化,從而使計算機之間能夠彼此明確對方的意圖。
下面本文介紹一下網路程式設計,網路程式設計也稱為socket程式設計,socket通常譯作“套接字”,但原意其實意譯應該為”介面“。也就是作業系統提供給開發人員進行網路開發的API介面。這套介面通常可以引數的調整支援多種協議,包括TCP、UDP和IP等等。下面本文從套接字程式設計和協議兩方面分別詳細的進行介紹。
網路程式設計
為了便於理解,本文先從具體的內容開始,也就是通過一個例項介紹一下網路程式設計是怎麼回事。
本文將以TCP協議為例介紹網路程式設計和協議之前的關係。為了簡單,便於理解,本文以Python為例進行介紹,如果不瞭解Python程式語言關係也不大,下面程式碼很容易理解。我們知道在網路通訊中無論是BS架構還是CS架構,通常分為服務端和客戶端,只不過BS架構中的瀏覽器就是客戶端。因此,本文的示例也包含服務端和客戶端2部分的程式碼。程式碼功能很簡單,就是實現客戶端和服務端傳送字串。
圖1 客戶端服務端通訊模型
這個程式碼清單是服務端的程式碼,這段程式碼的作用就是在服務端的某個埠建立監聽,並等待客戶端建立連線。完成連線建立後,等待客戶端傳送資料,並將資料回傳給客戶端。
#!/usr/bin/env python3 #-*- coding:utf-8 -*- from socket import * from time import ctime host = '' port = 12345 buffsize = 2048 ADDR = (host,port) # 建立一個基於TCP協議的套接字 tctime = socket(AF_INET,SOCK_STREAM) tctime.bind(ADDR) # 在指定的地址和埠監聽 tctime.listen(3) while True: print('Wait for connection ...') tctimeClient,addr = tctime.accept() print("Connection from :",addr) while True: data = tctimeClient.recv(buffsize).decode() if not data: break tctimeClient.send(('[%s] %s' % (ctime(),data)).encode()) tctimeClient.close() tctimeClient.close()
閱讀服務端的程式碼可以看出主要包括,socket、bind、listen、accept、recv和send幾個。其中值得關注的是listen和accept,兩者分別用於監聽埠和接受客戶端的連線請求。
下面程式碼清單是客戶端的實現,這裡特別的地方是有一個connect函式,該函式實現與服務端建立連線。
#!/usr/bin/env python3 #-*- coding:utf-8 -*- from socket import * HOST ='localhost' PORT = 12345 BUFFSIZE=2048 ADDR = (HOST,PORT) tctimeClient = socket(AF_INET,SOCK_STREAM) tctimeClient.connect(ADDR) while True: data = input(">") if not data: break tctimeClient.send(data.encode()) data = tctimeClient.recv(BUFFSIZE).decode() if not data: break print(data) tctimeClient.close()
通過上述示例程式碼可以看出服務端通常是被動的,而客戶端則要主動一些。服務端程式建立對某個埠的監聽,等待客戶端的連線請求。客戶端向服務端傳送連線請求,不出意外的情況下連線建立成功,這時客戶端和服務端之前就可以互發資料了。當然,在實際生產環境中意外是經常的,因此從協議和介面層面,需要處理各種意外,本文在協議部分將詳細介紹。
另外,本文實現了一個基本的客戶端和服務端通訊的程式,這個模式的通訊在實際生產中幾乎不再使用。在實際生產中為了提高資料傳輸和處理的效率,通常採用非同步模式,這些內容超出了本文的介紹範圍,後續文章會逐漸介紹。
TCP協議詳解
前文說了網路協議是網路中不同計算機資訊通訊的語言,為了實現互動,這個語言就需要有一定的格式。本文以TCP協議為例進行介紹。
TCP協議是一個可靠的傳輸協議,其可靠性表現在2方面,一方面是保證資料包可以按照發送的順序到達,另外一方面是保證資料包一定程度的正確性(後文詳解為什麼是一定程度上的正確性)。其可靠性的實現則基於2點技術,一點是具有一個CRC校驗,這樣如果資料包中的某些資料出現錯誤可以通過該校驗和發現;另外一點是每個資料包都有一個序號,這樣就能保證資料包的順序性,如果出現錯位的資料包可以請求重發。
既然說到了格式,那我們先看一下TCP資料包的資料格式。如下圖是TCP資料包的格式,包括原埠、目的埠、序列號和標識位等等內容,內容有些多,看著可能有點眼花。但從大的方面理解,這個資料包其實只包含2部分內容,一個是包頭,另外一個則是具體需要傳輸的資料。在TCP協議的控制邏輯中,包頭起著最為關鍵的作用,它是TCP協議中諸如建立連線、斷開連線、重傳和錯誤校驗等各種特性的基礎。
圖2 TCP資料包格式
包頭的其它資訊的含義都比較明瞭,本文僅僅介紹幾個標誌位(URG、ACK、PSH、RST、SYN和FIN)的含義。具體含義如下:
- ACK: 確認序號有效。
- RST:重置連線
- SYN:發起一個新連線
- FIN:釋放一個連線
連線的建立
TCP在具體傳輸資料之前需要建立連線。這裡的連線並不是物理連線,物理連線基於底層的協議已經建立完成,而且TCP建立連線也是要假設底層連線已經成功,TCP的連線其實是一個虛擬的,邏輯的連線。簡單粗暴的理解,就是客戶端和服務端分別記錄了各自接受到的資料包的序號,並且將自身設定為某種狀態。在TCP協議中,連線的建立通常成為3次握手,從字面的概念可以看出,連線的建立需要經過3次確認的過程。
圖3 建立連線的3次握手
TCP協議3次握手的過程如圖所示,初始狀態客戶端和服務端都處於關閉狀態。主要過程分為3步:
- 客戶端傳送預連線資料包: TCP的連線是由客戶端主動發起建立,客戶端會發送一個數據包(報文)給服務端,需要注意的是資料包中的SYN標識位為1。我們前文已經介紹,如果SYN為1,則說明為建立連線的資料包。同時,在該資料包中包含一個請求序列號,該序列號也是建立連線的依據。
- 服務端回覆連線確認: 服務端確認可以建立連線(服務端不一定可以建立連線,因為系統中套接字的數量是有限的)的情況下會向客戶端傳送一個應答資料包。在應答資料包中會將ACK標誌位設定為1,表示為服務端應答資料包。同時,在應答資料包中會設定請求序列號和應答序列號的值,具體參考圖3.
- 客戶端回覆連線確認: 最後,客戶端再次傳送一個連線確認資料包,告訴服務端連線建立成功。
從上面流程可以看出,連線的建立需要經過多次互動,這就是我們日常中所說的建立連線是高成本的操作。在實際生產環境中,為了應對這個問題,會減少連線建立的頻度,通常的做法是建立連線池,傳輸資料時直接從連線池中獲取連線,而不是新建連線。
有人可能覺得可以對建立連線的過程進行優化,比如將客戶端最後一次的確認取消掉,覺得這個沒有卵用。對於正常情況確實沒有多大的作用,這裡主要是應對異常情況。因為網路拓撲是非常複雜的,特別是在廣域網中,有著數不清的網路節點,因此會出現各種異常情況。因此,TCP協議在設計的時候必須要保證異常情況下的可靠性。
我們這裡舉一個例子,就是連線請求超時的情況。假設客戶端向服務端傳送一個連線請求,由於各種原因,請求一直沒有到達服務端,因此服務端也就沒有回覆連線確認訊息。客戶端連線超時,因此客戶端重新發送一個連線請求到服務端,這次比較順利,很快到達了,並且順利建立了連線。之後,前一個數據包經過長途跋涉最終還是到了服務端,服務端也向客戶端傳送了回覆資料包,服務端認為連線是建立成功的,並且會維持連線。但客戶端層面認為連線是超時的,因此將永遠不會關閉該連線。這樣就會造成服務端有殘留的資源,從而造成服務端資源浪費,久而久之可能會導致服務端無新連線資源可用。
另外一個需要說明的是客戶端和服務端的套接字都有相應的狀態,而且狀態會隨著連線的不同階段變化。初始狀態都是CLOSE,最終連線建立成功後都是ESTABLISHED,具體變化過程如圖3所示。後面本文會詳細介紹狀態變化情況。
傳輸資料
完成連線建立之後,客戶端和服務端就可以進行資料傳輸了。我們知道TCP是可靠的傳輸,那麼傳輸的可靠性是通過什麼來保證的呢?主要就是通過包頭中的校驗和、請求序列號和應答序列號(參考圖2)。
TCP資料內容的可靠性是通過校驗和保證的。TCP在傳送資料時都會計算整個資料包的校驗和,並存儲在包頭的校驗和欄位中。接收方會按照規則進行計算,從而確認接收到的資料是否是正確的。傳送發計算校驗和的流程大概如下:
- 把偽首部、TCP包頭和TCP資料分為16為的字,並把TCP包頭中的校驗和欄位置0
- 用反碼加法累加所有16位數字
- 對計算結果去反,將其填充到TCP包頭的校驗和欄位
接收方將所有原碼相加,高位疊加,如果全為1則表示資料正確,否則說明資料有錯誤。
TCP資料包順序的可靠性是通過請求序列號和應答序列號保證的。在資料傳輸中的每個請求都會有一個請求序列號,而在接收方接收到資料後會傳送一個應答序列號,這樣傳送方就能知道資料是否被正確接收,而接收方也能知道資料是否出現亂序,從而保證資料包的順序性。
斷開連線
TCP關閉連線分為4步,稱為4次揮手。連線的關閉不一定是在客戶端發起,服務端也可以發起關閉連線。關閉連線的過程如下:
- 發起方傳送一個FIN置位的資料包,用來請求關閉傳送方到接收方的連線
- 接收方傳送一個應答,ACK標誌位為1,確認關閉。此時完成了發起方到接收方的連線,也即傳送方無法再向接收方傳送資料,但接收方還可以向傳送方傳送資料。
- 接收方資料傳輸完成後向發起方傳送一個FIN為1的包,表示請求斷開連線
- 發起方回覆一個ACK包,確認關閉成功
圖4 關閉連線流程示意圖
TCP是全雙工通訊,因此關閉連線時需要雙向關閉連線。首先是關閉發起方關閉本端的連線,然後是關閉接收方在收到發起方的關閉請求後,除了回覆關閉應答外,還要確保資料傳輸完成後發起一個關閉連線的請求,保證雙向同時關閉。
截止到這裡,本文介紹了基於TCP協議進行網路程式設計的主要內容。當然這個只是入門級的,如果需要真正理解TCP協議和網路程式設計還需要學習很多內容。後續本號將陸續介紹給大家。