golang常用库之-go net包 | golang实现udp 服务器

golang常用库之-go net包 | golang实现udp 服务器

一、UDP基础

UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

UDP通信协议

  • 无连接;
  • UDP使用尽最大努力交付,不保证可靠性UDP是面向报文的,UDP对应用层交付下来的报文,既不合并,也不拆分,而是保留报文的边界;
  • 应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文
  • UDP没有拥塞控制;
  • UDP支持一对一,一对多,多对一和多对多的交互通信。
  • UDP的首部开销小,只有8字节。

UDP的优点:简单,轻量化。
UDP的缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。

这里需要注意一点,并不是所有使用UDP协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制。

UDP使用场景

UDP协议的正确使用场合(谨慎使用)
参考URL: https://network.51cto.com/art/201905/596637.htm

UDP协议的正确使用场合(谨慎使用)
对于短连接通信,一方面如果业务只需要发一两个包并且对丢包有一定的容忍度,同时业务自己有简单的轮询或重复机制,那么采用UDP会较为好些。在这样的场景下,如果用TCP,仅仅握手就需要3个包,这样显然有点不划算。

如果业务实时性要求非常高,并且不能忍受重传,那么首先就是UDP了或者只能用UDP了,例如NTP 协议,重传NTP消息纯属添乱(为什么呢?重传一个过期的时间***来,还不如发一个新的UDP包同步新的时间过来)。如果NTP协议采用TCP,撇开握手消耗较多数据包交互的问题,由于TCP受Nagel算法等影响,用户数据会在一定情况下会被内核缓存延后发送出去,这样时间同步就会出现比较大的偏差,协议将不可用。

多点通信的场景下

对于一些多点通信的场景,如果采用有连接的TCP,那么就需要和多个通信节点建立其双向连接,然后有时在NAT环境下,两个通信节点建立其直接的TCP连接不是一个容易的事情,在涉及NAT穿越的时候,UDP协议的无连接性使得穿透成功率更高(原因详见:由于UDP的无连接性,那么其完全可以向一个组播地址发送数据或者轮转地向多个目的地持续发送相同的数据,从而更为容易实现多点通信。)

一个典型的场景是多人实时音视频通信,这种场景下实时性要求比较高,可以容忍一定的丢包率。比如:对于音频,对端连续发送p1、p2、p3三个包,另一端收到了p1和p3,在没收到p2的保持p1的***一个音(也是为什么有时候网络丢包就会听到嗞嗞嗞嗞嗞嗞…或者卟卟卟卟卟卟卟卟…重音的原因),等到到p3就接着播p3了,不需要也不能补帧,一补就越来越大的延时。对于这样的场景就比较合适用UDP了,如果采用TCP,那么在出现丢包的时候,就可能会出现比较大的延时。

UDP的使用原则小结
通常情况下,UDP的使用范围是较小的,在以下的场景下,使用UDP才是明智的。

[1] 实时性要求很高,并且几乎不能容忍重传:
例子:NTP协议,实时音视频通信,多人动作类游戏中人物动作、位置。
[2] TCP实在不方便实现多点传输的情况;
[3] 需要进行NAT穿越;
[4] 对网络状态很熟悉,确保udp网络中没有氓流行为,疯狂抢带宽;
[5] 熟悉UDP编程。

总结:UDP协议是直接发送,不会判断是否接收和发送成功,应用场景:当强调输出性能而非完整性时,如音频和多媒体的应用。
一般使用udp的场景都是收发包非常快速的场景。

二、为什么TCP会粘包、UDP不会粘包?

1500字节为何成为互联网MTU标准?
参考URL: https://www.freesion.com/article/5773577654/
tcp和upd粘包、拆包、ip分片问题
参考URL: https://blog.csdn.net/jinxinliu1/article/details/80609272

1. TCP协议面向字节流

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

1.1 TCP粘包、拆包解决办法

解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

2. UDP协议面向数据报

UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

UDP不存在粘包问题,是由于UDP发送的时候,没有经过Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)。

UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。

