关于Linux下几种并发服务器的总结

总结一下Linux下常见的几种并发编程方式。

多进程并发

这是出现的最早的也是写起来最简单的一种方式。大概可以总结成父进程接收客户端的连接请求,启动子进程负责与客户端通信。
在这里插入图片描述
多进程服务器代码:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

void client_handler(int fd)
{
    
    
    char buf[32] = {
    
    0};
    int ret;

    while (1)
    {
    
    
        ret = recv(fd, buf, sizeof(buf), 0);
        if (-1 == ret)
        {
    
    
            perror("recv");
            exit(1);
        }
        else if (0 == ret)
        {
    
    
            break;
        }
        if (!strcmp(buf, "bye"))
        {
    
    
            break;
        }
        printf("%s\n", buf);

        memset(buf, 0, sizeof(buf));
    }

    close(fd);
    kill(getppid(), SIGALRM);
}

void my_wait(int sig)
{
    
    
    int status;
    wait(&status);
}

int main()
{
    
    
    //创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
    
    
        perror("socket");
        exit(1);
    }

    int opt = 1;
    setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr;

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8000);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (-1 == ret)
    {
    
    
        perror("bind");
        exit(1);
    }

    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
    
    
        perror("listen");
        exit(1);
    }

    printf("等待客户端的连接...\n");
    struct sockaddr_in client_addr;
    int length = sizeof(client_addr);

    signal(SIGALRM, my_wait);

    while (1)
    {
    
    
        int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
        if (-1 == fd)
        {
    
    
            perror("accept");
            exit(1);
        }
        printf("接受客户端的连接 %d\n", fd);

        pid_t pid = fork();
        if (0 == pid)
        {
    
    
            client_handler(fd);
            exit(0);
        }
    }

    close(sockfd);

    return 0;
}

多进程服务器优点在于:

  • 方法简单,易于理解;
  • 进程的特点在于健壮,即一个进程奔溃掉并不会影响其他进程的执行。

多进程并发的缺点也很明显:

  • 因为多个进程地址空间相互独立,所以各个进程想要实现数据的传递,必须使用指定的进程间通信方式;
  • 进程的开销比较大,如果并发量比较大,服务器的负载会变得很大。

为了解决以上两个缺点,于是就有了线程。

多线程并发

跟进程相比,线程的优点很多:

  • 资源消耗少;
  • 线程间切换速度快;
  • 线程间通信不需要复杂的通信机制;
  • 使多CPU的系统工作更加有效。

我们可以为每一个客户端创建一个线程,这样同样的并发量对服务器造成的压力会比进程小。

在这里插入图片描述
多线程服务器代码:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>

void *ClientHandler(void *arg)
{
    
    
    int ret;
    int fd = *(int *)arg;
    char buf[32] = {
    
    0};

    pthread_detach(pthread_self());    //线程结束,自动释放资源

    while (1)
    {
    
    
        ret = recv(fd, buf, sizeof(buf), 0);
        if (-1 == ret)
        {
    
    
            perror("recv");
            exit(1);
        }
        else if (0 == ret)    //客户端异常退出
        {
    
    
            break;
        }
        printf("接收%d客户端%s\n", fd, buf);

        memset(buf, 0, sizeof(buf));
    }

    printf("%d 客户端退出!\n", fd);
    close(fd);
}

int main()
{
    
    
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
    
    
        perror("socket");
        exit(1);
    }

    int opt = 1;
    setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr;

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = 8000;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (-1 == ret)
    {
    
    
        perror("bind");
        exit(1);
    }

    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
    
    
        perror("listen");
        exit(1);
    }

    printf("等待客户端的连接...\n");
    struct sockaddr_in client_addr;
    int length = sizeof(client_addr);
    while (1)
    {
    
    
        int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
        if (-1 == fd)
        {
    
    
            perror("accept");
            exit(1);
        }
        printf("接受客户端的连接 %d\n", fd);

        //为每一个客户端创建新的线程
        pthread_t tid;
        ret = pthread_create(&tid, NULL, ClientHandler, &fd);
        if (ret != 0)
        {
    
    
            perror("pthread_create");
            exit(1);
        }
    }

    close(sockfd);

    return 0;
}

