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

Hello Guys, I’m LukeTseng. 歡迎你也感謝你點入本篇文章,本系列主要讀本為《Computer Networking: A Top-Down Approach, 8th 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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 -> 進入無限迴圈 -> 收到信 -> 處理 -> 依據「寄件人地址」回信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

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

TCP Socket 應用

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

image

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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(專員/分機):有人敲門後動態產生,服務完就銷毀。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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?為了並行性(Concurrency),如果只有一個 Socket,當 Server 正在跟 Client A 傳輸大檔案時,Client B 就連不進來了。透過這種機制,serverSocket 可以持續在門口接客,而將實際的服務工作交給多個 connectionSocket 去做平行處理(雖此例為單執行緒,但實務上會配合多執行緒(Multithreading)使用)。

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

比較項目UDP SocketTCP 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)。

運作流程:

角色流程與關鍵函式備註
Client1. socket() 建立
2. sendto(data, (ip, port)) 傳送
3. recvfrom(bufsize) 接收
4. close() 關閉連線
無需 bind() 綁定 Port(作業系統會自動分配 Port),傳送時必須打包「目的地地址」。
Server1. socket() 建立
2. bind('', port) 綁定 Port
3. while True: 迴圈監聽
4. recvfrom() 接收
5. sendto() 回傳
必須 bind() 做綁定(固定 Port 別人才找得到)。
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,服務完即銷毀。

運作流程:

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

UDP vs TCP Socket Programming

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