【基于one-loop-per-thread的高并发服务器】--- 自主实现HttpServer

Welcome to 9ilk's Code World

(๑•́ ₃ •̀๑) 个人主页: 9ilk

(๑•́ ₃ •̀๑) 文章专栏: 项目


本项目是对之前仿muduo实现高并发服务器这个项目的延续,这个项目是支持协议扩展,根据不同协议进行切换上下文,实现不同协议的支持。下面给出几篇相关博客,有兴趣的朋友可以了解:

muduo高并发服务器 --- 项目介绍&&模块划分

muduo高并发服务器 --- 前置技术

muduo高并发服务器 --- Server模块

模块划分

  1. Util模块:实现一些工具接口,比如对文件的读写、URL编/解码、获取HTTP状态码对应的描述信息、根据文件后缀名获取mime(Content-Type)。

  2. HttpRequest模块:该模块是存储HTTP请求信息的模块,旨在接收到一个数据,然后按照HTTP请求格式进行解析,得到各个关键要素存放起来,让HTTP请求分析更加明显。主要包含的内容其实就是HTTP请求报文的各个要素,如请求方法、URL、协议版本、头部字段、正文等。基于此需要实现一些接口,如头部字段的保存和获取、查询字符串的保存和获取、长短链接的判断、正文长度的获取等。

  3. HttpResponse模块:该模块主要用来存储HTTP响应信息,在进行业务处理的同时,往Response中填充响应要素,填充完毕之后将其组织成HTTP响应格式的数据,然后发送给客户端。其主要包含的内容其实就是HTTP响应报文的各个雅俗,如协议版本、状态码、描述信息(调用Util模块填充)、头部字段、响应正文等。因此它也需要实现头部字段的保存和获取、长短连接的设置与判断、正文的设置等接口。

  4. HttpContext模块:这个模块是一个HTTP请求接收的上下文模块,主要是为了防止在一次接收的数据中,不是一个完整的HTTP请求,则解析过程并未完成,无法进行完整的请求处理,需要在下次接收到新数据后继续根据上下文进行解析,最终得到一个HttpRequest请求信息对象,因此在请求数据的接收以及解析部分需要一个上下文来进行控制接收和处理节奏。

  5. HttpServer模块:这个模块是最终给组件使用者提供的HTTP服务器模块,用于以简单的接口实现服务器的搭建,使HTTP服务器的搭建变得更简便。因此它的内部是需要包含一个TcpServer对象的,同时由于我们支持的是HTTP协议,还需要提供两个接口,一个是连接建立成功的设置上下文接口,还有一个是数据处理接口,收到数据之后需要根据HTTP协议解析请求然后组织回复发送,最后还要包含一个HashMap存储请求与处理函数的映射表,组件使用者向HttpServer设置哪些请求应该使用哪些函数进行处理,等TcpServer收到对应的请求就会使用对应的函数进行处理。

Util工具类

主要实现以下功能:

  1. 读取文件内容
  2. 向文件写入内容
  3. URL编码解码
  4. 通过HTTP状态码获取描述信息
  5. 根据文件后缀名获取mime(Content-type,即请求资源的格式类型)
  6. 判断一个文件是否是目录
  7. 判断一个文件是否是一个普通文件
  8. HTTP资源路径的有效性判断(比如/../index.html,我们不能让客户端请求服务器上的任意文件,只能请求资源目录下的文件)

说明:

  1. 用户在地址栏或页面提交数据之后,浏览器会按照规范把与语法冲突的字符进行编码,然后再把编码后的URI发送给服务器,服务器需要对其解码。在RFC文档中规定的URL绝对不编码字符.``-``_``~以及字母 和数字,还有一些字符(!``$``&``'``()``*``+``,``;``=)用于在每个组件中起到分隔作用的,如=用于表示查询参数中的键值对,&符号用于分隔查询多个键值对。当组件中的普通数据包含这些特殊字符时,需要对其进行编码,如果是作为分割符就不需要进行编码。 还有一个就是在不同的一些标准中的特殊处理,w3c标准中规定param中的空格必须被编码为+

  2. http请求的资源路径有效性:客户端只能请求相对根目录中的资源 , 其他地方的资源都不予理会, 这个相对根目录映射的是某个服务器上的子目录。

cpp 复制代码
class Util
{
public:
  //字符串分割函数 abc,ada,
  static size_t Split(const string& src,const string& sep,vector<string>* array)
  {
      size_t offset = 0;//
      while(offset < src.size())
      {
          size_t pos = src.find(sep,offset); //从src的offset位置开始找sep
          //没找到分隔符:将剩余部分存到数组
          if(pos == std::string::npos)
          {
              array->push_back(src.substr(offset));
              return array->size();
          }
          //abd,,efg对于连续两个分割符不用提取但是注意要跳过分割符
          if(pos == offset)
          {
             offset = pos + sep.size(); //别忘了更新offset
             continue;
          }
          //正常情况
          array->push_back(src.substr(offset,pos-offset));
          offset = pos + sep.size(); //别忘了更新offset
      }
      return array->size();
  }
  //读取文件内容
  static bool ReadFile(const string& filename,string* result)
  {
    //1.定义流对象打开文件
    ifstream ifs(filename,std::ios::binary);
    if(!ifs.is_open())
    {
        ERR_LOG("open %s file failed!!",filename.c_str());
        perror("Error");  // 打印系统的错误信息
        ifs.close();
        return false;
    }
    //2.获取文件大小并读取文件(注意使用流提取,遇到换行符会停止)
    string line;
    while (getline(ifs, line)) 
    {
       *result += line;
       *result += "\n";
    }
    // 3. 判断是否读取完文件
    if (ifs.eof())  // 如果是正常的文件结束
    {
        return true;  // 文件读取成功
    }
    // 如果不是因为文件结束而导致读取失败
    ERR_LOG("read %s file failed!!", filename.c_str());
    return false;
  }
  //向文件写入数据
  static bool WriteFile(const string& filename,const string& data)
  {
        //1.打开文件
        ofstream ofs(filename,std::ios::binary|std::ios::trunc); 
        if(!ofs.is_open())
        {
            DBG_LOG("open %s file failed!!",filename.c_str());
            ofs.close();
            return false;
        }
        //2.写文件
        ofs << data;
        //3.判断
        if(ofs.good() == false)
        {
            DBG_LOG("write %s file failed!!",filename.c_str());
            ofs.close();
            return false;
        }
        DBG_LOG("Write OK");
        return true;
  }
 //URL编码
  static string UrlEnCode(const string& url,bool is_spaceto_plus)
  {
       string result;
       for(const auto& ch : url)
       {
           //不需要编码
           if(ch == '.' || ch == '-' || ch == '_' || ch == '~' || isalnum(ch))
           {
                result += ch;
                continue;
           }
           //遇到空格
           if(ch == ' ' && is_spaceto_plus)
           {
              result += '+';
              continue;
           }
           //剩下的是需要编码为%HH
           char temp[4];
           snprintf(temp,4,"%%%02X",ch);
           result += temp;
       } 
       return result;
  }

  static char HEXTOB(char c)
  {
      //数字
      if(c >= '0' && c <= '9')
       return c - '0';
      //小写字符
      if(c >= 'a' && c <= 'z')
        return c - 'a' + 10; //注意这里要加10
      if(c >= 'A' && c <= 'Z')
        return c - 'A' + 10;
  }

  static string UrlDeCode(const string& url,bool is_spaceto_plus)
  {
     string result;
     for(int i = 0 ; i < url.size() ; i++)
     {
        char ch = url[i];
        if(ch == '+' && is_spaceto_plus)
        {
              result += ' ';
              continue;
        }
        //遇到%
        if(ch == '%' && i+2 < url.size())
        {
            // cout << (char)url[i+1] << " " << (char)url[i+2] << " " ;
            char ch1 = HEXTOB(url[i+1]);
            char ch2 = HEXTOB(url[i+2]);
            char temp = ch1 * 16 +  ch2;
            // cout << (char)temp << endl;  
            result += (char)temp; //注意这里要强转成char
            i += 2; //注意这里要跳过两个字符
            continue;
        }
        //不需要编码
        result += ch;
     }
     return result;
  }

  //响应状态码的描述信息获取
  static string StatuDesc(int statu_code)
  {
     auto it = statu_info.find(statu_code);
     if(it == statu_info.end())
       return "NotKown Statu";
     return it->second; 
  }
  
  //根据文件后缀名获取文件mime
  static string ExtMime(const string& filename)
  {
      //1.先根据文件名获取文件后缀
      size_t pos = filename.find_last_of('.');
      if(pos == string::npos)
          return "No Suffix";
       string Suffix = filename.substr(pos); 
      //2.哈希表中查找
      auto it = file_mime.find(Suffix);
      if(it == file_mime.end())
       return "NotKown Mime";
      return it->second; 

  }

  //判断一个文件是否是一个目录
  static bool isDirectory(const string& filename )
  {
    //1.获取文件属性
    struct stat st;
    int n = ::stat(filename.c_str(),&st);
    if(n < 0) return false;
    //2.s_mode
    return S_ISDIR(st.st_mode);
  }

  //判断一个文件是否是一个普通文件
  static bool isRegular(const string& filename )
  {
    //1.获取文件属性
    struct stat st;
    int n = ::stat(filename.c_str(),&st);
    if(n < 0) return false;
    //2.s_mode
    return S_ISREG(st.st_mode);
  }
  //http请求的资源路径有效性判断
  static bool ValidPath(const string& path)
  {
      int level = 0 ;
      //1.字符串切分
      vector<string> array;
      Split(path,"/",&array);
      //2.计算深度
      for(auto& s : array)
      {
         if(s == "..")
         {
            level--;
            if(level < 0) return false;///任意一层走出相对根目录,就认为有问题
            continue;
         }
         level++;
      }
      return true;
  }

};

HttpRequest模块

这个模块存储的其实就是客户端发过来的HTTP请求解析后的要素,因此主要提供对这些要素的设置接口,同时还需要提供判断长短连接的接口,用于决定处理完一次数据是否继续处理还是关闭连接,还有一个接口就是Reset(),用于在处理一次完整的请求之后重置,否则可能会对下次请求造成干扰。

cpp 复制代码
class HttpRequest
{
public:
   string _method; //请求方法
   string _path; //资源路径
   string _version;//协议版本
   string _body;   //请求正文
   std::smatch _matches; //资源路径的正则提取
   unordered_map<string,string> _headers;//头部字段
   unordered_map<string,string> _params; //查询字符串
public:
   void SetHeader(string& key,string& val)   //插入头部字段
   {
      _headers[key] = val;
   }
   bool HasHeader(string key) const //是否存在指定头部字段
   {
      auto it = _headers.find(key);
      if(it == _headers.end()) return false;
      return true;
   }
   string GetHeader(string key)const //获取指定头部字段的值
   {
       auto it = _headers.find(key);
       if(it == _headers.end()) return "None";
       return it->second;
   }
   void SetParams(string& key,string&val)//插入查询字符串
   {
       _params[key] = val;
   }
   bool HasParam(string& key)  //判断是否存在指定的查询字符串
   {
      auto it = _params.find(key);
      if(it == _params.end()) return false;
      return true;
   }
   string GetParam(string&key) //获取指定的查询字符串
   {
      auto it = _params.find(key);
      if(it == _params.end()) return "None";
      return it->second;
   }
   size_t ContentLength() //获取正文长度
   {
       auto it = _headers.find("Content-Length");
       if(it == _headers.end()) return 0;
       return stol(it->second);
   }
   bool Close()const //判断是否是长连接
   {
      //没有Connection字段或有Connetion字段但是是Close的都是短连接
      if(HasHeader("Connection") && GetHeader("Connection") == "keep-alive")
        return true;
      return false;
   }

   HttpRequest()
   :_version("HTTP/1.1")
   {}

   void Reset()  //每次上下文请求被处理完需要重置一下,否则可能会对下次请求造成干扰
   {
        _method.clear();
        _path.clear();
        _version = "HTTP/1.1";
        _body.clear();
        std::smatch matches;
        _matches.swap(matches);
        _headers.clear();
        _params.clear();
   }
};

HttpResponse模块

响应信息要素:

  1. 响应状态码
  2. 头部字段
  3. 响应正文
  4. 重定向信息(是否进行了重定向标志,重定向的路径)

功能性接口:

  1. 头部字段的新增,查询,获取
  2. 为了便于成员的访问,因此将成员设置为公有成员
  3. 正文的设置
  4. 重定向的设置
  5. 长短连接的判断
cpp 复制代码
class HttpResponse
{
public:
   int _status; //状态码
   string _body; //正文
   bool _is_redirect; //表示是否重定向
   string _redir_url;
   unordered_map<string,string> _headers;//头部字段
public: 
    HttpResponse()
    :_status(200),_is_redirect(false)
    {}

    HttpResponse(int statu)
    :_status(statu),_is_redirect(false)
    {}

   void Reset() //重置上下文
   {
      _status = 200;
      _is_redirect = false;
      _redir_url.clear();
      _body.clear();
      _headers.clear();
   }

   void SetHeader(string key,string val) //设置正文
   {
       _headers[key] = val;
   }

   bool HasHeader(string key) //是否存在指定头部字段
   {
      auto it = _headers.find(key);
      if(it == _headers.end()) return false;
      return true;
   }
   string GetHeader(string key) //获取指定头部字段的值
   {
       auto it = _headers.find(key);
       if(it == _headers.end()) return "None";
       return it->second;
   }
   void SetContent(string body,string type) //设置正文的同时把Content-type也设置了
   {
         _body = body;
         SetHeader("Content-type",type);
   }

   bool Close()//判断是否是长链接
   {
      //没有Connection字段或有Connetion字段但是是Close的都是短连接
      if(HasHeader("Connection") && GetHeader("Connection") == "keep-alive")
        return true;
      return false;
   }

   bool SetRedircet(string& url,int statu = 302) //设置重定向
   {
      _is_redirect = true;
      _redir_url = url;
      _status = statu;
   }
};

HttpContext模块

该模块是个协议上下文模块,用于在收到数据之后解析数据,记录HTTP请求的接收和处理进度的。因为有可能在接收到的数据时,并不是一条完整的HTTP请求,因此需要多次收到数据后才能处理完成一条完整的HTTP请求,所以在每次处理的时候,需要将处理进度记录起来,以便于下次从当前进度继续向下处理。

管理的要素:

  1. 接收状态:
  • RECV_HTTP_LINE:表示当前处于接收并处理请求行的阶段。
  • RECV_HTTP_HEAD:表示当前处于接收并处理头部的阶段。
  • RECV_HTTP_BODY:表示当前处于接收正文的阶段。
  • RECV_HTTP_OVER:表示当前请求已经接收完成,可以对请求进行处理的阶段。
  • RECV_HTTP_ERROR:表示解析过程发生错误。
  1. 响应状态码:在请求的接收并处理中,有可能会出现各种不同的问题,比如解析出错、访问资源不对、无权限等问题。而这些错误的响应状态码是不一样的。

  2. HttpRequest _request:已经解析得到的请求信息

cpp 复制代码
bool RecvHttpLine(Buffer* buf)
   {
      if(_recv_statu != RECV_HTTP_LINE) return false;
      //1.从缓冲区中读取数据
      string line = buf->GetLineAndPop();
      //读取数据不足一行(GetLineAndPop会返回空串)也就是找不到\r\n结尾的数据
      if(line.size() == 0)
      {
         //1.1缓冲区数据不足一行且缓冲区中的可读数据很长
         if(buf->ReadAbleSize() > MAX_LINE)
         {
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 414; //URI TOO LONG
            return false;
         }
         //1.2不足一行但是没超过MAX_LINE : 等待新数据到来
         return true;
      }
      //1.3读取数据有一行,但是超过MAX_LINE
      if(line.size() > MAX_LINE)
      {
           _recv_statu = RECV_HTTP_ERROR;
           _resp_statu = 414; //URI TOO LONG
           DBG_LOG("414 error");
           return false;
      }
      //2.读取数据出一行且在正常范围,则进行解析
      bool ret = ParseHttpLine(line);
      if(ret == false) return false;
      //更新状态
      _recv_statu = RECV_HTTP_HEAD;
      // DBG_LOG("ParseLine Success");
      return true;
   }
   //注意&作为分隔符不会被编码
   bool ParseHttpLine(string& line)
   {
      //1.使用正则库解析请求行
      std::smatch matches;
      std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?",std::regex::icase);
      bool ret = std::regex_match(line,matches,e);
      if(ret == false)
      {
          _recv_statu = RECV_HTTP_ERROR;
          _resp_statu = 400; //BAD REQUEST
          DBG_LOG("BAD REQUEST");
          return false;
      }
      //0 : GET /bitejiuyeke/login?user=xiaoming&pass=123123HTTP/1.1
      //1:  GET
      //2:  /bitejiuyeke/login
      //3 : user=xiaoming&pass=123123
      //4:  HTTP/1.1

      //2.保存关键要素
      //2.1
      _request._method = matches[1];
      std::transform(_request._method.begin(),_request._method.end(),_request._method.begin(),::toupper);
      _request._path = Util::UrlDeCode(matches[2],false);
      _request._version = matches[4];
      // DBG_LOG("%s %s %s",_request._method.c_str(),_request._path.c_str(),_request._version.c_str());
      //2.2查询字符串要素 以&分割查询字符串然后设置到HttpRequest的_params中
      vector<string> query_array;
      Util::Split(matches[3],"&",&query_array);
      for(const auto& query : query_array)
      {
         //按照=进行分隔
         size_t  pos = query.find("=");
         if(pos == string::npos)
         {
             _recv_statu = RECV_HTTP_ERROR;
             _resp_statu = 400; //BAD REQUEST
             DBG_LOG("BAD REQUEST");
             return false;
         }
         //注意要进行解码
         string key = Util::UrlDeCode(query.substr(0,pos),true);
         string val = Util::UrlDeCode(query.substr(pos+1),true);
         _request.SetParams(key,val);
      }
      return true;
   }

   bool RecvHttpHead(Buffer* buf)
   {
         if(_recv_statu != RECV_HTTP_HEAD) return false;
         while(buf->ReadAbleSize() > 0) //有数据就读
         {
              //1.从缓冲区中读取数据
               string line = buf->GetLineAndPop();
               //读取数据不足一行(GetLineAndPop会返回空串)
               if(line.size() == 0)
               {
                  //1.1缓冲区数据不足一行且缓冲区中的可读数据很长
                  if(buf->ReadAbleSize() > MAX_LINE)
                  {
                     _recv_statu = RECV_HTTP_ERROR;
                     _resp_statu = 414; //URI TOO LONG
                     return false;
                  }
                  //1.2不足一行但是没超过MAX_LINE : 等待新数据到来
                  return true;
               }
               //1.3读取数据有一行,但是超过MAX_LINE
               if(line.size() > MAX_LINE)
               {
                  _recv_statu = RECV_HTTP_ERROR;
                  _resp_statu = 414; //URI TOO LONG
                  return false;
               }
               //2.正常读取到一行数据, 读取到空行结束
               if(line == "\n" || line == "\r\n")
                 break;
               //3.解析头部
               bool ret = ParseHttpHead(line);
               if(ret == false) return false;
         }
       _recv_statu = RECV_HTTP_BODY;
      // DBG_LOG("ParseHead Success");
       return true;
   }

   bool ParseHttpHead(string& line)
   {
         if(line.back() == '\n') line.pop_back();
         if(line.back() == '\r') line.pop_back();
         size_t pos = line.find(":");
         if(pos == string::npos)
         {
             _recv_statu = RECV_HTTP_ERROR;
             _resp_statu = 400; //BAD REQUEST
             DBG_LOG("BAD REQUEST");
             return false;
         }
         //注意要进行解码
         string key = line.substr(0,pos);
         string val = line.substr(pos+1);
         _request.SetHeader(key,val);
        return true;
   }

   bool RecvHttpBody(Buffer* buf)
   {
      if(_recv_statu != RECV_HTTP_BODY) return false;
       //1.获取正文长度
      size_t content_len = _request.ContentLength();
      //2.正文长度为0,则解析完毕
      if(content_len == 0)
      {
          _recv_statu = RECV_HTTP_OVER;
          return true;
      }
      //3.正文长度不为0 计算还需要接收的正文长度
      size_t need_len = content_len - _request._body.size();
      //4.接收正文到body中
      //4.1 缓冲区中数据包含当前请求所需正文
      if(buf->ReadAbleSize() >= need_len)
      {
         _request._body.append(buf->ReadPos(),need_len);
         buf->MoveReadIdx(need_len);
         _recv_statu = RECV_HTTP_OVER;
         return true;
      }
      //4.2 缓冲区数据不满足要求,等待新数据(bug:这里是读取ReadAbleSize)
      _request._body.append(buf->ReadPos(),buf->ReadAbleSize());
      buf->MoveReadIdx(buf->ReadAbleSize());
      return true;
   }

public:
   HttpContext()
   :_resp_statu(200),_recv_statu(RECV_HTTP_LINE)
   {}
   int RespStatu() //获取响应状态码
   {
      return _resp_statu;
   }
   HttpRecvStatus RecvStatu() //获取当前接收及解析状态
   {
      return _recv_statu;
   }
   HttpRequest& Request() //获取解析好的请求
   {
      return _request;
   }
   void RecvHttpRequest(Buffer* buf) //接收并解析HTTP请求
   {
      //这里不要break 当接收处理完请求行应该立即接收头部
      switch(_recv_statu)
      {
         case RECV_HTTP_LINE: RecvHttpLine(buf);
         case RECV_HTTP_HEAD: RecvHttpHead(buf);
         case RECV_HTTP_BODY: RecvHttpBody(buf);
      }
      return;
   }
};

