• Home
  • About
    • Road to Coding photo

      Road to Coding

      只要那一抹笑容尚存, 我便心无旁骛

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags

Udp

08 Mar 2019

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协议计算校验和中要注意的是两点:

  1. 因为UDP的数据报长度可以是奇数, 但是数据校验和算法是需要偶数字节, 我们会在数据报结尾补上一个虚字节, 虚字节,的意思就是, 这个字节也仅仅是用来进行校验和的计算, 并不会实际发送出去
  2. 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分片的重组只能发生在目的主机上. 有这样两个原因:

  1. 不进行重组要比重组更能减轻网络传输的压力, (这个理由也不是那么让人信服)
  2. 各个分片进行传输的网络路径不同, 某个路由器上的信息可能只是数据的子集, 无法进行重组

在进行重组时, 我们需要设置重组计时器, 否则会使得对端缓存耗尽, 增加被攻击的机会. 我们从收到第一个分片就开始计时, 收到任何分片也不会重置计时, 如果超时, 就会丢弃拷贝分片, 并返回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协议:

  • 无连接的
  • 数据报协议
  • 不可靠的
  1. 连接 TCP进行通信的双方首先要connect(2), accept(2), 进行三路握手, 四路交换断开连接 反观, UDP是无连接, 直接进行邮箱投递, 即使有connect(2), 也只是进行对端信箱的记录

  2. 协议 TCP提供的是字节流协议, 无消息边界. UDP提供的是数据报服务, 有明显地消息边界

  3. 可靠性 这是两种协议很大的区分点 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用的少, 后面在详细分析如何增加可靠性



Share Tweet +1