2026年9月16日 星期三

Python TCP 分組網路實作:1 位組長收集 5 位組員的 IP、MAC、學號與姓名

大一網際網路應用|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)的傳輸層協定。

因此組員不能直接把資料丟給組長,而是先:

找到組長 IP

找到 Port 5005

建立 TCP Connection

傳送 JSON 資料

組長接收

組長回覆

關閉 Connection

五、TCP 與 UDP 有什麼不同?

比較 TCP UDP
連線 需要建立連線 不需建立連線
可靠傳輸 提供可靠、有序的位元組流 本身不保證可靠送達
資料形式 Byte Stream Datagram
常見 Socket SOCK_STREAM SOCK_DGRAM

初學時可以先這樣理解:

TCP:比較像先打電話接通,再開始說話。

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 操作:

bind()

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 建立         │
     │                                  │

完成連線後,應用程式才開始交換學生資料。

注意: Python 程式不需要自己寫 SYN、ACK。 這些工作由作業系統中的 TCP/IP 協定堆疊處理。 我們使用 Socket API 操作即可。

十三、sendall() 做什麼?

組員使用:

sock.sendall(data)

把 JSON 編碼後的 Bytes 寫入 TCP 連線。

組長則使用:

data = client_socket.recv(4096)

從 TCP 位元組流讀取資料。

本實驗的 JSON 很短,因此用單次 recv(4096) 即可作為入門示範。 但要特別知道: TCP 是 Byte Stream,不保留應用程式訊息邊界。 正式系統若要傳送較長或連續多筆資料,通常要自行設計長度欄位、分隔符號或其他 framing 機制。


十四、完整操作流程

① 6 位學生分組

② 選出 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

這裡同樣可以比較:

JSON 中組員自己回報的 IP

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 沒有接受連線,或連線受到系統/網路政策阻擋。

首先檢查:

  1. 組長是否先執行 tcp_leader.py
  2. 組員設定的 LEADER_IP 是否正確。
  3. Server 與 Client 是否都使用 Port 5005。
  4. Windows 防火牆是否允許 Python 網路通訊。
  5. 目前網路是否允許組員電腦與組長電腦互相連線。

二十一、UDP 與 TCP 程式的關鍵差異

學過上一個 UDP 實驗後,可以直接比較:

功能 UDP TCP
Socket SOCK_DGRAM SOCK_STREAM
Server 等待 recvfrom() listen() + accept()
Client 連線 不需 TCP 連線 connect()
傳送 sendto() sendall()
接收 recvfrom() recv()

二十二、課堂思考題

  1. 為什麼 TCP Server 要先執行?
  2. SOCK_STREAM 代表什麼?
  3. bind() 的作用是什麼?
  4. listen() 的作用是什麼?
  5. accept() 在等待什麼?
  6. connect() 做了什麼?
  7. TCP 三向交握中的 SYN、SYN-ACK、ACK 是什麼?
  8. sendall() 和 UDP 的 sendto() 有什麼不同?
  9. 為什麼 TCP Server 可以知道 Client 的來源 IP?
  10. 為什麼 MAC 還是由組員自己放進 JSON?
  11. 為什麼 TCP 不能假設一次 recv() 永遠等於一次 sendall()
  12. 組長收到 5 位組員後,為什麼程式就結束?如果要一直提供服務,應該修改哪個部分?

二十三、從 TCP 實驗看懂網際網路應用

這次看起來只是把:

「學號 + 姓名 + IP + MAC」

從 5 台組員電腦傳到組長電腦, 背後卻已經串起一整套網路概念:

Client / Server

IP Address + Port

Socket

TCP

connect()

Three-Way Handshake

accept()

JSON

sendall() / recv()

組長建立節點資料表

透過這個實驗,同學不只是知道「TCP 是可靠的、連線導向的傳輸層協定」, 而是真正讓兩台電腦建立 TCP Connection, 再把自己建立的資料送過網路。

因此,學習網際網路最重要的不只是記住協定名稱, 而是理解: 應用程式的資料,如何透過 Socket、TCP、IP 與實際網路, 從一台電腦到達另一台電腦。

沒有留言:

張貼留言