云计算百科
云计算领域专业知识百科平台

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_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语言在系统编程中的强大能力。通过合理运用进程管理、套接字编程和并发控制技术,可以构建出高性能的网络应用。关键点包括:

  • 正确的进程管理:处理僵尸进程,避免资源泄露
  • 高效的资源分配:及时关闭不需要的文件描述符
  • 健壮的错误处理:应对各种网络异常
  • 可扩展的架构:支持进程池等优化技术
  • 这种模式虽然资源消耗大于线程模型,但在稳定性、安全性和隔离性方面具有优势,特别适合需要高可靠性的服务端应用。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C语言多进程TCP服务器与客户端
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!