仿muduo库实现并发服务器-http服务器(2)

仿muduo库实现高并发服务器-面试常见问题 阅读详情

HttpServer模块

功能

搭建HTTP服务器,其实就是把前面实现的server模块、httprequest模块、httpresponse模块、httpcontext模块整合起来,并且引入业务处理路由表。

设计思想

服务器收到请求将请求解析后,需要对请求URL进行业务处理,而业务处理函数应该是用户提供的,服务器并不知道用户想要怎么处理资源路径;而请求是各种各样的,请求方法不一样,资源路径也不一样,为了更好地管理用户提供的业务处理函数,这里设计了路由表

以POST路由表为例:表中记录了针对哪个资源路径应该调用哪个业务处理的映射关系。当服务器收到请求,根据请求方法找到对应的路由表,在路由表查找有没有对应资源路径的服务器,有则执行函数

也就是说,什么请求怎么处理,由用户决定,用户来实现业务处理函数,然后将请求(请求方法和资源路径)与处理函数的映射关系添加到服务器的路由表;而服务器收到数据、解析完毕后,只需要在路由表查找是否有对应请求的映射关系,执行业务处理函数即可

要素

  1. GET请求的路由映射表:记录请求资源路径和对应处理函数的映射关系,一般是功能性请求
  2. POST请求的路由映射表
  3. PUT请求的路由映射表
  4. DELETE请求的路由映射表
  5. 静态资源相对根目录:请求资源可能是静态资源
  6. TCP服务器:进行连接的IO操作

功能接口

供用户使用的公有接口

  • 添加请求-业务处理映射信息
  • 设置静态资源根目录
  • 设置线程池中线程数量
  • 启动服务器

私有接口:服务器自己设计实现

上图为路由查找模拟图

服务器对新数据到来的处理流程:OnMessage
1、从socket接收数据后,放到用户接收缓冲区
2、调用OnMessage回调函数对接收缓冲区进行处理
(1)首先要对请求进行解析(需要协议上下文),得到一个HttpRequest结构(具有所有的请求要素)
(2)对请求进行路由查找,执行对应处理方法:
        a.如果是静态资源请求,比如html、image,将静态资源文件读取出来,填充到HttpResponse响应中
        b.不是静态,就是功能性请求,就要根据不同的请求方法,在对应的路由表查找路径与处理函数的映射关系,找到了就执行函数
3、对静态资源或者功能性请求进行处理完毕后,得到了一个填充好的HttpResponse对象,根据http响应的格式组织响应,进行发送。

由上面的处理流程可以引申出其他私有接口:

  • OnConnected:给TcpServer设置协议上下文
  • OnMessage:处理接收缓冲区数据
  • 请求的路由查找

        静态资源请求查找和处理
        功能性请求查找和处理

  • 组织响应进行回复

代码实现

