本期我们就来学习一个常见的协议:HTTP协议
HTTP协议是我们日常生活中最常见的协议。如果你经常浏览网站就对它不陌生。那么它究竟有什么奥秘呢?我们来学习一下吧!
相关代码已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢请点个赞谢谢
目录
[User Info(用户信息)](#User Info(用户信息))
[通用头(General Headers)](#通用头(General Headers))
[请求头(Request Headers)](#请求头(Request Headers))
[响应头(Response Headers)](#响应头(Response Headers))
[常见HTTP Header总结表](#常见HTTP Header总结表)
HTTP协议介绍
HTTP (HyperText Transfer Protocol,超文本传输协议)是一个基于TCP/IP的应用层协议,它定义了客户端(如浏览器)与服务器之间如何交换超文本(HTML、图片、JSON等)。HTTP是无状态的,意味着每个请求都是独立的,服务器不会保留之前的请求信息。这种设计简化了服务器实现,但也催生了Cookie、Session等保持会话状态的机制
HTTP协议访问服务器的过程

为什么需要域名呢?因为域名是方便人类阅读的设计。
HTTP本质是为了获取服务器上的文件内容!而为了找到对应的内容,就需要一个路径,这个路径就是URL。它的结构为:
IP + Port + 目标文件地址(路径)
其中标识唯一,这样就可以根据IP+路径就可以在全网中定位找到这个文件
URL
URL是URI(Uniform Resource Identifier,统一资源标识符)的一种具体形式,用于定位网络上的资源。它告诉客户端:使用什么协议、访问哪台主机、通过哪个端口、获取哪个路径下的资源,并附带哪些参数和内部锚点。RFC 3986定义了URL的通用语法,结构如下:
bash
scheme://user:pass@host:port/path?query#fragment
URL结构
我们以以下URL为例
cpp
https://john.doe:123456@www.example.com:8080/news/latest?page=2&lang=en#top
Scheme(协议方案)
示例: https
作用: 指定客户端与服务器通信时使用的应用层协议。常见的有http、https、ftp、file等。协议决定了数据格式、默认端口以及安全行为。在C++实现中,根据scheme选择合适的库函数或处理逻辑:例如https需要集成TLS/SSL,而http则直接使用TCP。
User Info(用户信息)
示例: john.doe:123456@
作用: 可选的用户认证信息,格式为username:password,用于向服务器提供身份凭证。由于以明文形式出现在URL中(即便Base64编码也极易解码),安全性极差,现代Web开发中极少使用(已逐渐被废弃)。如果在C++中解析到此类信息,通常应记录警告,并建议用户改用Authorization头部。
实现要点: 解析时需要处理可能包含的保留字符(如:、@),它们需经过百分号编码。
Host(主机名)
示例: www.example.com
作用: 标识资源所在服务器的域名或IP地址。客户端通过DNS将主机名解析为IP地址,然后建立TCP连接。主机名可以是一个注册域名(如www.example.com)、IP地址字面量(如192.168.1.1),或者是IPv6地址(需用方括号括起,如[::1])。
C++实现: 使用getaddrinfo或Boost.Asio的resolver进行域名解析。注意处理解析失败、超时以及IPv4/IPv6双栈情况。
Port(端口号)
示例: 8080
作用: 指定服务器上监听的TCP端口。每个协议都有默认端口(HTTP为80,HTTPS为443),如果URL中省略端口,则使用默认端口。显式指定端口可用于非标准服务或测试环境。
实现要点: 在C++中创建socket连接时,必须将主机名和端口组合成端点。使用telnet或curl测试非标准端口是调试常用手段。
Path(路径)
示例: /news/latest
作用: 指向服务器上具体资源的层次结构路径。可以是一个静态文件路径(如/images/logo.png),也可以是服务端路由(如REST API的端点)。路径通常以/开头,且区分大小写(取决于服务器实现)。
解析细节: 路径中可能包含.和..段,但在发送给服务器前,客户端通常应进行规范化(去除.和..),以避免路径遍历攻击。在C++中,可借助std::filesystem进行路径处理,但需注意平台差异。
Query(查询字符串)
示例: ?page=2&lang=en
作用: 向服务器传递额外参数,通常用于动态页面或API请求。查询字符串以?开始,由多个key=value对组成,用&分隔。参数值必须经过百分号编码(如空格编码为%20或+),以消除歧义。
C++处理: 解析查询字符串时需注意编码解码。可以使用现成库(如Boost.URL),或手动编写状态机解析。同时要防范参数污染(同名参数多次出现)和注入攻击。
Fragment(片段标识符)
示例: #top
作用: 指示浏览器或客户端定位到资源内部的某一部分(如HTML页面中的锚点<a name="top">)。片段由#引导,不会随HTTP请求发送给服务器,仅在客户端本地使用。
实现注意: 在构造HTTP请求时,必须将fragment部分剥离,只发送path和query给服务器。但在某些单页应用(SPA)中,前端会利用fragment进行路由,此时需特殊处理。
URL编码与解码
URL中只允许ASCII字符集的一个子集(字母、数字、以及-_.~等保留字符)。其他字符(包括中文、空格、特殊符号)必须进行百分号编码 ,即将其转换为%后跟两位十六进制数。例如空格编码为%20,中文"你好"在UTF-8下编码为%E4%BD%A0%E5%A5%BD。
C++实现: 编码函数需遍历字符串,对非保留字符进行转换;解码函数则遇到%时读取后续两个十六进制数并还原。务必注意UTF-8多字节字符的正确处理,避免截断。
urlencode和urldencode
URL的设计初衷是基于ASCII字符集,且某些字符在URL中具有特殊含义(如/分隔路径,?引入查询,#标识片段)。当需要在URL中包含以下字符时,必须进行编码:
-
保留字符 :如
:、/、?、#、[、]、@、!、$、&、'、(、)、*、+、,、;、=等。如果这些字符作为数据而非分隔符出现,就需要编码。 -
不安全字符 :如空格、引号、
<、>等,它们可能被传输过程误解释。 -
非ASCII字符:如中文、日文、Emoji等,必须编码后才能安全传输。
URL编码(Percent-Encoding)正是为了解决这一问题而诞生的标准机制,定义于RFC 3986
URL编码,又称百分号编码,将需要编码的字符转换成一个%后跟两位十六进制数(表示该字符在ASCII或UTF-8等字符集中的字节值)
URL解码是编码的逆过程,将%XX形式的序列还原为原始字符,并将可能的+转换为空格(根据上下文)。解码时必须谨慎处理非法序列(如不完整的%或无效的十六进制),避免崩溃或安全漏洞。
URI是访问Linux服务器的web根目录
HTTP协议请求与响应格式
HTTP是典型的服务端与客户端模型的协议,本质上是以行为单位的"字符串"(字节流),分为请求行、请求报头、空行、请求正文,前三部分统称为报头。任何将报头与有效载荷分离呢?通过特殊符号(空行)
对于HTTP而言,接收方肯定保证至少读完报头,并且可以通过按行分割自行完成反序列化
HTTP请求

HTTP响应

HTTP方法
GET(常用)
作用:获取目标资源的表示。只应获取数据,不应有副作用。
表达式 :GET <request-target> HTTP/1.1
请求目标通常包含路径和查询字符串,如 /api/users?page=2。
-
参数选项:
-
头部 :可携带条件头部(
If-Modified-Since、If-None-Match)实现缓存验证;Accept系列头部协商响应格式。 -
消息体:规范不允许GET携带消息体,虽然某些实现可能支持,但应避免依赖。
-
-
C++实现:
cpp// 使用libcurl curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/api/users?page=2"); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); // 设置Accept头部 struct curl_slist *headers = nullptr; headers = curl_slist_append(headers, "Accept: application/json"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);在Boost.Beast中构造GET请求:
cpphttp::request<http::string_body> req{http::verb::get, "/api/users?page=2", 11}; req.set(http::field::host, "example.com"); req.set(http::field::accept, "application/json");
HEAD
作用 :与GET类似,但服务器不得 返回消息体,只返回头部。常用于检查资源是否存在、获取元数据(如Content-Length)而不传输实际数据。
表达式 :HEAD <request-target> HTTP/1.1
-
参数选项:头部同GET,但响应无主体。
-
C++实现:
cpp// libcurl启用HEAD请求 curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);在Boost.Beast中:
cpphttp::request<http::empty_body> req{http::verb::head, "/api/users", 11};
POST(常用)
作用:向指定资源提交数据,通常导致服务器状态变化或创建子资源(如提交表单、上传文件)。非幂等。
-
表达式 :
POST <request-target> HTTP/1.1 -
参数选项:
-
头部 :
Content-Type指明消息体格式(如application/x-www-form-urlencoded、multipart/form-data、application/json);Content-Length或Transfer-Encoding: chunked描述主体。 -
消息体:包含提交的数据。
-
-
C++实现(libcurl发送JSON):
cppstd::string json = R"({"name":"John"})"; curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, json.size()); headers = curl_slist_append(headers, "Content-Type: application/json");在Boost.Beast中:
cpphttp::request<http::string_body> req{http::verb::post, "/api/users", 11}; req.set(http::field::content_type, "application/json"); req.body() = json; req.prepare_payload(); // 自动设置Content-Length
PUT
作用 :将请求消息体替换目标资源的当前表示。如果资源不存在,服务器可能创建它(取决于实现)。幂等。
表达式 :PUT <request-target> HTTP/1.1
-
参数选项:头部同POST,但语义为完整替换。
-
C++实现:
cpp// libcurl使用自定义请求方法 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);Boost.Beast直接支持:
cpphttp::request<http::string_body> req{http::verb::put, "/api/users/123", 11};
DELETE
-
作用:删除指定资源。幂等。
-
表达式 :
DELETE <request-target> HTTP/1.1 -
参数选项:可包含消息体(但无规范意义),通常不携带。
-
C++实现:
cppcurl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");Boost.Beast:
cpphttp::request<http::empty_body> req{http::verb::delete_, "/api/users/123", 11};
OPTIONS
-
作用 :查询服务器对特定资源支持哪些HTTP方法,或通配(
*)查询服务器整体能力。 -
表达式 :
OPTIONS <request-target> HTTP/1.1,或OPTIONS * HTTP/1.1。 -
参数选项 :响应头部
Allow列出支持的方法(如Allow: GET, HEAD, PUT)。 -
C++实现:
cppcurl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS");解析响应头部
Allow即可。
PATCH
-
作用 :对资源进行部分修改,而非完整替换。非幂等(但可以设计为幂等)。
-
表达式 :
PATCH <request-target> HTTP/1.1 -
参数选项 :
Content-Type指明补丁格式(如application/json-patch+json、application/merge-patch+json)。消息体包含修改指令。 -
C++实现:同PUT,使用自定义方法"PATCH"。
TRACE
-
作用:回显服务器收到的请求,用于调试和测试。安全、幂等。
-
表达式 :
TRACE <request-target> HTTP/1.1 -
参数选项:服务器响应消息体为整个请求消息。
-
注意:由于安全风险(可能泄露信息),现代服务器常禁用TRACE。
-
C++实现:
cppcurl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "TRACE");
CONNECT
-
作用:建立到目标服务器的隧道,通常用于SSL隧道代理。
-
表达式 :
CONNECT <host>:<port> HTTP/1.1 -
参数选项:客户端请求后,代理服务器建立连接并返回200,之后原始数据透传。
-
C++实现:一般由代理库处理,不常用手写。
HTTP状态码
HTTP状态码(Status Code)是服务器响应报文状态行中的三位数字,用于概括请求处理的结果。
状态码是机器可读的,客户端根据它决定下一步行为(如缓存、重定向、重试、显示错误等)。
状态码由首位数字定义类别:
1xx(信息性):请求已接收,继续处理。
2xx(成功):请求成功接收、理解并接受。
3xx(重定向):需要进一步操作以完成请求。
4xx(客户端错误):请求包含语法错误或无法完成。
5xx(服务器错误):服务器无法完成明显有效的请求。
状态码表格
| 状态码 | 原因短语 | 含义与典型场景 |
|---|---|---|
| 1xx 信息性 | ||
| 100 | Continue | 服务器已收到请求头,客户端应继续发送请求体(用于避免发送大消息体被拒)。 |
| 101 | Switching Protocols | 服务器同意客户端切换协议的请求,例如从HTTP/1.1升级到WebSocket。 |
| 102 | Processing | WebDAV:服务器已收到并处理请求,但尚未完成(防止客户端超时)。 |
| 103 | Early Hints | 用于在最终响应前提前返回一些响应头,允许预加载资源。 |
| 2xx 成功 | ||
| 200 | OK | 请求成功。对于GET,响应体包含资源;对于POST,包含操作结果。 |
| 201 | Created | 请求成功并创建了新资源,响应头Location应包含新资源URI。 |
| 202 | Accepted | 请求已接受但尚未处理完成,适用于异步操作。 |
| 203 | Non-Authoritative Information | 返回的元信息来自第三方副本,并非原始服务器。 |
| 204 | No Content | 请求成功,但响应无主体(如DELETE成功)。 |
| 205 | Reset Content | 告知客户端重置文档视图(如表单清空)。 |
| 206 | Partial Content | 成功处理了部分GET请求(用于断点续传或多部分下载)。 |
| 207 | Multi-Status | WebDAV:提供多个独立操作的状态(XML格式)。 |
| 208 | Already Reported | WebDAV:避免重复枚举内部成员。 |
| 226 | IM Used | 服务器完成了GET请求,并返回了实例操作后的结果。 |
| 3xx 重定向 | ||
| 300 | Multiple Choices | 请求资源有多个可选表示,客户端可从中选择(如不同格式)。 |
| 301 | Moved Permanently | 资源已永久移动到新URI,客户端后续请求应使用新URI(更新书签)。 |
| 302 | Found | 资源临时位于其他URI,但客户端仍应使用原URI进行后续请求(常见于临时跳转)。 |
| 303 | See Other | 请求处理结果可另寻URI获取(常用于POST后重定向到结果页)。 |
| 304 | Not Modified | 资源未修改,可使用缓存版本(配合If-Modified-Since或If-None-Match)。 |
| 305 | Use Proxy | 请求必须通过指定代理访问(已废弃,因安全问题)。 |
| 306 | (Unused) | 原预留状态码,现未使用。 |
| 307 | Temporary Redirect | 类似302,但要求客户端保持请求方法不变(即POST重定向后仍用POST)。 |
| 308 | Permanent Redirect | 类似301,但要求客户端保持请求方法不变(即POST重定向后仍用POST)。 |
| 4xx 客户端错误 | ||
| 400 | Bad Request | 请求语法错误,服务器无法理解(如格式错误的JSON、缺少必要字段)。 |
| 401 | Unauthorized | 请求需要身份验证,响应应包含WWW-Authenticate质询。 |
| 402 | Payment Required | 预留用于数字支付系统,目前极少使用。 |
| 403 | Forbidden | 服务器理解请求但拒绝执行(权限不足,且不希望说明原因)。 |
| 404 | Not Found | 服务器找不到请求的资源。 |
| 405 | Method Not Allowed | 请求方法不被支持,响应头Allow应列出允许的方法。 |
| 406 | Not Acceptable | 根据请求头Accept,无法生成客户端可接受的响应。 |
| 407 | Proxy Authentication Required | 类似401,但要求代理服务器进行身份验证。 |
| 408 | Request Timeout | 服务器等待客户端发送请求超时。 |
| 409 | Conflict | 请求与服务器当前状态冲突(如版本冲突、并发修改)。 |
| 410 | Gone | 资源曾经存在,但现已永久移除,且无转发地址。 |
| 411 | Length Required | 请求未指定Content-Length,而服务器需要。 |
| 412 | Precondition Failed | 请求头中条件(如If-Match)未满足。 |
| 413 | Payload Too Large | 请求体过大,服务器拒绝处理。 |
| 414 | URI Too Long | 请求目标URI过长,服务器拒绝处理。 |
| 415 | Unsupported Media Type | 请求体格式不被支持(如服务器只接受JSON,却发送了XML)。 |
| 416 | Range Not Satisfiable | 请求范围无效(如超出资源大小)。 |
| 417 | Expectation Failed | 请求头Expect: 100-continue期望的行为无法满足。 |
| 418 | I'm a teapot | 愚人节玩笑(RFC 2324),服务器拒绝冲泡咖啡(因为它是茶壶)。实际中极少用。 |
| 421 | Misdirected Request | 请求被发送到无法生成响应的服务器(如HTTPS虚拟主机问题)。 |
| 422 | Unprocessable Entity | WebDAV:请求格式正确,但语义错误(如验证失败)。 |
| 423 | Locked | WebDAV:资源被锁定。 |
| 424 | Failed Dependency | WebDAV:请求失败,因为依赖的另一个请求失败。 |
| 425 | Too Early | 服务器不愿意处理可能重放的请求(用于防止重放攻击)。 |
| 426 | Upgrade Required | 客户端应切换协议(如TLS/1.0升级到更高版本)。 |
| 428 | Precondition Required | 请求必须包含条件头(如If-Match)以保证安全。 |
| 429 | Too Many Requests | 客户端在一定时间内发送了太多请求(限流)。 |
| 431 | Request Header Fields Too Large | 请求头字段过大,服务器拒绝处理。 |
| 451 | Unavailable For Legal Reasons | 因法律原因资源不可用(如被屏蔽)。 |
| 5xx 服务器错误 | ||
| 500 | Internal Server Error | 服务器内部错误(通用未知错误)。 |
| 501 | Not Implemented | 服务器不支持请求所需功能(如不支持该方法)。 |
| 502 | Bad Gateway | 作为网关或代理时,从上游服务器收到无效响应。 |
| 503 | Service Unavailable | 服务器暂时过载或维护,无法处理请求。可附带Retry-After头部。 |
| 504 | Gateway Timeout | 作为网关或代理时,未及时从上游服务器收到响应。 |
| 505 | HTTP Version Not Supported | 服务器不支持或拒绝支持请求使用的HTTP版本。 |
| 506 | Variant Also Negotiates | 服务器存在内部配置错误,导致内容协商循环。 |
| 507 | Insufficient Storage | WebDAV:服务器存储空间不足,无法完成请求。 |
| 508 | Loop Detected | WebDAV:服务器检测到无限循环。 |
| 510 | Not Extended | 服务器需要进一步扩展才能处理请求(策略未满足)。 |
| 511 | Network Authentication Required | 客户端需要进行网络认证(如接入Wi-Fi需登录)。 |
HTTPheader
HTTP Header位于请求/响应起始行之后,是一个个键值对,格式为 Name: value,大小写不敏感。它们为消息提供了额外的上下文信息,使得客户端和服务器能够更智能地交互。
根据使用范围,Header可分为:
通用头 :适用于请求和响应,如 Cache-Control、Connection。
请求头 :仅出现在请求中,如 Host、User-Agent。
响应头 :仅出现在响应中,如 Server、Set-Cookie。
实体头 :描述消息体内容的属性,如 Content-Type、Content-Length,可出现在请求或响应中
详解
通用头(General Headers)
Cache-Control
作用:指定请求/响应链上的缓存策略。
常用指令 :no-cache、no-store、max-age=<seconds>、public、private、must-revalidate。
C++实现 :在libcurl中可通过 CURLOPT_HTTPHEADER 设置;在Boost.Beast中通过
cpp
req.set(http::field::cache_control, "no-cache")
Connection
作用:控制当前连接的管理选项,如是否持久连接。
常用值 :keep-alive(默认HTTP/1.1)、close(请求关闭连接)。
C++实现 :服务器根据此头决定是否关闭socket;客户端可通过 CURLOPT_TCP_KEEPALIVE 等控制。
Date
作用:消息生成的日期时间,使用RFC 1123格式。
C++实现 :在C++中可用 std::chrono 系统时钟生成,或使用 gmtime 格式化。
Transfer-Encoding
作用 :指定消息体的传输编码,通常为 chunked。
注意 :不能与 Content-Length 同时出现。
C++实现:解析时需要处理分块数据,发送时需按分块格式组装。
请求头(Request Headers)
Host
作用:必需,指定服务器域名(及端口),用于虚拟主机。
示例 :Host: www.example.com:8080
C++实现:libcurl自动添加,但可覆盖;Boost.Beast需显式设置。
User-Agent
作用:标识客户端软件信息,帮助服务器进行内容适配和统计。
示例 :User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
C++实现 :libcurl默认设为 curl/<version>,可通过 CURLOPT_USERAGENT 修改。
Accept
作用:告知服务器客户端可理解的媒体类型,用于内容协商。
示例 :Accept: text/html, application/xhtml+xml, application/xml;q=0.9
C++实现 :在libcurl中通过 CURLOPT_HTTPHEADER 添加;服务器需解析并选择合适的响应类型。
Accept-Encoding
作用 :告知服务器客户端支持的压缩算法,如 gzip、deflate、br。
示例 :Accept-Encoding: gzip, deflate, br
C++实现 :libcurl会自动添加并解压(需设置 CURLOPT_ACCEPT_ENCODING);服务器需对响应压缩。
Accept-Language
作用:告知服务器客户端偏好的自然语言。
示例 :Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
C++实现:服务器据此选择合适语言版本。
Authorization
作用:携带客户端身份凭证,如Basic认证(Base64编码)、Bearer令牌。
示例 :Authorization: Basic dXNlcjpwYXNz、Authorization: Bearer <token>
C++实现 :libcurl可通过 CURLOPT_USERPWD 设置Basic,或通过 CURLOPT_XOAUTH2_BEARER 等。
Cookie
作用 :携带服务器之前通过 Set-Cookie 设置的会话状态。
示例 :Cookie: sessionId=abc123; theme=dark
C++实现 :libcurl通过 CURLOPT_COOKIE 或 CURLOPT_COOKIEFILE 管理;服务器需解析多个键值对。
Referer
作用:指示当前请求的来源页面URL,用于防盗链、统计分析。
示例 :Referer: https://www.example.com/page.html
C++实现 :libcurl可通过 CURLOPT_REFERER 设置。
Content-Type
作用:指示请求体的媒体类型。
示例 :Content-Type: application/json、multipart/form-data; boundary=---xxx
C++实现 :libcurl设置 CURLOPT_HTTPHEADER;服务器解析时需根据此头选择解析器。
Content-Length
作用:指示请求体的字节长度,若使用分块编码则不出现。
C++实现:发送时需正确计算;接收时读取指定字节数。
If-Modified-Since / If-None-Match
作用:条件请求,用于缓存验证。服务器若资源未变化则返回304 Not Modified。
示例 :If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT、If-None-Match: "etag123"
C++实现 :服务器需比较时间或ETag;客户端可通过 CURLOPT_TIMECONDITION 等设置。
Origin
作用:CORS请求中标识来源域名,服务器据此决定是否允许跨域。
示例 :Origin: https://www.example.com
C++实现 :服务器需检查此头,并返回合适的 Access-Control-Allow-Origin。
响应头(Response Headers)
Server
作用:标识服务器软件信息,通常用于调试。
示例 :Server: Apache/2.4.1 (Unix)
C++实现:可自定义,但建议隐藏具体版本以增强安全性。
Content-Type(同请求)
作用:指示响应体的媒体类型。
Content-Length(同请求)
Content-Encoding
作用:指示响应体使用的压缩算法。
示例 :Content-Encoding: gzip
C++实现:客户端需根据此头解压。
Location
作用:重定向响应中指明新的资源URI。
示例 :Location: https://www.example.com/new-page
C++实现 :客户端需自动跟随(libcurl默认不自动,需设置 CURLOPT_FOLLOWLOCATION)。
Set-Cookie
作用 :要求客户端保存Cookie,可包含属性如 Expires、Path、Domain、Secure、HttpOnly、SameSite。
示例 :Set-Cookie: sessionId=abc123; Path=/; HttpOnly
C++实现 :服务器可发送多个 Set-Cookie 头;客户端需解析存储,下次请求回带。
Cache-Control(通用头)
ETag
作用:资源的实体标签,用于缓存验证。
示例 :ETag: "686897696a7c876b7e"
C++实现 :服务器可基于内容哈希生成;客户端在后续请求中通过 If-None-Match 使用。
Last-Modified
作用:资源最后修改时间,用于缓存验证。
示例 :Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
C++实现 :服务器可从文件系统获取;客户端通过 If-Modified-Since 使用。
Access-Control-Allow-Origin
作用 :CORS响应中指示允许访问的源,若为 * 表示允许所有。
示例 :Access-Control-Allow-Origin: https://www.example.com
C++实现 :服务器需根据请求 Origin 设置此头。
WWW-Authenticate
作用:与401状态码配合,告知客户端认证方式。
示例 :WWW-Authenticate: Basic realm="User Visible Realm"
C++实现:客户端收到401后,根据此头提示用户输入凭证。
常见HTTP Header总结表
| Header名称 | 类型 | 作用简述 | 常见值示例 |
|---|---|---|---|
| Cache-Control | 通用 | 指定缓存策略 | no-cache, max-age=3600 |
| Connection | 通用 | 管理连接选项 | keep-alive, close |
| Date | 通用 | 消息生成时间 | Wed, 21 Oct 2015 07:28:00 GMT |
| Transfer-Encoding | 通用 | 消息体传输编码 | chunked |
| Host | 请求 | 指定服务器域名(必需) | www.example.com:8080 |
| User-Agent | 请求 | 客户端标识 | Mozilla/5.0 ... |
| Accept | 请求 | 客户端可理解的媒体类型 | text/html, application/json;q=0.9 |
| Accept-Encoding | 请求 | 客户端支持的压缩算法 | gzip, deflate, br |
| Accept-Language | 请求 | 客户端偏好的语言 | zh-CN,zh;q=0.9,en;q=0.8 |
| Authorization | 请求 | 客户端身份凭证 | Basic dXNlcjpwYXNz, Bearer <token> |
| Cookie | 请求 | 携带会话状态 | sessionId=abc123; theme=dark |
| Referer | 请求 | 来源页面URL | https://www.example.com/page.html |
| Content-Type | 实体 | 消息体媒体类型 | application/json, text/plain |
| Content-Length | 实体 | 消息体字节长度 | 1024 |
| If-Modified-Since | 请求 | 缓存验证:仅当资源在此时间后修改才返回 | Wed, 21 Oct 2015 07:28:00 GMT |
| If-None-Match | 请求 | 缓存验证:仅当资源ETag不匹配时返回 | "etag123" |
| Origin | 请求 | CORS请求来源 | https://www.example.com |
| Server | 响应 | 服务器软件信息 | Apache/2.4.1 |
| Location | 响应 | 重定向目标URI | https://www.example.com/new |
| Set-Cookie | 响应 | 要求客户端保存Cookie | sessionId=abc123; Path=/; HttpOnly |
| ETag | 响应 | 资源实体标签 | "686897696a7c876b7e" |
| Last-Modified | 响应 | 资源最后修改时间 | Tue, 15 Nov 1994 12:45:26 GMT |
| Access-Control-Allow-Origin | 响应 | CORS允许的源 | * 或 https://www.example.com |
| WWW-Authenticate | 响应 | 认证质询(与401配合) | Basic realm="User Visible Realm" |
| Content-Encoding | 实体 | 响应体压缩算法 | gzip |
HTTP服务器
日志
cpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <chrono>
#include <iomanip>
#include <ctime>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
//RAII风格代码:资源获取即初始化
// 互斥锁类,封装了pthread_mutex_t
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Ptr()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard // RAII风格代码
{
public:
LockGuard(Mutex &lock):_lockref(lock)
{
_lockref.Lock();
}
~LockGuard()
{
_lockref.Unlock();
}
private:
Mutex &_lockref;
};
//日志本体
namespace Logger
{
enum class LogLevel
{
INFO, ///< 信息性消息,记录应用程序的正常运行状态(如启动、配置加载等)
WARNING, ///< 警告,表示潜在的问题或非预期的情形,但应用程序仍能继续运行
ERROR, ///< 错误,表示发生了严重的操作失败,但不影响整个应用程序的继续运行
FATAL, ///< 致命错误,表示严重的故障,通常会导致应用程序终止
DEBUG ///< 调试信息,用于开发和排错阶段,记录详细的内部状态或流程
};
std::string LogLevelToString(LogLevel level)
{
switch (level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::DEBUG:
return "DEBUG";
default:
return "UNKNOWN";
}
}
std::string GetCurrentDateTime()
{
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
std::tm tm_info; // 用户提供的缓冲区
localtime_r(&now_c, &tm_info); // POSIX 线程安全函数
std::ostringstream oss;
oss << std::put_time(&tm_info, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
// 输出角度 -- 刷新策略
// 1. 显示器打印
// 2. 文件写入
// 日志的生成:
// 1. 构建日志字符串
// 2. 根据不同的策略,进行刷新
//策略模式接口
class LogStrategy
{
public:
virtual void LogRefresh(const std::string &message) = 0;
virtual ~LogStrategy() = default;
};
// 控制台日志刷新策略, 日志将来要向显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
// 显示器打印策略刷新
void LogRefresh(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cerr << message << std::endl; // ??
}
~ConsoleStrategy()
{
}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
namespace fs = std::filesystem;
// 文件策略
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
: _logpath(path),
_logfilename(name)
{
if (std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath);
}
catch(const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
if(!_logpath.empty()&&_logpath.back() != '/')
_logpath += '/';
}
// 文件策略刷新
void LogRefresh(const std::string &message) override
{
{
std::string targetlogfile = _logpath + _logfilename;
LockGuard lockguard(_mutex);
std::ofstream logFile(targetlogfile, std::ios::app);// 以追加模式打开文件
if (!logFile.is_open())
{
std::cerr << "无法打开日志文件: " << targetlogfile << std::endl;
return;
}
logFile << message << "\n";
logFile.close();
}
}
~FileLogStrategy()
{
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
// 根据日志等级分类保存的策略类
class LevelFileLogStrategy : public LogStrategy {
public:
/**
* @brief 构造函数,指定日志根目录
* @param log_dir 存放等级日志文件的目录,默认为 "./logs"
*/
LevelFileLogStrategy(const std::string &log_dir = "./logs") : _log_dir(log_dir) {
// 确保目录存在
if (!std::filesystem::exists(_log_dir)) {
std::filesystem::create_directories(_log_dir);
}
// 规范化目录路径,末尾添加 '/'
if (!_log_dir.empty() && _log_dir.back() != '/') {
_log_dir += '/';
}
}
/**
* @brief 刷新日志:根据等级写入对应文件
* @param message 完整的日志消息(格式由 Logger::LogMessage 生成)
*/
void LogRefresh(const std::string &message) override {
// 1. 从消息中提取日志等级
LogLevel level = extractLogLevel(message);
// 2. 构造对应的文件名(例如:INFO.log)
std::string filename = _log_dir + LogLevelToString(level) + ".log";
// 3. 线程安全地追加写入文件
LockGuard lockguard(_mutex);
std::ofstream logFile(filename, std::ios::app);
if (!logFile.is_open()) {
std::cerr << "无法打开日志文件: " << filename << std::endl;
return;
}
logFile << message << "\n";
logFile.close();
}
private:
std::string _log_dir; // 日志根目录
Mutex _mutex; // 文件写入互斥锁
/**
* @brief 从日志消息中解析出等级
* @param msg 完整日志消息,格式为 "[时间][等级][PID][文件:行号] 用户内容"
* @return 对应的 LogLevel 枚举值,解析失败时默认返回 INFO
*/
LogLevel extractLogLevel(const std::string &msg) {
// 寻找第一个 ']' 的位置
size_t first_close = msg.find(']');
if (first_close == std::string::npos) {
return LogLevel::INFO; // 格式错误,默认 INFO
}
// 寻找第二个 '[' 的位置
size_t second_open = msg.find('[', first_close);
if (second_open == std::string::npos) {
return LogLevel::INFO;
}
// 寻找第二个 ']' 的位置
size_t second_close = msg.find(']', second_open);
if (second_close == std::string::npos) {
return LogLevel::INFO;
}
// 提取等级字符串(如 "INFO")
std::string level_str = msg.substr(second_open + 1, second_close - second_open - 1);
// 映射到 LogLevel 枚举
if (level_str == "INFO") return LogLevel::INFO;
if (level_str == "WARNING") return LogLevel::WARNING;
if (level_str == "ERROR") return LogLevel::ERROR;
if (level_str == "FATAL") return LogLevel::FATAL;
if (level_str == "DEBUG") return LogLevel::DEBUG;
return LogLevel::INFO; // 未知等级,默认 INFO
}
};
class Logger
{
public:
Logger()
{}
void UseConsoleStrategy()
{
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
void UseLevelFileStrategy(const std::string &log_dir = "./logs")
{
_strategy = std::make_unique<LevelFileLogStrategy>(log_dir);
}
void Debug(const std::string &message)
{
if (_strategy!= nullptr)
{
_strategy->LogRefresh("[DEBUG] " + GetCurrentDateTime() + " - " + message);
}
}
//日志内容
//一条完整的日志信息=> [日志级别] + 当前时间 + 进程ID + 文件名 + 行号 + 日志信息
//我们想以RAII形式刷新日志信息
class LogMessage
{
public:
LogMessage(LogLevel level, const std::string &filename, size_t line, Logger &logger)
: _level(level), _filename(filename), _line(line),_logger(logger)
{
_cur_time = GetCurrentDateTime();
_pid=getpid();
// 构建日志左半部分信息
std::stringstream oss;
oss <<"["<<_cur_time<<"]"
<<"["<<LogLevelToString(_level)<<"]"
<<"["<<_pid<<"]"
<<"[" << filename << ":"
<< line << "]";
_LogInfo = oss.str();
}
template<typename T>
LogMessage& operator<<(const T&info)
{
std::stringstream oss;
oss<< info;
_LogInfo += oss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy != nullptr)
{
_logger._strategy->LogRefresh(_LogInfo);
}
}
private:
std::string _cur_time;
LogLevel _level;
pid_t _pid;
std::string _filename;
size_t _line;
std::string _LogInfo;
Logger &_logger ;//方便进行后续策略方式刷新
};
//对LogMessage进行()重载
//必须用拷贝,否则会导致<<重载的时候内容消失
LogMessage operator()(LogLevel level, const std::string filename, size_t line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{}
private:
std::unique_ptr<LogStrategy> _strategy;
};
//日志对象全局使用
Logger logger;
#define ENABLE_LOG_CONSOLE() logger.UseConsoleStrategy()
#define ENABLE_LOG_FILE() logger.UseFileStrategy()
#define Log(level) logger(level, __FILE__, __LINE__)
#define ENABLE_LOG_LEVEL_FILE(log_dir) logger.UseLevelFileStrategy(log_dir)
}
客户端
cpp
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define CONV(address) ((struct sockaddr*)address)
//网络客户端封装
class InetAddr
{
private:
std::string IP_;
uint16_t port_;
struct sockaddr_in address_;
socklen_t len_;
public:
InetAddr()=default;
InetAddr(const struct sockaddr_in& address) : address_(address),len_(sizeof(address))
{
IP_ = inet_ntoa(address_.sin_addr); // 将二进制IP转换为字符串
port_ = ntohs(address_.sin_port); // 端口转换正确
}
InetAddr(uint16_t port,const std::string ip="0.0.0.0"):IP_(ip),port_(port)
{
bzero(&address_, sizeof(address_));
address_.sin_family = AF_INET;
address_.sin_addr.s_addr = inet_addr(IP_.c_str());
address_.sin_port = htons(port_);
len_=sizeof(address_);
}
struct sockaddr *GetNetAddress()
{
return CONV(&address_);
}
~InetAddr()=default;
socklen_t len()
{
return len_;
}
bool operator==(const InetAddr&addrs)
{
return (this-> IP_==addrs.IP_)&&(this->port_==addrs.port_);
}
void operator=(struct sockaddr_in &addrs)
{
char ipstr[32];
inet_ntop(AF_INET, &(addrs.sin_addr), ipstr, sizeof(ipstr));
IP_ = ipstr;
port_ = ntohs(addrs.sin_port);
}
std::string ToString()
{
return "[" + IP_ + ":" + std::to_string(port_) + "]";
}
};
套接字
cpp
#pragma once
#include "InetAddr.hpp"
#include "Log.hpp"
#include<iostream>
#include<cstdlib>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
namespace CustomProtocol
{
using namespace Logger;
enum
{
SUCCESS = 0,
SOCKET_ERROR = -1,
BIND_ERR=-2,
LISTEN_ERR=-3,
};
class Socket
{
private:
public:
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie() = 0;
virtual ssize_t Recv(std::string *OutMessage) = 0;
virtual int Send(const std::string &InMessage) = 0;
virtual void Close() = 0;
virtual bool Connect(InetAddr &addr) = 0;
virtual int Socketfd()=0;
virtual std::shared_ptr<Socket> Accept(InetAddr &addr) = 0;
public:
Socket()=default;
~Socket()=default;
void BuildTcpSocket(uint16_t port)
{
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie();
}
};
class TcpSocket : public Socket
{
private:
int sockfd_;
public:
TcpSocket()
:sockfd_(0)
{}
TcpSocket(int sockfd)
:sockfd_(sockfd)
{}
~TcpSocket()
{
close(sockfd_);
}
void CreateSocketOrDie() override
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0)
{
Log(LogLevel::FATAL)<<"socket create error";
exit(SOCKET_ERROR);
}
}
void BindSocketOrDie(uint16_t port) override
{
InetAddr addr(port);
if (bind(sockfd_, (struct sockaddr*)addr.GetNetAddress(), addr.len()) != 0)
{
Log(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
}
void ListenSocketOrDie() override
{
if (listen(sockfd_, 5) != 0)
{
Log(LogLevel::FATAL) << "listen socket error";
exit(LISTEN_ERR);
}
}
std::shared_ptr<Socket> Accept(InetAddr &addr) override
{
struct sockaddr_in clientaddr;
socklen_t len=sizeof(clientaddr);
int connfd = accept(sockfd_, CONV(&clientaddr), &len);
if(connfd<0)
{
Log(LogLevel::FATAL)<<"accept socket error";
return nullptr;
}
addr=clientaddr;
return std::make_shared<TcpSocket>(connfd);
}
ssize_t Recv(std::string *OutMessage) override
{
char inbuffer[1024];
ssize_t n = recv(sockfd_, inbuffer, sizeof(inbuffer)-1, 0);
if(n > 0)
{
inbuffer[n] = 0;
*OutMessage += inbuffer; // 追加写入的
}
return n;
}
int Send(const std::string &InMessage) override
{
return send(sockfd_, InMessage.c_str(), InMessage.size(), 0);
}
void Close() override
{
if(sockfd_<0)
return;
close(sockfd_);
}
bool Connect(InetAddr &addr) override
{
int n = connect(sockfd_, addr.GetNetAddress(), addr.len());
if(n < 0)
return false;
else
return true;
}
int Socketfd() override
{
return sockfd_;
}
};
class UdpSocket : public Socket
{
};
}
TCP服务器
cpp
#include"Socket.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
#include<memory>
#include<signal.h>
#include<functional>
static const int cport=8080;
using namespace CustomProtocol;
using Handler_t = std::function<std::string(std::string&)>;
class TcpServer
{
private:
uint16_t port_;
std::unique_ptr<Socket> listsock_;
Handler_t handler_;
public:
TcpServer(Handler_t handler,uint16_t port=cport)
:port_(port),handler_(handler)
{
listsock_=std::make_unique<TcpSocket>();
listsock_->BuildTcpSocket(port_);
}
~TcpServer()
{}
void Loop()
{
signal(SIGCHLD,SIG_IGN);
while(1)
{
InetAddr clientaddr;
auto sockfd=listsock_->Accept(clientaddr);
if(!sockfd)
continue;
Log(LogLevel::DEBUG)<<"accept one new link ,socketfd: "<<clientaddr.ToString();
if(fork()==0)
{
ServerIO(sockfd,clientaddr);
sockfd->Close();
exit(1);
}
sleep(1);
}
}
private:
void ServerIO(std::shared_ptr<Socket> sockfd,InetAddr& clientaddr)
{
std::string inbuffer,outbuffer;
while(1)
{
outbuffer.clear();
int n=sockfd->Recv(&inbuffer);
if(n<=0)
{
Log(LogLevel::ERROR)<<"client quit "<<clientaddr.ToString();
break;
}
Log(LogLevel::DEBUG)<<"recv from "<<clientaddr.ToString()<<" : "<<inbuffer;
if(handler_)
outbuffer = handler_(inbuffer);
if(outbuffer.empty())
continue;
int m=sockfd->Send(outbuffer);
if(m<=0)
{
Log(LogLevel::ERROR)<<"send to "<<clientaddr.ToString()<<" failed";
break;
}
Log(LogLevel::DEBUG)<<"send to "<<clientaddr.ToString()<<" : "<<outbuffer;
}
}
};
HTTP协议
cpp
#pragma once
#include <iostream>
#include<cstdio>
#include <string>
#include <unordered_map>
#include <sstream>
#include <fstream>
#include <stdexcept>
#include<functional>
#include<sys/stat.h>
#include"Log.hpp"
// #define DEBUG 1
const std::string linesep = "\r\n";
const std::string spacesep=" ";
const std::string headersep=": ";
const std::string suffix_sep=".";
const std::string argsep="?";
const std::string c_http_version="HTTP/1.1";
const std::string c_first_page="index.html";
const std::string c_wwwroot="./wwwroot";
const std::string page_404=c_wwwroot+"/404.html";
using namespace Logger;
// RAII文件读取器
class FileReader
{
private:
std::ifstream file_;
public:
explicit FileReader(const std::string& filename)
: file_(filename)
{
if (!file_.is_open())
{
throw std::runtime_error("Failed to open file: " + filename);
}
}
~FileReader()
{
// 析构函数会自动关闭文件
}
std::string ReadAll()
{
std::string content;
std::string line;
while (std::getline(file_, line))
{
content += line + "\n";
}
return content;
}
// 获取底层文件流引用,用于二进制读取等操作
std::ifstream& GetFileStream()
{
return file_;
}
// 二进制方式读取整个文件
std::string ReadAllBinary()
{
file_.seekg(0, std::ios::end);
int filesize = file_.tellg();
file_.seekg(0, std::ios::beg);
std::string content;
content.resize(filesize);
file_.read((char*)content.c_str(), filesize);
return content;
}
// 禁用拷贝构造和赋值
FileReader(const FileReader&) = delete;
FileReader& operator=(const FileReader&) = delete;
};
//工具类
class Util
{
public:
//空:没有读到完整行
//"\r\n":读到空行
//其他:正常读取到一行
static std::string ReadLine(std::string& str)
{
auto pos = str.find(linesep);
if(pos == std::string::npos)
return "";
std::string line= str.substr(0, pos);
if(line.empty())
return linesep;
str.erase(0, line.size() + linesep.size());
return line;
}
static std::string ReadFile(const std::string& filename)
{
try
{
// 使用RAII FileReader封装,自动管理文件资源
FileReader reader(filename);
// 采用二进制方式读取数据
std::ifstream& in = reader.GetFileStream();
in.seekg(0, std::ios::end);
int filesize = in.tellg();
in.seekg(0, std::ios::beg);
std::string content;
content.resize(filesize);
in.read((char*)content.c_str(), filesize);
// FileReader析构时会自动关闭文件(RAII)
return content;
}
catch (const std::exception& e)
{
Log(LogLevel::ERROR) << "ReadFile error: " << filename << " - " << e.what();
return std::string();
}
// catch (const std::exception& e)
// {
// Log(LogLevel::ERROR) << "open file error: " << filename << " - " << e.what();
// return "";
// }
}
};
// HTTP请求类
class HttpRequest
{
private:
bool ParseReqLine(std::string &httpstr)
{
std::string line = Util::ReadLine(httpstr);
if (line.empty() || line == linesep)
return false;
std::stringstream ss(line);
ss >> method_ >> uri_ >> version_;
// 检查是否成功读取所有三个字段
if (ss.fail() || method_.empty() || uri_.empty() || version_.empty())
{
Log(LogLevel::ERROR) << "request line format error: " << line;
return false;
}
if(uri_=="/")
{
uri_+=c_first_page;
}
uri_=c_wwwroot+uri_;
return true;
}
bool ParseHeader(std::string &httpstr)
{
std::string line;
while (true)
{
line = Util::ReadLine(httpstr);
// 遇到空行表示头部结束
if (line == linesep)
return true;
// 没有读到完整行
if (line.empty())
return false;
auto pos = line.find(headersep);
if (pos == std::string::npos)
{
Log(LogLevel::ERROR) << "header format error: " << line;
return false;
}
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + headersep.size());
header_.insert(std::make_pair(key, value));
}
}
bool ParseText(std::string &httpstr)
{
// 1. 有没有正文??结合方法
if (strcasecmp(method_.c_str(), "GET") == 0)
{
auto pos = uri_.find(argsep);
if(pos == std::string::npos)
{
text_ = std::string();
return true;
}
else
{
std::cout << "uri_: "<< uri_ << std::endl;
std::string temp = uri_;
uri_ = temp.substr(0, pos);
std::cout << "uri_: "<< uri_ << std::endl;
text_ = temp.substr(pos+argsep.size());
std::cout << "text_: "<< text_ << std::endl;
}
}
else
{
if (header_.find("Content-Length") == header_.end())
{
text_ = "";
}
// 2. 正文多大?从哪开始?
// POST方法
int content_len = std::stoi(header_["Content-Length"]);
text_ = httpstr.substr(0, content_len);
httpstr.erase(0, content_len);
}
return true;
}
public:
// 反序列化
bool Deserialize(std::string &httpstr)
{
std::cout << httpstr;
// 1. 解析请求行
bool n = ParseReqLine(httpstr);
(void)n;
// Log(LogLevel::DEBUG)<<"method_# "<<method_;
// Log(LogLevel::DEBUG)<<"uri_# "<<uri_;
// Log(LogLevel::DEBUG)<<"version_# "<<version_;
// std::cout<<httpstr<<std::endl;
//解析报头
n=ParseHeader(httpstr);
(void)n;
blank_line_ = linesep;
// for(auto &it:header_)
// {
// std::cout<<it.first<<"# "<<it.second<<"\r\n\r\n";
// }
// std::cout<<"start|"<<httpstr<<"|end"<<std::endl;
//解析正文
n=ParseText(httpstr);
(void)n;
return true;
}
std::string URI()
{
return uri_;
}
std::string RequestContent()
{
return Util::ReadFile(uri_);
}
std::string Suffix()
{
size_t pos = uri_.find_last_of(".");
if (pos != std::string::npos)
{
suffix_ = uri_.substr(pos);
}
return suffix_;
}
std::string Text()
{
return text_;
}
void DebugPrint()
{
std::cout << "_version: " << version_ << std::endl;
std::cout << "_uri: " << uri_ << std::endl;
std::cout << "_method: " << method_ << std::endl;
for (auto &header : header_)
{
std::cout << "==>" << header.first << " # " << header.second << std::endl;
}
std::cout << blank_line_;
std::cout << text_ << std::endl;
}
private:
std::string method_;
std::string uri_;
std::string version_;
std::unordered_map<std::string, std::string> header_;
std::string blank_line_;
std::string text_;
std::string suffix_;//文件后缀名
};
// HTTP响应类
class HttpResponse
{
private:
std::string version_;
int code_;
std::string code_desc_;
std::unordered_map<std::string, std::string> header_;
std::string blank_line_;
std::string text_;
public:
HttpResponse()
:version_(c_http_version),code_(0),blank_line_(linesep)
{
}
void SetStatusCode(int code)
{
code_=code;
code_desc_=SetStatusCodeDescription(code);
}
void SetHeader(std::string key,std::string value)
{
//存在就覆盖,不存在就添加
header_[key]=value;
}
void SetText(const std::string text)
{
text_=text;
}
size_t TextSize()
{
return text_.size();
}
// 序列化
std::string Serialize()
{
// 构建状态行
std::string respstr = version_;
respstr += spacesep;
respstr += std::to_string(code_);
respstr += spacesep;
respstr += code_desc_;
respstr += linesep;
// 构建报头
for(auto &header : header_)
{
std::string line = header.first + headersep + header.second + linesep;
respstr += line;
}
// 构建空行
respstr += blank_line_;
// 构建正文
respstr += text_;
return respstr;
}
private:
std::string SetStatusCodeDescription(int code)
{
switch(code)
{
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 204:
return "No Content";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 304:
return "Not Modified";
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
default:
return "Unknown Status";
}
}
};
using server_t =std::function<void(HttpRequest& req,HttpResponse& resp)>;
class MiniType
{
public:
static std::string Suffix2MimeType(const std::string &suffix)
{
auto iter = mini_type_.find(suffix);
if (iter != mini_type_.end())
return iter->second;
else
return "text/html";
}
static std::string Suffix2MimeType(const std::string &suffix)
{
auto iter = mini_type_.find(suffix);
if(iter != mini_type_.end())
{
return iter->second;
}
else
{
return "text/html";
}
}
private:
static std::unordered_map<std::string, std::string> mini_type_;
};
// 初始化10种常见的文件后缀到Content-Type的映射
std::unordered_map<std::string, std::string> MiniType::mini_type_ = {
// 文本类型
{".txt", "text/plain"},
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
// 图片类型
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".png", "image/png"},
{".gif", "image/gif"},
// 应用类型
{".pdf", "application/pdf"},
{".json", "application/json"},
{".xml", "application/xml"},
// 脚本类型
{".js", "application/javascript"},
// 其他常用类型
{".zip", "application/zip"},
{".mp3", "audio/mpeg"},
{".mp4", "video/mp4"}
};
class HttpProtocol
{
public:
HttpProtocol()=default;
void RegisterService(const std::string &uri, server_t service)
{
std::string key = c_wwwroot + uri;
http_server_[key] = service; // key -> wwwroot/Login
}
bool IsReqService(const std::string &uri)
{
auto iter = http_server_.find(uri);
if (iter == http_server_.end())
return false;
else
return true;
}
std::string HandlerHttpRequest(std::string &req)
{
#ifdef DEBUG
std::cout << "**************" << std::endl;
std::cout << req << std::endl;
std::string status_line = "HTTP/1.1 200 OK\r\n";
status_line += "\r\n";
std::string html = '<html><body><h1>hello world!</h1></body></html>';
return status_line + html;
#endif
HttpRequest http_req;
http_req.Deserialize(req);
HttpResponse http_resp;
if(IsReqService(http_req.URI()))
{
http_server_[http_req.URI()](http_req, http_resp);
}
else
{
// 1. 保证报文完整性 --- IGN
// 2. 反序列化
std::cout<<"req: "<<req<<std::endl;
std::string suffix = http_req.Suffix();
std::cout<<"suffix: "<<suffix<<std::endl;
// 3. 根据不同的请求方法和请求URI,给用户返回不同的报文
// 根据结构化http_req 转换 成为 结构化的http_resp
std::string content = http_req.RequestContent();
if(content.empty())
{
// 404
http_resp.SetStatusCode(302);
http_resp.SetHeader("Location", "/404.html");
// http_resp.SetText(Util::ReadFile(page_404));
}
else
{
http_resp.SetStatusCode(200);
http_resp.SetHeader("Content-Length",std::to_string(content.size()));
http_resp.SetHeader("Connection", "close");
http_resp.SetText(content);
}
return http_resp.Serialize();
}
}
~HttpProtocol()
{}
private:
std::unordered_map<std::string, server_t> http_server_;
};
HTTP服务器运行
cpp
#include "TcpServer.hpp"
#include "Http.hpp"
#include <memory>
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t" << proc << " port" << std::endl;
}
// 使用HTTP 实现restful风格的服务接口
// GET /Login HTTP/1.1
// GET /exec HTTP/1.1
//
// ls -a -l
// 可不可以是另一个http的client?
void Login(HttpRequest&req, HttpResponse &resp)
{
std::cout << "\nLogin functon been called!" << std::endl;
req.DebugPrint();
std::string data = req.Text();
std::cout << "data is: " << data << std::endl; // name=aaaaa&passwd=bbbbb
// 根据分隔符 name aaaaa passwd bbb
// 访问数据库, 查找数据库
resp.SetStatusCode(200);
resp.SetHeader("Content-Type", MiniType::Suffix2MimeType(".txt"));
resp.SetText("Login success!");
resp.SetHeader("Content-Length", std::to_string(resp.TextSize()));
}
void Register(HttpRequest&req, HttpResponse &resp)
{
}
void GetProductList(HttpRequest&req, HttpResponse &resp)
{
}
void Search(HttpRequest&req, HttpResponse &resp)
{
}
void Connect(HttpRequest&req, HttpResponse &resp)
{
// 我们的http是浏览器或者其他客户端的服务端
// 但是我们的服务端,可不可以是其他server的客户端!
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = std::stoi(argv[1]);
// 2. 定义HTTP协议
std::unique_ptr<HttpProtocol> protocol = std::make_unique<HttpProtocol>();
protocol->RegisterService("/Login", Login);
protocol->RegisterService("/Register", Register);
protocol->RegisterService("/Search", Search);
protocol->RegisterService("/api/getprocutlist", GetProductList);
// 3. 定义网络对象
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
[&protocol](std::string &inbuffer)->std::string{
return protocol->HandlerHttpRequest(inbuffer);
}, port
);
// 4. 启动
tsvr->Loop();
return 0;
}
客户端运行
我们可以直接利用我们的浏览器测试

本期关于HTTP协议相关的内容到这里结束了,喜欢请点个赞谢谢
封面图自取:
