• 【lwip】11-UDP协议&源码分析


    前言

    主要分析源码实现。

    源码部分,本章节也只分析协议实现部分和最原始的南北接口。

    北向协议栈接口和套接字接口的封装后面有独立章节分析。

    即是UDP RAW接口。

    友链:

    11.1 传输层说明

    IP协议只能完成数据报在互联网主机之间的传递,交付。

    而传输层主要负责向两个主机中进程之间的通信提供服务。

    传输层的几个重要的任务:

    1. 为两个通信的进程提供连接机制。即是传输层需要识别两个正在通信的进程。
    2. 传输层应该提供数据传输服务。在数据发送端,传输层将用户数据进行组装递交给IP层发送出去。在接收端,等待属于同一应用程序的所有数据单元到达,然后解析转交给用户。
    3. 为了提供可靠的传输服务,传输层还可以提供流量控制、数据确认等,保证两个主机的应用程序之间数据的有效性。

    11.2 UDP协议简介

    UDP 是 User Datagram Protocol 的简称,中文名是用户数据报协议。

    11.3 UDP特点

    UDP特点:

    1. 无连接、不可靠。
    2. 尽可能提供交付数据服务,出现差错直接丢弃,无反馈。
    3. 面向报文,发送方的 UDP 拿到上层数据直接添加个 UDP 首部,然后进行校验后就递交给IP 层,而接收的一方在接收到 UDP 报文后简单进行校验,然后直接去除数据递交给上层应用。
    4. 支持一对一,一对多,多对一,多对多的交互通信。
    5. 速度快,UDP 没有 TCP 的握手、确认、窗口、重传、拥塞控制等机制,UDP 是一个无状态的传输协议,所以它在传递数据时非常快,即使在网络拥塞的时候 UDP 也不会降低发送的数据。

    11.4 UDP端口号

    UDP 报文协议根据对应的端口号传递到目标主机的应用线程的。

    传输层到应用层的唯一标识是通过端口号决定的,两个线程之间进行通信必须用端口号进行识别。

    使用“IP 地址 + 端口号”来区分主机不同的线程。

    范围:[0,65535],因为只有2字节:

    1. 端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用[1,1023]之间的端口号,是由ICANN来管理的;
    2. 端口号从[1024, 49151]是被注册的端口,也成为“用户端口”,被IANA指定为特殊服务使用。
    3. 大多数TCP/IP实现给临时端口号分配[1024, 5000]之间的端口号。
    4. 大于5000的端口号是为其他服务器预留的。
    5. 客户端只需保证该端口号在本机上是唯一的就可以了。客户端端口号因存在时间很短暂又称临时端口号

    常见的UDP协议端口号:




    说明
    0 保留
    7 echo 报文回送服务器端口
    53 DNS 域名服务器端口
    69 TFT 中小型文件传输协议端口
    123 NTP 网络时问协议端口
    是用来同步网络中各个计算机时问的协议
    161 SNMP 简单网络管理协议端口

    11.5 UDP报文

    UDP 报文也被称为用户数据报。

    UDP报文封装:

    • UDP的数据区就是用户程序的数据了。

    UDP报文格式:

    源端口号:用户发送数据的进程锁绑定的本地端口号。

    • 用户进程调用UDP相关业务时,可以不配置源端口号。若不配置,内部会自动适配一个临时端口号。

    目的端口号:远端主机用户进程接收数据绑定的端口号。

    总长度:是UDP数据报的总长度:UDP首部+UDP数据区。

    • 这个字段有点冗余,因为在IP报文中就包含了IP首部和IP总长度,这样能计算出UDP数据报的长度。所以,UDP LITE就把这个字段改为需要进行校验和的UDP报文数据长度(从UDP首部算起)。
    • 对于UDP LITE协议,这个字段为0时,表示对整个UDP报文进行校验和计算。参考RFC 3828 chap. 3.1
    • 对于UDP LITE协议,这个字段要么为0,要么不少于UDP报文首部长度(即是校验和至少要涵盖UDP首部)。参考RFC 3828 chap. 3.1

    检验和:UDP协议为UDP伪首部+UDP首部+UDP数据区所有数据都加入校验和。UDP LITE协议为“总长度”指定的长度加入校验和,从UDP伪首部算起,再加上伪首部校验和。

    • 填入0时,表示不进行校验和。而在实际计算校验和得到的结果刚好为0时,则向校验和字段填入0FFFF。

      • 填入0XFFFF的可行性证明:如果校验和结果为0,即是其它数据的和为0XFFFF。而对端继续校验和时,就是0XFFFF+0XFFFF,结果还是0XFFFF。

    UDP LITE:UDP协议的校验和是UDP首部和UDP数据区,如果数据区很多数据,一个校验失败就丢弃了,代价有点大,所以衍生出UDP LITE。只校验UDP报文前面指定数据长度的数据。一般用于实时适配、实时通话等这些要求通信速度快,可靠性要求不高的业务中。

    11.6 UDP伪首部和校验和

    UDP校验和的计算包括了三部分:UDP伪首部+UDP首部+UDP数据区。

    UDP伪首部包含IP首部一些字段。其目的是让UDP验证数据是否已经正确到达目的地。

    UDP伪首部只参与校验,不参与实际发送。

    伪首部中UDP总长度和UDP首部的总长度字段一致。

    11.7 wireshark报文分析

    11.8 UDP数据结构

    参考udp.cudp.h文件

    11.8.1 UDP首部

    UDP首部长度:

    UDP首部数据结构:

    11.9 UDP控制块

    UDP控制块是整个UDP协议实现的核心部分。

    LWIP使用UDP控制块来描述一个UDP连接的所有相关信息,包括源端口号、目的端口号、源IP、目的IP等等。

    LWIP为每个UDP连接都分配一个UDP控制块,并用链表udp_pcbs链起来。

    但是LWIP也给UDP控制块数量设限制,MEMP_NUM_UDP_PCB为UDP控制块的内存池数量。该宏缺省为8。

    UDP控制块数据结构:

    11.10 端口号相关

    11.10.1 端口号范围

    11.10.2 端口号初始值

    UDP的端口号由全局值udp_port累加管理。

    其初始值有两次初始:第一次是变量赋值,第二次是调用udp_init()进行随机初始。

    变量初始值:

    随机初始化:

    • 需要开启LWIP随机宏LWIP_RAND

    11.10.3 udp_new_port()端口号申请

    端口号申请是有udp_port进行累加,溢出就复位到UDP_LOCAL_PORT_RANGE_START

    11.11 UDP控制块操作函数

    UDP控制块的操作函数相对简单,因为没有流量控制、没有确认机制等等。

    11.11.1 udp_new():新建UDP控制块

    udp_new()

    • MEMP_UDP_PCB内存池中获取UDP控制块资源。
    • 初始化部分字段。

    11.11.2 udp_remove():删除UDP控制块

    udp_remove()

    • struct udp_pcb *pcb:需要删除的UDP控制块。

    11.11.3 udp_bind():绑定控制块

    当UDP服务于应用程序时,数据流需要底层和应用层进行对接,就需要把UDP控制块绑定到本地IP和端口号。

    绑定控制块时需要注意的是:

    1. 检查是否有PCB已经绑定了当前IP和端口号。
    2. 当前PCB有没有已经插入了udp_pcbs链表。

    小笔记:在没有设置SOF_REUSEADDR选项功能时,需要确保一个UDP报文最多只能到达一个应用程序。即是一个网络接口中的一个端口号。需要注意的是任意IP。

    udp_bind()

    • struct udp_pcb *pcb:需要绑定本地IP和端口号的UDP控制块。

    • ip_addr_t *ipaddr:UDP控制块需要绑定的本地IP地址。

      • 如果为NULL,则绑定本地IP为全0的IP。即表示本地任意IP都可。
      • 如果不为空,则绑定指定的本地IP。
    • u16_t port:UDP控制块需要绑定的本地端口号。

      • 如果为0,则绑定由内部调用udp_new_port()随机生成端口号。
      • 如果不为0,则绑定指定的端口号。
    • 先检查下当前UDP控制块有没有插入了udp_pcbs链表,因为绑定成功后,需要插入该链表。已经插入了,就不需要重复操作。

    • 检查绑定的IP地址。传入为空,则赋值为全0的IP地址。

    • 检查绑定的端口号。

      • 如果为0,则调用udp_new_port()生成一个并绑定。

      • 如果不为0,则遍历udp_pcbs链表,判断是否有其它UDP控制块重复使用这个端口号。确保一个UDP报文最多只有一个应用程序去向。相同条件:端口号相同且IP报文能到达这个服务。IP报文能否到达这个服务,可以通过以下判断(其一即符合要求):重复端口号的UDP控制块绑定的IP对比当前UDP控制块需要绑定的IP。

        1. 两个IP一致。
        2. 任一IP为全0。(出现万能IP)
        3. 如果开启了SO_REUSE且两个UDP控制块都配置了SO_REUSEADDR功能,则不用对比了,直接支持复用。
      • 这里需要注意是否开启SO_REUSEADDR选项,即是立即启用端口号的功能。由宏SO_REUSE决定有没有这个功能,用户在代码中设置SO_REUSEADDR是否开启该功能。

    • 把需要绑定的IP和端口号填入UDP控制块。绑定成功。

    • 确保当前UDP控制块插入了udp_pcbs链表。

    11.11.4 udp_bind_netif():绑定网卡

    udp控制块也可以绑定指定网卡。

    udp_bind_netif()

    • udp_pcb *pcb:需要绑定网卡的UDP控制块。

    • struct netif *netif:UDP控制块需要绑定的网卡。

      • 为NULL时,表示解绑。
    • 获取网卡索引,绑定到UDP控制块。

    11.11.5 udp_connect():连接控制

    (本地行为)

    UDP协议是没有连接状态的,但是为什么UDP可以调用udp_connect()这个函数?有什么用?

    • 调用这个是为了这个UDP控制块本地长期绑定一个远端IP和端口号,减少后面重复绑定和解绑的步骤。

    先了解下UDP sendto() 函数传输数据过程 :

    1. 第 1 阶段:向 UDP 控制块注册目标 IP 和端口号。
    2. 第 2 阶段:传输数据。
    3. 第 3 阶段:删除 UDP 控制块中注册的目标地址信息。

    如果需要频繁发送,那第一阶段和第三阶段是重复多余的,所以可以使用 已连接(connect)UDP 控制块。

    所以udp_connect()这个函数的目的是把UDP控制块注册长期目标IP和端口号,这样中途调用发送函数时,不需要重新注册和注销。

    可以使用udp_disconnect()进行注销。

    udp_connect()

    • struct udp_pcb *pcb:需要连接的UDP控制块。
    • ip_addr_t *ipaddr:远端IP地址。
    • u16_t port:远端端口号。
    • 先检查有没有绑定了本地应用程序:即是UDP控制块有没有绑定了本地IP(包括任意IP)和本地端口号。还没有绑定,则调用udp_bind()进行绑定。
    • 注册远端IP和远端端口号。
    • 标记当前UDP控制块状态为已连接状态。
    • 确保当前UDP控制块已激活:即是是否插入了udp_pcbs链表。还没插入就需要插入处理。

    11.11.6 udp_disconnect():断开连接

    (本地行为)

    就是本地注销远端IP和远端端口号的绑定。

    udp_disconnect()

    • 重置UDP控制块远端IP和远端端口号字段。
    • 解绑本地网卡。
    • 标记UDP控制块为未连接。

    11.11.7 udp_recv():控制块注册接收函数

    udp_recv()只是用于UDP控制块注册接收函数。

    11.11.8 udp_netif_ip_addr_changed():更新UDP控制块本地IP

    当底层网卡在IP层的IP有所更新时,需要把UDP控制块中本地IP绑定就的IP也更新。

    即是当IP地址改变时,将从netif.c调用此函数检查并更新。

    udp_netif_ip_addr_changed()

    • ip_addr_t *old_addr:旧IP。
    • ip_addr_t *new_addr:新IP。
    • 检索LWIP中UDP控制块链表udp_pcbs,把绑定就的IP更新到新的IP去。

    11.12 UDP发送数据

    注意校验和相关宏:

    • LWIP_CHECKSUM_ON_COPY:在支持使用数据区已经计算好的UDP数据区校验和。
    • CHECKSUM_GEN_UDP:在软件中生成出UDP数据包的校验和。

    在分析前先说明需要分析的几个函数的关系:

    • udp_send():UDP RAW的接口,需要的参数只需要UDP和用户数据即可。

    • udp_sendto():UDP RAW的接口,对比上面函数,可以指定远端IP和与远端端口号。

    • udp_sendto_if():UDP RAW的接口,对比上面udp_sendto()函数,该函数还能指定网卡。

    • udp_sendto_if_src():UDP RAW的接口,也是UDP发送数据的基函数,是实现组装UDP包,和转交到IP层的接口函数。上面的函数都是必须经过该函数实现的。

      • 主要分析该函数。其它函数看看就好了。

    11.12.1 udp_sendto_if_src():UDP发送数据基函数

    先分析UDP发送数据基函数,即是组装UDP报文的函数。

    然后再分析其它封装这个基函数的相关API。

    udp_sendto_if_src()

    • struct udp_pcb *pcb:负责本次数据交互的UDP控制块。
    • struct pbuf *p:需要发送的数据的pbuf。
    • ip_addr_t *dst_ip:远端IP。
    • u16_t dst_port:远端端口号。
    • struct netif *netif:本地网卡,即是发送本次UDP报文的网络接口。
    • ip_addr_t *src_ip:源IP地址。
    • 检查传入的参数是否异常。
    • 检查当前UDP控制块有没有绑定了本地IP(包括任意IP)和本地端口号。还没绑定就需要调用udp_bind()进行绑定。
    • 预测检查UDP报文长度:UDP数据区追加UDP首部后是否溢出,溢出丢弃。
    • 检查pbuf长度能否扩充到链路层报文:不能就申请多一个pbuf q,包含么UDP首部+IP首部+链路层首部,然后拼接到当前pbuf,让其拥有足够空间。
    • 填充UDP报文首部的几个字段。
    • 其中,UDP首部的长度字段和校验和需要按协议类型和相关宏处理。
    • 最后把UDP报文转交给IP层:调用ip_output_if_src()转发出去。

    11.12.2 udp_send():UDP发送数据函数

    从用户的角度看,用户前期配置好UDP控制块后,后面发送数据只需要提供两个参数:UDP控制块和需要发送的数据即可。

    所以就有了udp_send()函数,该函数的实现是层层封装UDP发送数据的基函数udp_sendto_if_src()实现的:udp_send() --> udp_sendto() --> udp_sendto_if() --> udp_sendto_if_src()

    udp_send():没有指定远端IP和端口号则使用这个UDP PCB中的。

    • udp_pcb *pcb:负责本次发送的UDP控制块。
    • struct pbuf *p:需要发送的UDP数据。

    udp_sendto():指定远端IP和远端端口号的UDP发送。

    • struct udp_pcb *pcb:负责本次发送的UDP控制块。

    • struct pbuf *p:需要发送的数据的pbuf。

    • ip_addr_t *dst_ip:远端IP地址。

    • u16_t dst_port:远端端口号地址。

    • 传入参数校验。

    • 还需要指定本地网卡:

      • 如果UDP控制块已经绑定了本地网卡,则直接调用该网卡即可。
      • 否则,需要根据远端IP地址,用ip4_route_src()去路由匹配。这个匹配逻辑可以参考前面IP章节。
    • 然后调用udp_sendto_if()发送出去。

    udp_sendto_if():确定UDP本地IP地址,然后调用udp_sendto_if_src()UDP发送数据基函数进行组包。

    • struct udp_pcb *pcb:负责本次发送的UDP控制块。

    • struct pbuf *p:需要发送的数据的pbuf。

    • ip_addr_t *dst_ip:远端IP地址。

    • u16_t dst_port:远端端口号地址。

    • struct netif *netif:指定发送UDP报文的网卡。

    • 参数校验。

    • 确定本地IP:

      • 如果UDP控制块没有指定本地IP,则获取指定的网卡的IP作为UDP本地IP。
      • 如果UDP控制块指定了本地IP,则这个IP必须和指定网卡的IP一致,否则不发送。

    11.13 UDP接收数据

    UDP接收处理数据,南向是通过udp_input()API给IP层收到UDP数据报后上交到UDP协议处理。

    udp_input()

    • struct pbuf *p:收到UDP报文的pbuf。

    • struct netif *inp:收到该UDP报文的网卡。

    • 参数校验。

    • 报文校验。

    • 匹配UDP PCB:通过IP和端口号确保该UDP报文得到某个应用程序。遍历UDP PCB udp_pcbs

      • UDP PCB 本地端口、IP和UDP报文目的端口和IP匹配:端口一致且IP匹配:

        • 当前UDP PCB没有指定本地IP,或UDP报文的目的IP就是指向当前UDP PCB的IP。本地可以匹配成功。
        • 如果UDP报文对应的目的IP是一个广播地址,且当前UDP设置了SOF_BROADCAST选项。这个IP是全广播地址或者和当前UDP PCB IP处于同一个子网。本地可以匹配成功。
        • 如果UDP PCB 本地端口、IP和UDP报文目的端口和IP匹配成功后,但是该UDP PCB还没有处于连接状态,则可以记录到uncon_pcb变量中,有更适合且未连接的UDP PCB适配本次UDP报文的,更新到uncon_pcb中。
      • UDP PCB 远端端口、IP和UDP报文源端口和IP匹配:端口一致且IP匹配:

        • UDP PCB远端IP随意或者就是当前UDP报文的源IP。远端匹配成功。
        • 如果UDP PCB 远端端口、IP和UDP报文源端口和IP匹配失败,则可以使用UDP PCB 本地端口、IP和UDP报文目的端口和IP匹配但是未连接的UDP PCBuncon_pcb
      • 上述都匹配成功后,UDP PCB即可匹配成功,当前 UDP 报文是给我们的。

    • 校验和校验:

      • UDP协议:校验和字段为0,不用校验。校验和字段不为0,则全部校验。
      • UDP LITE协议:UDP报文的总长度字段值即为需要进行校验和计算的数据长度。注意:长度字段为0表示整个报文校验。(参考RFC 3828章3.1)
    • pbuf偏移头部,指向UDP数据区。即是用户数据。

    • 如果开启了SOF_REUSEADDR选项:则把当前UDP PCB包复制转发到所有能匹配成功的UDP PCB。

      • 如果没有开启该选项,当前UDP报文就只递交给第一个匹配成功的DUP PCB了。
    • 把数据回调到上层应用:pcb->recv()

    11.14 UDP RAW接口编程

    UDP层初始化接口:

    南向供IP层使用:

    UDP RAW相关接口分析:北向,供用户使用

    11.15 个人博客


    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16880148.html
  • 关于博主: 嵌入式从业者。RTOS、Linux lwip mbedtls...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    世界连续动作预测模型-方向模型
    Sentinel底层原理(下)
    健身房小程序开发|健身房小程序运营介绍
    【开发篇】三、web下单元测试与mock数据
    一文了解Spring Boot启动类SpringApplication
    梳理RWKV 4,5(Eagle),6(Finch)架构的区别以及个人理解和建议
    k8s 中的 Pod 细节了解
    MyBatis标签之Select resultType和resultMap
    高精度随流检测技术助力金融行业实现智能运维
    Go常用命令与基础语法
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16880148.html