HttpContext模块中的关键函数是RecvHttpRequest(),未来收到数据之后就会调用该函数提取出请求的要素,由于每次收到的数据不一定是一个完整的请求,因此这个函数是需要调用的。同时为了提高效率,假如一次获取上来的数据包含了请求行和头部,处理完请求行之后应该继续处理头部,而不是退出等待数据,因此我们在每个case之后不设置break。

HttpServer模块

HttpSever模块是一个整合模块,它需要在我们封装的TcpServer基础上,修改收到数据之后调用MessageCallback的逻辑,主要流程是:

  1. 从socket接收数据,放到接收缓冲区
  2. 调用OnMessage回调函数进行业务处理
  3. 对请求进行解析,得到了一个HttpRequest结构,包含了所有的请求要素。
  4. 进行请求的路由查找 --- 找到对应请求的处理方法
  5. 对静态资源请求/功能性请求进行处理完毕后,得到了一个填充响应信息的HttpResponse对象,组织http格式响应,进行发送

而这其中请求又分为两种:

一种是静态资源请求(比如是一些实体文件资源html、image等),需要将这些请求资源读取出来然后填充到HttpResponse结构中,需要注意的是,我们需要判断请求资源的合理性(路径是否有效以及是否存在该资源)

另一种是功能性请求 ,我们的实现思想是设计一张路由表,表中记录了针对哪个请求,应该使用哪个函数进行业务处理的映射关系 。不同请求方法各自对应一张路由表,当服务器收到一个请求,就去对应路由表查找,有则执行对应的处理函数,也就是说由组件使用者设定收到的请求怎么处理,让他们去注册 ;如果没有对应处理哈数则返回404,表示请求资源不存在。这样做的好处是用户只需要实现业务处理函数,然后将请求与处理函数的映射关系添加到服务器中,而服务器只需要接收数据,解析数据,查找路由表映射关系,执行业务处理函数即可。

