大一網際網路應用|Python TCP 分組網路實作:1 位組長收集 5 位組員的 IP、MAC、學號與姓名
在「網際網路應用」課程中,我們會學到 IP Address、MAC Address、Port、Socket、TCP、Client、Server 等重要觀念。
如果只背誦這些名詞,很難真正理解一筆資料究竟如何從一台電腦傳送到另一台電腦。 因此這次我們直接使用 Python, 讓學生自己建立一個小型的 TCP Client / Server 網路。
每組由一位學生擔任組長, 組長的電腦擔任 TCP Server; 另外 5 位學生擔任組員, 組員的電腦擔任 TCP Client。
每位組員輸入自己的學號與姓名後, Python 自動取得本機的 IP 與 MAC Address, 將資料轉換成 JSON, 再透過 TCP 傳送到組長電腦。
如何讓 5 位組員先與組長建立 TCP 連線, 再將自己的學號、姓名、IP 與 MAC 傳送給組長?
一、學習目標
- 理解 TCP Client / Server 架構。
- 了解 IP Address、MAC Address 與 Port。
- 理解 Socket 在網路程式中的角色。
- 理解 TCP 是 Connection-oriented Protocol。
- 了解 TCP Server 的 bind、listen、accept。
- 了解 TCP Client 的 connect。
- 使用 sendall() 與 recv() 傳送資料。
- 使用 JSON 交換結構化資料。
- 完成「1 位組長 + 5 位組員」TCP 網路實驗。
二、分組方式
每組共有 6 位學生:
| 角色 | 人數 | 程式 | 網路角色 |
|---|---|---|---|
| 組長 | 1 | tcp_leader.py | TCP Server |
| 組員 | 5 | tcp_node.py | TCP Client |
三、網路架構
組員1 Client ─┐
組員2 Client ─┤
組員3 Client ─┤
組員4 Client ─┤── TCP ──→ 組長 Server
組員5 Client ─┘ Port 5005
│
↓
建立本組節點資料表
例如組長 IP:
192.168.1.100
組員可能分別為:
組員1:192.168.1.101 組員2:192.168.1.102 組員3:192.168.1.103 組員4:192.168.1.104 組員5:192.168.1.105
這些只是範例,實際 IP 由教室的網路環境決定。
四、TCP 最大的特色:先建立連線
TCP 的全名是:
Transmission Control Protocol
TCP 是連線導向(Connection-oriented)的傳輸層協定。
因此組員不能直接把資料丟給組長,而是先:
↓
找到 Port 5005
↓
建立 TCP Connection
↓
傳送 JSON 資料
↓
組長接收
↓
組長回覆
↓
關閉 Connection
五、TCP 與 UDP 有什麼不同?
| 比較 | TCP | UDP |
|---|---|---|
| 連線 | 需要建立連線 | 不需建立連線 |
| 可靠傳輸 | 提供可靠、有序的位元組流 | 本身不保證可靠送達 |
| 資料形式 | Byte Stream | Datagram |
| 常見 Socket | SOCK_STREAM | SOCK_DGRAM |
初學時可以先這樣理解:
UDP:比較像直接寄出一張明信片。
六、TCP Socket
建立 TCP Socket:
sock = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
)
其中:
AF_INET:使用 IPv4。SOCK_STREAM:建立 TCP Socket。
這裡就是與上一個 UDP 實驗非常重要的差異:
UDP socket.SOCK_DGRAM TCP socket.SOCK_STREAM
七、TCP Server 的四個重要步驟
組長端有四個非常重要的 Socket 操作:
↓
listen()
↓
accept()
↓
recv()
1. bind()
server.bind(("0.0.0.0", 5005))
讓 Server 使用本機的 TCP Port 5005。
2. listen()
server.listen(5)
讓 Socket 進入監聽狀態,準備接受連線。 參數代表等待連線佇列的 backlog 設定,並不是「最多只能服務 5 位學生」。
3. accept()
client_socket, address = server.accept()
等待一位組員建立 TCP 連線。
4. recv()
data = client_socket.recv(4096)
接收該組員送來的資料。
八、組長端 TCP Server 完整程式
組長儲存為:
tcp_leader.py
import socket
import json
SERVER_IP = "0.0.0.0"
SERVER_PORT = 5005
# 建立 TCP Socket
server = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
)
# 允許程式重新啟動後較快重新使用 Port
server.setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1
)
# 綁定 IP 與 Port
server.bind(
(SERVER_IP, SERVER_PORT)
)
# 開始監聽
server.listen(5)
students = {}
print("=" * 60)
print("TCP 小組節點資料收集系統")
print("本電腦角色:組長 TCP Server")
print(f"等待 TCP Port {SERVER_PORT}...")
print("=" * 60)
while len(students) < 5:
print("\n等待組員連線...")
# 接受 TCP Connection
client_socket, address = server.accept()
print(
f"收到 TCP 連線:"
f"{address[0]}:{address[1]}"
)
try:
# 接收資料
data = client_socket.recv(4096)
if not data:
client_socket.close()
continue
# JSON → Python Dictionary
student = json.loads(
data.decode("utf-8")
)
student_id = student["student_id"]
name = student["name"]
ip = student["ip"]
mac = student["mac"]
students[student_id] = {
"name": name,
"ip": ip,
"mac": mac,
"source_ip": address[0]
}
print()
print(
f"目前收到 "
f"{len(students)} / 5 位組員"
)
print("-" * 50)
print(f"學號:{student_id}")
print(f"姓名:{name}")
print(
f"組員回報 IP:{ip}"
)
print(
f"TCP 實際來源 IP:"
f"{address[0]}"
)
print(f"MAC:{mac}")
print(
f"來源 Port:"
f"{address[1]}"
)
print("-" * 50)
# 回覆組員
message = (
f"組長已收到 "
f"{student_id} {name} 的資料"
)
client_socket.sendall(
message.encode("utf-8")
)
except Exception as e:
print(
"處理資料時發生錯誤:",
e
)
finally:
# 關閉這一位組員的 Connection
client_socket.close()
print()
print("=" * 90)
print("本組 5 位組員資料收集完成")
print("=" * 90)
print(
f"{'學號':<15}"
f"{'姓名':<12}"
f"{'回報IP':<18}"
f"{'來源IP':<18}"
f"{'MAC'}"
)
print("-" * 90)
for student_id, info in students.items():
print(
f"{student_id:<15}"
f"{info['name']:<12}"
f"{info['ip']:<18}"
f"{info['source_ip']:<18}"
f"{info['mac']}"
)
server.close()
九、組員端 TCP Client 完整程式
5 位組員都使用相同程式:
tcp_node.py
import socket
import uuid
import json
# =========================================
# 修改成「組長電腦」的 IP
# =========================================
LEADER_IP = "192.168.1.100"
SERVER_PORT = 5005
# =========================================
# 取得本機 IP
# =========================================
def get_ip():
s = socket.socket(
socket.AF_INET,
socket.SOCK_DGRAM
)
try:
# 此處不是真的傳資料給 8.8.8.8
# 主要用來判斷系統會選用哪個本機 IP
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
except:
ip = "127.0.0.1"
finally:
s.close()
return ip
# =========================================
# 取得 MAC Address
# =========================================
def get_mac():
mac = uuid.getnode()
return ":".join(
f"{(mac >> i) & 0xff:02X}"
for i in range(40, -1, -8)
)
# =========================================
# 主程式
# =========================================
print("=" * 50)
print("TCP 小組節點資料傳送程式")
print("本電腦角色:組員 TCP Client")
print("=" * 50)
student_id = input("請輸入學號:")
name = input("請輸入姓名:")
ip = get_ip()
mac = get_mac()
student = {
"student_id": student_id,
"name": name,
"ip": ip,
"mac": mac
}
print()
print("準備傳送資料")
print("-" * 40)
print("學號:", student_id)
print("姓名:", name)
print("IP:", ip)
print("MAC:", mac)
print("-" * 40)
print(
"組長 Server:",
LEADER_IP
)
# =========================================
# 建立 TCP Socket
# =========================================
sock = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
)
try:
# =====================================
# 建立 TCP Connection
# =====================================
print("\n正在連線到組長...")
sock.connect(
(LEADER_IP, SERVER_PORT)
)
print("TCP 連線成功!")
# =====================================
# Dictionary → JSON → Bytes
# =====================================
data = json.dumps(
student,
ensure_ascii=False
).encode("utf-8")
# =====================================
# 傳送資料
# =====================================
sock.sendall(data)
print(
"資料已透過 TCP "
"傳送給組長!"
)
# =====================================
# 等待組長回覆
# =====================================
reply = sock.recv(4096)
print(
"\n組長回覆:",
reply.decode("utf-8")
)
except ConnectionRefusedError:
print(
"\n連線失敗!"
"請確認組長程式是否已啟動。"
)
except Exception as e:
print(
"\n發生錯誤:",
e
)
finally:
sock.close()
print(
"\nTCP Connection 已關閉"
)
十、注意:get_ip() 裡為什麼還看到 SOCK_DGRAM?
有同學可能會發現:
明明這一課改成 TCP,為什麼 get_ip() 裡面還有:
socket.SOCK_DGRAM
這裡的 UDP Socket 不是本次學生資料傳輸使用的通訊協定。 它只是利用作業系統的路由選擇,協助程式判斷目前會使用哪個本機 IP。
真正將學生資料傳給組長的 Socket 是:
sock = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM
)
所以本實驗的主要資料傳輸仍然是 TCP。
十一、組員最重要的一行:connect()
UDP 版本沒有真正的 TCP Connection, 但 TCP Client 必須先:
sock.connect(
(LEADER_IP, SERVER_PORT)
)
可以把它理解為:
組員知道:
組長 IP
192.168.1.100
+
TCP Port
5005
↓
192.168.1.100:5005
↓
建立 TCP Connection
十二、TCP 三向交握
在組員的 connect() 與組長的 accept()
背後,TCP 會進行建立連線的程序。
最經典的就是 Three-Way Handshake(三向交握)。
組員 Client 組長 Server
│ │
│ ----------- SYN --------------→ │
│ │
│ ←-------- SYN + ACK ------------ │
│ │
│ ----------- ACK --------------→ │
│ │
│ TCP Connection 建立 │
│ │
完成連線後,應用程式才開始交換學生資料。
十三、sendall() 做什麼?
組員使用:
sock.sendall(data)
把 JSON 編碼後的 Bytes 寫入 TCP 連線。
組長則使用:
data = client_socket.recv(4096)
從 TCP 位元組流讀取資料。
本實驗的 JSON 很短,因此用單次 recv(4096)
即可作為入門示範。
但要特別知道:
TCP 是 Byte Stream,不保留應用程式訊息邊界。
正式系統若要傳送較長或連續多筆資料,通常要自行設計長度欄位、分隔符號或其他 framing 機制。
十四、完整操作流程
↓
② 選出 1 位組長
↓
③ 組長查詢自己的 IP
↓
④ 告訴其他 5 位組員
↓
⑤ 組長啟動 tcp_leader.py
↓
⑥ Server bind()
↓
⑦ Server listen()
↓
⑧ 組員執行 tcp_node.py
↓
⑨ Client connect()
↓
⑩ Server accept()
↓
⑪ 建立 TCP Connection
↓
⑫ 組員 sendall()
↓
⑬ 組長 recv()
↓
⑭ 組長回覆
↓
⑮ 關閉該次 Connection
十五、實際執行
Step 1:組長先啟動
python tcp_leader.py
畫面:
============================================================ TCP 小組節點資料收集系統 本電腦角色:組長 TCP Server 等待 TCP Port 5005... ============================================================ 等待組員連線...
Step 2:組員設定組長 IP
LEADER_IP = "192.168.1.100"
Step 3:組員執行
python tcp_node.py
例如:
請輸入學號:411001001 請輸入姓名:王小明 準備傳送資料 ---------------------------------------- 學號:411001001 姓名:王小明 IP:192.168.1.101 MAC:34:12:98:AA:01:10 ---------------------------------------- 組長 Server:192.168.1.100 正在連線到組長... TCP 連線成功! 資料已透過 TCP 傳送給組長! 組長回覆: 組長已收到 411001001 王小明 的資料 TCP Connection 已關閉
十六、組長看到的資料
收到 TCP 連線: 192.168.1.101:52341 目前收到 1 / 5 位組員 -------------------------------------------------- 學號:411001001 姓名:王小明 組員回報 IP: 192.168.1.101 TCP 實際來源 IP: 192.168.1.101 MAC: 34:12:98:AA:01:10 來源 Port: 52341
這裡同樣可以比較:
與
TCP Server 從 accept() 得到的實際來源 IP
十七、MAC 為什麼仍然需要組員自己回報?
雖然我們從 UDP 改成 TCP, 但這個觀念沒有改變。
組員
│
├─ 學號
├─ 姓名
├─ IP
└─ MAC
│
↓
Dictionary
↓
JSON
↓
TCP
↓
組長 Server
Server 從 TCP 連線可以知道對端 IP 與來源 Port, 但 JSON 中的 MAC 是由組員程式自行取得後主動回報的應用資料。
十八、五位組員完成後
========================================================================================== 本組 5 位組員資料收集完成 ========================================================================================== 學號 姓名 回報IP 來源IP MAC ------------------------------------------------------------------------------------------ 411001001 王小明 192.168.1.101 192.168.1.101 34:12:98:AA:01:10 411001002 李小華 192.168.1.102 192.168.1.102 50:EB:F6:22:18:21 411001003 陳大文 192.168.1.103 192.168.1.103 10:62:E5:31:21:33 411001004 林小美 192.168.1.104 192.168.1.104 A4:BB:6D:11:23:44 411001005 張志明 192.168.1.105 192.168.1.105 D8:43:AE:12:35:55
十九、從組員輸入姓名到組長收到資料
↓
取得 IP、MAC
↓
Python Dictionary
↓
JSON
↓
UTF-8 Bytes
↓
TCP Socket
↓
connect()
↓
TCP Connection
↓
sendall()
↓
IP Network
↓
組長 Port 5005
↓
accept()
↓
recv()
↓
JSON Decode
↓
建立節點資料表
↓
組長回覆
↓
close()
二十、如果出現 Connection refused
例如:
ConnectionRefusedError
通常代表 Client 已經找到目標主機, 但指定的 TCP Port 沒有接受連線,或連線受到系統/網路政策阻擋。
首先檢查:
- 組長是否先執行
tcp_leader.py。 - 組員設定的
LEADER_IP是否正確。 - Server 與 Client 是否都使用 Port 5005。
- Windows 防火牆是否允許 Python 網路通訊。
- 目前網路是否允許組員電腦與組長電腦互相連線。
二十一、UDP 與 TCP 程式的關鍵差異
學過上一個 UDP 實驗後,可以直接比較:
| 功能 | UDP | TCP |
|---|---|---|
| Socket | SOCK_DGRAM |
SOCK_STREAM |
| Server 等待 | recvfrom() |
listen() + accept() |
| Client 連線 | 不需 TCP 連線 | connect() |
| 傳送 | sendto() |
sendall() |
| 接收 | recvfrom() |
recv() |
二十二、課堂思考題
- 為什麼 TCP Server 要先執行?
SOCK_STREAM代表什麼?bind()的作用是什麼?listen()的作用是什麼?accept()在等待什麼?connect()做了什麼?- TCP 三向交握中的 SYN、SYN-ACK、ACK 是什麼?
sendall()和 UDP 的sendto()有什麼不同?- 為什麼 TCP Server 可以知道 Client 的來源 IP?
- 為什麼 MAC 還是由組員自己放進 JSON?
- 為什麼 TCP 不能假設一次
recv()永遠等於一次sendall()? - 組長收到 5 位組員後,為什麼程式就結束?如果要一直提供服務,應該修改哪個部分?
二十三、從 TCP 實驗看懂網際網路應用
這次看起來只是把:
「學號 + 姓名 + IP + MAC」
從 5 台組員電腦傳到組長電腦, 背後卻已經串起一整套網路概念:
↓
IP Address + Port
↓
Socket
↓
TCP
↓
connect()
↓
Three-Way Handshake
↓
accept()
↓
JSON
↓
sendall() / recv()
↓
組長建立節點資料表
透過這個實驗,同學不只是知道「TCP 是可靠的、連線導向的傳輸層協定」, 而是真正讓兩台電腦建立 TCP Connection, 再把自己建立的資料送過網路。
因此,學習網際網路最重要的不只是記住協定名稱, 而是理解: 應用程式的資料,如何透過 Socket、TCP、IP 與實際網路, 從一台電腦到達另一台電腦。
沒有留言:
張貼留言