第四章 UDP 通讯

4.1 相关函数介绍

sendto() 发送数据

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

/*
功能:
    发送数据到指定的目标地址。常用于UDP协议的无连接通信。

参数:
    sockfd: 套接字描述符,指向已经创建的UDP套接字。
    buf: 指向要发送数据的缓冲区。
    len: 要发送的数据长度,以字节为单位。
    flags: 发送标志,通常设为0。
    dest_addr: 目标地址结构体的指针,指定接收数据的目标地址。
    addrlen: 目标地址结构体的长度。

返回值:
    成功: 返回发送的字节数。
    失败: 返回 -1,并设置 errno 以指示错误类型。
*/

recvfrom() 接收数据

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

/*
功能:
    从指定的套接字接收数据,并可以获取发送方的地址信息。常用于UDP协议的无连接通信。

参数:
    sockfd: 套接字描述符,指向已经创建的UDP套接字。
    buf: 指向接收数据的缓冲区。
    len: 缓冲区的大小,以字节为单位。
    flags: 接收标志,通常设为0。
    src_addr: 发送方地址结构体的指针,用于存储发送方的地址信息。
    addrlen: 发送方地址结构体的长度指针,接收后存储实际地址长度。

返回值:
    成功: 返回接收的字节数。
    失败: 返回 -1,并设置 errno 以指示错误类型。
*/

4.2 UDP 开发流程和示例程序

[!WARNING]

下面图有问题 ==服务器== 和 ==客户端== 图标放反了

UDP开发流程

服务器流程

1、创建用户数据报 套接字

2、填充服务器网络信息结构体

3、将套接字与服务器的网络信息结构体绑定

4、收发数据

5、关闭套接字

客户端

1、创建用户数据报 套接字

2、填充服务器网络信息结构体

3、收发数据

4、关闭套接字

服务器

// udp_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>


#define PORT 8080  // 服务器监听的端口
#define BUFFER_SIZE 1024  // 缓冲区大小

