reactor (百万并发服务器) -- 1(上)

简介: reactor (百万并发服务器) -- 1(上)

  为了从点滴开始,文章会先从一些基础socket去补充一些经常发生但是没有很深入去思考的细节。然后我们再开始去设计reactor的设计,可以选择跳过起过前面部分。

       为了能从0开始去设计,测试,优化...整个过程会分为2-3篇文章输出,喜欢的可以点歌关注哦。

socket的API理解

       这部分不过多详细的去解释,只是对使用过程中容易忽略的地方进行补充。很基础的部分不是很了解的部分,可以看我的另一篇文章。

    //-- 创建(分配)一个管理者sockfd
    // 参数1:表示创建套接字使用的协议族   -- AF_INET(IPv4地址) AF_INET6(IPv6地址) AF_UNIX(本地通信)
    // 参数2:表示创建套接字使用的协议类型     -- SOCK_STREAM(字节流套接字) SOCK_DGRAM(数据报套接字)
    // 参数3: 表示创建套接字使用的协议 -- 0(由操作系统自动选择适当的协议类型) IPPROTO_TCP(TCP) TPPROTO_UDP(UDP)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

       为了更好的理解,我们可以把socket理解为一家门店招聘一个管理者或者一个小饭店的老板。那么现在我们有管理者了,现在需要把这个管理者安排到指定的门店去工作。

-- 选择门店(设置这个门店接待的客人类型)
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
-- 表示可以进门的顾客是 AF_INET(IPv4协议族的)
serveraddr.sin_family = AF_INET;
// INADDR_ANY(0.0.0.0) 表示可以接待来着任何地方的顾客 
// 127.0.0.1/localhost: 只可以让本机访问,其他计算机是无法访问的(只接待内部人员)
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
-- 表示顾客可以从哪个门口进入
serveraddr.sin_port = htons(2048);
-- 将管理者安排到指定的门店
bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));

       现在我们已经安排好我们的门店管理者(店长)了,我们还需要一个记录顾客进店的登记表,或者管理进店的秩序方式,防止发生踩踏事件,也为了公平起见,先来的客人,我们先进行服务。在计算机中采用队列的先进先出性质能很好的解决。

// --记录所有进门的顾客信息(连接)
// 创建等待队列,将所有经过端口连接的请求放到等待队列
// 如果队列已满,则新的请求就会被拒绝,这个数字并不是一个硬性要求,实际上系统会设定一个合适的值
listen(sockfd, 10);

       现在我们已经把管理方案和管理人员都安排好了,我们就可以开业了吗?等等,我们还需要记录顾客的信息呢?我们需要知道顾客从哪里来,这样我们才能更地道的为顾客提供服务。所以我们需要先准备一张表格,登记顾客的信息。

struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);

       到现在为止,我们就完事具备了,等待顾客的光临,然后为每一个顾客专门安排一个一对一个的服务员,我们主打一个服务。

// -- 等待顾客到来,当顾客当来,填充clientaddr表,然后由店长叫一个专门的服务员来为顾客提供服务
// 这个函数是阻塞的,可以设置成非阻塞的方式(使用fcntl将套接字socket设置成非阻塞的即可)
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);

       接下来就是服务员与顾客之间的对话了。顾客提出需求,服务员进行解答和提供服务。

// 顾客提出要求,服务员通过recv的方式接收到 
recv(clientfd, buffer, 128, 0);
// 业务处理 --> 针对不同的需求,服务员会做出相应的处理方式
TODO
// 给顾客提供反馈,让顾客收获到快乐
send(clientfd, buffer, 128, 0);

       顾客总有离开的时候,这个时候我们需要把安排的服务员收回,下次让他为其他顾客提供服务。

close(clientfd);

       夜深了,门店需要打样了。我们要把管理者也收回。

close(sockfd);

-------------------------------------------------------------------------------------------------------------------------

现在我们来实操一下(这里我们只进行TCP进行表述):