#define DEFALT_TIMEOUT 10
class HttpServer
{
private:
    using Handler = std::function<void(const HttpRequest &, HttpResponse *)>; // 业务处理函数
    using RouteTable = std::vector<std::pair<std::regex, Handler>>;           // 请求路由表
    RouteTable _get_route;
    RouteTable _put_route;
    RouteTable _post_route;
    RouteTable _delete_route;
    std::string _static_dir; // 静态资源相对根目录
    TcpServer _server;

private:
    // 请求数据解析并处理
    void OnMessage(const PtrConnection &ptr, Buffer *buf)
    {
        while (buf->ReadAbleSize() > 0)
        {
            // 1、获取上下文,解析数据,得到httprequest对象
            HttpContext *context = ptr->GetContext()->get<HttpContext>();
            context->RecvHttpRequest(buf);
            HttpRequest &req = context->Request();
            HttpResponse rsp(context->ResponStatus());
            // 2、解析出错,直接回复错误响应,关闭连接
            if (context->RecvStatus() == HTTP_RECV__ERROR)
            {
                ErrorResponse(req, &rsp);                 // 填充一个错误显示界面到httpresponse响应
                WriteResponse(ptr, req, rsp);             // 组织响应,发回给客户端
                context->ReSet();                         // 防止下一条请求的上下文信息错误
                buf->MoveReadOffset(buf->ReadAbleSize()); // 出现错误就把缓冲区数据清空
                ptr->Shutdown();
                return;
            }
            // 2.5 不足一条完整请求
            if (context->RecvStatus() != HTTP_RECV_OVER)
                return;
            // 3、解析成功,获得完整的httprequest,进行路由处理
            Route(req, &rsp);
            // 4、组织httpresponse响应
            WriteResponse(ptr, req, rsp);
            // 5、重置上下文
            context->ReSet();
            // 6、根据响应的长短连接判断是否关闭连接还是继续处理
            if (rsp.Close() == true)
                ptr->Shutdown();
        }
        return;
    }
    // 设置协议上下文
    void OnConnected(const PtrConnection &ptr)
    {
        ptr->SetContext(Any(HttpContext()));
        DBG_LOG("New Connection: %p", ptr.get());
    }
    // 路由查找
    void Route(HttpRequest &req, HttpResponse *rsp)
    {
        // 1、判断路径合法
        if (Util::VailPath(req._path) == false)
        {
            rsp->_status = 400; // Bad Request
            return;
        }
        // 2、是否是静态资源并进行处理
        if (IsFileHandler(req) == true)
        {
            FileHandler(req, rsp);
        }
        // 3、是否是功能性请求,进行路由查找
        if (req._method == "GET" || req._method == "HEAD")
        {
            return Dispatcher(req, rsp, _get_route);
        }
        else if (req._method == "PUT")
        {
            return Dispatcher(req, rsp, _put_route);
        }
        else if (req._method == "POST")
        {
            return Dispatcher(req, rsp, _post_route);
        }
        else if (req._method == "DELETE")
        {
            return Dispatcher(req, rsp, _delete_route);
        }
        // 4、都不满足,说明请求方法不在支持的列表中
        rsp->_status = 405; // Method Not Allowed
        return;
    }
    // 静态资源文件的判断
    bool IsFileHandler(const HttpRequest &req)
    {
        // 1. 必须有静态资源根目录
        if (_static_dir.size() == 0)
            return false;
        // 2. 请求方法必须是GET/HEAD
        if (req._method != "GET" && req._method != "HEAD")
            return false;
        // 3. 请求资源必须存在,并且是一个普通文件
        // 如果请求路径是 / , /video/ ,在路径后面加"index.html"
        std::string path_tmp = _static_dir + req._path; // 不修改请求路径,记得加上静态资源根目录
        if (req._path.back() == '/')
        {
            path_tmp += "index.html"; //    /video/.mp3   /index.html
        }
        if (Util::IsRegular(path_tmp) == false)
            return false;
        return true;
    }
    // 静态资源请求的处理
    void FileHandler(const HttpRequest &req, HttpResponse *rsp)
    {
        std::string path_tmp = _static_dir + req._path; // 不修改请求路径
        if (req._path.back() == '/')
        {
            path_tmp += "index.html"; //    /video/.mp3   /index.html
        }
        // 读取文件内容,填充响应:body,head
        if (Util::ReadFile(path_tmp, &(rsp->_body)) == false)
        {
            rsp->_status = 500; // Internal Server Error
            return;
        }
        std::string mime = Util::ExtMime(path_tmp);
        rsp->SetHeader("Content-Type", mime);
        return;
    }
    // 功能性请求的处理
    void Dispatcher(HttpRequest &req, HttpResponse *rsp, const RouteTable &table)
    {
        // 根据请求的路由表table,遍历路由表,将请求路径与表元素的匹配规则(正则表达式)进行匹配,匹配成功就可以执行对应的业务函数
        // e:/numbers/(\d+)  req._path:/numbers/123
        for (auto &relation : table)
        {
            const std::regex &re = relation.first;
            const Handler &func = relation.second;
            bool ret = std::regex_match(req._path, req._matches, re);
            if (ret == true)
                return func(req, rsp); // 传入新的httpresponse
        }
        rsp->_status = 404; // 请求方法是有效的,但是请求的资源路径在请求路由表匹配不成功,说明请求的资源路径是无效的
    }
    // 错误界面设置:填充httpresponse
    void ErrorResponse(const HttpRequest &req, HttpResponse *rsp)
    {
        // 1、编写一个简单的错误界面
        std::string body;
        body += "<html>";
        body += "<head>";
        body += "<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>";
        body += "</head>";
        body += "<body>";
        body += "<h1>";
        body += std::to_string(rsp->_status);
        body += " ";
        body += Util::StatuDesc(rsp->_status);
        body += "</h1>";
        body += "<body>";
        body += "</html>";
        // 2、页面数据当做响应正文,放入rsp
        rsp->SetContent(body, "text/html");
    }
    // 按照http格式组织响应
    void WriteResponse(const PtrConnection &ptr, const HttpRequest &req, HttpResponse &rsp)
    {
        // 0、先完善头部字段:长短连接、正文长度、正文类型、是否重定向
        if (req.Close() == true)
        {
            rsp.SetHeader("Connection", "close");
        }
        else
        {
            rsp.SetHeader("Connection", "keep-alive");
        }
        if (rsp._body.size() > 0 && rsp.HasHeader("Content-Length") == false)
        {
            rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
        }
        if (rsp._body.size() > 0 && rsp.HasHeader("Content-Type") == false)
        {
            rsp.SetHeader("Content-Type", "application/octet-stream");
        }
        if (rsp._is_redirect == true)
        {
            rsp.SetHeader("Location", rsp._redirect_url);
        }
        // 1、状态行:协议版本 状态码 状态码描述\r\n
        std::string response = req._version + " " + std::to_string(rsp._status) + " " + Util::StatuDesc(rsp._status) + "\r\n";
        // 2、响应头部:key: value\r\nkey: value\r\n\r\n
        for (auto &e : rsp._headers)
        {
            std::string key = e.first;
            std::string val = e.second;
            response += key + ": " + val + "\r\n";
        }
        response += "\r\n";
        // 3、正文
        response += rsp._body;
        ptr->Send(response.c_str(), response.size());
    }

public:
    HttpServer(int port, int timeout = DEFALT_TIMEOUT) : _server(port)
    {
        _server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));
        _server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
        _server.EnableInactiveRelease(timeout);
    }
    // 添加请求路径-业务处理映射
    void SetGet(const std::string &path, const Handler &cb)
    {
        std::regex e(path);
        _get_route.push_back(std::make_pair(e, cb));
    }
    void SetPut(const std::string &path, const Handler &cb)
    {
        std::regex e(path);
        _put_route.push_back(std::make_pair(e, cb));
    }
    void SetPost(const std::string &path, const Handler &cb)
    {
        std::regex e(path);
        _post_route.push_back(std::make_pair(e, cb));
    }
    void SetDelete(const std::string &path, const Handler &cb)
    {
        std::regex e(path);
        _delete_route.push_back(std::make_pair(e, cb));
    }
    // 设置静态资源根目录
    void SetDir(const std::string &path)
    {
        assert(Util::IsDirectory(path));
        _static_dir = path;
    }
    // 设置线程池线程数量
    void SetThreadCount(int count)
    {
        _server.SetThreadCount(count);
    }
    // 启动服务器
    void Start()
    {
        _server.Start();
    }
};