是不是使用多线程实现的服务器就是完美的呢?当然不是。

多线程因为是共享同一个地址空间,所以一个线程的奔溃会导致整个进程挂掉。同时线程的通信方式过于简单,只需要读取内存就行,会导致多个线程同时访问共享资源。程序员的大部分时间都消耗在了解决多线程并发的问题上。

因此,多线程解决并发的问题也并不是完美的方案。

扫描二维码关注公众号,回复: 12301903 查看本文章

多路复用

并发服务器并不是只有进程和线程才能解决,还有一个目前比较流行的技术叫做多路复用。

什么是多路复用技术?

举个例子,假如你是一个老师(服务器),现在让班级里30个学生做一道数学题,做好之后你逐个检查,有这么几种方案。

第一种:从第一个人开始检查,顺序把30个人检查完。这种效率是最低的,一旦中间遇到某个学生解题没有思路,那么将会影响整个班的进度。这种方法都谈不上是并发服务器。

第二种:找来30个老师,每个老师负责检查一个学生,不难理解,效率是相当的高。这种方法就是多进程/多线程并发服务器。

第三种:你站在讲台上,告诉学生题目答完后叫一声,但是你又不知道是谁叫的,所以一听到声音你就逐个询问,直到找到这个学生并且检查他的答案。这种就是多路复用select的实现方案。

顺便讲一下第四种:你站在讲台上,告诉学生题目答完后举手,这种方法直接省去了你逐个询问,效率高于select,在服务器上使用epoll技术实现,也是多路复用的一种。

多路复用基本上可以归纳为两个因素:事件以及处理事件的函数。

程序在不断的循环,等待事件的到来,来了之后根据事件类型的不同调用不同的事件处理函数。
图片
select并发服务器代码:

#include <stdio.h>
#include <sys/types.h>    
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    
       
    int fd[1024] = {
    
    0};
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);   
    if (-1 == sockfd)
    {
    
       
        perror("socket");
        exit(1);
    }

    int opt = 1;
    setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr;

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = 8000;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (-1 == ret)
    {
    
    
        perror("bind");
        exit(1);
    }


    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
    
    
        perror("listen");
        exit(1);
    }

    fd_set readfd, tmpfd;         //定义集合
    FD_ZERO(&readfd);             //清空集合
    FD_SET(sockfd, &readfd);      //添加到集合
    int maxfd = sockfd, i = 0;
    struct sockaddr_in client_addr;
    int length = sizeof(client_addr);
    char buf[32] = {
    
    0};

    while (1)
    {
    
    
        tmpfd = readfd;
        ret = select(maxfd + 1, &tmpfd, NULL, NULL, NULL);
        if (-1 == ret)
        {
    
    
            perror("select");
            exit(1);
        }

        if (FD_ISSET(sockfd, &tmpfd))      //判断sockfd是否还留在集合里面  判断是否有客户端发起连接
        {
    
    
            for (i = 0; i < 1024; i++)    //选择合适的i
            {
    
    
                if (fd[i] == 0)
                {
    
    
                    break;
                }
            }
            fd[i] = accept(sockfd, (struct sockaddr *)&client_addr, &length);
            if (-1 == fd[i])
            {
    
    
                perror("accept");
                exit(1);
            }

            printf("接受来自%s的客户端的连接 fd = %d\n", inet_ntoa(client_addr.sin_addr), fd[i]);

            FD_SET(fd[i], &readfd);       //新的文件描述符加入到集合中
            if (maxfd < fd[i])
            {
    
    
                maxfd = fd[i];
            }
        }
        else                               //有客户端发消息
        {
    
    
            for (i = 0; i < 1024; i++)
            {
    
    
                if (FD_ISSET(fd[i], &tmpfd))    //判断是哪个fd可读
                {
    
    
                    ret = recv(fd[i], buf, sizeof(buf), 0);
                    if (-1 == ret)
                    {
    
    
                        perror("recv");
                    }
                    else if (0 == ret)
                    {
    
    
                        close(fd[i]);    //关闭TCP连接
                        FD_CLR(fd[i], &readfd);    //从集合中清除掉
                        printf("客户端%d下线!\n", fd[i]);
                        fd[i] = 0;
                    }
                    else
                    {
    
    
                        printf("收到%d客户端的消息%s\n", fd[i], buf);
                    }
                    memset(buf, 0, sizeof(buf));
                    break;
                }
            }
        }
    }

    return 0;
}

