National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 21

Python socket(一):TCP

用 Python 寫 TCP 用戶端與伺服器
Interaction 25 minute(s)

Learning Objectives

  • 說明 socket 是程式使用作業系統網路功能的介面,並寫出 TCP 用戶端的 connect、sendall、recv、close
  • 在 str 與 bytes 之間正確轉換(encode、decode),並說明為什麼網路上傳的是位元組
  • 說明 TCP 是位元組流,recv 不一定一次收完,用迴圈讀到對方關閉或讀到換行
  • 處理連線被拒絕、逾時與查不到名稱三種常見的例外
  • 寫出只處理一個用戶端的 TCP 伺服器:bind、listen、accept、recv 迴圈

程式怎麼用網路

前面幾課從外面看網路:封包怎麼走、TCP 怎麼建立連線。這一課換個角度,從程式來用網路。 瀏覽器、聊天軟體、遊戲,底層都是同一件事:程式請作業系統開一條連線,把位元組送出去、收回來。

程式和作業系統網路功能之間的介面叫 socket。你可以把它想成一支電話:

動作 電話的比喻 Python
建立 socket 拿起一支電話 socket.socket(AF_INET, SOCK_STREAM)
連線 撥號,對方接起來(三向交握) s.connect((位址, 埠))
送出 說話 s.sendall(資料)
接收 聽對方說 s.recv(最多幾個位元組)
關閉 掛電話(送出 FIN) s.close()

AF_INET 表示 IPv4 位址,SOCK_STREAM 表示 TCP(下一課的 UDP 是 SOCK_DGRAM)。 連線對象寫成 (位址, 埠) 這樣的元組(tuple),例如 ("192.168.1.20", 5000)。

這一課的範例都在模擬網路上執行:你的程式跑在 PC1(192.168.1.10),伺服器 SRV 在 192.168.1.20。 每執行一步,畫面會畫出連線的時序圖(SYN、SYN-ACK、ACK、資料、FIN)。 模擬網路用虛擬時間,延遲是示意值;程式碼的寫法和在真的電腦上執行相同。 計算機概論軌第 30 課〈協定分層與 TCP/IP〉介紹過 TCP 的角色,這一課直接動手寫。

逐步看:第一個 TCP 用戶端

伺服器的 5000 埠是一個「回音」服務:收到什麼就原樣送回。注意兩件事: connect 那一步,時序圖出現三向交握;encode 之後,9 個字元變成 15 個位元組。

str 與 bytes,以及位元組流

socket 只認得 bytes(位元組序列),不認得 Python 的 str。直接 s.sendall("hello") 會得到 TypeError: a bytes-like object is required, not 'str'。轉換的方向要記清楚:

方向 寫法 範例
str → bytes(送出前) 文字.encode("utf-8") "你好".encode("utf-8") 是 6 個位元組
bytes → str(收到後) 資料.decode("utf-8") b"OK\n".decode("utf-8") 是 "OK\n"
直接寫 bytes 字面值 b"..." b"GET club\n"(只能放 ASCII 字元)

兩端要用同一種編碼,事先講好;現在通常是 UTF-8。HTTP 的標頭規定用 ASCII,內容的編碼則寫在標頭裡(charset=utf-8)。

還有一件更重要的事:TCP 送的是一條位元組流(byte stream),不是一則一則的訊息。 你 sendall 兩次,對方可能一次 recv 就收到兩次的內容;你 sendall 一大段,對方也可能要 recv 好幾次才收完。 TCP 只保證位元組的順序和完整,不保留送出時的分段。所以讀資料要用迴圈,讀到下面其中一種情況為止:

  1. 對方關閉連線:recv 回傳空的 b""。加了 Connection: close 的 HTTP 回應就是這樣結束的。
  2. 讀到約定好的結尾:例如每則訊息以換行 \n 結尾,讀到換行才算一則。
  3. 讀到約定好的長度:例如 HTTP 的 Content-Length 標頭事先說好內容有幾個位元組。

逐步看:recv 迴圈讀到對方關閉

用 socket 自己送出一個 HTTP 請求。伺服器的回應有 144 個位元組,送完就關閉連線。 這個程式故意每次只拿 64 個位元組,看迴圈跑了幾次、最後怎麼知道「收完了」。with 區塊結束時會自動 close。

小測驗:bytes 與位元組流

1.

下面的程式會印出什麼?

2.

下面的程式模擬「收到兩段資料、接起來再切行」,會印出什麼?

3.

用戶端連續呼叫 sendall(b"A\n") 和 sendall(b"B\n")。伺服器呼叫一次 recv(1024),可能拿到什麼?

4.

recv 回傳空的 b"" 代表什麼?

第 1 次作答

逐步看:連線被拒絕、逾時與查不到名稱

網路程式一定會遇到失敗。這個程式依序連四個目標,各自示範一種結果。看時序圖上每一種結果長什麼樣子: 正常的一來一往、收到 RST、送出後等不到回應,以及連一個封包都還沒送出就失敗。

三種常見的失敗

