分享
  1. 首页
  2. 文章

udp编程的那些事与golang udp的实践

sheepbao · · 3154 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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 doc


有疑问加站长微信联系(非本文作者)

本文来自:开源中国博客

感谢作者:sheepbao

查看原文:udp编程的那些事与golang udp的实践

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
3154 次点击 ∙ 1 赞
2 回复 | 直到 2019年10月01日 22:14:13
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