客户端我们就不写了,使用sockTools进行测试。

提取码:s5wy

我们先思考2个问题:

  • 系统中出现大量TIME_WAIT状态的原因?
  • 系统中出现大量的CLOSE_WAIT状态的原因?

我们来复现第一种情况:系统中出现大量TIME_WAIT状态的原因?

#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <error.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        perror("socket():");
        return -1;
    }
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htons(INADDR_ANY);
    serveraddr.sin_port = htons(2048);   // 这里补充一个细节:端口1024以前是系统的,如果要用需要使用root权限
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)))
    {
        perror("bind");
        return -1;
    }
    listen(sockfd, 10);
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    if(-1 == clientfd)
    {
        perror("clientfd");
        return -1;
    }
    // char buffer[128] = {0}; 
    // while(1)
    // {
    //     int count = recv(clientfd, buffer, 128, 0);
    //     if(count == 0)
    //     {
    //         printf("断开\n");
    //         close(clientfd);
    //     }
    //     else 
    //     {
    //         send(clientfd, buffer, 128, 0);
    //     }
    // }
    close(sockfd);
    return 0;
}

我们来看下上面的代码,当我们连接之后,立马就退出了。然后就进入了TIME_WAIT状态,接下来我们怎么分析呢?

在TCP连接中,主动关闭的一方会进入TIME_WAIT状态,这样是不正常的。说明服务器会总是断开连接导致系统中出现大量的TIME_CLOSE状态。导致原因可能是进程异常退出(崩溃)...

我们来复现第二种情况:系统中出现大量的CLOSE_WAIT状态的原因?

#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <error.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        perror("socket():");
        return -1;
    }
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htons(INADDR_ANY);
    serveraddr.sin_port = htons(2048);   // 这里补充一个细节:端口1024以前是系统的,如果要用需要使用root权限
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)))
    {
        perror("bind");
        return -1;
    }
    listen(sockfd, 10);
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    if(-1 == clientfd)
    {
        perror("clientfd");
        return -1;
    }
    char buffer[128] = {0}; 
    while(1)
    {
        int count = recv(clientfd, buffer, 128, 0);
        if(count == 0)
        {
            printf("断开\n");
            // close(clientfd);
        }
        else 
        {
            send(clientfd, buffer, 128, 0);
        }
    }
    close(sockfd);
    return 0;
}

在TCP连接中,对端关闭连接后,收到FIN后发送ACK确认收到了要关闭的信息,随后进入CLOSE_WAIT状态,关闭后进入LAST_ACK状态。按这个道理分析,我们很容易得出当服务器未调用close出现在TCP状态中,对端调用close关闭连接后,服务器回送ACK,表明收到了消息后进入半连接状态,当服务器调用close后,退出CLOSE_WAIT状态。其实从字面就能猜出来,关闭等待嘛,合适关闭呢,调用close呗。说明连接没关闭,这样是很危险的,很容易造成描述符没有释放而程序崩溃。


这个话题到这里就结束了,后面遇到一些情况,会进行补充...

多线程/多进程服务器

       在上面的代码中我们很容易看出来,这个服务器最大的缺陷是什么?

       只能处理一个连接。我们来测试一下:

       连接1的情况:

       连接2的情况:

