asio实现http服务器

简介

前文介绍了asio如何实现并发的长连接tcp服务器,今天介绍如何实现http服务器,在介绍实现http服务器之前,需要讲述下http报文头的格式,其实http报文头的格式就是为了避免我们之前提到的粘包现象,告诉服务器一个数据包的开始和结尾,并在包头里标识请求的类型如get或post等信息。

HTTP包头信息

一个标准的HTTP报文头通常由请求头和响应头两部分组成。

HTTP 请求头

HTTP请求头包括以下字段:

  • Request-line:包含用于描述请求类型、要访问的资源以及所使用的HTTP版本的信息。
  • Host:指定被请求资源的主机名或IP地址和端口号。
  • Accept:指定客户端能够接收的媒体类型列表,用逗号分隔,例如 text/plain, text/html。
  • User-Agent:客户端使用的浏览器类型和版本号,供服务器统计用户代理信息。
  • Cookie:如果请求中包含cookie信息,则通过这个字段将cookie信息发送给Web服务器。
  • Connection:表示是否需要持久连接(keep-alive)。

比如下面就是一个实际应用

  1. GET /index.html HTTP/1.1
  2. Host: www.example.com
  3. Accept: text/html, application/xhtml+xml, */*
  4. User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
  5. Cookie: sessionid=abcdefg1234567
  6. Connection: keep-alive

上述请求头包括了以下字段:

  • Request-line:指定使用GET方法请求/index.html资源,并使用HTTP/1.1协议版本。
  • Host:指定被请求资源所在主机名或IP地址和端口号。
  • Accept:客户端期望接收的媒体类型列表,本例中指定了text/html、application/xhtml+xml和任意类型的文件(/)。
  • User-Agent:客户端浏览器类型和版本号。
  • Cookie:客户端发送给服务器的cookie信息。
  • Connection:客户端请求后是否需要保持长连接。

HTTP 响应头

HTTP响应头包括以下字段:

  • Status-line:包含协议版本、状态码和状态消息。
  • Content-Type:响应体的MIME类型。
  • Content-Length:响应体的字节数。
  • Set-Cookie:服务器向客户端发送cookie信息时使用该字段。
  • Server:服务器类型和版本号。
  • Connection:表示是否需要保持长连接(keep-alive)。

在实际的HTTP报文头中,还可以包含其他可选字段。
如下是一个http响应头的示例

  1. HTTP/1.1 200 OK
  2. Content-Type: text/html; charset=UTF-8
  3. Content-Length: 1024
  4. Set-Cookie: sessionid=abcdefg1234567; HttpOnly; Path=/
  5. Server: Apache/2.2.32 (Unix) mod_ssl/2.2.32 OpenSSL/1.0.1e-fips mod_bwlimited/1.4
  6. Connection: keep-alive

上述响应头包括了以下字段:

  • Status-line:指定HTTP协议版本、状态码和状态消息。
  • Content-Type:指定响应体的MIME类型及字符编码格式。
  • Content-Length:指定响应体的字节数。
  • Set-Cookie:服务器向客户端发送cookie信息时使用该字段。
  • Server:服务器类型和版本号。
  • Connection:服务器是否需要保持长连接。

客户端的编写

客户端每次发送数据都要携带头部信息,所以为了减少每次重新构造头部的开销,我们在客户端的构造函数里将头部信息构造好,作为一个成员放入客户端的类成员里。

  1. client(boost::asio::io_context& io_context,
  2. const std::string& server, const std::string& path)
  3. : resolver_(io_context),
  4. socket_(io_context)
  5. {
  6. // Form the request. We specify the "Connection: close" header so that the
  7. // server will close the socket after transmitting the response. This will
  8. // allow us to treat all data up until the EOF as the content.
  9. std::ostream request_stream(&request_);
  10. request_stream << "GET " << path << " HTTP/1.0\r\n";
  11. request_stream << "Host: " << server << "\r\n";
  12. request_stream << "Accept: */*\r\n";
  13. request_stream << "Connection: close\r\n\r\n";
  14. size_t pos = server.find(":");
  15. std::string ip = server.substr(0, pos);
  16. std::string port = server.substr(pos + 1);
  17. // Start an asynchronous resolve to translate the server and service names
  18. // into a list of endpoints.
  19. resolver_.async_resolve(ip, port,
  20. boost::bind(&client::handle_resolve, this,
  21. boost::asio::placeholders::error,
  22. boost::asio::placeholders::results));
  23. }

