yout: post title: 用户数据报协议 (UDP) date: March 17, 2019 3:23 PM excerpt: 之前前述的所有网络编程内容都是以TCP协议为基础的, 本篇我们来分析一下UDP协议, 如何用UDP进行网络编程, 以及其与TCP协议的异同 categories:
- Server tags:
 - Networkprogramming
 - TCP/IP comments: true —
 
UDP
众所周知, 网络协议是分层的, 用户数据报协议(User Datagram Protocol)是传输层的协议. 它构建在Internet ProtocolIP协议之上.
UDP协议事实上只是在IP协议之上添加了端口号和校验和 UDP协议没有差错纠正, 流量控制, 拥塞控制, 仅仅有差错检测可选
因为UDP协议实际上的确是一个很简单的协议 (RFC描述仅仅只有3页, 几十年间也没有特别大的变化) 那么, 我们重点关注UDP这两方面的内容: UDP校验和, UDP数据报长度限制
UDP校验和
同大多数协议(IP, ICMP)一致, UDP使用相同的数学函数计算校验和. 但是, UDP协议计算校验和中要注意的是两点:
- 因为UDP的数据报长度可以是奇数, 但是数据校验和算法是需要偶数字节, 我们会在数据报结尾补上一个虚字节, 
虚字节,的意思就是, 这个字节也仅仅是用来进行校验和的计算, 并不会实际发送出去 - UDP校验和计算, 会包含衍生自
IPv4的首部, 或者是从IPv6的头部, 同理, 这些头部也是虚的, 只是用来进行校验和计算, 并不会实际发送出去 
UDP数据报长度
这里我们提一下数据报的长度. UDP协议实际上是将, 每一段buffer, 包装成为一个数据报, 然后发送出去. 可以说: 对端的读写次数与本端的读写次数是一致的
但是, UDP协议也存在一定的问题: 数据包的长度限制. 理论上一个IPv4数据报的长度是65535个字节, 减去IPv4头部和UDP头部,剩下的字节数为65507字节.
发送数据报的长度
实现上, 我们可以使用Socket API进行限制. 比如: setsockopt(fd, SOL_SOCKET, SO_RECVBUF, &val ,sizeog(val)); 同时, 还需要修改内核缓冲区大小/proc/sys/net/udp_rmem_min, /proc/sys/net/udp_wmrm_min等
接受数据报的长度
对端发送多送多少, 并不意味着本端能够接受多少. 比如现在流量很大, 会导致数据包快速到达, 我们都可以收到吗? 答案是API截断, 丢弃超过缓冲区限制的额外数据. 在Linux上可以通过IO接口的MGS_TRUNC查看有多少数据被截断
IP的分片与重组
提到UDP协议, 其中有数据包的概念. 我们这里就来分析一个概念: TCP分包, 粘包, 这实际上是一个伪概念 因为TCP协议是一个面向连接的字节流协议, 它不像UDP协议有消息边界, TCP协议不具有消息边界. 那么, 所谓的分包, 粘包的概念是从何而来的呢?
一切的罪魁祸首是: IP的分片与重组
根据网络体系结构的知识: (以5层模型为例) 应用层 -> 传输层 -> 网络层 -> 数据链路层 -> 物理层 我们可知: 与IP协议直接对接的就是数据链路层, 链路层对每个帧的大小有一个上限, MTU (最大传输单元) 为了保持与链路层细节的一致和分离, 所以我们引入了IP的分片和重组
IP进行分片的细节是这样的:
- 由转发表查找外开接口的
MTU - 判断
MTU和数据大小 - 若超过
MTU则进行IP分片 
其中分片有这样的细节: IPv4中, 可以在源主机或者端对端的路径上的分片, IPv6只能在源主机分片
IP分片后在网络环境中进行传输, IP分片的重组只能发生在目的主机上. 有这样两个原因:
- 不进行重组要比重组更能减轻网络传输的压力, (这个理由也不是那么让人信服)
 - 各个分片进行传输的网络路径不同, 某个路由器上的信息可能只是数据的子集, 无法进行重组
 
