UDP是面向数据报的传输层协议,UDP之间的通信以数据报为界限,每次调用send发送数据,不管数据多小,都会产生一个UDP数据报,然后被组装成一个IP数据报,发送出去。每次调用recv接收数据,都只能接收到一个UDP数据报。所以客户端调用了几次send发送UDP数据报,服务器端就要调用几次recv来接收UDP数据报。

这和TCP不同,TCP是面向字节流的传输层协议,为了避免IP分配MTU vs MSS,调用send发送数据,在数据量大时,会将数据分成几个TCP数据报。也可能在启用了Nagle的情况下,将多次调用send发送的数据,打包成一个TCP数据报,再发送出去。调用recv接收数据时,也和调用send的次数无关。
这种情况下,如果调用两次send发送数据,调用一次recv接收数据时,可能会接收到第一次send发送的全部数据+第二次send发送的部分数据。这是协议正常的行为,谨记,TCP是面向字节流

(如果每次调用send发送一个字节,都要发送一个TCP数据报出去,那么就产生了20(IP Header) + 20(TCP Header) + 1(数据) = 41 byte的分组数据。我们为了传输一个字节,带上了40个字节的头部数据,有效数据比很低,在互联网中,大量的这些小分组会增加阻塞的可能,Nagle算法就是为了解决这些问题而提出。
该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组 , 在该分组的确认到达之前不能发送其他的小分组。相反,TCP收集这些少量的分组,并在确认到来时以 一个分组的方式发出去。可以通过TCP_NODELAY来禁用Nagle算法)

本来只打算记录下自己在使用UDP遇到的一些问题,同时讲了下TCP

UDP的发送

GKCFkF.jpg

UDP头部length字段,指明了udp报文(头部+数据)的最大长度是2^16 - 1 = 65535 byte。但因为UDP报文封装在IP报文中,IP报文(头部+数据)的最大长度是2^16 - 1 = 65535 byte , 所以一个UDP报文数据部分的最大长度是(2^16 - 1)(IP报文的最大长度) - 20(IP头部的长度) - 8(UDP头部的长度) = 65507 byte。

UDP maximum packet size
Why UDP header has 'length field'?

UDP报文的长度还受UDP发送缓冲区大小的限制。

下面是一个UDP客户端程序udp_client.go

package main

import (
 "log"
 "math/rand"
 "net"
 "time"
)

const udpPackageSize = 65507          // udp报文大小
const udpWriteBufferSize = 100 * 1024 // udp发送缓冲区大小

func main() {
 rAddr := net.UDPAddr{
  IP:net.IPv4(127, 0, 0, 1),
  Port:11112,
 }
 conn, err := net.DialUDP("udp", nil, &rAddr)
 if err != nil {
  log.Fatal(err)
 }
 defer conn.Close()

 err = conn.SetWriteBuffer(udpWriteBufferSize)
 if err != nil {
  log.Fatal(err)
 }

 buf := make([]byte, udpPackageSize)
 rand.Seed(time.Now().UnixNano())
 rand.Read(buf)

 _, err = conn.Write(buf)
 if err != nil {
  log.Fatal(err)
 }
}

调整上面的常量udpPackageSizeudpWriteBufferSize,运行程序。可以看到当发送缓冲区大小 >= 65507时,UDP报文的最大长度是65507。当发送缓冲区 < 65507时,UDP报文的最大长度是发送缓冲区的大小。
当UDP报文长度 > 65507,或者UDP报文长度大于发送缓冲区大小,go程序会跑出异常message too long

GYOKXj.jpg

其实UDP套接字并不像TCP套接字UDP套接字其实没有发送缓冲区,任何UDP套接字都有发送缓冲区大小,不过它仅仅是可写到该套接字的UDP报文的大小上限。如果一个应用进程写一个大于套接字发送缓冲区大小的数据报,操作系统内核会返回一个错误。go程序里会提示message too long

为什么UDP不需要发送缓冲区,因为UDP是不可靠的,发送端将UDP报文发送出去后,不需要目的地的确认,发出去就可以了。所以不需要有发送缓冲区,来保存数据的副本。

从写一个UDP套接字的write调用成功返回表示所写的数据报或其所有片段已被加入数据链路层的输出队列。

UDP的接收

UDP套接字也像TCP套接字,使用接收缓冲区来接收数据,直到由应用程序来读取。TCP拥有流量控制机制,可以通过窗口大小,通知发送端自己接收缓冲区的可用空间,发送端可以降低发送的速度。

UDP就不一样,当收到的报文装不进接收缓冲区,就直接丢弃掉。

下面是一个UDP服务器端的程序udp_server.go

package main

import (
 "log"
 "net"
)

const bufferSize = 1024        // 用户空间buf大小
const udpReaderBufferSize = 100 // udp接收缓冲区大小

func server(pc net.PacketConn, addr net.Addr, buf []byte) {
 log.Printf("接收到数据 %v", buf)
}

func main() {
 lAddr := net.UDPAddr{
  IP:net.IPv4(127, 0, 0, 1),
  Port:11112,
 }
 pc, err := net.ListenUDP("udp", &lAddr)
 if err != nil {
  log.Fatal(err)
 }
 defer pc.Close()

 err = pc.SetReadBuffer(udpReaderBufferSize)
 if err != nil {
  log.Fatal(err)
 }

 for {
  log.Println("等待数据")
  for {
   buf := make([]byte, bufferSize)
   n, addr, err := pc.ReadFrom(buf)
   if err != nil {
    continue
   }
   log.Printf("接收到数据长度 %d", n)
   go server(pc, addr, buf)
  }
 }
}

可以看到目前的UDP接收缓存区大小是100 byte,如果使用刚开始的UDP客户端程序发送长度大于100 byte的UDP报文,这个报文会被直接丢弃掉。服务端一直在等待中。

2020/04/02 12:32:59 等待数据

UDP的有界性

调整接收缓冲区大小

const udpReaderBufferSize = 100 * 1024

这样足以接收最长的UDP报文, 使用客户端连续发送100 byte200 byte的两个UDP报文,程序输出

2020/04/02 12:35:18 接收到数据长度 100
2020/04/02 12:35:18 接收到数据长度 200

可以看出UDP之间的通信以数据报为界限,客户端调用多少次send,服务器端就要调用recv来接收。

如果客户端此时发送一个10000 byteUDP报文,但我们程序的用户空间buf大小只有1024 byte,会怎样?

2020/04/02 21:03:18 等待数据
2020/04/02 21:03:22 接收到数据长度 1024
2020/04/02 21:03:22 接收到数据 [40 69 155 134 82 160 .....]

可以看到UDP的接收还是以数据报为界限,应用程序从UDP接收缓冲区拿到10000 byteUDP报文, 但因为空间buf只有1024 byte, 后面的8976 byte就被丢弃了。

UDP的无序性和非可靠性

UDP客户端发送出去的数据报,是无序的,也是不可靠的,UDP客户端发送1,2,3三个数据报,UDP服务器端可能会先接收到3,然后才是1,2,在现实的网络中,接收到顺序有多种可能性。同时UDP也没有重发机制,UDP客户端将数据报发送出去后,就不管了,UDP服务器端可能会丢失数据报,甚至还可能会出现重复的数据报。

Last modification:April 2nd, 2020 at 09:33 pm
如果觉得我的文章对你有用,请尽情赞赏 🐶