cpp 复制代码
class HttpServer
{
private:
   using Handler = std::function<void(const HttpRequest&,HttpResponse*)>;
   using Handlers = std::vector<pair<std::regex,Handler>>;
   //由于每一次请求我们都需要将正则表达式进行编译来构造正则表达式对象,这样是比较慢的,我们可以直接记录映射正则和处理方法的映射
   //将key修改为regex对象,即正则表达式对象
   //路由表
   Handlers _get_route;
   Handlers _post_route;
   Handlers _put_route;
   Handlers _delete_route;
   string _basedir; //静态资源根目录
   TcpServer _server;
private:
    void WriteResponse(const PtrConnection& conn,const HttpRequest& req, HttpResponse& rsp) //将HttpRespnse中的要素按照http协议格式进行组织发送
    {
       //1.完善rsp中的头部字段
       //1.1短/长链接
       if(req.Close()) rsp.SetHeader("Connection","keep-alive");
       else rsp.SetHeader("Connection","close");
       //1.2Content-Length:看正文是不是不为空&&没被设置
       if(!rsp._body.empty() && !rsp.HasHeader("Content-Length"))
            rsp.SetHeader("Content-Length",to_string(rsp._body.size()));
        //1.3Content-Type: 看正文是不是不为空 && Content-Type没被设置(默认设置为application/octet-stream)
       if(!rsp._body.empty() && !rsp.HasHeader("Content-Type"))
            rsp.SetHeader("Content-Type","application/octet-stream");
       //1.4 Location:是否允许重定向&& location没被设置
       if(rsp._is_redirect)
            rsp.SetHeader("Location",rsp._redir_url);
       //2. 将rsp中的要素按照HTTP协议格式组织起来
       stringstream ss;
       ss << req._version << " " <<  to_string(rsp._status) << " " << Util::StatuDesc(rsp._status) << "\r\n";
       for(auto& pr : rsp._headers )
           ss << pr.first << ":" << pr.second << "\r\n";
       ss << "\r\n";
       ss << rsp._body;  
       //3.发送响应
       conn->Send(ss.str().c_str(),ss.str().size());
    }