我们回到代码:

    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    if(-1 == clientfd)
    {
        perror("clientfd");
        return -1;
    }
    char buffer[128] = {0}; 
    // 循环
    while(1)
    {
        int count = recv(clientfd, buffer, 128, 0);
        if(count == 0)
        {
            printf("断开\n");
            close(clientfd);
        }
        else 
        {
            send(clientfd, buffer, 128, 0);
        }
    }
    close(sockfd);

  我们很容易看出来,我们没有为第二个连接提供专门服务员进行服务。那有什么办法吗?

  很容易想到,开线程/开进程,接下来我们就对我们的代码进行改进吧!!!

       哈哈,似乎问题得到的合理的解决,那么这样真的满足所有的需求吗?这里我才两个连接呢,如果我们有1w个连接同时在线呢? 很读朋友很快想到,那用线程池呗,其实不然。用线程池不是解决这个问题的核心,如果连接池的连接数拿到设置成1w吗?那如果有1w+1的连接呢,还不是要去创建往线程池中添加一个线程。

       这里插一个故事:apache的C10K问题。C10K问题是只支持一万个并发连接问题,在Apache中,每一个客户端请求的会分配一个独立的线程或者进程来处理。当并发请求增加时,系统将消耗更多的线程或者进程资源,这将导致内存和CPU资源的过度使用,从而影响服务器的性能。

       如何解决呢?怎么办呢?害,好像这个问题无解了。

       我们需要寻找在一种在一个线程中对应多个连接的方法 --> IO多路复用出现了

IO多路复用

       在Linux中,IO多路复用主要包括了select/poll/epoll技术。接下来我们将对这个几个技术做一个的中的疑问提出一些看法,很基础的部分请跳到开头的提供的文章中。


       select

  • 为什么说select会受到1024的限制呢?
  • select的性能权限是什么呢?
  • select的使用上的缺点在哪呢?

我们先来使用和测试下select,验证下它是否能在一个线程中处理多个连接的问题。(传参的说明在代码中有注释,这里就不过多的重复了)

#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <error.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2048);
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)))
    {
        perror("bind()");
        return -1;
    } 
    listen(sockfd, 10);
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    // 定义监视IO集合
    fd_set rfds,rset;
    // 初始化时全部置为0
    FD_ZERO(&rfds);
    // 将需要监视的IO置1
    FD_SET(sockfd, &rfds);
    // 因为select内部的循环是 for(int i = 0; i < __nfds; ++i)
    // 为了减少循环,我们提前记录下来
    int maxFd = sockfd;
    int fds[1024] = {0};
    fds[maxFd] = 1;
    while(1)
    {
        rset = rfds;
        // 这里的+1很重要哦
        // 参数1: 表示需要监视的文件描述符数量+1
        // 参数2:表示需要监视的读事件集合
        // 参数3: 表示需要监视的写事件集合
        // 参数4:表示需要坚实的错误事件集合
        // 参数5: 超时时间,NULL表示阻塞方式
        // 返回: 就绪描述符个数
        int ready = select(maxFd + 1, &rset, NULL, NULL, NULL);
        // 对连接事件的处理
        if(FD_ISSET(sockfd, &rset))
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            // 将新的需要监视的IO加入到集合中
            FD_SET(clientfd, &rfds);
            maxFd = clientfd;
            if(clientfd > maxFd)
            {
                maxFd = clientfd;
            }
            fds[clientfd] = 1;
        }
        for(int i = sockfd + 1; i <= maxFd; ++i)
        {
            if(FD_ISSET(i, &rset))
            {
                char buffer[128] = {0};
                int count = recv(i, buffer, 128, 0);
                if(count == 0)
                {
                    printf("断开 %d\n", i);
                    fds[i] = 0;
                    FD_CLR(i, &rfds);
                    close(i);
                    // 如果当前最大的被关闭了,则需要更新(减小无用循环)
                    if(i == maxFd)
                    {
                        for(int k = maxFd; k > sockfd + 1; --k)
                        {
                            if(fds[i] == 1)
                            {
                                maxFd = k;
                            }
                        }
                    }
                    continue;
                }
                send(i, buffer, count, 0);
            }
        }
    }
    close(sockfd);  
    return 0;
}

