Linux网络:应用层HTTP网络协议

本期我们就来学习一个常见的协议:HTTP协议

HTTP协议是我们日常生活中最常见的协议。如果你经常浏览网站就对它不陌生。那么它究竟有什么奥秘呢?我们来学习一下吧!

相关代码已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢请点个赞谢谢

目录

HTTP协议介绍

URL

URL结构

Scheme(协议方案)

[User Info(用户信息)](#User Info(用户信息))

Host(主机名)

Port(端口号)

Path(路径)

Query(查询字符串)

Fragment(片段标识符)

URL编码与解码

urlencode和urldencode

HTTP协议请求与响应格式

HTTP请求

HTTP响应

HTTP方法

GET(常用)

HEAD

POST(常用)

PUT

DELETE

OPTIONS

PATCH

TRACE

CONNECT

HTTP状态码

状态码表格

HTTPheader

详解

[通用头(General Headers)](#通用头(General Headers))

[请求头(Request Headers)](#请求头(Request Headers))

[响应头(Response Headers)](#响应头(Response Headers))

[常见HTTP Header总结表](#常见HTTP Header总结表)

HTTP服务器

日志

客户端

套接字

TCP服务器

HTTP协议

HTTP服务器运行

客户端运行


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

作用: 指定客户端与服务器通信时使用的应用层协议。常见的有httphttpsftpfile等。协议决定了数据格式、默认端口以及安全行为。在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连接时,必须将主机名和端口组合成端点。使用telnetcurl测试非标准端口是调试常用手段。

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-SinceIf-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请求:

    cpp 复制代码
    http::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");

作用 :与GET类似,但服务器不得 返回消息体,只返回头部。常用于检查资源是否存在、获取元数据(如Content-Length)而不传输实际数据。

表达式HEAD <request-target> HTTP/1.1

  • 参数选项:头部同GET,但响应无主体。

  • C++实现

    cpp 复制代码
    // libcurl启用HEAD请求
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);

    在Boost.Beast中:

    cpp 复制代码
    http::request<http::empty_body> req{http::verb::head, "/api/users", 11};
    复制代码

POST(常用)

作用:向指定资源提交数据,通常导致服务器状态变化或创建子资源(如提交表单、上传文件)。非幂等。

  • 表达式POST <request-target> HTTP/1.1

  • 参数选项

    • 头部Content-Type指明消息体格式(如application/x-www-form-urlencodedmultipart/form-dataapplication/json);Content-LengthTransfer-Encoding: chunked描述主体。

    • 消息体:包含提交的数据。

  • C++实现(libcurl发送JSON):

    cpp 复制代码
    std::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中:

    cpp 复制代码
    http::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直接支持:

    cpp 复制代码
    http::request<http::string_body> req{http::verb::put, "/api/users/123", 11};

DELETE

  • 作用:删除指定资源。幂等。

  • 表达式DELETE <request-target> HTTP/1.1

  • 参数选项:可包含消息体(但无规范意义),通常不携带。

  • C++实现

    cpp 复制代码
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");

    Boost.Beast:

    cpp 复制代码
    http::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++实现

    cpp 复制代码
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS");
    复制代码

    解析响应头部Allow即可。

PATCH

  • 作用 :对资源进行部分修改,而非完整替换。非幂等(但可以设计为幂等)。

  • 表达式PATCH <request-target> HTTP/1.1

  • 参数选项Content-Type指明补丁格式(如application/json-patch+jsonapplication/merge-patch+json)。消息体包含修改指令。

  • C++实现:同PUT,使用自定义方法"PATCH"。

TRACE

  • 作用:回显服务器收到的请求,用于调试和测试。安全、幂等。

  • 表达式TRACE <request-target> HTTP/1.1

  • 参数选项:服务器响应消息体为整个请求消息。

  • 注意:由于安全风险(可能泄露信息),现代服务器常禁用TRACE。

  • C++实现

    cpp 复制代码
    curl_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-SinceIf-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-ControlConnection

请求头 :仅出现在请求中,如 HostUser-Agent

响应头 :仅出现在响应中,如 ServerSet-Cookie

实体头 :描述消息体内容的属性,如 Content-TypeContent-Length,可出现在请求或响应中

详解

通用头(General Headers)

Cache-Control

作用:指定请求/响应链上的缓存策略。

常用指令no-cacheno-storemax-age=<seconds>publicprivatemust-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

作用 :告知服务器客户端支持的压缩算法,如 gzipdeflatebr

示例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 dXNlcjpwYXNzAuthorization: Bearer <token>

C++实现 :libcurl可通过 CURLOPT_USERPWD 设置Basic,或通过 CURLOPT_XOAUTH2_BEARER 等。

Cookie

作用 :携带服务器之前通过 Set-Cookie 设置的会话状态。

示例Cookie: sessionId=abc123; theme=dark

C++实现 :libcurl通过 CURLOPT_COOKIECURLOPT_COOKIEFILE 管理;服务器需解析多个键值对。

Referer

作用:指示当前请求的来源页面URL,用于防盗链、统计分析。

示例Referer: https://www.example.com/page.html

C++实现 :libcurl可通过 CURLOPT_REFERER 设置。

Content-Type

作用:指示请求体的媒体类型。

示例Content-Type: application/jsonmultipart/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 GMTIf-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,可包含属性如 ExpiresPathDomainSecureHttpOnlySameSite

示例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协议相关的内容到这里结束了,喜欢请点个赞谢谢

封面图自取:

相关推荐
1234567890@world2 小时前
FFmpeg | Day1 FFmpege音视频开发与学习
学习·ffmpeg·音视频
wanhengidc2 小时前
选择站群服务器的好处
运维·服务器·网络·安全·智能手机
二年级程序员2 小时前
认识与了解 C++
开发语言·c++
古城小栈2 小时前
Rust跨平台编译打包 之 三大战役
开发语言·后端·rust
RoboWizard2 小时前
解锁高效办公新体验 金士顿高速闪存盘
运维·服务器·网络·缓存·智能手机
TsukasaNZ2 小时前
代码性能剖析工具
开发语言·c++·算法
Trouvaille ~2 小时前
【项目篇】从零手写高并发服务器(五):Channel事件管理与Poller模块
运维·服务器·c++·reactor·高并发·多路转接·epoll
sensen_kiss2 小时前
CPT306 Principles of Computer Games Design 电脑游戏设计原理 Pt.2 游戏引擎
学习·游戏引擎
keep intensify2 小时前
康复训练 7
网络·网络协议·tcp/ip