    bool isFileHandler(const HttpRequest& req)//判断是否是静态资源请求
    {
       //1.必须设置了静态资源目录
       if(_basedir.empty())
       {
          DBG_LOG("basedir empty");
          return false;
       }
       //2.请求方法必须是GET或HEAD(注意这里判断条件是req._method != "GET" && req._method != "HEAD")
      //  if(req._method != "GET" || req._method != "HEAD")
      //  {
      //     DBG_LOG("method Error,%s",req._method.c_str());
      //     return false;
      //  }
       if(req._method != "GET" && req._method != "HEAD")
       {
          DBG_LOG("method Error,%s",req._method.c_str());
          return false;
       }
       //3.请求的资源路径必须是一个合法路径
       if(!Util::ValidPath(req._path))
       {
           DBG_LOG("Not a ValidPath");
           return false;
       }
       //4.请求的资源必须存在且是一个普通文件(注意/的情况在后面追加上index.html)
       string req_path = _basedir + req._path; //为了避免直接修改请求的资源路径,因此定义一个临时对象象
      //  DBG_LOG("%s",req_path.c_str());
       if(req_path.back() == '/')
         req_path += "index.html";
       if(!Util::isRegular(req_path)) return false;
       return true; 
    }

    bool FileHandler(const HttpRequest& req,HttpResponse* rsp )//静态资源的处理
    {
          string req_path = _basedir + req._path;
         //  DBG_LOG("%s",req_path.c_str());
          if(req_path.back() == '/')
            req_path += "index.html";
          //1.读取文件数据到body中
          bool ret = Util::ReadFile(req_path,&rsp->_body);
          if(ret == false) return false;
          //2.设置mime
          string mime = Util::ExtMime(req_path);
          rsp->SetHeader("Content-Type",mime);
          return true;
    } 

