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 22

Python socket(二):UDP 與協定設計

UDP、多個用戶端,自己設計簡單的協定
Interaction 25 minute(s)

Learning Objectives

  • 用 bind、recvfrom、sendto 寫一個 UDP 伺服器,說明一個 UDP socket 怎麼服務很多個寄件人
  • 寫一個依序服務多個用戶端的 TCP 伺服器,說明還沒輪到的連線在哪裡等
  • 設計一個一行一個指令、以 OK/ERROR 回應的簡單文字協定
  • 說明 TCP 沒有訊息邊界,用緩衝區加換行(或長度欄位)切出一則一則訊息
  • 用逾時與重送讓 UDP 程式在封包遺失時還能完成工作,並說明重送可能造成重複

從連得上到講得通

上一課用 TCP 寫了用戶端與伺服器:建立連線、送資料、收資料、關閉。這一課處理接下來一定會遇到的三個問題:

  1. UDP 怎麼寫伺服器? UDP 沒有連線,伺服器不需要 listen、accept,一個 socket 就能收所有人的資料(第 15 課講過 UDP 的特性)。
  2. 雙方怎麼知道一則訊息到哪裡結束? 兩支程式要溝通,除了連得上,還要約定訊息的格式,這份約定就是協定(protocol)。 HTTP、SMTP、Redis 都是這樣設計出來的,你也可以設計自己的。
  3. 封包遺失怎麼辦? TCP 會自動重傳;UDP 不會,要由你的程式決定等多久、要不要重送。
問題 這一課的做法
UDP 伺服器 bind 之後用 recvfrom 收、用 sendto 回覆給寄件人
多個用戶端 UDP:同一個 socket 依到達順序處理;TCP:accept 一個、服務完再 accept 下一個
訊息邊界 一行一則訊息,用緩衝區湊滿一行才處理
遺失 settimeout 設等待上限,逾時就重送,最多幾次

這一課的程式跑在模擬網路上:右側的時序圖畫出每個資料報與 TCP 區段,遺失的資料報畫成 ×。 模擬網路的延遲是示意值;真的網路上,延遲與遺失的情況會隨時間變化。

UDP 伺服器:bind、recvfrom、sendto

TCP 伺服器的流程是 bind → listen → accept 得到一條連線 → 在那條連線上 recv、send。 UDP 沒有連線,所以少了中間兩步:

TCP 伺服器 UDP 伺服器
建立 socket(AF_INET, SOCK_STREAM) socket(AF_INET, SOCK_DGRAM)
綁定位址與連接埠 bind bind
等待 listen、accept(每個用戶端一個新的 socket) 不需要
收 conn.recv(n):拿到一段位元組 recvfrom(n):拿到一整個資料報和寄件人位址
回覆 conn.sendall(資料) sendto(資料, 寄件人位址)

UDP 伺服器只有一個 socket,所有用戶端的資料報都排在同一個佇列,recvfrom 依到達的順序一個一個拿。 每次都要記下 recvfrom 回傳的位址,回覆時用 sendto 寄回去,因為 socket 本身不記得「對方是誰」。

recvfrom(1024) 的 1024 是這次最多收幾個位元組。UDP 保留訊息邊界:對方 sendto 一次,你 recvfrom 一次就拿到那一整個資料報, 不會黏在一起也不會被切開(資料報比 1024 長時,多出來的部分在多數系統上會被丟掉,所以上限要設得夠大)。

逐步看:一個 UDP socket 收兩個感測器的資料

兩個感測器 S1、S2 每 0.6 秒送一筆讀值到 PC1 的 9000 埠,兩個錯開 0.3 秒。看第 11 行:程式停在 recvfrom, 時間直接跳到下一個資料報到達的時刻;addr 每次都不同,伺服器靠它知道要回覆給誰。

TCP 伺服器依序服務多個用戶端

TCP 伺服器每次 accept 得到一條連線,在那條連線上收送,直到對方關閉(recv 回傳空的 b""),再回去 accept 下一個。 這是最簡單的寫法,叫做依序(iterative)伺服器:

srv.listen()
while True:
    conn, addr = srv.accept()      # 等下一個用戶端
    serve(conn)                    # 服務這個用戶端,直到對方關閉
    conn.close()

服務第一個用戶端時,第二個用戶端如果也來連線,作業系統會先替它完成三向交握,把連線放在 listen 的等待佇列(backlog)裡, 等程式下一次 accept 才拿出來。所以第二個用戶端不會被拒絕,只是要等。

這種寫法的缺點也很明顯:一個用戶端很慢(或一直不關閉),後面的人全部都要等。實務上會用執行緒(threading)、 selectors(同時等待很多個 socket)或 asyncio 同時服務多個連線;這一課的模擬網路只支援依序的寫法,觀念上先把一個連線處理好。

設計一個文字協定,以及 TCP 的訊息邊界

設計協定時要回答幾個問題:一則訊息長什麼樣子、怎麼知道它結束了、成功和失敗各回什麼。 最容易除錯的是文字協定:一行一個指令,一行一個回應,用肉眼就看得懂(SMTP、Redis 的指令都長這樣)。

這一課的範例服務(模擬網路裡的 SRV,6000 埠)用這份約定:

用戶端送出 伺服器回應 說明
SET 鍵 值 OK 存一筆資料
GET 鍵 VALUE 值 或 NOT_FOUND 讀一筆資料
QUIT BYE,然後關閉連線 結束
其他 ERROR unknown command 不認得的指令

每一則訊息都以換行 \n 結束,這就是訊息邊界。為什麼一定要約定邊界?因為 TCP 是位元組流(byte stream):

  • 對方 sendall 三次,你可能一次 recv 就全部拿到(黏在一起)
  • 對方 sendall 一次,你也可能要 recv 好幾次才拿完(被切開)

TCP 只保證位元組的順序正確、不遺失,不保證「一次送」對應「一次收」。所以接收端要準備一個緩衝區: 收到的位元組先接在後面,裡面出現完整的一行才拿出來處理,剩下的半行留著等下一次 recv。

切訊息常見的兩種方法:

方法 做法 適合
分隔字元 每則訊息以 \n 結束 文字協定;內容本身不能含換行
長度欄位 先送固定 4 個位元組表示長度,再送內容 二進位資料、內容可能含任何位元組

UDP 沒有這個問題:一個資料報就是一則訊息,但一則訊息不能超過一個資料報的大小。

逐步看:三個指令的回應黏在一起

用戶端一次送出三行指令。看第 7 行的 recv(1024):伺服器分三次回應,用戶端卻一次就收到三行;程式要自己用換行切開。

逐步看:用緩衝區處理被切開的訊息

自己寫伺服器,實作一個只有 ADD 的小協定。C1 先連進來,第二個指令 ADD 10 20 被切成兩段送到; C1 關閉之後,C2 才被 accept,它的兩行指令卻在同一段裡。看 buf 的內容怎麼變化。

互動練習:完成計算機協定的伺服器

協定的約定:ADD a b 回 OK 和、MUL a b 回 OK 積、其他回 ERROR;每則訊息以換行結束。 三個用戶端依序連進來:C1 送 ADD 2 3;C2 在同一段裡送 ADD 1 1 和 MUL 4 5 兩行;C3 把 MUL 6 7 切成 MU、L 6 7 兩段送,最後再送 HELLO。 起始程式假設「一次 recv 就是一個指令」。依序完成任務,每次改完按執行,看時序圖上 PC1 回了什麼。

小測驗:UDP 伺服器與訊息邊界

1.

UDP 伺服器回覆用戶端時,為什麼要用 sendto(資料, addr),而不是像 TCP 那樣直接 send(資料)?

2.

下面模擬接收端緩衝區的程式會印出什麼?

buf = b""
for chunk in [b"SET a 1\nGE", b"T a\n"]:
    buf += chunk
    while b"\n" in buf:
        line, buf = buf.split(b"\n", 1)
        print(line.decode())
