# 【計算機網路筆記】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 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 Source:Computer Networking: A Top-Down Approach (8th ed., p. 191, Figure 2.28)
### TCP Socket 應用
繼續延續大小寫轉換的例子,下圖是 TCP Socket 的應用程式流程圖:

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()`) |