    void Dispatcher( HttpRequest& req,HttpResponse* rsp,Handlers& handlers)//功能性请求的分类处理
    {
        for(auto& handler : handlers)
        {
           regex& re = handler.first;
           Handler& func = handler.second;
           // regex_macth(原始字符串,提取数据存放容器,正则表达式匹配原则)
           bool ret = std::regex_match(req._path,req._matches,re); //注意这里修改了req._matches不能使用const对象
           if(ret == false) return;
           return func(req,rsp);//传入请求信息和空的rsp
        }
        //没有就报错
        rsp->_status = 404;
    }

    void Route(HttpRequest& req,HttpResponse* rsp) //路由 
    {
         //1.是静态资源请求则进行静态资源处理
         if(isFileHandler(req))
         {
            FileHandler(req,rsp);
            return;
         }   
         // DBG_LOG("功能性请求...");
         // DBG_LOG("%s",req._method.c_str());
         // DBG_LOG("%s",req._path.c_str());
         //2.功能性请求:看是否能在路由表找到
         if(req._method == "GET" || req._method == "HEAD")
             return Dispatcher(req,rsp,_get_route);
         else if(req._method == "POST")   
              return Dispatcher(req,rsp,_post_route); 
         else if(req._method == "PUT")
              return Dispatcher(req,rsp,_put_route); 
          else if(req._method == "DELETE")
              return Dispatcher(req,rsp,_delete_route);
         //3.不是静态或是功能性但是没有找到对应路由,报错405
          rsp->_status = 405; //Method Not Allowed
          DBG_LOG("405 ERROR");
    }

