基于 fork 的简易多进程TCP回显服务器 ( C ) 源码解析
点击查看代码
#define _XOPEN_SOURCE 700 // 确保使用POSIX标准功能
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库函数如exit
#include <string.h> // 字符串操作函数
#include <unistd.h> // POSIX系统调用(read/write/close等)
#include <signal.h> // 信号处理
#include <sys/wait.h> // 进程等待函数
#include <sys/socket.h> // 套接字相关函数
#include <arpa/inet.h> // 网络地址转换
#include <errno.h> // 错误码
void echo_loop(int fd); // 函数前向声明
void sigchld_handler(int sig)
{
while (waitpid(-1, NULL, WNOHANG) > 0)
; // 非阻塞回收所有终止的子进程
}
int main()
{
struct sigaction sa;
sa.sa_handler = sigchld_handler; // 指定处理函数
sigemptyset(&sa.sa_mask); // 清空阻塞信号集
sa.sa_flags = SA_RESTART; // 系统调用被中断后自动重启
if (sigaction(SIGCHLD, &sa, NULL) == -1)
{ // 注册SIGCHLD处理器
perror("sigaction");
exit(EXIT_FAILURE);
}
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET: IPv4协议
// SOCK_STREAM : 流式套接字(TCP)
if (listen_fd < 0)
{
perror("socket creation failed");
exit(EXIT_FAILURE);
}
int optval = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
struct sockaddr_in serv_addr = {0};
serv_addr.sin_family = AF_INET; // 设置地址族为 IPv4。
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
serv_addr.sin_port = htons(8080); // 端口转换网络字节序
if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
perror("bind failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
if (listen(listen_fd, SOMAXCONN) < 0) // SOMAXCONN是系统允许的最大连接队列长度
{
perror("listen failed");
close(listen_fd);
exit(EXIT_FAILURE);
}
printf("Server running on port 8080\n");
for (;;)
{
struct sockaddr_in cli_addr;
socklen_t clilen = sizeof(cli_addr);
int conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &clilen);
if (conn_fd < 0)
{
if (errno == EINTR)
continue; // 被信号中断则重试
perror("accept error");
break;
}
pid_t pid = fork();
if (pid == 0)
{ // 子进程
close(listen_fd); // 子进程不需要监听
// 打印客户端信息
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Client connected: %s:%d\n", client_ip, ntohs(cli_addr.sin_port));
echo_loop(conn_fd); // 处理客户端请求
close(conn_fd);
exit(EXIT_SUCCESS);
}
else if (pid > 0)
{ // 父进程
close(conn_fd); // 父进程不需要连接套接字
}
else
{
perror("fork error");
close(conn_fd);
}
}
close(listen_fd);
return 0;
}
void echo_loop(int fd)
{
char buf[1024];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0)
{
if (write(fd, buf, n) != n)
{
perror("write error");
break;
}
}
if (n < 0)
{
perror("read error");
}
}
struct sigaction sa;
- 定义一个 sigaction 结构体变量 sa,用于指定信号处理的行为。
- sigaction 结构体包含了信号处理的各种选项和配置。
sa.sa_handler = sigchld_handler;
- 设置 sa 的 sa_handler 成员为 sigchld_handler 函数指针。
- sigchld_handler 是自定义的信号处理函数,用于处理 SIGCHLD 信号(当子进程终止或停止时发送)。
sigemptyset(&sa.sa_mask);
- 使用 sigemptyset 函数清空 sa.sa_mask,即设置为空的信号掩码。
- 信号掩码指定了在信号处理函数执行期间要阻塞的信号集合。清空掩码意味着在处理 SIGCHLD 信号时不会额外阻塞其他信号。
if (sigaction(SIGCHLD, &sa, NULL) == -1)
- 调用 sigaction 函数注册 SIGCHLD 信号的处理行为。
- 参数说明:
- SIGCHLD:指定要处理的信号为 SIGCHLD,即子进程状态改变时发送的信号。
- &sa:指向 sigaction 结构体的指针,包含信号处理的配置。
- NULL:不保存旧的信号处理行为。
- 检查 sigaction 是否成功。如果返回值为 -1,表示注册失败。
exit(EXIT_FAILURE);:终止程序,返回失败状态码,确保程序不会继续执行后续代码。
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
- 调用 socket 函数创建一个新的套接字,并返回该套接字的文件描述符 listen_fd。
- 参数说明:
- AF_INET:指定地址族为 IPv4。IPv4 是最常用的互联网协议版本,适用于大多数网络环境。
- SOCK_STREAM:指定套接字类型为流式套接字(TCP)。TCP 提供可靠的、面向连接的通信服务,适用于需要保证数据传输完整性和顺序的应用场景。
- 0:协议字段,通常设置为 0,表示使用默认协议(对于 TCP 来说,默认协议是 IP 协议)。
socket 函数的作用
- socket 函数用于创建一个套接字,它是网络通信的基础。创建的套接字可以用于发送和接收数据。
返回值: - 成功时返回一个非负整数,作为新创建的套接字的文件描述符。
- 失败时返回 -1,并设置全局变量 errno 来指示具体的错误原因。
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))
- 调用 setsockopt 函数设置套接字选项。
- 参数说明:
- listen_fd:监听套接字的文件描述符。
- SOL_SOCKET:表示操作的是套接字层的选项。
- SO_REUSEADDR:启用地址复用选项,允许绑定到一个已经被使用的地址(例如,服务器重启时可以立即使用相同的端口)。
- &optval:指向一个整数值的指针,optval 的值为 1,表示启用该选项。
- sizeof(optval):指定选项值的大小。
struct sockaddr_in serv_addr = {0};
- 定义了一个 sockaddr_in 结构体变量 serv_addr,并使用 {0} 初始化它。
- sockaddr_in 是一个用于表示 IPv4 地址和端口的结构体,定义在 <netinet/in.h> 头文件中。它的主要成员包括:
- sin_family:地址族(如 AF_INET 表示 IPv4)。
- sin_port:16位的网络字节序端口号。
- sin_addr:32位的网络字节序 IP 地址。
- sin_zero:填充字段,通常不需要使用。
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
- 设置 serv_addr 结构体中的 sin_addr.s_addr 成员为 INADDR_ANY,表示服务器将监听所有可用的网络接口(即所有网卡)。
参数说明: - INADDR_ANY:宏定义,值为 0,表示绑定到所有可用的 IP 地址。
- htonl:将主机字节序(通常是小端序或大端序,取决于系统架构)转换为网络字节序(大端序)。虽然 INADDR_ANY 的值为 0,在大多数情况下不需要转换,但使用 htonl 是一种良好的编程习惯,确保代码的可移植性。
serv_addr.sin_port = htons(8080);
- 设置 serv_addr 结构体中的 sin_port 成员为 8080,表示服务器将监听 8080 端口。
参数说明: - htons:将主机字节序的端口号转换为网络字节序(大端序),以确保在网络通信中正确传输端口号。
- 8080:指定服务器监听的端口号。选择 8080 端口是因为它是一个常用的非特权端口,适合用于测试和开发环境。
在创建并配置监听套接字的过程中,需要指定服务器将监听哪个 IP 地址和端口。
INADDR_ANY 表示服务器将监听所有可用的网络接口,这意味着无论客户端从哪个网络接口连接,服务器都能接收请求。
8080 是一个常用的 HTTP 代理端口,选择这个端口可以方便地进行调试和测试。
if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
-
调用 bind 函数,将监听套接字 listen_fd 绑定到由 serv_addr 指定的地址和端口。
-
参数说明:
- listen_fd:监听套接字的文件描述符。
- (struct sockaddr *)&serv_addr:指向 sockaddr_in 结构体的指针,强制转换为 sockaddr * 类型,因为 bind 函数需要的是通用的 sockaddr 指针。
- sizeof(serv_addr):指定 serv_addr 结构体的大小。
-
struct sockaddr 是一个通用的套接字地址结构体,用于支持多种协议(如 IPv4、IPv6 等)。
-
struct sockaddr_in 是专门用于 IPv4 的地址结构体。
为了满足 bind 函数的参数要求,必须将 serv_addr 的地址强制转换为 struct sockaddr * 类型。
bind 函数的作用
- 绑定地址和端口:将套接字与特定的 IP 地址和端口号关联起来。对于服务器来说,这一步非常重要,因为它决定了服务器在哪个地址和端口上监听客户端连接请求。
- 参数检查:如果 bind 成功返回 0,表示绑定成功;如果返回 -1,表示绑定失败,并设置全局变量 errno 来指示具体的错误原因。
listen(listen_fd, SOMAXCONN)
- 调用 listen 函数,将套接字 listen_fd 设置为监听状态。
- 参数说明:
- listen_fd:监听套接字的文件描述符。
- SOMAXCONN:表示系统允许的最大连接队列长度。
listen 函数的作用
- listen 函数用于将一个已绑定的套接字(通过 bind 函数绑定到特定地址和端口)转换为监听套接字,使其能够接受客户端连接请求。
- 第二个参数 SOMAXCONN 指定了内核为该套接字维护的未完成连接队列的最大长度。当有新的连接请求到达时,如果队列已满,后续的连接请求将被拒绝。
struct sockaddr_in cli_addr;
- 定义了一个 sockaddr_in 结构体变量 cli_addr,用于存储客户端的地址信息(包括 IP 地址和端口号)。sockaddr_in 是一种专门用于 IPv4 的地址结构。
socklen_t clilen = sizeof(cli_addr);
- 定义了一个 socklen_t 类型的变量 clilen,并将其初始化为 cli_addr 的大小。这个变量用于传递给 accept 函数,告知它 cli_addr 的大小。
- sizeof(cli_addr) 计算出 cli_addr 结构体的字节长度,确保 accept 函数知道要填充多少字节到 cli_addr 中。
int conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &clilen);
- 调用 accept 函数,阻塞等待一个新的客户端连接。
- listen_fd 是监听套接字文件描述符,由之前的 socket 和 bind 调用创建并绑定到服务器的某个端口上。
- (struct sockaddr *)&cli_addr 是一个指向 cli_addr 的指针,强制转换为 sockaddr * 类型,因为 accept 函数需要的是通用的 sockaddr 指针。
- &clilen 是传递给 accept 函数的指针,用于返回实际填充的地址结构的大小。
- accept 成功后返回一个新的文件描述符 conn_fd,表示与客户端之间的连接。如果失败则返回 -1 并设置 errno。
close(listen_fd);
- 调用 close 函数关闭文件描述符 listen_fd。
- listen_fd 是监听套接字的文件描述符,用于接受新的客户端连接请求。
为什么在子进程中关闭 listen_fd?
- 资源管理:每个打开的文件描述符都会占用系统资源。子进程不需要监听新的连接,因此关闭 listen_fd 可以释放该资源,避免不必要的资源浪费。
- 避免干扰:如果子进程不关闭 listen_fd,它仍然可以接受新的连接请求,这会导致多个进程同时监听同一个端口,引发潜在的竞争条件和混乱。
- 简化逻辑:子进程的任务是处理与特定客户端的通信,关闭 listen_fd 确保子进程专注于当前连接,不会被新的连接请求干扰。
inet_ntop 函数
- inet_ntop 是一个用于将网络字节序的二进制地址转换为可读字符串格式的函数。
- 函数原型:
- const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 参数说明:
- af:地址族,例如 AF_INET 表示 IPv4,AF_INET6 表示 IPv6。
- src:指向包含二进制地址的结构体(如 in_addr 或 in6_addr)的指针。
- dst:指向保存转换后的字符串的缓冲区的指针。
- size:缓冲区的大小。
inet_ntop(AF_INET, &cli_addr.sin_addr, client_ip, sizeof(client_ip));
- AF_INET:指定地址族为 IPv4。
- &cli_addr.sin_addr:指向 sockaddr_in 结构体中 sin_addr 成员的指针,该成员包含客户端的二进制 IP 地址。
- client_ip:指向一个字符数组,用于保存转换后的点分十进制字符串形式的 IP 地址。
- sizeof(client_ip):指定 client_ip 缓冲区的大小,确保有足够的空间存储转换后的字符串。
作用 - 将客户端的二进制 IP 地址(在网络字节序中表示)转换为人类可读的点分十进制格式(如 192.168.1.1),并存储在 client_ip 字符数组中。
- 这样可以方便地打印或记录客户端的 IP 地址信息,便于调试和日志记录。
printf("Client connected: %s:%d\n", client_ip, ntohs(cli_addr.sin_port));
- ntohs(cli_addr.sin_port):
- cli_addr.sin_port 是客户端的端口号,存储在网络字节序(大端序)中。
- ntohs 函数将网络字节序的端口号转换为主机字节序(小端序或大端序,取决于系统架构),以便正确显示端口号。
close(conn_fd);
- 调用 close 函数关闭文件描述符 conn_fd。
- conn_fd 是通过 accept 函数创建的连接套接字,用于与特定客户端进行通信。
为什么在父进程中关闭 conn_fd?
- 资源管理:每个打开的文件描述符都会占用系统资源。父进程不需要与客户端直接通信,因此关闭 conn_fd 可以释放该资源,避免不必要的资源浪费。
- 避免干扰:如果父进程不关闭 conn_fd,它仍然可以与客户端通信,这会导致多个进程同时处理同一个连接,引发潜在的竞争条件和混乱。
- 简化逻辑:父进程的任务是继续监听新的连接请求,而子进程负责处理已建立的连接。关闭 conn_fd 确保父进程专注于监听新连接,不会被当前连接干扰。

浙公网安备 33010602011771号