爲什麼 UDP 需要建連

最近在用 Go 程序寫 udp 服務的時候,有一次服務端忘記啓動了,直接啓動的客戶端發現如下錯誤:

2021/10/17 11:54:59 read udp 127.0.0.1:53913->127.0.0.1:6060:
recvfrom: connection refused

我的內心還是尷尬的,因爲這和課本上講的不一樣。

udp 不是無連接的麼,爲什麼還會出現 connection refused 呢.

本文會按照先解密再分析的思路來講解這個問題。

注⚠️:

1、 強烈推薦看這篇文章 (https://ops.tips/blog/udp-client-and-server-in-go/)。從 UDP 的使用到各個函數的原理分析都很細緻。

2、 本文的所有代碼在原文同級目錄均有

3、 因爲是公衆號,爲了照顧體驗,所以我們只貼關鍵代碼,完整代碼可以點擊原文

connection refused 的 Go 代碼

server.go

func main() {
  udpAddr,_ := net.ResolveUDPAddr("udp4", "6000")
  conn, err := net.ListenUDP("udp", udpAddr)

  for {
    buffer := make([]byte, 4096)
    n, addr, _ := conn.ReadFromUDP(buffer)

    message := buffer[:n]
    conn.WriteToUDP(message, addr)
  }

}

這裏需要指出的是 net.ListenUDP 返回的是 UDPConn,而 net.ListenTCP 返回的是 TCPListener。

後者每有一個連接進來都會調用 Accept 函數,而前者是無連接的自然也就沒有 Accept 這個步驟。

client.go

func main() {
  RemoteAddr, _ := net.ResolveUDPAddr("udp", ":6060")
  conn, err := net.DialUDP("udp", nil, RemoteAddr)

  conn.Write([]byte("hello"))

  buffer := make([]byte, 4096)
  conn.ReadFromUDP(buffer)
  fmt.Println(string(buffer))
}

當我們不運行 server 直接運行 client 的時候就會出現 connection refused。我們用 Go 還找不到答案(後續會說爲什麼),需要通過 C 繼續探索。

結論

說結論之前需要我們先看 C 語言實現 UDP 的 client 和 server 的代碼:

server.c

int main() {

  sockfd = socket(AF_INET, SOCK_DGRAM, 0)
  // ...
  bind(sockfd, (const struct sockaddr *)
   &servaddr, sizeof(servaddr))

  int len, n;


  n = recvfrom(sockfd, (char *)buffer, MAXLINE,
        MSG_WAITALL, ( struct sockaddr *)
        &cliaddr, &len);
  

  sendto(sockfd, (const char *)hello,
    strlen(hello),
    MSG_CONFIRM, (const struct sockaddr *) 
     &cliaddr, len);
  printf("Hello message sent.\n");

  return 0;
}

client.c

int main() {

  sockfd = socket(AF_INET, SOCK_DGRAM, 0)
  // ...

  sendto(sockfd, (const char *)hello, 
    strlen(hello),
    MSG_CONFIRM, (const struct sockaddr *)
    &servaddr, sizeof(servaddr));
  printf("Hello message sent.\n");

  n = recvfrom(sockfd, (char *)buffer, MAXLINE,
        MSG_WAITALL, (struct sockaddr *)
        &servaddr, &len);

  printf("Server : %s\n", buffer);


  return 0;
}

雖然是 C 代碼,但是並不難讀

這段程序不啓動 server 直接調用 client 的話會一直卡在 sendto。

如果你熟悉 Go 的 net.DialUDP 的話,就知道他系統調用了 connect 系統調用,我們可以通過 strace(strace ./udp) 驗證一下:

server 不啓動報錯如下:

從上面兩段(Go 和 C 的)來分析,UDP 建連是爲了響應錯誤,udp 的建連並不會像 TCP 那樣真的進行的三次握手只是在內核中做 socket -> 目的 ip+port 的映射,當我們調用 sendto 出錯的時候(比如地址不可達)如果不建立連接的話,內核知道這個錯誤但是不知道發給哪個 socket,建連之後就知道了。

除了容錯之外還有一方面就是效率,先來看下不建立連接的 UDP 發包過程:

建立 socket -> sendto -> 斷開 socket -> 建立 socket -> sendto -> 斷開 socket。

如果建連的話就是:

建立 socket -> sendto-> sendto-> sendto -> 斷開 socket。

即 socket 複用。

建連的消耗

對比一下幾組數據:

UDP 原理

圖 from:https://www.geeksforgeeks.org/udp-server-client-implementation-c/

如果你寫過 TCP 程序的就知道 TCP 使用 write/read 的參數中都是不用指定遠程地址了,這也是面向連接的意思,在一個連接上收發就不用指定了。但是 UDP 這種無連接(non-connected)的協議就不一樣,調用 writeto/readfrom 的時候必須把遠程地址加上。

如果你再看一下最開始的 Go 程序,會發現 client 中 net.DialUDP 返回的 UDPConn 是能調用 Write 的,net.ListenUDP 返回的 UDPLinstener 就不能調用 Read/write。因爲前者是 connected 後者是 non-connected。

你可能想問 socket 是什麼?socket 是文件描述符,裏面包含了很多文件的操作(比如 read/write/close)。

read/readfrom 有什麼區別?如果同時寫過 TCP 和 UDP 網絡程序的話這個會很好理解 read 是面向連接的,readfrom 是面向無連接的。

ssize_t read(int fd, void *buf, size_t count);
recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen)

總結

本文通過 Go 代碼引出爲什麼 UDP 需要建立連接,然後通過 C 程序爲你講述不建立連接的後果,再然後通過一些數據表明了建立連接的耗時最後着重分析了 UDP 的原理。

這兩週接觸網絡編程我才意識到,原來書本上的理論覺得再正確到了實際編程的時候還有很多細節要考慮。

如果想繼續深入 UDP in Go 可以看 https://ops.tips/blog/udp-client-and-server-in-go/,講的深入淺出,從示例到內核原理都展示出來,看的時候讓我覺得 “這哥們基本功真紮實”。

還可以看下鳥窩的分享,裏面有大量的程序實例 https://colobu.com/2016/10/19/Go-UDP-Programming/。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_Bn43_G1aHAYkW2qVCyLDw