UDP协议

UDP简介

核心定义

UDP(User Datagram Protocol)即用户数据报协议,是面向无连接的传输层协议,因无需连接建立、确认机制,被称为“不可靠协议”(无法确保接收方是否收到数据)。

核心特征

  • 无连接:通信前无需建立连接,资源占用少
  • 无确认:接收数据后不向发送方返回回执
  • 无序性:不保证数据传输顺序,丢失不重发
  • 帧同步:通信双方每次收发数据量相等(数据报形式)
  • 高效率:无需缓冲区、分包序列号,传输速度快

适用场景

  • 小尺寸数据传输(如DNS查询、NTP时间同步)
  • 无线网络等难应答场景
  • 广播、组播模式(如局域网设备发现)
  • 实时数据传输(即时通讯文本/音视频、流媒体、VoIP、IPTV)
图示

类比:UDP类似“寄信”,寄件人无法确认收件人是否签收,但流程简单、效率高。


UDP通信流程

整体流程框架

角色 核心步骤
发送端(客户端) 1. 创建UDP套接字 → 2. 设置接收端地址 → 3. 发送数据 → 4. 关闭套接字
接收端(服务器) 1. 创建UDP套接字 → 2. 绑定本地地址 → 3. 接收数据 → 4. 关闭套接字
图示

发送端详细步骤

  1. 创建UDP通信对象:通过socket()函数创建套接字,指定协议族和类型
  2. 设置目标地址:定义struct sockaddr_in结构体,填写接收端IP和端口(需字节序转换)
  3. 发送数据报:通过sendto()函数发送数据,指定目标地址和地址长度
  4. 关闭资源:通过close()关闭套接字

接收端详细步骤

  1. 创建UDP通信对象:与发送端一致,使用socket()函数
  2. 绑定本地地址:通过bind()函数将套接字与本地IP、端口绑定(对外公布联系方式)
  3. 接收数据报:通过recvfrom()函数阻塞等待数据,可获取发送方地址(可选)
  4. 关闭资源:通过close()关闭套接字

注意:接收端必须绑定地址,否则无法被发送端定位;发送端可无需绑定(系统自动分配临时端口)。


关键API接口

套接字创建(socket)

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 参数说明:
    • domain:通信协议族(AF_INET=IPv4,AF_UNIX=本地通信)
    • type:套接字类型(SOCK_DGRAM=UDP,SOCK_STREAM=TCP)
    • protocol:协议控制(默认0,自动匹配前两个参数)
  • 返回值:成功返回文件描述符,失败返回-1

数据发送(sendto)

#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:目标地址结构体长度(sizeof(dest_addr)
  • 返回值:成功返回发送字节数,失败返回-1

数据接收(recvfrom)

#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buffer, size_t length,
                 int flags, struct sockaddr *restrict address,
                 socklen_t *restrict address_len);
  • 参数说明:
    • sockfd:UDP套接字文件描述符
    • buffer:存储接收数据的缓冲区
    • length:缓冲区最大容量
    • flags:接收标记(默认0)
    • address:发送方地址结构体(可选,NULL表示不获取)
    • address_len:发送方地址长度(可选,NULL表示不获取)
  • 返回值:成功返回接收字节数,失败返回-1
  • 特性:无数据时会阻塞等待

地址绑定(bind)

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数说明:
    • sockfd:需要绑定的套接字
    • addr:本地地址结构体(IP+端口)
    • addrlen:地址结构体长度
  • 返回值:成功返回0,失败返回-1

地址结构体(sockaddr_in)

#include <netinet/in.h>
struct sockaddr_in {
    sa_family_t sin_family;   // 协议族(AF_INET)
    in_port_t sin_port;       // 端口号(网络字节序)
    struct in_addr sin_addr;  // IP地址结构体
};

struct in_addr {
    uint32_t s_addr;          // IP地址(网络字节序)
};

字节序与IP转换函数

#include <arpa/inet.h>
// 字符串IP → 网络字节序IP
in_addr_t inet_addr(const char *cp);  // 如"192.168.9.1" → 网络字节序
int inet_aton(const char *cp, struct in_addr *inp);

// 网络字节序IP → 字符串IP
char *inet_ntoa(struct in_addr in);  // 如网络字节序 → "192.168.9.1"

// 主机字节序 ↔ 网络字节序(端口/IP转换)
uint16_t htons(uint16_t hostshort);  // 主机→网络(端口,16位)
uint32_t htonl(uint32_t hostlong);   // 主机→网络(IP,32位)
uint16_t ntohs(uint16_t netshort);   // 网络→主机(端口)
uint32_t ntohl(uint32_t netlong);    // 网络→主机(IP)

