udp編程的那些事與golang udp的實踐

udp編程的那些事與golang udp的實踐

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

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實踐

實踐出真知,編程就須要多實踐,才能體會其中的奧妙。socket

echo客戶端和服務端

echo服務,實現數據包的回顯,這是不少人網絡編程起點,由於這個服務足夠簡單,但又把網絡的數據流都過了一遍,這裏也用go udp實現一個echo服務。
實現客戶端發送一個「hello」,服務端接收消息並原封不動的返回給客戶度。tcp

server.goui

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.gocode

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.goserver

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 doc

相關文章
相關標籤/搜索