轻识Logo
目录

    UDP协议源码分析

    UDP协议User Datagram Protocol 的简称, 中文名是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,位于 TCP/IP协议 模型的 传输层,如下图:


    也就是说 UDP协议 是建立中 IP协议(网络层)之上的,IP协议 用于区分网络上不同的主机( IP协议源码分析),而 UDP协议 用于区分同一台主机上不同的进程发送(接收)的网络数据,如下图所示:


    从上图可以看出,UDP协议 通过 端口号 来区分不同进程的数据包。

    UDP协议头

    下面我们来看看 UDP协议 的协议头部,如下图所示:

    从上图可知,UDP头部 由四个字段组成:源端口目标端口数据包长度 和 校验和

    源端口 用于指示本机的进程,而 目标端口 用于指示远端的进程。数据包长度 表示这个 UDP 数据包总长度(包括UDP头部和 数据长度),而 校验和 用于校验数据包在传输的过程中是否损坏了。

    下面我们看看 UDP头部 在内核中的表示方式,如下代码:

    struct udphdr {    __u16   source;  // 源端口    __u16   dest;    // 目标端口    __u16   len;     // 数据包长度    __u16   check;   // 校验和};

    可以看出,udphdr 结构的字段与 UDP头部 结构图中的字段一一对应。最后,我们来看看 UDP头部 在数据包的具体位置,如下图:

    下面我们主要通过 UDP 数据包的发送和接收两个过程来分析 UDP 在内核中的实现原理。

    UDP数据包发送

    数据的发送是由应用层调用 send() 或者 write() 系统调用,将数据传递到传输层协议处理,如下图:

    从上图可以看出,用户态 的应用程序调用 send() 系统调用时会触发调用 内核态 的 sys_send() 内核函数,而 sys_send() 最终会调用 inet_sendmsg() 函数发送数据。

    inet_sendmsg() 函数会根据用户使用的传输层协议选择不同的数据发送接口,比如 UDP 协议就会使用 udp_sendmsg() 函数发送数据。

    我们来分析一下 UDP 协议的发送接口 udp_sendmsg() 函数的实现,代码如下(由于 udp_sendmsg() 函数的实现比较复杂,所以我们分段分析):

    int udp_sendmsg(struct sock *sk, struct msghdr *msg, int len){    int ulen = len + sizeof(struct udphdr);    struct ipcm_cookie ipc;    struct udpfakehdr ufh;    struct rtable *rt = NULL;    int free = 0;    int connected = 0;    u32 daddr;    u8  tos;    int err;

    udp_sendmsg() 函数的参数含义如下:

    • sk:Socket 对象。

    • msg:要发送的数据实体,其类型为 msghdr 结构。

    • len:要发送的数据长度。

    上面的代码主要定义了一些局部变量,如:

    • ulen 变量就是要发送的数据总长度(UDP头部长度和数据长度之和)。

    • rt 变量表示数据传输的路由信息,其类型为 rtable 结构。

    • ufh 变量是调用 IP 层 ip_build_xmit() 函数时的上下文,主要用于构建 UDP协议头部 ,其类型为 udpfakehdr 结构。

    我们接口分析 udp_sendmsg() 函数:

        // 是否提供了接收数据的目标IP地址和端口    if (msg->msg_name) {        // 接收数据的目标IP地址和端口        struct sockaddr_in *usin = (struct sockaddr_in*)msg->msg_name;
    if (msg->msg_namelen < sizeof(*usin)) return -EINVAL;
    if (usin->sin_family != AF_INET) { if (usin->sin_family != AF_UNSPEC) return -EINVAL; }
    // 设置接收数据的目标IP地址和端口 ufh.daddr = usin->sin_addr.s_addr; ufh.uh.dest = usin->sin_port;
    if (ufh.uh.dest == 0) return -EINVAL; } else { if (sk->state != TCP_ESTABLISHED) return -ENOTCONN;
    ufh.daddr = sk->daddr; // 使用绑定Socket的IP地址 ufh.uh.dest = sk->dport; // 使用绑定Socket的端口 connected = 1; }

    上面的代码主要完成以下几个工作:

    • 如果用户发送数据时提供了目标 IP 地址和端口,就把用户提供的目标 IP 地址和端口复制到 ufh 变量中。

    • 否则就把绑定到 Socket 对象的目标 IP 地址和端口复制到 ufh 变量中,并且设置 connected 变量为 1。

    我们继续分析 udp_sendmsg() 函数,代码如下:

        if (connected)        rt = (struct rtable*)sk_dst_check(sk, 0); // 获取路由信息对象缓存
    if (rt == NULL) { // 如果路由信息对象还没被缓存 // 调用 ip_route_output() 函数获取路由信息对象 err = ip_route_output(&rt, daddr, ufh.saddr, tos, ipc.oif); if (err) goto out;
    err = -EACCES; if (rt->rt_flags & RTCF_BROADCAST && !sk->broadcast) goto out;
    if (connected) sk_dst_set(sk, dst_clone(&rt->u.dst)); // 设置路由信息对象缓存 }

    上面的代码比较简单,首先调用 sk_dst_check() 查看 路由信息对象 是否被缓存,如果已经缓存,那么直接使用此 路由信息对象。否则调用 ip_route_output() 函数获取 路由信息对象,并且调用 sk_dst_set() 设置 路由信息对象 缓存。

    路由信息对象 指明数据在传送过程的 下一跳 主机的信息(通常为网关),有了 下一跳 主机的信息,就可以把数据转发给 下一跳 主机,然后由 下一跳 主机继续完成发送工作。

    我们继续分析 udp_sendmsg() 函数,代码如下:

        ufh.saddr = rt->rt_src; // 设置源IP地址    if (!ipc.addr) // 如果没有提供目标IP地址,使用路由信息的目标IP地址        ufh.daddr = ipc.addr = rt->rt_dst;    ufh.uh.len = htons(ulen);    ufh.uh.check = 0;    ufh.iov = msg->msg_iov;    ufh.wcheck = 0;
    // 构建MAC头部、IP头部和UDP头部并且下发给IP协议层 err = ip_build_xmit(sk, (sk->no_check == UDP_CSUM_NOXMIT ? udp_getfrag_nosum : udp_getfrag), &ufh, ulen, &ipc, rt, msg->msg_flags);
    out: ip_rt_put(rt); ... return err;}

    上面的代码主要把 路由信息对象 的源IP地址复制到 ufh 变量中,然后调用 ip_build_xmit() 函数完成数据发送的后续工作。ip_build_xmit() 函数的第一个参数用于复制 UDP头部 和负载数据到数据包的函数指针,IP 层通过调用此函数把 UDP头部 和负载数据复制到数据包中。

    ip_build_xmit() 函数是 IP 协议层的实现,这里就不作说明,可以参考此文章: IP协议源码分析。

    总的来说,udp_sendmsg() 函数的主要工作就是为要发送的数据包构建 UDP头部,然后把数据包交由 IP 层完成接下来的发送操作,所以 UDP协议 的发送过程比较简单。

    UDP数据包接收

    当网卡设备接收到数据包后,会交由内核协议栈处理。内核协议栈对数据包的处理是由下至上,如下图所示:

    也就是说,物理层处理完数据包后会交由链路层处理,而链路层处理完交由网络层处理,以此类推。

    所以当网络层(IP协议)处理完数据包后,会交由传输层处理,在本文中介绍的传输层协议是 UDP协议,所以这里主要介绍的是 UDP协议 对数据包的处理过程。

    当 IP 协议层处理完数据包后,如果 IP 头部的上层协议字段(protocol 字段)指明的是 UDP协议,那么就会调用 udp_rcv() 函数处理数据包。下面我们来分析一下 udp_rcv() 函数的实现,代码如下:

    int udp_rcv(struct sk_buff *skb, unsigned short len){    struct sock *sk;    struct udphdr *uh;    unsigned short ulen;    struct rtable *rt = (struct rtable*)skb->dst; // 路由信息对象    u32 saddr = skb->nh.iph->saddr; // 远端IP地址(源IP地址)    u32 daddr = skb->nh.iph->daddr; // 本地IP地址(目标IP地址)
    uh = skb->h.uh; // UDP头部 ... // 根据目标端口获取对用的 Socket 对象 sk = udp_v4_lookup(saddr, uh->source, daddr, uh->dest, skb->dev->ifindex); if (sk != NULL) { udp_queue_rcv_skb(sk, skb); // 把数据包添加到Socket对象的receive_queue队列中 sock_put(sk); return 0; } ...}

    udp_rcv() 函数主要完成两个工作:

    • 调用 udp_v4_lookup() 函数获取目标端口对应的 Socket 对象。

    • 调用 udp_queue_rcv_skb() 函数把数据包添加到 Socket 对象的 receive_queue 队列中。

    UDP协议 使用了一个名为 udp_hash 的哈希表来保存所有绑定了端口的 Socket 对象,当应用程序调用 bind() 系统调用为 Socket 对象绑定端口时,就会将此 Socket 对象添加到 udp_hash 哈希表中。

    而 udp_v4_lookup() 函数就是根据目标端口从 udp_hash 哈希表中获取对应的 Socket 对象,udp_v4_lookup() 函数实现如下:

    __inline__ struct sock *udp_v4_lookup(u32 saddr, u16 sport, u32 daddr, u16 dport, int dif){    struct sock *sk;
    read_lock(&udp_hash_lock); // 为udp_hash哈希表上锁 sk = udp_v4_lookup_longway(saddr, sport, daddr, dport, dif); if (sk) sock_hold(sk); read_unlock(&udp_hash_lock); return sk;}

    udp_v4_lookup() 函数首先为 udp_hash 哈希表上锁,然后调用 udp_v4_lookup_longway() 函数从 udp_hash 哈希表中获取对应目标端口的 Socket 对象,udp_v4_lookup_longway() 函数的实现如下:

    struct sock *udp_v4_lookup_longway(u32 saddr, // 源IP地址(远端IP地址)                      u16 sport, // 源端口(远端端口)                      u32 daddr, // 目标IP地址(本地IP地址)                      u16 dport, // 目标端口(本地端口)                      int dif){    struct sock *sk, *result = NULL;    unsigned short hnum = ntohs(dport);    int badness = -1;
    // 根据目标端口从 udp_hash 哈希表中获取对应的 Socket 对象 for (sk = udp_hash[hnum&(UDP_HTABLE_SIZE - 1)]; sk != NULL; sk = sk->next) { if (sk->num == hnum) { // 对比目标端口是否匹配 int score = 0;
    if (sk->rcv_saddr) { // 如果Socket设置了固定的本地接收IP if(sk->rcv_saddr != daddr) // 对比目标IP地址是否匹配 continue; score++; }
    if (sk->daddr) { // 如果Socket设置了固定的远端接收IP if(sk->daddr != saddr) // 对比源IP地址是否匹配 continue; score++; }
    if (sk->dport) { // 如果Socket设置了固定的远端接收端口 if(sk->dport != sport) // 对比源端口是否匹配 continue; score++; }
    if (sk->bound_dev_if) { // 如果Socket设置了固定的接收网络设备 if(sk->bound_dev_if != dif) // 对比接收设备是否匹配 continue; score++; }
    if (score == 4) { // 完美匹配, 那么直接返回即可 result = sk; break; } else if(score > badness) { // 否则使用分数最高的Socket对象 result = sk; badness = score; } } }
    return result;}

    udp_v4_lookup_longway() 函数的主要逻辑就是根据目标端口从 udp_hash 哈希表中获取对应的 Socket 对象。

    由于同一个端口有可能绑定了多个 Socket 对象,所以 udp_v4_lookup_longway() 函数查找 Socket 对象时使用了最优匹配,也就是说除了匹配目标端口外,还可能会匹配源 IP 地址、源端口和目标 IP 地址等。

    找到目标端口对应的 Socket 对象后,就可以调用 udp_queue_rcv_skb() 函数把数据包添加到 Socket 对象的 receive_queue 队列中,udp_queue_rcv_skb() 函数实现如下:

    static int udp_queue_rcv_skb(struct sock * sk, struct sk_buff *skb){    ...    if (sock_queue_rcv_skb(sk, skb) < 0) {        ...        return -1;    }    return 0;}

    从上面代码可以看出,udp_queue_rcv_skb() 函数最终会调用 sock_queue_rcv_skb() 函数完成任务,所以我们来分析一下 sock_queue_rcv_skb() 函数的实现,代码如下:

    static inline int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){    ...    // 把数据包添加到Socket对象的receive_queue队列    skb_queue_tail(&sk->receive_queue, skb);    if (!sk->dead)        sk->data_ready(sk, skb->len); // 唤醒等待Socket对象就绪的进程    return 0;}

    sock_queue_rcv_skb() 通过调用 skb_queue_tail() 函数把 skb 数据包添加到 Socket 对象的 receive_queue 队列中,并且唤醒等待 Socket 对象就绪的进程。

    当把数据包添加到 Socket 对象的 receive_queue 队列后,UDP协议 的接收工作就此完毕。


    浏览 20
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    举报
    UDP协议详解
    嵌入式Linux
    0
    为什么HTTP3.0使用UDP协议?
    码农沉思录
    0
    为什么 DNS 使用 UDP 协议
    k8s技术圈
    0
    为什么 DNS 协议使用 UDP?只使用了 UDP 吗?
    飞天小牛肉
    0
    QUIC快速 UDP 互联网连接协议
    QUIC(QuickUDPInternetConnections)是由Google从2013年开始研究的基于UDP的可靠传输协议,它最早的原型是SPDY+QUIC-Crypto+ReliableUDP
    QUIC快速 UDP 互联网连接协议
    0
    Laminar多人 fps 游戏 UDP 协议
    Laminar是一种应用级传输协议,为快节奏的fps游戏提供基于UDP的可配置可靠性和排序保证,且提供轻量级的界面。它的灵感来自GafferonGames,功能类似RakNet,SteamSocket
    Laminar多人 fps 游戏 UDP 协议
    0
    Laminar多人 fps 游戏 UDP 协议
    Laminar 是一种应用级传输协议,为快节奏的 fps 游戏提供基于 UDP 的可配置可靠性和排序
    Laminar多人 fps 游戏 UDP 协议
    0
    QUIC快速 UDP 互联网连接协议
    QUIC (Quick UDP Internet Connections)是由Google从2013
    QUIC快速 UDP 互联网连接协议
    0
    SystemServer 源码分析
    程序员Android
    0
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    举报

    玻璃钢生产厂家广西玻璃钢牛动物雕塑厂家镇江小型玻璃钢花盆河南玻璃钢卡通雕塑吉祥物梅州玻璃钢动物雕塑供应商铜川玻璃钢雕塑价格批发东莞商场美陈公司温州仿铜西式玻璃钢雕塑河南玻璃钢浮雕学校镂空雕塑芜湖卡通玻璃钢雕塑厂家咨询客服玻璃钢天使雕塑厂家玻璃钢昆虫雕塑摆件玻璃钢雕塑好的有哪些浙江特色商场美陈多少钱上海商场主题创意商业美陈怎么做海沧园林玻璃钢雕塑推荐乐山玻璃钢雕塑厂家德阳动物玻璃钢雕塑厂家镇平玻璃钢雕塑加工厂家曲阳县玻璃钢雕塑生产厂家广州玻璃钢艺术雕塑商场美陈卫生间布置案例现代玻璃钢卡通雕塑商家潜江玻璃钢雕塑厂家玻璃钢雕塑上色流程视频河南玻璃钢雕塑摆件研发公司玻璃钢果篮雕塑公司江苏商场开业美陈杭州玻璃钢金属雕塑设计上海节庆商场美陈市场价玻璃钢雕塑谷仓香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

    玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化