仿muduo库实现高并发服务器-下

目录

LoopThread模块

LoopThreadPool模块

TcpServer模块

HTTP服务器

HttpRequest模块,存储HTTP请求信息要素,包括

知识点:正则表达式,是一套字符串匹配规则,用来快速查找,提取,校验字符串

回归HttpRequest模块

HttpResponse模块,存储Http相应信息要素

[HttpContent模块,用于 提取并解析 一个完整的请求HttpRequest](#HttpContent模块,用于 提取并解析 一个完整的请求HttpRequest)

HttpServer模块


引入

LoopThread模块

引入LoopThread模块,将EventLoop与线程Thread整合到一起,是对 EventLoop+专属运行线程 的整体封装,

  • 1.创建EventLoop对应的线程
  • 2.在子线程中实例化EventLoop对象

实现方法:

  1. EventLoop的构造函数时将当前正在运行线程的id传进去,所以创建子线程,在子线程里创建EventLoop对象
  2. GetLoop()函数是给父线程调用的,子线程把自己的EventLoop地址传给父线程,便于后续主线程将连接分发到不同子线程
  3. 而且还需要锁和条件变量,锁:父线程要读取_loop,子线程要往里写入_loop;条件变量:父线程一旦创建完子线程,立刻调用GetLoop获取子线程的_loop,子线程可能没执行完ThreadEntry(),_loop是空值,子线程正在执行ThreadEntry()和父线程正在执行GetLoop()同时执行,要父线程阻塞在子线程的条件变量中,这样获得的_loop是有效的;直到子线程赋值完_loop,唤醒该条件变量下的所有线程
cpp 复制代码
class LoopThread
{
private:
    // 0.互斥锁+条件变量,实现loop获取的同步关系,避免线程创建但还没实例化出loop,外界获取loop的情况
    // 外界获取loop必须得等到它已经实例化完了
    std::mutex _mutex;
    std::condition_variable _cond;
    // 1.eventloop对应的线程
    std::thread _thread;
    // 2.本身并不创建eventloop,只是记录真实对象的地址
    EventLoop *_loop;

private:
    // 线程入口函数
    // 1.实例化一个EventLoop对象,并且开始运行eventloop模块的功能,loop.Start()死循环
    // 2.唤醒cond上可能阻塞的线程
    void ThreadEntry()
    {
        // 子线程栈上创建的对象,主线程找不到它的地址,不能直接访问它
        EventLoop loop; // 生命周期随loopthread
        {
            std::unique_lock<std::mutex> lock(_mutex);
            //_loop是LoopThread的成员变量,因为是线程,只要拿到地址,父子都能访问,记录_loop的真实地址
            // 取子线程 私有对象的地址&loop,写入到共享的指针变量_loop(父子线程共用的变量)
            // 主线程用指针操控_loop,和子线程用的是同一个EventLoop对象
            _loop = &loop;
            // 这样主线程就拿到了 子线程真正运行的EventLoop,为什么要拿到呢?
            //  主线程要把新连接 分配给子线程的EventLoop,但又碰不到它
            // GetLoop()拿到 子线程中干活的 EventLoop,就需要把它的地址提前存入_loop
            _cond.notify_all();//唤醒在条件变量下阻塞的所有线程,告诉它们,_loop已经初始化完成了,可以拿走了!
        }
        loop.Start(); // 子线程的loop 开始处理事件
    }

public:
    // 创建线程,设置线程入口函数,创建子线程立刻跑
    LoopThread() : _loop(nullptr), _thread(std::thread(&LoopThread::ThreadEntry, this))
    {
    }
    // 新连接到来了,要给它分配一个eventloop,首先得获取到这个eventloop
    // 返回当前线程关联的EventLoop对象的指针
    EventLoop *GetLoop()
    {
        EventLoop *loop = nullptr;
        {
            std::unique_lock<std::mutex> lock(_mutex); // 加锁
            // loop为空,线程放在条件变量下等待
            // 每次被唤醒后,自动检查条件是否满足,避免虚假唤醒
            _cond.wait(lock, [&]()
                       { return _loop != nullptr; });
            // 满足条件
            loop = _loop;
        }
        return loop;
    }
    // 一个问题,当线程创建完毕,但还没有实例化EventLoop对象的时候,
    // 如果外界获取GetLoop时,就会获取到空,不合适,所以要加锁
};

LoopThreadPool模块

管理所有线程LoopThread,拿到所有线程的地址进而通过GetLoop()拿到子线程的对应的Eventloop地址,线程的创建、连接conn分发到哪个线程对应的EventLoop

注意事项,

  • 1.线程数量可配置,主从Reactor,主线程负责新连接的获取,从属线程负责新连接的事件监控以及处理。为了更加灵活,从属线程的数量可以为0,也就是线程池的数量可以为0,也就是但Reactor服务器,一个线程负责获取新连接+新连接的处理
  • 2.连接的分配,主线程负责获取新连接,接着要处理这个新连接,也就是监控和事件就绪的处理。如果由0个从属线程,则直接将这个连接分配给主线程的EventLoop,直接处理;如果有多个从属线程,采取均分的思想,将连接分配给对应线程专属的EventLoop,EventLoop负责新连接的监听以及处理。
  • 3.父线程Create()创建所有线程的时候,一旦子线程创建成功,子线程就在它的栈上创建EventLoop,把这个地址交给父线程,父线程通过GetLoop()就可以获取到;父线程创建完子线程后立刻调用GetLoop(),有可能此时子线程可能还没执行完ThreadEntry(),没将_loop的地址交给父线程,因此父线程就会阻塞在对应子线程的条件变量中,直到获取到_loop返回
cpp 复制代码
class LoopThreadPool
{
private:
    // 从属线程的数量
    int _thread_count;
    int _next_loop_idx; // eventloop的下标
    // 线程分配,如果从属线程为0,直接分配到主线程,否则就逐个分配到_thread里面对应的eventloop
    EventLoop *_baseloop; // 主eventloop,运行在主线程
    // 将这个指针设置给conn,就相当于把conn挂到了eventloop,就挂到了线程上
    std::vector<EventLoop *> _loops;
    // 管理从属线程
    std::vector<LoopThread *> _threads;
public:
    LoopThreadPool(EventLoop *baseloop) : _thread_count(0), _next_loop_idx(0), _baseloop(baseloop)
    {
    }
    // 设置线程数量
    void SetThreadPoolCount(int count)
    {
        _thread_count = count;
    }
    // 创建、启动所有从属线程
    void Create()
    {
        if (_thread_count > 0)
        {
            _threads.resize(_thread_count);
            _loops.resize(_thread_count);
            // 创建LoopThread
            for (int i = 0; i < _thread_count; i++)
            {
                _threads[i] = new LoopThread();     // 创建LoopThread,就创建了子线程+对应的eventloop,并启动
                _loops[i] = _threads[i]->GetLoop(); // 获取子线程的 eventloop指针,实例化eventloop完成才会返回
            }
        }
    }
    // 获取下一个EventLoop,用来轮转分配
    EventLoop *NextLoop()
    {
        if (_thread_count == 0)
        {
            return _baseloop; // 从属线程为0,直接返回主线程
        }
        _next_loop_idx = (_next_loop_idx + 1) % _thread_count;
        return _loops[_next_loop_idx];
    }
};
cpp 复制代码
Connection(uint64_t conn_id, int sockfd, EventLoop *loop)
     : _conn_id(conn_id), _sockfd(sockfd), _enable_inactive_release(false), _loop(loop),
       _statu(CONNECTING), _socket(_sockfd), _channel(_sockfd, _loop)
{
     //...
}
void Established() // 连接建立就绪后,进行channel回调设置,启动读监控,调用_connected_callback
{
    _loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this));
}

