0x00 前言
本文先介紹因特網(wǎng)的核心協(xié)議 TCP ,再以 Python 的 socket 模塊為例介紹網(wǎng)絡(luò)套接字,最后給出 TCP 服務(wù)器與客戶端的 Python 腳本,并演示兩者之間的通信過程。
0x01 TCP 協(xié)議
TCP(Transmission Control Protocol,傳輸控制協(xié)議)是一種面向連接、可靠的、基于字節(jié)流的傳輸層通信協(xié)議。
TCP 協(xié)議的執(zhí)行過程分為連接創(chuàng)建(Connection Establishment)、數(shù)據(jù)傳送(Data Transfer)和連接終止(Connection Termination)三個階段,其中「連接創(chuàng)建」與「連接終止」分別是耳熟能詳?shù)?TCP 協(xié)議三次握手(TCP Three-way Handshake)與四次揮手(TCP Four-way Handshake),也是理解本文 TCP 服務(wù)器與客戶端通信過程的兩個核心階段。
為了能更好地理解下述過程,對 TCP 協(xié)議頭的關(guān)鍵區(qū)段做以下幾點說明:
- 報文的功能在 TCP 協(xié)議頭的標(biāo)記符(Flags)區(qū)段中定義,該區(qū)段位于第 104~111 比特位,共占 8 比特,每個比特位對應(yīng)一種功能,置 1 代表開啟,置 0 代表關(guān)閉。例如,SYN 報文的標(biāo)記符為 00000010,ACK 報文的標(biāo)記符為 00010000,ACK + SYN 報文的標(biāo)記符為 00010010。
- 報文的序列號在 TCP 協(xié)議頭的序列號(Sequence Number)區(qū)段中定義,該區(qū)段位于第 32~63 比特位,共占 32 比特。例如,在「三次握手」過程中,初始序列號 seq 由數(shù)據(jù)發(fā)送方隨機(jī)生成。
- 報文的確認(rèn)號在 TCP 協(xié)議頭的確認(rèn)號(Acknowledgement Number)區(qū)段中定義,該區(qū)段位于第 64~95 比特位,共占 32 比特。例如,在「三次握手」過程中,確認(rèn)號ack 為前序接收報文的序列號加 1,代表下一次期望接收到的報文序列號。
連接創(chuàng)建
所謂的「三次握手」,即 TCP 服務(wù)器與客戶端成功建立通信連接必經(jīng)的三個步驟,共需通過三個報文完成。
一般而言,首先發(fā)送 SYN 報文的一方是客戶端,服務(wù)器則是監(jiān)聽來自客戶端的建立連接請求。
Handshake Step 1
客戶端向服務(wù)器發(fā)送 SYN 報文(SYN=1)請求建立連接。
此時報文的初始序列號為 seq = x ,確認(rèn)號為ack = 0,發(fā)送完畢后,客戶端進(jìn)入 SYN_SENT 狀態(tài)。
Handshake Step 2
服務(wù)器接收到客戶端的 SYN 報文后,發(fā)送 ACK + SYN 報文(ACK=1,SYN=1)確認(rèn)客戶端的建立連接請求,并也向其發(fā)起建立連接請求。
此時報文的序列號為 seq = y,確認(rèn)號為 ack = x+1,發(fā)送完畢后,服務(wù)器進(jìn)入 SYN_RCVD狀態(tài)。
Handshake Step 3
客戶端接收到服務(wù)器的 SYN 報文后,發(fā)送 ACK 報文(ACK=1)確認(rèn)服務(wù)器的建立連接請求。
此時報文的序列號為 seq=x+1,確認(rèn)號為 ack=y+1。發(fā)送完畢后,客戶端進(jìn)入 ESTABLISHED 狀態(tài);當(dāng)服務(wù)器接收該報文后,也進(jìn)入了 ESTABLISHED 狀態(tài)。
至此,「三次握手」過程全部結(jié)束,TCP 通信連接成功建立。
讀者可參照以下「三次握手」的示意圖進(jìn)行理解:
連接終止(Connection Termination)
所謂的「四次揮手」,即 TCP 服務(wù)器與客戶端完全終止通信連接必經(jīng)的四個步驟,共需通過四個報文完成。
由于 TCP 通信連接是全雙工的,因此每個方向的連接可以單獨關(guān)閉,即可視為一對「二次揮手」,或一對單工連接。主動先發(fā)送 FIN 報文的一方,意味著想要關(guān)閉到另一方的通信連接,即在此方向上不再傳輸數(shù)據(jù),但仍可以接收來自另一方傳輸過來的數(shù)據(jù),直到另一方也發(fā)送 FIN 報文,雙方的通信連接才完全終止。
注意,首先發(fā)送 FIN 報文的一方,既可以是客戶端,也可以是服務(wù)器。下面以客戶端先發(fā)起關(guān)閉請求為例,對「四次揮手」的過程進(jìn)行講解。
Handshake Step 1
當(dāng)客戶端不再向服務(wù)器傳輸數(shù)據(jù)時,則向其發(fā)送 FIN 報文(FIN=1)請求關(guān)閉連接。
此時報文的初始序列號為 seq = u ,確認(rèn)號為ack = 0,(若此報文中 ACK=1,則 ACK 的值與客戶端的前序接收報文有關(guān))。發(fā)送完畢后,客戶端進(jìn)入 FIN_WAIT_1 狀態(tài)。
Handshake Step 2
服務(wù)器接收到客戶端的 FIN 報文后,發(fā)送 ACK 報文(ACK = 1)確認(rèn)客戶端的關(guān)閉連接請求。
此時報文的序列號為 seq = v, 確認(rèn)號為ack = u + 1,發(fā)送完畢后,服務(wù)器進(jìn)入 CLOSE_WAIT 狀態(tài);當(dāng)客戶端接收該報文后,進(jìn)入 FIN_WAIT_2 狀態(tài)。
注意,此時 TCP 通信連接處于半關(guān)閉狀態(tài),即客戶端不再向服務(wù)器傳輸數(shù)據(jù),但仍可以接收服務(wù)器傳輸過來的數(shù)據(jù)。
Handshake Step 3
當(dāng)服務(wù)器不再向客戶端傳輸數(shù)據(jù)時,則向其發(fā)送 FIN + ACK 報文(FIN=1,ACK=1)請求關(guān)閉連接。
此時報文的序列號為 seq = w(若在半關(guān)閉狀態(tài),服務(wù)器沒有向客戶端傳輸過數(shù)據(jù),則 seq = v+1 ),確認(rèn)號為 ack = u+1。發(fā)送完畢后,服務(wù)器進(jìn)入 LAST_ACK 狀態(tài)。
Handshake Step 4
客戶端接收到服務(wù)器的 FIN + ACK 報文后,發(fā)送 ACK 報文(ACK = 1)確認(rèn)服務(wù)器的關(guān)閉連接請求。
此時報文的序列號為 seq=u+1,確認(rèn)號為 ack=w+1。發(fā)送完畢后,客戶端進(jìn)入 TIME_WAIT 狀態(tài);當(dāng)服務(wù)器接收該報文后,進(jìn)入 CLOSED 狀態(tài);當(dāng)客戶端等待了 2MSL 后,仍沒接到服務(wù)器的響應(yīng),則認(rèn)為服務(wù)器已正常關(guān)閉,自己也進(jìn)入 CLOSED 狀態(tài)。
至此,「四次揮手」過程全部結(jié)束,TCP 通信連接成功關(guān)閉。
讀者可參照以下「四次揮手」的示意圖進(jìn)行理解:
0x02 Network Socket
Network Socket(網(wǎng)絡(luò)套接字)是計算機(jī)網(wǎng)絡(luò)中進(jìn)程間通信的數(shù)據(jù)流端點,廣義上也代表操作系統(tǒng)提供的一種進(jìn)程間通信機(jī)制。
進(jìn)程間通信(Inter-Process Communication,IPC)的根本前提是能夠唯一標(biāo)示每個進(jìn)程。在本地主機(jī)的進(jìn)程間通信中,可以用 PID(進(jìn)程 ID)唯一標(biāo)示每個進(jìn)程,但 PID 只在本地唯一,在網(wǎng)絡(luò)中不同主機(jī)的 PID 則可能發(fā)生沖突,因此采用「IP 地址 + 傳輸層協(xié)議 + 端口號」的方式唯一標(biāo)示網(wǎng)絡(luò)中的一個進(jìn)程。
小貼士:網(wǎng)絡(luò)層的 IP 地址可以唯一標(biāo)示主機(jī),傳輸層的 TCP/UDP 協(xié)議和端口號可以唯一標(biāo)示該主機(jī)的一個進(jìn)程。注意,同一主機(jī)中 TCP 協(xié)議與 UDP 協(xié)議的可以使用相同的端口號。
所有支持網(wǎng)絡(luò)通信的編程語言都各自提供了一套 socket API,下面以 Python 3 為例,講解服務(wù)器與客戶端建立 TCP 通信連接的交互過程:
腦海中先對上述過程產(chǎn)生一定印象后,更易于理解下面兩節(jié) TCP 服務(wù)器與客戶端的 Python 實現(xiàn)。
0x03 TCP 服務(wù)器
- Line 6:定義一個 tcplink() 函數(shù),第一個 conn 參數(shù)為服務(wù)器與客戶端交互數(shù)據(jù)的套接字對象,第二個 addr 參數(shù)為客戶端的 IP 地址與端口號,用二元組 (host, port) 表示。
- Line 8:連接成功后,向客戶端發(fā)送歡迎信息 b"Welcome!/n"。
- Line 9:進(jìn)入與客戶端交互數(shù)據(jù)的循環(huán)階段。
- Line 10:向客戶端發(fā)送詢問信息 b"What's your name?"。
- Line 11:接收客戶端發(fā)來的 bytes 對象。
- Line 12:若 bytes 對象為 b"exit",則向客戶端發(fā)送結(jié)束響應(yīng)信息 b"Good bye!/n",并結(jié)束與客戶端交互數(shù)據(jù)的循環(huán)階段。
- Line 15:若 bytes 對象不為 b"exit",則向客戶端發(fā)送問候響應(yīng)信息 b"Hello %s!/n",其中 %s 是客戶端發(fā)來的 bytes 對象。
- Line 16:關(guān)閉套接字,不再向客戶端發(fā)送數(shù)據(jù)。
- Line 19:創(chuàng)建 socket 對象,第一個參數(shù)為 socket.AF_INET,代表采用 IPv4 協(xié)議用于網(wǎng)絡(luò)通信,第二個參數(shù)為 socket.SOCK_STREAM,代表采用 TCP 協(xié)議用于面向連接的網(wǎng)絡(luò)通信。
- Line 20:向 socket 對象綁定服務(wù)器主機(jī)地址 (“127.0.0.1”, 6000),即本地主機(jī)的 TCP 6000 端口。
- Line 21:開啟 socket 對象的監(jiān)聽功能,等待客戶端的連接請求。
- Line 24:進(jìn)入監(jiān)聽客戶端連接請求的循環(huán)階段。
- Line 25:接收客戶端的連接請求,并獲得與客戶端交互數(shù)據(jù)的套接字對象 conn 與客戶端的 IP 地址與端口號 addr,其中 addr 為二元組 (host, port)。
- Line 26:利用多線程技術(shù),為每個請求連接的 TCP 客戶端創(chuàng)建一個新線程,實現(xiàn)了一臺服務(wù)器同時與多臺客戶端進(jìn)行通信的功能。
- Line 27:開啟新線程的活動。
0x04 TCP 客戶端
- Line 5:創(chuàng)建 socket 對象,第一個參數(shù)為 socket.AF_INET,代表采用 IPv4 協(xié)議用于網(wǎng)絡(luò)通信,第二個參數(shù)為 socket.SOCK_STREAM,代表采用 TCP 協(xié)議用于面向連接的網(wǎng)絡(luò)通信。
- Line 6:向 (“127.0.0.1”, 6000) 主機(jī)發(fā)起連接請求,即本地主機(jī)的 TCP 6000 端口。
- Line 7:連接成功后,接收服務(wù)器發(fā)來的歡迎信息 b"Welcome!/n",并轉(zhuǎn)換為字符串后打印輸出。
- Line 9:創(chuàng)建一個非空字符串變量 data,并賦初值為 "client"(只要是非空字符串即可),用于判斷是否接收來自服務(wù)器發(fā)來的詢問信息 b"What's your name?"。
- Line 10:進(jìn)入與服務(wù)器交互數(shù)據(jù)的循環(huán)階段。
- Line 11:當(dāng)變量 data 非空時,則接收服務(wù)器發(fā)來的詢問信息。
- Line 13:要求用戶輸入名字。
- Line 14:當(dāng)用戶的輸入為空時,則重新開始循環(huán),要求用戶重新輸入。
- Line 16:當(dāng)用戶的輸入非空時,則將字符串轉(zhuǎn)換為 bytes 對象后發(fā)送至服務(wù)器。
- Line 17:接收服務(wù)器的響應(yīng)數(shù)據(jù),并將響應(yīng)的 bytes 對象轉(zhuǎn)換為字符串后打印輸出。
- Line 18:當(dāng)用戶的輸入為 "exit" 時,則終止與服務(wù)器交互數(shù)據(jù)的循環(huán)階段,即將關(guān)閉套接字。
- Line 21:關(guān)閉套接字,不再向服務(wù)器發(fā)送數(shù)據(jù)。
0x05 TCP 進(jìn)程間通信
將 TCP 服務(wù)器與客戶端的腳本分別命名為 tcp_server.py 與 tcp_client.py,然后存至桌面,筆者將在 Windows 10 系統(tǒng)下用 PowerShell 進(jìn)行演示。
小貼士:讀者進(jìn)行復(fù)現(xiàn)時,要確保本機(jī)已安裝 Python 3,注意筆者已將默認(rèn)的啟動路徑名 python 改為了 python3。
單服務(wù)器 VS 單客戶端
單服務(wù)器 VS 多客戶端
0x06 Python API Reference
socket 模塊
本節(jié)介紹上述代碼中用到的內(nèi)建模塊 socket,是 Python 網(wǎng)絡(luò)編程的核心模塊。
socket() 函數(shù)
socket() 函數(shù)用于創(chuàng)建網(wǎng)絡(luò)通信中的套接字對象。函數(shù)原型如下:
- family 參數(shù)代表地址族(Address Family),默認(rèn)值為 AF_INET,用于 IPv4 網(wǎng)絡(luò)通信,常用的還有 AF_INET6,用于 IPv6 網(wǎng)絡(luò)通信。family 參數(shù)的可選值取決于本機(jī)操作系統(tǒng)。
- type 參數(shù)代表套接字的類型,默認(rèn)值為 SOCK_STREAM,用于 TCP 協(xié)議(面向連接)的網(wǎng)絡(luò)通信,常用的還有 SOCK_DGRAM,用于 UDP 協(xié)議(無連接)的網(wǎng)絡(luò)通信。
- proto 參數(shù)代表套接字的協(xié)議,默認(rèn)值為 0,一般忽略該參數(shù),除非 family 參數(shù)為 AF_CAN,則 proto 參數(shù)需設(shè)置為 CAN_RAW 或 CAN_BCM。
- fileno 參數(shù)代表套接字的文件描述符,默認(rèn)值為 None,若設(shè)置了該參數(shù),則其他三個參數(shù)將會被忽略。
創(chuàng)建完套接字對象后,需使用對象的內(nèi)置函數(shù)完成網(wǎng)絡(luò)通信過程。注意,以下函數(shù)原型中的「socket」是指 socket 對象,而不是上述的 socket 模塊。
bind() 函數(shù)
bind() 函數(shù)用于向套接字對象綁定 IP 地址與端口號。注意,套接字對象必須未被綁定,并且端口號未被占用,否則會報錯。函數(shù)原型如下:
- address 參數(shù)代表套接字要綁定的地址,其格式取決于套接字的 family 參數(shù)。若 family 參數(shù)為 AF_INET,則 address 參數(shù)表示為二元組 (host, port),其中 host 是用字符串表示的主機(jī)地址,port 是用整型表示的端口號。
listen() 函數(shù)
listen() 函數(shù)用于 TCP 服務(wù)器開啟套接字的監(jiān)聽功能。函數(shù)原型如下:
- backlog 可選參數(shù)代表套接字在拒絕新連接之前,操作系統(tǒng)可以掛起的最大連接數(shù)。backlog 參數(shù)一般設(shè)置為 5,若未設(shè)置,系統(tǒng)會為其自動設(shè)置一個合理的值。
connect() 函數(shù)
connect() 函數(shù)用于 TCP 客戶端向 TCP 服務(wù)器發(fā)起連接請求。函數(shù)原型如下:
address 參數(shù)代表套接字要連接的地址,其格式取決于套接字的 family 參數(shù)。若 family 參數(shù)為 AF_INET,則 address 參數(shù)表示為二元組 (host, port),其中 host 是用字符串表示的主機(jī)地址,port 是用整型表示的端口號。
accept() 函數(shù)
accept() 函數(shù)用于 TCP 服務(wù)器接受 TCP 客戶端的連接請求。函數(shù)原型如下:
accept() 函數(shù)的返回值是二元組 (conn, address),其中 conn 是服務(wù)器用來與客戶端交互數(shù)據(jù)的套接字對象,address 是客戶端的 IP 地址與端口號,用二元組 (host, port) 表示。
send() 函數(shù)
send() 函數(shù)用于向遠(yuǎn)程套接字對象發(fā)送數(shù)據(jù)。注意,本機(jī)套接字必須與遠(yuǎn)程套接字成功連接后才能使用該函數(shù),否則會報錯??梢?,send() 函數(shù)只能用于 TCP 進(jìn)程間通信,而對于 UDP 進(jìn)程間通信應(yīng)該用 sendto() 函數(shù)。函數(shù)原型如下:
bytes 參數(shù)代表即將發(fā)送的 bytes 對象數(shù)據(jù)。例如,對于字符串 "hello world!" 而言,需要用 encode() 函數(shù)轉(zhuǎn)換為 bytes 對象 b"hello world!" 才能進(jìn)行網(wǎng)絡(luò)傳輸。
flags 可選參數(shù)用于設(shè)置 send() 函數(shù)的特殊功能,默認(rèn)值為 0,也可由一個或多個預(yù)定義值組成,用位或操作符 | 隔開。詳情可參考 Unix 函數(shù)手冊中的 send(2),flags 參數(shù)的常見取值有 MSG_OOB、MSG_EOR 、MSG_DONTROUTE等。
send() 函數(shù)的返回值是發(fā)送數(shù)據(jù)的字節(jié)數(shù)。
recv() 函數(shù)
recv() 函數(shù)用于從遠(yuǎn)程套接字對象接收數(shù)據(jù)。注意,與 send() 函數(shù)不同,recv() 函數(shù)既可用于 TCP 進(jìn)程間通信,也能用于 UDP 進(jìn)程間通信。函數(shù)原型如下:
bufsize 參數(shù)代表套接字可接收數(shù)據(jù)的最大字節(jié)數(shù)。注意,為了使硬件設(shè)備與網(wǎng)絡(luò)傳輸更好地匹配,bufsize 參數(shù)的值最好設(shè)置為 2 的冪次方,例如 4096。
flags 可選參數(shù)用于設(shè)置 recv() 函數(shù)的特殊功能,默認(rèn)值為 0,也可由一個或多個預(yù)定義值組成,用位或操作符 |隔開。詳情可參考 Unix 函數(shù)手冊中的 recv(2),flags 參數(shù)的常見取值有 MSG_OOB、MSG_PEEK、MSG_WAITALL 等。
recv() 函數(shù)的返回值是接收到的 bytes 對象數(shù)據(jù)。例如,接收到 bytes 對象 b"hello world!",最好用 decode() 函數(shù)轉(zhuǎn)換為字符串 "hello world!" 再打印輸出。
close() 函數(shù)
close() 函數(shù)用于關(guān)閉本地套接字對象,釋放與該套接字連接的所有資源。
threading 模塊
本節(jié)介紹上述代碼中用到的內(nèi)建模塊 threading,是 Python 多線程的核心模塊。
Thread() 類
Thread() 類可以創(chuàng)建線程對象,用于調(diào)用 start() 函數(shù)啟動新線程。類原型如下:
- group 參數(shù)作為以后實現(xiàn) ThreadGroup() 類的保留參數(shù),目前默認(rèn)值為 None。
- target 參數(shù)代表線程被 run() 函數(shù)激活后調(diào)用的函數(shù),默認(rèn)值為 None,即沒有任何函數(shù)會被調(diào)用。
- name 參數(shù)代表線程名,默認(rèn)值為 None,則系統(tǒng)會自動為其命名,格式為「Thread-N」,N 是從 1 開始的十進(jìn)制數(shù)。
- args 參數(shù)代表 target 參數(shù)指向函數(shù)的普通參數(shù),用元組(tuple)表示,默認(rèn)值為空元組 ()。
- kwargs 參數(shù)代表 target 參數(shù)指向函數(shù)的關(guān)鍵字參數(shù),用字典(dict)表示,默認(rèn)值為空字典 {}。
- daemon 參數(shù)用于標(biāo)示進(jìn)程是否為守護(hù)進(jìn)程。若設(shè)置為 True,則標(biāo)示為守護(hù)進(jìn)程;若設(shè)置為 False,則標(biāo)示為非守護(hù)進(jìn)程;若設(shè)置為 None,則繼承當(dāng)前父線程的 daemon 參數(shù)值。
創(chuàng)建完線程對象后,需使用對象的內(nèi)置函數(shù)控制多線程活動。
start() 函數(shù)
start() 函數(shù)用于開啟線程活動。函數(shù)原型如下:
注意,每個線程對象只能調(diào)用一次 start() 函數(shù),否則會導(dǎo)致 RuntimeError 錯誤。
0x07 總結(jié)
本文介紹了 TCP 協(xié)議與 socket 編程的基礎(chǔ)知識,再用 Python 3 實現(xiàn)并演示了 TCP 服務(wù)器與客戶端的通信過程,其中還運用了簡單的多線程技術(shù),最后將腳本中涉及到的 Python API 做成了的參考索引,有助于理解實現(xiàn)過程。
【責(zé)任編輯:武曉燕 TEL:(010)68476606】