例外 時序圖上看到 代表什麼 常見原因
ConnectionRefusedError SYN 送出,對方回 RST 對方主機在,但那個埠沒有程式在聽 伺服器程式沒啟動、埠號寫錯
TimeoutError(socket.timeout) 送出後等不到回應 在設定的時間內沒有收到東西 對方沒有回應、防火牆把封包丟掉、主機不存在
socket.gaierror 沒有任何封包 名稱查不到 IP 名稱打錯、DNS 設定有問題(第 18 課)

這三個例外都是 OSError 的子類別。另外有一個 connect_ex:失敗時不丟例外,而是回傳錯誤代碼(0 表示成功),適合「先試試看連不連得上」的檢查程式。寫程式時的習慣:

  • 一定要設逾時:socket.create_connection((位址, 埠), timeout=秒數) 或 s.settimeout(秒數)。沒有設定時,recv 會無限期等待,程式看起來像當掉。
  • 用 with 或 try … finally 管理 socket:不管成功或丟出例外,都會關閉,不會留下沒關的連線。
  • 先接住特定的例外,再接一般的 OSError:錯誤訊息才能告訴使用者該怎麼處理(檢查埠號?檢查網路?檢查名稱?)。

互動練習:和 key-value 伺服器對話

伺服器 192.168.1.20 的 6000 埠是一個簡單的 key-value 服務,一行一個指令: SET 鍵 值 回 OK、GET 鍵 回 VALUE 值 或 NOT_FOUND、QUIT 回 BYE 並關閉連線。每個回覆也以換行結尾。

起始程式已經寫好 read_line:緩衝區裡還沒有換行就繼續 recv,有換行就切出一行,剩下的留在緩衝區給下次用。 按「模擬執行」看結果,再依任務改程式。

TCP 伺服器:bind、listen、accept

用戶端主動撥號;伺服器則是先在某個埠等著,有人打來才接。伺服器多了三個步驟:

步驟 寫法 做什麼
綁定 srv.bind(("0.0.0.0", 8000)) 佔用本機的 8000 埠;0.0.0.0 表示所有介面都收
監聽 srv.listen() 開始接受連線;作業系統替你完成三向交握,排進佇列
接受 conn, addr = srv.accept() 從佇列拿出一條連線;沒有人連進來就一直等

accept 回傳一個新的 socket conn,專門和這一個用戶端通訊;原本的 srv 繼續負責等下一個人。 之後用 conn.recv、conn.sendall 收送,和用戶端的寫法一樣。

setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 讓伺服器程式重新啟動時可以立刻再綁同一個埠。 沒有它,剛關閉的連線還在 TIME_WAIT 狀態(第 16 課)時,bind 可能失敗並出現「Address already in use」。

這一課的伺服器一次只處理一個用戶端;處理多個用戶端的寫法在下一課。

逐步看:處理一個用戶端的伺服器

你的程式跑在 PC1,當伺服器。模擬網路裡有一個用戶端 C1(192.168.1.31),0.3 秒時連進來, 隔 0.2 秒送一行,送完兩行就關閉。注意 accept 那一步,時間直接跳到有人連進來的時候。

互動練習:加法伺服器

你的程式當伺服器(8000 埠)。用戶端 C1 會送三行,每行兩個整數,第二次送出時兩行黏在一起: 先送 3 4\n,0.3 秒後送 10 20\n5 5\n。每收到一行,就回覆兩數的和加換行(7\n、30\n、10\n)。

提示:照用戶端的做法準備一個緩衝區 buf,每次 recv 到的資料接到後面,再用 while b"\n" in buf: 一行一行切出來處理。

小測驗:例外與伺服器

1.

用戶端連到 192.168.1.20 的 5001 埠,時序圖顯示送出 SYN 後立刻收到 RST。Python 會丟出哪個例外?

2.

伺服器的 conn, addr = srv.accept() 回傳的 conn 是什麼?

3.

下面的程式會印出什麼?(模擬一個「讀到換行為止」的緩衝區)

第 1 次作答

重點整理

  • socket 是程式使用網路的介面。TCP 用戶端:socket(AF_INET, SOCK_STREAM) → connect → sendall → recv → close;用 with 確保關閉。
  • socket 收送的是 bytes:送出前 encode("utf-8"),收到後 decode("utf-8"),兩端要用同一種編碼。
  • TCP 是位元組流,不保留送出時的分段:recv 可能只拿到一部分,也可能一次拿到好幾則。用迴圈讀到對方關閉(b"")、讀到換行,或讀到約定的長度。
  • 三種常見失敗:ConnectionRefusedError(RST,沒有程式在聽)、TimeoutError(等不到回應,一定要設逾時)、socket.gaierror(名稱查不到)。
  • TCP 伺服器:bind → listen → accept;accept 回傳一個新的 socket 專門服務這個用戶端。

下一課用 UDP 寫程式,並同時服務多個用戶端,最後自己設計一個簡單的協定:訊息怎麼分段、錯誤怎麼回報。

這一課完成了!
Next lesson:Python socket(二):UDP 與協定設計
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。