给新连接conn分配eventloop之后,conn可以调用 EstablishedInLoop(),进而将自己加入到对应子线程中,进行事件监控

TcpServer模块

主线程EventLoop(每一个线程内部都要属于自己的epoll)、监听套接字Acceptor、连接池ThreadPool、各类回调 全部整合在一起,对外暴露极简的启动 / 停止接口,上层业务不用关心底层 IO、线程、监听细节。

管理:

  • 1.Acceptor对象,封装监听套接字,读事件就绪的阶段处理回调函数 NewConnection()传入,监听事件 Channel 加入到当前主线程 EventLoop 的 Poller 进行事件监控
  • 2.主EventLoop对象,主线程主Reactor,_baseloop实现对监听套接字的事件监控
  • 3.一旦有新连接,为它创建Conn,也就是监听套接字的回调函数NeConnection()的逻辑,再管理所有连接,服务器层面上的管理,连接要释放,首先要调用连接自己的销毁函数,其次调用服务器层面的函数,将它从tcpserver类中移除
  • 4.LoopThreadPool对象,创建线程池,将新连接挂到从属线程,进行事件监听和处理

功能:

  • 1.设置从属线程池数量
  • 2.设置新连接connection建立的阶段回调函数,比如 连接建立回调OnConnected,收到消息进行业务处理回调OnMessage...,提供Connection中Channel的HandlerRead()的部分逻辑,这样一旦事件就绪,1.调用HandlerRead()读取数据到Buffer,2.再进行业务处理OnMessage()****_channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
  • 3.是否启动非活跃连接超时销毁功能
  • 4.启动服务器
  • 5.添加定时任务的功能

流程:

  • 1.在TcpServer中实例化一个Acceptor对象,一个EventLoop对象--bashloop
  • 2.将Acceptor挂到baseloop上进行事件监控
  • 3.一旦Acceptor就绪了可读事件,执行读事件回调函数,获取新连接
  • 4.对新连接创建Connection,并进行管理
  • 5.对连接Connection设置功能回调--连接建立完成回调,收到消息回调,关闭回调,任意事件回调
  • 6.启动Connection的非活跃连接超时销毁功能
  • 7.将新连接对应的Connection挂到LoopThreadPool的从属线程的EventLoop中,进行事件监控
  • 8.一旦Connection连接可读事件就绪,执行读事件回调函数,读取数据,之后TcpServer设置的消息回调函数--进行业务处理
