前言
- 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报文体进行解析处理。
- 项目源代码可以从这里下载:项目地址
- 主要代码
- 复制代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140#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("%sn", 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("rn"); 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("rn"); int bodySize = parseBodySize(httpRequest); std::string strRequestH; do{ int iPos = httpRequest.find("rn", headPos + strlen("rn")); 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("rn", 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四次挥手过程,这里也不过多阐述了。
最后
以上就是飘逸黑猫最近收集整理的关于HTTP协议详解 - 通过C++实现HTTP服务剖析HTTP协议的全部内容,更多相关HTTP协议详解内容请搜索靠谱客的其他文章。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复