项目介绍背景介绍主要模块介绍
1、基于muduo网络库的WebServer:2、HTTP协议栈3、JsonServer:4、SQLConnection5、main模块 实现过程
1、确定如何实现一个HTTPWebServer2、手动实现HTTP协议栈3、结合HTTP协议栈与muduo网络库实现HTTPWebServer4、JsonServer的实现:5、SQLConnection的实现:6、onRequest回调函数/简易cgi的实现:7、main函数的实现: 结果展示总结参考
项目介绍项目github:github链接
本项目基于C++语言、陈硕老师的muduo网络库、mysql数据库以及jsoncpp,服务器监听两个端口,一个端口用于处理http请求、另一个端口用于处理发送来的json数据。
此项目是配合一个qt+opencv车牌识别前端程序,识别出车牌后打包为json数据发送给后端服务器保存在mysql数据库中并且显示在网页上。
本项目为个人学习所编写的小项目,没有非常细致地考虑到非常完整的HTTP协议栈等内容。可只监听80号端口使用POST方法来传输JSON数据,但是考虑到信息来源不一定实现了HTTP的POST协议,因此另监听一个端口只传输JSON数据比较方便。
主要模块介绍 1、基于muduo网络库的WebServer:该WebServer基于muduo网络库与HTTP协议栈,muduo网络库主要负责socket连接的管理以及数据的管理,而在连接建立时,在连接的上下文context中初始化HTTPcontext,然后在接收到HTTP请求时,通过调用onMessage回调函数将接收到的数据保存在连接的HTTPcontext中并且处理HTTP请求,然后通过onRequest回调函数根据请求生成HTTP响应,在哈希表(与数据库同步)中获取所有车牌返回对应的HTML页面。
2、HTTP协议栈HTTP协议栈主要包括HTTPContext, HTTPRequest, HTTPResponse类
(1) HTTPContext类:主要用于逐行处理、储存HTTPRequest(HTTP请求),然后该类再储存在muduo的TCPConnection(连接类)中供回调函数使用,处理HTTP请求,可以避免TCP粘包的情况。
(2) HTTPRequest类:利用枚举类型储存HTTP请求方法,hashmap来储存一个连接的HTTP头部,string来储存请求的body。
(3) HTTPResponse类:默认返回400 NotFound,需要在WebServer中设置HTTP的onRequest回调,手动根据HTTP请求构造HTTP响应,需要设置状态码、状态码描述、响应头部、实体。
(1) JsonServer负责监听一个端口,在建立连接后,收到数据时,直接解析该Json是否完整,不完整就继续监听端口,并且将不完整的Json数据储存于连接的BUFFER中,在解析完整的Json表单后将BUFFER清空即可。
(2) 该server还包含一个SQLConnection,在解析到正确的Json后将车牌与ID储存于MySQL数据库中即可。
通过RAII的手法控制SQL连接,保证SQL连接正确的生命周期,并且实现query、insert成员函数实现查询与插入的功能。
5、main模块根据需求设置两个server的IP地址、端口、HTTP的onRequest的回调函数(生成符合需求的HTTP响应,可以理解为简易的cgi)
实现过程 1、确定如何实现一个HTTPWebServer在刚开始项目时,需要确定如何用C++语言实现一个简单的HTTPWebServer,可以用基础的socket编程+手动实现HTTP协议栈从底层实现;可以使用现成的WebServer,如lighttpd、apache、nginx等服务器+cgi程序实现。但是最终我选择使用陈硕老师的muduo网络库+手动实现HTTP协议栈从应用层面实现一个WebServer,原因如下:首先之前我自己就已经从底层实现过一个WebServer(github:tinyWebServer),使用的是非阻塞IO模型+reactor模拟proactor的事件处理模式,因此并不想copy代码实现该项目,其次muduo网络库有非常多值得学习的细节,例如如何正确高效地管理socket连接、不同于我之前实现WebServer的模型,muduo网络库使用主reactor+从reactor,per thread one loop的模型的事件处理模式。
2、手动实现HTTP协议栈PS:此处参考了nearXDU大佬的文章
实现一个简易HTTPWebServer光利用muduo网络库只能负责管理连接、数据,而实际应用层的HTTP协议栈需要自己实现,因此设计了HTTPContext, HTTPRequest, HTTPResponse。HTTPContext是每一个TCP连接对应的一次HTTP请求的一个上下文,包括请求,储存于muduo::net::TCPConnection的context中。HTTP解析的详细实现不必多说,HTTP协议是基础中的基础,代码中主要利用了C++内置的处理字符串的方法来解析HTTP请求,并利用unordered_map将解释出来的请求信息储存于HTTPRequest类的缓存中,而HTTPResponse则是专门为muduo网络库所设计,在设置好HTTP响应的请求行、请求头、实体后调用其appendToBuffer的成员函数然后从muduo网络库发送响应。代码如下:
httpcontext.h
#ifndef HTTPCONTEXT_H #define HTTPCONTEXT_H #include#include #include #include "httprequest.h" using namespace std; using namespace muduo; using namespace muduo::net; class HttpContext { public: enum HttpRequestParseState { kExpectRequestLine, kExpectHeaders, kExpectBody, kGotAll }; HttpContext():state_(kExpectRequestLine) { } bool parseRequest(Buffer *buf, Timestamp receiveTime); bool gotAll() const { return state_ == kGotAll; } void reset() { state_ = kExpectRequestLine; HttpRequest dummy; request_.swap(dummy); } const HttpRequest& request() const { return request_; } private: //解析请求行 bool processRequestLine(const char* begin, const char* end); private: HttpRequestParseState state_; //解析结果保存在request_成员中 HttpRequest request_; }; #endif // HTTPCONTEXT_H
httpcontext.cpp:
#include "httpcontext.h"
//解析请求行
bool HttpContext::processRequestLine(const char *begin, const char *end)
{
bool succeed = false;
const char *start = begin;
const char *space = find(start, end, ' ');
//设置请求方法 method_
if(space != end && request_.setMethod(start, space))
{
start = space + 1;
space = find(start, end, ' ');
if(space != end)
{
//解析URI
const char *question = find(start, space, '?');
if(question != space)
{
request_.setPath(start, question);
request_.setQuery(question, space);
}
else
{
request_.setPath(start, space);
}
//解析HTTP版本号
start = space + 1;
succeed = end-start == 8 && equal(start, end-1, "HTTP/1.");
if(succeed)
{
if(*(end-1) == '1')
{
request_.setVersion(HttpRequest::HTTP11);
}
else if(*(end-1) == '0')
{
request_.setVersion(HttpRequest::HTTP10);
}
else
{
succeed = false;
}
}
}
}
return succeed;
}
//解析请求头
bool HttpContext::parseRequest(Buffer *buf, Timestamp receiveTime)
{
bool ok = true;
bool hasMore = true;
while(hasMore)
{
//解析请求行
if(state_ == kExpectRequestLine)
{
const char *crlf = buf->findCRLF();
if(crlf)
{
//开始解析请求行
ok = processRequestLine(buf->peek(), crlf);
if(ok)
{
//解析成功
request_.setReceiveTime(receiveTime);
//回收请求行buffer
buf->retrieveUntil(crlf+2);
state_ = kExpectHeaders;
}
else
{
hasMore = false;
}
}
else
{
hasMore = false;
}
}
//解析请求头
else if(state_ == kExpectHeaders)
{
const char *crlf = buf->findCRLF();
if(crlf)
{
//冒号
const char *colon = find(buf->peek(), crlf, ':');
if(colon != crlf)
{
request_.addHeader(buf->peek(), colon, crlf);
}
else
{
//empty line, end of header
//FIXME:
state_ = kGotAll;
hasMore = false;
}
buf->retrieveUntil(crlf+2);//回收
}
else
{
hasMore = false;
}
}
else if(state_ == kExpectBody)
{
cout << "HttpContext: parse body" << endl;
}
}//end while
return ok;
}
httprequest.h
#ifndef HTTPREQUEST_H #define HTTPREQUEST_H #include#include #include #include using namespace std; using namespace muduo; class HttpRequest { public: enum Method { INVALID, GET, POST, HEAD, PUT, DELETe }; enum Version { UNKNOWN, HTTP10, HTTP11 }; HttpRequest(); void setVersion(Version v); Version getVersion() const; bool setMethod(const char *start, const char *end); Method method() const; const char* methodString() const; void setPath(const char* start, const char* end); const string& path() const; void setQuery(const char *start, const char *end); const string& query() const; void setReceiveTime(Timestamp t); Timestamp receiveTime() const; void addHeader(const char *start, const char *colon, const char *end); string getHeader(const string &field) const; const unordered_map & headers() const; void swap(HttpRequest& that); private: Method method_; Version version_; string path_; string query_; Timestamp receiveTime_; unordered_map headers_; }; #endif // HTTPREQUEST_H
httprequest.cpp
#include "httprequest.h"
HttpRequest::HttpRequest():method_(INVALID),version_(UNKNOWN)
{
}
void HttpRequest::setVersion(HttpRequest::Version v)
{
version_ = v;
}
HttpRequest::Version HttpRequest::getVersion() const
{
return version_;
}
bool HttpRequest::setMethod(const char *start, const char *end)
{
assert(method_ == INVALID);
string m(start,end);
if(m == "GET")
{
method_ = GET;
}
else if(m == "POST")
{
method_ = POST;
}
else if(m == "HEAD")
{
method_ = HEAD;
}
else if(m == "PUT")
{
method_ = PUT;
}
else if(m == "DELETE")
{
method_ = DELETE;
}
else
{
method_ = INVALID;
}
return method_ != INVALID;
}
HttpRequest::Method HttpRequest::method() const
{
return method_;
}
const char *HttpRequest::methodString() const
{
const char *result = "UNKNOWN";
switch(method_)
{
case GET:
result = "GET";
break;
case POST:
result = "POST";
break;
case HEAD:
result = "HEAD";
break;
case PUT:
result = "PUT";
break;
case DELETE:
result = "DELETE";
break;
default:
break;
}
return result;
}
void HttpRequest::setPath(const char *start, const char *end)
{
path_.assign(start,end);
}
const string &HttpRequest::path() const
{
return path_;
}
void HttpRequest::setQuery(const char *start, const char *end)
{
query_.assign(start,end);
}
const string &HttpRequest::query() const
{
return query_;
}
void HttpRequest::setReceiveTime(Timestamp t)
{
receiveTime_ = t;
}
Timestamp HttpRequest::receiveTime() const
{
return receiveTime_;
}
void HttpRequest::addHeader(const char *start, const char *colon, const char *end)
{
string field(start,colon);
++colon;
while(colon < end && isspace(*colon))
++colon;
string value(colon,end);
while(!value.empty() && isspace(value[value.size()-1]))
value.resize(value.size()-1);
headers_[field] = value;
}
string HttpRequest::getHeader(const string &field) const
{
string result;
unordered_map::const_iterator it = headers_.find(field);
if(it != headers_.end())
result = it->second;
return result;
}
const unordered_map &HttpRequest::headers() const
{
return headers_;
}
void HttpRequest::swap(HttpRequest &that)
{
std::swap(method_, that.method_);
path_.swap(that.path_);
query_.swap(that.query_);
receiveTime_.swap(that.receiveTime_);
headers_.swap(that.headers_);
}
httpresponse.h
#ifndef HTTPRESPONSE_H #define HTTPRESPONSE_H #include#include #include #include using namespace std; using namespace muduo; using namespace muduo::net; class HttpResponse { public: enum HttpStatusCode { CODE_UNKNOWN, CODE_200 = 200, CODE_301 = 301, CODE_400 = 400, CODE_404 = 404 }; explicit HttpResponse(bool close):statusCode_(CODE_UNKNOWN),closeConnection_(close) { } void setStatusCode(HttpStatusCode code); void setStatusMessage(const string &message); void setCloseConnection(bool on); bool closeConnction() const; void setContentType(const string &contentType); void addHeader(const string &key, const string &value); void setBody(const string &body); void appendToBuffer(Buffer *output) const; private: //响应头 unordered_map headers_; //响应码 HttpStatusCode statusCode_; //状态信息 string statusMessage_; //是否keep_alive bool closeConnection_; //响应报文 string body_; }; #endif // HTTPRESPONSE_H
httpresponse.cpp
#include "httpresponse.h"
void HttpResponse::setStatusCode(HttpResponse::HttpStatusCode code)
{
statusCode_ = code;
}
void HttpResponse::setStatusMessage(const string &message)
{
statusMessage_ = message;
}
void HttpResponse::setCloseConnection(bool on)
{
closeConnection_ = on;
}
bool HttpResponse::closeConnction() const
{
return closeConnection_;
}
void HttpResponse::setContentType(const string &contentType)
{
addHeader("Content-Type", contentType);
}
void HttpResponse::addHeader(const string &key, const string &value)
{
headers_[key] = value;
}
void HttpResponse::setBody(const string &body)
{
body_ = body;
}
void HttpResponse::appendToBuffer(Buffer *output) const
{
char buf[32];
//构造响应行
snprintf(buf, sizeof(buf), "HTTP/1.1 %d ", statusCode_);
output->append(buf);
output->append(statusMessage_);
output->append("rn");
if(closeConnection_)
{
output->append("Connection: closern");
}
else
{
//Keep-Alive需要Content-Length
snprintf(buf, sizeof(buf), "Content-Length: %zdrn", body_.size());
output->append(buf);
output->append("Connection: Keep-Alivern");
}
for(auto it = headers_.begin(); it != headers_.end(); ++it)
{
output->append(it->first);
output->append(": ");
output->append(it->second);
output->append("rn");
}
output->append("rn");
//响应报文
output->append(body_);
}
3、结合HTTP协议栈与muduo网络库实现HTTPWebServer
PS:可以在此注册定时器来主动关闭超时链接
muduo网络库是基于对象的,基于其设计服务器需要基于对象,在Server类中包含muoduo::net::TcpServer,然后注册连接、接收、断开等时刻的回调函数实现服务器的基础功能,主要HTTP设计代码如下:
httpserver.h
#ifndef HTTPSERVER_H #define HTTPSERVER_H #include#include #include #include #include #include #include #include "httpcontext.h" #include "httprequest.h" #include "httpresponse.h" using namespace std; using namespace muduo; using namespace muduo::net; class HttpServer { public: //http回调函数 typedef function HttpCallback; //构造、析构函数 explicit HttpServer(EventLoop* loop,const InetAddress& listenAddr); ~HttpServer(); EventLoop* getLoop() const { return server_.getLoop(); } void setHttpCallback(const HttpCallback& cb) { httpCallback_ = cb; } void setThreadNum(const int numThreads) { server_.setThreadNum(numThreads); } void start(); private: void onConnection(const TcpConnectionPtr &conn); void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp receiveTime); void onRequest(const TcpConnectionPtr &conn,const HttpRequest&); private: TcpServer server_; HttpCallback httpCallback_; }; #endif // HTTPSERVER_H
httpserver.cpp
#include "httpserver.h"
void defaultHttpCallback(const HttpRequest&,HttpResponse* resp)
{
resp->setStatusCode(HttpResponse::CODE_400);
resp->setStatusMessage("Not Found");
resp->setCloseConnection(true);
}
HttpServer::HttpServer(EventLoop *loop, const InetAddress &listenAddr):server_(loop, listenAddr, "wyeHttpServer"), httpCallback_(defaultHttpCallback)
{
server_.setConnectionCallback(std::bind(&HttpServer::onConnection, this, placeholders::_1));
server_.setMessageCallback(std::bind(&HttpServer::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3));
}
HttpServer::~HttpServer()
{
}
void HttpServer::start()
{
LOG_WARN << "HttpServer[" << server_.name() << "] starts listenning on " << server_.ipPort();
server_.start();
}
//新连接回调
void HttpServer::onConnection(const TcpConnectionPtr &conn)
{
if(conn->connected())
{
conn->setContext(HttpContext());
}
}
//消息回调
void HttpServer::onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp receiveTime)
{
HttpContext *context = boost::any_cast(conn->getMutableContext());
//解析请求
if(!context->parseRequest(buf, receiveTime))
{
conn->send("HTTP/1.1 400 Bad Requestrnrn");
conn->shutdown();
}
if(context->gotAll())
{
//请求解析完毕
onRequest(conn, context->request());
context->reset();
}
}
void HttpServer::onRequest(const TcpConnectionPtr &conn, const HttpRequest &req)
{
const string &connection = req.getHeader("Connection");
bool close = connection == "close" || (req.getVersion() == HttpRequest::HTTP10 && connection != "Keep-Alive");
HttpResponse response(close);//构造响应
httpCallback_(req, &response);
Buffer buf;
//此时response已经构造好,将向客户发送response添加到buffer中
response.appendToBuffer(&buf);
conn->send(&buf);
//如果非Keep-Alive就关闭
if(response.closeConnction())
{
conn->shutdown();
}
}
4、JsonServer的实现:
解析Json有很多方法,最初想使用Boost库来解析,但是在1.75版本前解析Json需要用到boost::property_tree,操作较为复杂,一段优秀的代码应该是使用更好的轮子写出来的,并且会是通俗易懂的。因此推荐,并且我也使用了Jsoncpp库来解析Json数据。然后JsonServer中还包含了SQLConnection,用于储存解析的Json数据到MySQL中。而具体实现还是基于muduo网络库,与HTTPServer类的设计大同小异,主要Json服务器设计代码如下:
jsonprocess.h
#ifndef JSONPROCESS_H #define JSONPROCESS_H #include#include #include #include #include #include
jsonprocess.cpp
#include "jsonprocess.h" #include5、SQLConnection的实现:JsonProcess::JsonProcess(EventLoop *loop, const InetAddress &listenAddr, map > &mp):server_(loop, listenAddr, "JsonProcess"), jsonMap(mp),totalId(0) { server_.setMessageCallback(std::bind(&JsonProcess::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3)); server_.setConnectionCallback(std::bind(&JsonProcess::onConnection, this, placeholders::_1)); sqlConnection_.connectToSqlServer(); sqlConnection_.query(jsonMap, totalId); // for(auto it=jsonMap.begin(); it!=jsonMap.end(); ++it) // { // st.insert(it->second); // } } void JsonProcess::start() { LOG_INFO << "HttpServer[" << server_.name() << "] starts listenning on " << server_.ipPort(); server_.start(); } void JsonProcess::onConnection(const TcpConnectionPtr &conn) { LOG_INFO << "New JsonProcess Connection: " << conn->peerAddress().toIpPort(); } void JsonProcess::onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp t) { LOG_INFO << "New Json Message"; parseJson(buf, conn->peerAddress().toIp()); } void JsonProcess::parseJson(Buffer *buf, string ip) { const char *str = buf->peek(); //不处理粘包情况 FIX ME: string jsonStr(str, str+buf->readableBytes()); //解析json Json::Value jsonRoot; Json::Reader jsonReader; if(!jsonReader.parse(jsonStr, jsonRoot)) { LOG_WARN << "Json Message is not completed"; return ; } // int id = jsonRoot["id"].asInt(); //弃置 int id = totalId++; string license_plate = jsonRoot["license_plate"].asString(); LOG_INFO << "parse json result:" << id << ":" << license_plate; //查重 // if(st.find(license_plate) != st.end()) // { // LOG_WARN << "license already existed!!"; // buf->retrieveAll(); // return ; // } // else // st.insert(license_plate); long long myTime = Timestamp::now().secondsSinceEpoch(); Timestamp t((myTime+28800)*1e6); //插入到mysql if(!sqlConnection_.insert(id, license_plate, myTime, ip)) { LOG_WARN << "insert to sql fail!!"; return ; } //插入到map jsonMap[id] = vector {license_plate, t.toFormattedString(), ip}; LOG_INFO << id << ":" << license_plate; buf->retrieveAll(); }
主要用得到RAII的手段,保证SQL连接的生命周期,查询、插入操作则是很标准的C++SQL操作,SQLConnection主要代码如下:
sqlconnection.h
#ifndef SQLCONNECTION_H #define SQLCONNECTION_H #include
sqlconnection.cpp
#include "sqlconnection.h"
SqlConnection::SqlConnection(string server, string user, string password, string database):server_(server), user_(user), password_(password), database_(database), isConnected_(false)
{
conn_ = mysql_init(NULL);
}
SqlConnection::~SqlConnection()
{
disconnectFromSqlServer();
}
bool SqlConnection::connectToSqlServer()
{
if(!mysql_real_connect(conn_, server_.c_str(), user_.c_str(), password_.c_str(), database_.c_str(), 0, NULL, 0))
{
LOG_WARN << mysql_error(conn_);
return false;
}
if(mysql_query(conn_, "use licensePlate;"))
{
LOG_WARN << mysql_error(conn_);
return false;
}
if(mysql_query(conn_, "set names utf8"))
{
LOG_WARN << mysql_error(conn_);
return false;
}
if(mysql_query(conn_, "select * from cars;"))
{
LOG_WARN << mysql_error(conn_);
return false;
}
res = mysql_store_result(conn_); //深拷贝
cols = mysql_num_fields(res);
mysql_free_result(res);
LOG_WARN << "Connect to MYSQL";
return true;
}
bool SqlConnection::query(map> &mp, int& totalId)
{
if(mysql_query(conn_, "SELECt id,license_plate,unix_timestamp(time),ip FROM cars;"))
{
LOG_WARN << mysql_error(conn_);
return false;
}
res = mysql_store_result(conn_); //深拷贝
// int rows = mysql_num_rows(res);
while((row = mysql_fetch_row(res)) != NULL)
{
int id;
string license_plate;
Timestamp myTime;
string ip;
for(int i=0;i{license_plate, myTime.toFormattedString(), ip};
totalId = ++id;
}
mysql_free_result(res);
return true;
}
bool SqlConnection::insert(int id, string license_plate, long long myTime, string ip)
{
// time_t myTime = Timestamp::now().secondsSinceEpoch();
string sqlInsert = "insert into cars(license_plate,time,ip) values("";
sqlInsert += license_plate;
sqlInsert += "",FROM_UNIXTIME(";
sqlInsert += to_string(myTime);
sqlInsert += "),"";
sqlInsert += ip;
sqlInsert += "");";
if(mysql_query(conn_, sqlInsert.c_str()))
{
LOG_WARN << mysql_error(conn_);
return false;
}
return true;
}
void SqlConnection::disconnectFromSqlServer()
{
mysql_free_result(res);
mysql_close(conn_);
LOG_WARN << "Disconnect from sqlserver";
}
6、onRequest回调函数/简易cgi的实现:
在JsonWebServer初始化阶段会将本地内存的车牌哈希表进行相应的初始化,而新来的Json数据则会更新本地哈希表。然后具体onRequest回调函数中,生成的HTML页面会将本地哈希表的所有车牌插入其中,实现将数据库的车牌显示到浏览器网页的功能,并且使用unordered_set来进行去重操作。
回调函数(简易cgi):
void onRequest(const HttpRequest& req, HttpResponse *resp)
{
if(req.method() != HttpRequest::GET)
{
resp->setCloseConnection(true);
resp->setStatusCode(HttpResponse::CODE_400);
resp->setStatusMessage("Bad Request");
return ;
}
string body;
ifstream inFile;
string path = req.path();
int it = path.find('.');
if(it != string::npos)
{
inFile.open("beijing.jpg", ios_base::in | ios_base::binary);
resp->setContentType("image/jpg");
if(!inFile.is_open())
{
resp->setStatusCode(HttpResponse::CODE_404);
resp->setStatusMessage("Not Found");
resp->setCloseConnection(true);
return ;
}
char buf[1024];
memset(buf, 0, sizeof(buf));
while(!inFile.eof())
{
inFile.read(buf,sizeof(buf));
body += string(buf,buf+sizeof(buf));
memset(buf, 0, sizeof(buf));
}
inFile.close();
}
else
{
body += "车牌识别系统 车牌识别系统
| id号 | 车牌号码 | 时间 | IP地址 |
|---|---|---|---|
| ") + to_string(idx++) + " | "; body += string("") + (it->second)[0] + " | "; body += string("") + (it->second)[1] + " | "; body += string("") + (it->second)[2] + " | "; body += "
- ";
// for(auto it=globalMap.begin();it!=globalMap.end();++it)
// {
// body += string("") + (it->second).first + " " + (it->second).second + "";
// }
// body += "
根据muduo网络库的要求,创建loop对象,绑定loop对象到服务器类中,然后根据需求设置两个server的IP地址、端口、HTTP的onRequest的回调函数(生成符合需求的HTTP响应,可以理解为简易的cgi),最后start服务器并且启动loop即可。
main.cpp
#include结果展示#include #include #include
本次分享的小项目使用了muduo网络库,C++的网络库本来就比较稀有,仅有muduo、boost.Asio、libevent、ACE这几个,而实际使用muduo网络库来编写的项目就非常稀少,因此这次这个项目可以有抛砖引玉的作用,为有兴趣研究网络编程的同学提供一个以muduo网络库为基础的项目实例。
参考《Linux多线程服务端编程:使用muduo C++网络库》 ——陈硕
从零开始学写HTTP服务器(六)使用muduo网络库



