當前位置: 妍妍網 > 碼農

一個指令碼,既可以提升你python,又可以讓你掌握TCP~

2024-05-29碼農

關註上方 浩道Linux ,回復 資料 ,即可獲取海量 L inux Python 網路通訊、網路安全 等學習資料!

前言

大家好,這裏是 浩道Linux ,主要給大家分享 L inux P ython 網路通訊、網路安全等 相關的IT知識平台。

今天浩道跟大家分享一個python指令碼,學完既可以讓你python編程能力得到提升,又可以讓你的TCP知識得到回顧。

文章來源:

https://ciphersaw.me/2018/05/23/python-trick-tcp-server-and-client/

一、前言

「網路」一直以來都是黑客最熱衷的競技場。數據在網路中肆意傳播:主機掃描、程式碼註入、網路嗅探、數據篡改重放、拒絕服務攻擊……黑客的功底越深厚,能做的就越多。

Python 作為一種解釋型手稿語言,自 1991 年問世以來,其簡潔、明確、可讀性強的語法深受黑客青睞,特別在網路工具的編寫上,避免了繁瑣的底層語法,沒有對執行速度的高效要求,使得 Python 成為安全工作者的必備殺手鐧。

本文先介紹因特網的核心協定 TCP ,再以 Python 的 socket 模組為例介紹網路套接字,最後給出 TCP 伺服器與客戶端的 Python 指令碼,並演示兩者之間的通訊過程。

二、TCP 協定

TCP(Transmission Control Protocol,傳輸控制協定)是一種面向連線、可靠的、基於字節流的傳輸層通訊協定。

TCP 協定的執行過程分為 連線建立 (Connection Establishment)、 數據傳送 (Data Transfer)和 連線終止 (Connection Termination)三個階段,其中「連線建立」與「連線終止」分別是耳熟能詳的 TCP 協定 三次握手 (TCP Three-way Handshake)與 四次揮手 (TCP Four-way Handshake),也是理解本文 TCP 伺服器與客戶端通訊過程的兩個核心階段。