cpp 复制代码
class TcpServer
{
private:
    // id,自动增长
    uint64_t _next_id;
    // 服务器要监听的端口
    int _port;
    // 非活跃连接的统计时间,多长时间无通信就是非活跃连接
    int _timeout;
    // 是否启动非活跃超时销毁的功能
    bool _enable_inactive_release;
    // 主线程的EventLoop对象,负责监听事件的处理
    EventLoop _baseloop;
    // 监听套接字的管理对象
    Acceptor _acceptor;

    // 从属EventLoop线程池,把获取的新连接 交给从属的eventloop
    LoopThreadPool _pool;
    // 所有连接的管理,为了便于查找,用unordered_map
    // 保存shared_ptr,一旦删除,才表明,这个连接要释放
    std::unordered_map<uint64_t, PtrConnection> _conns;

private:
    // Connection需要的回调
    using ConnectedCallback = std::function<void(const PtrConnection &)>;
    using MessageCallback = std::function<void(const PtrConnection &, Buffer *)>;
    using ClosedCallback = std::function<void(const PtrConnection &)>;
    using AnyEventCallback = std::function<void(const PtrConnection &)>;

    using Functor =std::function<void()>;

    ConnectedCallback _connected_callback;
    MessageCallback _message_callback;
    ClosedCallback _closed_callback;
    AnyEventCallback _event_callback;

private:
    // 辅助功能
    //  为新连接构造一个Connection
    void NewConnection(int fd)
    {
        //DBG_LOG("new connection function!");
        _next_id++;
        // 有了新连接,获取它,并分配到从属线程当中
        PtrConnection conn(new Connection(_next_id, fd, _pool.NextLoop()));

        // 设置阶段回调函数
        conn->SetMessageCallback(_message_callback);
        conn->SetClosedCallback(_closed_callback);
        conn->SetConnectedCallback(_connected_callback);
        conn->SetAnyEventCallback(_event_callback);

        conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection,this,std::placeholders::_1));
        // 启动非活跃超时销毁
        if(_enable_inactive_release==true)
            conn->EnableInactiveRelease(_timeout);

        conn->Established(); // 就绪初始化
        _conns.insert(std::make_pair(_next_id, conn));
    }
    // 从管理的_conn里移除Connection连接,才能真正释放连接
    // SetSrvCallback() 把服务器内部管理的Connection信息移除,连接最终被释放
    void RemoveConnection(const PtrConnection& conn)
    {
        _baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop,this,conn));
    }
    
    // 添加定时任务功能
    void RunAfterInLoop(const Functor &task, int delay)
    {
        _next_id++;
        _baseloop.TimerAdd(_next_id, delay, task);
    }
    void RemoveConnectionInLoop(PtrConnection& conn)
    {
        int id=conn->GetId();
        auto it=_conns.find(id);
        if(it!=_conns.end())
            _conns.erase(it);
    }

public:
    TcpServer(int port)
        : _port(port), _next_id(0), _enable_inactive_release(false),
          _acceptor(&_baseloop, port), _pool(&_baseloop)
    {  
        // 1.1设置acceptor读事件就绪的回调函数
        _acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection,this,std::placeholders::_1));
        // 2.开启监听,将监听套接字挂到主线程baseloop上
        _acceptor.Listen();
    }
    void SetThreadCount(int count)
    {
        return _pool.SetThreadPoolCount(count);
    }
    void Start()
    {
        // 1.创建线程池的从属线程,之前要设置线程数量
        _pool.Create();
        // 3.主线程eventloop启动,开始监听 客户端的新连接
        _baseloop.Start(); // 死循环
        return;
    }
    // 添加定时任务功能
    void RunAfter(const Functor &task, int delay)
    {
        _baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay));
    }

    void SetConnectedCallback(const ConnectedCallback &cb) { _connected_callback = cb; }
    void SetMessageCallback(const MessageCallback &cb) { _message_callback = cb; }
    void SetClosedCallback(const ClosedCallback &cb) { _closed_callback = cb; }
    void SetAnyEventCallback(const AnyEventCallback &cb) { _event_callback = cb; }

    // 非活跃连接的超时销毁功能
    void EnableInactiveRelease(int timeout)
    {
        _timeout = timeout;
        _enable_inactive_release = true;
    }
};

EchoServer,对tcpserver的进一步封装

cpp 复制代码
class EchoServer
{
private:
    TcpServer _server;
private:
    // 给server设置的阶段回调函数
    //...
public:
    EchoServer(int port) : _server(port)
    {
        _server.SetThreadCount(2);
        _server.EnableInactiveRelease(10);
        _server.SetClosedCallback(std::bind(&EchoServer::OnClosed,this,std::placeholders::_1));
        _server.SetConnectedCallback(std::bind(&EchoServer::OnConnected,this,std::placeholders::_1));
        _server.SetMessageCallback(std::bind(&EchoServer::OnMessage,this,std::placeholders::_1,std::placeholders::_2));
        _server.Start();
    }
    void Start()
    {
        _server.Start();
    }
};