基础示例代码

UDP发送端(客户端)

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

int main() {
    // 1. 创建UDP对象
    int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_socket < 0) {
        perror("udp socket error");
        return -1;
    }
    perror("udp socket");

    // 2. 设置接收端地址
    struct sockaddr_in dest_addr;
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(8888);  // 目标端口(网络字节序)
    dest_addr.sin_addr.s_addr = inet_addr("192.168.9.1");  // 目标IP

    // 3. 循环发送数据
    char send_buf[128] = {0};
    while (1) {
        bzero(send_buf, sizeof(send_buf));
        fgets(send_buf, sizeof(send_buf), stdin);  // 从键盘读取数据
        int ret = sendto(udp_socket, send_buf, strlen(send_buf), 0,
                        (struct sockaddr *)&dest_addr, sizeof(dest_addr));
        if (ret > 0) {
            printf("成功发送%d字节数据\n", ret);
        }
    }

    // 4. 关闭套接字(实际不会执行,需手动终止)
    close(udp_socket);
    return 0;
}

UDP接收端(服务器)

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

int main() {
    // 1. 创建UDP对象
    int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_socket < 0) {
        perror("udp socket error");
        return -1;
    }
    perror("udp socket");

    // 2. 设置本地地址并绑定
    struct sockaddr_in my_addr;
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(8888);  // 本地端口
    my_addr.sin_addr.s_addr = inet_addr("192.168.9.1");  // 本地IP(0.0.0.0 =INADDR_ANY)换作0可以接收所有人的信息

    if (bind(udp_socket, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
        perror("bind error");
        return -1;
    }
    perror("bind");

    // 3. 循环接收数据
    char recv_buf[1024] = {0};
    struct sockaddr_in send_addr;  // 存储发送方地址
    socklen_t addr_len = sizeof(send_addr);
    while (1) {
        bzero(recv_buf, sizeof(recv_buf));
        int ret = recvfrom(udp_socket, recv_buf, sizeof(recv_buf), 0,
                          (struct sockaddr *)&send_addr, &addr_len);
        if (ret > 0) {
            // 转换发送方IP和端口为本地字节序
            char *send_ip = inet_ntoa(send_addr.sin_addr);
            int send_port = ntohs(send_addr.sin_port);
            printf("[%s:%d] 接收%d字节:%s\n", send_ip, send_port, ret, recv_buf);
        }
    }

    // 4. 关闭套接字
    close(udp_socket);
    return 0;
}

UDP双向通信实现

// 接收端(同时支持发送响应)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

int main() {
    int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_socket < 0) { perror("socket error"); return -1; }

    struct sockaddr_in my_addr = {AF_INET, htons(8888), inet_addr("192.168.9.1")};
    if (bind(udp_socket, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0) {
        perror("bind error"); return -1;
    }

    char recv_buf[1024] = {0}, send_buf[1024] = {0};
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);

    while (1) {
        // 接收客户端数据
        bzero(recv_buf, sizeof(recv_buf));
        int recv_len = recvfrom(udp_socket, recv_buf, sizeof(recv_buf), 0,
                               (struct sockaddr *)&client_addr, &addr_len);
        if (recv_len <= 0) continue;
        printf("客户端[%s:%d]:%s", inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port), recv_buf);

        // 发送响应数据
        bzero(send_buf, sizeof(send_buf));
        printf("请输入响应:");
        fgets(send_buf, sizeof(send_buf), stdin);
        sendto(udp_socket, send_buf, strlen(send_buf), 0,
              (struct sockaddr *)&client_addr, addr_len);
    }

    close(udp_socket);
    return 0;
}

UDP高级功能

广播功能

广播定义

向当前网络中所有主机发送数据,只需往网络的广播地址发送UDP数据包(如192.168.9.255)。