在进行重组时, 我们需要设置重组计时器, 否则会使得对端缓存耗尽, 增加被攻击的机会. 我们从收到第一个分片就开始计时, 收到任何分片也不会重置计时, 如果超时, 就会丢弃拷贝分片, 并返回ICMPv4信息, 告知对端分片丢失
网络编程中使用UDP
之前都是使用TCP协议进行网络编程. 现在我们来看看使用UDP进行网络编程的用法有何异同. 主要的差异集中在UDP的IO函数有些许差异.
  int connect(int sockfd, const struct sockaddr *addr,
                  socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                     struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                   const struct sockaddr *dest_addr, socklen_t addrlen);
这里着重介绍, recvfrom(3)和snedto(2)函数, 这两个函数是支持数据报通信的
flags: 用于设置各种标志MSG_CMSG_CLOEXEC,MSG_TRUNC,MSG_WAITALL等src_addr: 值-结果参数, 用于获取对端信箱, 接收使用dest_addr: 用于指定对端信箱, 发送使用
其他用法与一般的网络编程无异.
我在上面还列出了, connect(2), 有什么用呢? 实际上UDP协议通信也是可以使用connect(2)的.
什么 ? !, UDP不是无连接的协议么 是的, 没错
但是, connect(2)并未发起连接, 只是进行了对端邮箱地址的记录, 这样之后, 就可以直接进行R/W了 当然那是两端都要connect(2).
即是说, 建立起连接之后, 就可以使用read, recv, write, send等IO函数, recvmsg, sendmsg是通用函数, 对于两种协议都是适用的.
recvfrom(fd, buffer, length, flags, nullptr, nullptr)等价于 recv(fd, buffer, len, flags) 所以, 发起连接之后, 我们直接使用R/W函数即可
UDP通信是分为: 未连接的通信, 已连接的通信 对于未连接的通信: 直接使用recvform, sendto进行邮件转发即可 对于已连接的通信: 直接使用R/W函数即可, 因为对端邮箱已经被记录.
实际上, 使用connect(2)之后, 进行邮件投递的效率会更高
谈一谈UDP和TCP的区别
传输层出名的协议就是TCP协议和UDP协议了. 那么, 这两个协议之间有何异同呢?
TCP协议:
- 面向连接
 - 字节流协议
 - 可靠的
 
UDP协议:
- 无连接的
 - 数据报协议
 - 不可靠的
 
-  
连接 TCP进行通信的双方首先要
connect(2),accept(2), 进行三路握手, 四路交换断开连接 反观, UDP是无连接, 直接进行邮箱投递, 即使有connect(2), 也只是进行对端信箱的记录 -  
协议 TCP提供的是字节流协议, 无消息边界. UDP提供的是数据报服务, 有明显地消息边界
 -  
可靠性 这是两种协议很大的区分点 TCP有超时重传, 流量控制, 拥塞控制, 保活机制, 差错纠正 UDP仅仅只有可选的差错检测, 所有的可靠性都需要用户层面进行实现
 
那么, UDP就这么一无是处吗? 非也, 有的需求下, UDP可以取代TCP
结合UDP的特性, 我们可以有以下结论:
- 多播, 广播的需求下,必须使用UDP, 因为TCP是端对端的连接
 - 简单地请求-应答程序可以使用UDP, 因为一个简单地请求-应答, UDP只需要2个分组, TCP需要20个分组 (如: 
HTTP/1.0, 选用TCP有其他方面的考虑) - 海量数据传输不能使用UDP, 因为设计海量数据传输, 必须配合各种保证可靠的机制,
 - 音视频流: 丢包严重得多, 但是TCP的重传代价太高, 一般都采用UDP通信
 - 特殊情况下: 内网服务, 实现简单, 历史包袱.都有可能成为选用UDP的理由
 
给UDP应用增加可靠性
主要是增加: 超时和重传, 序列号 这两个特性在大多数现有的UDP应用程序中都是提供的.
序列号, 就是给每个分组添加, 之后服务器回射就好 超时和重传, 需要采用karn算法进行实现. 因为实在UDP用的少, 后面在详细分析如何增加可靠性