這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
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 mainimport ("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 addressudpAddr, err := net.ResolveUDPAddr("udp", *addr)if err != nil {log.Fatalln("Error: ", err)}// Build listining connectionsconn, err := net.ListenUDP("udp", udpAddr)if err != nil {log.Fatalln("Error: ", err)}defer conn.Close()// Interacting with one client at a timerecvBuff := make([]byte, 1024)for {log.Println("Ready to receive packets!")// Receiving a messagern, 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 mainimport ("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 AddressremoteAddr, err := net.ResolveUDPAddr("udp", *raddr)if err != nil {log.Fatalln("Error: ", err)}// Make a connectiontmpAddr := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"),Port: 0,}conn, err := net.DialUDP("udp", tmpAddr, remoteAddr)// Exit if some error occuredif 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 serverbuf := 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 mainimport ("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 AddresslocalAddr, 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 connectionsconn, err := net.ListenUDP("udp", localAddr)// Exit if some error occuredif 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 serverbuf := 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