TCP和UDP可以使用同一個埠號嗎?
首先說答案:可以。怎麼理解呢?
我想這個問題要從計算機網路通訊談起,學過計算機網路的同學,可能都還記得7層或者4層網路模型,TCP/UDP屬於其中的傳輸層協議,在傳輸層之下是網路層,網路層主要透過IP協議來進行通訊,這也是我們日常程式開發中能夠接觸到的最底層了,再往下的資料鏈路層和物理層就不是我們這些普通程式設計師需要關心的了。
IP
我們先具體看下網路層。在IP網路層,傳送者向接收者傳輸資料的時候,首先需要知道接收者的IP地址,IP地址可以在網路中唯一標識一臺計算機,然後資料就可以根據IP協議抵達接收者所在的計算機,但是接收者所在的計算機上執行了幾十個程式,計算機應該把這個資料交給哪個程式呢?
埠號
這就像快遞員到達了一棟大樓,下一步它怎麼把快遞送到對應的使用者手中呢?聰明的你一定想到了,那就是門牌號。
在計算機中,埠號就是門牌號。計算機的作業系統可以為不同的程式繫結不同的埠號,這樣傳送者傳送資料時不僅要設定接收者的IP,還要加上接收者的埠號,如此接收者所在的計算機就能把資料轉發給正確的程式了。
TCP/UDP
那麼TCP和UDP能不能使用同一個埠號呢?其實在查詢埠號之前還有一個傳輸層協議的處理過程,作業系統收到資料後,會先檢視資料包使用的是TCP協議還是UDP協議,然後再根據協議進行不同的解析處理,提取到資料後,再轉發到擁有對應埠的程式。
所以TCP和UDP是可以使用相同的埠號的,這在現實中也是常見的。比如 DNS(域名系統)可能需要同時支援 TCP 和 UDP 查詢,這兩種查詢就都可以透過53這個標準埠來進行接收和響應。
但是在同一個傳輸協議下,埠號就不能相同了。如果相同,作業系統的協議棧就不知道該把這個資料包轉給哪個程式了,這種設計會增加很多麻煩。
有的同學可能會觀察到一個現象,那就是同一個計算機上的多個網站可以共享80或者443埠,這其實是應用層的能力,這些網站都寄宿在同一個Web伺服器程式上,這個Web伺服器程式繫結了80埠,Web伺服器收到資料後再根據HTTP協議中的主機頭(可以理解成域名)轉發給不同的網站程式。
還有,如果你的電腦上有多個IP,那就更沒有問題了。不同的IP代表不同的網路介面,即使都使用TCP協議,只要IP不同,埠號一樣也完全不會衝突。
“IP+傳輸層協議+埠號”就是我們常說的套接字,它能確保資料從一個網路程式傳遞到另一個網路程式。大家如果直接使用TCP和UDP程式設計,就需要手動為套接字設定這幾個引數。
示例
口說無憑,再給大家寫個demo,使用go語言,簡單易懂:
下邊的程式會啟動一個TCP伺服器和一個UDP伺服器,它們繫結相同的IP和埠號。這裡為了方便測試,使用了127.0.0.1這個本機IP,你也可以換成區域網或者公網IP。
package main
import (
"fmt"
"net"
"os"
)
func main() {
// 定義監聽的埠
port := "127.0.0.1:12345"
// 啟動TCP伺服器
go startTCPServer(port)
// 啟動UDP伺服器
startUDPServer(port)
}
func startTCPServer(port string) {
// 透過TCP協議監聽埠
l, err := net.Listen("tcp", port)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
defer l.Close()
fmt.Println("TCP Server Listening on " + port)
// 持續接收TCP資料
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
os.Exit(1)
}
fmt.Println("Received TCP connection")
conn.Close()
}
}
func startUDPServer(port string) {
// 透過UDP協議監聽埠
addr, err := net.ResolveUDPAddr("udp", port)
if err != nil {
fmt.Println("Error resolving: ", err.Error())
os.Exit(1)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("Error listening: ", err.Error())
os.Exit(1)
}
defer conn.Close()
fmt.Println("UDP Server Listening on " + port)
buffer := make([]byte, 1024)
// 持續接收UDP資料
for {
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("Error reading: ", err.Error())
continue
}
fmt.Printf("Received UDP packet: %s\n", string(buffer[:n]))
}
}
然後再建立兩個客戶端,一個是TCP客戶端:
package main
import (
"fmt"
"net"
"os"
)
func main() {
// 連線到伺服器
conn, err := net.Dial("tcp", "localhost:12345")
if err != nil {
fmt.Println("Error connecting:", err.Error())
os.Exit(1)
}
defer conn.Close()
// 傳送資料
_, err = conn.Write([]byte("Hello TCP Server!"))
if err != nil {
fmt.Println("Error sending data:", err.Error())
return
}
fmt.Println("Message sent to TCP server")
}
另一個是UDP客戶端:
package main
import (
"fmt"
"net"
"os"
)
func main() {
ServerAddr, err := net.ResolveUDPAddr("udp", "localhost:12345")
if err != nil {
fmt.Println("Error resolving: ", err.Error())
os.Exit(1)
}
conn, err := net.DialUDP("udp", nil, ServerAddr)
if err != nil {
fmt.Println("Error dialing: ", err.Error())
os.Exit(1)
}
defer conn.Close()
// 傳送資料
_, err = conn.Write([]byte("Hello UDP Server!"))
if err != nil {
fmt.Println("Error sending data:", err.Error())
return
}
fmt.Println("Message sent to UDP server")
}
我們可以看到,客戶端發起請求的時候都使用了 localhost:12345 這個目標地址,其中的localhost 實際上是個域名,它會被本地計算機解析為 127.0.0.1。這塊不清楚的可以看我之前寫的這篇:
實際執行效果如下:
最後總結下:在網路通訊中,同一臺計算機中,TCP和UDP協議可以使用相同的埠號。每個網路程序中的套接字地址都是唯一的,由三元組(IP地址,傳輸層協議,埠號)標識。作業系統會根據資料包中的傳輸層協議(TCP或UDP)以及埠號,將接收到的資料正確地交付給相應的應用程式。