udp程式設計的那些事與golang udp的實踐

sheepbao發表於2017-06-26

udp程式設計的那些事與golang udp的實踐

tcp/ip大協議中,tcp程式設計大家應該比較熟,應用的場景也很多,但是udp在現實中,應用也不少,而在大部分博文中,都很少對udp的程式設計進行研究,最近研究了一下udp程式設計,正好做個記錄。
sheepbao 2017.06.15

tcp Vs udp

tcp和udp都是著名的傳輸協議,他們都是基於ip協議,都在OSI模型中傳輸層。tcp我們都很清楚,它提供了可靠的資料傳輸,而udp我們也知道,它不提供資料傳輸的可靠性,只是盡力傳輸。 他們的特性決定了它們很大的不同,tcp提供可靠性傳輸,有三次握手,4次分手,相當於具有邏輯上的連線,可以知道這個tcp連線的狀態,所以我們都說tcp是面向連線的socket,而udp沒有握手,沒有分手,也不存在邏輯上的連線,所以我們也都說udp是非面向連線的socket。
我們都畏懼不知道狀態的東西,所以即使tcp的協議比udp複雜很多,但對於系統應用層的程式設計來說,tcp程式設計其實比udp程式設計容易。而udp相對比較靈活,所以對於udp程式設計反而沒那麼容易,但其實掌握後udp程式設計也並不難。

udp協議

udp的首部

        2               2       (byte)
+---+---+---+---+---+---+---+---+      -
|    src port   |    dst port   |      |
+---+---+---+---+---+---+---+---+      8(bytes)
|     length    |   check sum   |      |
+---+---+---+---+---+---+---+---+      -
|                               |
+              data             +      
|                               |
+---+---+---+---+---+---+---+---+  

udp的首部真的很簡單,頭2個位元組表示的是原埠,後2個位元組表示的是目的埠,埠是系統層區分程式的標識。接著是udp長度,最後就是校驗和,這個其實很重要,現在的系統都是預設開啟udp校驗和的,所以我們才能確保udp訊息傳輸的完整性。如果這個校驗和關閉了,那會讓我們絕對會很憂傷,因為udp不僅不能保證資料一定到達,還不能保證即使資料到了,這個資料是否是正確的。比如:我在傳送端傳送了“hello”,而接收端卻接收到了“hell”。如果真的是這樣,我們就必須自己去校驗資料的正確性。還好udp預設開發了校驗,我們可以保證udp的資料完整性。

udp資料的封裝

                                    +---------+
                                    | 應用資料 |
                                    +---------+             
                                    |         |
                                    v         v
                          +---------+---------+
                          | udp首部  | 應用資料 |
                          +---------+---------+
                          |                   |
                          v     UDP資料包      v
                +---------+---------+---------+
                | ip首部   | udp首部  | 應用資料 |
                +---------+---------+---------+
                |                             |
                v           IP資料包           v
      +---------+---------+---------+---------+---------+
      |乙太網首部 | ip首部   | udp首部 | 應用資料 |乙太網尾部 |
      +---------+---------+---------+---------+---------+
      |   14        20         8                   4    |
      |                  -> 乙太網幀 <-                  |

資料的封裝和tcp是一樣,應用層的資料加上udp首部,構成udp資料包,再加上ip首部構成ip資料包,最後加上乙太網首部和尾部構成乙太網幀,經過網路卡傳送出去。

Golang udp實踐

實踐出真知,程式設計就需要多實踐,才能體會其中的奧妙。

echo客戶端和服務端

echo服務,實現資料包的回顯,這是很多人網路程式設計起點,因為這個服務足夠簡單,但又把網路的資料流都過了一遍,這裡也用go udp實現一個echo服務。
實現客戶端傳送一個“hello”,服務端接收訊息並原封不動的返回給客戶度。

server.go

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
)

var addr = flag.String("addr", ":10000", "udp server bing address")

func init() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    flag.Parse()
}

