Boost.Asio学习之简单的HTTP服务器

本文详细介绍了如何使用Boost.Asio库创建一个简单的HTTP服务器,包括Main函数、HTTP_SERVER类、Connection_manager类、Connection类的设计和实现,以及Request_Parser和Request_Handler类的功能。文章特别强调了std::tie和std::tuple在处理HTTP请求解析中的作用,并展示了如何通过tie解包处理返回值。整个过程涉及TCP连接的异步读取、HTTP请求解析和响应的构建与发送。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码见:http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/examples/cpp11_examples.html
或者:https://github.com/NearXdu/AsioLearn

这是boost.asio的example中比较大的一示例了,主要是HTTP协议的在应用层的业务比较复杂,不过提供的例子实现了简单的HTTP服务器。

1.Main

抛开业务不说,http服务器本质上来说是一个TCP服务器。因此,根据看的那么多boost.asio官方提供的example,它的套路还是比较单一的:

#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include "http_server.hpp"

int main(int argc, char* argv[])
{
    try
    {
    // Check command line arguments.
    if (argc != 4)
    {
        std::cerr << "Usage: http_server <address> <port> <doc_root>\n";
        std::cerr << "  For IPv4, try:\n";
        std::cerr << "    receiver 0.0.0.0 80 .\n";
        std::cerr << "  For IPv6, try:\n";
        std::cerr << "    receiver 0::0 80 .\n";
        return 1;
    }

    // Initialise the server.
    http::server::server s(argv[1], argv[2], argv[3]);

    // Run the server until stopped.
    s.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "exception: " << e.what() << "\n";
    }
    return 0;
}

2.HTTP_SERVER类

在server的构造函数中,将进行一系列的初始化,例如初始化监听套接字,初始化已连接套接字,初始化io_service,初始化信号signal等等。

    server::server(const std::string& address, const std::string& port,
        const std::string& doc_root)
        : io_service_(),
        signals_(io_service_),
        acceptor_(io_service_),
        connection_manager_(),
        socket_(io_service_),
        request_handler_(doc_root)//处理请求的类
    {
        // Register to handle the signals that indicate when the server should exit.
        // It is safe to register for the same signal multiple times in a program,
        // provided all registration for the specified signal is made through Asio.
        signals_.add(SIGINT);
        signals_.add(SIGTERM);
#if defined(SIGQUIT)
        signals_.add(SIGQUIT);
#endif // defined(SIGQUIT)

        do_await_stop();

        // Open the acceptor with the option to reuse the address (i.e. SO_REUSEADDR).
        boost::asio::ip::tcp::resolver resolver(io_service_);
        boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve({address, port});
        acceptor_.open(endpoint.protocol());
        acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
        acceptor_.bind(endpoint);
        acceptor_.listen();

        do_accept();//等待连接
    }

除此之外,我们还看到一些成员:connection_manager保存所有连接、request_handler将处理HTTP业务请求。

来看do_accept(),异步接受TCP连接,完成回调操作将开始进入HTTP业务处理。

void server::do_accept()//异步接收连接
    {
        acceptor_.async_accept(socket_,
            [this](boost::system::error_code ec)
            {
            // Check whether the server was stopped by a signal before this
            // completion handler had a chance to run.
            if (!acceptor_.is_open())
            {
            return;
            }

            if (!ec)
            {
            //connection表示一个连接
            connection_manager_.start(std::make_shared<connection>(
                std::move(socket_), connection_manager_, request_handler_));
            }
            do_accept();
            });
    }

3.Connection_manager类

该类的左右有点像聊天室程序中的chatroom,在start方法中,通过make_shared调用connection方法的构造函数将socket已连接套接字封装到connection中,并保存connection的shared_ptr对象到一个set中。

void connection_manager::start(connection_ptr c)
    {
        connections_.insert(c);//connections_为保存connection的shared_ptr set
        c->start();//调用connection的start开始读取网络套接字数据
    }

4.Connection类

在connection_manager::start()方法中调用connection::start(),开始异步接收数据,并进入HTTP业务处理模块(解析请求、url等)。

//connection.cpp

    void connection::start()
    {
        do_read();//异步读取数据
    }
