muduo的http库剖析

    本文地址:(LYanger的博客:http://blog.csdn.net/freeelinux/article/details/53643802)


一:http协议

    首先看一下http request:

  • request line + header + body (header分为普通报头,请求报头与实体报头)
  • header与body之间有一空行(CRLF)
    请求方法有:GET、POST、HEAD、PUT、DELETE等
    协议版本:1.0、1.1

    详细知识参见这篇博客: http必知必会
    或者有关面试的http知识见这篇博客: http面试必知必会
    http断点续传原理: http断点续传原理

二:muduo库http测试以及抓包

    muduo库的http库自带了一个http_server的测试用例,即muduo/net/http/tests/HttpServer.cc,我们将它拿来测试一下,并用wireshark抓包分析http协议。
    编译时需要加-lmudo_base -lmuduo_net -lmuduo_http -lpthread选项。在这里就不上代码了,后面会有分析,我们主要先抓包分析协议。
    
    虚拟机运行该测试用例,使用tcpdump输入:

      tcpdump -Xvvenn -i eth1 tcp[20:2]=0x4745 or tcp[20:2]=0x4854 -s 80 -w tcpdump.cap
    
    开始捕获,浏览器输入虚拟机地址和端口号,然后收到响应,截图如下:
muduo的http库剖析_第1张图片
   
    我们将保存的.cap文件使用windows的wireshark打开,关于wireshark怎么分析可以参考: Wireshark数据抓包教程之认识捕获分析数据包
muduo的http库剖析_第2张图片
    
    如图,我们不管别的,点击上方选项:分析->追踪流->TCP流就可以得到:
muduo的http库剖析_第3张图片

    这样我们就可以窥探所有的http数据报内容了,对比第一张图网页中显示的内容,我们可以总结一下:
    先看 GET请求:使用HTTP 1.1版本。服务器地址192.168.33.147,端口8000。使用keep-alive长链接。Cache-Control是用来控制的,常见的取值有private、no-cache、
max_age等,如果指定max-age值,那么在此值的时间内就不会重新访问服务器,数据由缓存直接返回,单位为秒。
    常用请求头:
  • Accept:浏览器可接受的媒体类型(MIME)类型
  • Accept-Language:浏览器所希望的语言种类
  • Accept-Encoding:浏览器能够解码的方法,如,gzip,deflate等
  • User-Agent:告诉HTTP服务器,客户端使用的操作系统和浏览器的版本和名称
  • Connection:表示是否需要持久连接,Keep-Alive表示长连接,close表示短连接
    
    再看 HTTP响应包:status line+header+body(header分为普通报头,响应报头与实体报头),header与body之间有一空行(CRLF)。
    状态响应码:
  • 1xx    提示信息-表示请求已被成功接收,继续处理
  • 2xx    成功-表示请求已被成功接收,理解,接受
  • 3xx    重定向-要完成请求必须进行更进一步的处理
  • 4xx    客户端错误-请求有语法错误或请求无法实现
  • 5xx    服务器端错误-服务器执行一个有效请求失败

三:muduo的http库剖析

    muduo的http库一共有四个类,HttpRequest(http请求类封装)、HttpResponse(http响应类封装)、HttpContext(http协议解析类)、HttpServer(http服务器封装)。

在这里我们就拿出上文中例子的代码来剖析muduo的http库,实例代码:
#include 
#include 
#include 
#include 
#include 

#include 
#include 

using namespace muduo;
using namespace muduo::net;

extern char favicon[555];
bool benchmark = false;
//响应回调函数
void onRequest(const HttpRequest& req, HttpResponse* resp)
{
  std::cout << "Headers " << req.methodString() << " " << req.path() << std::endl;
  if (!benchmark)   //如果为真,打印头部
  {
    const std::map& headers = req.headers();
    for (std::map::const_iterator it = headers.begin();
         it != headers.end();
         ++it)
    {   
      std::cout << it->first << ": " << it->second << std::endl;
    }   
  }
  //如果路径为根路径
  if (req.path() == "/")
  {
  resp->setStatusCode(HttpResponse::k200Ok);  //状态码200
    resp->setStatusMessage("OK");   //ok
    resp->setContentType("text/html");  //html文本
    resp->addHeader("Server", "Muduo");   //增加头部
    string now = Timestamp::now().toFormattedString();   //生成时间戳
    resp->setBody("This is title"    //标题
        "

Hello

Now is " + now + //内容 ""); } else if (req.path() == "/favicon.ico") //如果访问/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"路径,省略 { ... } else { resp->setStatusCode(HttpResponse::k404NotFound); //如果都不是,出现我们没有设置的路径,那么返回404,找不到 resp->setStatusMessage("Not Found"); resp->setCloseConnection(true); //断开连接 } } int main(int argc, char* argv[]) { int numThreads = 0; if (argc > 1) { benchmark = true; //用来标志服务器是否打印header信息 Logger::setLogLevel(Logger::WARN); numThreads = atoi(argv[1]); //多线程数,如果为0,则不启动多线程 } EventLoop loop; HttpServer server(&loop, InetAddress(8000), "dummy"); //HttpServer类,客端可以以基于对象的方式把它包含起来 server.setHttpCallback(onRequest); //设置响应回调 server.setThreadNum(numThreads); //可能启动多线程 server.start(); //启动 loop.loop(); //循环等待 } //这是一个图片数组,省略了一部分 char favicon[555] = { '\x89', 'P', 'N', 'G', '\xD', '\xA', '\x1A', '\xA', ... }

上述程序就是前文中的程序,结果前文中使用wireshark已经分析过了。

我们来过一下流程,首先客端创建一个HttpServer对象,它的成员是这样的:
class HttpServer : boost::noncopyable
{
 public:
  typedef boost::function HttpCallback;
  HttpServer(EventLoop* loop,
             const InetAddress& listenAddr,
             const string& name,
             TcpServer::Option option = TcpServer::kNoReusePort);

  ~HttpServer();  // force out-line dtor, for scoped_ptr members.
  EventLoop* getLoop() const { return server_.getLoop(); }
  /// Not thread safe, callback be registered before calling start().
  void setHttpCallback(const HttpCallback& cb);
  void setThreadNum(int numThreads);
  void start();
 private:
  void onConnection(const TcpConnectionPtr& conn);
  void onMessage(const TcpConnectionPtr& conn,
                 Buffer* buf,
                 Timestamp receiveTime);
  void onRequest(const TcpConnectionPtr&, const HttpRequest&);

  TcpServer server_;    //http服务器也是一个Tcp服务器,所以包含一个TcpServer
  HttpCallback httpCallback_;   //在处理http请求时(即调用onRequest)的过程中回调此函数,对请求进行具体的处理。
};
    作为一个暴露给外部客端的类, HttpServer就是一个服务器的封装,由于http协议底层使用tcp协议,所以它包含了一个TcpServer。并且向客户提供了一个回调函数的接口,当服务器收到http请求时,调用客户端的处理函数进行处理,该函数必须在start()之前设定。
    HttpServer支持多线程,也可以使用单线程,同样setThreadNum()函数必须在start()函数调用之前被调用。

    HttpServer的构造函数是这样的:
HttpServer::HttpServer(EventLoop* loop,
                       const InetAddress& listenAddr,
                       const string& name,
                       TcpServer::Option option)
  : server_(loop, listenAddr, name, option),
    httpCallback_(detail::defaultHttpCallback)
{
  //连接到来回调该函数
  server_.setConnectionCallback(
      boost::bind(&HttpServer::onConnection, this, _1));  
  //消息到来回调该函数
  server_.setMessageCallback(
      boost::bind(&HttpServer::onMessage, this, _1, _2, _3));
}
初始化TcpServer,并将HttpServer的回调函数传给TcpServer。主要有两个函数:

其一是onConenction()函数:

void HttpServer::onConnection(const TcpConnectionPtr& conn)
{
  if (conn->connected())
  {
    //构造一个http上下文对象,用来解析http请求
    conn->setContext(HttpContext()); //TcpConnection和一个HttpContext绑定,利用boost::any
  }
}
该函数为一个新的TcpConnection绑定一 个HttpContext对象,使用的是TcpConnection中的boost::any,绑定之后,HttpContext就相当于TcpConnection的成员了,TcpConection在MessageCallback中就可以随意的使用HttpContext对象了。

那么,HttpContext类是干什么的? 来看一下它的类:
class HttpContext : public muduo::copyable
{
 public:
  enum HttpRequestParseState  //解析请求状态的枚举常量
  {
    kExpectRequestLine,   //当前正处于解析请求行的状态
    kExpectHeaders,          //当前正处于解析请求头部的状态
    kExpectBody,               //当前正处于解析请求实体的状态
    kGotAll,                       //解析完毕
  };
  HttpContext()
    : state_(kExpectRequestLine)  //初始状态,期望收到一个请求行
  {
  }
  // default copy-ctor, dtor and assignment are fine
  // return false if any error
  bool parseRequest(Buffer* buf, Timestamp receiveTime);
  
  bool gotAll() const
  { return state_ == kGotAll; }

  //重置HttpContext状态,异常安全
  void reset()
  {
    state_ = kExpectRequestLine;
    HttpRequest dummy;   //构造一个临时空HttpRequest对象,和当前的成员HttpRequest对象交换置空,然后临时对象析构
    request_.swap(dummy);
  }

  const HttpRequest& request() const
  { return request_; }

  HttpRequest& request()
  { return request_; }

 private:
  bool processRequestLine(const char* begin, const char* end);

  HttpRequestParseState state_;   //请求的解析状态
  HttpRequest request_;    //http请求类
};
它就是一个http的上下文对象,实际上是一个状态机。针对http包进行处理,我们可以看出它在处理过程中在四种状态间切换:
  • kExpectRequestLine,  
  • kExpectHeaders,        
  • kExpectBody,    
  • kGotAll,                   
未收到http包是期待处理请求行,处理请求行之后处理头部,然后体部,最后gotall完成,不过中间可能会出错,到达不了这一步,会有相应的处理方案。它的函数待会再分析,因为它是HttpServer绑定的onMessage()回调函数会用到


其二是onMessage()函数:

void HttpServer::onMessage(const TcpConnectionPtr& conn,
                           Buffer* buf,
                           Timestamp receiveTime)
{
  //取出请求,mutable可以改变
  HttpContext* context = boost::any_cast(conn->getMutableContext());

  //调用context的parseRequest解析请求,返回bool是否请求成功
  if (!context->parseRequest(buf, receiveTime))
  {
    conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");   //失败,发送400
    conn->shutdown();  //关闭连接
  }

  if (context->gotAll())  //请求成功
  {
    //调用onRequest
    onRequest(conn, context->request());
    //一旦请求处理完毕,重置context,因为HttpContext和TcpConnection绑定了,我们需要解绑重复使用。
    context->reset();
  }
}
没错,当TcpConnection中所拥有的连接有消息到来时,会调用它的messageCallback(应该是这名字,不过就是这个意思)函数,其实就是调用HttpServer的onMessage()函数。我们知道,我们之前在onConnection()函数中把HttpContext利用boost::any绑定给了TcpConnection,在onMessage()函数中我们就可以对TcpConnection使用HttpContext类来解析数据包了。

onMessage()函数首先调用HttpContext的parserRequset()函数解析请求,判断请求是否合法,进而选择关闭连接,或者处理请求。
HttpContex类的parserRequset()函数是这样实现的,利用状态机编程:
bool HttpContext::parseRequest(Buffer* buf, Timestamp receiveTime)
{
  bool ok = true;
  bool hasMore = true;
  while (hasMore)
  {
    //初始状态是处于解析请求行的状态,下一次循环不是该状态就不会进入,一般只进入一次
    if (state_ == kExpectRequestLine)
    {
      //首先查找\r\n,就会到GET / HTTP/1.1的请求行末尾
      const char* crlf = buf->findCRLF();
      if (crlf)
      {
        //解析请求行
        ok = processRequestLine(buf->peek(), crlf);
        if (ok)  //如果成功,设置请求行事件
        {
          //设置请求时间
          request_.setReceiveTime(receiveTime);
          //将请求行从buf中取回,包括\r\n
          buf->retrieveUntil(crlf + 2);
          state_ = kExpectHeaders;  //将Httpontext状态改为KexpectHeaders状态
        }
        else
        {
          hasMore = false;
        }
      }
      else
      {
        hasMore = false;
      }
    }
    else if (state_ == kExpectHeaders)  //处于Header状态
    {
      const char* crlf = buf->findCRLF();
      if (crlf)
      {
        const char* colon = std::find(buf->peek(), crlf, ':');  //查找:
        if (colon != crlf)
        {
          //找到添加头部,加到map容器
          request_.addHeader(buf->peek(), colon, crlf);
        }
        else
        {
          // empty line, end of header
          // FIXME:
          state_ = kGotAll;  //一旦请求完毕,再也找不到':'了,状态改为gotall状态,循环退出
          hasMore = false;
        }
        buf->retrieveUntil(crlf + 2);  //请求完毕也把crlf取回
      }
      else
      {
        hasMore = false;
      }
    }
    else if (state_ == kExpectBody)
    {
      // FIXME:
    }
  }
  return ok;
}
它先是解析请求行,如果有错has_more为false会跳出循环,后面代码解析所有的头部数据,有错也会跳出循环。
解析请求行是这样的:
bool HttpContext::processRequestLine(const char* begin, const char* end)
{
  bool succeed = false;
  const char* start = begin;
  const char* space = std::find(start, end, ' ');   //查找空格
  //格式 : GET / HTTP/1.1
  //找到GET并设置请求方法
  if (space != end && request_.setMethod(start, space))
  {
    start = space+1;
    space = std::find(start, end, ' ');  //再次查找
    if (space != end)  //找到
    {
      const char* question = std::find(start, space, '?');
      if (question != space)  //找到了'?',说明有请求参数
      {
        //设置路径
        request_.setPath(start, question);
        //设置请求参数
        request_.setQuery(question, space);
      }
      else
      {
        //没有找到只设置路径
        request_.setPath(start, space);
      }
      start = space+1;

      //查找有没有"HTTP/1."
      succeed = end-start == 8 && std::equal(start, end-1, "HTTP/1.");
      if (succeed)  //如果成功,判断是采用HTTP/1.1还是HTTP/1.0
      {
        if (*(end-1) == '1')
        {
          request_.setVersion(HttpRequest::kHttp11);
        }
        else if (*(end-1) == '0')
        {
          request_.setVersion(HttpRequest::kHttp10);
        }
        else
        {
          succeed = false;  //请求行失败
        }
      }
    }
  }
  return succeed;
}
好的,请求行一旦解析完毕,如果失败会关闭连接,成功的化onMessage()函数会继续进行,开始执行onRequest()函数处理请求:
void HttpServer::onRequest(const TcpConnectionPtr& conn, const HttpRequest& req)
{
  //取出头部
  const string& connection = req.getHeader("Connection");
  // 如果connection为close或者1.0版本不支持keep-alive,标志着我们处理完请求要关闭连接
  bool close = connection == "close" ||
    (req.getVersion() == HttpRequest::kHttp10 && connection != "Keep-Alive");
  //使用close构造一个HttpResponse对象,该对象可以通过方法.closeConnection()判断是否关闭连接
  HttpResponse response(close);
  //typedef boost::function HttpCallback;
  //执行用户注册的回调函数
  httpCallback_(req, &response);
  Buffer buf;   //用户处理后的信息,追加到缓冲区
  response.appendToBuffer(&buf);
  conn->send(&buf);  //发送数据
  if (response.closeConnection())  //如果关闭
  {
    conn->shutdown();  //关了它
  }
}
这里有又一个新的 HttpResponse对象,这个类只有一个.h文件,我们来看一下:
class HttpRequest : public muduo::copyable
{
 public:
  enum Method  //请求方法
  {
    kInvalid, kGet, kPost, kHead, kPut, kDelete
  };
  enum Version  //协议版本
  {
    kUnknown, kHttp10, kHttp11
  };

  HttpRequest()  
    : method_(kInvalid),
      version_(kUnknown)
  {
  }

  //设置版本
  void setVersion(Version v)
   {
    version_ = v;
  }

  Version getVersion() const
  { return version_; }

  //设置方法,
  bool setMethod(const char* start, const char* end)
  {
    assert(method_ == kInvalid);
    //使用字符串首尾构造string,不包括尾部,如char *s="123", string s=(s,s+3),则s输出为123
    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;
  }

  //返回请求方法
  Method method() const
  { return method_; }

  //请求方法转换成字符串
  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_; }

  //添加头部信息,客户传来一个字符串,我们把它转化成field: value的形式
  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);   //heade值
    //去除右空格,如果右边有空格会一直resize-1
    while (!value.empty() && isspace(value[value.size()-1]))
    {
      value.resize(value.size()-1);
    }
    //std::map headers_;  
    headers_[field] = value;
  }

  //根据头域返回值
  string getHeader(const string& field) const
  {
    string result;
    std::map::const_iterator it = headers_.find(field);
    if (it != headers_.end())
    {
      result = it->second;
    }
    return result;
  }

  //返回头部
  const std::map& headers() const
  { return headers_; }

  //交换
  void swap(HttpRequest& that)
  {
    std::swap(method_, that.method_);
    path_.swap(that.path_);
    query_.swap(that.query_);
    receiveTime_.swap(that.receiveTime_);
    headers_.swap(that.headers_);
  }

 private:
  Method method_;      //请求方法
  Version version_;      //请求版本
  string path_;             //请求路径
  string query_;           //请求参数
  Timestamp receiveTime_;    //请求接收时间
  std::map headers_;    //header列表
};
这个类的作用就是示例代码中那样,设置响应的各种信息,包括头部,体部,示例代码中这样用的:
  resp->setStatusCode(HttpResponse::k200Ok);  //状态码200
    resp->setStatusMessage("OK");   //ok
    resp->setContentType("text/html");  //html文本
    resp->addHeader("Server", "Muduo");   //增加头部
    string now = Timestamp::now().toFormattedString();   //生成时间戳
    resp->setBody("This is title"    //标题
        "

Hello

Now is " + now + //内容 "");
然后onRequest()函数这样执行的:
void HttpServer::onRequest(const TcpConnectionPtr& conn, const HttpRequest& req)
{
  //取出头部
  const string& connection = req.getHeader("Connection");
  // 如果connection为close或者1.0版本不支持keep-alive,标志着我们处理完请求要关闭连接
  bool close = connection == "close" ||
    (req.getVersion() == HttpRequest::kHttp10 && connection != "Keep-Alive");
  //使用close构造一个HttpResponse对象,该对象可以通过方法.closeConnection()判断是否关闭连接
  HttpResponse response(close);
  //typedef boost::function HttpCallback;
  //执行用户注册的回调函数
  httpCallback_(req, &response);
  Buffer buf;   //用户处理后的信息,追加到缓冲区
  response.appendToBuffer(&buf);
  conn->send(&buf);  //发送数据
  if (response.closeConnection())  //如果关闭
  {
    conn->shutdown();  //关了它
  }
}
 
          
    

也就是说使用HttpResponse类设置完响应各种信息之后,我们就可以把他发回给用户了。如果使用短连接,那就关掉该连接即可。


muduo的http库的代码到这里就暂时结束了,要是想到什么我会再补充。


今天12月15号,离来年三月春招还有两个半月,这两个半月一定努力,向某人证明自己。





你可能感兴趣的:(Muduo源码剖析,muduo源码剖析)