UDP面向报文形式, 系统是不会缓冲的, 也不会做优化的, Send的时候, 就会直接Send到网络上, 对方收不收到也不管, 所以这块数据总是能够能一包一包的形式接收到, 而不会出现前一个包跟后一个包都写到缓冲然后一起Send。

基于数据报是指无论应用层交给 UDP 多长的报文,UDP 都照样发送,即一次发送一个报文。

3. TCP、UDP数据包大小的限制-应用程序中我们用到的Data的长度最大是多少

首先要看TCP/IP协议,涉及到四层:链路层,网络层,传输层,应用层。   
其中以太网(Ethernet)的数据帧在链路层
IP包在网络层   
TCP或UDP包在传输层   
TCP或UDP中的数据(Data)在应用层   
它们的关系是 数据帧{IP包{TCP或UDP包{Data}}}

在应用程序中我们用到的Data的长度最大是多少,直接取决于底层的限制。

  1. 不同的 数据链路层,有不同的 MTU。
    在这里插入图片描述

以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。

2.在网络层,因为IP包的首部要占用20字节,所以这的MTU为1500-20=1480; 
3.在传输层,对于UDP包的首部要占用8字节,所以这的MTU为1480-8=1472;

所以,在应用层,你的Data最大长度为1472。当我们的UDP包中的数据多于MTU(1472)时,发送方的IP层需要分片fragmentation进行传输,而在接收方IP层则需要进行数据报重组,由于UDP是不可靠的传输协议,如果分片丢失导致重组失败,将导致UDP数据包被丢弃。

从上面的分析来看,在普通的局域网环境下,UDP的数据最大为1472字节最好(避免分片重组)。

但在网络编程中,Internet中的路由器可能有设置成不同的值(小于默认值),虽然目前大多数的路由设备的MTU都为1500(实际上互联网也普遍以 1500 字节运行)。鉴于Internet上的标准MTU值为576字节,所以建议在进行Internet的UDP编程时.最好将UDP的数据长度控件在548字节(576-8-20)以内。

网络中的所有节点必须同时增大MTU,网络中小MTU的节点遇到上家发来的大于MTU的Frame(且没有切分标记),则直接丢弃。

UDP 包的大小就应该是 1500 - IP头(20) - UDP头(8) = 1472(Bytes) (避免分片重组)
TCP 包的大小就应该是 1500 - IP头(20) - TCP头(20) = 1460 (Bytes)

总结:
我们设定包的大小对于UDP和TCP协议是不同的,关键是看系统性能和网络性能,网络是状态很好的局域网,那么UDP包分大点,提高系统的性能。
总之 ,如果网络不好,包大小最好为1400以下。

4. 总结

TCP粘包是什么? 为什么UDP不粘包?为什么UDP要冗余长度字段?
参考URL: https://zhuanlan.zhihu.com/p/359177898

UDP则是面向消息传输的,是有保护消息边界的,接收方一次只接受一条独立的信息,所以不存在粘包问题。

UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。而接收方在接收数据报的时候,也不会像面对 TCP 无穷无尽的二进制流那样不清楚啥时候能结束。正因为基于数据报和基于字节流的差异,TCP 发送端发 10 次字节流数据,而这时候接收端可以分 100 次去取数据,每次取数据的长度可以根据处理能力作调整;但 UDP 发送端发了 10 次数据报,那接收端就要在 10 次收完,且发了多少,就取多少,确保每次都是一个完整的数据报。

跟 UDP 不同在于,TCP 发送端在发的时候就不保证发的是一个完整的数据报,仅仅看成一连串无结构的字节流,这串字节流在接收端收到时哪怕知道长度也没用,因为它很可能只是某个完整消息的一部分。

粘包出现的根本原因是不确定消息的边界。接收端在面对"无边无际"的二进制流的时候,根本不知道收了多少 01 才算一个消息。一不小心拿多了就说是粘包。

TCP会存在粘包问题,UDP 是基于数据报的传输协议,不会有粘包问题。

三、golang net包 | golang实现udp 服务器

golang提供了网络编程使用net包,使用net包可以进行很基础的socket编程。

