unix网络编程2.2——高并发服务器(二)多进程与多线程实现

前置知识

阅读本文需要先阅读下面的文章:

unix网络编程1.1——TCP协议详解(一)

unix网络编程2.1——高并发服务器(一)基础——io与文件描述符、socket编程与单进程服务端客户端实现

前言

  • 在上文<unix网络编程2.1>中最后实现了一个单进程的客户端与服务端,但是仅限于服务器与客户端一对一进行通信,如果希望可以多个客户端同时与服务端建立连接,并且完成数据通信,
    一般有两种思路,本文介绍第一种通过多进程或多线程的方式(下一篇文章会介绍IO多路复用的方式)

分析单进程服务端代码

核心代码如下(具体可见上一节):

listen_fd = socket(AF_INET, SOCK_STREAM, 0);

bind(listen_fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));

listen(listen_fd, 128);

connect_fd = accept(listen_fd, (struct sockaddr *)&clientAddr, &clientLen);

while (1) {
    n = read(connect_fd, buf, sizeof(buf));
    write(connect_fd, buf, n);
}

逻辑很简单:socket创建监听的listenfd,bind listenfd与规定的IP和端口,然后重点:accept依靠listenfd阻塞等待客户端的连接,当没有客户端连接时,accept一直等待,一旦客户端连接成功,
accept返回connect_fd,accept不再阻塞,执行后续的代码,服务端的逻辑会继续执行accept后续的代码,即与客户端读写数据。这里最重要的是,accept只会阻塞到一次客户端成功的连接,那么我们能不能把代码改成下面这样呢?让服务端在accept一直循环去接受客户端:

listen_fd = socket(AF_INET, SOCK_STREAM, 0);

bind(listen_fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));

listen(listen_fd, 128);

while (1) {
    connect_fd = accept(listen_fd, (struct sockaddr *)&clientAddr, &clientLen);
}


while (1) {
    n = read(connect_fd, buf, sizeof(buf));
    write(connect_fd, buf, n);
}

这样的问题一目了然,当accept成功后,第一个while循环将不会退出,会继续等待下一个客户端的连接,而无法执行后续与客户端读写数据的流程,因此这样也不行,但是这给了我们一个思路:
能不能把accept对listen_fd的阻塞监听(等待客户端连接),交给进程/线程A去做呢?而与客户端的读写数据,则由进程/线程B去做,这样完全可以实现建立连接与读写数据的解耦!!!!
当然可以!!!

多进程:

多进程/线程代码模型图:

image

多进程服务端代码:

#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <ctype.h>
#include <unistd.h>
#include <stdio.h>
#include <arpa/inet.h>  // struct sockaddr_in对应的头文件 <arpa/inet.h> 
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>

#define SERV_PORT 9999
#define SERV_IP "192.168.239.128"

// 模拟客户端:nc 127.0.0.1 9999

void wait_child(int signum)
{
    // 如果大于0则继续回收
    pid_t pid;
    while ((pid = waitpid(0, NULL, WNOHANG)) > 0) {
        printf("回收成功,被回收的子进程id=%d\n", pid);
    };
}

int main(void)
{
    int lfd, cfd;
    struct sockaddr_in serv_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pid_t pid;
    char buf[BUFSIZ]; // 8k
    char tmp[INET_ADDRSTRLEN];
    int n, i;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 或者这种写法:inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
    
    bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    listen(lfd, 128);

    while (1) {
        cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_addr_len); // 客户端的
        printf("服务端建立连接后返回的 connectFd=%d\n", cfd);
        printf("accept done, client ip:%s, port:%d\n", 
                inet_ntop(AF_INET, &client_addr.sin_addr, tmp, sizeof(tmp)),
                ntohs(client_addr.sin_port));


        /* 多进程 */
        pid = fork();
        if (pid < 0) {
            perror("fork error");
            exit(1);
        } else if (pid == 0) {
            // 设计: 子进程完成与客户端的交互
            close(lfd);
            break; // 跳转到下面执行后续逻辑
        } else {
            /* 设计:父进程 
            1.揽活,接收子进程 
            2.回收子进程,否则产生僵尸进程, 利用信号(也可以用其他方案)
            */
            close(cfd);
            signal(SIGCHLD, wait_child);
        }
    }

    // 设计: 子进程完成与客户端的交互
    if (pid == 0) {
        // printf("创建了一个新的客户端连接, connectfd=%d\n", cfd);
        while (1) {
            // recv更好
            n = read(cfd, buf, sizeof(buf));
            if (n == 0) {
                // 说明client close,发送了 FIN
                close(cfd);
                // 子进程直接退出:会产生僵尸进程:[server] <defunct>
                return 0;
            } else if (n == -1) {
                // 可能被信号打断
                perror("read error");
                exit(1);
            } else {
                printf("客户端发来的数据=%s", buf); // 不用换行?
                // 处理数据
                for (i = 0; i < n; i++) {
                    buf[i] = toupper(buf[i]);
                }
                write(cfd, buf, n);
            }
        }
    }

    return 0;
}

使用多进程并发服务器时要考虑以下几点:

  • 父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
  • 系统内创建进程个数(与内存大小相关)
  • 进程创建过多是否降低整体服务性能(进程调度)

多线程

多线程服务端代码(原理同多进程)

/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666

struct s_info
{
    struct sockaddr_in cliaddr;
    int connfd;
};
void *do_work(void *arg)
{
    int n, i;
    struct s_info *ts = (struct s_info *)arg;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    /* 可以在创建线程前设置线程创建属性,设为分离态,哪种效率高内? */
    pthread_detach(pthread_self());
    while (1)
    {
        n = Read(ts->connfd, buf, MAXLINE);
        if (n == 0)
        {
            printf("the other side has been closed.\n");
            break;
        }
        printf("received from %s at PORT %d\n",
               inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
               ntohs((*ts).cliaddr.sin_port));
        for (i = 0; i < n; i++)
            buf[i] = toupper(buf[i]);
        Write(ts->connfd, buf, n);
    }
    Close(ts->connfd);
}

int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    int i = 0;
    pthread_t tid;
    struct s_info ts[256];

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    Listen(listenfd, 20);

    printf("Accepting connections ...\n");
    while (1)
    {
        cliaddr_len = sizeof(cliaddr);
        connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        ts[i].cliaddr = cliaddr;
        ts[i].connfd = connfd;
        /* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */
        pthread_create(&tid, NULL, do_work, (void *)&ts[i]);
        i++;
    }
    return 0;
}

在使用线程模型开发服务器时需考虑以下问题:

  • 调整进程内最大文件描述符上限
  • 线程如有共享数据,考虑线程同步
  • 服务于客户端线程退出时,退出处理。(退出值,分离态)
  • 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU

总结

多进程/线程对系统资源消耗很大,受限于文件描述符,进程和线程数都很受限,因此我们要考虑用更好的设计思想和数据结构来设计更有效的高并发服务器,下一节我们一起学习IO多路复用,
用一个进程可以更好的实现很多客户端与服务器的连接与数据通信。

posted @ 2022-11-20 00:37  胖白白  阅读(252)  评论(0编辑  收藏  举报