一、UDP协议的特点

UDP,全称是用户数据报协议。说白了,它就是个"只管发,不管到"的传输协议。

我在项目中用过不少UDP,最深的感受就是——它简单到让人有点不放心。但恰恰是这种简单,让它在某些场景下比TCP更香。

UDP的核心特点

  • 无连接:发数据前不需要握手,直接扔出去就行
  • 不可靠:数据丢了?乱序了?重复了?UDP不管
  • 无状态:不维护连接状态,不记录收发进度
  • 面向报文:应用层给多少数据,UDP就发多少,不拆分也不合并
  • 支持广播和组播:这是TCP做不到的

重要:UDP头部只有8个字节,而TCP头部至少20个字节。开销小,延迟低,这是UDP最大的优势。

二、UDP Socket通信流程

UDP的通信流程,比TCP简单太多了。你想想看,TCP要三次握手、四次挥手,UDP呢?直接发就完了。

服务端流程

  1. 创建socket:socket(AF_INET, SOCK_DGRAM, 0)
  2. 绑定地址:bind()
  3. 接收数据:recvfrom()
  4. 发送数据:sendto()
  5. 关闭socket:close()

客户端流程

  1. 创建socket:socket(AF_INET, SOCK_DGRAM, 0)
  2. 发送数据:sendto()
  3. 接收数据:recvfrom()
  4. 关闭socket:close()

个人经验:客户端其实可以不调用bind(),系统会自动分配一个临时端口。但我建议你还是显式bind一下,方便调试时抓包定位。

服务端 socket() bind() recvfrom() sendto() close() 客户端 socket() sendto() recvfrom() close() 数据流向 数据流向

三、UDP服务器的实现

写一个UDP服务器,核心就是recvfrom和sendto这两个函数。我刚开始用的时候,老是把它们的参数顺序搞混,后来干脆写了个小工具函数封装了一下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    // 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(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(sockfd);
        exit(1);
    }

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

    // 3. 循环接收数据
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                         (struct sockaddr*)&client_addr, &addr_len);
        if (n < 0) {
            perror("recvfrom");
            continue;
        }

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

        // 4. 回显数据
        sendto(sockfd, buffer, n, 0,
               (struct sockaddr*)&client_addr, addr_len);
    }

    close(sockfd);
    return 0;
}

注意:recvfrom的最后一个参数addr_len,传入时必须是sizeof(client_addr),传出时才是实际地址长度。我曾经在这里踩过坑,传了个0进去,结果recvfrom一直返回-1。

四、UDP客户端的实现

客户端更简单,连bind都不用。直接sendto就完事了。

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

#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    socklen_t addr_len = sizeof(server_addr);
    char buffer[BUFFER_SIZE];

    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(PORT);

    // 3. 发送数据
    while (1) {
        printf("Enter message: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = 0;

        sendto(sockfd, buffer, strlen(buffer), 0,
               (struct sockaddr*)&server_addr, addr_len);

        // 4. 接收回显
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                         (struct sockaddr*)&server_addr, &addr_len);
        if (n > 0) {
            printf("Server echo: %s\n", buffer);
        }
    }

    close(sockfd);
    return 0;
}

小技巧:客户端也可以调用connect(),虽然UDP是无连接的,但connect之后可以用send()和recv()代替sendto()和recvfrom(),代码会简洁一些。不过要注意,connect之后就只能和这一个地址通信了。

五、UDP的广播与组播

这是UDP独有的能力。TCP是点对点的,UDP可以一对多。

广播

广播地址通常是xxx.xxx.xxx.255,或者255.255.255.255。发送广播前,需要设置socket选项:

int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

然后往广播地址发数据,同一网段的所有机器都能收到。

组播

组播比广播更灵活。它用D类地址(224.0.0.0 ~ 239.255.255.255)。加入组播组需要:

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));

经验之谈:我在做视频监控项目时,用组播把摄像头画面分发给多个客户端。如果当时用TCP,每个客户端都要单独建连接,服务器压力会大很多。组播一发,所有客户端都能收到,省事多了。

六、UDP的丢包与乱序

UDP不保证可靠传输,所以丢包和乱序是家常便饭。为什么会这样?因为UDP没有确认机制,也没有序号机制。

丢包的原因

  • 网络拥塞,路由器丢包
  • 接收缓冲区满了
  • 数据包损坏被丢弃

应对策略

如果应用层需要可靠性,就得自己实现。我常用的方法:

  • 超时重传:发送后启动定时器,没收到确认就重发
  • 序号机制:每个包加个序号,接收方检测乱序和重复
  • 应用层ACK:自己实现确认包

避坑指南:我曾经在一个项目中,直接用UDP传文件,结果大文件传输时丢包率高达30%。后来加了简单的超时重传机制,才把丢包率降到可接受范围。记住,UDP不保证可靠,应用层必须自己处理。

七、UDP与TCP的选择

场景 推荐协议 原因
文件传输、网页浏览 TCP 需要可靠传输
视频直播、语音通话 UDP 容忍丢包,要求低延迟
DNS查询 UDP 一次请求一次响应,简单高效
物联网传感器数据 UDP 数据量小,连接开销不划算
远程控制 TCP 命令必须可靠到达

说白了,选TCP还是UDP,就看你能不能接受丢包。能接受丢包,就用UDP,快;不能接受,就用TCP,稳。

八、UDP Socket综合案例

最后,我写一个简单的聊天程序,展示UDP的实际应用。这个程序既可以是服务端,也可以是客户端,启动时通过参数指定模式。

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

#define PORT 9999
#define BUFFER_SIZE 1024

void *receive_thread(void *arg) {
    int sockfd = *(int*)arg;
    struct sockaddr_in from_addr;
    socklen_t addr_len = sizeof(from_addr);
    char buffer[BUFFER_SIZE];

    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
                         (struct sockaddr*)&from_addr, &addr_len);
        if (n > 0) {
            printf("\n[%s:%d] %s\n> ",
                   inet_ntoa(from_addr.sin_addr),
                   ntohs(from_addr.sin_port),
                   buffer);
            fflush(stdout);
        }
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    int sockfd;
    struct sockaddr_in addr;
    pthread_t tid;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(PORT);

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sockfd);
        exit(1);
    }

    printf("Chat started on port %d\n", PORT);
    printf("Enter messages (format: IP:message)\n");

    // 启动接收线程
    pthread_create(&tid, NULL, receive_thread, &sockfd);

    // 主线程负责发送
    char input[BUFFER_SIZE];
    while (1) {
        printf("> ");
        fflush(stdout);
        fgets(input, BUFFER_SIZE, stdin);
        input[strcspn(input, "\n")] = 0;

        // 解析目标IP和消息
        char *ip = strtok(input, ":");
        char *msg = strtok(NULL, "");

        if (ip && msg) {
            struct sockaddr_in dest;
            dest.sin_family = AF_INET;
            dest.sin_addr.s_addr = inet_addr(ip);
            dest.sin_port = htons(PORT);

            sendto(sockfd, msg, strlen(msg), 0,
                   (struct sockaddr*)&dest, sizeof(dest));
        }
    }

    close(sockfd);
    return 0;
}

使用说明:启动两个终端,都运行这个程序。在其中一个输入"127.0.0.1:你好",另一个就能收到消息。这就是一个最简单的UDP聊天室。

UDP虽然简单,但用好了威力很大。我做了这么多年嵌入式,UDP在音视频传输、传感器数据采集、游戏通信这些场景下,几乎是首选。记住它的特点,选对场景,UDP就是一把利器。

公众号:蓝海资料掘金营,微信deep3321