相关文章
|
6月前
|
开发框架 缓存 .NET
并发请求太多,服务器崩溃了?试试使用 ASP.NET Core Web API 操作筛选器对请求进行限流
并发请求太多,服务器崩溃了?试试使用 ASP.NET Core Web API 操作筛选器对请求进行限流
273 0
|
9月前
|
算法 Java
并发垃圾回收算法对于大规模服务器应用的优势
并发垃圾回收算法对于大规模服务器应用的优势
|
8月前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
142 0
|
7月前
|
缓存 弹性计算 数据库
阿里云2核4G服务器支持多少人在线?程序效率、并发数、内存CPU性能、公网带宽多因素
2核4G云服务器支持的在线人数取决于多种因素:应用效率、并发数、内存、CPU、带宽、数据库性能、缓存策略、CDN和OSS使用,以及用户行为和系统优化。阿里云的ECS u1实例2核4G配置,适合轻量级应用,实际并发量需结合具体业务测试。
129 0
阿里云2核4G服务器支持多少人在线?程序效率、并发数、内存CPU性能、公网带宽多因素
|
8月前
|
网络协议
UDP服务器的并发方案
UDP服务器的并发方案
104 0
|
3天前
|
存储 机器学习/深度学习 人工智能
2025年阿里云GPU服务器租用价格、选型策略与应用场景详解
随着AI与高性能计算需求的增长,阿里云提供了多种GPU实例,如NVIDIA V100、A10、T4等,适配不同场景。2025年重点实例中,V100实例GN6v单月3830元起,适合大规模训练;A10实例GN7i单月3213.99元起,适用于混合负载。计费模式有按量付费和包年包月,后者成本更低。针对AI训练、图形渲染及轻量级推理等场景,推荐不同配置以优化成本和性能。阿里云还提供抢占式实例、ESSD云盘等资源优化策略,支持eRDMA网络加速和倚天ARM架构,助力企业在2025年实现智能计算的效率与成本最优平衡。 (该简介为原文内容的高度概括,符合要求的字符限制。)
|
4天前
|
存储 弹性计算 人工智能
2025年阿里云企业云服务器ECS选购与配置全攻略
本文介绍了阿里云服务器的核心配置选择方法论,涵盖算力需求分析、网络与存储设计、地域部署策略三大维度。针对不同业务场景,如初创企业官网和AI模型训练平台,提供了具体配置方案。同时,详细讲解了购买操作指南及长期运维优化建议,帮助用户快速实现业务上云并确保高效运行。访问阿里云官方资源聚合平台可获取更多最新产品动态和技术支持。
|
6天前
|
弹性计算 JavaScript 前端开发
一键安装!阿里云新功能部署Nodejs环境到ECS竟然如此简单!
Node.js 是一种高效的 JavaScript 运行环境,基于 Chrome V8 引擎,支持在服务器端运行 JavaScript 代码。本文介绍如何在阿里云上一键部署 Node.js 环境,无需繁琐配置,轻松上手。前提条件包括 ECS 实例运行中且操作系统为 CentOS、Ubuntu 等。功能特点为一键安装和稳定性好,支持常用 LTS 版本。安装步骤简单:登录阿里云控制台,选择扩展程序管理页面,安装 Node.js 扩展,选择实例和版本,等待创建完成并验证安装成功。通过阿里云的公共扩展,初学者和经验丰富的开发者都能快速进入开发状态,开启高效开发之旅。
|
8天前
|
弹性计算 JavaScript 前端开发
一键安装!阿里云新功能部署Nodejs环境到ECS竟然如此简单!
一键安装!阿里云新功能部署Nodejs环境到ECS竟然如此简单!
一键安装!阿里云新功能部署Nodejs环境到ECS竟然如此简单!
|
3天前
|
存储 人工智能 弹性计算
2025年阿里云企业高性能云服务器租用价格与选型详解
随着企业数字化转型,阿里云于2025年推出多款高性能云服务器实例,涵盖计算、通用和内存密集型场景。文章分析了企业选择云服务器的核心要点,包括明确业务需求(如计算密集型任务推荐计算型实例)、性能与架构升级(如第八代实例性能提升20%),以及第九代实例支持AI等高算力需求。同时提供了配置价格参考和成本优化策略,助力企业实现效率与成本的最优平衡。

热门文章

最新文章