    void ErrorHandler(const HttpRequest& req,HttpResponse* rsp)//解析出错调用
    {
      //1.组织错误回复
      string body;
      body += "<!DOCTYPE html>";
      body += "<html kang='en'>";
      body += "<head>";
      body += "<meta charset='UTF-8'>";
      body += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>";
      body += "<title>Error</title>";
      body += "</head>";
      body += "<body>";
      body += "<h1>" + to_string(rsp->_status) + Util::StatuDesc(rsp->_status) + "</h1>";
      body += "</body>";
      body += "</html>";
      //2.设置到rsp的body中
      rsp->SetContent(body,"text/html");
    }
    void OnConnection(const PtrConnection& conn) //为连接设置上下文
    {
       conn->SetContext(HttpContext());
       DBG_LOG("NEW CONNECTION:%p",conn.get());
    }

    void OnMessage(const PtrConnection& conn,Buffer* buffer) //缓冲区数据解析+处理
    {
         string content = buffer->ReadAsString(buffer->ReadAbleSize());
         // DBG_LOG("%s",content.c_str());
         while(buffer->ReadAbleSize() > 0)
         {
            //1.获取连接对应的上下文
            HttpContext* context = conn->GetContext()->get<HttpContext>();
            //2.通过上下文对缓冲区数据进行解析,得到HttpRequest对象
            context->RecvHttpRequest(buffer);
            //2.1缓冲区数据解析出错,直接回复出错响应,关闭连接
            HttpRequest& req = context->Request();
            HttpResponse rsp(context->RespStatu());
            if(context->RespStatu()>=400) //进行错误响应关闭连接
            {
                  // DBG_LOG("%s",buffer->ReadAsString(buffer->ReadAbleSize()).c_str());
                  DBG_LOG("parse error");
                  ErrorHandler(req,&rsp);//填充一个错误显示页面数据到rsp中
                  WriteResponse(conn,req,rsp); //组织响应发送
                  //bug:
                  context->Reset();
                  buffer->MoveReadIdx(buffer->ReadAbleSize());
                  conn->ShutDown();//关闭连接
                  return;
            }
            //2.2解析正常:可能请求没有获取完整/请求已经获取完毕进行处理
            if(context->RecvStatu() != RECV_HTTP_OVER)
               return;
            //3.请求路由+业务处理
            Route(req,&rsp);
            //4.对HttpRespnse进行组织发送
            WriteResponse(conn,req,rsp);
            // DBG_LOG("缓冲区数据:%d",buffer->ReadAbleSize());
            //5.请求处理完成,重置连接的上下文,防止影响到下一请求
            context->Reset();
            //6.判断长短连接:长连接则缓冲区有数据就循环处理,短连接则断开连接
            if(rsp.Close() == false)
            {
                DBG_LOG("关闭短链接");
               conn->ShutDown();
            }  
         }
    }

