Python 絕技 —— TCP服務器與客戶端

i春秋做家:wasrehpicphp

0×00 前言

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

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

本文做爲「Python 絕技」系列工具文章的開篇,先介紹因特網的核心協議——TCP 協議,再以 Python 的 socket 模塊爲例介紹網絡套接字,最後給出 TCP 服務器與客戶端的 Python 腳本,並演示二者之間的通訊過程。linux

0×01 TCP 協議

TCP(Transmission Control Protocol,傳輸控制協議)是一種面向鏈接、可靠的、基於字節流的傳輸層通訊協議。編程

TCP 協議的運行分爲鏈接建立(Connection Establishment)、數據傳送(Data Transfer)和鏈接終止(Connection Termination)三個階段,其中「鏈接建立」階段是耳熟能詳的 TCP 協議三次握手(TCP Three-way Handshake),也是理解本文 TCP 服務器與客戶端通訊過程的階段。安全

鏈接建立(Connection Establishment)

所謂的「三次握手」,即 TCP 服務器與客戶端成功創建通訊鏈接必經的三個步驟,共需經過三個報文完成。服務器

Handshake Step 1

客戶端向服務器發送 SYN 報文(SYN = 1)請求鏈接。此時報文的初始序列號爲 seq = x,確認號爲 ack = 0。網絡

Handshake Step 2

服務器接收到客戶端的 SYN 報文後,發送 ACK + SYN 報文(ACK = 1,SYN = 1)確認客戶端的鏈接請求,並也向其發起鏈接請求。此時報文的序列號爲 seq = y,確認號爲 ack = x + 1。多線程

Handshake Step 3

客戶端接收到服務器的 SYN 報文後,發送 ACK 報文(ACK = 1)確認服務器的鏈接請求。此時報文的序列號爲 seq = x + 1,確認號爲 ack = y + 1。socket

對於上述過程的理解,須要注意如下幾點:

  • 報文的功能在 TCP 協議頭的標記符(Flags)區段中定義,該區段位於第 104~111 比特位,共佔 8 比特,每一個比特位對應一種功能,置 1 表明開啓,置 0 表明關閉。例如,SYN 報文的標記符爲 00000010,ACK + SYN 報文的標記符爲 00010010
  • 報文的序列號在 TCP 協議頭的序列號(Sequence Number)區段中定義,該區段位於第 32~63 比特位,共佔 32 比特。在「三次握手」過程當中,初始序列號 seq 由數據發送方隨機生成。
  • 報文的確認號在 TCP 協議頭的確認號(Acknowledgement Number)區段中定義,該區段位於第 64~95 比特位,共佔 32 比特。在「三次握手」過程當中,確認號 ack 爲前序接收報文的序列號加 1。

爲了更方便地理解,下面給出一張 TCP 協議三次握手的示意圖:

image.png

0×02 Network Socket

Network Socket(網絡套接字)是計算機網絡中進程間通訊的數據流端點,廣義上也表明操做系統提供的一種進程間通訊機制。

進程間通訊(Inter-Process Communication,IPC)的根本前提是可以惟一標示每一個進程。在本地主機的進程間通訊中,能夠用 PID(進程 ID)惟一標示每一個進程,但 PID 只在本地惟一,在網絡中不一樣主機的 PID 則可能發生衝突,所以採用「IP 地址 + 傳輸層協議 + 端口號」的方式惟一標示網絡中的一個進程。

小貼士:網絡層的 IP 地址能夠惟一標示主機,傳輸層的 TCP/UDP 協議和端口號能夠惟一標示該主機的一個進程。注意,同一主機中 TCP 協議與 UDP 協議的可使用相同的端口號。

全部支持網絡通訊的編程語言都各自提供了一套 socket API,下面以 Python 3 爲例,講解服務器與客戶端創建 TCP 通訊鏈接的交互過程:

image.png

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

0×03 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).decode()                 if data == "exit":                         conn.send(b"Good bye!\n")                         break                 conn.send(b"Hello %s!\n" % data.encode())         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:鏈接成功後,向客戶端發送問候信息 "Welcome!\n"
  • Line 9:進入與客戶端交互數據的循環階段。
  • Line 10:向客戶端發送詢問信息 "What's your name?"
  • Line 11:接收客戶端發來的非空字符串。
  • Line 12:若是非空字符串爲 "exit",則向客戶端發送結束信息 "Good bye!\n",並結束與客戶端交互數據的循環階段。
  • Line 15:若是非空字符串不爲 "exit",則向客戶端發送問候信息 "Hello %s!\n",其中 %s 是客戶端發來的非空字符串。
  • 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:開啓新線程的活動。

