应用——UDP Socket 编程笔记

UDP Socket 编程笔记

一、UDP 基础知识

1. UDP 特点

  • 无连接:无需建立连接即可通信

  • 不可靠:不保证数据到达、不保证顺序

  • 面向数据报:有明确的报文边界

  • 高效:开销小,速度快

2. TCP vs UDP

特性 TCP UDP
连接方式 面向连接 无连接
可靠性 可靠传输 不可靠
数据边界 流式(无边界) 数据报(有边界)
速度 较慢 较快
头部大小 20-60字节 8字节

3. 核心数据结构(同TCP)

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

总结:UDP编程相对TCP更简单,但需要应用层处理可靠性问题。适合实时性要求高、可容忍少量丢失的场景(如视频流、DNS查询、在线游戏等)。

二、UDP 编程核心函数

1. 发送数据

复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • dest_addr: 目标地址结构体

  • addrlen: 地址结构体长度

  • 返回:实际发送的字节数,-1表示错误

2. 接收数据

复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • src_addr: 用于保存发送方地址

  • addrlen: 输入输出参数,需要初始化

  • 返回:实际接收的字节数,-1表示错误,0表示对方关闭连接

三、示例程序分析

示例1:简单回显服务器(带时间戳)

服务器端 (server.c)
cpp 复制代码
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
    // 1. 创建UDP套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 绑定地址
    struct sockaddr_in ser, cli;
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("192.168.14.128");
    
    bind(sockfd, (SA)&ser, sizeof(ser));
    
    // 3. 循环处理客户端请求
    socklen_t len = sizeof(cli);
    while (1) {
        char buf[512] = {0};
        
        // 接收数据(获取客户端地址)
        recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);
        printf("recv:%s\n", buf);
        
        // 添加时间戳
        time_t tm;
        time(&tm);
        struct tm *info = localtime(&tm);
        sprintf(buf, "%s %d:%d:%d", buf, info->tm_hour, 
                info->tm_min, info->tm_sec);
        
        // 发送回客户端
        sendto(sockfd, buf, strlen(buf) + 1, 0, (SA)&cli, len);
    }
    
    close(sockfd);
    return 0;
}
客户端 (cli.c)
cpp 复制代码
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
    // 1. 创建UDP套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 设置服务器地址
    struct sockaddr_in ser;
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("192.168.14.128");
    
    // 3. 发送10次数据
    int i = 10;
    while (i--) {
        char buf[512] = "hello";
        
        // 发送数据
        sendto(sockfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));
        
        // 接收响应
        bzero(buf, sizeof(buf));
        recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
        printf("recv:%s\n", buf);
        
        sleep(1);
    }
    
    close(sockfd);
    return 0;
}

示例2:UDP聊天程序(父子进程)

服务器端 (server.c - 聊天版本)
复制代码
// 创建UDP套接字并绑定
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in ser, cli;
// ... 绑定代码 ...

// 首次接收获取客户端地址
char buf[512] = {0};
recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);

// 创建父子进程
pid_t pid = fork();
if (pid > 0) {  // 父进程:发送消息
    while (1) {
        bzero(buf, sizeof(buf));
        printf("to B:");
        fgets(buf, sizeof(buf), stdin);
        sendto(sockfd, buf, strlen(buf) + 1, 0, (SA)&cli, sizeof(cli));
        
        if (0 == strcmp(buf, "#quit\n")) {
            kill(pid, 9);
            exit(0);
        }
    }
} else if (0 == pid) {  // 子进程:接收消息
    while (1) {
        bzero(buf, sizeof(buf));
        recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
        if (0 == strcmp(buf, "#quit\n")) {
            kill(getppid(), 9);
            exit(0);
        }
        printf("from B:%s", buf);
        fflush(stdout);
    }
}
客户端 (cli.c - 聊天版本)
复制代码
// 代码结构与服务器端类似
// 父子进程分别处理发送和接收

实现原理

  1. 父子进程分离了输入和输出

  2. 父进程负责读取用户输入并发送

  3. 子进程负责接收并显示消息

  4. 使用#quit作为退出指令

示例3:UDP文件传输

服务器端 (文件接收)
复制代码
int fd = open("2.png", O_WRONLY | O_CREAT | O_TRUNC, 0666);

while (1) {
    char buf[1024] = {0};
    int rd_ret = recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);
    if (rd_ret <= 0) {
        break;
    }
    write(fd, buf, rd_ret);
    
    // 发送确认
    bzero(buf, sizeof(buf));
    strcpy(buf, "go on");
    sendto(sockfd, buf, strlen(buf), 0, (SA)&cli, len);
}
客户端 (文件发送)
复制代码
int fd = open("/home/linux/1.png", O_RDONLY);
char buf[1024] = {0};

