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的发送
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)
}
}
调整上面的常量udpPackageSize
和udpWriteBufferSize
,运行程序。可以看到当发送缓冲区大小 >= 65507时,UDP报文的最大长度是65507。当发送缓冲区 < 65507时,UDP报文的最大长度是发送缓冲区的大小。
当UDP报文长度 > 65507,或者UDP报文长度大于发送缓冲区大小,go程序会跑出异常message too long
。
其实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 byte
和200 byte
的两个UDP报文
,程序输出
2020/04/02 12:35:18 接收到数据长度 100
2020/04/02 12:35:18 接收到数据长度 200
可以看出UDP之间的通信以数据报为界限,客户端调用多少次send
,服务器端就要调用recv
来接收。
如果客户端此时发送一个10000 byte
的UDP报文
,但我们程序的用户空间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 byte
的UDP报文
, 但因为空间buf只有1024 byte
, 后面的8976 byte就被丢弃了。
UDP的无序性和非可靠性
UDP客户端发送出去的数据报,是无序的,也是不可靠的,UDP客户端发送1,2,3三个数据报,UDP服务器端可能会先接收到3,然后才是1,2,在现实的网络中,接收到顺序有多种可能性。同时UDP也没有重发机制,UDP客户端将数据报发送出去后,就不管了,UDP服务器端可能会丢失数据报,甚至还可能会出现重复的数据报。