//---------------------------------------//

    void connection::do_read()
    {
        auto self(shared_from_this());
        socket_.async_read_some(boost::asio::buffer(buffer_),
            [this, self](boost::system::error_code ec, std::size_t bytes_transferred)
            {
            //完成回调,收到数据后要开始解析HTTP请求
            if (!ec)
            {
            request_parser::result_type result;
            std::tie(result, std::ignore) = request_parser_.parse(
            request_, buffer_.data(), buffer_.data() + bytes_transferred);
            //这里参数需要两个迭代器,表示流的开始和结束
            //request,iterator_begin(),iterator_end()

            if (result == request_parser::good)//解析请求成功
            {
            request_handler_.handle_request(request_, reply_);
            do_write();
            }
            else if (result == request_parser::bad)//解析请求失败
            {
            reply_ = reply::stock_reply(reply::bad_request);
            do_write();
            }
            else//继续异步读取消息
            {
            do_read();
            }
            }
            else if (ec != boost::asio::error::operation_aborted)
            {
            connection_manager_.stop(shared_from_this());
            }
            });
    }

先不考虑业务逻辑怎么实现,这里段代码的大致逻辑就是套接字异步的读取网络数据,当读取完成后,进入完成回调(lambda),开始对收到的TCP流进行请求解析,如果解析成功, request_handler_.handle_request(request_, reply_);将开始处理请求,并且通过on_write方法写入响应。

4.1 std::tie和std::tuple

这部分参考:http://www.cnblogs.com/qicosmos/p/3318070.html
这里看到一个有意思的语法:
std::tie和std::tuple,这是C++11的用法,可以简化我们的程序。其功能就好像一个匿名的结构体。
比如说:

tuple<const char*, int>tp = make_tuple(sendPack,nSendSize); //构造一个tuple

tp就相当于:

struct A
{
char* p;
int len;
};

用tuple

auto tp = return std::tie(1, "aa", 2);
//tp的类型实际是:
std::tuple<int&,string&, int&>

再看看如何获取它的值:

const char* data = std::get<0>(); //获取第一个值
int len = std::get<1>(); //获取第二个值

还有一种方法也可以获取元组的值,通过std::tie解包tuple

int x,y;
string a;
std::tie(x,a,y) = tp; 

通过tie解包后,tp中三个值会自动赋值给三个变量。

解包时,我们如果只想解某个位置的值时,可以用std::ignore占位符来表示不解某个位置的值。比如我们只想解第三个值时:

std::tie(std::ignore,std::ignore,y) = tp; //只解第三个值了

了解了tuple和tie之后,对于asio的代码就不陌生了。

request_parser::result_type result;
            std::tie(result, std::ignore) = request_parser_.parse(
            request_, buffer_.data(), buffer_.data() + bytes_transferred);//request,iterator_begin(),iterator_end()

通过tie来解包request_parser_.parse的返回值。


5.Request_Parser类

这要看parse方法:

    template <typename InputIterator>
            std::tuple<result_type, InputIterator> parse(request& req,
                InputIterator begin, InputIterator end)
            {
            while (begin != end)
            {
                result_type result = consume(req, *begin++);
                if (result == good || result == bad)
                return std::make_tuple(result, begin);
            }
            return std::make_tuple(indeterminate, begin);
            }

根据tie和tuple描述,果然parse方法的返回值是一个tuple类型,这样就可以通过tie来解包了。

再看这个函数,首先它将通过template定义了迭代其类型接受两个参数begin和end,根据之前的代码不难发现,begin即是buffer.data()、end即是buffer.data()+bytes_transferrred。再看代码:

while(begin!=end)
//...

