代码见: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