---
# System prepended metadata

title: '【計算機網路筆記】2.7 Socket Programming: Creating Network Applications'
tags: [計算機網路, Web, 網路, 網路概論, 電腦雜談, 電腦, 網頁]

---

# 【計算機網路筆記】2.7 Socket Programming: Creating Network Applications

[TOC]

Hello Guys, I'm LukeTseng. 歡迎你也感謝你點入本篇文章，本系列主要讀本為《Computer Networking: A Top-Down Approach, **8**th Edition》，就是計算機網路的聖經，會製作該系列也主要因為修課上會用到。若你喜歡本系列或本文，不妨動動你的手指，為這篇文章按下一顆愛心吧，或是追蹤我的個人公開頁也 Ok。

---

**複習：什麼是 Socket？**

簡單來說，Socket（插座）就是應用程式（Application）與網路（Network）之間的介面（Interface）。

Socket 的存在是為了把「網路層只做到主機對主機」提升成「傳輸層要做到行程（process）對行程」的溝通，並讓同一台主機上的許多應用程式能同時共用 TCP/UDP 而不互相搞混。

做比喻的話，行程（process）就像是房子，而 Socket 如同房子中的門。

當使用者想發訊息給別人，使用者會把訊息推出「門」（Socket）外，一旦出門，訊息就交給了「運輸部門」（傳輸層），不用管它是怎麼送的，只要相信它會送到對方的門口。

**兩種主要的傳輸選擇：**

在建立 Socket 時，身為開發者必須做出一個決定：要用哪種傳輸層協定？

1. UDP（User Datagram Protocol）：不可靠、無連線。
2. TCP（Transmission Control Protocol）：可靠、連線導向。

## 2.7.1 Socket Programming with UDP

UDP 為一種非連線導向（Connectionless）的傳輸協定。

在寫程式時，意即在傳送資料前，不需與對方做握手（Handshaking）協定來建立連線。

運作邏輯：

1. 沒有連線：傳送端直接把資料打包，推送到網路中。
2. 明確地址：因為沒有預先建立的通道，所以每個資料（Packet／Segment）在送出時，都必須明確附上目的地的 IP 位址 與埠號（Port Number）。
3. 不保證送達：如同寄平信，寄出後就管不了，網路會盡力送，但可能會遺失或亂序。

UDP 適用情境：適合對速度要求高、能容忍少量資料遺失的應用，例如 DNS 查詢、語音串流或即時遊戲。

### 術語解析

- 資料包（Datagram）：在 UDP 程式設計中，傳送的獨立資料單元通常被稱為 Datagram。
- 埠號（Port Number）：用來識別主機上的特定行程（Process）。
    - Server Port：通常固定（如 HTTP 是 80，接下來的範例用 12000），好讓大家找得到。
    - Client Port：通常由作業系統自動分配。
- `AF_INET`：代表我們使用 IPv4 位址家族。
- `SOCK_DGRAM`：代表我們使用 Datagram socket，也就是 UDP。

### UDP Socket 應用

接下來的範例會做以下這四件事情：

1. 客戶端會讀入一行字元（資料），並將該行資料送給伺服端。
2. 伺服端接收該資料並將字元轉為大寫。
3. 伺服端將修改後的這行資料送給客戶端。
4. 客戶端接收修改後的資料，接著印出在螢幕上。