代码分析

    // 设置协议上下文

    void OnConnected(const PtrConnection &ptr)

    {

        ptr->SetContext(Any(HttpContext()));

        DBG_LOG("New Connection: %p", ptr.get());

    }

在收到数据前就设置上下文的必要性:如果在OnMessage里面创建httptext对象,里面是一个循环,看似一直在运行,但是当接收的数据不足以构成一个完整请求,退出OnMessage函数并且该连接是长连接,那么下次接收到数据再次进入OnMessage函数,上一次的上下文信息(即处理进度)就找不到了,所以要在OnMessage外设置上下文对象,并且让connection管理起来,只有连接关闭这个上下文对象才随之释放

    void OnMessage(const PtrConnection &ptr, Buffer *buf)
    {
        while (buf->ReadAbleSize() > 0)
        {
            //......
            HttpRequest &req = context->Request();
            HttpResponse rsp(context->ResponStatus());
            // 2、解析出错,直接回复错误响应,关闭连接
            if (context->RecvStatus() == HTTP_RECV__ERROR)
            {
            //......    
    }

这里要对响应的状态码初始化,因为前面context解析过程中如果出错,那么响应状态码等于大于400,这里的响应就不能默认为200

using RouteTable = std::vector<std::pair<std::regex, Handler>>;    // 请求路由表

在插入正则表达式和业务处理函数时,将正则表达式先编译成std::regex对象,在后面的Dispatcher进行路由查找时,不需要每次匹配前都把key重新编译-->std::regex re(it->first),而是直接在路由表查找std::regex对象,这样可以节省编译std::regex过程消耗的时间,直接进行匹配

测试

静态资源和功能性请求测试

代码编译通过后,先简单写几个业务处理函数,针对静态资源获取已经四种请求方法进行测试,下面的测试代码对于功能性请求的业务处理都是将请求以一定的格式组织返回

#include "http.hpp"
std::string EchoRequest(const HttpRequest& req)
{
    std::stringstream ss;
    ss << req._method << " " << req._path << " " << req._version << "\r\n";
    for(auto &e : req._query)
    {
        ss << e.first << "=" << e.second << "&";
    }
    ss << "\r\n";
    for(auto &e : req._headers)
    {
        ss << e.first << ": " << e.second << "\r\n";
    }
    ss << "\r\n";
    ss << req._body;
    return ss.str();
}
void Hello(const HttpRequest& req, HttpResponse *rsp)
{
    const std::string &s = EchoRequest(req);
    rsp->SetContent(s, "text/plain");
}
void Login(const HttpRequest& req, HttpResponse *rsp)
{
    const std::string &s = EchoRequest(req);
    rsp->SetContent(s, "text/plain");
}
void PutFile(const HttpRequest& req, HttpResponse *rsp)
{
    const std::string &s = EchoRequest(req);
    rsp->SetContent(s, "text/plain");
}
void DelteFile(const HttpRequest& req, HttpResponse *rsp)
{
    const std::string &s = EchoRequest(req);
    rsp->SetContent(s, "text/plain");
}
int main()
{
    HttpServer server(8080);
    server.SetThreadCount(3);
    server.SetDir("./wwwroot/");
    server.SetGet("/hello", Hello);
    server.SetPost("/login", Login);
    server.SetPut("/hjy.png", PutFile);
    server.SetDelete("/hjy.png", DelteFile);
    server.Start();
    return 0;
}

静态资源根目录下也创建了一个index.html页面返回,里面对服务器进行了POST请求

<html>
    <head>
        <meta charset="utf8">
    </head>
    <body>
        <form action="/login" method="post">
            <input type="text" name="username"><br/>
            <input type="password" name="password"><br/>
            <input type="submit" value="提交" name="submit">
        </form>
    </body>
</html>

需要注意的是,在使用浏览器进行测试记得开放云服务器端口号,而且单纯通过浏览器的输入框是测试不了PUT和DELETE的,这样可以用 Linux 终端的 curl 命令发送 PUT 和 DELETE 请求,直接验证 “请求是否能到服务器”。

长连接测试

// 长连接测试:客户端在一段时间内持续发送信息,连接不关闭
#include "../source/server.hpp"
int main()
{
    Socket client;
    client.CreateClient(8080, "127.0.0.1");
    std::string request = "GET /hello?username=hjy&password=1234 HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
    while (1)
    {
        client.Send(request.c_str(), request.size());
        char buffer[1024] = {0};
        client.Recv(buffer, 1023);
        DBG_LOG("%s", buffer);
        sleep(3);
    }
    client.Close();
    return 0;
}

超时释放连接测试

//超时连接测试:客户端发送一次信息,后面不发送信息,等待10s,超时,服务器主动关闭连接
#include "../source/server.hpp"
int main()
{
    Socket client;
    client.CreateClient(8080, "127.0.0.1");
    std::string request = "GET /hello?username=hjy&password=1234 HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
    while (1)
    {
        client.Send(request.c_str(), request.size());
        char buffer[1024] = {0};
        client.Recv(buffer, 1023);
        DBG_LOG("%s", buffer);
        sleep(20);
    }
    client.Close();
    return 0;
}

头部正文大小和实际发送正文大小不符

//给服务器发送一条请求,告知发送数据大小为150,但是实际请求正文只发15个字节,RecvHttpRequest不能退出,10s后超时连接释放
//连续给服务器发送几条小的请求,但是实际发送的正文大小小于150,会导致后面接收的请求被当做前一条请求的正文部分,导致解析出错

#include "../source/server.hpp"
int main()
{
    Socket client;
    client.CreateClient(8080, "127.0.0.1");
    std::string request = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 150\r\n\r\n我是客户端";
    while (1)
    {
        client.Send(request.c_str(), request.size());
        client.Send(request.c_str(), request.size());
        client.Send(request.c_str(), request.size());
        client.Send(request.c_str(), request.size());
        char buffer[1024] = {0};
        client.Recv(buffer, 1023);
        DBG_LOG("%s", buffer);
        sleep(20);
    }
    client.Close();
    return 0;
}

代码运行分析:很明显服务器在处理第二条请求会把它当成第一条请求的正文,然后正文读取处理完发送响应。
又接着去处理缓冲区剩余数据肯定会解析错误,服务器就得组织错误界面发回给客户端,然后就清空缓冲区,关闭连接。
而这个错误响应的响应头是connection:close,是因为我请求解析错误,然后不能正确提取头部,所以在组织响应时,就默认是close。

容易出错的点:OnMessage函数

    void OnMessage(const PtrConnection &ptr, Buffer *buf)

    {

        while (buf->ReadAbleSize() > 0)

        {

            // 1、获取上下文,解析数据,得到httprequest对象

            HttpContext *context = ptr->GetContext()->get<HttpContext>();

            context->RecvHttpRequest(buf);

            HttpRequest &req = context->Request();

            HttpResponse rsp(context->ResponStatus());

            // 2、解析出错,直接回复错误响应,关闭连接

            if (context->RecvStatus() == HTTP_RECV__ERROR)

            {

                ErrorResponse(req, &rsp);                 // 填充一个错误显示界面到httpresponse响应

                WriteResponse(ptr, req, rsp);             // 组织响应,发回给客户端

                context->ReSet();                         // 防止下一条请求的上下文信息错误

                buf->MoveReadOffset(buf->ReadAbleSize()); // 出现错误就把缓冲区数据清空

                ptr->Shutdown();

                return;

            }

            // 2.5 不足一条完整请求

            if (context->RecvStatus() != HTTP_RECV_OVER)

                return;

            // 3、解析成功,获得完整的httprequest,进行路由处理

            Route(req, &rsp);

            // 4、组织httpresponse响应

            WriteResponse(ptr, req, rsp);

            // 5、重置上下文

            context->ReSet();

            // 6、根据响应的长短连接判断是否关闭连接还是继续处理

            if (rsp.Close() == true)

                ptr->Shutdown();

        }

        return;

    }

如果没有context->ReSet();  当解析出错,服务器提供错误界面,组织响应然后发送,下一步要关闭连接,但是在ShutDown函数里会先判断接收缓冲区还有没有数据,假设有数据那么还会继续执行OnMessage函数,而这个时候得到的context是上一次解析出错的context(一个connection对应一个context),也就是说接收状态还是HTTP_RECV__ERROR,那么RecvHttpRequest(buf)根本就不会执行,接收缓冲区的数据就根本读不出来,一直是接收错误状态,一直要服务器提供错误界面,组织响应然后发送,下一步要关闭连接.......程序进入死循环就会崩溃。
所以要在每次处理完错误后,关闭连接前,先把context重置一下,那么下次再进入OnMessage就可以执行
RecvHttpRequest,将数据读取出来(至少请求行读取到"\r\n"就可以移动接收缓冲区的读偏移);或者换成buf->MoveReadOffset(buf->ReadAbleSize());既然都出现解析错误了,就把接收缓冲区清空,不让服务器再继续处理数据。

业务处理超时,查看服务器的处理情况

// 业务处理超时,查看服务器的处理情况

// 当描述符1234都就绪,而1的处理花费30s后,如果234都是通信连接,那么服务器会在EventLoop::Start()里依次调用它们的HandleEvent,去刷新活跃度;
// 但是如果定时器描述符是2,并且先进行处理,就可能会导致其他就绪连接因为超时还没来得及刷新活跃度就被提前释放
// 统一所有 ReleaseInLoop 调用都通过 QueueInLoop,将ReleaseInLoop压入队列,最后在Start的RunAllTask执行

#include "../source/server.hpp"
int main()
{
    for (int i = 0; i < 10; i++)
    {
        pid_t pid = fork();
        if (pid < 0)
        {
            DBG_LOG("Fork Failed");
            return -1;
        }
        else if (pid == 0)
        {
            Socket client;
            client.CreateClient(8080, "127.0.0.1");
            std::string request = "GET /hello?username=hjy&password=1234 HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
            while (1)
            {
                client.Send(request.c_str(), request.size());
                char buffer[1024] = {0};
                client.Recv(buffer, 1023);
                DBG_LOG("%s", buffer);
            }
            client.Close();
        }
    }
    while (1)
    {
        sleep(1);
    }
    return 0;
}

源代码可能出现的问题:(下面执行的线程都是从属线程)当描述符1、2、3、4都就绪,而1的处理花费30s后,如果234都是通信连接,那么服务器会在EventLoop::Start()里依次调用它们的HandleEvent,去刷新活跃度;
!!!但是如果定时器描述符是2,并且先进行处理,就可能会导致其他就绪连接因为还没来得及刷新活跃度就被提前释放,处理出错流程:
执行定时器描述符channel的HandleEvent-->TimerWheel::Ontime()的RunTimerTask(),那么就会去执行对应_tick的析构函数,即_task_cb就是定时任务:Connection::ReleaseInLoop(),此时就会把Connection释放,如果这个Connection刚好是3或者4对应的,那么后面再进行3或者4的HandleEvent,就会发现找不到对应的Channel(PtrConnection释放,成员Channel也释放),就会出现内存访问错误。

解决方法:将ReleaseInLoop延后执行:
(1)统一所有 ReleaseInLoop 调用都通过 QueueInLoop,将ReleaseInLoop压入队列,最后在Start的RunAllTask执行。

    void Release()

    {

        _loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop, shared_from_this()));

    }