这样直接初始化echoserver,并start就能运行回显服务器了

cpp 复制代码
EchoServer server(8080);
server.Start();

测试


工具类的实现

知识点:Http的请求格式

1.请求行,格式:请求方法 URL HTTP版本 \r\n,比如 GET /index.html HTTP/1.1,各部分含义:请求方法:告诉服务器你想怎么做,是GET,获取资源,还是POST,提交数据,比如登陆。URL:想访问服务器上的哪个资源,路径,以及传递参数。

2.请求报头,每一行都是,键:空格 值\r\n。比如Content-Length: 10,如果有请求正文,告诉服务器正文有多少字节,告诉服务器一堆额外描述,比如客户端信息等

3.请求正文,要发给服务器的业务数据,不是所有请求都有,比如POST,表单的数据放在请求正文中,GET,可以没有请求正文,查询的参数放在URI里

URL:

原始的字符串是 q=苹果&香蕉,也就是提交的 查询参数是苹果&香蕉,直接拼接URI,

/search?msg=苹果&香蕉

拿这个举例,

解析规则:&是参数之间的分隔标记,

先用&把多个参数分离,得到多个小段 msg=苹果 香蕉

再对每一小段,用 = 分割成 参数名=参数值

  • msg=苹果 → key=msg,value=苹果
  • 香蕉 → 里面没有 =,key=香蕉,value = 空字符串

所以要采取URI编码,把数据里的&,编码成%26,编码的完整URL为:/search?msg=苹果%26香蕉,

问号后内容:msg=苹果%26香蕉

  • 字符串里没有普通 &,不会切割成两段
  • 整段拆分:msg = 苹果%26香蕉
  • 服务器拿到值后自动解码,%26 还原回 & 最终得到你想要的完整:msg 的值 = 苹果&香蕉

某些字符不能随意出现,因为已经被 url 当做特殊意义理解了,

  • RFC 3986标准,不编码特殊字符有:. - _ ~ 以及字母和数字
  • W3C 标准,查询字符串中的空格,比如msg=苹果 香蕉,需要编码成+

编码规则:将特殊字符的ascii值,转换为两个16进制字符,并且加一个前缀%

URL编码

cpp 复制代码
static std::string UrlEncode(const std::string url,bool convert_space_to_plus)
    {
        std::string res;
        for(auto& c:url)
        {  
            if(c=='.'||c=='-'||c=='_'||c=='~')
            {
                //不用编码的字符
                res+=c;
                continue;
            }
            if(c==' '&&convert_space_to_plus==true)
            {
                res+='+';
                continue;
            }
            //数字和字母不需要转换
            if(isalnum(c))
            {
                res+=c;
                continue;
            }
            //剩下的字符都需要编码,%HH,比如不是W3C标准的空格
            char tmp[4]={0};
            //%%,第一个%为转义字符,输出一个%
            //%X,把数字输出为大写的16进制
            snprintf(tmp,4,"%%%02X",c);
            res+=tmp;
        }
        return res;
    }

URL解码

cpp 复制代码
// 把十六进制的的字符,翻译成对应的整数数值,数字0-15
    char HEXTOI(char c)
    {
        if(c >= '0' && c <= '9')
        {
            //数字字符
            return c-'0';
        }
        else if(c >= 'a' && c <= 'z')
        {
            //小写字符
            return c-'a'+ 10;
        }
        else if(c >= 'A' && c <= 'Z')
        {
            return c-'A'+ 10;//B--11
        }
        else 
        {
            //有问题
            return -1;
        }
    }
    static std::string UrlDecode(std::string url , bool convert_plus_to_space)
    {
        // %2B 
        // 遇到%,将紧随其后的两个字符转换为两个数字,第一个数字*16,+第二个数字,就是这个字符的ascii
        std::string res;
        for(int i=0;i<url.size();i++)
        {
            if(url[i]=='+'&& convert_plus_to_space==true)
            {//加号+ 转成空格
                res+=' ';
                continue;
            }
            if(url[i]=='%' && i+2 < url.size())
            {
                char v1=HEXTOI(url[i+1]); // 第一个数字
                char v2=HEXTOI(url[i+2]);
                // 字符底层存储的是数字
                char val= (v1 * 16) + v2;
                res+=val;
                i+=2;
                continue;
            }
            res+=url[i];
        }
        return res;
    }

通过文件名判断这个文件是否是目录

HTTP服务器

HttpRequest模块,存储HTTP请求信息要素,包括

  • 请求行:请求方法,URL,协议版本

URL又包括:资源路径,查询字符串 GET /search?word=C++&en=uft8 HTTP/1.1

  • 请求报头: key:value\r\nkey:value\r\n... Content-Length:0\r\n
  • 请求正文

要素:请求方法,资源路径,协议版本,查询字符串,请求报头,正文,正则表达式帮助提取

**URI 比如,**https://leetcode.cn//?envType=study-plan-v2\&envId=top-100

接口:

1.提供查询字符串的功能,比如能解析 URL 里 ? 后面的键值、拿到参数、根据参数返回不同内容 GET /search?word=C++

  • 解析 URI,区分路径和 ? 后的查询字符串;
  • 分割 &=,提取所有键值对;
  • 支持 URL 解码(%2B+);
  • 业务代码可以读取参数,动态生成不同响应。

2.请求报头里有多个key,value,要实现一套管理请求报头的方法,包括单个查询、获取,插入功能

3.获取正文长度Content-Length

4.判断长连接还是短连接Connection:close / keep-alive


知识点:正则表达式,是一套字符串匹配规则,用来快速查找,提取,校验字符串

std::regex_match函数,判断整个字符串是否完全匹配 正则规则

bool regex_match( 待匹配字符串, 正则表达式 );从头到尾整串必须完全匹配

复制代码
std::regex_match ("subject", std::regex("(sub)(.*)") )

. :匹配任意单个字符,但不能匹配 \r\n 换行 / 回车符

* :前面的字符出现0次或无数次

+:前面字符出现一次或多次。 意思就是,整个字符串必须 以sub开头,后面可以跟任意字符(包括啥也不跟)

():功能1,分组,把括号内的内容看成一个整体,用来限定量词(*/+/?/{})的作用范围。

  • ab+a 固定,b 重复 1 次及以上 → 匹配 ababbabbb
  • (ab)+:把 ab 当成整体重复 → 匹配 abababababab

():功能2,捕获分组,()会把匹配到的内容单独存放起来,后续可以按编号取出

例子1

  • std::string s = "subject";
  • std::regex reg("(sub)(.*)");
  • std::smatch res;
  • regex_match(s, res, reg)

//res0固定代表整个完整匹配的字符串subject res1表示第一个()捕获的内容sub res2表示第二个()捕获的内容ject

bool std::regex_match(const std::string& src,std::smatch& matches,std::regex& e)

src原始字符串;matches存放符合规则的提取的数据;类似数组 e正则表达式的匹配规则;返回值用于确定匹配是否成功

例子2

  • std::string str="/numbers/1234";
  • std::regex e("/numbers/(\\d+)");
  • std::smatch res;
  • std::regex_match(str,res,e);//res0=/numbers/ res1=1234

\d匹配单个数字,+表示前面的\d数字至少出现1次,单个\是转义字符,要想写出\d字面量,必须再在前面加上一个\,\\d,然后()来捕获 匹配的一串连续数字

\[\]:表示匹配在\[\]里的任意字符,abc匹配a或b或c,单个字符匹配

^:两种含义,在\[\]里,\^表示取反\^abc匹配非a/b/c的字符

不在\[\]里,^是字符串的开头锚点,^/numbers/,表示匹配的字符串必须以/numbers/开头,比如/numbers/abc

?:两种含义,表示前面 字符/分组 出现了0次或者1次,字面量?,想匹配真实?,必须\?

例子1

  • 字符串:/user/1001/?name=tom
  • 正则:^(\^?+)(\?(.*))?$

^:从字符串开头开始匹配

(\^?+):分组 1,匹配所有不是?的字符 → 拿到路径 /user/1001

\?:转义,匹配字面问号

(.*):分组 3,问号后面所有内容 → 查询串 name=tom

?&:分组后面的?→(\?(.*))?,整个 ?xxx 这一整段,要么出现 1 次,要么完全没有

正则默认就是 从左到右、逐个字符依次匹配

cpp 复制代码
int main()
{
    //HTTP请求格式:请求方法GET  URL资源 /bit/login?user=sxm&password=123123  协议版本HTTP/1.1\r\n
    std::string str="GET /bit/login?user=sxm&password=123123 HTTP/1.1";
    std::smatch res;
    //1.请求方法的匹配 GET POST ...
    //第一个正则
    std::regex e("(GET|POST|HEAD|PUT|DELETE) .*");
    bool ret=std::regex_match(str,res,e);
    if(ret==false) return -1;
    
    //2.请求方法后面以空格开始,直到?作为结束的字符串,提取URL中的资源路径
    // [^?]*,[^?]表示匹配非问好字符,*表示0次或多次,从当前位置开始,连续匹配所有非?字符,直到碰到?为止
    // .*匹配任意单个字符,0个或多个,也就是往后所有剩余字符
    //第二个正则
    std::smatch res1;
    std::regex e1("(GET|POST|HEAD|PUT|DELETE) ([^?]*).*");
    bool ret1=std::regex_match(str,res1,e1);
    if(ret1==false) return -1;
    
    //3.提取URL中的查询字符串,?开始,空格结束
    // \\?表示原始的?,(.*)表示提取?之后的任意字符0或多次,直到遇到 空格
    std::smatch res2;
    std::regex e2("(GET|POST|HEAD|PUT|DELETE) ([^?]*)\\?(.*) .*");
    bool ret2=std::regex_match(str,res2,e2);
    if(ret2==false) return -1;
    
    //4.提取协议版本
    // \\.表示原始的.,[01]表示匹配任意一个字符 0或1
    std::smatch res3;
    std::regex e3("(GET|POST|HEAD|PUT|DELETE) ([^?]*)\\?(.*) (HTTP/1\\.[01])");
    bool ret3=std::regex_match(str,res3,e3);
    if(ret3==false) return -1;
    for(auto& s:res3)
    {
        std::cout<<s<<std::endl;
    }

    return 0;
}