0×04 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:鏈接成功後,接收服務器發送過來的問候信息 "Welcome!\n"
  • Line 9:建立一個非空字符串變量 data,並賦初值爲 "client"(只要是非空字符串便可),用於判斷是否接收來自服務器發來的詢問信息 "What's your name?"
  • Line 10:進入與服務器交互數據的循環階段。
  • Line 12:當用戶的輸入非空且不等於 "exit"(記爲非法字符串)時,則接收服務器發來的詢問信息。
  • Line 13:要求用戶輸入名字,一條合法字符串便可。
  • Line 14:當用戶輸入非空,則從新開始循環,要求用戶從新輸入合法字符串。
  • Line 16:當用戶輸入合法字符串時,則將字符串轉換爲 bytes 對象後發送至服務器。
  • Line 17:接收服務器的響應數據,並將 bytes 對象轉換爲字符串後打印輸出。
  • Line 18:當用戶輸入字符串 "exit" 時,則結束與服務器交互數據的循環階段,即將關閉套接字。
  • Line 21:關閉套接字,再也不向服務器發送數據。

0×05 TCP 進程間通訊

將 TCP 服務器與客戶端的腳本分別命名爲 tcp_server.py 與 tcp_client.py,而後存至桌面,筆者將在 Windows 10 系統下用 PowerShell 進行演示。

小貼士:讀者進行復現時,要確保本機已安裝 Python 3,注意筆者已將默認的啓動路徑名 python 改成了 python3

單服務器 VS 單客戶端

image.png

  1. 在其中一個 PowerShell 中運行命令 python3 ./tcp_server.py,服務器顯示 Waiting for connection...,並監聽本地主機的 TCP 6000 端口,進入等待鏈接狀態;
  2. 在另外一個 PowerShell 中運行命令 python3 ./tcp_client.py,服務器顯示 Accept new connection from 127.0.0.1:42101,完成與本地主機的 TCP 42101 端口創建通訊鏈接,並向客戶端發送問候信息與詢問信息,客戶端接收到信息後打印輸出;
  3. 若客戶端向服務器發送字符串 Alice 與 Bob,則收到服務器的問候響應信息;
  4. 若客戶端向服務器發送空字符串,則要求從新輸入字符串;
  5. 若客戶端向服務器發送字符串 exit,則收到服務器的結束響應信息;
  6. 客戶端與服務器之間的通訊鏈接已關閉,服務器顯示 Connection from 127.0.0.1:42101 is closed,並繼續監聽客戶端的鏈接請求。

單服務器 VS 多客戶端

image.png

  1. 在其中一個 PowerShell 中運行命令 python3 ./tcp_server.py,服務器顯示 Waiting for connection...,並監聽本地主機的 TCP 6000 端口,進入等待鏈接狀態;
  2. 在另三個 PowerShell 中分別運行命令 python3 ./tcp_client.py,服務器同時與本地主機的 TCP 4271九、4272一、42722 端口創建通訊鏈接,並分別向客戶端發送問候信息與詢問信息,客戶端接收到信息後打印輸出;
  3. 三臺客戶端分別向服務器發送字符串 Client1Client2Client3,並收到服務器的問候響應信息;
  4. 全部客戶端分別向服務器發送字符串 exit,並收到服務器的結束響應信息;
  5. 全部客戶端與服務器之間的通訊鏈接已關閉,服務器繼續監聽客戶端的鏈接請求。

0×06 Python API Reference

socket 模塊

此小節介紹上述代碼中用到的 socket 模塊內置函數,也是 socket 編程的核心函數。

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 編程的常見函數。注意,如下函數原型中的「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_PEEK、MSG_WAITALL 等。

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 參數值。

threading 對象

此小節介紹上述代碼中用到的 threading 對象內置函數,也是多線程編程的必用函數。注意,如下函數原型中的「threading」是指 threading 對象,而不是 threading 模塊。

start() 函數

start() 函數用於開啓線程活動。函數原型以下:

threading.start()

注意,每一個線程對象只能調用一次 start() 函數,不然會致使 RuntimeError 錯誤。

0×07 總結

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

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

本文的相關參考請移步至:

簡單理解Socket
TCP編程 – 廖雪峯的官方網站
多線程 – 廖雪峯的官方網站

有問題你們能夠留言哦,也歡迎你們到春秋論壇中來耍一耍  >>>點擊跳轉

相關文章
相關標籤/搜索