為了能更好地理解下述過程,對 TCP 協定頭的關鍵區段做以下幾點說明:

  • 報文的功能在 TCP 協定頭的 標記符 (Flags)區段中定義,該區段位於第 104~111 位元位,共占 8 位元,每個位元位對應一種功能,置 1 代表開啟,置 0 代表關閉。例如,SYN 報文的標記符為 00000010 ,ACK 報文的標記符為 00010000 ,ACK + SYN 報文的標記符為 00010010

  • 報文的序列號在 TCP 協定頭的 序列號 Sequence Number )區段中定義,該區段位於第 32~63 位元位,共占 32 位元。例如,在「三次握手」過程中,初始序列號由數據發送方隨機生成。

  • 報文的確認號在 TCP 協定頭的 確認號 (Acknowledgement Number)區段中定義,該區段位於第 64~95 位元位,共占 32 位元。例如,在「三次握手」過程中,確認號為前序接收報文的序列號加 1,代表下一次期望接收到的報文序列號。

  • 2.1 連線建立(Connection Establishment)

    所謂的「三次握手」,即 TCP 伺服器與客戶端成功建立通訊連線必經的三個步驟,共需透過三個報文完成。

    一般而言,首先發送 SYN 報文的一方是客戶端,伺服器則是監聽來自客戶端的建立連線請求。

    Handshake Step 1

    客戶端向伺服器發送 SYN 報文(SYN = 1)請求建立連線。

    此時報文的初始序列號為seq = x ,確認號為 ack = 0。發送完畢後,客戶端進入 SYN_SENT 狀態。

    Handshake Step 2

    伺服器接收到客戶端的 SYN 報文後,發送 ACK + SYN 報文(ACK = 1,SYN = 1,)確認客戶端的建立連線請求,並也向其發起建立連線請求。

    此時報文的序列號為seq = y ,確認號為ack = x + 1 。發送完畢後,伺服器進入 SYN_RCVD 狀態。

    Handshake Step 3

    ,發送 ACK 報文(ACK = 1)確認伺服器的建立連線請求。

    此時報文的序列號為seq = x + 1 ,確認號為 ack = y + 1 。發送完畢後,客戶端進入 ESTABLISHED 狀態;當伺服器接收該報文後,也進入了 ESTABLISHED 狀態。

    至此,「三次握手」過程全部結束,TCP 通訊連線成功建立。

    讀者可參照以下「三次握手」的示意圖進行理解:

    2.2 連線終止(Connection Termination)

    所謂的「四次揮手」,即 TCP 伺服器與客戶端完全終止通訊連線必經的四個步驟,共需透過四個報文完成。

    由於 TCP 通訊連線是 全雙工 的,因此每個方向的連線可以單獨關閉,即可視為一對「二次揮手」,或一對單工連線。主動先發送 FIN 報文的一方,意味著想要關閉到另一方的通訊連線,即在此方向上不再傳輸數據,但仍可以接收來自另一方傳輸過來的數據,直到另一方也發送 FIN 報文,雙方的通訊連線才完全終止。

    註意,首先發送 FIN 報文的一方,既可以是客戶端,也可以是伺服器 。下面以客戶端先發起關閉請求為例,對「四次揮手」的過程進行講解。

    Handshake Step 1

    當客戶端不再向伺服器傳輸數據時,則向其發送 FIN 報文(FIN = 1 )請求關閉連線。

    此時報文的初始序列號為seq = u ,確認號為ack = 0 (若此報文中ACK = 1 ,則ack 的值與客戶端的前序接收報文有關)。發送完畢後,客戶端進入 FIN_WAIT_1 狀態。

    Handshake Step 2

    伺服器接收到客戶端的 FIN 報文後,發送 ACK 報文(ACK = 1 )確認客戶端的關閉連線請求。

    此時報文的序列號為seq = v ,確認號為 ack = u + 1。發送完畢後,伺服器進入 CLOSE_WAIT 狀態;當客戶端接收該報文後,進入 FIN_WAIT_2 狀態。

    註意,此時 TCP 通訊連線處於 半關閉 狀態,即客戶端不再向伺服器傳輸數據,但仍可以接收伺服器傳輸過來的數據。

    Handshake Step 3

    FIN + ACK 報文(FIN = 1,ACK = 1)請求關閉連線。此時報文的序列號為seq = w (若在半關閉狀態,伺服器沒有向客戶端傳輸過數據,則seq = v + 1 ),確認號為 ack = u + 1。發送完畢後,伺服器進入 LAST_ACK 狀態。

    Handshake Step 4

    客戶端接收到伺服器的 FIN + ACK 報文後,發送 ACK 報文(ACK = 1)確認伺服器的關閉連線請求。

    此時報文的序列號為 seq = u + 1,確認號為 ack = w + 1。發送完畢後,客戶端進入 TIME_WAIT 狀態;當伺服器接收該報文後,進入 CLOSED 狀態;當客戶端等待了 2MSL 後,仍沒接到伺服器的響應,則認為伺服器已正常關閉,自己也進入 CLOSED 狀態。

    至此,「四次揮手」過程全部結束,TCP 通訊連線成功關閉。

    讀者可參照以下「四次揮手」的示意圖進行理解:

    三、Network Socket

    Network Socket (網路套接字)是電腦網路中行程間通訊的數據流端點,廣義上也代表作業系統提供的一種行程間通訊機制。

    行程間通訊 (Inter-Process Communication,IPC)的根本前提是 能夠唯一標示每個行程 。在本地主機的行程間通訊中,可以用 PID(行程 ID)唯一標示每個行程,但 PID 只在本地唯一,在網路中不同主機的 PID 則可能發生沖突,因此采用 「IP 地址 + 傳輸層協定 + 埠號」 的方式唯一標示網路中的一個行程。

    小貼士:網路層的 IP 地址可以唯一標示主機,傳輸層的 TCP/UDP 協定和埠號可以唯一標示該主機的一個行程。註意,同一主機中 TCP 協定與 UDP 協定的可以使用相同的埠號。

    所有支持網路通訊的程式語言都各自提供了一套 socket API,下面以 Python 3 為例,講解伺服器與客戶端建立 TCP 通訊連線的互動過程:

    腦海中先對上述過程產生一定印象後,更易於理解下面兩節 TCP 伺服器與客戶端的 Python 實作。

    四、TCP 伺服器

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    import socket
    import threading
    def tcplink(conn, addr):
    print("Accept new connection from %s:%s" % addr)
     conn.send(b"Welcome!\n")
    while True:
    conn.send(b"What's your name?")
    data = conn.recv(1024)
    if data == b"exit":
    conn.send(b"Good bye!\n")
    break
    conn.send(b"Hello %s!\n" % data)
     conn.close()
    print("Connection from %s:%s is closed" % addr)
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("127.0.0.1", 6000))
    s.listen(5)
    print("Waiting for connection...")
    while True:
     conn, addr = s.accept()
     t = threading.Thread(target = tcplink, args = (conn, addr))
     t.start()

  • Line 6:定義一個 tcplink() 函式,第一個 conn 參數為伺服器與客戶端互動數據的套接字物件,第二個 addr 參數為客戶端的 IP 地址與埠號,用二元組 (host, port) 表示。

  • Line 8:連線成功後,向客戶端發送歡迎資訊 b"Welcome!\n"。

  • Line 9:進入與客戶端互動數據的迴圈階段。

  • Line 10:向客戶端發送詢問資訊 b"What's your name?"。

  • Line 11:接收客戶端發來的 bytes 物件。

  • Line 12:若 bytes 物件為 b"exit",則向客戶端發送結束響應資訊 b"Good bye!\n",並結束與客戶端互動數據的迴圈階段。

  • Line 15:若 bytes 物件不為 b"exit",則向客戶端發送問候響應資訊 b"Hello %s!\n",其中 %s 是客戶端發來的 bytes 物件。

  • Line 16:關閉套接字,不再向客戶端發送數據。

  • Line 19:建立 socket 物件,第一個參數為 socket.AF_INET,代表采用 IPv4 協定用於網路通訊,第二個參數為 socket.SOCK_STREAM,代表采用 TCP 協定用於面向連線的網路通訊。

  • Line 20:向 socket 物件繫結伺服器主機地址 (「127.0.0.1」, 6000),即本地主機的 TCP 6000 埠。

  • Line 21:開啟 socket 物件的監聽功能,等待客戶端的連線請求。

  • Line 24:進入監聽客戶端連線請求的迴圈階段。

  • Line 25:接收客戶端的連線請求,並獲得與客戶端互動數據的套接字物件 conn 與客戶端的 IP 地址與埠號 addr,其中 addr 為二元組 (host, port)。

  • Line 26:利用多執行緒技術,為每個請求連線的 TCP 客戶端建立一個新執行緒,實作了一台伺服器同時與多台客戶端進行通訊的功能。

  • Line 27:開啟新執行緒的活動。

  • 五、TCP 客戶端

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("127.0.0.1", 6000))
    print(s.recv(1024).decode())
    data = "client"
    while True:
    if data:
    print(s.recv(1024).decode())
     data = input("Please input your name: ")
    if not data:
    continue
     s.send(data.encode())
    print(s.recv(1024).decode())
    if data == "exit":
    break
    s.close()

  • Line 5:建立 socket 物件,第一個參數為 socket.AF_INET,代表采用 IPv4 協定用於網路通訊,第二個參數為 socket.SOCK_STREAM,代表采用 TCP 協定用於面向連線的網路通訊。

  • Line 6:向 (「127.0.0.1」, 6000) 主機發起連線請求,即本地主機的 TCP 6000 埠。

  • Line 7:連線成功後,接收伺服器發來的歡迎資訊 b"Welcome!\n",並轉換為字串後打印輸出。

  • Line 9:建立一個非空字串變量 data,並賦初值為 "client"(只要是非空字串即可),用於判斷是否接收來自伺服器發來的詢問資訊 b"What's your name?"。

  • Line 10:進入與伺服器互動數據的迴圈階段。

  • Line 11:當變量 data 非空時,則接收伺服器發來的詢問資訊。

  • Line 13:要求使用者輸入名字。

  • Line 14:當使用者的輸入為空時,則重新開始迴圈,要求使用者重新輸入。

  • Line 16:當使用者的輸入非空時,則將字串轉換為 bytes 物件後發送至伺服器。

  • Line 17:接收伺服器的響應數據,並將響應的 bytes 物件轉換為字串後打印輸出。

  • Line 18:當使用者的輸入為 "exit" 時,則終止與伺服器互動數據的迴圈階段,即將關閉套接字。

  • Line 21:關閉套接字,不再向伺服器發送數據。

  • 六、TCP 行程間通訊

    將 TCP 伺服器與客戶端的指令碼分別命名為 tcp_server.py 與 tcp_client.py,然後存至桌面,筆者將在 Windows 10 系統下用 PowerShell 進行演示。

    小貼士:讀者進行復現時,要確保本機已安裝 Python 3,註意筆者已將預設的啟動路徑名 python 改為了 python3。

    單伺服器 VS 單客戶端
  • 在其中一個 PowerShell 中執行命令 python3 ./tcp_server.py,伺服器顯示 Waiting for connection...,並監聽本地主機的 TCP 6000 埠,進入等待連線狀態;

  • 在另一個 PowerShell 中執行命令 python3 ./tcp_client.py,伺服器顯示 Accept new connection from 127.0.0.1:42101,完成與本地主機的 TCP 42101 埠建立通訊連線,並向客戶端發送歡迎資訊與詢問資訊,客戶端接收到資訊後打印輸出;

  • 若客戶端向伺服器發送字串 Alice 與 Bob,則收到伺服器的問候響應資訊;

  • 若客戶端向伺服器發送空字串,則被要求重新輸入;

  • 若客戶端向伺服器發送字串 exit,則收到伺服器的結束響應資訊;

  • 客戶端與伺服器之間的通訊連線已關閉,伺服器顯示 Connection from 127.0.0.1:42101 is closed,並繼續監聽客戶端的連線請求。

  • 單伺服器 VS 多客戶端
  • 在其中一個 PowerShell 中執行命令 python3 ./tcp_server.py,伺服器顯示 Waiting for connection...,並監聽本地主機的 TCP 6000 埠,進入等待連線狀態;

  • 在另三個 PowerShell 中分別執行命令 python3 ./tcp_client.py,伺服器同時與本地主機的 TCP 42719、42721、42722 埠建立通訊連線,並分別向客戶端發送歡迎資訊與詢問資訊,客戶端接收到資訊後打印輸出;

  • 三台客戶端分別向伺服器發送字串 Client1、Client2、Client3,並收到伺服器的問候響應資訊;

  • 所有客戶端分別向伺服器發送字串 exit,並收到伺服器的結束響應資訊;

  • 所有客戶端與伺服器之間的通訊連線已關閉,伺服器繼續監聽客戶端的連線請求。

  • 七、Python API Reference

    socket 模組

    本節介紹上述程式碼中用到的內建模組 socket,是 Python 網路編程的核心模組。

    socket() 函式

    socket() 函式用於建立網路通訊中的套接字物件。函式原型如下:

    socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

    family 參數代表地址族(Address Family),預設值為 AF_INET,用於 IPv4 網路通訊,常用的還有 AF_INET6,用於 IPv6 網路通訊。family 參數的可選值取決於本機作業系統。
    type 參數代表套接字的型別,預設值為 SOCK_STREAM,用於 TCP 協定(面向連線)的網路通訊,常用的還有 SOCK_DGRAM,用於 UDP 協定(無連線)的網路通訊。
    proto 參數代表套接字的協定,預設值為 0,一般忽略該參數,除非 family 參數為 AF_CAN,則 proto 參數需設定為 CAN_RAW 或 CAN_BCM。
    fileno 參數代表套接字的檔描述符,預設值為 None,若設定了該參數,則其他三個參數將會被忽略。建立完套接字物件後,需使用物件的內建函式完成網路通訊過程。註意,以下函式原型中的「socket」是指 socket 物件,而不是上述的 socket 模組。

    bind() 函式

    bind() 函式用於向套接字物件繫結 IP 地址與埠號。註意,套接字物件必須未被繫結,並且埠號未被占用,否則會報錯。函式原型如下:

    socket.bind(address)

    address 參數代表套接字要繫結的地址,其格式取決於套接字的 family 參數。若 family 參數為 AF_INET,則 address 參數列示為二元組 (host, port),其中 host 是用字串表示的主機地址,port 是用整型表示的埠號。

    listen() 函式

    listen() 函式用於 TCP 伺服器開啟套接字的監聽功能。函式原型如下:

    socket.listen([backlog])

    backlog 可選參數代表套接字在拒絕新連線之前,作業系統可以掛起的最大連線數。backlog 參數一般設定為 5,若未設定,系統會為其自動設定一個合理的值。

    connect() 函式

    connect() 函式用於 TCP 客戶端向 TCP 伺服器發起連線請求。函式原型如下:

    socket.connect(address)

    address 參數代表套接字要連線的地址,其格式取決於套接字的 family 參數。若 family 參數為 AF_INET,則 address 參數列示為二元組 (host, port),其中 host 是用字串表示的主機地址,port 是用整型表示的埠號。

    accept() 函式

    accept() 函式用於 TCP 伺服器接受 TCP 客戶端的連線請求。函式原型如下:

    socket.accept()

    accept() 函式的返回值是二元組 (conn, address),其中 conn 是伺服器用來與客戶端互動數據的套接字物件,address 是客戶端的 IP 地址與埠號,用二元組 (host, port) 表示。

    send() 函式

    send() 函式用於向遠端套接字物件發送數據。註意,本機套接字必須與遠端套接字成功連線後才能使用該函式,否則會報錯。可見,send() 函式只能用於 TCP 行程間通訊,而對於 UDP 行程間通訊應該用 sendto() 函式。函式原型如下:

    socket.send(bytes[, flags])

    bytes 參數代表即將發送的 bytes 物件數據。例如,對於字串 "hello world!" 而言,需要用 encode() 函式轉換為 bytes 物件 b"hello world!" 才能進行網路傳輸。
    flags 可選參數用於設定 send() 函式的特殊功能,預設值為 0,也可由一個或多個預定義值組成,用位或操作符 | 隔開。詳情可參考 Unix 函式手冊中的 send(2),flags 參數的常見取值有 MSG_OOB、MSG_EOR 、MSG_DONTROUTE等。
    send() 函式的返回值是發送數據的字節數。

    recv() 函式

    recv() 函式用於從遠端套接字物件接收數據。註意,與 send() 函式不同,recv() 函式既可用於 TCP 行程間通訊,也能用於 UDP 行程間通訊。函式原型如下:

    socket.recv(bufsize[, flags])

    bufsize 參數代表套接字可接收數據的最大字節數。註意,為了使硬體裝置與網路傳輸更好地匹配,bufsize 參數的值最好設定為 2 的冪次方,例如 4096。
    flags 可選參數用於設定 recv() 函式的特殊功能,預設值為 0,也可由一個或多個預定義值組成,用位或操作符 | 隔開。詳情可參考 Unix 函式手冊中的 recv(2),flags 參數的常見取值有 MSG_OOB、MSG_PEEK、MSG_WAITALL 等。
    recv() 函式的返回值是接收到的 bytes 物件數據。例如,接收到 bytes 物件 b"hello world!",最好用 decode() 函式轉換為字串 "hello world!" 再打印輸出。

    close() 函式

    close() 函式用於關閉本地套接字物件,釋放與該套接字連線的所有資源。

    socket.close()

    threading 模組

    本節介紹上述程式碼中用到的內建模組 threading,是 Python 多執行緒的核心模組。

    Thread() 類

    Thread() 類可以建立執行緒物件,用於呼叫 start() 函式啟動新執行緒。類原型如下:

    class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

  • group 參數作為以後實作 ThreadGroup () 類的保留參數,目前預設值為 None。

  • target 參數代表執行緒被 run() 函式啟用後呼叫的函式,預設值為 None,即沒有任何函式會被呼叫。

  • name 參數代表執行緒名,預設值為 None,則系統會自動為其命名,格式為「Thread-N」,N 是從 1 開始的十進制數。

  • args 參數代表 target 參數指向函式的普通參數,用元組(tuple)表示,預設值為空元組 ()。

  • kwargs 參數代表 target 參數指向函式的關鍵字參數,用字典(dict)表示,預設值為空字典 {}。

  • daemon 參數用於標示行程是否為守護行程。若設定為 True,則標示為守護行程;若設定為 False,則標示為非守護行程;若設定為 None,則繼承當前父執行緒的 daemon 參數值。

  • 建立完執行緒物件後,需使用物件的內建函式控制多執行緒活動。

  • start() 函式

    start() 函式用於開啟執行緒活動。函式原型如下:

    Thread.start()

    註意,每個執行緒物件只能呼叫一次 start() 函式,否則會導致 RuntimeError 錯誤。

    八、總結

    本文介紹了 TCP 協定與 socket 編程的基礎知識,再用 Python 3 實作並演示了 TCP 伺服器與客戶端的通訊過程,其中還運用了簡單的多執行緒技術,最後將指令碼中涉及到的 Python API 做成了的參考索引,有助於理解實作過程。

    筆者水平有限,若文中出現不足或錯誤之處,還望大家不吝相告,多多包涵,歡迎讀者前來交流技術,感謝閱讀。

    更多精彩

    關註公眾號 浩道Linux

    浩道Linux ,專註於 Linux系統 的相關知識、 網路通訊 網路安全 Python相關 知識以及涵蓋IT行業相關技能的學習, 理論與實戰結合,真正讓你在學習工作中真正去用到所學。同時也會分享一些面試經驗,助你找到高薪offer,讓我們一起去學習,一起去進步,一起去漲薪!期待您的加入~~~ 關註回復「資料」可 免費獲取學習資料 (含有電子書籍、視訊等)。

    喜歡的話,記得 點「贊」 「在看」