这个Demo主要演示了如何在Linux环境下使用UDP协议实现一个简单的回声服务器(Echo Server)以及与之对应的客户端应用程序:
服务器端
#include <stdio.h> // 引入标准输入输出库,用于打印信息
#include <stdlib.h> // 引入标准库,提供exit()等函数
#include <string.h> // 引入字符串处理函数库
#include <unistd.h> // 引入UNIX标准函数库,包含如close()函数
#include <arpa/inet.h> // 引入网络字节顺序转换和网络协议相关函数库
#include <sys/socket.h> // 引入 socket 创建和操作函数库
#define BUF_SIZE 30 // 定义缓冲区大小为30字节
void error_handling(char *message); // 错误处理函数的声明
int main(int argc, char *argv[]) // 程序主入口
{
int serv_sock; // 用于存储服务端socket描述符
char message[BUF_SIZE]; // 定义缓冲区,存储接收到的消息
int str_len; // 存储接收到的数据长度
socklen_t clnt_adr_sz; // 客户端地址结构大小
struct sockaddr_in serv_adr, clnt_adr; // 服务端和客户端地址结构
if(argc != 2){ // 如果命令行参数不等于2(程序名和端口号),则输出用法提示
printf("Usage : %s <port>\\n", argv[0]);
exit(1); // 退出程序
}
// 创建 UDP socket
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == -1) // 如果 socket 创建失败,则调用错误处理函数
error_handling("UDP socket creation error");
// 初始化服务端地址结构
memset(&serv_adr, 0, sizeof(serv_adr)); // 清空地址结构
serv_adr.sin_family = AF_INET; // 设置协议族为 IPv4
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置服务器可以接收任意客户端发送的数据
serv_adr.sin_port = htons(atoi(argv[1])); // 将端口号转换为网络字节顺序,并设置端口
// 绑定 socket 到指定的 IP 地址和端口
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
// 进入循环,等待并处理客户端消息
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); // 关闭 socket
return 0;
}
// 错误处理函数
void error_handling(char *message)
{
fputs(message, stderr); // 将错误信息输出到标准错误流
fputc('\\n', stderr); // 输出换行符
exit(1); // 退出程序
}
这个 UDP 回声服务器的流程可以总结为以下几个步骤:
1. 参数检查
-
服务器启动时,会检查命令行参数是否正确,确保用户输入了端口号。如果参数不正确,程序会输出用法说明并退出。
-
例如,如果命令行输入是 ./server 12345,12345 就是指定的端口号。
2. 创建 UDP Socket
-
使用 socket() 函数创建一个 UDP socket。PF_INET 表示使用 IPv4 协议,SOCK_DGRAM 表示创建一个数据报(UDP)socket。
-
如果 socket 创建失败,调用 error_handling() 函数输出错误信息并退出程序。
3. 绑定 Socket 到端口
-
服务器构建一个sockaddr_in类型的地址结构,并设置:
-
sin_family = AF_INET:指定使用 IPv4 协议。
-
sin_addr.s_addr = htonl(INADDR_ANY):允许服务器监听来自任何 IP 地址的请求。
-
sin_port = htons(atoi(argv[1])):设置服务器监听的端口号。
-
-
使用 bind() 函数将创建的 UDP socket 与该地址结构绑定,确保服务器通过指定的端口接收数据。
-
如果绑定失败,调用 error_handling() 输出错误信息并退出。
4. 进入主循环,等待并处理客户端请求
-
在无限循环中,服务器等待接收客户端发送的数据:
-
使用 recvfrom() 函数从客户端接收数据。该函数将收到的数据存储在 message 缓冲区中,str_len 表示接收到的数据长度。
-
客户端的地址信息(如 IP 地址和端口)被保存在 clnt_adr 中,clnt_adr_sz 保存客户端地址的大小。
-
5. 回送接收到的数据
-
一旦接收到数据,服务器会使用 sendto() 函数将相同的数据发送回客户端。这实现了回声服务器的功能,即将客户端发送的消息原封不动地返回。
6. 重复处理
-
服务器会继续重复步骤 4 和 5,处理下一个客户端发来的数据,直到程序被外部终止。
7. 关闭 Socket
-
虽然这段代码进入了无限循环,但如果退出循环(例如遇到错误或程序被外部信号终止),服务器会关闭 socket,释放资源。
客户端
#include <stdio.h> // 引入标准输入输出库,用于打印信息
#include <stdlib.h> // 引入标准库,提供 exit() 等函数
#include <string.h> // 引入字符串处理函数库
#include <unistd.h> // 引入 UNIX 标准函数库,包含 close() 函数
#include <arpa/inet.h> // 引入网络字节顺序转换和网络协议相关函数库
#include <sys/socket.h> // 引入 socket 创建和操作函数库
#define BUF_SIZE 30 // 定义缓冲区大小为 30 字节
void error_handling(char *message); // 错误处理函数的声明
int main(int argc, char *argv[]) // 程序主入口
{
int sock; // 用于存储客户端 socket 描述符
char message[BUF_SIZE]; // 定义缓冲区,存储发送的消息
int str_len; // 存储接收到的数据长度
socklen_t adr_sz; // 存储服务器地址的大小
struct sockaddr_in serv_adr, from_adr; // 服务器和接收数据的地址结构
if(argc != 3){ // 检查命令行参数是否为 3 个(程序名、服务器 IP 和端口号)
printf("Usage : %s <IP> <port>\\n", argv[0]);
exit(1); // 如果参数不正确,则退出程序
}
// 创建 UDP socket
sock = socket(PF_INET, SOCK_DGRAM, 0);
if(sock == -1) // 如果 socket 创建失败,则调用错误处理函数
error_handling("socket() error");
// 初始化服务器地址结构
memset(&serv_adr, 0, sizeof(serv_adr)); // 清空地址结构
serv_adr.sin_family = AF_INET; // 设置协议族为 IPv4
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); // 从标准输入读取用户消息
// 如果用户输入 "q" 或 "Q",则退出循环
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); // 关闭 socket
return 0;
}
// 错误处理函数
void error_handling(char *message)
{
fputs(message, stderr); // 将错误信息输出到标准错误流
fputc('\\n', stderr); // 输出换行符
exit(1); // 退出程序
}
客户端的流程如下:
1.命令行参数检查
首先检查输入的参数是否正确(需要提供服务器的 IP 地址和端口号)。如果不正确,则输出用法说明并退出。
2.创建 UDP Socket
通过 socket() 函数创建一个 UDP socket,准备与服务器进行通信。如果创建失败,则调用 error_handling() 输出错误信息并退出。
3.配置服务器地址
设置 sockaddr_in 结构体,配置服务器的 IP 地址和端口号。inet_addr() 函数将输入的 IP 地址转换为网络字节顺序,htons() 函数将端口号转换为网络字节顺序。
4.消息发送与接收
-
进入一个无限循环,提示用户输入消息。
-
用户输入的消息通过 sendto() 发送到服务器。如果输入的是 "q" 或 "Q",则退出循环。
-
客户端等待服务器回显消息,通过 recvfrom() 函数接收服务器的回应。
-
接收到服务器的消息后,输出到屏幕上。
5.关闭 Socket
当退出循环后,关闭 socket 连接,结束客户端程序。
程序执行
void main(char* argv0) { // 程序的主入口,接收程序的名称作为参数(argv0)
pid_t pid = fork(); // 创建子进程,fork() 会返回两次,父进程返回子进程的 PID,子进程返回 0
if (pid > 0) { // 如果 pid 大于 0,表示当前代码在父进程中执行
// 主进程部分
int argc = 3; // 主进程的参数个数设为 3(程序名、IP 地址、端口号)
// 构造主进程的参数数组,用于调用 client 函数
char* argv[] = {
argv0, // 程序名(从参数传入)
(char*)"127.0.0.1", // 服务器的 IP 地址
(char*)"9999", // 服务器的端口号
NULL // 确保数组以 NULL 结尾,符合 C 字符串数组的规范
};
// 调用 client 函数,传入主进程的参数(argc 和 argv)
client(argc, argv);
int status = 0; // 存储子进程的退出状态
// 等待子进程结束并获取其退出状态
wait(&status);
}
else if (pid == 0) { // 如果 pid 等于 0,表示当前代码在子进程中执行
// 子进程部分
int argc = 2; // 子进程的参数个数设为 2(程序名、端口号)
// 构造子进程的参数数组,用于调用 server 函数
char* argv[] = {
argv0, // 程序名(从参数传入)
(char*)"9999", // 监听的端口号
NULL // 确保数组以 NULL 结尾
};
// 调用 server 函数,传入子进程的参数(argc 和 argv)
server(argc, argv);
}
else { // 如果 pid 小于 0,表示 fork 失败
// 打印错误信息,表示 fork 调用失败
perror("fork failed");
}
}
重要概念解释
serv_adr.sin_family = AF_INET;
功能概述
这行代码设置 serv_adr 结构体中的 sin_family 成员为 AF_INET,表示使用 IPv4 协议族。在网络编程中,sin_family 字段用于指定地址族,不同的地址族对应不同的网络协议。
知识点详解
-
struct sockaddr_in 结构体:这是一个用于存储 IPv4 地址信息的结构体,定义在 <arpa/inet.h> 头文件中。serv_adr 就是这个结构体类型的变量。
-
sin_family 成员:该成员用于指定地址族,AF_INET 是一个常量,表示 IPv4 协议族。常见的地址族还有 AF_INET6 表示 IPv6 协议族。
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
功能概述
这行代码将命令行提供的 IP 地址(argv[1])转换为网络字节顺序,并存储在 serv_adr 结构体的 sin_addr.s_addr 成员中。
知识点详解
-
sin_addr 成员:struct sockaddr_in 结构体中的 sin_addr 成员也是一个结构体,其定义为 struct in_addr sin_addr;,用于存储 IP 地址。sin_addr.s_addr 是一个 32 位的无符号整数,用于存储 IPv4 地址的二进制表示。
-
inet_addr 函数
该函数位于<arpa/inet.h>头文件中,其原型为in_addr_t inet_addr(const char *cp);
-
参数:cp 是一个指向点分十进制表示的 IPv4 地址字符串的指针,这里是 argv[1],即命令行中的第二个参数。
-
返回值:返回一个 in_addr_t 类型的值,表示转换后的网络字节顺序的 IP 地址。如果输入的字符串不是有效的 IP 地址,inet_addr 会返回 INADDR_NONE。不过需要注意的是,inet_addr 在处理 255.255.255.255 时可能会有问题,现在更推荐使用 inet_pton 函数。
-
serv_adr.sin_port = htons(atoi(argv[2]));
功能概述
这行代码将命令行提供的端口号(argv[2])转换为网络字节顺序,并存储在 serv_adr 结构体的 sin_port 成员中。
知识点详解
-
sin_port 成员:struct sockaddr_in 结构体中的 sin_port 成员是一个 16 位的无符号整数,用于存储端口号。
-
atoi 函数:该函数位于 <stdlib.h> 头文件中,其原型为 int atoi(const char *nptr);。它的作用是将字符串转换为整数,这里将 argv[2](命令行中的第三个参数)转换为整数类型的端口号。
-
htons 函数
该函数位于<arpa/inet.h>头文件中,其原型为uint16_t htons(uint16_t hostshort);
-
功能:htons 表示 “host to network short”,即从主机字节顺序转换为网络字节顺序。在不同的计算机系统中,字节序可能不同(大端序或小端序),而网络通信中要求使用大端序(网络字节顺序),因此需要使用 htons 函数进行转换。
-
参数:hostshort 是一个 16 位的无符号整数,表示主机字节顺序的端口号。
-
返回值:返回网络字节顺序的端口号。
-
UDP网络编程一般步骤
同时也可以总结出UDP网络编程的一般步骤:
创建UDP Socket
-
使用 socket() 函数指定协议族(通常为 PF_INET 或 AF_INET)和通信类型(使用 SOCK_DGRAM 表示UDP),建立一个UDP套接字。
-
此步骤是整个通信的起点,套接字充当网络通信的端点。
配置地址结构
-
定义并初始化 sockaddr_in 结构体,用来存储IP地址和端口信息。
-
对于服务器端,可以将 IP 地址设置为 INADDR_ANY 以允许接收任意源的数据;对于客户端,则需要指定目标服务器的具体IP地址和端口号。
-
通过 inet_addr() 和 htons() 等函数进行网络字节序转换,确保数据传输的一致性。
绑定Socket(仅对服务器)
-
服务器端需调用 bind() 函数,将创建的socket绑定到指定的IP地址和端口上。
-
绑定后,服务器可以接收发往该端口的UDP数据报。
数据发送与接收
-
接收数据(服务器端): 使用 recvfrom() 函数接收来自客户端的数据报,同时获取客户端的地址信息。
-
发送数据:
-
对于服务器端:将接收到的数据通过 sendto() 函数返回给客户端,实现回声或响应功能。
-
对于客户端:使用 sendto() 将消息发送到服务器指定的地址,然后使用 recvfrom() 接收服务器回复的数据。
-
-
UDP不保证数据传输的可靠性,因此在实际应用中可能需要设计重发、丢包检测等机制。
错误处理
每个系统调用(如 socket()、bind()、sendto()、recvfrom() 等)都应检查其返回值,发现错误时,通过适当的错误处理机制(例如调用自定义的 error_handling() 函数)输出错误信息并安全退出程序。
资源关闭与清理
当数据传输完成或程序退出前,务必调用 close() 函数关闭socket,释放系统资源,避免内存泄露和端口占用问题。
评论前必须登录!
注册