# 【計算機網路筆記】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()`) |