11 udp 发送数据的流程梳理

Posted 蓝风9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11 udp 发送数据的流程梳理相关的知识,希望对你有一定的参考价值。

前言

呵呵 之前曾经看到过 湖光大佬 的 tcp 的流程梳理

呵呵 很高深 有很多不明白的地方, 不光是涉及到 linux 网络处理本身的东西, 还涉及到了 tcp协议 的一些具体的实现, 是非常的复杂

这里之前 在 0voice/linux_kernel_wiki 上面看到了网络协议栈部分的梳理

呵呵 自己也稍微走了一下 流程, 这里稍微记录一下

主要核心的内容包含了, 用户数据传递过来, 数据包的封装, 然后到 数据包发送到驱动层 的这个流程, 当然 是没有上面的 0voice/linux_kernel_wiki 网络协议栈 部分内容详细, 以及准确 

记录于 2022.05.02 

测试环境 : linux 4.10.14 + qemu 2.5.0 

测试用例

测试用例, 拷贝自网络, 但是忘机记录具体的地址信息了, ^_^ 

udp 服务器 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8082
#define BUFSIZE 512
char buf[BUFSIZE+1];
int main()

  //第 1 步 创建套接字
  int sockfd=socket(AF_INET,SOCK_DGRAM,0);
  //第 2 步 设置地址结构体
  struct sockaddr_in svraddr;
  svraddr.sin_family=AF_INET;//使用 internet 协议
  svraddr.sin_port=htons(PORT);
//  inet_aton("0.0.0.0",&svraddr.sin_addr);
  inet_aton("192.168.0.103",&svraddr.sin_addr);
  //第 3 步 绑定
  int ret=bind(sockfd,(struct sockaddr*)&svraddr,sizeof(svraddr));
  if(ret<0)printf("cannot bind!\\r\\n");exit(-1);;
  while(1)
  
        struct sockaddr_in cli;
        int len=sizeof(cli);
    int z=recvfrom(sockfd,buf,BUFSIZE,0,(struct sockaddr*)&cli,&len);//第 6 步 读取套接字
    buf[z]='\\0';
    printf("%s\\r\\n",buf);//打印
  


udp 客户端 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8082
#define BUFSIZE 512
char buf[BUFSIZE+1];
int main()

  //第 1 步 创建一个体套接字
  int sockfd=socket(AF_INET,SOCK_DGRAM,0);
  //第 2 步 设置 addr 结构体
  struct sockaddr_in svraddr;
  svraddr.sin_family=AF_INET;//使用 internet 协议
  svraddr.sin_port=htons(PORT);
  inet_aton("0.0.0.0",&svraddr.sin_addr);
  //第 3 步 连接服务器
  //connect(sockfd,(struct sockaddr*)&svraddr,sizeof(svraddr));
  buf[0] = '1';
  buf[1] = '2';
  buf[2] = '3';
//  while(1)
//  
  sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&svraddr,sizeof(svraddr)); //第 4 步 向套接字中写入字符串
//  

如上 客户端发送了 三个字节 的数据 

以太网头部, ip头部, udp 头部 合计为 14 + 20 + 8 = 42 字节, 因此 客户端给服务器发送的数据报文长度为 45 字节

//Mac头部,总长度14字节  
typedef struct _eth_hdr  
  
    unsigned char dstmac[6]; //目标mac地址  
    unsigned char srcmac[6]; //源mac地址  
    unsigned short eth_type; //以太网类型  
 eth_hdr;
//IP头部,总长度20字节  
typedef struct _ip_hdr  
  
    #if LITTLE_ENDIAN  
    unsigned char ihl:4;     //首部长度  
    unsigned char version:4, //版本   
    #else  
    unsigned char version:4, //版本  
    unsigned char ihl:4;     //首部长度  
    #endif  
    unsigned char tos;       //服务类型  
    unsigned short tot_len;  //总长度  
    unsigned short id;       //标志  
    unsigned short frag_off; //分片偏移  
    unsigned char ttl;       //生存时间  
    unsigned char protocol;  //协议  
    unsigned short chk_sum;  //检验和  
    struct in_addr srcaddr;  //源IP地址  
    struct in_addr dstaddr;  //目的IP地址  
 ip_hdr;
//UDP头部,总长度8字节  
typedef struct _udp_hdr  
  
    unsigned short src_port; //远端口号  
    unsigned short dst_port; //目的端口号  
    unsigned short uhl;      //udp头部长度  
    unsigned short chk_sum;  //16位udp检验和  
 udp_hdr;

这是一段相似的报文, 不过 服务器 和 客户端 主机和我们调试的主机不同 