int main() {
    int sockfd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr, cliaddr;

    // 创建套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 清空并设置服务器地址
    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 设置服务器信息
    servaddr.sin_family = AF_INET;        // IPv4
    servaddr.sin_addr.s_addr = INADDR_ANY; //   

    // 绑定套接字到地址
    if (bind(sockfd, (const struct sockaddr *)&servaddr,
             sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("UDP 服务器正在监听端口 %d...\n", PORT);

    int len, n;
    len = sizeof(cliaddr); // 必须初始化

    // 接收来自客户端的数据
    n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
                 (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0';
    printf("从客户端收到: %s\n", buffer);

    // 准备发送的数据
    const char *hello = "Hello from UDP server";

    // 发送数据给客户端
    sendto(sockfd, (const char *)hello, strlen(hello), 0,
           (const struct sockaddr *)&cliaddr, len);
    printf("发送消息给客户端: %s\n", hello);

    // 关闭套接字
    close(sockfd);

    return 0;
}

客户端

// udp_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>


#define PORT 8080  // 服务器的端口
#define BUFFER_SIZE 1024  // 缓冲区大小

int main() {
    int sockfd;
    char buffer[BUFFER_SIZE];
    const char *hello = "Hello from UDP client";
    struct sockaddr_in servaddr;

    // 创建套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("创建套接字失败");
        exit(EXIT_FAILURE);
    }

    // 清空并设置服务器地址
    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;  // IPv4
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = INADDR_ANY;

    int n, len;

    // 发送数据给服务器
    sendto(sockfd, (const char *)hello, strlen(hello), 0,
           (const struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("发送消息给服务器: %s\n", hello);

    // 接收来自服务器的数据
    len = sizeof(servaddr);
    n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
                 (struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("从服务器收到: %s\n", buffer);

    // 关闭套接字
    close(sockfd);

    return 0;
}

4.2 TFTP 文件协议

简单文件传输协议 用于设计传输小文件

特点

​ 基于UDP 协议实现

​ 不进行用于的有效性认证

数据传输模式

​ octet 二进制模式

​ netascii 文本模式

​ mail 不支持了

TFTP 协议分析

TFTP协议

这是一个描述TFTP(Trivial File Transfer Protocol)数据包结构的图。TFTP是一种简单的文件传输协议,通常在较低资源设备或简单的网络文件传输场景下使用。

TFTP数据包类型及其结构

读写请求(RRQ/WRQ)数据包

  • IP头部(20字节):IP数据报的首部,包含源地址、目的地址等信息。
  • UDP头部(8字节):UDP数据报的首部,包含源端口、目的端口等信息。
  • 操作码(Opcode)(2字节):操作码1表示读请求(RRQ),操作码2表示写请求(WRQ)。
  • 文件名(N字节):要读取或写入的文件名,以字节形式表示。
  • 0字节终止符(1字节):用于分隔文件名和传输模式。
  • 模式(N字节):表示传输模式,通常是“netascii”(文本模式)、“octet”(二进制模式)或“mail”。
  • 0字节终止符(1字节):用于标识模式的结束。

数据包(DATA)

  • 用于传输文件内容的数据。
  • 结构:
    • 操作码(Opcode)(2字节):固定值3,表示数据包。
    • 块编号(2字节):当前数据块的编号,从1开始递增,用于确保数据包顺序正确。
    • 数据(512字节):实际传输的数据内容,每个数据包最多传输512字节。

确认包(ACK)

  • 用于确认接收到的数据包。
  • 结构:
    • 操作码(Opcode)(2字节):固定值4,表示确认包。
    • 块编号(2字节):确认的块编号,与收到的数据包的块编号一致。

错误包(ERROR)

  • 用于指示传输过程中发生的错误。
  • 结构:
    • 操作码(Opcode)(2字节):固定值5,表示错误包。
    • 错误代码(2字节):表示特定错误类型的代码,例如文件未找到、访问权限错误等。
    • 错误信息(N字节):可变长度的错误描述信息,帮助诊断问题。
    • 0字节终止符(1字节):标识错误信息的结束。

数据包传输过程

  • TFTP数据包传输使用UDP协议(User Datagram Protocol),因为UDP是一种无连接、不可靠的传输协议,传输效率较高,但需要由TFTP协议本身负责数据重传和顺序控制。
  • 一个TFTP会话通常以读或写请求(RRQ/WRQ)开始,服务器和客户端通过传输数据包(DATA)和确认包(ACK)来交换数据。任何错误将通过错误包(ERROR)来通知对方。

这种数据结构使TFTP非常简单,适用于传输小文件或在内存和计算能力有限的设备上进行数据传输。

组装请求的方法
    char buf[128];
    char file_name[128];
组装方式1 通过改变指针的操作空间
    *(unsigned char *)buf = htons(1);

组装方式2 对每个字节进行写入   注意 需要使用网络字节序
buff[0] = 0;
buff[1] = 1;

组装方式3 sprintf
sprintf(buf , "%c%c%S%c%s%c" , 0 , 1 , file_name , 0 , "octet" , 0);

客户端示例代码

#include <head.h>

#define TFTP_MODE "octet" // 操作模式

// TFTP 操作码
#define RRQ 1
#define DATA 3
#define ACK 4
#define ERROR 5

#define BUF_MAX 600

// UDP 通讯初始化
int UDP_Client_Init(struct sockaddr_in *UDP_addr, const char *IP_val, in_port_t port);

// 终端输入 IP地址 + 端口
int main(int argc, char const *argv[])
{
    // 终端输入 判定
    ARGV_IF(argc, 3);

    // UDP 通讯初始化
    struct sockaddr_in UDP_addr;
    socklen_t UDP_len = sizeof(UDP_addr);
    int Fd_udp_soc = UDP_Client_Init(&UDP_addr, argv[1], atoi(argv[2]));

    /**************************** TFTP 客户端*****************************/
    char buf[600];
    char File_name[64];
    int buf_len;

    unsigned short code; // 操作码
    unsigned short num;  // 块编号
    char txt_data[516];

    // 接收结构体
    struct sockaddr_in From_addr;
    socklen_t From_len = sizeof(From_addr);

    // 用户输入文件名
    printf("输入你要下载的文件名\n");
    scanf("%s", File_name);

    int fd_file = open(File_name , O_WRONLY | O_TRUNC | O_CREAT , 0666);

    // 发送请求信号
    buf_len = sprintf(buf, "%c%c%s%c%s%c", 0, 1, File_name, 0, TFTP_MODE, 0);

    sendto(Fd_udp_soc, buf, buf_len, 0, (struct sockaddr *)&UDP_addr, UDP_len);

    while (1)
    {
        //  接收 解析数据
        buf_len = recvfrom(Fd_udp_soc, buf, BUF_MAX, 0, (struct sockaddr *)&From_addr, &From_len);

        code = ntohs(*(unsigned short *)buf);      // 获取 操作码
        num = ntohs(*(unsigned short *)(buf + 2)); // 获取 块编号
        strncpy(txt_data, buf + 4, buf_len - 4);   // 获取 数据

        printf("\n接收数据:%hu %hu %s\n", code, num, txt_data);

        if (DATA == code)
        {
            // 将数据写入到文件中
            write( fd_file , buf + 4 , buf_len - 4);
            if ((buf_len - 4) < 512)
            {
                printf("传输完成\n");
                break;
            }


            // 构建应答信号
            memset(buf, 0, sizeof(buf));
            *((unsigned short *)buf) = htons(ACK);       // 将操作码转换为网络字节序
            *((unsigned short *)(buf + 2)) = htons(num); // 将块编号转换为网络字节序

            // 发送应答信息
            sendto(Fd_udp_soc, buf, 4, 0, (struct sockaddr *)&From_addr, From_len);
        }
    }

    /**************************** TFTP 客户端 *****************************/

    // 4、关闭套接字.
    close(fd_file);
    close(Fd_udp_soc);

    return 0;
}

int UDP_Client_Init(struct sockaddr_in *UDP_addr, const char *IP_val, in_port_t port)
{
    // 1、创建用户数据报 套接字
    int Fd_udp_soc = 0;
    if (-1 == (Fd_udp_soc = socket(AF_INET, SOCK_DGRAM, 0)))
    {
        ERRLOG("socket");
    }
    // 2、填充服务器网络信息结构体
    UDP_addr->sin_family = AF_INET;
    UDP_addr->sin_port = htons(port);
    UDP_addr->sin_addr.s_addr = inet_addr(IP_val);

    // 连接测试
    // sendto( Fd_udp_soc , "UDP-连接成功\n" , strlen("UDP-连接成功\n") , 0 , (struct sockaddr *)UDP_addr , sizeof(struct sockaddr_in) );

    return Fd_udp_soc;
}

4.4 抓包问题

4.4.1 Wireshark 安装问题

windows 安装

鼠标右键点击 Wireshark-win64-3.2.2.exe,以管理员身份运行,

选择安装路径后默认下一步即可,如果提示缺少插件,就都勾选安装即可,尤其是有一页会出现 usbpcapxx 一定要安装,

最后一页有一个 立即重启 (reboot now) 除了这个都勾选。

Ubuntu 安装
     sudo apt-get install wireshark

无法获取无线网卡的办法

管理员 用户打开 cmd
    输入 net start npf

重启  Wireshark 以管理员方式打开

如果不行 检查 npcap 状态
管理员输入
    检查状态
    sc query npcap
    如果状态显示 STATE              : 1  STOPED
    则代表没有启动 
    启动方式
    sc start npcap

当可以看到网卡信息 说明成功了 如下

当有网卡下有波浪线的时候代表 已经有数据进行了收发

在这里选择 我们需要的网卡

抓包软件_1

有数据包界面

抓包软件_2

常用的过滤

tcp.port==8888          查端口
ip.src==192.168.0.1     查询 源 IP 地址
ip.dst==192.168.0.1     查询 目标地址 IP 地址
可以使用 and  和 or  连接多个语句

4.4.2 软件说明

开始捕获 和 停止捕获Wireshark_界面

No.             编号
Time            时间
Source          源 IP 地址
Destination     目标IP地址
Protocol        协议
Length          长度
Info            包的简要说明

抓包软件_4

Ethernet                        以太网头
Internet Protool                IP头信息   网络层信息
Transmission Control Protool    TCP头信息  传输层信息

抓包软件_5

信息说明

image-20240909104632688

4.4.3 链路层分析

MAC 头部协议

mac帧头部占14字节(6+6+2,目的mac+源mac+类型) ,

抓包分析_1

Frame 650: 194 bytes on wire (1552 bits), 194 bytes captured (1552 bits) on interface \Device\NPF_{6A122268-C6CF-44ED-98DC-85AF5A01CB95}, id 0
Ethernet II, Src: Intel_74:74:30 (a0:59:50:74:74:30), Dst: Intel_74:74:30 (a0:59:50:74:74:30)
    Destination: Intel_74:74:30 (a0:59:50:74:74:30)
    Source: Intel_74:74:30 (a0:59:50:74:74:30)
    Type: IPv4 (0x0800)
    [Stream index: 1]
Internet Protocol Version 4, Src: 172.20.10.9, Dst: 172.20.10.3
Transmission Control Protocol, Src Port: 35514, Dst Port: 6666, Seq: 1, Ack: 1, Len: 128

目的mac 地址
Destination: Intel_74:74:30 (a0:59:50:74:74:30)

源 mac 地址
Source: Intel_74:74:30 (a0:59:50:74:74:30)

使用 的IP 协议
Type: IPv4 (0x0800)

抓包软件_7

4.4.4 网络层数据

IP头数据分析

抓包分析_2

版本:     这是IP协议的版本号,占4个比特位。4表示IPv4协议,6表示IPv6协议。

首部长度:  IP头部的长度,占4个比特位。这个字段表示IP头部的长度,以4字节为单位。最大值为15(1111),表示60字节。

服务类型:服务类型字段用于定义数据包的优先级和服务质量。8个比特位通常分为三个部分:优先级(Precedence)、延迟(Delay)、吞吐量(Throughput)、可靠性(Reliability)。

总长度:    IP头 + TCP头 + 用户数据 的长度

标识:一个唯一的标识符,占16个比特位,用于识别数据包。在数据包分片时,所有分片具有相同的标识符,以便接收端可以重新组装。

标志:占3个比特位,用于控制和指示分片。包括3个标志位:
第一个比特未使用,总是0。
第二个比特为“不分片位(DF)”,如果为1,表示数据包不可分片。
第三个比特为“更多分片位(MF)”,如果为1,表示数据包后面还有分片。

片偏移:占13个比特位,表示当前数据包片相对于原始数据包的偏移量(单位为8字节)。用于分片重组。

生存时间: 占8个比特位,用于限制数据包的生命周期,通常表示数据包可以经过的路由器的最大数量。每经过一个路由器,TTL减1,TTL为0时,数据包被丢弃。

协议类型: 占8个比特位,表示上层协议的类型。例如,6表示TCP,17表示UDP。

头部校验和: 占16个比特位,用于校验IP头部的完整性。发送端计算校验和并填充,接收端进行校验,校验失败则丢弃数据包。
源IP 地址:占32个比特位,表示数据包的发送方的IP地址。

目的IP 地址:占32个比特位,表示数据包的接收方的IP地址。

抓包信息

Frame 650: 194 bytes on wire (1552 bits), 194 bytes captured (1552 bits) on interface \Device\NPF_{6A122268-C6CF-44ED-98DC-85AF5A01CB95}, id 0
Ethernet II, Src: Intel_74:74:30 (a0:59:50:74:74:30), Dst: Intel_74:74:30 (a0:59:50:74:74:30)
    Destination: Intel_74:74:30 (a0:59:50:74:74:30)
    Source: Intel_74:74:30 (a0:59:50:74:74:30)
    Type: IPv4 (0x0800)
    [Stream index: 1]
Internet Protocol Version 4, Src: 172.20.10.9, Dst: 172.20.10.3
    0100 .... = Version: 4
    .... 0101 = Header Length: 20 bytes (5)
    Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
    Total Length: 180
    Identification: 0x92b0 (37552)
    010. .... = Flags: 0x2, Don't fragment
    ...0 0000 0000 0000 = Fragment Offset: 0
    Time to Live: 64
    Protocol: TCP (6)
    Header Checksum: 0x3b5f [validation disabled]
    [Header checksum status: Unverified]
    Source Address: 172.20.10.9
    Destination Address: 172.20.10.3
    [Stream index: 1]
Transmission Control Protocol, Src Port: 35514, Dst Port: 6666, Seq: 1, Ack: 1, Len: 128

抓包分析_3

交换机是工作在链路层的设备  是根据 MAC地址来决定如何转发信息
路由器是工作在网络层的设备  是根据 IP地址来决定如何转发信息

4.4.5 传输层

源端口:    发送方的端口  (随机的应为没有绑定)
目的端口:   接收方的端口
顺序号:seq
确认号:ack
头部长度: 也是 4倍的单位 5 表示 20 
```
Source Port (源端口): 35514
Destination Port (目标端口): 6666
Stream Index (流索引): 5
Stream Packet Number (流数据包编号): 7
Conversation Completeness (会话完整性): Complete, WITH_DATA (31)
TCP Segment Length (TCP段长度): 128
Sequence Number (序列号): 1 (relative sequence number) 相对序列号
Sequence Number (raw) (原始序列号): 2568202456
Next Sequence Number (下一个序列号): 129 (relative sequence number) 相对序列号
Acknowledgment Number (确认号): 1 (relative ack number) 相对确认号
Acknowledgment Number (raw) (原始确认号): 2104686676
Header Length (头部长度): 32 bytes (8)
Flags (标志位): 0x018 (PSH, ACK)
Window (窗口大小): 229
Calculated Window Size (计算得出的窗口大小): 29312
Window Size Scaling Factor (窗口大小缩放因子): 128
Checksum (校验和): 0x25f8 [unverified] 未验证
Checksum Status (校验和状态): Unverified 未验证
Urgent Pointer (紧急指针): 0
Options (选项): (12 bytes), No-Operation (NOP), No-Operation (NOP), No-Operation (NOP), Timestamps (时间戳)
TCP Option - No-Operation (NOP) 空操作
TCP Option - No-Operation (NOP) 空操作
TCP Option - Timestamps (时间戳): TSval 1045131, TSecr 279527307
Timestamps (时间戳):
    Time since first frame in this TCP stream (从此TCP流的第一个数据包开始的时间): 2.195243000 seconds
    Time since previous frame in this TCP stream (从上一个数据包开始的时间): 2.194942000 seconds
SEQ/ACK Analysis (序列/确认分析):
    iRTT (初始往返时间): 0.000287000 seconds
    Bytes in flight (未被确认的字节数): 128
    Bytes sent since last PSH flag (自上次PSH标志后发送的字节数): 128
TCP Payload (TCP负载): 128 bytes

4.4.6 应用层

数据如下

抓包分析_4