广播实现步骤

  1. 创建UDP套接字(同基础流程)
  2. 开启广播功能(通过setsockopt()设置SO_BROADCAST属性)
  3. 设置广播地址(目标IP为网络广播地址,端口统一)
  4. 发送广播数据(使用sendto()

广播示例代码

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

int main() {
    // 1. 创建UDP对象
    int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_socket < 0) { perror("socket error"); return -1; }

    // 2. 开启广播功能
    int broadcast_flag = 1;
    if (setsockopt(udp_socket, SOL_SOCKET, SO_BROADCAST,
                   &broadcast_flag, sizeof(broadcast_flag)) < 0) {
        perror("开启广播失败");
        close(udp_socket);
        return -1;
    }
    printf("开启广播成功\n");

    // 3. 设置广播地址
    struct sockaddr_in broadcast_addr;
    broadcast_addr.sin_family = AF_INET;
    broadcast_addr.sin_port = htons(8888);
    broadcast_addr.sin_addr.s_addr = inet_addr("192.168.9.255");  // 广播地址

    // 4. 发送广播数据
    char *msg = "Hello, 所有主机!";
    int ret = sendto(udp_socket, msg, strlen(msg), 0,
                    (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
    if (ret > 0) {
        printf("广播发送成功,字节数:%d\n", ret);
    } else {
        perror("广播发送失败");
    }

    close(udp_socket);
    return 0;
}

网络属性设置(setsockopt/getsockopt)

常用属性列表

属性层次 属性名称 数据格式 说明
SOL_SOCKET SO_DEBUG bool 开启网络调试信息
SOL_SOCKET SO_REUSEADDR bool 关闭后立即重用本地地址和端口
SOL_SOCKET SO_BROADCAST bool 开启广播功能
SOL_SOCKET SO_RCVBUF int 接收缓冲区大小
SOL_SOCKET SO_SNDBUF int 发送缓冲区大小
SOL_SOCKET SO_RCVTIMEO struct timeval 接收数据超时时间
SOL_SOCKET SO_SNDTIMEO struct timeval 发送数据超时时间
IPPROTO_IP IP_TTL int 数据包生存时间(跳数限制)

缓冲区大小设置

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

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) { perror("socket error"); return -1; }

    // 获取默认缓冲区大小
    int sndbuf, rcvbuf;
    socklen_t len = sizeof(sndbuf);
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &len);
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);
    printf("默认发送缓冲区:%d,接收缓冲区:%d\n", sndbuf, rcvbuf);

    // 设置缓冲区大小
    sndbuf = 2048;
    rcvbuf = 4096;
    setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

    // 验证设置结果
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &len);
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);
    printf("设置后发送缓冲区:%d,接收缓冲区:%d\n", sndbuf, rcvbuf);

    close(sockfd);
    return 0;
}

接收超时设置

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

int main() {
    int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_socket < 0) { perror("socket error"); return -1; }

    // 设置接收超时为5秒
    struct timeval tv;
    tv.tv_sec = 5;    // 秒
    tv.tv_usec = 0;   // 微秒
    if (setsockopt(udp_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
        perror("设置超时失败");
        close(udp_socket);
        return -1;
    }

    // 绑定地址(省略,同接收端基础代码)
    // ...

    // 接收数据(5秒内无数据则返回错误)
    char buf[1024] = {0};
    struct sockaddr_in send_addr;
    socklen_t addr_len = sizeof(send_addr);
    int ret = recvfrom(udp_socket, buf, sizeof(buf), 0,
                      (struct sockaddr *)&send_addr, &addr_len);
    if (ret < 0) {
        perror("接收超时或失败");
        close(udp_socket);
        return -1;
    }

    printf("接收数据:%s\n", buf);
    close(udp_socket);
    return 0;
}

地址端口复用(解决Address already in use)

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

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) { perror("socket error"); return -1; }

    // 开启地址端口复用
    int reuse_flag = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                   &reuse_flag, sizeof(reuse_flag)) < 0) {
        perror("开启复用失败");
        close(sockfd);
        return -1;
    }

    // 绑定地址(即使端口被TIME_WAIT占用也能绑定成功)
    // ...

    close(sockfd);
    return 0;
}

数据校验

数据校验图

网络检索接口

常用检索API列表

序号 API名称 功能描述
1 gethostname() 获取本地主机名
2 getsockname() 获取本端套接字绑定的地址信息
3 getpeername() 获取对端套接字的地址信息(仅TCP连接)
4 gethostbyname() 根据域名获取主机IP地址(如www.baidu.com)
5 gethostbyaddr() 根据IP地址获取主机信息
6 getprotobyname() 根据协议名获取协议号(如"udp"→17)
7 getservbyname() 根据服务名获取端口号(如"http"→80)

示例:根据域名获取IP(gethostbyname)

#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    // 获取百度域名的IP地址
    struct hostent *host_info = gethostbyname("www.baidu.com");
    if (host_info == NULL) { perror("获取主机信息失败"); return -1; }

    // 遍历所有IP地址(一个域名可能对应多个IP)
    printf("www.baidu.com 的IP地址:\n");
    for (int i = 0; host_info->h_addr_list[i] != NULL; i++) {
        struct in_addr *ip_addr = (struct in_addr *)host_info->h_addr_list[i];
        printf("%s\n", inet_ntoa(*ip_addr));
    }

    return 0;
}