Frame 654: 45 bytes on wire (360 bits), 45 bytes captured (360 bits) on interface en0, id 0
Ethernet II, Src: Apple_c9:48:f9 (38:f9:d3:c9:48:f9), Dst: VMware_4e:80:29 (00:0c:29:4e:80:29)
    Destination: VMware_4e:80:29 (00:0c:29:4e:80:29)
    Source: Apple_c9:48:f9 (38:f9:d3:c9:48:f9)
    Type: IPv4 (0x0800)
Internet Protocol Version 4, Src: 192.168.0.103, Dst: 192.168.0.20
    0100 .... = Version: 4
    .... 0101 = Header Length: 20 bytes (5)
    Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
    Total Length: 31
    Identification: 0xaf64 (44900)
    Flags: 0x00
    Fragment Offset: 0
    Time to Live: 64
    Protocol: UDP (17)
    Header Checksum: 0x499e [validation disabled]
    [Header checksum status: Unverified]
    Source Address: 192.168.0.103
    Destination Address: 192.168.0.20
User Datagram Protocol, Src Port: 53260, Dst Port: 8082
    Source Port: 53260
    Destination Port: 8082
    Length: 11
    Checksum: 0x2a3b [unverified]
    [Checksum Status: Unverified]
    [Stream index: 5]
    [Timestamps]
    UDP payload (3 bytes)
Data (3 bytes)
    Data: 313233
    [Length: 3]

udp 发送数据包的流程 

用户调用 sendto 系统调用 和 内核进行交互, 传输的 buf 的内容是用户程序中传入的 buf 前三个字节分别为 '1', '2', '3' 

我们这里不会关注 整个流程, 我们只会关注一部分的点 

1. 用户数据 封装 msghdr 的地方 

2. msghdr 封装 skb  的地方

3. 封装 用户数据 的地方

4. 封装 ip 头的地方

5. 封装 udp 头的地方 

6. 封装 eth 头的地方

7. 封装好了 skb 之后内核传递数据给 driver 的地方

1. 用户数据 封装 msghdr 的地方 

用户程序 调用系统调用传入的输入 

buf 为 0x601080, len 为 3, 这三个字节分别对应于 '1', '2', '3' 

然后根据传入的 buf 创建了 msghdr 向下交互 

(gdb) x/2gx 0x601080
0x601080:	0x0000000000333231	0x0000000000000000

2. msghdr 封装 skb  的地方 

from 对应的是传入的 msghdr 

getfrag 对应的是将数据填充到 skb 中的函数 

3. 封装 用户数据 的地方

 将 msghdr 中封装的 buf 的数据拷贝到 skb 的指定位置[预留 ip 头 + udp 头的长度] 

这里的偏移为 20 + 8 = 28 

这里 拷贝了 '1', '2', '3' 到 skb 中

4. 封装 ip 头的地方

封装了 skb 之后, 会更新 skb 的 iphdr 的相关信息 

iphdr 数据填充之后, 我们拆解一下关键的信息项, 注意 dump 出来的内存为小端序 

source_ip 在 0xffff88007f44101c 的位置, 值为 0x0100007f = 127.0.0.1 

dst_ip 在 0xffff88007f441020 的位置, 值为 0x0100007f = 127.0.0.1 

protocol 为 0xffff88007f441019 的位置 值为 0x11 = 17 = udp 

ttl 为 0xffff88007f441018 的位置 值为 0x40 = 64 