问题1. .匹配不了\r\n

  • std::string str="GET /bit/login?user=sxm&password=123123 HTTP/1.1\r\n";
  • // \\.表示原始的.,01表示匹配任意一个字符 0或1,\r\n可以有或者没有
  • // (?:\n|\r\n)? ,(?:...)表示,匹配某个格式字符串,单数不提取 最后的问好,表示匹配0次或者1次,可能有也可能没有
  • std::smatch res3;
  • std::regex e3("(GET|POST|HEAD|PUT|DELETE) (\^?*)\\?(.*) (HTTP/1\\.01)(?:\n|\r\n)?");
  • bool ret3=std::regex_match(str,res3,e3);

问题2:而且可能没有查询字符串,那这时候就要 在\\?(.*) 整体后面加?

  • std::string str="GET /bit/login HTTP/1.1\r\n";
  • std::regex e3("(GET|POST|HEAD|PUT|DELETE) (\^?*)(\\?(.*))? (HTTP/1\\.01)(?:\n|\r\n)?");
  • 接着加 ?:,表示不要以?开头的字符串,要(.*)的字符串,外层不捕获,内层捕获
  • 2. std::regex e3("(GET|POST|HEAD|PUT|DELETE) (\^?*)(?:\\?(.*))? (HTTP/1\\.01)(?:\n|\r\n)?");

回归HttpRequest模块

HttpRequest 还需要添加一个要素,std::smatch 正则表达式,来保存提取的请求行的信息

cpp 复制代码
class HttpRequest
{
public:
    std::string _method;//请求方法
    std::string _path;//资源路径
    std::string _version;//协议版本
    std::string _body;//正文
    std::unordered_map<std::string,std::string> _headers;//请求报头的key-value
    std::unordered_map<std::string,std::string> _params;//url 查询字符串
    std::smatch _matches;//存放资源路径url的数据
public:
    //设置\插入 头部字段
    void SetHeader(const std::string& key,const std::string val);
    //查询,是否有 头部字段
    bool HasHeader(const std::string& key);
    //获取 头部字段
    std::string GetHeader(const std::string& key);
    //设置、插入 查询字符串
    void SetParam(const std::string& key,const std::string val);
    //查询 查询字符串
    bool HasParam(const std::string& key);
    //获取 查询字符串
    std::string GetParam(const std::string& key);
    //获取正文长度
    size_t ContentLength();
    //判断是长连接还是短连接
    bool Close();
    //重置 请求
    void ReSet();
};

HttpResponse模块,存储Http相应信息要素

要素:响应状态码,请求报头字段,相应正文,重定向信息:是否进行了重定向的标准,以及重定向的路径

接口:

1.头部字段的新增、查询、获取

2.正文的设置

3.长短连接的判断

cpp 复制代码
class HttpResponse
{
public:
    int _statu;//响应状态码
    std::unordered_map<std::string,std::string> _headers;//头部字段
    std::string _body;//相应正文
    bool _redirect;//重定向标志,响应里是否设置了重定向
    std::string _redirect_url;//重定向的url
public:
    HttpResponse():_statu(200),_redirect(false)
    {}
    HttpResponse(int statu):_statu(statu),_redirect(false)
    {}
    //头部字段的插入
    void SetHeader(const std::string& key,const std::string& val);
    //是否包含某个头部字段
    bool HasHeader(const std::string& key);
    //获取某个头部字段
    std::string GetHeader(const std::string& key);
    //正文设置,同时设置正文类型Content-Type
    void SetContent(const std::string& body,const std::string& type="text/html");
    //设置重定向
    void SetRedirect(const std::string& url,int statu=302); _redirect_url=url;
    }
    //长短连接的判断
    bool Close();
    //重置响应
    void Reset();
};

HttpContent模块,用于 提取并解析 一个完整的请求HttpRequest

每个连接配套一套buffer,TCP recv读取到的数据全部放入buffer,缓存不完整的分段数据-TCP分包、粘包,只存放原始的字节流

HttpContent管3件事:记录当前的解析状态(请求行、头部、正文、完成、错误);提供一套解析函数,消耗 buffer 里的数据;把解析成功的数据存放至HttpContent的内部成员request

每个连接配一个Content协议上下文,用于存放这个连接准备发送的但可能还不完整的请求httprequest