我们的客户端构造了一个request_成员变量,依次写入请求的路径,主机地址,期望接受的媒体类型,以及每次收到请求后断开连接,也就是短链接的方式。
接着又异步解析ip和端口,解析成功后调用handle_resolve函数。
handle_resolve函数里异步处理连接

  1. void handle_resolve(const boost::system::error_code& err,
  2. const tcp::resolver::results_type& endpoints)
  3. {
  4. if (!err)
  5. {
  6. // Attempt a connection to each endpoint in the list until we
  7. // successfully establish a connection.
  8. boost::asio::async_connect(socket_, endpoints,
  9. boost::bind(&client::handle_connect, this,
  10. boost::asio::placeholders::error));
  11. }
  12. else
  13. {
  14. std::cout << "Error: " << err.message() << "\n";
  15. }
  16. }

处理连接

  1. void handle_connect(const boost::system::error_code& err)
  2. {
  3. if (!err)
  4. {
  5. // The connection was successful. Send the request.
  6. boost::asio::async_write(socket_, request_,
  7. boost::bind(&client::handle_write_request, this,
  8. boost::asio::placeholders::error));
  9. }
  10. else
  11. {
  12. std::cout << "Error: " << err.message() << "\n";
  13. }
  14. }

在连接成功后,我们首先将头部信息发送给服务器,发送完成后监听对端发送的数据

  1. void handle_write_request(const boost::system::error_code& err)
  2. {
  3. if (!err)
  4. {
  5. // Read the response status line. The response_ streambuf will
  6. // automatically grow to accommodate the entire line. The growth may be
  7. // limited by passing a maximum size to the streambuf constructor.
  8. boost::asio::async_read_until(socket_, response_, "\r\n",
  9. boost::bind(&client::handle_read_status_line, this,
  10. boost::asio::placeholders::error));
  11. }
  12. else
  13. {
  14. std::cout << "Error: " << err.message() << "\n";
  15. }
  16. }

当收到对方数据时,先解析响应的头部信息

  1. void handle_read_status_line(const boost::system::error_code& err)
  2. {
  3. if (!err)
  4. {
  5. // Check that response is OK.
  6. std::istream response_stream(&response_);
  7. std::string http_version;
  8. response_stream >> http_version;
  9. unsigned int status_code;
  10. response_stream >> status_code;
  11. std::string status_message;
  12. std::getline(response_stream, status_message);
  13. if (!response_stream || http_version.substr(0, 5) != "HTTP/")
  14. {
  15. std::cout << "Invalid response\n";
  16. return;
  17. }
  18. if (status_code != 200)
  19. {
  20. std::cout << "Response returned with status code ";
  21. std::cout << status_code << "\n";
  22. return;
  23. }
  24. // Read the response headers, which are terminated by a blank line.
  25. boost::asio::async_read_until(socket_, response_, "\r\n\r\n",
  26. boost::bind(&client::handle_read_headers, this,
  27. boost::asio::placeholders::error));
  28. }
  29. else
  30. {
  31. std::cout << "Error: " << err << "\n";
  32. }
  33. }

上面的代码先读出HTTP版本,以及返回的状态码,如果状态码不是200,则返回,是200说明响应成功。接下来把所有的头部信息都读出来。

  1. void handle_read_headers(const boost::system::error_code& err)
  2. {
  3. if (!err)
  4. {
  5. // Process the response headers.
  6. std::istream response_stream(&response_);
  7. std::string header;
  8. while (std::getline(response_stream, header) && header != "\r")
  9. std::cout << header << "\n";
  10. std::cout << "\n";
  11. // Write whatever content we already have to output.
  12. if (response_.size() > 0)
  13. std::cout << &response_;
  14. // Start reading remaining data until EOF.
  15. boost::asio::async_read(socket_, response_,
  16. boost::asio::transfer_at_least(1),
  17. boost::bind(&client::handle_read_content, this,
  18. boost::asio::placeholders::error));
  19. }
  20. else
  21. {
  22. std::cout << "Error: " << err << "\n";
  23. }
  24. }

上面的代码逐行读出头部信息,然后读出响应的内容,继续监听读事件读取相应的内容,直到接收到EOF信息,也就是对方关闭,继续监听读事件是因为有可能是长连接的方式,当然如果是短链接,则服务器关闭连接后,客户端也是通过异步函数读取EOF进而结束请求。

  1. void handle_read_content(const boost::system::error_code& err)
  2. {
  3. if (!err)
  4. {
  5. // Write all of the data that has been read so far.
  6. std::cout << &response_;
  7. // Continue reading remaining data until EOF.
  8. boost::asio::async_read(socket_, response_,
  9. boost::asio::transfer_at_least(1),
  10. boost::bind(&client::handle_read_content, this,
  11. boost::asio::placeholders::error));
  12. }
  13. else if (err != boost::asio::error::eof)
  14. {
  15. std::cout << "Error: " << err << "\n";
  16. }
  17. }

