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 Protocol
IP协议之上.
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用的少, 后面在详细分析如何增加可靠性