前言
- C/C++程序员一般很少会接触到HTTP服务端的东西,所以对HTTP的理解一般停留在理论。 本文章实现通过C++实现了一个http服务,可以通过代码对HTTP协议有更深的理解,并且通过抓包工具对HTTP协议进行更为详细的分析。
HTTP协议简介
- HTTP(hypertext transport protocol 超文本传输协议):一种无状态的,以请求/应答方式运行的协议,它使用可扩展的语义和自描述消息格式,与基于网络的超文本信息系统灵活的互动。
HTTP报文格式
-
请求报文:由请求行,头部字段集合,消息正文三大部分组成。
-
请求行:描述请求的基本信息
- 请求方法
-
请求方法 说明 GET 请求服务器发送某个资源 POST 用来传输实体的主体 PUT 用来传输文件 HEAD 获取报文首部,用于确认URI的有效性及资源更新的日期时间等 DELETE 删除文件 OPTIONS 查询针对请求URI指定的资源支持的方法 TRACE 用于追踪路径 CONNECT 要求与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信
-
- URI:统一资源标识符(Uniform Resource Identifier)
- HTTP版本
-
HTTP版本 说明 HTTP/0.9 1991年制定,只支持GET方法 HTTP/1.0 1996年诞生,增加了POST,HEAD方法 HTTP/1.1 1999年发布并成为标准,增加了PUT方法,并允许持久连接
-
- 请求方法
-
头部字段集合:使用key-value形式更详细地说明报文。主要分为四类:通用首部,请求首部,响应首部,实体首部
- 通用首部:提供与报文相关的基本信息。既可以出现在请求报文中,也可以出现在响应报文中。
-
首部字段名 说明 CacheControl 控制缓存的行为 Connection 允许客户端和服务端指定与请求/响应连接有关的选项 Date 创建报文的日期时间 Pragma 另一种随报文传送指示的方式,但并不专用于缓存 Transfer-Encoding 告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式 Trailer 报文末端的首部一览 Update 给出了发送端可能想要"升级"使用的新版本或协议 Via 显示了报文经过的中间节点(代理,网关)
-
- 请求首部:只在请求报文中有意义的首部。用于说明是谁或什么在发送请求,请求源自何处,或者客户端的喜好和及能力
-
首部字段名 说明 Accept 告诉服务器能够发送哪些媒体类型 Accept-Charset 告诉服务器能够发送哪些字符集 Accept-Encoding 告诉服务器能够发送哪些编码方式 Accept-Language 告诉服务器能够发送哪些语言 Authorization 包含了客户端提供给服务器,以便对其自身进行认证的数据 From 提供了客户端用户的E-mail地址 Host 给出了接收请求的服务器的主机名和端口号 If-Match 如果实体标记与文档当前的实体标记相匹配,就获取这份文档 If-Modified-Since 除非在某个指定的日期之后资源被修改过,否则就限制这个请求 If-None-Match 如果提供的实体标记与当前文档的实体标记不相符,就获取文档 If-Range 允许对文档的某个范围进行条件请求 If-Unmodified-Since 除非在某个指定日期之后资源没有被修改过,否则就限制这个请求 Max-Forward 将请求转发给其他代理或网关的最大次数 Proxy-Authorization 与Authorization首部相同,但这个首部实在与代理进行认证时使用 Range 如果服务器支持范围请求,就请求资源的指定范围 Referer 提供包含当前请求URI的文档的URL TE 告诉服务器可以使用哪些扩展传输编码 User-Agent 将发起请求的应用程序名称告知服务器
-
- 实体首部:提供有关实体及其内容得到大量信息
-
首部字段名 说明 Allow 资源可支持的HTTP方法 Content-Encoding 实体主体适用的编码方式 Content-Language 实体主体的自然语言 Content-Length 实体主体的大小(单位:字节) Content-Location 替代对应资源的URI Content-MD5 实体主体的报文摘要 Content-Range 实体主体的位置范围 Content-Type 实体主体的媒体类型 Expires 实体主体过期的日期时间 Last-Modified 资源的最后修改日期
-
- 通用首部:提供与报文相关的基本信息。既可以出现在请求报文中,也可以出现在响应报文中。
-
消息正文:实际传输的数据,可以是纯文本,也可以是图片、视频等二进制数据
-
-
响应报文:由响应行,头部字段集合,消息正文三大部分组成。
- 响应行:描述响应的基本信息
- HTTP版本:上面已经介绍过了
- 状态码:状态码的职责是当客户端向服务端发送请求时,描述返回的请求结果
-
状态码 类别 原因短语 1XX 信息性状态码 接收的请求正在处理 2XX 成功状态码 请求正常处理完毕 3XX 重定向状态码 需要进行附加操作以完成请求 4XX 客户端错误状态码 服务端无法处理请求 5XX 服务端错误状态码 服务器处理请求出错 - 常用的错误码主要有14种
-
错误码 错误码描述 详细描述 200 OK 表示从客户端发来的请求在服务端被正常处理了 204 No Content 无内容。服务器成功处理,但未返回内容 206 Partial Content 部分内容。服务器成功处理了部分GET请求 301 Moved Permanently 永久重定向,意思是本地请求的资源以及不存在,使用新的URI再次访问 302 Found 临时重定向,临时则所请求的资源暂时还在,但是目前需要用另一个URI访问 303 See Other 与301类似,使用GET和POST请求查看 304 Not Modified 运用于缓存控制。它用于 If-Modified-Since 等条件请求,表示资源未修改,可以理解成"重定向已到缓存的文件" 307 Temporary Redirect 临时重定向,与302类似,使用GET请求重定向 400 Bad Request 客户端请求的语法错误,服务器无法理解 401 Unauthorized 表示发送的请求需要有通过HTTP认证的认证信息 403 Forbidden 这一个是表示服务器禁止访问资源。原因比如涉及到敏感词汇、法律禁止等 404 Not Found 服务器无法根据客户端的请求找到资源 500 Internal Server Error 服务器内部错误,无法完成请求 503 Service Unavailable 表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的"网络服务正忙,请稍后重试"的提示信息就是状态码 503
-
- 状态码描述:作为状态码补充,是更详细的解释文字,帮助理解原因
- 头部字段合集:上面已经介绍过,这里只介绍下响应首部字段
- 响应首部
-
首部字段名 说明 Accept-Ranges 对此资源来说,服务器可接受的范围类型 Age 响应持续时间 ETag 资源的匹配信息 Location 令客户端重定向至指定URI Proxy-Authenticate 代码服务器对客户端的认证信息 Retry-After 如果资源不可用,在此日期或时间重试 Server 服务器应用程序软件的名称和版本 Vary 代理服务器缓存的管理信息 WWW-Authenticate 服务器对客户端的认证信息
-
- 响应首部
- 消息正文
- 响应行:描述响应的基本信息
C++实现http服务
- 我参考TinyHttpd项目,使用C++实现了一个http服务,功能比较简单,目前只支持GET和POST请求。并且也只是对http请求报文进行了解析,然后进行简单回应,未实现其他功能。使用第三方json解析库json11对json报文体进行解析处理。
- 项目源代码可以从这里下载:项目地址
- 主要代码
-
#include "httpd.h" void threadFunc(void* arg, int conn){ Httpd* httpd = (Httpd*)arg; // 接收http请求 char bodyBuf[1024] = {0}; int recvSize = recv(conn, bodyBuf, sizeof(bodyBuf), 0); printf("%s\n", bodyBuf); std::string strMethod; std::string strUri; std::string strVersion; std::map<std::string, std::string> requestHead; std::string requestBody; // 解析http请求,包括请求方式(目前只支持GET和POST请求),URI,http版本 httpd->parseHttpRequestInfo(bodyBuf, strMethod, strUri, strVersion); // 解析http请求头 httpd->parseHttpRequestHead(bodyBuf, requestHead); //解析http请求体 httpd->parseHttpRequestBody(bodyBuf, requestBody); //根据不同请求方式进行响应 if(strMethod.compare("GET") == 0){ httpd->httpResponseHtml(conn); }else if(strMethod.compare("POST") == 0){ std::string data1; std::string data2; if(httpd->parseBodyJson(requestBody, data1, data2)){ httpd->httpResponseJson(conn, data1, data2); } } //关闭套接字 close(conn); } bool Httpd::start(){ //定义sockfd int server_sockfd = socket(AF_INET,SOCK_STREAM, 0); ///定义sockaddr_in struct sockaddr_in server_sockaddr; server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_port = htons(4000); server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); //bind,成功返回0,出错返回-1 if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){ perror("bind"); return false; } //listen,成功返回0,出错返回-1 if(listen(server_sockfd, 5) == -1){ perror("listen"); return false; } //客户端套接字 char buffer[1024] = {0}; struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr); int conn = 0; while(1){ //成功返回非负描述字,出错返回-1 conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length); if(conn < 0){ perror("connect"); return false; } //开启线程处理请求 std::thread th; th = std::thread(threadFunc, this, conn); th.join(); } close(server_sockfd); return true; } bool Httpd::parseHttpRequestInfo(std::string httpRequest, std::string& method, std::string& uri, std::string& version){ int recvSize = httpRequest.size(); //查找请求头 std::string strRequestHead; int pos = httpRequest.find("\r\n"); strRequestHead = httpRequest.substr(0, pos); //解析请求类型 method = strRequestHead.substr(0, strRequestHead.find(" ")); //解析uri uri = strRequestHead.substr(strRequestHead.find(" ") + 1, strRequestHead.find(" ", strRequestHead.find(" ") + 1) - strRequestHead.find(" ")); //解析http版本 version = strRequestHead.substr(strRequestHead.rfind(" "), strRequestHead.size() - strRequestHead.rfind(" ")); return true; } bool Httpd::parseHttpRequestHead(std::string httpRequest, std::map<std::string, std::string>& requestHead){ int recvSize = httpRequest.size(); int headPos = httpRequest.find("\r\n"); int bodySize = parseBodySize(httpRequest); std::string strRequestH; do{ int iPos = httpRequest.find("\r\n", headPos + strlen("\r\n")); strRequestH = httpRequest.substr(headPos, iPos - headPos); if(strRequestH.find(":") != std::string::npos){ std::string strKey = strRequestH.substr(0, strRequestH.find(":")); std::string strValue = strRequestH.substr(strRequestH.find(":") + 1, strRequestH.size() - strRequestH.find(":")); requestHead.insert(std::pair<std::string, std::string>(strKey, strValue)); } headPos = iPos; } while(headPos < recvSize - bodySize && headPos > 0); return true; } bool Httpd::parseHttpRequestBody(std::string httpRequest, std::string& requestBody){ int recvSize = httpRequest.size(); int bodySize = parseBodySize(httpRequest); if(bodySize == 0){ return false; } requestBody = httpRequest.substr(recvSize - bodySize, bodySize); return true; } int Httpd::parseBodySize(std::string httpRequest){ std::string strContentLength; int posLengthStart = httpRequest.find("Content-Length: ") + strlen("Content-Length: "); int posLengthEnd = httpRequest.find("\r\n", httpRequest.find("Content-Length: ") + strlen("Content-Length: ")); strContentLength = httpRequest.substr(posLengthStart, posLengthEnd - posLengthStart); return atoi(strContentLength.c_str()); }
-
- 通过代码我们可以看到,其实底层还是TCP编程,只不过TCP通信时,我们是直接拿数据,不用遵守什么规则。但如果要进行HTTP通信,就要遵守人家的规则,按照请求报文的格式去进行解析,才能拿到服务端想要的信息,然后再根据响应报文去组装数据,返回给客户端。
演示
- POST请求演示,我通过postman演示下post请求,目前实现的功能是将请求数据拼接后返回。
- postman界面演示
- 服务端打印
- postman界面演示
- GET请求演示,直接在浏览器中访问,返回一个html格式的页面
- 浏览器页面
- 服务端打印
- 浏览器页面
抓包分析
- 下面我们通过wireShark工具抓包分析下http协议的通信过程,发送一个post请求。
- 通过抓包可以看到,在http通信前,先要通过TCP三次握手建立连接,并且一次请求结束后,进行TCP四次挥手断开连接(http协议目前是支持长连接的,也就是建立连接后,可以发送多个http请求,我这里为了分析方便,在发送一次http请求后就关闭了套接字)。
- 先看下前三行,是TCP建立连接的过程,我在使用wireShark抓包分析TCP协议进行了详细介绍,这里就不再过多阐述了。
- 第四行开始是http通信,可以看到http请求的所有信息
- 再看第七行,是http服务的响应
- 后面是TCP四次挥手过程,这里也不过多阐述了。