在主函数中调用客户端请求服务器信息, 请求的路由地址为/

  1. int main(int argc, char* argv[])
  2. {
  3. try
  4. {
  5. boost::asio::io_context io_context;
  6. client c(io_context, "127.0.0.1:8080", "/");
  7. io_context.run();
  8. getchar();
  9. }
  10. catch (std::exception& e)
  11. {
  12. std::cout << "Exception: " << e.what() << "\n";
  13. }
  14. return 0;
  15. }

服务器设计

为了方便理解,我们从服务器的调用流程讲起

  1. int main(int argc, char* argv[])
  2. {
  3. try
  4. {
  5. std::filesystem::path path = std::filesystem::current_path() / "res";
  6. // 使用 std::cout 输出拼接后的路径
  7. std::cout << "Path: " << path.string() << '\n';
  8. std::cout << "Usage: http_server <127.0.0.1> <8080> "<< path.string() <<"\n";
  9. // Initialise the server.
  10. http::server::server s("127.0.0.1", "8080", path.string());
  11. // Run the server until stopped.
  12. s.run();
  13. }
  14. catch (std::exception& e)
  15. {
  16. std::cerr << "exception: " << e.what() << "\n";
  17. }
  18. return 0;
  19. }

主函数里构造了一个server对象,然后调用了run函数使其跑起来。
run函数其实就是调用了server类成员的ioservice

  1. void server::run()
  2. {
  3. io_service_.run();
  4. }

server类的构造函数里初始化一些成员变量,比如acceptor连接器,绑定了终止信号,并且监听对端连接

  1. server::server(const std::string& address, const std::string& port,
  2. const std::string& doc_root)
  3. : io_service_(),
  4. signals_(io_service_),
  5. acceptor_(io_service_),
  6. connection_manager_(),
  7. socket_(io_service_),
  8. request_handler_(doc_root)
  9. {
  10. signals_.add(SIGINT);
  11. signals_.add(SIGTERM);
  12. #if defined(SIGQUIT)
  13. signals_.add(SIGQUIT);
  14. #endif
  15. do_await_stop();
  16. boost::asio::ip::tcp::resolver resolver(io_service_);
  17. boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve({ address, port });
  18. acceptor_.open(endpoint.protocol());
  19. acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
  20. acceptor_.bind(endpoint);
  21. acceptor_.listen();
  22. do_accept();
  23. }

接收连接

  1. void server::do_accept()
  2. {
  3. acceptor_.async_accept(socket_,
  4. [this](boost::system::error_code ec)
  5. {
  6. if (!acceptor_.is_open())
  7. {
  8. return;
  9. }
  10. if (!ec)
  11. {
  12. connection_manager_.start(std::make_shared<connection>(
  13. std::move(socket_), connection_manager_, request_handler_));
  14. }
  15. do_accept();
  16. });
  17. }

接收函数里通过connection_manager_启动了一个新的连接,用来处理读写函数。
处理方式和我们之前的写法类似,只是我们之前管理连接用的server,这次用的conneciton_manager

  1. void connection_manager::start(connection_ptr c)
  2. {
  3. connections_.insert(c);
  4. c->start();
  5. }

start函数里处理读写

  1. void connection::start()
  2. {
  3. do_read();
  4. }

处理读数据比较复杂,我们分部分解释

  1. void connection::do_read()
  2. {
  3. auto self(shared_from_this());
  4. socket_.async_read_some(boost::asio::buffer(buffer_),
  5. [this, self](boost::system::error_code ec, std::size_t bytes_transferred)
  6. {
  7. if (!ec)
  8. {
  9. request_parser::result_type result;
  10. std::tie(result, std::ignore) = request_parser_.parse(
  11. request_, buffer_.data(), buffer_.data() + bytes_transferred);
  12. if (result == request_parser::good)
  13. {
  14. request_handler_.handle_request(request_, reply_);
  15. do_write();
  16. }
  17. else if (result == request_parser::bad)
  18. {
  19. reply_ = reply::stock_reply(reply::bad_request);
  20. do_write();
  21. }
  22. else
  23. {
  24. do_read();
  25. }
  26. }
  27. else if (ec != boost::asio::error::operation_aborted)
  28. {
  29. connection_manager_.stop(shared_from_this());
  30. }
  31. });
  32. }

通过request_parser_解析请求,然后根据请求结果选择处理请求还是返回错误。

  1. std::tuple<result_type, InputIterator> parse(request& req,
  2. InputIterator begin, InputIterator end)
  3. {
  4. while (begin != end)
  5. {
  6. result_type result = consume(req, *begin++);
  7. if (result == good || result == bad)
  8. return std::make_tuple(result, begin);
  9. }
  10. return std::make_tuple(indeterminate, begin);
  11. }