需要用ReleaseInLoop的地方都用Release,包括非活跃连接释放任务的添加,HandleError和HandleClose, ShutDownInLoop。

(2)确保在处理就绪事件时,先处理所有非定时器事件,再处理定时器事件

void EventLoop::Start()

{

    while (1)

    {

        std::vector<Channel *> actives;

        _poller.Poll(&actives);

       

        // 分离定时器事件和其他事件

        std::vector<Channel*> timer_events;

        std::vector<Channel*> other_events;

        for (auto &channel : actives)

        {

            if (channel->Fd() == _timerwheel.Fd())

                timer_events.push_back(channel);

            else

                other_events.push_back(channel);

        }

        // 先处理所有非定时器事件

        for (auto &channel : other_events)

        {

            channel->HandleEvent();

        }

        // 再处理定时器事件

        for (auto &channel : timer_events)

        {

            channel->HandleEvent();

        }

        // 最后执行任务队列

        RunAllTask();

    }

}

定时器超时任务执行问题

定时器不关注服务器处理任务处理多长时间,反正定时器一定是一秒执行一次定时任务,要是因为其他事件处理时间太长,导致定时器来不及ReadTimeFd,那就要根据超时次数,超时多少就执行多少次定时任务,不能越过前面的超时任务,只执行一次