![image](https://hackmd.io/_uploads/ByoKkvxO-l.png)

Image Source：Computer Networking: A Top-Down Approach (8th ed., p. 185, Figure 2.27)

客戶端的程式檔案名為 `UDPClient.py`，伺服端稱為 `UDPServer.py`。

`UDPClient.py`：Client 的任務是建立 Socket -> 準備資料與地址 -> 寄出 -> 等待回信。

```python=
from socket import *
serverName = '127.0.0.1' # Server 的 IP 或主機名稱 (要在本機執行請輸入 '127.0.0.1' 或 'localhost')
serverPort = 12000 # Server 指定的 Port
# AF_INET: IPv4
# SOCK_DGRAM: UDP
clientSocket = socket(AF_INET, SOCK_DGRAM) # 建立 socket
message = input('Input lowercase sentence:') # 獲取使用者輸入
# clientSocket.sendto(data, address) 傳送封包
# address 為一個 tuple (IP, Port)
# UDP 必須在 sendto() 中明確指定目的地地址 (IP, Port)
# message.encode() 將字串轉為 bytes，因為網路傳輸的是位元組
clientSocket.sendto(message.encode(), (serverName, serverPort))
# recvfrom(bufsize) 接收回應
# bufsize (緩衝區) 設定每次接收的最大位元組數
# modifiedMessage: 收到的資料
# serverAddress: 對方(Server) 的地址資訊
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
print(modifiedMessage.decode()) # 將 bytes 轉回字串並印出
clientSocket.close() # 關閉 Socket
```

程式重點：

- 地址打包：注意 `sendto()` 函式，我們把資料和目的地 `(serverName, serverPort)` 一起包進去。作業系統會自動把客戶端的 IP 和 Port 也附在封包裡作為「寄件人地址」，讓 Server 知道怎麼回信。
- 自動分配 Port：客戶端程式碼沒有寫 `bind()`，這代表我們不介意作業系統給我們哪個 Port，只要能用就好。

`bind()` 方法會在伺服端中看到，用於將 Socket 物件綁定到特定的 IP 位址和埠號（Port）。

---

接下來是 `UDPServer.py`：Server 的任務是建立 Socket -> 綁定（Bind）特定 Port -> 進入無限迴圈 -> 收到信 -> 處理 -> 依據「寄件人地址」回信。

```python=
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_DGRAM) # 建立 UDP Socket
# 綁定 bind(address)
# '' 代表綁定本機所有可用的網路介面
# 這樣做是因為 Server 必須在一個眾所皆知的 Port 上等待, 別人才找得到
serverSocket.bind(('', serverPort))
print("The server is ready to receive")
while True:
    # 接收封包
    # recvfrom 回傳 (資料, 來源地址) 的 tuple 資料
    # clientAddress 即 Client 的 IP 和 Port
    message, clientAddress = serverSocket.recvfrom(2048)
    modifiedMessage = message.decode().upper() # 處理資料 (轉大寫)
    # 回傳封包
    # 使用剛才拿到的 clientAddress 作為目的地
    serverSocket.sendto(modifiedMessage.encode(), clientAddress)
```

程式重點：

- Server 端必須使用 `bind()`，就像開店要掛招牌、選固定地址一樣，否則 Client 端不知道要把封包寄到哪裡。
- `recvfrom()` 回傳的 `clientAddress` 非常重要，因為 UDP 沒有連線狀態，Server 唯一知道「是誰寄來」以及「該回給誰」的依據，就是從封包裡拆出來的來源地址。

## 2.7.2 Socket Programming with TCP

TCP 是連線導向（Connection-Oriented）的協定。

表示 Client 端和 Server 端在交換資料前，必須先進行握手（Handshaking）協定來建立一條邏輯上的連線。

**TCP 特點：**

- 可靠性：TCP 保證資料**無誤、不遺漏、按順序**送達。
- 位元組流（Byte Stream）：應用程式看到的不是一個個獨立的封包，而是一條連續的資料流（Pipe），Client 端只要把資料丟進管子，TCP 就會負責把它送到另一端。

**TCP vs UDP：**

- 在 UDP 中，Client 端每次寄信都要寫地址。
- 在 TCP 中，一旦連線建立（電話接通），Client 端只需對著話筒（Socket）說話，不需要每次都喊對方的名字。

### 術語解析

- 握手（Handshaking）：在傳送任何實際資料前，Client 端和 Server 端交換控制封包（TCP 三次交握）來建立連線的過程，由作業系統在背後完成。
- 歡迎 Socket（Welcoming Socket）：Server 端最初建立的 Socket，就像公司的總機櫃檯，它只負責做接待歡迎，任意主機上執行的客戶端行程所發出的初始聯繫，但不負責具體的對話服務。
- 連線 Socket（Connection Socket）：當總機接到電話後，會轉接給一位專員，這個專員（新的 Socket）專門負責服務這一位特定的客戶。Server 端會為每一個連上的 Client 端建立一個專屬的連線 Socket。

如下圖，可見 TCP Server 行程會有兩個 sockets。

![image](https://hackmd.io/_uploads/rkL7_ieO-x.png)

Image Source：Computer Networking: A Top-Down Approach (8th ed., p. 191, Figure 2.28)

### TCP Socket 應用

繼續延續大小寫轉換的例子，下圖是 TCP Socket 的應用程式流程圖：

![image](https://hackmd.io/_uploads/HyO5uogOZe.png)

Image Source：Computer Networking: A Top-Down Approach (8th ed., p. 192, Figure 2.29)

`TCPClient.py`：Client 的任務是建立 Socket -> 發起連線 -> 傳送資料 -> 接收回應 -> 關閉連線。

```python=
from socket import *
serverName = 'localhost'
serverPort = 12000
# 建立 TCP Socket
# SOCK_STREAM: 代表使用 TCP (Stream)
# 注意:這裡還沒有指定 Port, 作業系統會自動分配一個
clientSocket = socket(AF_INET, SOCK_STREAM)
# 發起連線
# 這行指令會觸發 TCP 的三次交握
# 括號內是 Tuple (Server IP, Server Port)
clientSocket.connect((serverName, serverPort))
sentence = input('Input lowercase sentence:')
# 傳送資料
# 與 UDP 的差異: 這裡用 send() 而不是 sendto()
# 因為連線已經建立, 不需要再指定地址, 直接丟進 Socket
clientSocket.send(sentence.encode())
# 接收回應
modifiedSentence = clientSocket.recv(1024)
print('From Server: ', modifiedSentence.decode())
# 關閉連線
# 這會發送 TCP FIN 封包, 通知 Server 要關閉連線
clientSocket.close()
```

程式重點：

- `clientSocket.connect()` 是 TCP 獨有的步驟，執行完這行，Client 和 Server 之間就建立了一條虛擬管道。
- 隱含的地址：注意 `send()` 函式不需要參數 `(serverName, serverPort)`，因為 Socket 已經記住連線對象是誰了。

---

`TCPServer.py`：Server 的任務比較複雜，它需要兩個 Socket：

1. Server Socket（門鈴／總機）：永遠開著，等待敲門。
2. Connection Socket（專員／分機）：有人敲門後動態產生，服務完就銷毀。

```python=
from socket import *

serverPort = 12000

# 建立歡迎 Socket (Welcoming Socket)
serverSocket = socket(AF_INET, SOCK_STREAM)

# 綁定 Port
serverSocket.bind(('', serverPort))

# 開始監聽 (listen)
# 參數 1 代表等待佇列的長度 (Queue length)
serverSocket.listen(1)

print('The server is ready to receive')

while True:
    # 接受連線
    # 當 Client 端敲門 (連線) 時, accept() 回傳一個 tuple (connectionSocket, addr):
    # connectionSocket: 一個全新的 Socket, 專門用來跟這個 Client 端說話
    # addr (address): Client 端的 IP 和 Port
    connectionSocket, addr = serverSocket.accept()
    
    # 接收資料 (使用新的 Connection Socket 接收)
    sentence = connectionSocket.recv(1024).decode()
    
    capitalizedSentence = sentence.upper()
    
    # 回傳資料 (使用新的 Connection Socket 回傳)
    connectionSocket.send(capitalizedSentence.encode())
    
    # 關閉專屬連線
    # 服務完客戶端後, 就把該專屬 Socket 關掉
    # 注意：原本的 serverSocket (歡迎 Socket) 還在迴圈外活得好好的, 仍繼續等下一個人
    connectionSocket.close()
```

程式重點：

- `serverSocket.accept()` 是一個會阻塞（Block）的指令，程式跑到這裡會停下來，直到有 Client 連上來為止。一旦有連線，它會生出一個新的 connectionSocket。
- 為什麼要兩個 Socket？為了[並行性](https://zh.wikipedia.org/zh-tw/%E5%B9%B6%E5%8F%91%E6%80%A7)（Concurrency），如果只有一個 Socket，當 Server 正在跟 Client A 傳輸大檔案時，Client B 就連不進來了。透過這種機制，serverSocket 可以持續在門口接客，而將實際的服務工作交給多個 connectionSocket 去做平行處理（雖此例為單執行緒，但實務上會配合多執行緒（Multithreading）使用）。

## TCP 與 UDP Socket 程式設計差異對照表

| 比較項目 | UDP Socket | TCP Socket |
| -------- | -------- | -------- |
| 連線建立 | 無連線（Connectionless） | 需要 `connect()` 和 `accept()` |
| 傳送指令 | `sendto(data, address)` | `send(data)`（地址已隱含） |
| 接收指令 | `recvfrom()`（需接收來源地址） | `recv()`（位元組流） |
| Server 架構 | 單一 Socket 處理所有封包 | 歡迎 Socket + 多個 連線 Socket |
| 資料邊界 | 保留（傳送幾次就接收幾次） | 不保留（資料流可能需要多次使用 `recv()`） |

## 總整理

### 複習

- Socket（插座）：
    - 定義：應用程式（Application）與網路（Network）之間的介面，亦為應用層與傳輸層間的介面。
    - 功能：將「主機對主機」的傳輸提升為「行程（Process）對行程」。
    - 比喻：Process 是房子，Socket 是門，訊息推出門後，由傳輸層負責運送。
- 開發者的傳輸層協定選擇：
    - UDP：不可靠、無連線、速度快。
    - TCP：可靠、連線導向。

### UDP Socket Programming

UDP 特點：非連線導向（Connectionless）、無握手協定、需明確指定地址、不保證送達。

socket 建立關鍵參數：

- `AF_INET`：IPv4。
- `SOCK_DGRAM`：Datagram（代表 UDP）。

運作流程：

| 角色     | 流程與關鍵函式                                                                             | 備註                                                                     |
|--------|-------------------------------------------------------------------------------------|------------------------------------------------------------------------|
| Client | 1. `socket()` 建立<br>2. `sendto(data, (ip, port))` 傳送<br>3. `recvfrom(bufsize)` 接收<br>4. `close()` 關閉連線       | 無需 `bind()` 綁定 Port（作業系統會自動分配 Port），傳送時必須打包「目的地地址」。                                 |
| Server | 1. `socket()` 建立<br>2. `bind('', port)` 綁定 Port<br>3. `while True:` 迴圈監聽<br>4. `recvfrom()` 接收<br>5. `sendto()` 回傳 | 必須 `bind()` 做綁定（固定 Port 別人才找得到）。<br>`recvfrom()` 回傳 `(data, clientAddress)`，回信時需使用該地址。 |

### TCP Socket Programming

TCP 特點：連線導向（Connection-Oriented）、需做握手協定（Handshaking）、可靠傳輸、位元組流（Byte Stream）。

socket 建立關鍵參數：

- `AF_INET`：IPv4。
- `SOCK_STREAM`：Stream（TCP）。

#### TCP 核心機制：雙 Socket 架構

Server 端會有兩種 Socket，以處理並行性 (Concurrency)：

1. Welcome Socket（歡迎 Socket）：如同總機櫃檯，只負責 listen（監聽） 和 accept（接受連線），永遠開啟的通道。
2. Connection Socket（連線 Socket）：如同專屬專員，accept 接受連線後動態產生，只服務特定 Client，服務完即銷毀。

運作流程：

| 角色     | 流程與關鍵函式                                                                                                                                      | 備註                                                         |
|--------|----------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
| Client | 1. `socket()` 建立<br>2. `connect((ip, port))` 連線<br>3. `send(data)` 傳送<br>4. `recv()` 接收<br>5. `close()` 關閉連線                                                               | `connect()` 會觸發三次交握。<br>`send()`/`recv()` 不需指定地址（因為連線已建立）。                  |
| Server | 1. `socket()` 建立（此為 Welcome Socket）<br>2. `bind()` 綁定 Port<br>3. `listen(n)` 開始監聽<br>4. `while True:` 無窮迴圈<br>5. `accept()` 接受連線<br>6.（產生 Connection Socket）處理資料<br>7. `close()` 關閉專屬連線 | `accept()` 會阻塞（block）直到有連線，並回傳 `(newSocket, addr)`。<br>收發資料是用新產生的 Socket 來做。 |

### UDP vs TCP Socket Programming

| 比較項目 | UDP Socket | TCP Socket |
| -------- | -------- | -------- |
| 連線建立 | 無連線（Connectionless） | 需要 `connect()` 和 `accept()` |
| 傳送指令 | `sendto(data, address)` | `send(data)`（地址已隱含） |
| 接收指令 | `recvfrom()`（需接收來源地址） | `recv()`（位元組流） |
| Server 架構 | 單一 Socket 處理所有封包 | 歡迎 Socket + 多個 連線 Socket |
| 資料邊界 | 保留（傳送幾次就接收幾次） | 不保留（資料流可能需要多次使用 `recv()`） |