func main() {
    //Resolving address
    udpAddr, err := net.ResolveUDPAddr("udp", *addr)
    if err != nil {
        log.Fatalln("Error: ", err)
    }

    // Build listining connections
    conn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        log.Fatalln("Error: ", err)
    }
    defer conn.Close()

    // Interacting with one client at a time
    recvBuff := make([]byte, 1024)
    for {
        log.Println("Ready to receive packets!")
        // Receiving a message
        rn, rmAddr, err := conn.ReadFromUDP(recvBuff)
        if err != nil {
            log.Println("Error:", err)
            return
        }

        fmt.Printf("<<< Packet received from: %s, data: %s\n", rmAddr.String(), string(recvBuff[:rn]))
        // Sending the same message back to current client
        _, err = conn.WriteToUDP(recvBuff[:rn], rmAddr)
        if err != nil {
            log.Println("Error:", err)
            return
        }
        fmt.Println(">>> Sent packet to: ", rmAddr.String())
    }
}

client1.go

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
)

var raddr = flag.String("raddr", "127.0.0.1:10000", "remote server address")

func init() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    flag.Parse()
}

func main() {
    // Resolving Address
    remoteAddr, err := net.ResolveUDPAddr("udp", *raddr)
    if err != nil {
        log.Fatalln("Error: ", err)
    }

    // Make a connection
    tmpAddr := &net.UDPAddr{
        IP:   net.ParseIP("127.0.0.1"),
        Port: 0,
    }

    conn, err := net.DialUDP("udp", tmpAddr, remoteAddr)
    // Exit if some error occured
    if err != nil {
        log.Fatalln("Error: ", err)
    }
    defer conn.Close()

    // write a message to server
    _, err = conn.Write([]byte("hello"))
    if err != nil {
        log.Println(err)
    } else {
        fmt.Println(">>> Packet sent to: ", *raddr)
    }

    // Receive response from server
    buf := make([]byte, 1024)
    rn, rmAddr, err := conn.ReadFromUDP(buf)
    if err != nil {
        log.Println(err)
    } else {
        fmt.Printf("<<<  %d bytes received from: %v, data: %s\n", rn, rmAddr, string(buf[:rn]))
    }
}

client2.go

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
)

var (
    laddr = flag.String("laddr", "127.0.0.1:9000", "local server address")
    raddr = flag.String("raddr", "127.0.0.1:10000", "remote server address")
)

func init() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    flag.Parse()
}

func main() {
    // Resolving Address
    localAddr, err := net.ResolveUDPAddr("udp", *laddr)
    if err != nil {
        log.Fatalln("Error: ", err)
    }

    remoteAddr, err := net.ResolveUDPAddr("udp", *raddr)
    if err != nil {
        log.Fatalln("Error: ", err)
    }

    // Build listening connections
    conn, err := net.ListenUDP("udp", localAddr)
    // Exit if some error occured
    if err != nil {
        log.Fatalln("Error: ", err)
    }
    defer conn.Close()

    // write a message to server
    _, err = conn.WriteToUDP([]byte("hello"), remoteAddr)
    if err != nil {
        log.Println(err)
    } else {
        fmt.Println(">>> Packet sent to: ", *raddr)
    }

    // Receive response from server
    buf := make([]byte, 1024)
    rn, remAddr, err := conn.ReadFromUDP(buf)
    if err != nil {
        log.Println(err)
    } else {
        fmt.Printf("<<<  %d bytes received from: %v, data: %s\n", rn, remAddr, string(buf[:rn]))
    }
}

這裡實現echo的服務端和客戶端,和tcp的差不多,但是有一些小細節需要注意。
對於server端,先net.ListenUDP建立udp一個監聽,返回一個udp連線,這裡需要注意udp不像tcp,建立tcp監聽後返回的是一個Listener,然後阻塞等待接收一個新的連線,這樣區別是因為udp一個非面向連線的協議,它沒有會話管理。同時也因為udp是非面向連線的協議,當接收到訊息後,想把訊息返回給當前的客戶端時,是不能像tcp一樣,直接往conn裡寫的,而是需要指定遠端地址。
對於client端,類似tcp先Dial,返回一個連線,對於傳送訊息用Write,接收訊息用Read,當然udp也可以用ReadFromUDP,這樣可以知道從哪得到的訊息。但其實client也可以用另一種方式寫,如client2.go程式,先建立一個監聽,返回一個連線,用這個連線傳送訊息給服務端和從伺服器接收訊息,這種方式和tcp倒是有很大的不同。

參考

golang pkg

相關文章