下面对之前的代码稍作修改

    uint64_t ReadTimeFd()

    {

        uint64_t times;

        // read读取到的数据times就是从上一次read之后的超时次数

        // 有可能因为其他描述符业务处理时间长,导致后面处理定时器描述符事件的时候,已经超时很多次了,需要执行很多定时任务

        // 返回的times相当于要执行多少次超时任务

        int n = read(_timerfd, &times, 8);

        if (n < 0)

        {

            ERR_LOG("read errpr");

            abort();

        }

        return times;

    }

   ////这个函数应该每秒钟被执行一次,相当于秒针向后走了一步

    void RunTimerTask()

    {

        _tick = (_tick + 1) % _capacity;

        _wheel[_tick].clear(); // 清空指定位置的数组,就会把数组中保存的所有管理 定时器任务对象 的shared_ptr释放掉

    }

    // 读取定时器内容,执行任务

    void OnTime()

    {

        //实际超时多少次,就执行多少次RunTimerTask

        uint64_t times = ReadTimeFd();

        for (int i = 0; i < times; i++)

        {

            RunTimerTask();

        }

    }

一次性给服务器发送多条正确的请求

//一次性给服务器发送多条请求
#include "../source/server.hpp"
int main()
{
    Socket client;
    client.CreateClient(8080, "127.0.0.1");
    std::string request = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
    request += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
    request += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
    while (1)
    {
        client.Send(request.c_str(), request.size());
        char buffer[1024] = {0};
        client.Recv(buffer, 1023);
        DBG_LOG("%s", buffer);
        sleep(3);
    }
    client.Close();
    return 0;
}

