16. UDP编程.md 11 KB

UDP编程

1. UDP协议特点

UDP(User Datagram Protocol)是一种无连接的传输层协议,提供简单、高效的数据传输服务。

1.1 核心特点

  • 无连接:通信前不需要建立连接,直接发送数据
  • 不可靠:不保证数据包的可靠传输,可能丢失、重复、乱序
  • 数据报:以数据报为单位发送,每次发送都是独立的
  • 高效:头部开销小(仅8字节),传输效率高
  • 支持广播/组播:可以向多个主机发送数据

1.2 UDP头部结构

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Length             |           Checksum            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 源端口(16位):发送方端口号
  • 目的端口(16位):接收方端口号
  • 长度(16位):UDP头部+数据的总长度
  • 校验和(16位):用于错误检测

2. UDP vs TCP对比

特性 UDP TCP
连接方式 无连接 面向连接
可靠性 不可靠 可靠
传输单位 数据报 字节流
头部大小 8字节 20-60字节
传输效率 较低
流量控制
拥塞控制
广播/组播 支持 不支持
适用场景 实时应用、DNS、DHCP 文件传输、网页、邮件

3. UDP服务器编程

3.1 编程流程

socket() → bind() → recvfrom()/sendto() → close()

3.2 关键函数

socket()

int socket(int domain, int type, int protocol);
  • domain:AF_INET(IPv4)或AF_INET6(IPv6)
  • type:SOCK_DGRAM(数据报套接字)
  • protocol:0(自动选择协议)

bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

绑定本地地址和端口,客户端可以不绑定(系统自动分配)。

recvfrom()

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd:套接字描述符
  • buf:接收缓冲区
  • len:缓冲区大小
  • flags:标志位(通常为0)
  • src_addr:发送方地址(输出参数)
  • addrlen:地址结构体大小(输入输出参数)

sendto()

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • dest_addr:目的地址
  • addrlen:地址结构体大小

4. UDP客户端编程

4.1 编程流程

socket() → [bind()] → sendto() → recvfrom() → close()

客户端通常不需要bind,系统会自动分配端口。

4.2 注意事项

  • 客户端需要知道服务器的地址和端口
  • 可以不调用bind,系统自动分配端口
  • 如果需要固定端口,可以调用bind

5. UDP Echo完整示例

5.1 UDP Echo服务器

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

#define BUF_SIZE 1024
#define PORT 8888

int main() {
    int server_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUF_SIZE];
    ssize_t recv_len;

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

    // 2. 绑定地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("UDP Echo server listening on port %d\n", PORT);

    // 3. 循环接收和发送数据
    while (1) {
        client_len = sizeof(client_addr);
        recv_len = recvfrom(server_fd, buffer, BUF_SIZE, 0,
                           (struct sockaddr *)&client_addr, &client_len);
        if (recv_len < 0) {
            perror("recvfrom failed");
            continue;
        }

        printf("Received from %s:%d: %.*s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               (int)recv_len, buffer);

        // 回显数据
        if (sendto(server_fd, buffer, recv_len, 0,
                   (struct sockaddr *)&client_addr, client_len) < 0) {
            perror("sendto failed");
        }
    }

    close(server_fd);
    return 0;
}

5.2 UDP Echo客户端

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

#define BUF_SIZE 1024
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888

int main() {
    int client_fd;
    struct sockaddr_in server_addr;
    socklen_t server_len;
    char buffer[BUF_SIZE];
    ssize_t recv_len;

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

    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("inet_pton failed");
        close(client_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 发送数据
    printf("Enter message: ");
    fgets(buffer, BUF_SIZE, stdin);
    buffer[strcspn(buffer, "\n")] = 0;  // 移除换行符

    if (sendto(client_fd, buffer, strlen(buffer), 0,
               (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("sendto failed");
        close(client_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 接收回显数据
    server_len = sizeof(server_addr);
    recv_len = recvfrom(client_fd, buffer, BUF_SIZE, 0,
                       (struct sockaddr *)&server_addr, &server_len);
    if (recv_len < 0) {
        perror("recvfrom failed");
    } else {
        buffer[recv_len] = '\0';
        printf("Echo received: %s\n", buffer);
    }

    close(client_fd);
    return 0;
}

5.3 编译和运行

# 编译服务器
gcc -o udp_server udp_server.c

# 编译客户端
gcc -o udp_client udp_client.c

# 运行服务器
./udp_server

# 运行客户端(另一个终端)
./udp_client

6. sendto/recvfrom的addr参数处理

6.1 地址结构体

struct sockaddr_in {
    short            sin_family;   // AF_INET
    unsigned short   sin_port;     // 端口号
    struct in_addr   sin_addr;     // IP地址
    char             sin_zero[8];  // 填充字节
};

struct in_addr {
    in_addr_t s_addr;  // 网络字节序的IP地址
};

6.2 地址转换函数

// 点分十进制 → 网络字节序
int inet_pton(int af, const char *src, void *dst);

// 网络字节序 → 点分十进制
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

// 主机字节序 → 网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);

// 网络字节序 → 主机字节序
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

6.3 注意事项

  • recvfromsrc_addr参数是输出参数,返回发送方地址
  • sendtodest_addr参数是输入参数,指定目的地址
  • 地址结构体长度必须正确传递
  • IPv4使用sockaddr_in,IPv6使用sockaddr_in6

7. 嵌入式场景

7.1 轻量级通信

  • 设备发现:局域网内设备自动发现
  • 状态上报:设备定期向服务器发送状态
  • 远程控制:发送控制命令到设备
  • 日志传输:轻量级日志数据传输

7.2 广播/组播

广播示例

// 设置广播属性
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

// 发送广播消息
struct sockaddr_in broadcast_addr;
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(8888);
broadcast_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);

sendto(sockfd, message, strlen(message), 0,
       (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));

组播示例

// 加入组播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);

setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

// 发送组播消息
struct sockaddr_in multicast_addr;
multicast_addr.sin_family = AF_INET;
multicast_addr.sin_port = htons(8888);
multicast_addr.sin_addr.s_addr = inet_addr("224.0.0.1");

sendto(sockfd, message, strlen(message), 0,
       (struct sockaddr *)&multicast_addr, sizeof(multicast_addr));

7.3 实际应用场景

  • MQTT协议:基于UDP的轻量级消息协议
  • CoAP协议:物联网设备通信协议
  • DNS查询:域名解析通常使用UDP
  • DHCP:动态主机配置协议
  • 实时音视频:RTP协议基于UDP

8. 注意事项

8.1 可靠性保证

UDP本身不可靠,需要在应用层实现:

  • 超时重传:设置超时机制,超时后重传
  • 确认机制:接收方发送确认消息
  • 序列号:为数据包编号,检测丢失和乱序
  • 校验和:使用校验和检测数据损坏

8.2 性能优化

  • 减少数据拷贝:使用sendmmsg/recvmmsg批量发送
  • 使用writev/readv:分散/聚集IO
  • 调整缓冲区大小:设置SO_SNDBUFSO_RCVBUF
  • 避免频繁bind:服务器端只bind一次

8.3 常见错误

  • 忘记字节序转换:端口和IP地址需要转换
  • 地址结构体大小错误addrlen参数传递错误
  • 缓冲区溢出:接收缓冲区太小
  • 未处理EAGAIN:非阻塞模式下需要处理

9. 面试要点

9.1 常见问题

  1. UDP和TCP的区别是什么?

    • 连接方式、可靠性、传输效率、头部大小、适用场景
  2. UDP服务器为什么不需要listen和accept?

    • UDP是无连接的,不需要建立连接
  3. 如何保证UDP传输的可靠性?

    • 应用层实现确认、重传、排序等机制
  4. UDP的适用场景有哪些?

    • 实时应用、DNS、DHCP、广播/组播
  5. 如何实现UDP的广播和组播?

    • 设置SO_BROADCAST属性,使用特定的组播地址

9.2 编程要点

  • 记住UDP编程的四个步骤:socket→bind→recvfrom/sendto→close
  • 理解recvfromsendto的addr参数含义
  • 注意字节序转换:htonshtonlntohsntohl
  • 服务器端需要绑定地址,客户端通常不需要
  • 广播/组播需要设置相应的socket选项

9.3 性能考虑

  • UDP头部开销小,传输效率高
  • 适合实时性要求高的应用
  • 需要应用层处理可靠性问题
  • 大量数据传输时考虑分片问题