(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000:	0xffff88007f441400	0x0000000000000040
0xffff88007f441010:	0x00406b6500400045	0x0100007f00401140
0xffff88007f441020:	0x000000000100007f	0x00333231000001f8
0xffff88007f441030:	0x0000000000000008	0x0000000400000003
0xffff88007f441040:	0x0000000000000238	0x0000000000400238
0xffff88007f441050:	0x0000000000400238	0x000000000000001c
0xffff88007f441060:	0x000000000000001c	0x0000000000000001
0xffff88007f441070:	0x0000000500000001	0x0000000000000000
0xffff88007f441080:	0x0000000000400000	0x0000000000400000
0xffff88007f441090:	0x0000000000000924	0x0000000000000924

5. 封装 udp 头的地方 

封装了 skb 之后会将 skb 从 udp 层传递到 ip 层, 在此之前会先封装 udphdr 

udphdr 数据填充之后, 我们拆解一下关键的信息项, 注意 dump 出来的内存为小端序 

source_port 在 0xffff88007f441024 的位置, 值为 0x9857 = 38999 

dst_port 在 0xffff88007f441026 的位置, 值为 0x1f92 = 8082 

length 在 0xffff88007f441028 的位置, 值为 0x00b = 11 

checksum 在 0xffff88007f441030 的位置, 目前还没有计算, 值为 0x0000 = 0 

(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000:	0xffff88007f441400	0x0000000000000040
0xffff88007f441010:	0x00406b6500400045	0x0100007f00401140
0xffff88007f441020:	0x921f57980100007f	0x0033323100000b00
0xffff88007f441030:	0x0000000000000008	0x0000000400000003
0xffff88007f441040:	0x0000000000000238	0x0000000000400238
0xffff88007f441050:	0x0000000000400238	0x000000000000001c
0xffff88007f441060:	0x000000000000001c	0x0000000000000001
0xffff88007f441070:	0x0000000500000001	0x0000000000000000
0xffff88007f441080:	0x0000000000400000	0x0000000000400000
0xffff88007f441090:	0x0000000000000924	0x0000000000000924

计算了 checksum 之后, 填充到 skb 中 

这里的 checksum 为 0xfe1e, 其具体的值不重要 

(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000:	0xffff88007f441400	0x0000000000000040
0xffff88007f441010:	0x00406b6500400045	0x0100007f00401140
0xffff88007f441020:	0x921f57980100007f	0x003332311efe0b00
0xffff88007f441030:	0x0000000000000008	0x0000000400000003
0xffff88007f441040:	0x0000000000000238	0x0000000000400238
0xffff88007f441050:	0x0000000000400238	0x000000000000001c
0xffff88007f441060:	0x000000000000001c	0x0000000000000001
0xffff88007f441070:	0x0000000500000001	0x0000000000000000
0xffff88007f441080:	0x0000000000400000	0x0000000000400000
0xffff88007f441090:	0x0000000000000924	0x0000000000000924

6. 封装 eth 头的地方

ip 层处理之后, 路由查询, 然后来到了 数据链路层, 封装 source_mac, dst_mac, 网络层标志 等 

ethhdr 数据填充之后, 我们拆解一下关键的信息项, 注意 dump 出来的内存为小端序 

source_mac 在 0xffff88007f441002 的位置, 值为 0x000000000000 

dst_mac 在 0xffff88007f441008 的位置, 值为 0x000000000000  

type 在 0xffff88007f44100e 的位置, 值为 0x000b 为 IPV4

(gdb) x/20gx 0xffff88007f441000
0xffff88007f441000:	0x0000000000000000	0x0008000000000000
0xffff88007f441010:	0x004094681f000045	0x0100007f37d41140
0xffff88007f441020:	0x921f44950100007f	0x003332311efe0b00
0xffff88007f441030:	0x0000000000000008	0x0000000400000003
0xffff88007f441040:	0x0000000000000238	0x0000000000400238
0xffff88007f441050:	0x0000000000400238	0x000000000000001c
0xffff88007f441060:	0x000000000000001c	0x0000000000000001
0xffff88007f441070:	0x0000000500000001	0x0000000000000000
0xffff88007f441080:	0x0000000000400000	0x0000000000400000
0xffff88007f441090:	0x0000000000000924	0x0000000000000924

7. 封装好了 skb 之后内核传递数据给 driver 的地方

这里两种处理方式, 一种是直接将 skb 传递给 drvier, 直接发送 数据包 

另外一种是 进入 qdisc 队列, 根据给定的策略从 队列中刷出数据到 drvier 发送数据包, 多了一层缓冲 提高效率 

不管是上面哪一种方式, 调用 driver 的入口都是 dev_hard_start_xmit, 调用链路如下  

dev_hard_start_xmit - xmit_one - netdev_start_xmit - __netdev_start_xmit - ops->ndo_start_xmit

我这里虚拟机似乎是存在问题? 一直添加到队列, 但是没有 flush 的过程, 呵呵

ops->ndo_start_xmit 就是对应的硬件驱动发送数据包的具体的实现 

本地回环 设备对应的实现是在 drivers/net/loopback.c 中, 发送数据包的实现为 loopback_xmit, 具体实现是直接调用 netif_rx 来添加数据包入队 

后记

当然 具体的实现还有更多的细节, 这里仅仅是一个 case 来梳理了一些 用户数据 到 封包 到 发送到驱动的流程 

参考

  0voice/linux_kernel_wiki 

以上是关于11 udp 发送数据的流程梳理的主要内容,如果未能解决你的问题,请参考以下文章

rpcsockettcp/udp简要梳理

11.5 11.6

udp网络程序-发送接收数据

udp网络程序-发送接收数据

Socks5 udp代理

PYTHON-UDP