可以看出,boost.asio这个demo中,是逐个字节的解析http请求。因此不难得出consume函数的第二个变量是一个char类型。

    request_parser::result_type request_parser::consume(request& req, char input)
    {
        switch (state_)
        {
        case method_start:
            if (!is_char(input) || is_ctl(input) || is_tspecial(input))
            {

    //...
    }

这段代码太长,但逻辑还算清楚,由于是逐个字节的解析,那么就要考虑当前解析到什么状态了,比如对于一个HTTP请求来说,它大概是这个样子的:

METHOD URL VERSION\r\n
Host: xxxx\r\n
Content-Type: xxxx\r\n
Content-Length: xxxx\r\n
\r\n
content

因此HTTP Server需要随时记录当前TCP字节流解析到什么步骤了,并把当前的解析到的字符插入到请求的相应位置中,在这个demo中,设计了一个Request类,用来保存解析的结果,例如string method,当解析到相应步骤时,会将当前字符插入到对应的成员中,例如:
req.method.push_back(input)


6. Request_Handler类

当我们解析完收到的TCP字节流后,已经构造好了一个HTTP请求的对象,它包含了该请求中的所有内容,例如:请求头、请求头、请求报文。讲道理的话,应该需要对这些内容进行解析,比如说,最起码对请求方法做一个简单的区分、或者对静态请求或者动态请求做一个简单的区分。在boost的这个demo中,没有考虑更复杂的情况,它仅仅考虑GET方法请求服务器上的静态资源。

    void request_handler::handle_request(const request& req, reply& rep)
    {//解析请求
        // Decode url to path.
        std::string request_path;
        if (!url_decode(req.uri, request_path))//解析请求地址
        {
        rep = reply::stock_reply(reply::bad_request);
        return;
        }//解析url

        // Request path must be absolute and not contain "..".
        // 请求url的条件
        if (request_path.empty() || request_path[0] != '/'
            || request_path.find("..") != std::string::npos)
        {
        rep = reply::stock_reply(reply::bad_request);
        return;
        }

        // If path ends in slash (i.e. is a directory) then add "index.html".
        if (request_path[request_path.size() - 1] == '/')//如果请求url最后一个字符是/,那么加上一个index.html
        {
        request_path += "index.html";
        }

        // Determine the file extension.
        std::size_t last_slash_pos = request_path.find_last_of("/");//最后一个/符号
        std::size_t last_dot_pos = request_path.find_last_of(".");//最后一个.
        std::string extension;
        if (last_dot_pos != std::string::npos && last_dot_pos > last_slash_pos)
        {
        extension = request_path.substr(last_dot_pos + 1);//获得扩展名
        }

        // Open the file to send back.
        std::string full_path = doc_root_ + request_path;//文件的完整目录
        std::cout<<full_path<<std::endl;
        std::ifstream is(full_path.c_str(), std::ios::in | std::ios::binary);//打开文件--二进制
        if (!is)
        {
        rep = reply::stock_reply(reply::not_found);
        return;
        }

        // Fill out the reply to be sent to the client.
        // 响应码
        rep.status = reply::ok;//response
        char buf[512];
//获取文件内容
        while (is.read(buf, sizeof(buf)).gcount() > 0)
        {
        //append(const char *,size_t size)
        rep.content.append(buf, is.gcount());//gcount()返回读取数目
        }
        //响应头
        rep.headers.resize(2);
        rep.headers[0].name = "Content-Length";
        rep.headers[0].value = std::to_string(rep.content.size());
        rep.headers[1].name = "Content-Type";
        rep.headers[1].value = mime_types::extension_to_type(extension);//扩展名->Content-Type
    }

6.1 reply类

最后我们看到了响应类,这个类中包含了要回送到用户的响应:

std::vector<boost::asio::const_buffer> reply::to_buffers()
    {
        std::vector<boost::asio::const_buffer> buffers;
        //创建响应行
        buffers.push_back(status_strings::to_buffer(status));
        //创建响应头
        for (std::size_t i = 0; i < headers.size(); ++i)
        {
        header& h = headers[i];
        //响应key
        buffers.push_back(boost::asio::buffer(h.name));
        buffers.push_back(boost::asio::buffer(misc_strings::name_value_separator));
        //响应value
        buffers.push_back(boost::asio::buffer(h.value));
        buffers.push_back(boost::asio::buffer(misc_strings::crlf));
        }
        //crlf
        buffers.push_back(boost::asio::buffer(misc_strings::crlf));
        //响应报文
        buffers.push_back(boost::asio::buffer(content));
        return buffers;
    }

它做的工作主要是将string或者char*类型的字串,构造成在write函数中需要的boost::asio::buffer。
因此,当我们回头再看connection类中do_write方法时,可以看到reply_.to_buffers()这样的调用。


6.2 do_write函数

当构造好响应报文后,应该将其发送到客户端,并且关闭连接(不考虑keep-alive)。毁掉connection类中,connection::do_write异步的发送数据,并在完成回调中关闭连接:

void connection::do_write()
    {
        auto self(shared_from_this());
        boost::asio::async_write(socket_, reply_.to_buffers(),
            [this, self](boost::system::error_code ec, std::size_t)
            {
            if (!ec)
            {
            // Initiate graceful connection closure.
            boost::system::error_code ignored_ec;
            socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both,
            ignored_ec);//shutdown_both关闭读端和写端--->close()
            }

            if (ec != boost::asio::error::operation_aborted)
            {
            connection_manager_.stop(shared_from_this());
            }
            });
    }

7. 参考

1.图解http
2.boost文档
3.http://www.cnblogs.com/qicosmos/p/3318070.html


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值