C语言多进程TCP服务器与客户端

发布于:2025-06-14 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、多进程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;
}

二、多进程服务器相关知识

  1. 进程管理

    1. 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;
}

多进程客户端关键技术

  1. 并发连接
    for (int i = 0; i < NUM_CLIENTS; i++) {
        pids[i] = fork();
        if (pids[i] == 0) {
            client_process(i + 1);
        }
    }
  2. 请求定制
    snprintf(message, sizeof(message), 
             "GET / HTTP/1.1\r\n"
             "Host: localhost\r\n"
             "User-Agent: Client/%d\r\n"
             "\r\n", client_id);
  3. 响应处理
    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

性能优化技术

  1. 进程池技术
    #define POOL_SIZE 5
    
    // 预先创建进程
    for (int i = 0; i < POOL_SIZE; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            worker_process(); // 工作进程循环处理请求
        }
    }
  2. 连接复用
    // 保持连接而非每次新建
    while (1) {
        // 处理多个请求
        process_request(socket);
    }
  3. 负载监控
    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);
    }

安全增强

  1. 权限降级
    if (setuid(getuid()) < 0) {
        perror("setuid failed");
        exit(EXIT_FAILURE);
    }
  2. 资源限制
    #include <sys/resource.h>
    
    struct rlimit limit = {
        .rlim_cur = 100, // 100个文件描述符
        .rlim_max = 100
    };
    setrlimit(RLIMIT_NOFILE, &limit);
  3. 输入验证
    // 验证接收的数据
    if (strstr(buffer, "malicious") != NULL) {
        close(socket);
        return;
    }

应用场景与扩展

适用场景

  1. 高并发网络服务(HTTP服务器)
  2. 并行数据处理系统
  3. 实时通信应用
  4. 分布式计算节点
  5. 压力测试工具

扩展方向

  1. 添加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));
  2. 实现进程间通信

    // 使用管道
    int pipefd[2];
    pipe(pipefd);
    write(pipefd[1], data, size);
  3. 添加日志系统

    void log_message(const char *msg) {
        FILE *log = fopen("server.log", "a");
        fprintf(log, "[%ld] %s\n", time(NULL), msg);
        fclose(log);
    }
  4. 配置热重载

    // 使用SIGHUP信号
    signal(SIGHUP, reload_config);

总结对比

特性 多进程服务器 多进程客户端
主要目的 处理并发连接 模拟并发请求
进程角色 父进程管理,子进程处理 父进程协调,子进程执行
资源消耗 较高(每个连接一个进程) 可控(可配置进程数)
适用场景 长期运行的服务 测试/批量任务
复杂度 高(需处理僵尸进程) 中(较简单)

多进程TCP服务器与客户端的实现展示了C语言在系统编程中的强大能力。通过合理运用进程管理、套接字编程和并发控制技术,可以构建出高性能的网络应用。关键点包括:

  1. 正确的进程管理:处理僵尸进程,避免资源泄露
  2. 高效的资源分配:及时关闭不需要的文件描述符
  3. 健壮的错误处理:应对各种网络异常
  4. 可扩展的架构:支持进程池等优化技术

这种模式虽然资源消耗大于线程模型,但在稳定性、安全性和隔离性方面具有优势,特别适合需要高可靠性的服务端应用。