• 20.添加HTTP模块


    添加一个简单的静态HTTP。

    这里默认读者是熟悉http协议的。

    来看看http请求Request的例子

    客户端发送一个HTTP请求到服务器的请求消息,其包括:请求行、请求头部、空行、请求数据。

    HTTP之响应消息Response 

    服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息,其包括:状态行、消息报头、空行和响应正文。

     前面所说的就是http的请求和响应答复。那我们可以封装出两个类。

    HttpRequest:http请求类封装

    HttpResponse:http响应类封装

    注意:这里会使用到我们之前写的Buffer类。因为服务器是把读到的数据存储在Buffer中的,所以大家要熟悉Buffer类的一些用法

    1、HttpRequest 类

    该类的主要作用是客户端发送请求,服务端收到的数据存放于Buffer中,之后解析成HttpRequest请求对象,调用成员函数设置请求头、请求体等。

    首先会有请求方式method_,http版本version_,请求头headers_(用map管理)。请求的路径path_(即是url),还有请求体query_

    请求体有可能是在url中的"?"后面,也可能是在请求头后面的。

    1. class HttpRequest
    2. {
    3. public:
    4. enum class Method{
    5. kInvalid, kGet, kPost, kHead, kPut, kDelete
    6. };
    7. enum class Version{
    8. kUnknown, kHttp10, kHttp11
    9. };
    10. HttpRequest()
    11. :method_(Method::kInvalid),
    12. version_(Version::kUnknown)
    13. {
    14. }
    15. void setVersion(Version v) { version_ = v; }
    16. Version getVersion()const { return version_; }
    17. bool setMethod(const char* start, const char* end)
    18. {
    19. string m(start, end);
    20. if (m == "GET") {
    21. method_ = Method::kGet;
    22. }
    23. else if (m == "POST") {
    24. method_ = Method::kPost;
    25. }
    26. //省略"HEAD","DELETE"等等方式......
    27. return method_ != Method::kInvalid;
    28. }
    29. Method getMothod()const { return method_; }
    30. const char* methodString()const {
    31. const char* result = "UNKNOWN";
    32. switch (method_) {
    33. case Method::kGet:
    34. result = "GET";
    35. break;
    36. case Method::kPost:
    37. result = "POST";
    38. break;
    39. //省略"HEAD","DELETE"等等方式......
    40. }
    41. return result;
    42. }
    43. void setPath(const char* start, const char* end) {
    44. path_.assign(start, end);
    45. }
    46. const string& path()const { return path_; }
    47. void setQuery(const char* start, const char* end) {
    48. query_.assign(start, end);
    49. }
    50. const string& query()const { return query_; }
    51. void addHeader(const char* start, const char* colon, const char* end)
    52. {
    53. //isspace(int c)函数判断字符c是否为空白符
    54. //说明:当c为空白符时,返回非零值,否则返回零。(空白符指空格、水平制表、垂直制表、换页、回车和换行符。
    55. // 要求冒号前无空格
    56. string field(start, colon);
    57. ++colon;
    58. while (colon < end && isspace(*colon))// 过滤冒号后的空格
    59. ++colon;
    60. string value(colon, end);
    61. while (!value.empty() && isspace(value[value.size() - 1]))//过滤value中的空格
    62. value.resize(value.size() - 1);
    63. headers_[field] = value;
    64. }
    65. string getHeader(const string& field)const
    66. {
    67. string result;
    68. auto it = headers_.find(field);
    69. if (it != headers_.end()) {
    70. return it->second;
    71. }
    72. return result;
    73. }
    74. const std::unordered_map& headers()const { return headers_; }
    75. private:
    76. Method method_;
    77. Version version_;
    78. string path_; //请求路径
    79. string query_; //请求体
    80. std::unordered_map headers_;
    81. };

    注意:添加请求头时,函数addHeader需要删除键值对的字符串左侧和右侧的空字符,保证解析正常。因为解析请求头时,对一行字符串用冒号“:”进行分割解析。

    2、HttpResponse 类

    服务器端得到的客户的请求信息后,再创建一个HttpResponse响应对象,也是会调用成员函数设置响应头部、响应体,并格式化到Buffer中,回复给客户端。

    按照上面的响应例子,那应该有响应头headers_,响应的状态码statusCode_,状态码的文字描述statusMessage_,响应体body_等等。

    成员函数就是一些设置状态码,设置响应头等操作。

    1. class HttpResponse
    2. {
    3. public:
    4. enum class HttpStatusCode
    5. {
    6. kUnknown,
    7. k200Ok = 200,
    8. k301MovedPermanently = 301,
    9. k400BadRequest = 400,
    10. k404NotFound = 404,
    11. };
    12. explicit HttpResponse(bool close)
    13. :statusCode_(HttpStatusCode::kUnknown),
    14. closeConnection_(close)
    15. {
    16. }
    17. void setStatusCode(HttpStatusCode code) { statusCode_ = code; }
    18. void setstatusMessage(const string& message) { statusMessage_ = message; }
    19. void setCloseConnection(bool on) { closeConnection_ = on; }
    20. bool closeConnection()const { return closeConnection_; }
    21. void setContentType(const string& contentType) { addHeader("Content-Type", contentType); }
    22. void addHeader(const string& key, const string& value) {
    23. headers_[key] = value;
    24. }
    25. void setBody(const string& body) { body_ = body; }
    26. void appendToBuffer(Buffer* output)const;
    27. private:
    28. std::unordered_map headers_;
    29. HttpStatusCode statusCode_; //状态码
    30. string statusMessage_; //响应行中的状态码文字描述
    31. bool closeConnection_; //是否关闭连接
    32. string body_; //响应体
    33. };

    这里特别值得一说的是如何把响应消息格式化的操作格式化appendToBuffer(Buffer* output)

    该函数默认使用HTTP1.1版本,按照HTTP协议对HttpResponse对象进行格式化输出到Buffer中。

    按照要求添加响应行,响应头,空行,响应体。

    1. void HttpResponse::appendToBuffer(Buffer* output) const
    2. {
    3. //响应行
    4. string buf = "HTTP/1.1 " + std::to_string(static_cast<int>(statusCode_));
    5. output->append(buf);
    6. output->append(statusMessage_);
    7. output->append("\r\n");
    8. //响应头部
    9. if (closeConnection_) {
    10. output->append("Connection: close\r\n");
    11. }
    12. else {
    13. output->append("Connection: Keep-Alive\r\n");
    14. buf = "Content-Length:" + std::to_string(body_.size()) + "\r\n";
    15. output->append(buf);
    16. }
    17. for (const auto& header : headers_) {
    18. buf = header.first + ": " + header.second + "\r\n";
    19. output->append(buf);
    20. }
    21. output->append("\r\n"); //空行
    22. output->append(body_); //响应体
    23. }

    3、HttpContext 类

    服务端接收客户请求,存在Buffer中,那怎么从Buffer中解析得到我们想要的信息呢这时,需要一个解析类HttpContext,解析后数据封装到回复HttpRequest中。

    其成员有处理的状态state_,响应request_。

    1. class HttpContext
    2. {
    3. public:
    4. enum class HttpRequestPaseState{
    5. kExpectRequestLine, //请求行
    6. kExpectHeaders, // 请求头
    7. kExpectBody, // 请求体
    8. kGotAll, //表示都处理完全
    9. };
    10. HttpContext()
    11. :state_(HttpRequestPaseState::kExpectRequestLine)//默认从请求行开始解析
    12. {
    13. }
    14. bool parseRequest(Buffer* buf);// 解析请求Buffer
    15. bool gotAll()const { return state_ == HttpRequestPaseState::kGotAll; }
    16. void reset()// 为了复用HttpContext
    17. {
    18. state_ = HttpRequestPaseState::kExpectRequestLine;
    19. HttpRequest dumy;
    20. request_.swap(dumy);
    21. }
    22. const HttpRequest& request() const{ return request_; }
    23. HttpRequest& request(){ return request_; }
    24. private:
    25. bool processRequestLine(const char* begin, const char* end);
    26. HttpRequestPaseState state_; //需要处理的状态,状态机
    27. HttpRequest request_;
    28. };

    一个正常的请求,一般至少是有请求行的,默认解析状态为kExpectRequestLine。

    这里就主要关注是如何解析Buffer的。

    3.1、请求解析 parseRequest(Buffer* buf)

    这里为了方便找到buf中的"\r\n",添加了Buffer::findCRLF()函数。

    1. const char Buffer::kCRLF[] = "\r\n";
    2. //为了方便解析http "\r\n"位置
    3. const char* findCRLF()const {
    4. const char* crlf = std::search(peek(), beginWirte(), kCRLF, kCRLF + 2);
    5. return crlf == beginWirte() ? nullptr : crlf;
    6. }

    传入需要解析的Buffer对象,根据期望解析的部分(即是状态state_)进行处理。

    处理就三种情况:请求行,请求头,请求体。具体的流程可以看代码

    1. bool HttpContext::parseRequest(Buffer* buf)
    2. {
    3. bool ok = true;
    4. bool hasMore = true;
    5. while (hasMore) {
    6. if (state_ == HttpRequestPaseState::kExpectRequestLine) { //解析请求行
    7. //查找出buf中第一次出现"\r\n"位置
    8. const char* crlf = buf->findCRLF();
    9. if (crlf) {
    10. //若是找到"\r\n",说明至少有一行数据,可以进行解析
    11. //buf->peek()为数据开始部分
    12. ok = processRequestLine(buf->peek(), crlf);
    13. if (ok) {//解析成功
    14. buf->retrieveUntil(crlf + 2);//buf->peek()向后移动2字节,到下一行
    15. state_ = HttpRequestPaseState::kExpectHeaders;
    16. }
    17. else {
    18. hasMore = false;
    19. }
    20. }
    21. else {
    22. hasMore = false;
    23. }
    24. }
    25. else if (state_ == HttpRequestPaseState::kExpectHeaders) {
    26. const char* crlf = buf->findCRLF(); //找到"\r\n"位置
    27. if (crlf) {
    28. const char* colon = std::find(buf->peek(), crlf, ':');//定位分隔符
    29. if (colon != crlf) {
    30. request_.addHeader(buf->peek(), colon, crlf); //添加键值对
    31. }
    32. else {
    33. /*state_ = HttpRequestPaseState::kGotAll;
    34. hasMore = false;*/
    35. state_ = HttpRequestPaseState::kExpectBody;//这样就可以解析body
    36. }
    37. buf->retrieveUntil(crlf + 2); //后移动2字节
    38. }
    39. else {
    40. hasMore = false;
    41. }
    42. }
    43. else if (state_ == HttpRequestPaseState::kExpectBody) {//解析请求体
    44. if (buf->readableBytes()) {//表明还有数据,那就是请求体
    45. request_.setQuery(buf->peek(), buf->beginWirte());
    46. }
    47. state_ = HttpRequestPaseState::kGotAll;
    48. hasMore = false;
    49. }
    50. }
    51. return ok;
    52. }

    3.1、请求行的解析 processRequestLine()

    请求行有固定格式Method URL Version \r\n,URL中可能带有请求参数。根据空格符进行分割成三段字符。URL可能带有请求参数,使用"?”分割解析。

    1. bool HttpContext::processRequestLine(const char* begin, const char* end)
    2. {
    3. bool succeed = true;
    4. const char* start = begin;
    5. const char* space = std::find(start, end, ' ');
    6. //第一个空格前的字符串是请求方法 例如:post
    7. if (space != end && request_.setMethod(start, space)) {
    8. start = space + 1;
    9. space = std::find(start, end, ' ');//寻找第二个空格 url
    10. if (space != end) {
    11. const char* question = std::find(start, space, '?');
    12. if (question != space) {// 有"?",分割成path和请求参数
    13. request_.setPath(start, question);
    14. request_.setQuery(question, space);
    15. }
    16. else {
    17. request_.setPath(start, space);//只有path
    18. }
    19. //最后一部分,解析http协议版本
    20. string version(space + 1, end);
    21. if (version == "HTTP/1.0")
    22. request_.setVersion(HttpRequest::Version::kHttp10);
    23. else if (version == "HTTP/1.1")
    24. request_.setVersion(HttpRequest::Version::kHttp11);
    25. else
    26. succeed = false;
    27. }
    28. }
    29. return succeed;
    30. }

    这样解析就完成了。

    4、HttpServer类

    为了可以方便使用,封装个HttpServer类。

    该类内部会有Server类型成员,并提供了一个回调函数的接口,当服务器收到http请求时,调用客户端的处理函数进行处理。

    HttpServer支持多线程,也可以使用单线程。

    1. class HttpServer
    2. {
    3. public:
    4. using HttpCallback = std::function<void(const HttpRequest&, HttpResponse*)>;
    5. HttpServer(EventLoop* loop, const InetAddr& listenAddr);
    6. void setHttpCallback(const HttpCallback& cb) { httpCallback_ = cb; }
    7. void start(int numThreads);
    8. private:
    9. void onConnetion(const ConnectionPtr& conn); //连接到来的回调函数
    10. void onMessage(const ConnectionPtr& conn, Buffer* buf); //消息到来的回调函数
    11. void onRequest(const ConnectionPtr& conn, const HttpRequest&);
    12. Server server_;
    13. HttpCallback httpCallback_;
    14. };

    函数setHttpCallback就是设置用户的业务处理回调函数的。

    4.1HttpServer构造函数

    1. //默认的回调函数
    2. void defaultHttpCallback(const HttpRequest& req, HttpResponse* resp)
    3. {
    4. resp->setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);
    5. resp->setstatusMessage("Not Found");
    6. resp->setCloseConnection(true);
    7. }
    8. //构造函数
    9. HttpServer::HttpServer(EventLoop* loop, const InetAddr& listenAddr)
    10. :server_(listenAddr,loop)
    11. , httpCallback_(defaultHttpCallback)
    12. {
    13. //新连接到来回调该函数
    14. server_.setConnectionCallback([this](const ConnectionPtr& conn) {onConnetion(conn); });
    15. //消息到来回调该函数
    16. server_.setMessageCallback([this](const ConnectionPtr& conn, Buffer* buf) {onMessage(conn, buf); });
    17. }

    这里就是初始化Server,并将HttpServer的回调函数传给Server。主要有两个函数。

    前面的HttpResponse类和HttpRequest类已经在HttpServer使用了,但是解析类HttpContext还没有使用。

    很容易想到是在回调函数中使用。在有消息到来的时候,就会进行解析数据,这时就会使用到HttpContext。可以在每次调用函数onMessage中创建HttpContext对象。这在短连接中使用是合适的。但是在长连接的情况下,这样可能效率不高

    那么就可以在有新连接到来的时刻,就设置好HttpContext。

    那就说到onConnetion函数

    4.2 连接到来的回调函数onConnetion

    1. //这里绑定一个HttpContext主要是为了长连接中仅分配一次对象,提高效率。
    2. void HttpServer::onConnetion(const ConnectionPtr& conn)
    3. {
    4. if (conn->connected()) {
    5. //conn->setContext(std::make_shared()); //c++11的std::shared_ptr
    6. conn->setContext(HttpContext()); //c++17的std::any
    7. }
    8. }

    该函数为一个新的Connection绑定一个HttpContext对象,绑定之后,HttpContext就相当于Connection的成员,TcpConection在MessageCallback中就可以随意的使用该HttpContext对象了。
    这里绑定一个HttpContext主要是为了长连接中仅分配一次对象,提高效率

    这里绑定使用的是c++17的std::any。std::any表示可以接受任意类型的变量。

    来看看Conntection类中需要添加的变量

    1. #include
    2. class Connection:public std::enable_shared_from_this
    3. {
    4. public:
    5. //省略之前的变量和函数
    6. //void setContext(std::shared_ptr context) { context_ = context; }
    7. //std::shared_ptr getConntext()const { return context_; }
    8. void setContext(const std::any& context) { context_ = context; }
    9. std::any* getMutableContext() { return &context_; }
    10. private:
    11. //std::shared_ptr context_; //c++11做法
    12. std::any context_; //用来解析http或者websocket或者其他协议的
    13. };

    首先我们要明确为什么要的是接收任意类型的变量这总做法,为什么不是直接就是用HttpContext类替代std::any。

    因为我们后续可能还需要解析其他协议的,例如websockte协议(下一节会讲解)。要是直接写HttpContext的话,那要解析websocket协议的时候,Connection类中还需要添加websocketContext类成员变量,这就很麻烦的。所以用std::any来就可以绑定所有的解析类。

    那又有疑惑,为什么不直接用void*?简单点说是,它类型不安全,还需要用户手动去delete。

    std::shared_ptr和void*一样不能解决类型安全的问题。详细的了解可以查看该文章https://www.cnblogs.com/gnivor/p/12793239.html

    那说完std::any和回调函数onConnetion,那就到函数onMessage。

    4.3 新消息到来的回调函数onMessage

    1. void HttpServer::onMessage(const ConnectionPtr& conn, Buffer* buf)
    2. {
    3. //HttpContext* context = reinterpret_cast(conn->getConntext().get()); //c++11做法
    4. auto context = std::any_cast(conn->getMutableContext()); //c++117
    5. if (!context) {
    6. LOG_ERROR<<"context is bad\n";
    7. return;
    8. }
    9. if (!context->parseRequest(buf)) {
    10. conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");
    11. conn->shutdown();
    12. }
    13. if (context->gotAll()) {
    14. onRequest(conn, context->request());
    15. context->reset();//一旦请求处理完毕,重置context,因为HttpContext和Connection绑定了,我们需要解绑重复使用
    16. }
    17. }

    当Connection中所拥有的连接有新消息到来时,会调用它的messageCallback_函数,其实就是调用HttpServer的onMessage()函数。而之前在函数onConnection()中把HttpContext利用std::any绑定给了Connection,那在该函数中就可以对Connection使用HttpContext类来解析数据包了。

    onMessage()函数首先调用HttpContext的parserRequset()函数解析请求,判断请求是否合法,进而选择关闭连接,或者处理请求(函数onRequest)。

    4.4处理请求的函数onRequest

    1. void HttpServer::onRequest(const ConnectionPtr& conn, const HttpRequest& req)
    2. {
    3. const std::string& connetion = req.getHeader("Connection");
    4. bool close = connetion == "close" || (req.getVersion() == HttpRequest::Version::kHttp10 && connetion != "Keep-Alive");
    5. HttpResponse response(close);
    6. //执行用户注册的回调函数
    7. httpCallback_(req, &response);
    8. Buffer buf;
    9. response.appendToBuffer(&buf);
    10. conn->send(&buf);//发送数据
    11. if (response.closeConnection()) {
    12. conn->shutdown();
    13. }
    14. }

    先判断是长连接还是短连接。接着使用close构造一个HttpResponse对象。之后很重要的是执行用户注册的回调函数,这个就是用户的业务函数。

    5.HtttpServer的用法

    1. #include"src/Server.h"
    2. //省略一些其他头文件
    3. //用户的业务处理的函数
    4. void onRequest(const HttpRequest& req, HttpResponse* resp)
    5. {
    6. if (req.path() == "/") {// 根目录请求
    7. resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);
    8. resp->setstatusMessage("OK");
    9. resp->setContentType("text/html");
    10. resp->addHeader("Server", "li");
    11. resp->setBody("This is title"
    12. "

      Hello

      Now is hello"
    13. "");
    14. }
    15. else if (req.path() == "/hello") {
    16. resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);
    17. resp->setstatusMessage("OK");
    18. resp->setContentType("text/plain");
    19. resp->setBody("hello, world!\n");
    20. }
    21. else {
    22. resp->setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);
    23. resp->setstatusMessage("Not Found");
    24. resp->setCloseConnection(true);
    25. }
    26. }
    27. int main(int argc, char* argv[])
    28. {
    29. EventLoop loop;
    30. HttpServer server(&loop, InetAddr(9999));
    31. server.setHttpCallback(onRequest); //比普通的server添加了这行
    32. server.start(0); //副io线程数量为0,单线程运行
    33. loop.loop();
    34. return 0;
    35. }

    主要就是用户自写的一个业务处理函数,之后调用HttpServer类的函数setHttpCallback来进行注册即可。

    这里例子是创建了端口是9999的HTTPServer,提供访问的是/,/hello。

    在浏览器输入 http://localhost:9999或者http://localhost:9999/hello即可访问成功。(localhost可以改成是自己linux的ip)

    HTTP调用的流程图

    HTTP服务器基本就是结束了,这里的是简单静态web服务器,我们没有解析客户发送过来的body。需要其他功能可以在这基础上进行完善或添加,比如支持fcgi。

    完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v20

  • 相关阅读:
    WebSocket的优缺点
    你绝对不知道的接口加密解决方案:Python的各种加密实现
    C语言暑假学习刷题——Day9
    黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第二章 网络工程基础(3)SSH与SSH隧道
    C++——继承
    JAVA接入OPC DA2.0详细流程
    阿里云ECS服务器vCPU是什么意思?
    Java后端社招3年
    谱定理等周边定理
    论文阅读 - Learning Human Interactions with the Influence Model
  • 原文地址:https://blog.csdn.net/m0_57408211/article/details/128385213