select的缺点:

  • 最大的并发数受内核的限制;
  • 每次都要扫描整个集合,集合越大,效率越低。

epoll并发服务器代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>

#define MAXSIZE     256

int main()
{
    
    
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
    
    
        perror("socket");
        exit(1);
    }

    int opt = 1;
    setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in server_addr;

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = 8000;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (-1 == ret)
    {
    
    
        perror("bind");
        exit(1);
    }

    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
    
    
        perror("listen");
        exit(1);
    }
    int epfd = epoll_create(MAXSIZE);   //创建epoll对象
    if (-1 == epfd)
    {
    
    
        perror("epoll_create");
        exit(1);
    }

    struct epoll_event ev, events[MAXSIZE] = {
    
    0};
    ev.events = EPOLLIN;       //监听sockfd可读
    ev.data.fd = sockfd;
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
    if (-1 == ret)
    {
    
    
        perror("epoll_ctl");
        exit(1);
    }

    struct sockaddr_in client_addr;   //用于保存客户端的信息
    int length = sizeof(client_addr), i;
    char buf[32] = {
    
    0};

    while (1)
    {
    
    
        int num = epoll_wait(epfd, events, MAXSIZE, -1);   //-1表示阻塞 
        if (-1 == num)
        {
    
    
            perror("epoll_wait");
            exit(1);
        }

        for (i = 0; i < num; i++)
        {
    
    
            if (events[i].data.fd == sockfd)    //有客户端发起连接
            {
    
    
                int fd = accept(sockfd, (struct sockaddr *)&client_addr, &length);
                if (-1 == fd)
                {
    
    
                    perror("accept");
                    exit(1);
                }

                printf("接受来自%s的连接fd=%d\n", inet_ntoa(client_addr.sin_addr), fd);

                //为新的文件描述符注册事件
                ev.data.fd = fd;
                ev.events = EPOLLIN;
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
                if (-1 == ret)
                {
    
    
                    perror("epoll_ctl");
                }
            }
            else
            {
    
    
                if (events[i].events & EPOLLIN)   //如果事件是可读的
                {
    
    
                    ret = recv(events[i].data.fd, buf, sizeof(buf), 0);
                    if (-1 == ret)
                    {
    
    
                        perror("recv");
                    }
                    else if (0 == ret)
                    {
    
    
                        ev.data.fd = events[i].data.fd;
                        ev.events = EPOLLIN;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);   //客户端退出,注销事件                        
                        close(events[i].data.fd);
                    }
                    else
                    {
    
    
                        printf("收到%d客户端的消息%s\n", events[i].data.fd, buf);
                    }
                    memset(buf, 0, sizeof(buf));
                }
            }
        }
    }

    return 0;
}

epoll的性能提升:

  • 没有最大并发连接的限制;
  • 主动调用回调函数,无需逐个检测。

多路复用解决了多线程中同步的问题,确实省去了很多麻烦。

但是由于只有一个线程,如果在处理某个事件的时候,遇到了IO操作,比如读取大文件,那么程序将会被阻塞,此时其他的事件将不会被处理。解决这个问题可以使用异步IO,就是读取文件的时候,不管文件有没有读完都会立即返回,不影响处理其他事件。至于IO操作什么时候完成,操作系统会有其他办法去检测。

总结

随着服务器负载的越来越大,高并发问题早已不是进程、线程或者多路复用能解决的,而是事件 + 线程 + 协程的组合,但是不管怎样,了解了历史才能更深刻的理解当下。以上就是给大家总结的几种比较经典的并发服务器实现方案。

以上有不足的地方欢迎指出讨论,最后,如果觉得学习资料难找的话,可以添加学习交流群:960994558 学习资料已经共享在群里了,期待你的加入~
在这里插入图片描述
这里推荐一个金牌大佬的免费课程,这个跟以往所见到的只会空谈理论的有所不同,正在学习的朋友可以体验一下
C/C++Linux服务器开发/后台架构师

猜你喜欢

转载自blog.csdn.net/weixin_52622200/article/details/113125675