1. 程式人生 > >muduo_net程式碼剖析之Http相關類介紹

muduo_net程式碼剖析之Http相關類介紹

在這裡插入圖片描述

[1] 請求類HttpRequest

注:該類僅僅是對HttpRequest中的成員屬性(即Http請求包內容)賦值,客戶端並沒有真正的向伺服器傳送請求動作。

提供了設定、獲取以下屬性變數的介面:method_(請求方法)、version_(協議版本1.0/1.1)、 string path_(請求路徑)、string query_、Timestamp receiveTime_(請求時間)、std::map<string, string> headers_(首部欄位)

//請求類
class HttpRequest : public muduo::copyable
{
public: enum Method //請求方法 { kInvalid, kGet, kPost, kHead, kPut, kDelete }; enum Version //HTTP版本 { kUnknown, kHttp10, kHttp11 }; HttpRequest() : method_(kInvalid), version_(kUnknown) { } void setVersion(Version v){ version_ = v; } Version getVersion() const { return
version_; } Method method() const { return method_; } bool setMethod(const char* start, const char* end) { assert(method_ == kInvalid); string m(start, end); if (m == "GET") { method_ = kGet; } else if (m == "POST") { method_ = kPost; } else if
(m == "HEAD") { method_ = kHead; } else if (m == "PUT") { method_ = kPut; } else if (m == "DELETE") { method_ = kDelete; } else { method_ = kInvalid; } return method_ != kInvalid; } const char* methodString() const { const char* result = "UNKNOWN"; switch(method_) { case kGet: result = "GET"; break; case kPost: result = "POST"; break; case kHead: result = "HEAD"; break; case kPut: result = "PUT"; break; case kDelete: result = "DELETE"; break; default: break; } 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 setReceiveTime(Timestamp t) { receiveTime_ = t; } Timestamp receiveTime() const { return receiveTime_; } void addHeader(const char* start, const char* colon, const char* end) { string field(start, colon); //header域 ++colon; while (colon < end && isspace(*colon)) //去除左空格 { ++colon; } string value(colon, end); //header值 while (!value.empty() && isspace(value[value.size()-1])) //去除右空格 { value.resize(value.size()-1); } headers_[field] = value; //元素<field,value>,新增到map中 } string getHeader(const string& field) const { string result; std::map<string, string>::const_iterator it = headers_.find(field); if (it != headers_.end()) { result = it->second; } return result; } const std::map<string, string>& headers() const { return headers_; } void swap(HttpRequest& that) { std::swap(method_, that.method_); std::swap(version_, that.version_); path_.swap(that.path_); query_.swap(that.query_); receiveTime_.swap(that.receiveTime_); headers_.swap(that.headers_); } private: Method method_; //請求方法 Version version_; //協議版本1.0/1.1 string path_; //請求路徑 string query_; Timestamp receiveTime_; //請求時間 std::map<string, string> headers_; //header列表 };

[2] 響應類HttpResponse

注:該類僅僅是對HttpResponse 中的成員屬性(即Http響應包內容)賦值,伺服器並沒有對客戶端的請求執行迴應動作。

  1. 提供了設定、獲取以下屬性變數的介面:statusCode_(狀態響應碼)、statusMessage_(狀態響應碼對應的文字資訊)、bool closeConnection_(是否關閉連線)、body_(實體)、std::map<string, string> headers_(新增首部欄位)
  2. appendToBuffer介面:將HttpResponse物件,儲存成Buffer格式,以便於響應給客戶端
class Buffer;
//響應類
class HttpResponse : public muduo::copyable
{
private:
  std::map<string, string> headers_; //header列表
  HttpStatusCode statusCode_; //狀態響應碼

  string statusMessage_; //狀態響應碼對應的文字資訊
  bool closeConnection_;//是否關閉連線
  string body_; //實體
public:
  enum HttpStatusCode //狀態碼
  {
    kUnknown,
    k200Ok = 200, //成功
    k301MovedPermanently = 301,//301重定向,請求的頁面永久性一直另一個地址
    k400BadRequest = 400, //錯誤的請求,語法格式有錯,伺服器無法處理此請求
    k404NotFound = 404, //請求的網頁不存在
  };

  explicit HttpResponse(bool close)
    : statusCode_(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_; }

  //設定文件媒體型別,即Content-Type欄位
  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; }

  //將HttpResponse打包成字串,儲存到Buffer中,以便傳送給客戶端
  void appendToBuffer(Buffer* output) const
  {
    //HTTP /1.1 200 OK
    char buf[32];
    snprintf(buf, sizeof buf, "HTTP/1.1 %d ", statusCode_); //200
    output->append(buf);
    output->append(statusMessage_); //狀態資訊OK
    output->append("\r\n");

    //長短連線的主要區別:是否新增body的長度
    if (closeConnection_)
    {
	  //如果是短連線,不需要告訴瀏覽器Content-Length,瀏覽器也能正確處理
      //短連結是建立一次,傳送一條資料,因此不存在粘包問題
	  output->append("Connection: close\r\n");
    }
    else
    {
      snprintf(buf, sizeof buf, "Content-Length: %zd\r\n", body_.size());//實體長度
      output->append(buf);
      output->append("Connection: Keep-Alive\r\n");
    }

    //遍歷首部欄位列表,將<key,value>新增到buf中
    for (const auto& header : headers_)
    {
      output->append(header.first);
      output->append(": ");
      output->append(header.second);
      output->append("\r\n");
    }
    output->append("\r\n"); //header與body之間的空行
    output->append(body_);
  }
};

[3] HttpContext解析請求包

僅有一個重要的核心函式parseRequest:解析請求包,如果解析成功,則會將解析到的請求行、首部欄位新增到成員變數HttpRequest request_中

class HttpContext : public muduo::copyable
{
private:
  //解析請求行,並判斷請求行語法是否正確:如果正確,返回false
  //                                  如果錯誤,返回true
  bool processRequestLine(const char* begin, const char* end);

  HttpRequestParseState state_;//請求解析狀態,狀態機控制解析過程
  HttpRequest request_; //http請求類,用於儲存解析後的結果
public:
  //列舉型別,控制狀態機應該執行哪個狀態的操作
  enum HttpRequestParseState 
  {
    kExpectRequestLine, //解析請求行
    kExpectHeaders,     //解析首部欄位
    kExpectBody,        //解析Body
    kGotAll,            //已經全部解析完成
  };

  HttpContext(): state_(kExpectRequestLine){}
  
  //[*] 解析請求包,解析順序:請求行、首部欄位
  bool parseRequest(Buffer* buf, Timestamp receiveTime); 

  //判斷是否全部解析完成
  bool gotAll() const { return state_ == kGotAll; }

  void reset() //解析完成後,清空HttpRequest物件request_
  {
    state_ = kExpectRequestLine;
    HttpRequest dummy;
    request_.swap(dummy); //將當前的request_置空
  }

  const HttpRequest& request() const { return request_; }
  HttpRequest& request() { return request_; }
};
/*
*解析請求包:請求行、首部欄位
*返回值:成功,true;失敗,false
*/
bool HttpContext::parseRequest(Buffer* buf, Timestamp receiveTime)
{
  bool ok = true;
  bool hasMore = true;
  while (hasMore)
  {
    if (state_ == kExpectRequestLine) //處於解析請求行的狀態
    {
      const char* crlf = buf->findCRLF();//找到\r\n的位置	
      if (crlf)
      {
        //解析請求行,並判斷請求行語法是否正確
        ok = processRequestLine(buf->peek(), crlf); 
        if (ok)
        {
          request_.setReceiveTime(receiveTime); //設定請求時間
          buf->retrieveUntil(crlf + 2);//將請求行和\r\n從buf中移除
          state_ = kExpectHeaders;//HttpContext將狀態改為kExpectHeaders
        }
        else
        {
          hasMore = false;
        }
      }
      else
      {
        hasMore = false;
      }
    }
    else if (state_ == kExpectHeaders) //處於解析首部欄位的狀態
    {
      const char* crlf = buf->findCRLF();
      if (crlf)
      {
        const char* colon = std::find(buf->peek(), crlf, ':');
        if (colon != crlf)
        {
          //將解析出來的首部欄位新增到成員變數request_中
          request_.addHeader(buf->peek(), colon, crlf);
        }
        else
        {
          // empty line, end of header
          // FIXME:
          state_ = kGotAll;
          hasMore = false;
        }
        buf->retrieveUntil(crlf + 2); //將( 當前解析的header+\r\n )從buf中移除
      }
      else
      {
        hasMore = false;
      }
    }
    else if (state_ == kExpectBody) //當前還不支援請求中帶body
    {
      // FIXME:
    }
  }
  return ok;
}

---------------------------------------------------------------
華麗的分割線
---------------------------------------------------------------

上面介紹了3個簡單的類,是為了下面介紹HttpServer類,該類是對客戶端發來的請求包做真正的迴應,它本質上就是一個TcpServer,只不過它是回覆了Http請求包而已。

HttpServer類

HttpServer類使用了TcpServer類的所有功能,它僅僅比TcpServer多一個函式介面setHttpCallback()和一個成員變數httpCallback_。該回調函式是用來處理客戶端發來的請求包的。整個執行過程:客戶端發來請求包後,將回調onMessage,在onMessage中又回調了onRequest,在onRequest中又回調了註冊的httpCallback_。詳細看下面程式碼:

//當client傳送來請求包給伺服器後,onMessage被回撥,此時客戶端發來的請求包被存放到buf中
//之後使用解析類物件context對buf進行解析
void HttpServer::onMessage(const TcpConnectionPtr& conn,
                           Buffer* buf, //客戶端發來的請求包被存放到buf中
                           Timestamp receiveTime)
{
  HttpContext* context = boost::any_cast<HttpContext>(conn->getMutableContext());

  //請求訊息解析失敗,即請求失敗
  if (!context->parseRequest(buf, receiveTime))
  {
    conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");
    conn->shutdown();
  }

  //請求訊息解析完畢
  if (context->gotAll())
  {
    onRequest(conn, context->request()); //回撥onRequest()
    context->reset();//本次請求處理完畢,重置HttpContext,適用於長連線
  }
}

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); //回調了使用者註冊的回撥函式httpCallback_
  
  Buffer buf;
  response.appendToBuffer(&buf);
  conn->send(&buf);
  if (response.closeConnection())
  {
    conn->shutdown();
  }
}

其中,預設的回撥函式httpCallback_

//req:傳入引數,包含了請求包的所有資訊
//resp:傳出引數,根據請求包req的資訊對resp進行賦值
void defaultHttpCallback(const HttpRequest& req, HttpResponse* resp)
{
  resp->setStatusCode(HttpResponse::k404NotFound);
  resp->setStatusMessage("Not Found");
  resp->setCloseConnection(true);
}

自定義的httpCallback_回撥函式的程式碼應該怎麼寫?

  1. req:傳入引數,包含了請求包的所有資訊
  2. resp:傳出引數,根據請求包req的資訊對resp進行賦值

程式碼書寫很簡單:根據req,給resp賦值即可

示例程式碼

HTTP客戶端程式碼:使用瀏覽器輸入請求的網址進行訪問

http://192.168.88.9:8000/hello
http://192.168.88.9:8000/favicon.ico
http://192.168.88.9:8000/image/png

HTTP伺服器程式碼:HttpServer_test.c

#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];
bool benchmark = false;

void onRequest_cb(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