位元組一面:TCP和UDP可以使用同一個連接埠號碼嗎?

2024.03.05


網路網路最佳化
在網路通訊中,同一台電腦中,TCP和UDP協定可以使用相同的連接埠號碼。每個網路進程中的套接字位址都是唯一的,由三元組(IP位址,傳輸層協議,連接埠號碼)標識。作業系統會根據資料包中的傳輸層協定(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]))
    }
}
  • 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.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.

然後再建立兩個客戶端,一個是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")
}
  • 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.

另一個是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")
}
  • 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.

我們可以看到,客戶端發起請求的時候都使用了localhost:12345 這個目標位址,其中的localhost 其實是個域名,它會被本地電腦解析為127.0.0.1。這塊不清楚的可以看我之前寫的這篇:

實際運作效果如下:

圖片圖片

最後總結下:在網路通訊中,同一台電腦中,TCP和UDP協定可以使用相同的連接埠號碼。每個網路進程中的套接字位址都是唯一的,由三元組(IP位址,傳輸層協議,連接埠號碼)標識。作業系統會根據資料包中的傳輸層協定(TCP或UDP)以及連接埠號,將接收到的資料正確地交付給對應的應用程式。