示例:获取本端套接字地址(getsockname)

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

int main() {
    int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (udp_socket < 0) { perror("socket error"); return -1; }

    // 绑定本地地址
    struct sockaddr_in my_addr = {AF_INET, htons(8888), inet_addr("192.168.9.1")};
    bind(udp_socket, (struct sockaddr *)&my_addr, sizeof(my_addr));

    // 获取绑定的地址信息
    struct sockaddr_in bound_addr;
    socklen_t addr_len = sizeof(bound_addr);
    getsockname(udp_socket, (struct sockaddr *)&bound_addr, &addr_len);

    printf("本端绑定IP:%s\n", inet_ntoa(bound_addr.sin_addr));
    printf("本端绑定端口:%d\n", ntohs(bound_addr.sin_port));

    close(udp_socket);
    return 0;
}

UDP与TCP对比

特性 UDP TCP
连接方式 无连接(无需建立/释放连接) 面向连接(三次握手建立,四次挥手释放)
可靠性 不可靠(无确认、无重传) 可靠(确认、重传、有序、流量控制)
数据传输方式 数据报(帧同步,大小固定) 字节流(无边界,需手动分包)
效率 高(资源占用少,无额外开销) 低(连接管理、确认机制开销大)
适用场景 实时数据、广播、小数据传输 文件传输、HTTP通信、可靠数据传输
头部开销 小(8字节) 大(20字节基础头部)
阻塞特性 仅recvfrom()阻塞(无数据时) 连接建立、recv()、send()均可能阻塞
TCP三次握手/四次挥手流程图
UDP与TCP传输模式对比图

远程通信与SSH配置(阿里云服务器)

服务器准备步骤

  1. 注册阿里云账号,购买/免费试用ECS云服务器
  2. 选择操作系统(推荐Ubuntu 22.04、CentOS 7.9)
  3. 记录服务器公网IP(如8.138.175.40)和登录账号密码

SSH远程连接(VS Code)

  1. 安装VS Code插件“Remote - SSH”
  2. 点击插件图标 → “Connect to Host” → 输入ssh 用户名@公网IP(如ssh ubuntu@8.138.175.40
  3. 输入服务器密码,连接成功后即可远程编辑代码、编译运行

安全组配置(开放端口)

  1. 登录阿里云ECS控制台 → 网络与安全 → 安全组
  2. 手动添加规则:
    • 协议类型:自定义UDP/TCP
    • 端口范围:1/65535(或指定端口如8888)
    • 授权对象:0.0.0.0/0(允许所有IP访问,生产环境需限制)
  3. 保存规则,确保UDP/TCP数据能通过服务器端口传输

远程UDP通信测试

  • 服务器端:运行UDP接收端代码(绑定公网IP或0.0.0.0,端口8888)
  • 本地端:运行UDP发送端代码(目标IP为服务器公网IP,端口8888)
  • 验证:本地发送数据,服务器能接收则说明配置成功

UDP常见问题排查

绑定失败(Address already in use)

  • 原因:端口被占用(如程序异常退出后端口处于TIME_WAIT状态)
  • 解决:开启SO_REUSEADDR属性(地址端口复用),或更换端口

数据发送成功但接收不到

  • 原因1:目标IP/端口错误(确认接收端绑定地址正确)
  • 原因2:防火墙/安全组未开放端口(检查服务器安全组、本地防火墙)
  • 原因3:广播数据未开启SO_BROADCAST属性(广播场景)
  • 解决:逐一排查IP、端口、防火墙配置,确认广播功能开启

数据丢失

  • 原因1:缓冲区溢出(接收端缓冲区太小,未及时读取数据)
  • 原因2:网络拥堵(UDP无流量控制,数据包被丢弃)
  • 原因3:数据报超过MTU大小(默认1500字节,超过会分片丢失)
  • 解决:增大接收缓冲区、控制发送速率、拆分大数据报

接收端阻塞无响应

  • 原因1:未开启超时设置(recvfrom()一直阻塞)
  • 原因2:发送端未发送数据或发送失败
  • 解决:设置SO_RCVTIMEO超时属性,检查发送端代码和网络连通性

posted @ 2025-12-09 08:37  Jaklin  阅读(107)  评论(0)    收藏  举报