cpp 复制代码
typedef enum 
{//将要、正c在
    RECV_HTTP_LINE,//当前接收到 请求行的阶段
    RECV_HTTP_HEAD,//处于 头部接收 阶段 
    RECV_HTTP_BODY,// 接收到 正文 的阶段
    RECV_HTTP_OVER,//结束阶段,等待处理请求
    RECV_HTTP_ERROR // 解析完整请求出错了 的阶段
}HttpRecvStatu;
#define MAX_LINE 8192
class HttpContent
{
private:
    int _resp_statu;// 响应状态码
    HttpRecvStatu _recv_statu;//当前接收 处于的阶段
    HttpRequest _request;//已经解析得到的 请求信息
private:
    //...
    // 解析请求行
    bool ParseHttpLine(std::string& line)
    {
        std::smatch matches;
        //"GET /bit/login?user=sxm&password=123123 HTTP/1.1";
        std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
        bool ret=std::regex_match(line,matches,e);  
        if(ret==false)
        {
            _recv_statu=RECV_HTTP_ERROR;
            _resp_statu=400;//bad request 解析失败,请求有问题
            return false;
        }
        //0 : GET /bit/login?user=sxm&password=123123 HTTP/1.1
        //1 : GET
        //2 : /bit/login
        //3 : user=sxm&password=123123
        //4 : HTTP/1.1
        // 放入到request里面
        _request._method=matches[1];
        // 资源路径的获取,需要URL解码,不需要+转空格
        _request._path=Util::UrlDecode(matches[2],false);
        _request._version=matches[4];
        // 查询字符串的解析,按&分割,按=分割,变成key-value,存放到_params里
        // 查询字符串的获取,需要URL解码,需要+转空格
        std::vector<std::string> query_string_array;
        std::string query_string=matches[3];
        Util::Split(query_string,"&",&query_string_array);
        for(auto& str:query_string_array)
        {
            //1.user=sxm  2.password=123123  key=value
            size_t pos=str.find("=");
            if(pos==std::string::npos) 
            {
                _recv_statu=RECV_HTTP_ERROR; //请求有问题
                _resp_statu=400;
                return false;
            }
            //找着了,填充请求字符串params
            std::string key=Util::UrlDecode(str.substr(0,pos),true);
            std::string val=Util::UrlDecode(str.substr(pos+1),true); 
            _request.SetParam(key,val);
        }
        //查询字符串中是经过url编码的,要进行解码
        return true;
    }
    // 接收请求行,从buffer里拿数据
    bool RecvHttpLine(Buffer* buf)
    {
        if(_recv_statu!=RECV_HTTP_LINE) return false;
        // 1.获取请求行的数据,到\r\n,
        // GetLineAndPop是获取buffer一行的方法,如果缓冲区数据不足一行,返回""
        // 或者一行的数据超大
        std::string line = buf->GetLineAndPop();
        if(line.size()==0)
        {
            //缓冲区数据不足以行,判断缓冲区的刻度数据长度,很长了还不足以行,有问题
            if(buf->ReadAbleSize()>MAX_LINE)
            {
                _recv_statu=RECV_HTTP_ERROR;
                _resp_statu=414;//uri too long
                return false;
            }
            //缓冲区数据不足一行,再等等新数据,不处理
            return true;
        }
        //接收到了一行数据,先判断
        if(line.size()>MAX_LINE)
        {
            _recv_statu=RECV_HTTP_ERROR;
            _resp_statu=414;//uri too long
            return false;
        }
        //然后解析首行,填充request
        bool ret=ParseHttpLine(line);
        if(ret==false) return false;
        //首行接收解析完毕,接下来进入头部获取
        _recv_statu=RECV_HTTP_HEAD;
        return true;
    }
public:
    HttpContent():_resp_statu(200),_recv_statu(RECV_HTTP_LINE)
    {}
    //...
    //接收并解析Http请求
    void RecvHttpRequest(Buffer* buf)
    {
        switch(_recv_statu)
        {
            case RECV_HTTP_LINE: 
                RecvHttpLine(buf);
                //不要加break,接着继续解析,如果上一个解析失败,不会更改状态,下一个也进不去
            case RECV_HTTP_HEAD: 
                RecvHttpHead(buf);
            case RECV_HTTP_BODY: 
                RecvHttpBody(buf);
        }
        return;
    }
};

HttpServer模块

对HTTP所有模块进行整合,搭建一个httpserver服务器

1.设计一张请求路由表,即映射关系,表中记录了请求路径的正则 和,对应函数的映射关系,进而来进行上层业务处理

  • 当服务器收到一个请求,是动态请求,
  • 1.通过该方法(Get)的Handles,匹配所有正则表达式,匹配成功,
  • 2.执行这个正则表达式对应的处理函数
  • 3.构建resp

组件使用者实现对应的业务处理函数,添加到服务器中,这样服务器只需要接收、解析,请求查找路由映射关系,执行业务回调函数

同一个 URL 路径/user,GET 和 POST 做的事完全不同,不能用同一个函数处理,所以要有多个请求方法的路由映射表,比如POST的路由映射表、GET的路由映射表等等

这个路由映射表,key:请求路径 value:对应的回调函数

url中的请求路径可能是 /number/1234----Number(),但请求路径是变化的,比如/number/12345,这些路径对应的回调函数都相同,所以可以用正则表达式填充<string,string> -> <regex,string>,/number/\\d+---统一使用Number()函数来处理

