一、多进程TCP服务器的创建
示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
// 僵尸进程处理
void zombie_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建TCP套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置SO_REUSEADDR
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d\n", PORT);
// 设置僵尸进程处理器
struct sigaction sa;
sa.sa_handler = zombie_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction failed");
exit(EXIT_FAILURE);
}
while (1) {
// 接受新连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept failed");
continue;
}
printf("New connection from %s:%d\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 创建子进程处理连接
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
close(new_socket);
continue;
}
if (pid == 0) { // 子进程
close(server_fd); // 关闭不需要的监听套接字
// 处理客户端请求
while (1) {
memset(buffer, 0, BUFFER_SIZE);
ssize_t bytes_read = read(new_socket, buffer, BUFFER_SIZE - 1);
if (bytes_read <= 0) {
if (bytes_read == 0)
printf("Client disconnected\n");
else
perror("read error");
break;
}
printf("Received: %s", buffer);
// 处理请求(示例:回显)
char *response = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"\r\n"
"Hello from server!";
send(new_socket, response, strlen(response), 0);
}
close(new_socket);
exit(EXIT_SUCCESS);
} else { // 父进程
close(new_socket); // 关闭不需要的客户端套接字
}
}
return 0;
}
二、多进程服务器相关知识
-
进程管理
-
fork()
创建子进程#include <unistd.h> pid_t fork(void); /* 成功返回进程 ID, 失败返回 -1。 */
僵尸进程
-
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t mypid = fork();//mypid 返回值为子进程pid
if(mypid == 0) // pid == 0 表示子进程
printf("I am child process\n");
else //父进程
{
printf("Child process ID is %d\n", mypid);
sleep(30);
}
if(mypid == 0)
puts("END Child process");
else
puts("end parent process");
return 0;
}
/*
此子进程为僵尸进程。僵尸进程:子进程执行完毕,但父进程未调用wait()函数或者waitpid()函数获取子进程的终止状态。
此函数中,if(pid == 0)时,执行的时子进程,else代表父进程
打印mypid的值即为子进程的进程ID。
*/
执行情况:
将函数执行起来之后,可以看见子进程以及执行完毕,但是由于父进程未调用wait函数或者waitpid函数,故子进程成为僵尸进程,在父进程执行期间一直存在,如上图在Linux系统内ps 查看信息所展示,父进程ID为2252,子进程ID为2253,在父进程存续期间,子进程成为僵尸进程,也一直存在。
SIGCHLD
信号处理僵尸进程
#include <signal.h>
void (*signal(int signo, void(*func)(int)))(int);
/*
在产生信号时调用,返回之前注册的函数指针
参数为int 类型,返回void 类型的函数指针
signo常用参数:{
SIGINT (2) - 中断信号 (Ctrl+C)
SIGSEGV (11) - 段错误 (无效内存访问)
SIGTERM (15) - 终止信号 (kill默认)
SIGCHLD (17) - 子进程状态改变
SIGALRM (14) - 定时器到期
}
*/
使用示例:(注册信号和处理函数)
signal(SIGCHLD, myfunc);
//子进程结束信号产生则调用myfunc函数。
alarm()函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回0,或以秒为单位的距SIGALRM信号发生所剩时间。
如果调用该函数的同时传递一个正整形参数,相应时间后(以秒为单位)将产生SIGALRM信号,若向该函数传递 0,则之前对SIGALRM信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用signal函数)终止进程,不做任何处理。注意!!
示例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void time_out(int sig)//alarm函数时间到达处理函数
{
if(sig == SIGALRM)
{
printf("time_out\n");
}
alarm(2);
}
void func(int sig) //ctrl+c 信号处理函数
{
if(sig == SIGINT)
puts("CTRL + C is prossed");
}
int main()
{
int i;
signal(SIGALRM, time_out);
signal(SIGINT, func);
alarm(2);
for(i = 0;i<3;i++) //每隔50s输出一次,理论程序会执行150s。
{
puts("loading>>>>");
sleep(50);
}
return 0;
}
输出效果:
可见,上述程序实际运行时间不到十秒,如果按下ctrl+c则更快结束。这是因为“发生信号时将唤醒由于调用sleep函数而进入阻塞状态的进程。”
即:如果程序自然执行,不输入ctrl+c,则程序每两秒产生SIGALR信号,同时唤醒sleep进程,即退出sleep状态,主程序中for循环执行了上次,程序三次进入sleep,同时没两秒也被唤醒。所以每2s被唤醒的时候也会退出sleep,所以程序只会输出如图第一种结果。按下ctrl+c时也会产生信号SIGINT信号唤醒sleep的进程,所以也只会输出三次。
总的来说,程序有三次循环,而每次信号的产生都会打断sleep状态,唤醒进程,所以程序只能被信号唤醒上次,也只会执行三次信号处理函数。
sigaction()函数
此处介绍一个sigaction函数,它类似于signal函数,且完全可以替换signal函数,且更稳定。
“sigaction函数在UNIX系列的不同操作系统中完全相同,而signal函数可能存在区别”。
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction*oldact);
//成功返回 0, 失败时返回-1.
/*
signo: 与signal函数相同,传递信号信息
act:对应于第一个参数的信号处理函数信息
oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递 0
*/
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
};
/*
sa_mask和sa_flags的所有位均初始化为 0 即可,这两个成员用于指定信号相关的 选项 和 特性,而我们的目的主要是防止产生僵尸进程,故省略。
sa_handler:保存信号处理函数的指针值(地址值)。
*/
则上面的函数可修改为:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void time_out(int sig)//alarm函数时间到达处理函数
{
if(sig == SIGALRM)
{
printf("time_out\n");
}
alarm(2);
}
void func(int sig) //ctrl+c 信号处理函数
{
if(sig == SIGINT)
puts("CTRL + C is prossed");
}
int main()
{
int i;
struct sigaction act;
act.sa_handler = time_out;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM, &act, 0);
alarm(2);
struct sigaction act_pro;
act_pro.sa_handler = func;
sigemptyset(&act_pro.sa_mask);
act_pro.sa_flags = 0;
sigaction(SIGINT, &act_pro, 0);
for(i = 0;i<3;i++) //每隔50s输出一次,理论程序会执行150s。
{
puts("loading>>>>");
sleep(50);
}
return 0;
}
执行情况如下:
套接字管理
// 父子进程资源分离
if (pid == 0) {
close(server_fd); // 子进程关闭监听套接字
} else {
close(new_socket); // 父进程关闭客户端套接字
}
多进程TCP客户端
完整客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define NUM_CLIENTS 3
#define BUFFER_SIZE 1024
void client_process(int client_id) {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
// 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
// 转换IP地址
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("invalid address");
exit(EXIT_FAILURE);
}
// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
printf("Client %d connected to server\n", client_id);
// 发送请求
char message[BUFFER_SIZE];
snprintf(message, sizeof(message),
"GET / HTTP/1.1\r\n"
"Host: localhost\r\n"
"User-Agent: Client/%d\r\n"
"\r\n", client_id);
send(sock, message, strlen(message), 0);
printf("Client %d sent request\n", client_id);
// 接收响应
ssize_t bytes_read;
while ((bytes_read = read(sock, buffer, BUFFER_SIZE - 1)) > 0) {
buffer[bytes_read] = '\0';
printf("Client %d received:\n%s\n", client_id, buffer);
}
close(sock);
printf("Client %d disconnected\n", client_id);
exit(EXIT_SUCCESS);
}
int main() {
pid_t pids[NUM_CLIENTS];
// 创建多个客户端进程
for (int i = 0; i < NUM_CLIENTS; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pids[i] == 0) { // 子进程
client_process(i + 1);
}
}
// 父进程等待所有子进程结束
for (int i = 0; i < NUM_CLIENTS; i++) {
waitpid(pids[i], NULL, 0);
}
printf("All clients completed\n");
return 0;
}
多进程客户端关键技术
-
并发连接
for (int i = 0; i < NUM_CLIENTS; i++) { pids[i] = fork(); if (pids[i] == 0) { client_process(i + 1); } }
-
请求定制
snprintf(message, sizeof(message), "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "User-Agent: Client/%d\r\n" "\r\n", client_id);
-
响应处理
while ((bytes_read = read(sock, buffer, BUFFER_SIZE - 1)) > 0) { buffer[bytes_read] = '\0'; printf("Client %d received:\n%s\n", client_id, buffer); }
系统测试与优化
测试方法
# 编译服务器
gcc server.c -o server
# 编译客户端
gcc client.c -o client
# 启动服务器
./server
# 在另一个终端启动客户端
./client
性能优化技术
-
进程池技术
#define POOL_SIZE 5 // 预先创建进程 for (int i = 0; i < POOL_SIZE; i++) { pid_t pid = fork(); if (pid == 0) { worker_process(); // 工作进程循环处理请求 } }
-
连接复用
// 保持连接而非每次新建 while (1) { // 处理多个请求 process_request(socket); }
-
负载监控
void monitor_load() { struct rusage usage; getrusage(RUSAGE_SELF, &usage); printf("CPU usage: %ld.%06ld sec\n", usage.ru_utime.tv_sec, usage.ru_utime.tv_usec); }
安全增强
-
权限降级
if (setuid(getuid()) < 0) { perror("setuid failed"); exit(EXIT_FAILURE); }
-
资源限制
#include <sys/resource.h> struct rlimit limit = { .rlim_cur = 100, // 100个文件描述符 .rlim_max = 100 }; setrlimit(RLIMIT_NOFILE, &limit);
-
输入验证
// 验证接收的数据 if (strstr(buffer, "malicious") != NULL) { close(socket); return; }
应用场景与扩展
适用场景
- 高并发网络服务(HTTP服务器)
- 并行数据处理系统
- 实时通信应用
- 分布式计算节点
- 压力测试工具
扩展方向
-
添加SSL/TLS加密
#include <openssl/ssl.h> SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); SSL *ssl = SSL_new(ctx); SSL_set_fd(ssl, socket); SSL_accept(ssl); SSL_read(ssl, buffer, sizeof(buffer));
-
实现进程间通信
// 使用管道 int pipefd[2]; pipe(pipefd); write(pipefd[1], data, size);
-
添加日志系统
void log_message(const char *msg) { FILE *log = fopen("server.log", "a"); fprintf(log, "[%ld] %s\n", time(NULL), msg); fclose(log); }
-
配置热重载
// 使用SIGHUP信号 signal(SIGHUP, reload_config);
总结对比
特性 | 多进程服务器 | 多进程客户端 |
---|---|---|
主要目的 | 处理并发连接 | 模拟并发请求 |
进程角色 | 父进程管理,子进程处理 | 父进程协调,子进程执行 |
资源消耗 | 较高(每个连接一个进程) | 可控(可配置进程数) |
适用场景 | 长期运行的服务 | 测试/批量任务 |
复杂度 | 高(需处理僵尸进程) | 中(较简单) |
多进程TCP服务器与客户端的实现展示了C语言在系统编程中的强大能力。通过合理运用进程管理、套接字编程和并发控制技术,可以构建出高性能的网络应用。关键点包括:
- 正确的进程管理:处理僵尸进程,避免资源泄露
- 高效的资源分配:及时关闭不需要的文件描述符
- 健壮的错误处理:应对各种网络异常
- 可扩展的架构:支持进程池等优化技术
这种模式虽然资源消耗大于线程模型,但在稳定性、安全性和隔离性方面具有优势,特别适合需要高可靠性的服务端应用。