 public:  
    HttpServer(int port,int timeout = DEFAULT_TIMEOUT)
    :_server(port)
    {
        _server.EnableInactiveRelease(timeout);
        _server.SetConnectedCallBack(std::bind(&HttpServer::OnConnection,this,std::placeholders::_1));
        _server.SetMessageCallBack(std::bind(&HttpServer::OnMessage,this,std::placeholders::_1,std::placeholders::_2));
    }
    void SetBaseDir(const string& dir) //设置静态资源根目录
    {
        bool ret = Util::isDirectory(dir);
        assert(ret == true);
        _basedir = dir; 
    }
    void Get(const string& pattern,const Handler& handler)
    {
       _get_route.push_back(std::make_pair(std::regex(pattern),handler));
    }
    void Post(const string& pattern,const Handler& handler)
    {
        _post_route.push_back(std::make_pair(std::regex(pattern),handler));
    }
    void Put(const string& pattern,const Handler& handler)
    {
        _put_route.push_back(std::make_pair(std::regex(pattern),handler));
    }
    void Delete(const string& pattern,const Handler& handler)
    {
        _delete_route.push_back(std::make_pair(std::regex(pattern),handler));
    }
    void SetThreadCount(int count) //设置服务器中线程池中数量
    {
        _server.SetThreadCount(count);
    }
    void EnableInactiveRelease(int timeout) //启动非活跃超时连接销毁
    {
        _server.EnableInactiveRelease(timeout);
    }
    void Listen() //启动服务器
    {
       _server.Start();
    }
};
相关推荐
GetcharZp2 小时前
玩转 Linux 机器视觉:手把手带你搞定 Ubuntu 下海康工业相机 C++ SDK
后端
星星在线5 小时前
MusicFree:一个「All in One」的个人音乐服务器,让听歌回归简单
前端·后端
_wyt0016 小时前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp
IT_陈寒6 小时前
Redis的SETNX并发问题让我加了三天班
前端·人工智能·后端
demo007x6 小时前
Docling 文档转换以及技术架构分析
前端·后端·程序员
袋鱼不重8 小时前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
大树888 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
用户8356290780518 小时前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还8 小时前
啥? 前端也要会干Java?🛵🛵🛵
后端
摇滚侠8 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql