【Linux开发】 04 Linux UDP 网络编程

一、为什么需要 UDP?

TCP 提供可靠的、面向连接的传输服务,但代价是:

  • 需要三次握手建立连接
  • 需要四次挥手断开连接
  • 需要维护连接状态(每个客户端一个套接字)
  • 有流量控制、拥塞控制等复杂机制

这就好比打电话:必须先拨号、等待对方接听、才能说话,说完还要挂断。

UDP 则像是寄明信片:写好地址,直接扔进邮筒,不保证对方一定能收到,也不需要提前建立连接。它简单、快速,适合对实时性要求高、对可靠性要求不高的场景(如视频直播、DNS查询)。


二、UDP 的核心特点

特性 说明
无连接 无需三次握手,直接发送数据
无消息边界 每个 sendto 发出的数据包是独立的消息,接收方用 recvfrom 一次收一个完整包(但可能丢包)
一对多通信 一个 UDP 套接字可以给多个不同地址发送数据
简单 不需要 listenaccept,只需 socket + bind(可选)
不可靠 可能丢包、乱序、重复,没有重传机制

三、UDP 编程核心函数

UDP 使用 sendtorecvfrom 函数发送和接收数据,每次调用都需要指定对方的地址。

3.1 sendto -- 发送数据

c 复制代码
#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);
参数 说明
sockfd UDP 套接字描述符
buf 要发送的数据缓冲区
len 要发送的字节数
flags 通常为 0
dest_addr 目标主机的地址信息(IP + 端口)
addrlen dest_addr 结构体的大小

返回值:成功返回发送的字节数,失败返回 -1。

3.2 recvfrom -- 接收数据

c 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数 说明
sockfd UDP 套接字描述符
buf 接收缓冲区
len 缓冲区大小
flags 通常为 0
src_addr 用于返回发送方的地址信息(IP + 端口)
addrlen 传入传出参数,指定 src_addr 的大小,并返回实际地址长度

返回值:成功返回接收的字节数,失败返回 -1。


四、UDP 回声服务器实现

4.1 服务器端代码(udp_echo_server.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock;                     // UDP 套接字
    char message[BUF_SIZE];            // 数据缓冲区
    int str_len;
    socklen_t clnt_adr_sz;
    struct sockaddr_in serv_adr, clnt_adr;

    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    // 1. 创建 UDP 套接字(注意 SOCK_DGRAM)
    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serv_sock == -1)
        error_handling("UDP socket creation error");

    // 2. 绑定地址(服务器必须绑定,否则客户端不知道往哪发)
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);   // 本机任意 IP
    serv_adr.sin_port = htons(atoi(argv[1]));       // 指定端口

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    // 3. 循环接收并回传数据
    while (1) {
        clnt_adr_sz = sizeof(clnt_adr);
        // 接收数据,同时获取发送方的地址
        str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        // 将收到的数据原样发回给发送方
        sendto(serv_sock, message, str_len, 0,
               (struct sockaddr*)&clnt_adr, clnt_adr_sz);
    }

    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

关键点

  • 创建套接字时用 SOCK_DGRAM 表示 UDP。
  • 服务器必须 bind 到固定端口,否则客户端无法找到它。
  • recvfrom 接收数据,同时获得客户端的地址。
  • sendto 将数据原路返回,目标地址就是刚才得到的客户端地址。

4.2 客户端代码(udp_echo_client.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len;
    socklen_t adr_sz;
    struct sockaddr_in serv_adr, from_adr;

    if (argc != 3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    // 1. 创建 UDP 套接字
    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    // 2. 初始化服务器地址
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);  // 服务器 IP
    serv_adr.sin_port = htons(atoi(argv[2]));       // 服务器端口

    while (1) {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        // 发送数据到服务器(每次都要指定服务器地址)
        sendto(sock, message, strlen(message), 0,
               (struct sockaddr*)&serv_adr, sizeof(serv_adr));

        // 接收回声(同时获取发送方地址,这里就是服务器地址)
        adr_sz = sizeof(from_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0,
                           (struct sockaddr*)&from_adr, &adr_sz);

        message[str_len] = 0;
        printf("Message from server: %s", message);
    }

    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

关键点

  • 客户端不需要 bind,第一次调用 sendto 时系统会自动分配一个随机端口。
  • 每次发送都需要指定服务器地址。
  • 接收时也用 recvfrom,可以拿到发送方地址(用于验证是否是服务器发回的)。

五、UDP 地址分配的秘密

5.1 为什么客户端不 bind 也能工作?

TCP 客户端通过 connect 隐式绑定了本地地址。UDP 客户端调用 sendto 时,如果套接字尚未绑定地址,操作系统会自动为该套接字分配一个临时端口和本地 IP。这个地址一直保留到套接字关闭。

所以,UDP 客户端通常不需要显式调用 bind,除非你想固定使用某个端口。

5.2 什么时候需要显式 bind?

  • 服务器:必须绑定知名端口,客户端才能找到。
  • 客户端 :如果需要固定端口(如某些防火墙规则),可以主动 bind
  • 多网卡场景:如果希望数据从特定网卡发送,可以绑定到特定 IP。

六、UDP 的特性与注意事项

6.1 UDP 的数据包边界

UDP 是面向数据报 的协议,每个 sendto 发出的数据包,接收方用一个 recvfrom 就能完整接收(前提是缓冲区足够大)。如果接收缓冲区小于数据包大小,多余的数据会被丢弃。

因此,在 UDP 编程中,你需要保证发送的数据包大小不超过接收方的缓冲区,通常建议小于 1500 字节(以太网 MTU 限制)。

6.2 UDP 的不可靠性

UDP 不保证数据包能到达,也不保证顺序。如果网络发生拥塞,可能丢包;如果发送速度过快,接收方来不及处理,也可能丢包。应用层需要自己处理这些情况。

6.3 一对多通信

一个 UDP 套接字可以给多个不同地址发送数据,也可以接收来自任意地址的数据。这非常适合广播、多播等场景。


七、拓展:如何实现可靠的 UDP?

虽然 UDP 本身不可靠,但可以在应用层增加可靠性机制,例如:

  • 确认机制:发送方等待接收方的 ACK,超时则重传。
  • 序号:给每个数据包编号,接收方检测丢包和乱序。
  • 滑动窗口:流量控制,避免发送过快。

著名的可靠 UDP 协议有:RUDP、UDT、QUIC(HTTP/3 使用)。

相关推荐
123过去2 小时前
mdb-sql使用教程
linux·网络·数据库·sql
hweiyu002 小时前
Linux命令:pgrep
linux·运维·服务器
eam0511232 小时前
HCIA复习实验
网络
瑞瑞大大2 小时前
国家“东数西算”工程八大算力枢纽节点及10个国家数据中心集群介绍
网络
belldeep2 小时前
python:Scapy 网络数据包操作库
网络·python·抓包·scapy
23.2 小时前
【分析】网络不通会显示404吗?
网络
文人sec2 小时前
【Linux 服务器上搭建 JMeter 性能测试与监控环境(实战版)】
linux·运维·服务器·jmeter·性能测试
papaofdoudou3 小时前
Linux内核的边界在哪里?
linux·运维·服务器
zzzsde3 小时前
【Linux】文件:基础IO
linux·运维·服务器