while (1) {
    bzero(buf, sizeof(buf));
    int rd_ret = read(fd, buf, sizeof(buf));
    if (rd_ret <= 0) {
        break;
    }
    
    // 发送文件数据
    sendto(sockfd, buf, rd_ret, 0, (SA)&ser, sizeof(ser));
    
    // 等待确认
    bzero(buf, sizeof(buf));
    recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
}

// 发送结束标志
sendto(sockfd, buf, 0, 0, (SA)&ser, sizeof(ser));

特点

  • 使用固定大小缓冲区传输

  • 简单的"go on"确认机制

  • 发送0长度数据表示传输结束

四、UDP编程注意事项

1. 地址绑定

复制代码
// 监听所有接口
ser.sin_addr.s_addr = INADDR_ANY;

// 监听特定IP
ser.sin_addr.s_addr = inet_addr("192.168.1.100");

// 仅本地通信(回环地址)
ser.sin_addr.s_addr = inet_addr("127.0.0.1");

2. 数据包大小限制

  • UDP数据包最大长度:65535字节

  • 实际受MTU限制(通常1500字节)

  • 建议应用层分片传输大文件

3. 可靠性问题解决方案

复制代码
// 1. 超时重传
struct timeval tv;
tv.tv_sec = 5;  // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

// 2. 序列号机制
typedef struct {
    uint32_t seq;      // 序列号
    uint32_t total;    // 总包数
    char data[1400];   // 数据
} udp_packet_t;

4. 并发处理

UDP本身是无连接的,可以通过以下方式处理多个客户端:

  • 记录每个客户端的地址

  • 使用多线程/多进程

  • 使用select()/poll()/epoll()多路复用

五、完整编程模板

服务器模板

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

#define PORT 50000
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    
    // 3. 绑定地址
    bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    
    printf("UDP Server listening on port %d\n", PORT);
    
    while (1) {
        // 4. 接收数据
        client_len = sizeof(client_addr);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
                        (struct sockaddr*)&client_addr, &client_len);
        
        // 5. 处理数据
        buffer[n] = '\0';
        printf("Received from %s:%d - %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
        
        // 6. 发送响应
        sendto(sockfd, buffer, n, 0,
               (struct sockaddr*)&client_addr, client_len);
    }
    
    close(sockfd);
    return 0;
}

客户端模板

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

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

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    // 2. 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    
    while (1) {
        printf("Enter message: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        
        // 3. 发送数据
        sendto(sockfd, buffer, strlen(buffer), 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr));
        
        // 4. 接收响应
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
        buffer[n] = '\0';
        printf("Server response: %s\n", buffer);
    }
    
    close(sockfd);
    return 0;
}

六、常见错误及解决方法

1. 地址已在使用

复制代码
# 错误:bind: Address already in use
# 解决:
netstat -anp | grep 50000  # 查看占用进程
kill -9 <PID>               # 结束进程
# 或使用 SO_REUSEADDR
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

2. 数据包丢失

  • 实现应用层的确认重传机制

  • 增加超时设置

  • 使用更小的数据包大小

3. 端口选择问题

  • 避免使用知名端口(0-1023)

  • 确保客户端和服务器使用相同端口

七、进阶主题

1. 广播通信

复制代码
// 设置广播权限
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));

// 广播地址
ser.sin_addr.s_addr = inet_addr("255.255.255.255");

2. 组播通信

复制代码
// 加入组播组
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));

3. 非阻塞UDP

复制代码
// 设置非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
相关推荐
init_23612 小时前
label-route-capability
服务器·前端·网络
cnskylee2 小时前
【Nginx】Nginx-1.28.1版本已恢复对CentOS 7的兼容性
运维·nginx·centos
2301_767902642 小时前
Containerd 从入门到实战
运维·容器
是阿威啊2 小时前
【第六站】测试本地项目连接虚拟机上的大数据集群
大数据·linux·hive·hadoop·spark·yarn
EchoL、2 小时前
【论文阅读】SteganoGAN:High Capacity Image Steganography with GANs
论文阅读·人工智能·笔记·算法
YJlio2 小时前
Windows Sysinternals 文件工具学习笔记(12.10):PendMoves + MoveFile 实战——重启后文件替换的安全姿势
windows·笔记·学习
知识分享小能手2 小时前
Ubuntu入门学习教程,从入门到精通, Ubuntu 22.04 文件和目录管理完全指南(7)
linux·学习·ubuntu
De-Alf2 小时前
Megatron-LM学习笔记(5)Model Linear线性层
笔记·学习·ai
Mr-Wanter2 小时前
麒麟V10x86 系统 curl报错SSLv3符号缺失问题解决
linux·服务器·github