cpp 复制代码
void Dispatcher(HttpRequest& req,HttpResponse* rsp,Handlers& handlers)
        {
            //再对应请求方法的路由表中,查找对应请求是否含有对应请求资源的处理函数,有就调用,没有就返回404
            // 路由表存储的是键值对---正则表达式:处理函数
            //用正则表达式,对请求的资源路径进行正则匹配,匹配成功,使用对应函数进行处理
            // /numbers/\d+  /numbers/12345
            for(auto& it : handlers)
            {
                //用&,这样不用每次调用的时候都拷贝一份新的正则对象,而是直接操作一份内存空间
                const std::regex& e=it.first;
                const Handler& functor=it.second;
                bool ret=std::regex_match(req._path,req._matches,e);
                if(ret==false)
                {
                    continue;//遍历下一个映射关系
                }
                //匹配成功,执行业务处理
                functor(req,rsp);
                return;
            }
            //路由表中没找到相应处理方法
            rsp->_statu=404;
            return;
        }

GET /api/getuser → HandleGetUser

POST /login → HandleLogin

2.还有静态资源请求,必须是Get 或者Head,服务器上预先存在的资源,文件内容写死在硬盘,统一走文件读取函数,所以要有静态资源请求根目录。

  • 1.比如 GET /1234.txt HTTP/1.1 静态资源根目录是 /wwwroot/1234.txt
  • 2.服务器发现请求的是静态资源,将/wwwroot自动拼接到静态资源的前面,得到/wwwroot/1234.txt,那就找这个文件,把这个文件的数据读出来,组织成一个响应rsp,返回给客户端
  • 3.还有一种特殊的请求,url的末尾直接是 /,单纯的目录,要自动拼接默认首页 index.html,得到index.html

判断是静态资源请求

cpp 复制代码
bool IsFileHandler(const HttpRequest& req)
{
    //1.必须设置了静态资源根目录
    if(_basedir.empty()) return false;
    //2.请求方法,必须是GET/HEAD
    if(req._method!="GET"&& req._method!="HEAD") return false;
    //3.请求的资源路径必须合法
    if(Util::ValidPath(req._path)==false) return false;

    //4.请求的资源必须存在
    // 有一种请求 /、/image/ 单纯的目录 ,特殊,追加首页 index.html
    std::string req_path=_basedir + req._path;//加上前面的相对根目录
    if(req._path.back()=='/') 
    {
        req_path += "index.html";
    }
    //这个文件是否是普通文件,静态资源
    if(Util::IsRegular(req_path)==false)
    {
        return false;
    }
    return true;
}

读取静态文件,组成一个响应

cpp 复制代码
// 处理静态资源请求,将静态资源数据读取出来,放到rsp的正文当中,并设置Content-Type
void FileHandler(const HttpRequest& req,HttpResponse* rsp)
{
    std::string req_path=_basedir + req._path;
    if(req._path.back()=='/') 
    {
        req_path += "index.html";
    }
    bool ret=Util::ReadFile(req_path,&rsp->_body);
    if(ret==false)
    {
        return;//读取文件失败
    }
    std::string mime=Util::ExtMime(req_path);
    rsp->SetHeader("Content-Type",mime);
    return;
}

/index.html、/logo.png、/music.aac → ExtMime

服务器处理流程:

  • 1.客户端发数据,从socket接收数据,放到buffer接收缓冲区
  • 2.调用OnMessage回调函数,进行业务处理
  • 3.对请求进行解析,得到了一个完整的HttpRequest请求
  • 4.查找请求对应的处理方法

4.1静态资源请求 html...,将数据读取出来,填充到response里

4.2动态资源请求,根据路由表,找到对应的处理函数,处理完将结果填充到

response里

  • 5.将请求处理完毕后,得到了HttpResponse,组织http格式并发送

感谢阅读^^,完。

相关推荐
爱和冰阔落1 小时前
【Linux】手写日志与固定线程池:任务队列、工作线程和安全退出
linux·运维·c++·redis·安卓
爱奥尼欧1 小时前
【Git】远程分支删了本地还在、两个分支历史不相干?两招故障排解
开发语言·git
Java后端的Ai之路1 小时前
Git冲突完整排查与实战:本地修改覆盖报错到成功推送全流程复盘
开发语言·人工智能·git·python·pop
skywalk81631 小时前
AI 中台记录:使用码道基于deepseek harness制作AI数字中台9.14日
开发语言·人工智能·ai中台
CVer儿1 小时前
vs2022配置qt
开发语言·qt
All for pursuit.1 小时前
【矩阵-2】240.搜索二维矩阵 II
数据结构·c++·算法·leetcode
世岩清上1 小时前
一次性完工的数字展厅,如何预留后期内容更新空间?
大数据·网络·人工智能·音视频·展厅改造
程序猿阿森1 小时前
C语言预处理完全指南:宏定义、条件编译与头文件规范,一篇搞懂工程化编程
c语言·c++·编译
西峰u1 小时前
Java多线程从入门到线程安全
java·开发语言·jvm