parse是解析请求的函数,内部调用了consume不断处理请求头中的数据,其实就是一个逐行解析的过程, consume函数很长,这里就不解释了,其实就是每解析一行就更改一下状态,这样可以继续解析。具体可以看看源码。

在consume()函数中,根据每个字符输入的不同情况,判断当前所处状态state_,进而执行相应的操作,包括:

  • 将HTTP请求方法、URI和HTTP版本号解析到request结构体中。
  • 解析每个请求头部字段的名称和值,并将其添加到request结构体中的headers vector中。
  • 如果输入字符为\r\n,则修改状态以开始下一行的解析。

最后,返回一个枚举类型request_parser::result_type作为解析结果,包括indeterminate、good和bad三种状态。其中,indeterminate表示还需要继续等待更多字符输入;good表示成功解析出了一个完整的HTTP请求头部;bad表示遇到无效字符或格式错误,解析失败。

解析完成头部后会调用处理请求的函数,这里只是简单的写了一个作为资源服务器解析资源请求的逻辑,具体可以看源码。

  1. void request_handler::handle_request(const request& req, reply& rep)
  2. {
  3. // Decode url to path.
  4. std::string request_path;
  5. if (!url_decode(req.uri, request_path))
  6. {
  7. rep = reply::stock_reply(reply::bad_request);
  8. return;
  9. }
  10. // Request path must be absolute and not contain "..".
  11. if (request_path.empty() || request_path[0] != '/'
  12. || request_path.find("..") != std::string::npos)
  13. {
  14. rep = reply::stock_reply(reply::bad_request);
  15. return;
  16. }
  17. // If path ends in slash (i.e. is a directory) then add "index.html".
  18. if (request_path[request_path.size() - 1] == '/')
  19. {
  20. request_path += "index.html";
  21. }
  22. // Determine the file extension.
  23. std::size_t last_slash_pos = request_path.find_last_of("/");
  24. std::size_t last_dot_pos = request_path.find_last_of(".");
  25. std::string extension;
  26. if (last_dot_pos != std::string::npos && last_dot_pos > last_slash_pos)
  27. {
  28. extension = request_path.substr(last_dot_pos + 1);
  29. }
  30. // Open the file to send back.
  31. std::string full_path = doc_root_ + request_path;
  32. std::ifstream is(full_path.c_str(), std::ios::in | std::ios::binary);
  33. if (!is)
  34. {
  35. rep = reply::stock_reply(reply::not_found);
  36. return;
  37. }
  38. // Fill out the reply to be sent to the client.
  39. rep.status = reply::ok;
  40. char buf[512];
  41. while (is.read(buf, sizeof(buf)).gcount() > 0)
  42. rep.content.append(buf, is.gcount());
  43. rep.headers.resize(2);
  44. rep.headers[0].name = "Content-Length";
  45. rep.headers[0].value = std::to_string(rep.content.size());
  46. rep.headers[1].name = "Content-Type";
  47. rep.headers[1].value = mime_types::extension_to_type(extension);
  48. }

上述代码根据url中的.来做切割,获取请求的文件类型,然后根据/切割url,获取资源目录,最后返回资源文件。
如果你想实现普通的路由请求返回json或者text格式,可以重写处理请求的逻辑。

总结

本文介绍了如何使用asio实现http服务器,具体可以查看下方源码,其实这些仅作为了解即可,不推荐从头造轮子,我们可以用一些C++ 成熟的http服务库比如beast,下一节再介绍。

视频连接https://space.bilibili.com/271469206/channel/collectiondetail?sid=313101

源码链接https://gitee.com/secondtonone1/boostasio-learn

热门评论
  • dong
    2023-09-06 10:20:24

    [IO复用,性能提升在哪里]

    服务器接收流程:
    1.绑定端口等待建立socket连接
    2.accept建立对话socket
    3.对话socket,循环async_read_some,读取数据包,解析html数据包 [线程池+协程]
    4.对话socket,循环async_write_some,生成html数据包,发送数据包 [线程池+协程]
    5.结束对话,关闭socket

    疑问:
    IO复用,是优化在哪里,流程个人理解也是一样,比如有新对话socket,它是通过回调处理,然后也是在回调里面循环读取数据包,再发送.

热门文章

  1. C++ 类的继承封装和多态

    喜欢(588) 浏览(4986)
  2. windows环境搭建和vscode配置

    喜欢(587) 浏览(2822)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(12227)
  4. 解密定时器的实现细节

    喜欢(566) 浏览(3481)
  5. slice介绍和使用

    喜欢(521) 浏览(2479)

最新评论

  1. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  2. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊
  3. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?
  4. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google

个人公众号

个人微信