📅 Day 2:另一种选择 —— 轻快高效的UDP
🎯 今日目标
- 深入理解 TCP 与 UDP 的核心差异
- 掌握 UDP 套接字编程的基本流程
- 实现一个基于 UDP 的在线词典服务器
- 理解 UDP 无连接通信的特点和应用场景
🧠 第一部分:理论基础
📘 TCP vs UDP 深度对比
特性 | TCP | UDP |
---|---|---|
连接性 | 面向连接(需要建立连接) | 无连接(直接发送数据) |
可靠性 | 可靠(确认、重传、流量控制) | 不可靠(尽力而为) |
速度 | 慢,开销大 | 快,开销小 |
数据边界 | 无(流式数据) | 有(数据报) |
应用场景 | Web、文件传输、邮件 | 视频、语音、游戏直播、DNS查询 |
🎯 UDP 的优势与劣势
优势:
- 无需建立连接,通信开销小
- 实时性好,延迟低
- 支持一对多通信(广播、组播)
劣势:
- 不保证数据到达
- 不保证数据顺序
- 不提供流量控制和拥塞控制
🧠 第二部分:重点函数详解
UDP 特有的关键函数
函数 | 功能 | 参数详细说明 | 返回值 |
---|---|---|---|
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) | 向指定地址发送UDP数据报 | sockfd : 套接字描述符;buf : 发送缓冲区;len : 发送长度;flags : 发送标志;dest_addr : 目标地址;addrlen : 地址结构体大小 | 成功返回发送字节数,失败返回-1 |
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) | 接收UDP数据报并获取发送方地址 | sockfd : 套接字描述符;buf : 接收缓冲区;len : 缓冲区大小;flags : 接收标志;src_addr : 发送方地址结构体;addrlen : 地址结构体大小指针 | 成功返回接收字节数,失败返回-1 |
与TCP的主要区别
特性 | TCP | UDP |
---|---|---|
套接字类型 | SOCK_STREAM | SOCK_DGRAM |
连接建立 | 需要 connect() | 不需要 |
数据接收 | recv() | recvfrom() |
数据发送 | send() | sendto() |
监听连接 | 需要 listen() 和 accept() | 不需要 |
🧱 第三部分:UDP 编程框架
UDP 服务器开发流程
1. 创建套接字 socket()
2. 绑定地址 bind()
3. 循环收发数据 recvfrom() / sendto()
4. 关闭套接字 close()
UDP 客户端开发流程
1. 创建套接字 socket()
2. 循环收发数据 sendto() / recvfrom()
3. 关闭套接字 close()
💻 第四部分:动手实战 —— UDP 在线词典
🧩 功能说明
- 服务器内置简单的键值对词典(如 “hello” -> “你好”)
- 客户端输入英文单词发送给服务器
- 服务器查询词典并返回对应中文释义
- 如果查不到,返回 “Not Found”
📁 文件结构
udp_server.c
udp_client.c
🖥️ udp_server.c(服务器端)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888 // 服务器监听端口号
#define MAX_BUFFER 1024 // 缓冲区大小
// 简单词典结构
typedef struct {
char word[50]; // 英文单词
char meaning[100]; // 中文释义
} Dictionary;
// 词典数据
Dictionary dict[] = {
{"hello", "你好"},
{"world", "世界"},
{"computer", "计算机"},
{"network", "网络"},
{"programming", "编程"}
};
int dict_size = sizeof(dict) / sizeof(Dictionary); // 词典大小
// 查找单词函数
char* lookup_word(char* word) {
for (int i = 0; i < dict_size; i++) {
if (strcmp(dict[i].word, word) == 0) {
return dict[i].meaning; // 找到返回释义
}
}
return "Not Found"; // 未找到返回"Not Found"
}
int main() {
int server_fd; // 服务器套接字描述符
struct sockaddr_in server_addr, client_addr; // 服务器和客户端地址结构体
socklen_t client_len = sizeof(client_addr); // 客户端地址结构体大小
char buffer[MAX_BUFFER]; // 数据缓冲区
// 1. 创建UDP套接字
// 函数原型:int socket(int domain, int type, int protocol);
// 参数说明:
// domain: 地址族,AF_INET表示IPv4
// type: 套接字类型,SOCK_DGRAM表示UDP数据报套接字
// protocol: 协议,0表示使用默认协议
// 返回值:成功返回套接字描述符,失败返回-1
server_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址结构
memset(&server_addr, 0, sizeof(server_addr)); // 清零地址结构体
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有本地IP地址
server_addr.sin_port = htons(PORT); // 设置端口号(网络字节序)
// 3. 绑定地址
// 函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 参数说明:
// sockfd: 套接字描述符
// addr: 指向地址结构体的指针
// addrlen: 地址结构体的大小
// 返回值:成功返回0,失败返回-1
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("UDP词典服务器启动,监听端口 %d...\n", PORT);
// 4. 循环处理客户端请求
while (1) {
// 函数原型: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: 地址结构体大小的指针
// 返回值:成功返回接收到的字节数,失败返回-1
int n = recvfrom(server_fd, buffer, MAX_BUFFER, 0,
(struct sockaddr *)&client_addr, &client_len);
if (n <= 0) continue; // 接收失败,继续等待
buffer[n] = '\0'; // 添加字符串结束符
printf("收到来自 %s:%d 的查询: %s",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 查找单词释义
char* result = lookup_word(buffer);
// 函数原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
// const struct sockaddr *dest_addr, socklen_t addrlen);
// 参数说明:
// sockfd: 服务器套接字
// buf: 发送数据的缓冲区
// len: 发送数据的长度
// flags: 发送标志,通常为0
// dest_addr: 目标地址结构体
// addrlen: 地址结构体大小
// 返回值:成功返回发送的字节数,失败返回-1
sendto(server_fd, result, strlen(result), 0,
(struct sockaddr *)&client_addr, client_len);
printf(" 返回结果: %s\n", result);
}
close(server_fd);
return 0;
}
🖥️ udp_client.c(客户端)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1" // 服务器IP地址(本地回环地址)
#define PORT 8888 // 服务器端口号
#define MAX_BUFFER 1024 // 缓冲区大小
int main() {
int client_fd; // 客户端套接字描述符
struct sockaddr_in server_addr; // 服务器地址结构体
socklen_t server_len = sizeof(server_addr); // 服务器地址结构体大小
char buffer[MAX_BUFFER]; // 数据缓冲区
// 1. 创建UDP套接字
// 函数原型:int socket(int domain, int type, int protocol);
// 参数说明:
// domain: 地址族,AF_INET表示IPv4
// type: 套接字类型,SOCK_DGRAM表示UDP数据报套接字
// protocol: 协议,0表示使用默认协议
// 返回值:成功返回套接字描述符,失败返回-1
client_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (client_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr)); // 清零地址结构体
server_addr.sin_family = AF_INET; // IPv4地址族
server_addr.sin_port = htons(PORT); // 端口号(网络字节序)
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 服务器IP地址
printf("UDP词典客户端启动!输入单词查询释义,输入'exit'退出。\n");
// 3. 循环查询单词
while (1) {
printf("请输入要查询的单词: ");
fgets(buffer, MAX_BUFFER, stdin);
// 移除换行符
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "exit") == 0) break; // 输入exit退出
// 发送查询请求到服务器
// 函数原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
// const struct sockaddr *dest_addr, socklen_t addrlen);
// 参数说明:
// sockfd: 客户端套接字
// buf: 发送数据的缓冲区
// len: 发送数据的长度
// flags: 发送标志,通常为0
// dest_addr: 目标地址结构体(服务器地址)
// addrlen: 地址结构体大小
// 返回值:成功返回发送的字节数,失败返回-1
sendto(client_fd, buffer, strlen(buffer), 0,
(struct sockaddr *)&server_addr, server_len);
// 接收服务器返回的结果
// 函数原型: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: 用于存储发送方地址信息的结构体(这里可以为NULL)
// addrlen: 地址结构体大小的指针(这里可以为NULL)
// 返回值:成功返回接收到的字节数,失败返回-1
int n = recvfrom(client_fd, buffer, MAX_BUFFER, 0, NULL, NULL);
if (n <= 0) {
printf("接收数据失败\n");
continue;
}
buffer[n] = '\0'; // 添加字符串结束符
printf("查询结果: %s\n\n", buffer);
}
close(client_fd);
return 0;
}
🛠️ 第五部分:编译与运行
编译命令详解
# 编译UDP服务器程序
gcc udp_server.c -o udp_server
# 编译UDP客户端程序
gcc udp_client.c -o udp_client
运行方式
# 终端1:启动UDP服务器
./udp_server
# 终端2:启动UDP客户端
./udp_client
🧪 第六部分:测试与验证
示例交互
客户端输入:
hello
computer
xyz
exit
服务器输出:
UDP词典服务器启动,监听端口 8888...
收到来自 127.0.0.1:54321 的查询: hello 返回结果: 你好
收到来自 127.0.0.1:54321 的查询: computer 返回结果: 计算机
收到来自 127.0.0.1:54321 的查询: xyz 返回结果: Not Found
客户端输出:
UDP词典客户端启动!输入单词查询释义,输入'exit'退出。
请输入要查询的单词: hello
查询结果: 你好
请输入要查询的单词: computer
查询结果: 计算机
请输入要查询的单词: xyz
查询结果: Not Found
请输入要查询的单词: exit
📌 第七部分:UDP 编程注意事项
⚠️ 常见问题及解决方案
-
数据丢失问题
- UDP不保证数据到达,需要应用层实现重传机制
- 解决方案:添加确认和重传逻辑
-
数据包乱序问题
- UDP不保证数据顺序,可能后发送的数据先到达
- 解决方案:在数据包中添加序列号
-
数据包大小限制
- UDP数据包有大小限制(通常64KB)
- 解决方案:分割大数据包或使用TCP
-
广播和组播支持
- UDP天然支持广播和组播通信
- 可用于局域网发现、实时通信等场景
🎯 第八部分:UDP 应用场景
🎮 典型应用场景
应用类型 | 说明 | 为什么用UDP |
---|---|---|
实时游戏 | 网络游戏、实时对战 | 对延迟敏感,允许少量数据丢失 |
音视频传输 | 视频直播、语音通话 | 实时性要求高,少量丢包不影响体验 |
DNS查询 | 域名解析 | 查询响应快,无连接开销 |
广播通信 | 局域网发现、时间同步 | 支持一对多通信 |
IoT设备通信 | 传感器数据上报 | 数据量小,对实时性要求高 |
📌 总结
✅ 今天你学会了:
- TCP 与 UDP 的核心差异和各自适用场景
- UDP 套接字编程的基本流程和关键函数
sendto()
和recvfrom()
函数的详细使用方法- 实现了一个功能完整的 UDP 在线词典服务器
- 理解了 UDP 无连接通信的特点和优势
🎯 明天我们将学习如何让服务器支持多个客户端并发连接,进入并发服务器的世界!
📌 Tips:
- UDP 编程中要注意数据的可靠性和顺序问题
- 多使用
man
命令查看函数文档 - 理解 UDP 的无连接特性,这是它与 TCP 的根本区别
- 实际项目中要根据业务需求选择合适的传输协议
👉 点个赞和关注,更多知识不迷路!!明天见!Day 3 更精彩:从一对一到一对多 —— 经典的并发服务器!