班级网站建设组织机构,沈阳成创网站建设公司,经典重庆论坛新闻评论,网站教程分享添加一个简单的静态HTTP。
这里默认读者是熟悉http协议的。
来看看http请求Request的例子
客户端发送一个HTTP请求到服务器的请求消息#xff0c;其包括#xff1a;请求行、请求头部、空行、请求数据。 HTTP之响应消息Response
服务器接收并处理客户端发过来的请求后会返…添加一个简单的静态HTTP。
这里默认读者是熟悉http协议的。
来看看http请求Request的例子
客户端发送一个HTTP请求到服务器的请求消息其包括请求行、请求头部、空行、请求数据。 HTTP之响应消息Response
服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息其包括状态行、消息报头、空行和响应正文。 前面所说的就是http的请求和响应答复。那我们可以封装出两个类。
HttpRequesthttp请求类封装
HttpResponsehttp响应类封装
注意这里会使用到我们之前写的Buffer类。因为服务器是把读到的数据存储在Buffer中的所以大家要熟悉Buffer类的一些用法。
1、HttpRequest 类
该类的主要作用是客户端发送请求服务端收到的数据存放于Buffer中之后解析成HttpRequest请求对象调用成员函数设置请求头、请求体等。
首先会有请求方式method_,http版本version_,请求头headers_用map管理)。请求的路径path_(即是url),还有请求体query_。
请求体有可能是在url中的?后面也可能是在请求头后面的。
class HttpRequest
{
public:enum class Method{kInvalid, kGet, kPost, kHead, kPut, kDelete};enum class Version{kUnknown, kHttp10, kHttp11};HttpRequest():method_(Method::kInvalid),version_(Version::kUnknown){}void setVersion(Version v) { version_ v; }Version getVersion()const { return version_; }bool setMethod(const char* start, const char* end){string m(start, end);if (m GET) {method_ Method::kGet;}else if (m POST) {method_ Method::kPost;}//省略HEAD,DELETE等等方式......return method_ ! Method::kInvalid;}Method getMothod()const { return method_; }const char* methodString()const {const char* result UNKNOWN;switch (method_) {case Method::kGet:result GET;break;case Method::kPost:result POST;break;//省略HEAD,DELETE等等方式......}return result;}void setPath(const char* start, const char* end) {path_.assign(start, end);}const string path()const { return path_; }void setQuery(const char* start, const char* end) {query_.assign(start, end);}const string query()const { return query_; }void addHeader(const char* start, const char* colon, const char* end){//isspace(int c)函数判断字符c是否为空白符//说明当c为空白符时返回非零值否则返回零。空白符指空格、水平制表、垂直制表、换页、回车和换行符。// 要求冒号前无空格string field(start, colon);colon;while (colon end isspace(*colon))// 过滤冒号后的空格colon;string value(colon, end);while (!value.empty() isspace(value[value.size() - 1]))//过滤value中的空格value.resize(value.size() - 1);headers_[field] value;}string getHeader(const string field)const{string result;auto it headers_.find(field);if (it ! headers_.end()) {return it-second;}return result;}const std::unordered_mapstring, string headers()const { return headers_; }private:Method method_;Version version_;string path_; //请求路径string query_; //请求体std::unordered_mapstring, string headers_;
};
注意添加请求头时函数addHeader需要删除键值对的字符串左侧和右侧的空字符保证解析正常。因为解析请求头时对一行字符串用冒号“:”进行分割解析。
2、HttpResponse 类
服务器端得到的客户的请求信息后再创建一个HttpResponse响应对象也是会调用成员函数设置响应头部、响应体并格式化到Buffer中回复给客户端。
按照上面的响应例子那应该有响应头headers_,响应的状态码statusCode_,状态码的文字描述statusMessage_响应体body_等等。
成员函数就是一些设置状态码设置响应头等操作。
class HttpResponse
{
public:enum class HttpStatusCode{kUnknown,k200Ok 200,k301MovedPermanently 301,k400BadRequest 400,k404NotFound 404,};explicit HttpResponse(bool close):statusCode_(HttpStatusCode::kUnknown),closeConnection_(close){}void setStatusCode(HttpStatusCode code) { statusCode_ code; }void setstatusMessage(const string message) { statusMessage_ message; }void setCloseConnection(bool on) { closeConnection_ on; }bool closeConnection()const { return closeConnection_; }void setContentType(const string contentType) { addHeader(Content-Type, contentType); }void addHeader(const string key, const string value) {headers_[key] value;}void setBody(const string body) { body_ body; }void appendToBuffer(Buffer* output)const;private:std::unordered_mapstring, string headers_;HttpStatusCode statusCode_; //状态码string statusMessage_; //响应行中的状态码文字描述bool closeConnection_; //是否关闭连接string body_; //响应体
};
这里特别值得一说的是如何把响应消息格式化的操作格式化appendToBuffer(Buffer* output)。
该函数默认使用HTTP1.1版本按照HTTP协议对HttpResponse对象进行格式化输出到Buffer中。
按照要求添加响应行响应头空行响应体。
void HttpResponse::appendToBuffer(Buffer* output) const
{//响应行string buf HTTP/1.1 std::to_string(static_castint(statusCode_));output-append(buf);output-append(statusMessage_);output-append(\r\n);//响应头部if (closeConnection_) {output-append(Connection: close\r\n);}else {output-append(Connection: Keep-Alive\r\n);buf Content-Length: std::to_string(body_.size()) \r\n;output-append(buf);}for (const auto header : headers_) {buf header.first : header.second \r\n;output-append(buf);}output-append(\r\n); //空行output-append(body_); //响应体
}
3、HttpContext 类
服务端接收客户请求存在Buffer中那怎么从Buffer中解析得到我们想要的信息呢。这时需要一个解析类HttpContext解析后数据封装到回复HttpRequest中。
其成员有处理的状态state_响应request_。
class HttpContext
{
public:enum class HttpRequestPaseState{kExpectRequestLine, //请求行kExpectHeaders, // 请求头kExpectBody, // 请求体kGotAll, //表示都处理完全};HttpContext():state_(HttpRequestPaseState::kExpectRequestLine)//默认从请求行开始解析{}bool parseRequest(Buffer* buf);// 解析请求Bufferbool gotAll()const { return state_ HttpRequestPaseState::kGotAll; }void reset()// 为了复用HttpContext{state_ HttpRequestPaseState::kExpectRequestLine;HttpRequest dumy;request_.swap(dumy);}const HttpRequest request() const{ return request_; }HttpRequest request(){ return request_; }private:bool processRequestLine(const char* begin, const char* end);HttpRequestPaseState state_; //需要处理的状态,状态机HttpRequest request_;
};
一个正常的请求一般至少是有请求行的默认解析状态为kExpectRequestLine。
这里就主要关注是如何解析Buffer的。
3.1、请求解析 parseRequest(Buffer* buf)
这里为了方便找到buf中的\r\n添加了Buffer::findCRLF()函数。
const char Buffer::kCRLF[] \r\n;//为了方便解析http \r\n位置
const char* findCRLF()const {const char* crlf std::search(peek(), beginWirte(), kCRLF, kCRLF 2);return crlf beginWirte() ? nullptr : crlf;
}
传入需要解析的Buffer对象根据期望解析的部分(即是状态state_)进行处理。
处理就三种情况请求行请求头请求体。具体的流程可以看代码
bool HttpContext::parseRequest(Buffer* buf)
{bool ok true;bool hasMore true;while (hasMore) {if (state_ HttpRequestPaseState::kExpectRequestLine) { //解析请求行//查找出buf中第一次出现\r\n位置const char* crlf buf-findCRLF();if (crlf) {//若是找到\r\n,说明至少有一行数据可以进行解析//buf-peek()为数据开始部分ok processRequestLine(buf-peek(), crlf);if (ok) {//解析成功buf-retrieveUntil(crlf 2);//buf-peek()向后移动2字节到下一行state_ HttpRequestPaseState::kExpectHeaders;}else {hasMore false;}}else {hasMore false;}}else if (state_ HttpRequestPaseState::kExpectHeaders) {const char* crlf buf-findCRLF(); //找到\r\n位置if (crlf) {const char* colon std::find(buf-peek(), crlf, :);//定位分隔符if (colon ! crlf) {request_.addHeader(buf-peek(), colon, crlf); //添加键值对}else {/*state_ HttpRequestPaseState::kGotAll;hasMore false;*/state_ HttpRequestPaseState::kExpectBody;//这样就可以解析body}buf-retrieveUntil(crlf 2); //后移动2字节}else {hasMore false;}}else if (state_ HttpRequestPaseState::kExpectBody) {//解析请求体if (buf-readableBytes()) {//表明还有数据那就是请求体request_.setQuery(buf-peek(), buf-beginWirte());}state_ HttpRequestPaseState::kGotAll;hasMore false;}}return ok;
}3.1、请求行的解析 processRequestLine()
请求行有固定格式Method URL Version \r\nURL中可能带有请求参数。根据空格符进行分割成三段字符。URL可能带有请求参数使用?”分割解析。
bool HttpContext::processRequestLine(const char* begin, const char* end)
{bool succeed true;const char* start begin;const char* space std::find(start, end, );//第一个空格前的字符串是请求方法 例如postif (space ! end request_.setMethod(start, space)) {start space 1;space std::find(start, end, );//寻找第二个空格 urlif (space ! end) {const char* question std::find(start, space, ?);if (question ! space) {// 有?分割成path和请求参数request_.setPath(start, question);request_.setQuery(question, space);}else {request_.setPath(start, space);//只有path}//最后一部分解析http协议版本string version(space 1, end);if (version HTTP/1.0)request_.setVersion(HttpRequest::Version::kHttp10);else if (version HTTP/1.1)request_.setVersion(HttpRequest::Version::kHttp11);elsesucceed false;}}return succeed;
}这样解析就完成了。
4、HttpServer类
为了可以方便使用封装个HttpServer类。
该类内部会有Server类型成员并提供了一个回调函数的接口当服务器收到http请求时调用客户端的处理函数进行处理。
HttpServer支持多线程也可以使用单线程。
class HttpServer
{
public:using HttpCallback std::functionvoid(const HttpRequest, HttpResponse*);HttpServer(EventLoop* loop, const InetAddr listenAddr);void setHttpCallback(const HttpCallback cb) { httpCallback_ cb; }void start(int numThreads);private:void onConnetion(const ConnectionPtr conn); //连接到来的回调函数void onMessage(const ConnectionPtr conn, Buffer* buf); //消息到来的回调函数void onRequest(const ConnectionPtr conn, const HttpRequest);Server server_;HttpCallback httpCallback_;};
函数setHttpCallback就是设置用户的业务处理回调函数的。
4.1HttpServer构造函数
//默认的回调函数
void defaultHttpCallback(const HttpRequest req, HttpResponse* resp)
{resp-setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);resp-setstatusMessage(Not Found);resp-setCloseConnection(true);
}
//构造函数
HttpServer::HttpServer(EventLoop* loop, const InetAddr listenAddr):server_(listenAddr,loop), httpCallback_(defaultHttpCallback)
{//新连接到来回调该函数server_.setConnectionCallback([this](const ConnectionPtr conn) {onConnetion(conn); });//消息到来回调该函数 server_.setMessageCallback([this](const ConnectionPtr conn, Buffer* buf) {onMessage(conn, buf); });
}
这里就是初始化Server并将HttpServer的回调函数传给Server。主要有两个函数。
前面的HttpResponse类和HttpRequest类已经在HttpServer使用了但是解析类HttpContext还没有使用。
很容易想到是在回调函数中使用。在有消息到来的时候就会进行解析数据这时就会使用到HttpContext。可以在每次调用函数onMessage中创建HttpContext对象。这在短连接中使用是合适的。但是在长连接的情况下这样可能效率不高。
那么就可以在有新连接到来的时刻就设置好HttpContext。
那就说到onConnetion函数。
4.2 连接到来的回调函数onConnetion
//这里绑定一个HttpContext主要是为了长连接中仅分配一次对象提高效率。
void HttpServer::onConnetion(const ConnectionPtr conn)
{if (conn-connected()) {//conn-setContext(std::make_sharedHttpContext()); //c11的std::shared_ptrvoidconn-setContext(HttpContext()); //c17的std::any}
}
该函数为一个新的Connection绑定一个HttpContext对象绑定之后HttpContext就相当于Connection的成员TcpConection在MessageCallback中就可以随意的使用该HttpContext对象了。 这里绑定一个HttpContext主要是为了长连接中仅分配一次对象提高效率。
这里绑定使用的是c17的std::any。std::any表示可以接受任意类型的变量。
来看看Conntection类中需要添加的变量
#includeany
class Connection:public std::enable_shared_from_thisConnection
{
public://省略之前的变量和函数//void setContext(std::shared_ptrvoid context) { context_ context; }//std::shared_ptrvoid getConntext()const { return context_; }void setContext(const std::any context) { context_ context; }std::any* getMutableContext() { return context_; }
private://std::shared_ptrvoid context_; //c11做法std::any context_; //用来解析http或者websocket或者其他协议的
};首先我们要明确为什么要的是接收任意类型的变量这总做法为什么不是直接就是用HttpContext类替代std::any。
因为我们后续可能还需要解析其他协议的例如websockte协议(下一节会讲解)。要是直接写HttpContext的话那要解析websocket协议的时候Connection类中还需要添加websocketContext类成员变量这就很麻烦的。所以用std::any来就可以绑定所有的解析类。
那又有疑惑为什么不直接用void*?简单点说是它类型不安全还需要用户手动去delete。
std::shared_ptr和void*一样不能解决类型安全的问题。详细的了解可以查看该文章https://www.cnblogs.com/gnivor/p/12793239.html
那说完std::any和回调函数onConnetion那就到函数onMessage。
4.3 新消息到来的回调函数onMessage
void HttpServer::onMessage(const ConnectionPtr conn, Buffer* buf)
{//HttpContext* context reinterpret_castHttpContext*(conn-getConntext().get()); //c11做法auto context std::any_castHttpContext(conn-getMutableContext()); //c117if (!context) {LOG_ERRORcontext is bad\n;return;}if (!context-parseRequest(buf)) {conn-send(HTTP/1.1 400 Bad Request\r\n\r\n);conn-shutdown();}if (context-gotAll()) {onRequest(conn, context-request());context-reset();//一旦请求处理完毕重置context因为HttpContext和Connection绑定了我们需要解绑重复使用}
}当Connection中所拥有的连接有新消息到来时会调用它的messageCallback_函数其实就是调用HttpServer的onMessage()函数。而之前在函数onConnection()中把HttpContext利用std::any绑定给了Connection那在该函数中就可以对Connection使用HttpContext类来解析数据包了。
onMessage()函数首先调用HttpContext的parserRequset()函数解析请求判断请求是否合法进而选择关闭连接或者处理请求(函数onRequest。
4.4处理请求的函数onRequest
void HttpServer::onRequest(const ConnectionPtr conn, const HttpRequest req)
{const std::string connetion req.getHeader(Connection);bool close connetion close || (req.getVersion() HttpRequest::Version::kHttp10 connetion ! Keep-Alive);HttpResponse response(close);//执行用户注册的回调函数httpCallback_(req, response);Buffer buf;response.appendToBuffer(buf);conn-send(buf);//发送数据if (response.closeConnection()) {conn-shutdown();}
}
先判断是长连接还是短连接。接着使用close构造一个HttpResponse对象。之后很重要的是执行用户注册的回调函数这个就是用户的业务函数。
5.HtttpServer的用法
#includesrc/Server.h
//省略一些其他头文件//用户的业务处理的函数
void onRequest(const HttpRequest req, HttpResponse* resp)
{if (req.path() /) {// 根目录请求resp-setStatusCode(HttpResponse::HttpStatusCode::k200Ok);resp-setstatusMessage(OK);resp-setContentType(text/html);resp-addHeader(Server, li);resp-setBody(htmlheadtitleThis is title/title/headbodyh1Hello/h1Now is hello /body/html);}else if (req.path() /hello) {resp-setStatusCode(HttpResponse::HttpStatusCode::k200Ok);resp-setstatusMessage(OK);resp-setContentType(text/plain);resp-setBody(hello, world!\n);}else {resp-setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);resp-setstatusMessage(Not Found);resp-setCloseConnection(true);}
}int main(int argc, char* argv[])
{EventLoop loop;HttpServer server(loop, InetAddr(9999));server.setHttpCallback(onRequest); //比普通的server添加了这行server.start(0); //副io线程数量为0,单线程运行loop.loop();return 0;
}
主要就是用户自写的一个业务处理函数之后调用HttpServer类的函数setHttpCallback来进行注册即可。
这里例子是创建了端口是9999的HTTPServer,提供访问的是/,/hello。
在浏览器输入 http://localhost:9999或者http://localhost:9999/hello即可访问成功。(localhost可以改成是自己linux的ip) HTTP调用的流程图 HTTP服务器基本就是结束了这里的是简单静态web服务器我们没有解析客户发送过来的body。需要其他功能可以在这基础上进行完善或添加比如支持fcgi。
完整源代码https://github.com/liwook/CPPServer/tree/main/code/server_v20