仿muduo库的高并发服务器测试报告 该高并发服务器核心功能覆盖HTTP标准方法、大文件传输、高并发处理及异常场景容错,具备稳定的性能表现(QPS达46,666)和可靠性,适用于高负载Web服务等场景。 阅读详情

相关推荐

【校招项目】基于C++11的muduo库

本项目是参考 muduo 实现的基于 Reactor 模型的多线程网络库。使用 C++ 11 编写去除 muduo 对 boost 的依赖,内部实现了一个小型的 HTTP 服务器,可支持 GET 请求和静态资源的访问,且附有异步日志监控服务端情况。项目已经实现了 Channel 模块、Poller 模块、事件循环模块、HTTP 模块、定时器模块、异步日志模块、内存池模块、数据库连接池模块。

weixin_46272577的博客 8009

【C++仿Muduo库 #1】基本了解

📃个人主页:island1314🔥个人专栏:Linux—登神长阶。

island1314的博客 2042

Dify实战:智能邮件工作流配置全解析

本文详细解析了如何利用Dify平台配置智能邮件工作流,实现邮件处理的自动化与智能化。内容涵盖从获取邮箱授权、安装插件、安全配置环境变量,到构建邮件发送工作流、创建调用该工作流的智能体(Agent)的完整步骤,并分享了连接知识库实现智能客服、定时发送日报等进阶自动化场景,旨在帮助用户显著提升邮件处理效率。

t8u9v0w1x的博客 795

仿muduo库实现one thread one loop式并发服务器

该项目目标是实现一个高并发的服务器。但并不是自己完全实现一个,而是仿照现有成熟的技术进行模拟实现。一些必备知识:线程、网络套接字编程、多路转接技术(epoll),另外还有一些小的知识,在本篇文章中会提前讲解。本项目主要分为多个模块来进行讲解,实际上就是一个个小的组件。通过这些组件,我们可以很快的搭建起来一个高并发式的服务器。

weixin_67596609的博客 1873

拆开华为BBU5900,看看5G基站的大脑里到底装了啥?

本文深度拆解华为BBU5900,揭示5G基站核心处理单元的硬件架构与设计哲学。通过详细解析模块化设计、智能电源系统和动态散热方案,展现其支撑千兆速率、毫秒级时延的技术奥秘,为5G网络部署提供实用参考。

weixin_31456563的博客 562

方差分解分析变量筛选与显著性计算

冗余分析(redundancy analysis,RDA)是一种回归分析结合主成分分析的排序方法,也是多响应变量(multiresponse)回归分析的拓展。在进行VPA时,首先就要对这些环境因子进行一个分类,然后在约束其它类环境因子的情况下,对某一类环境因子进行排序分析,这种分析也成为偏分析,即partialRDA。在地球环境科学的实际应用中,可以基于VPA结果得出不同类型的环境因素(如何气候、土壤性质以及植物)对生物群落组成(如植被丰度,微生物群落)的解释程度。对于由或创建的排序对象,函数。

