C语言多进程TCP服务器与客户端
一、多进程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_REUSEADDRint 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");elseperror("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 返回值为子进程pidif(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");elseputs("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 1024void 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语言在系统编程中的强大能力。通过合理运用进程管理、套接字编程和并发控制技术,可以构建出高性能的网络应用。关键点包括:
- 正确的进程管理:处理僵尸进程,避免资源泄露
- 高效的资源分配:及时关闭不需要的文件描述符
- 健壮的错误处理:应对各种网络异常
- 可扩展的架构:支持进程池等优化技术
这种模式虽然资源消耗大于线程模型,但在稳定性、安全性和隔离性方面具有优势,特别适合需要高可靠性的服务端应用。