由于UDP是“无连接”的,所以,服务器端不需要额外创建监听套接字,只需要指定好IP和port,然后监听该地址,等待客户端与之建立连接,即可通信。

创建监听地址:

func ResolveUDPAddr(network, address string) (*UDPAddr, error) 

创建监听连接:

func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) 

接收udp数据:

func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)

写出数据到udp:

func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

服务端

package main

import (
    "fmt"
    "net"
)

/* UDP 服务端 */
func main() {
    
    
    // 1. 监听
    listener,err := net.ListenUDP("udp",&net.UDPAddr {
    
    
        IP:		net.ParseIP("127.0.0.1"),
        Port:	30000,
    })
    if err != nil {
    
    
        fmt.Println("启动 server 失败,err:",err)
        return
    }
    // 退出时关闭资源
    defer listener.Close()
    // 循环收发数据
    for {
    
    
        var buf [1024]byte
        // 因为是无连接,所以需要知道对方地址 Addr
        n,addr,err := listener.ReadFromUDP(buf[:])
        if err != nil {
    
    
            fmt.Println("接收消息失败,err:",err)
            return
        }
        fmt.Printf("接收到来自 %v 的消息:%v\n",addr,string(buf[:n]))
        // 回复消息
        n,err = listener.WriteToUDP([]byte("hi"),addr)
        if err != nil {
    
    
            fmt.Println("回复失败,err:",err)
            return
        }
    }
}

demo2:

package main

import (
	"fmt"
	"net"
)

/* UDP 服务端 */

func process(listener net.UDPConn) {
    
    
	defer listener.Close()
	// 循环收发数据
	for {
    
    
		var buf [1024]byte
		// 因为是无连接,所以需要知道对方地址 Addr
		n, addr, err := listener.ReadFromUDP(buf[:])
		if err != nil {
    
    
			fmt.Println("接收消息失败,err:", err)
			return
		}
		fmt.Printf("接收到来自 %v 的消息:%v\n", addr, string(buf[:n]))
		// 回复消息
		n, err = listener.WriteToUDP([]byte("hi"), addr)
		if err != nil {
    
    
			fmt.Println("回复失败,err:", err)
			return
		}
	}
}

func main() {
    
    
	// 1. 监听
	listener,err := net.ListenUDP("udp",&net.UDPAddr {
    
    
		IP:		net.ParseIP("127.0.0.1"),
		Port:	30000,
	})
	if err != nil {
    
    
		fmt.Println("启动 server 失败,err:",err)
		return
	}
	process(*listener)
}

Client端

1、net.Dial() 建立连接
2、net.UDPConn.Write() 写数据
3、net.UDPConn.ReadFromUDP() 回复数据

package main

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

func main() {
    
    
	// 连接服务器
	conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
    
    
		IP:   net.IPv4(127, 0, 0, 1),
		Port: 30000,
	})

	if err != nil {
    
    
		log.Println("Connect to udp server failed,err:", err)
		return
	}

	for i := 0; i < 10; i++ {
    
    
		// 发送数据
		_, err := conn.Write([]byte(fmt.Sprintf("udp testing:%v", i)))
		if err != nil {
    
    
			fmt.Printf("Send data failed,err:", err)
			return
		}

		//接收数据
		result := make([]byte, 1024)
		n, remoteAddr, err := conn.ReadFromUDP(result)
		if err != nil {
    
    
			fmt.Printf("Read from udp server failed ,err:", err)
			return
		}
		fmt.Printf("Recived msg from %s, data:%s \n", remoteAddr, string(result[:n]))
	}
}

参考

Golang 学习二十五(UDP 编程)
参考URL: https://blog.csdn.net/shenyuanhaojie/article/details/124501077
golang 网络框架之 UDP篇
参考URL: https://zhuanlan.zhihu.com/p/41572002
golang 简单实现udp服务端和udp客户端
参考URL: https://blog.csdn.net/weixin_44676081/article/details/108966572

猜你喜欢

转载自blog.csdn.net/inthat/article/details/116488747