本文地址:(LYanger的博客:http://blog.youkuaiyun.com/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
开始捕获,浏览器输入虚拟机地址和端口号,然后收到响应,截图如下:
我们将保存的.cap文件使用windows的wireshark打开,关于wireshark怎么分析可以参考:
Wireshark数据抓包教程之认识捕获分析数据包
如图,我们不管别的,点击上方选项:分析->追踪流->TCP流就可以得到:
这样我们就可以窥探所有的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 <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(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 (std::map<string, string>::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("<html><head><title>This is title</title></head>" //标题
"<body><h1>Hello</h1>Now is " + now + //内容
"</body></html>");
}
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<void (const HttpRequest&,
HttpResponse*)> 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<HttpContext>(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<void (const HttpRequest&, HttpResponse*)> 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<string, string> headers_;
headers_[field] = value;
}
//根据头域返回值
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_);
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<string, string> 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("<html><head><title>This is title</title></head>" //标题
"<body><h1>Hello</h1>Now is " + now + //内容
"</body></html>");
然后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<void (const HttpRequest&, HttpResponse*)> HttpCallback; //执行用户注册的回调函数 httpCallback_(req, &response); Buffer buf; //用户处理后的信息,追加到缓冲区 response.appendToBuffer(&buf); conn->send(&buf); //发送数据 if (response.closeConnection()) //如果关闭 { conn->shutdown(); //关了它 } }
也就是说使用HttpResponse类设置完响应各种信息之后,我们就可以把他发回给用户了。如果使用短连接,那就关掉该连接即可。
muduo的http库的代码到这里就暂时结束了,要是想到什么我会再补充。
muduo的http库的代码到这里就暂时结束了,要是想到什么我会再补充。
今天12月15号,离来年三月春招还有两个半月,这两个半月一定努力,向某人证明自己。