wlh2067的博客 2519

一、C++项目:仿muduo库实现高性能高并发服务器

简单理解就是使用 I/O多路复用 统⼀监听事件,收到事件后分发给处理进程或线程,是编写高性能网络服务器的必备技术之⼀。当然准确来说,因为我们要实现的服务器本身并不存在业务,咱们要实现的应该算是⼀个高性能服务器基础库,是⼀个基础组件。并且,通过组件内提供的不同应⽤层协议支持,也可以快速完成⼀个高性能应⽤服务器的搭建(当前。在这里,要明确的是咱们要实现的是⼀个高并发服务器组件,因此当前的项目中并不包含实际的业务。通过咱们实现的高并发服务器组件,可以简洁快速的完成⼀个高性能的服务器搭建。

weixin_54447296的博客 4062

仿真每日一练 | Workbench下薄板疲劳分析

今天给大家介绍Workbench下薄板疲劳分析,采用稳态分析模块进行评估。完成求解后,插入疲劳工具,同时插入对应的寿命进行查看,如图7和图8所示。采用稳态分析模块,导入几何模型,效果如图1。添加固定约束与扭矩,同时添加转速如图4所示。主要包括7要素,对应的类型见图2。求解完成后查看应力,如图6所示。1-1-3、有限元系统模型构建。1-2-1、加载条件/边界条件。1-1-1、几何模型构建。1-1-2、材料定义。1-2-2、求解设置。1-3-1、查看结果。1-3-2、评估结果。1-3-3、修正结果。

ansysxuexizhe的博客 1071

【项目设计】仿 muduo 库实现 OneThreadOneEventLoop 式并发服务器

仿 muduo 库实现一个以主从 Reactor 为模型,以 OneThreadOneEventLoop 为事件驱动的高并发服务器组件。

在平淡的日子里寻找存在的价值 2532

K8s(二十一):在 Kubernetes 集群中部署 MySQL8.0 高可用集群(1主2从)

🔴 K8s(二十一):在 Kubernetes 集群中部署 MySQL8.0 高可用集群(1主2从)

✨ 欢迎来到【Seal ^_^ 的优快云博客】!✨ 3万+

YOLOv11【第六章:模型压缩与极致优化篇·第4节】Lamp 剪枝算法:无需超参的自动化剪枝策略!

🏆 本文收录于专栏 《YOLOv11实战:从入门到深度优化》。本专栏围绕 YOLOv11 的改进、训练、部署与工程优化 展开,系统梳理并复现当前主流的 YOLOv11 实战案例与优化方案,内容目前已覆盖 分类、检测、分割、追踪、关键点、OBB 检测 等多个方向。

**My Coding Family** 1210

C++项目:仿muduo库高并发服务器

摘要 本文介绍了基于muduo库One Thread One Loop线程模型的高并发服务器实现方案。项目采用主从Reactor模型,主Reactor负责新连接建立,子Reactor处理通信事件和业务逻辑,通过"一线程一循环"设计提升并发性能。核心模块包括: SERVER模块 Buffer模块:实现用户态缓冲区,保证数据完整性 Socket模块:封装套接字操作 Channel模块:管理描述符IO事件及回调 协议模块 提供HTTP等应用层协议支持 项目产出为可复用的高并发服务器组件,开发者

2301_80774875的博客 1711

3D Slicer学习记录(4)-PLUSapp连接双目相机Intel Realsense

plusapp是基于openIGTLink协议的免费的开源库和应用程序API接口,用于数据采集、预处理、校准和实时流成像、位置跟踪和其他传感器数据。这里以intel realsense双目相机为例详细的演示整个图像流导入到3D Slicer的流程。

weixin_39090239的博客 1346

实战项目:仿muduo库实现并发服务器

muduo是陈硕大神在Linux平台下基于C/C++开发的高性能网络库,在此基础上可以很方便的扩展,进行二次开发编写如http服务器。muduo网络库的核心框架,one thread per thread + Reactor模式。这也是大多数Linux端高性能网络编程框架应用的主要架构。 本篇文章,对该框架进行了较为详细讲解。

感谢观看 2003

Django框架中settings文件的部署建议

为了更好地管理开发和生产环境的配置,建议将settings文件分为多个文件,分别用于不同的环境。然后,通过在启动应用程序时指定不同的settings文件,来加载相应的配置。在Django框架中,settings.py文件是非常重要的配置文件,它包含了项目的各种设置和选项。在Django框架中,settings.py文件是非常重要的配置文件,它包含了项目的各种设置和选项。可以使用Python的。为了更好地管理开发和生产环境的配置,建议将settings文件分为多个文件,分别用于不同的环境。

WELL_CODER的博客 128

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

C++项目:仿mudou库one thread one loop式并发服务器实现

实现的⾼并发服务器组件,可以简洁快速的完成⼀个⾼性能的服务器搭建。 并且,通过组件内提供的不同应⽤层协议⽀持,也可以快速完成⼀个⾼性能应⽤服务器的搭建(当前为了便于项⽬的演⽰,项⽬中提供HTTP协议组件的⽀持)

终为nullptr 3294

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

项目:仿muduo库One-Thread-One-Loop式并发服务器

仿muduo库One-Thread-One-Loop式并发服务器的组件

海绵宝宝de派小星的博客 13万+

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

仿muduo库实现并发服务器

总缓冲区大小是一定的,开始的时候readbuff等于writebuff,为0,当写入数据的时候,写大小为数剧最后一个位置的后一个位置开始,对依然从数据开头开始读,读完之后,我们不用删除,当再次写入的时候,写入到上一个数据的最后一个位置的下一个位置,读的大小等于写的大小。功能:对一个描述符进行监控时间管理,意义,对于描述符的状态更容易维护,对之后的操作流程更加方便,功能设计:描述符是否可读,是否可写,监控可读,监控可写,解除可读,解除可写,解除所有文件描述符。

weixin_66828150的博客 939

基于muduo库实现高并发服务器

基于muduo库实现高并发服务器

qq_67582098的博客 3294

仿muduo库实现高并发服务器

Reactor模式是一种事件驱动的高性能网络编程模型,通过多路复用IO统一监听多个客户端的连接请求,再以非阻塞的方式将就绪事件派发给对应的处理线程/进程去处理。Reactoe模式是构建高并发服务器的核心技术。1.单Reactor单线程事件监听、事件派发、事件处理、业务处理都在同一个线程中完成。一个Reactor监听所有的事件(连接事件,IO事件),事件就绪后再由该Reactor派发给对应的事件处理器处理事件,如果是IO事件还要将事件处理后的数据交给业务处理逻辑。2.单Reactor多线程。

2301_76197086的博客 1396

仿muduo库的高并发服务器

这篇博客将会记录并学习高并发服务器这个项目下的结构设计思维和代码细节实现,希望后来者对该项目感兴趣并愿意阅读这篇文章的朋友能有所收获,也希望能给出不足之处借以完善。muduo 库作为一款基于 C++11 的高性能网络编程库,其功能围绕 “高效处理网络 IO 事件” 和 “简化多线程网络编程复杂度” 展开。由于适合处理大量网络连接和数据交互的场景中应用广泛,该库特别适合在 Linux 环境下开发高并发服务器(如分布式系统、中间件、游戏服务器等)。

make_day_day_up的博客 1321

[项目设计]仿muduo网络库实现高性能服务器组件

项目是仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器 通过实现的高并发服务器组件,可以简洁快速的完成⼀个高性能的服务器搭建。

a_zhee1的博客 1598

【项目日记】仿mudou的高并发服务器 --- 整体框架搭建 ,实现时间轮模块

本文介绍了仿mudou的高并发服务器的整体框架设计思路!

叫我龙翔的博客 1万+

【项目】仿muduo库one thread one loop式并发服务器HTTP协议模块

本项目实现了一个高性能C++ HTTP服务器,包含五个核心模块:1. Util模块提供URL编解码、文件读写等基础功能;2. HttpRequest/Response模块处理请求和响应数据结构;3. HttpContext模块管理请求解析状态;4. HttpServer模块集成TCP服务、路由映射和请求分发。服务器支持GET/POST/PUT/DELETE方法,处理静态资源和动态请求,具备长连接、超时管理等功能。测试验证了长连接、错误请求处理、大文件传输等场景,在5,000并发下实现5,369 QPS的吞吐

2303_77756141的博客 1353

【项目】仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器(Http测试板块)

测试

m0_70088010的博客 1078

【仿Mudou库one thread per loop式并发服务器实现】项目介绍+前置技术知识点

仿muduo库One Thread One Loop式主从Reactor模型实现高并发服务器这个项目是通过咱们实现的高并发服务器组件,可以简洁快速的完成一个高性能的服务器搭建。并且,通过组件内提供的不同应用层协议支持,也可以快速完成一个高性能应用服务器的搭建。在这里,要明确的是咱们要实现的是一个高并发服务器组件,因此当前的项目中并不包含实际的业务内容。

fight_p的博客 2904
上一篇: 仿muduo库实现并发服务器-http服务器(1)
下一篇: 在线五子棋对战(1)
bugu___
博客等级 码龄2年 63粉丝 28原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值