print(len(buf))
3.

依序(iterative)的 TCP 伺服器正在服務 C1 時,C2 也來連線。C2 會怎樣?

第 1 次作答

UDP 的逾時與重送

UDP 送出就不管了:資料報在路上遺失,送的一方不會收到任何通知;recvfrom 沒有設逾時,就會一直等下去。 UDP 的規格 RFC 768 寫明它不保證送達、也不防止重複。所以用 UDP 做「一問一答」(例如 DNS 查詢)時,程式要自己負責三件事:

  1. 設等待上限:sock.settimeout(0.5),recvfrom 超過 0.5 秒沒收到就丟出 TimeoutError(舊名稱 socket.timeout)。
  2. 重送:接住 TimeoutError,再送一次。
  3. 設重送次數上限:網路真的斷了,不能無限重送;試了幾次都失敗就回報錯誤。
sock.settimeout(0.5)
for attempt in range(3):
    sock.sendto(request, server)
    try:
        data, addr = sock.recvfrom(1024)
        break                        # 收到了
    except TimeoutError:
        pass                         # 逾時,下一圈重送
else:
    print("試了 3 次都失敗")         # for 迴圈沒有 break 才會執行 else

逾時要設多長?太短會在回覆還在路上時就重送,太長則遺失時要等很久。實務上常依量到的來回時間調整, 並且每次失敗把等待時間拉長(例如 0.5、1、2 秒),避免網路壅塞時大家一起重送讓情況更糟。

逐步看:第一個資料報遺失,逾時後重送

模擬網路設定讓 PC1 送出的第 1 個 UDP 資料報在路上遺失。看時序圖上的 ×,以及第 12 行等了 0.5 秒之後跳到 except。

互動練習:加上逾時與重送

這次模擬網路讓 PC1 送出的前兩個資料報都遺失。起始程式只送一次,而且 recvfrom 沒有逾時, 執行後會一直等到模擬時間結束。依序完成任務:

逐步看:遺失的是回覆,重送讓伺服器做了兩次

這次請求順利到達,遺失的是伺服器的第 1 個回覆。用戶端分不出是哪一個遺失,只能照樣重送; 伺服器收到兩次「點數加 100」,就加了兩次。看時序圖上兩個往右的 INCR 與那一個 ×。

小測驗:逾時、重送與協定設計

1.

下面模擬重送的程式會印出什麼?(lost 表示第幾次送出會遺失)

lost = {1, 2}
for attempt in range(1, 4):
    if attempt in lost:
        print("timeout")
        continue
    print("ok", attempt)
    break
else:
    print("give up")
2.

協定要傳送一張圖片(任何位元組都可能出現),用哪一種方式標出訊息邊界比較適合?

3.

用戶端用 UDP 查詢,settimeout(0.5),最多送 3 次。伺服器已經關機,這個查詢最久大約多久會放棄?

第 1 次作答

重點整理

  • UDP 伺服器:bind 之後 recvfrom 拿到「資料+寄件人位址」,用 sendto 回覆;一個 socket 依到達順序服務所有人。
  • 依序的 TCP 伺服器:accept 一個、服務到對方關閉、再 accept 下一個;還沒輪到的連線在 listen 的等待佇列裡。
  • 協定要約定訊息格式、邊界與回應;文字協定一行一則,成功回 OK、失敗回 ERROR,容易除錯。
  • TCP 沒有訊息邊界:用緩衝區累積,湊滿一行(或收滿長度欄位指定的位元組)才處理。UDP 一個資料報就是一則。
  • UDP 的一問一答要自己做 settimeout、重送與次數上限;重送可能讓伺服器收到重複的請求(例如回覆遺失時),協定要能處理:讓請求做幾次結果都一樣,或帶編號讓伺服器認出重複。

這一課的伺服器沒有任何存取控制,只要連得到就能用。下一課講網路安全:防火牆與 ACL 怎麼只讓需要的連線通過。

這一課完成了!
Next lesson:網路安全:防火牆、ACL 與 VPN
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。