前面【muduo学习笔记:net部分之Http–HttpRequest、HttpResponse 和 HttpContext】介绍了TCP数据数据Buffer承载的HTTP报文的解析,本文结合TcpServer,基于muduo实现一个简单的HttpServer。由于对协议解析不完善,它不适合写web的通用服务。
1、HttpServer的定义
有了TcpServer的基础,加上前面博客关于HttpRequest、HttpResponse 和 HttpContext的使用,我们可以封装一个HttpServer,在收到客户端的消息后,按照HTTP协议解析Buffer,并使用回调函供用户处理Request、返回Response。
class HttpServer : noncopyable
{
public:
typedef std::function<void (const HttpRequest&, HttpResponse*)> HttpCallback;
HttpServer(EventLoop* loop,
const InetAddress& listenAddr,
const string& name,
TcpServer::Option option = TcpServer::kNoReusePort);
EventLoop* getLoop() const { return server_.getLoop(); }
/// Not thread safe, callback be registered before calling start().
// 设置http请求的回调函数
void setHttpCallback(const HttpCallback& cb) { httpCallback_ = cb; }
void setThreadNum(int numThreads) { server_.setThreadNum(numThreads); }
void start();
private:
// TcpServer的新连接、新消息的回调函数
void onConnection(const TcpConnectionPtr& conn);
void onMessage(const TcpConnectionPtr& conn, Buffer* buf,Timestamp receiveTime);
// 在onMessage中调用,并调用用户注册的httpCallback_函数,对请求进行具体的处理
void onRequest(const TcpConnectionPtr&, const HttpRequest&);
TcpServer server_;
HttpCallback httpCallback_;
};
2、HttpServer的实现
在HttpServer构造函数中,注册TcpServer对外暴露的两个回调函数:新的连接、新消息到来。
server_.setConnectionCallback(std::bind(&HttpServer::onConnection, this, _1));
server_.setMessageCallback(std::bind(&HttpServer::onMessage, this, _1, _2, _3));
当客户端如浏览器连接上来,根据以前的分析可知,调用HttpServer::onConnection(), 绑定一个HttpContext
到TcpConnection
中的成员变量std::any context_
,这里绑定一个HttpContext主要是为了长连接中仅分配一次对象,提高效率。
void HttpServer::onConnection(const TcpConnectionPtr& conn)
{
if (conn->connected()){
conn->setContext(HttpContext()); // 绑定一个HttpContext到TcpConnection
}
}
接着客户端发出请求,比如访问服务器的某个路径,那么自动回调HttpServer::onMessage()
,
void HttpServer::onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime)
{
HttpContext* context = std::any_cast<HttpContext>(conn->getMutableContext());
// 解析请求
if (!context->parseRequest(buf, receiveTime)){
conn->send("HTTP/1.1 400 Bad Request\r\n\r\n"); // 解析失败, 400错误
conn->shutdown(); // 断开本段写
}
// 请求解析成功
if (context->gotAll()){
onRequest(conn, context->request()); // 调用onRequest()私有函数
context->reset(); // 复用HttpContext对象
}
}
其中parseRequest()
会将存放在Buffer
中的请求解析到server_.TcpConnection.context_.request_
中,最后调用HttpServer::onRequest()
,
void HttpServer::onRequest(const TcpConnectionPtr& conn, const HttpRequest& req)
{
// 长连接还是短连接
const string& connection = req.getHeader("Connection");
bool close = connection == "close" ||
(req.getVersion() == HttpRequest::kHttp10 && connection != "Keep-Alive");
HttpResponse response(close);
httpCallback_(req, &response); // 调用客户端的http处理函数,填充response
Buffer buf;
response.appendToBuffer(&buf); // 将响应格式化填充到buf中
conn->send(&buf); // 将响应回复发送给客户端
if (response.closeConnection()){
conn->shutdown(); // 如果是短连接,直接关闭。
}
}
即要用客户代码设置的httpCallback_
函数来填充HttpResponse
,然后发送给客户端。
3、HttpServer测试
muduo中自带的sample实现了几个GET请求,POST的请求可以根据实际情况进行解析。
#include <muduo/net/http/HttpServer.h>
#include <muduo/net/http/HttpRequest.h>
#include <muduo/net/http/HttpResponse.h>
#include <muduo/net/EventLoop.h>
#include <muduo/base/Logging.h>
#include <iostream>
#include <map>
using namespace muduo;
using namespace muduo::net;
extern char favicon[555]; // favicon图标数据
bool benchmark = false;
// 实际的请求处理
void onRequest(const HttpRequest& req, HttpResponse* resp)
{
// 打印所有的请求头
std::cout << "Headers " << req.methodString() << " " << req.path() << std::endl;
if (!benchmark){
const std::map<string, string>& headers = req.headers();
for (const auto& header : headers){
std::cout << header.first << ": " << header.second << std::endl;
}
}
if (req.path() == "/") // 根目录请求
{
resp->setStatusCode(HttpResponse::k200Ok);
resp->setStatusMessage("OK");
resp->setContentType("text/html");
resp->addHeader("Server", "Muduo");
string now = Timestamp::now().toFormattedString();
resp->setBody("<html><head><title>This is title</title></head>"
"<body><h1>Hello</h1>Now is " + now +
"</body></html>");
}
else if (req.path() == "/favicon.ico") // 图标请求
{
resp->setStatusCode(HttpResponse::k200Ok);
resp->setStatusMessage("OK");
resp->setContentType("image/png");
resp->setBody(string(favicon, sizeof favicon));
}
else if (req.path() == "/hello") // /hello,返回文本
{
resp->setStatusCode(HttpResponse::k200Ok);
resp->setStatusMessage("OK");
resp->setContentType("text/plain");
resp->addHeader("Server", "Muduo");
resp->setBody("hello, world!\n");
}
else // 不存在的URL,404 错误
{
resp->setStatusCode(HttpResponse::k404NotFound);
resp->setStatusMessage("Not Found");
resp->setCloseConnection(true);
}
}
int main(int argc, char* argv[])
{
int numThreads = 0;
if (argc > 1)
{
benchmark = true;
Logger::setLogLevel(Logger::WARN);
numThreads = atoi(argv[1]);
}
EventLoop loop;
HttpServer server(&loop, InetAddress(8000), "dummy");
server.setHttpCallback(onRequest);
server.setThreadNum(numThreads);
server.start();
loop.loop();
}
char favicon[555] = {
'\x89', 'P', 'N', 'G', '\xD', '\xA', '\x1A', '\xA','\x0', '\x0', '\x0', '\xD', 'I', 'H', 'D', 'R',
'\x0', '\x0', '\x0', '\x10', '\x0', '\x0', '\x0', '\x10','\x8', '\x6', '\x0', '\x0', '\x0', '\x1F', '\xF3', '\xFF',
'a', '\x0', '\x0', '\x0', '\x19', 't', 'E', 'X','t', 'S', 'o', 'f', 't', 'w', 'a', 'r',
'e', '\x0', 'A', 'd', 'o', 'b', 'e', '\x20','I', 'm', 'a', 'g', 'e', 'R', 'e', 'a',
'd', 'y', 'q', '\xC9', 'e', '\x3C', '\x0', '\x0', '\x1', '\xCD', 'I', 'D', 'A', 'T', 'x', '\xDA',
'\x94', '\x93', '9', 'H', '\x3', 'A', '\x14', '\x86', '\xFF', '\x5D', 'b', '\xA7', '\x4', 'R', '\xC4', 'm',
'\x22', '\x1E', '\xA0', 'F', '\x24', '\x8', '\x16', '\x16', 'v', '\xA', '6', '\xBA', 'J', '\x9A', '\x80', '\x8',
'A', '\xB4', 'q', '\x85', 'X', '\x89', 'G', '\xB0', 'I', '\xA9', 'Q', '\x24', '\xCD', '\xA6', '\x8', '\xA4',
'H', 'c', '\x91', 'B', '\xB', '\xAF', 'V', '\xC1', 'F', '\xB4', '\x15', '\xCF', '\x22', 'X', '\x98', '\xB',
'T', 'H', '\x8A', 'd', '\x93', '\x8D', '\xFB', 'F', 'g', '\xC9', '\x1A', '\x14', '\x7D', '\xF0', 'f', 'v',
'f', '\xDF', '\x7C', '\xEF', '\xE7', 'g', 'F', '\xA8', '\xD5', 'j', 'H', '\x24', '\x12', '\x2A', '\x0', '\x5',
'\xBF', 'G', '\xD4', '\xEF', '\xF7', '\x2F', '6', '\xEC', '\x12', '\x20', '\x1E', '\x8F', '\xD7', '\xAA', '\xD5', '\xEA',
'\xAF', 'I', '5', 'F', '\xAA', 'T', '\x5F', '\x9F', '\x22', 'A', '\x2A', '\x95', '\xA', '\x83', '\xE5', 'r',
'9', 'd', '\xB3', 'Y', '\x96', '\x99', 'L', '\x6', '\xE9', 't', '\x9A', '\x25', '\x85', '\x2C', '\xCB', 'T',
'\xA7', '\xC4', 'b', '1', '\xB5', '\x5E', '\x0', '\x3', 'h', '\x9A', '\xC6', '\x16', '\x82', '\x20', 'X', 'R',
'\x14', 'E', '6', 'S', '\x94', '\xCB', 'e', 'x', '\xBD', '\x5E', '\xAA', 'U', 'T', '\x23', 'L', '\xC0',
'\xE0', '\xE2', '\xC1', '\x8F', '\x0', '\x9E', '\xBC', '\x9', 'A', '\x7C', '\x3E', '\x1F', '\x83', 'D', '\x22', '\x11',
'\xD5', 'T', '\x40', '\x3F', '8', '\x80', 'w', '\xE5', '3', '\x7', '\xB8', '\x5C', '\x2E', 'H', '\x92', '\x4',
'\x87', '\xC3', '\x81', '\x40', '\x20', '\x40', 'g', '\x98', '\xE9', '6', '\x1A', '\xA6', 'g', '\x15', '\x4', '\xE3',
'\xD7', '\xC8', '\xBD', '\x15', '\xE1', 'i', '\xB7', 'C', '\xAB', '\xEA', 'x', '\x2F', 'j', 'X', '\x92', '\xBB',
'\x18', '\x20', '\x9F', '\xCF', '3', '\xC3', '\xB8', '\xE9', 'N', '\xA7', '\xD3', 'l', 'J', '\x0', 'i', '6',
'\x7C', '\x8E', '\xE1', '\xFE', 'V', '\x84', '\xE7', '\x3C', '\x9F', 'r', '\x2B', '\x3A', 'B', '\x7B', '7', 'f',
'w', '\xAE', '\x8E', '\xE', '\xF3', '\xBD', 'R', '\xA9', 'd', '\x2', 'B', '\xAF', '\x85', '2', 'f', 'F',
'\xBA', '\xC', '\xD9', '\x9F', '\x1D', '\x9A', 'l', '\x22', '\xE6', '\xC7', '\x3A', '\x2C', '\x80', '\xEF', '\xC1', '\x15',
'\x90', '\x7', '\x93', '\xA2', '\x28', '\xA0', 'S', 'j', '\xB1', '\xB8', '\xDF', '\x29', '5', 'C', '\xE', '\x3F',
'X', '\xFC', '\x98', '\xDA', 'y', 'j', 'P', '\x40', '\x0', '\x87', '\xAE', '\x1B', '\x17', 'B', '\xB4', '\x3A',
'\x3F', '\xBE', 'y', '\xC7', '\xA', '\x26', '\xB6', '\xEE', '\xD9', '\x9A', '\x60', '\x14', '\x93', '\xDB', '\x8F', '\xD',
'\xA', '\x2E', '\xE9', '\x23', '\x95', '\x29', 'X', '\x0', '\x27', '\xEB', 'n', 'V', 'p', '\xBC', '\xD6', '\xCB',
'\xD6', 'G', '\xAB', '\x3D', 'l', '\x7D', '\xB8', '\xD2', '\xDD', '\xA0', '\x60', '\x83', '\xBA', '\xEF', '\x5F', '\xA4',
'\xEA', '\xCC', '\x2', 'N', '\xAE', '\x5E', 'p', '\x1A', '\xEC', '\xB3', '\x40', '9', '\xAC', '\xFE', '\xF2', '\x91',
'\x89', 'g', '\x91', '\x85', '\x21', '\xA8', '\x87', '\xB7', 'X', '\x7E', '\x7E', '\x85', '\xBB', '\xCD', 'N', 'N',
'b', 't', '\x40', '\xFA', '\x93', '\x89', '\xEC', '\x1E', '\xEC', '\x86', '\x2', 'H', '\x26', '\x93', '\xD0', 'u',
'\x1D', '\x7F', '\x9', '2', '\x95', '\xBF', '\x1F', '\xDB', '\xD7', 'c', '\x8A', '\x1A', '\xF7', '\x5C', '\xC1', '\xFF',
'\x22', 'J', '\xC3', '\x87', '\x0', '\x3', '\x0', 'K', '\xBB', '\xF8', '\xD6', '\x2A', 'v', '\x98', 'I', '\x0',
'\x0', '\x0', '\x0', 'I', 'E', 'N', 'D', '\xAE', 'B', '\x60', '\x82',
};
在网页中使用调试模式,打开连接 “localhost:8000/”,显示界面、两次请求如下。标签页的图标正常显示,而且长度在请求中也是555字节。
控制台结果实际为3次请求,控制台打印的解析请求头与web端截图完全一致。
20210812 13:46:00.293944Z 1467 WARN HttpServer[dummy] starts listenning on 0.0.0.0:8000 - HttpServer.cc:53
20210812 13:46:25.036289Z 1467 INFO TcpServer::newConnection [dummy] - new connection [dummy-0.0.0.0:8000#1] from 127.0.0.1:1071 - TcpServer.cc:80
20210812 13:46:25.036585Z 1467 INFO TcpServer::newConnection [dummy] - new connection [dummy-0.0.0.0:8000#2] from 127.0.0.1:1072 - TcpServer.cc:80
Headers GET /
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
DNT: 1
Host: localhost:8000
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
20210812 13:46:25.301348Z 1467 INFO TcpServer::newConnection [dummy] - new connection [dummy-0.0.0.0:8000#3] from 127.0.0.1:1073 - TcpServer.cc:80
关闭标签页,控制台无任何反应,关闭浏览器之后,3个连接断开
20210812 13:47:29.260650Z 1467 INFO TcpServer::removeConnectionInLoop [dummy] - connection dummy-0.0.0.0:8000#2 - TcpServer.cc:109
20210812 13:47:29.260789Z 1467 INFO TcpServer::removeConnectionInLoop [dummy] - connection dummy-0.0.0.0:8000#1 - TcpServer.cc:109
20210812 13:47:29.260821Z 1467 INFO TcpServer::removeConnectionInLoop [dummy] - connection dummy-0.0.0.0:8000#3 - TcpServer.cc:109