网络:5.应用层协议HTTP
应用层协议HTTP
- (http是一个应用层协议,底层用的tcp)
一.HTTP补充知识:
域名和IP之间的关系

二.HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。
在互联网世界中,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。
它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如HTML文档)。
HTTP协议是客户端与服务器之间通信的基础。客户端通过HTTP协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP协议是一个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。
三.认识URL
平时我们俗称的 “网址” 其实就是说的 URL(统一资源定位符)也就是超链接

四.urlencode和urldecode(了解)
像 / ? : 等这样的字符, 已经被url 当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
例如:

“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程;
urlencode工具
浏览器urlencode;服务器urldecode;-- 这种模式叫作B/S模式
五.HTTP协议请求与响应格式
1. HTTP请求
- http协议,序列和反序列化用的是特殊字符(空格或换行符)进行子串拼接,且不依赖任何第三方库

- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用
\r\n分隔;遇到空行表示Header部分结束 - Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;

2. HTTP响应
完整请求获取方法:没法直接确定读到原完整报文,但是我有方法读到当前请求的完整报头!
- 读取到完整的请求报头[空行];
- 对报头进行反序列化,提取一个属性:Content-Length:有效载荷的长度;
- 在从剩余的字符串内容中,提取content-length个字符。

- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.

基本的应答格式

六.HTTP的方法

其中最常用的就是GET方法和POST方法.
请求是怎么表示自己要请求什么?资源服务端,资源在哪里?
资源位置用URI表示;资源都放在web(http)根目录下;
如果是GET/服务器要自动拼接首页(/index.html或/index.htm);
网页内容,必须是在服务器特定路径下的文件(图片,视频…,CSS,jS…).
1. HTTP常见方法
(1).GET方法(重点)
(GET: 获取资源)
用途:用于请求URL指定的资源。
示例: GET /index.html HTTP/1.1
特性:指定资源经服务器端解析后返回响应内容。
form表单:https://www.runoob.com/html/html-forms.html
std::string GetFileContentHelper(const std::string &path)
{// 一份简单的读取二进制文件的代码std::ifstream in(path, std::ios::binary);if (!in.is_open())return "";in.seekg(0, in.end);int filesize = in.tellg();in.seekg(0, in.beg);std::string content;content.resize(filesize);in.read((char *)content.c_str(), filesize);// std::vector<char> content(filesize);// in.read(content.data(), filesize);in.close();return content;
}
(2).POST方法(重点)
用途:用于传输实体的主体,通常用于提交表单数据。
示例: POST /submit.cgi HTTP/1.1
特性:可以发送大量的数据给服务器,并且数据包含在请求体中。
form表单:https://www.runoob.com/html/html-forms.html


(3).PUT方法(不常用)
防止用户给服务器乱上传东西,大多数浏览器都把这个方法禁掉了,
用途:用于传输文件,将请求报文主体中的文件保存到请求URL指定的位置。
示例: PUT /example.html HTTP/1.1
特性:不太常用,但在某些情况下,如RESTful API中,用于更新资源。
(4).HEAD方法
用途:与GET方法类似,但不返回报文主体部分,仅返回响应头。
示例: HEAD /index.html HTTP/1.1
特性:用于确认URL的有效性及资源更新的日期时间等。
// curl -i 显示
$ curl -i www.baidu.com
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 2381
Content-Type: text/html
Date: Sun, 16 Jun 2024 08:38:04 GMT
Etag: "588604dc-94d"
Last-Modified: Mon, 23 Jan 2017 13:27:56 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
<!DOCTYPE html>
...// 使用head方法,只会返回响应头
$ curl --head www.baidu.com
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 277
Content-Type: text/html
Date: Sun, 16 Jun 2024 08:43:38 GMT
Etag: "575e1f71-115"
Last-Modified: Mon, 13 Jun 2016 02:50:25 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
(5).DELETE方法(不常用)
用途:用于删除文件,是PUT的相反方法。
示例: DELETE /example.html HTTP/1.1
特性:按请求URL删除指定的资源。
(6).OPTIONS方法
用途:用于查询针对请求URL指定的资源支持的方法。
示例: OPTIONS * HTTP/1.1
特性:返回允许的方法,如GET、POST等。
不支持的效果
// 搭建一个nginx用来测试
// sudo apt install nginx
// sudo nginx -- 开启
// ps ajx | grep nginx -- 查看
// sudo nginx -s stop -- 停止服务$ sudo nginx -s stop
$ ps ajx | grep nginx
2944845 2945390 2945389 2944845 pts/1 2945389 S+ 1002 0:00 grep --color=auto nginx$ sudo nginx
$ ps axj | grep nginx
1 2945393 2945393 2945393 ? -1 Ss 0 0:00 nginx: master process nginx
2945393 2945394 2945393 2945393 ? -1 S 33 0:00 nginx: worker process
2945393 2945395 2945393 2945393 ? -1 S 33 0:00 nginx: worker process
2944845 2945397 2945396 2944845 pts/1 2945396 S+ 1002 0:00 grep --color=auto nginx// -X(大x) 指明方法
$ curl -X OPTIONS -i http://127.0.0.1/
HTTP/1.1 405 Not Allowed
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 16 Jun 2024 08:48:22 GMT
Content-Type: text/html
Content-Length: 166
Connection: keep-alive<html>
<head><title>405 Not Allowed</title></head>
<body>
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
支持的效果
HTTP/1.1 200 OK
Allow: GET, HEAD, POST, OPTIONS
Content-Type: text/plain
Content-Length: 0
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 16 Jun 2024 09:04:44 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization// 注意:这里没有响应体,因为Content-Length为0
七.HTTP的状态码

最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
| 状态码 | 含义 | 应用样例 |
|---|---|---|
| 100 | Continue | 上传大文件时,服务器告诉客户端可以继续上传 |
| 200 | OK | 访问网站首页,服务器返回网页内容 |
| 201 | Created | 发布新文章,服务器返回文章创建成功的信息 |
| 204 | No Content | 删除文章后,服务器返回“无内容”表示操作成功 |
| 301 | Moved Permanently 永久式重定向 | 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用 |
| 302 | Found / See Other 临时重定向 | 用户登录成功后,重定向到用户首页 |
| 304 | Not Modified | 浏览器缓存机制,对未修改的资源返回304状态码 |
| 400 | Bad Request | 填写表单时,格式不正确导致提交失败 |
| 401 | Unauthorized | 访问需要登录的页面时,未登录或认证失败 |
| 403 | Forbidden | 尝试访问你没有权限查看的页面 |
| 404 | Not Found | 访问不存在的网页链接 |
| 500 | Internal Server Error | 服务器崩溃或数据库错误导致页面无法加载 |
| 502 | Bad Gateway | 代理服务器无法从上游服务器获取有效响应 |
| 503 | Service Unavailable | 服务器维护或过载,暂时无法处理请求 |
以下是仅包含重定向相关状态码的表格:
| 状态码 | 含义 | 是否为临时重定向 | 应用样例 |
|---|---|---|---|
| 301 | Moved Permanently | 否(永久重定向) | 网站换域名后,自动跳转到新域名; 搜索引擎更新网站链接时使用(最大意义) |
| 302 | Found 或 See Other | 是(临时重定向) | 用户登录成功后,重定向到用户首页 |
| 307 | Temporary Redirect | 是(临时重定向) | 临时重定向资源到新的位置(较少使用) |
| 308 | Permanent Redirect | 否(永久重定向) | 永久重定向资源到新的位置(较少使用) |
重定向的htp的请求,至少是三部分:状态行,报头,空行;正文常常没有.

关于重定向的验证,以301为代表
**HTTP状态码301(永久重定向)和302(临时重定向)都依赖Location选项。**以下是关于两者依赖Location选项的详细说明:
- HTTP状态码301(永久重定向)
- 网站更换域名,或者更换网址
- 当服务器返回HTTP 301状态码时,表示请求的资源已经被永久移动到新的位置。
- 在这种情况下,服务器会在响应中添加一个Location头部,用于指定资源的新位置。这个Location头部包含了新的URL地址,浏览器会自动重定向到该地址。
- 例如,在HTTP响应中,可能会看到类似于以下的头部信息:
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n
- HTTP状态码302(临时重定向)
-
临时重定向–不改变任何信息; 多用于登录跳转, 页面跳转之类的工作。
-
当服务器返回HTTP 302状态码时,表示请求的资源临时被移动到新的位置。
-
同样地,服务器也会在响应中添加一个Location头部来指定资源的新位置。浏览器会暂时使用新的URL进行后续的请求,但不会缓存这个重定向。
-
例如,在HTTP响应中,可能会看到类似于以下的头部信息:
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n
总结:无论是HTTP 301还是HTTP 302重定向,都需要依赖Location选项来指定资源的新位置。这个Location选项是一个标准的HTTP响应头部,用于告诉浏览器应该将请求重定向到哪个新的URL地址。
八.HTTP常见Header
1. 常见Header
-
Content-Type: 数据类型(text/html等); 查表:https://tool.oschina.net/commons
一张网页内,可能会有多种资源,网页自己+图片,获得网页,识别网页内还有其他资源,浏览器会发起二次请求。应答要告诉对方,我的有效载荷是什么。
-
Content-Length: Body的长度
-
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
-
User-Agent: 声明用户的操作系统和浏览器版本信息;
-
Referer: 当前页面是从哪个页面跳转过来的;
-
Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
-
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
-
Accept:表示客户端能接受的响应内容类型(MIME类型查HTTPContent-type对照表);
-
Accept-Encoding:表示客户端支持的内容压缩格式(编码方式).
2. 关于connection报头
HTTP中的Connection 字段是HTTP报文头的一部分,它主要用于控制和管理客户端与服务器之间的连接状态
核心作用
- 管理持久连接:
Connection字段还用于管理持久连接(也称为长连接)。持久连接允许客户端和服务器在请求/响应完成后不立即关闭TCP连接,以便在同一个连接上发送多个请求和接收多个响应。
持久连接(长连接)
- HTTP/1.1:在HTTP/1.1协议中,默认使用持久连接。当客户端和服务器都不明确指定关闭连接时,连接将保持打开状态,以便后续的请求和响应可以复用同一个连接。
- HTTP/1.0:在HTTP/1.0协议中,默认连接是非持久的。如果希望在HTTP/1.0上实现持久连接,需要在请求头中显式设置
Connection: keep-alive。
语法格式
-
Connection: keep-alive:表示希望保持连接以复用TCP连接。 -
Connection: close:表示请求/响应完成后,应该关闭TCP连接。
下面附上一张关于HTTP常见header的表格
| 字段名 | 含义 | 样例 |
|---|---|---|
| Accept | 客户端可接受的响应内容类型 | Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 |
| Accept-Encoding | 客户端支持的数据压缩格式 | Accept-Encoding: gzip, deflate, br |
| Accept-Language | 客户端可接受的语言类型 | Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 |
| Host | 请求的主机名和端口号 | Host: www.example.com:8080 |
| User-Agent | 客户端的软件环境信息 | User-Agent: Mozilla/5.0 (Windows NT 10.0;Win64; x64) AppleWebKit/537.36 (KHTML, likeGecko) Chrome/91.0.4472.124 Safari/537.36 |
| Cookie | 客户端发送给服务器的 HTTP cookie信息 | Cookie: session_id=abcdefg12345; user_id=123 |
| Referer | 请求的来源URL | Referer:http://www.example.com/previous_page.html |
| Content-Type | 实体主体的媒体类型 | Content-Type: application/x-www-formurlencoded<br(对于表单提交) 或 Content-Type:application/json (对于JSON数据) |
| Content-Length | 实体主体的字节大小 | Content-Length: 150 |
| Authorization | 认证信息,如用户名和密码 | Authorization: BasicQWxhZGRpbjpvcGVuIHNlc2FtZQ== (Base64编码后的用户名:密码) |
| Cache-Control | 缓存控制指令 | 请求时: Cache-Control: no-cache 或 Cache-Control: max-age=3600 ;响应时: Cache-Control: public, max-age=3600 |
| Connection | 请求完后是关闭还是保持连接 | Connection: keep-alive 或 Connection: close |
| Date | 请求或响应的日期和时间 | Date: Wed, 21 Oct 2023 07:28:00 GMT |
| Location | 重定向的目标URL(与 3xx状态码配合使用) | Location:http://www.example.com/new_location.html (与302状态码配合使用) |
| Server | 服务器类型 | Server: Apache/2.4.41 (Unix) |
| Last-Modified | 资源的最后修改时间 | Last-Modified: Wed, 21 Oct 2023 07:20:00 GMT |
| ETag | 资源的唯一标识符,用于缓存 | ETag: "3f80f-1b6-5f4e2512a4100" |
| Expires | 响应过期的日期和时间 | Expires: Wed, 21 Oct 2023 08:28:00 GMT |
九.最简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 “hello world”; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到;
client&&server,是如何保证自己读到的报文是完整的?
step1:读取字节流,分析读到的字节流,确认是否存在空行;
step2:提取Content-Length:获得正文长度,然后在读取或者截取指定长度的内容;
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>void Usage() {printf("usage: ./server [ip] [port]\n");
}
int main(int argc, char* argv[]) {if (argc != 3) {Usage();return 1;}int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {perror("socket");return 1;}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(argv[1]);addr.sin_port = htons(atoi(argv[2]));int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("bind");return 1;}ret = listen(fd, 10);if (ret < 0) {perror("listen");return 1;}for (;;) {struct sockaddr_in client_addr;socklen_t len;int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);if (client_fd < 0) {perror("accept");continue;}char input_buf[1024 * 10] = {0}; // 用一个足够大的缓冲区直接把数据读完.ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);if (read_size < 0) {return 1;}printf("[Request] %s", input_buf);char buf[1024] = {0};const char* hello = "<h1>hello world</h1>";sprintf(buf, "HTTP/1.0 200 OK\nContent-Length:%lu\n\n%s", strlen(hello),hello);write(client_fd, buf, strlen(buf));}return 0;
}
编译, 启动服务. 在浏览器中输入 http://[ip]:[port], 就能看到显示的结果 “Hello World”


备注:
此处我们使用 9090 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口,
但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号.
使用chrome测试我们的服务器时, 可以看到服务器打出的请求中还有一个GET /favicon.ico HTTP/1.1这样的请求.
favicon.ico的作用: 网站标签页上的小图标.
十.完整http服务器
HTTP服务器(HttpServer)
实现一个基于TCP的HTTP服务器,支持静态资源返回和动态交互功能,支持长连接(keep-alive)和短连接
实现了一个完整的HTTP协议解析器,能够处理HTTP请求并返回响应,支持GET和POST方法,能够返回HTML、图片、视频等多种资源类型。
(1). Common.hpp
#pragma once#include <iostream>
#include <functional>
#include <string>
#include <memory>
#include <cstring>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>enum ExitCode
{OK = 0,USAGE_ERR,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERROR,OPEN_ERROR
};class NoCopy
{
public:NoCopy() {}~NoCopy() {}NoCopy(const NoCopy &) = delete;const NoCopy &operator=(const NoCopy &) = delete;
};#define CONV(addr) ((struct sockaddr*)&addr)
(2). Inet_Addr.hpp
和网络版本计算器唯一的区别:添加 SetAddr 方法
#pragma once
#include "Common.hpp"// 网络地址和主机地址之间进行转化的类class InetAddr
{
public:InetAddr(){}// 网络转主机InetAddr(struct sockaddr_in& addr){SetAddr(addr);}// 主机转网络InetAddr(const std::string& ip ,uint16_t port):_ip(ip),_port(port){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;//法一(线程不安全)//_addr.sin_addr.s_addr = inet_addr(_ip.c_str());//法二(线程安全)inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);}InetAddr(uint16_t port):_ip("0"),_port(port){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_addr.s_addr = INADDR_ANY;_addr.sin_port = htons(_port);}void SetAddr(struct sockaddr_in& addr){_addr = addr; //浅拷贝不会有影响_port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列// 4字节网络风格的IP -> 点分十进制的字符串风格的IP//法一(线程不安全)// _ip = inet_ntoa(_addr.sin_addr); //法二(线程安全)char ipbuffer[64];inet_ntop(AF_INET,&_addr.sin_addr,ipbuffer,sizeof(ipbuffer));_ip = ipbuffer;}uint16_t Port() const { return _port; }std::string Ip() const { return _ip; }// NetAddr需要引用,是因为Route.hpp的MessageRoute函数中// sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&user.NetAddr(), sizeof(user.NetAddr()));// 的第五个参数需要可以修改,不能传右值(临时变量)const struct sockaddr_in& NetAddr() { return _addr; }const struct sockaddr* NetAddrPtr() { return CONV(_addr); }socklen_t NetAddrLen(){return sizeof(_addr);}bool operator==(const InetAddr& addr){return _ip == addr._ip && _port == addr._port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}
private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
(3). Log.hpp
日志模块,支持控制台和文件两种输出策略(与远程控制和字典服务器项目(TcpEchoServer)和网络计算器(NetCal)中的Log.hpp完全相同)
(4). Mutex.hpp
互斥锁封装模块(与远程控制和字典服务器项目(TcpEchoServer)和网络计算器(NetCal)中的Mutex.hpp完全相同)
(5). Socket.hpp(模板方法模式)
和网络版本计算器唯一的区别:接收缓冲区大小扩大了
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Log.hpp"
#include "Common.hpp"
#include "Inet_Addr.hpp"namespace SocketModule
{using namespace LogModule;const static int gbacklog = 16;// 模板方法模式(固定套路代码常用)// 基类socket,大部分方法,都是纯虚方法class Socket{public:virtual ~Socket() {}virtual void SocketOrDie() = 0;virtual void BindOrDie(uint16_t port) = 0;virtual void ListenOrDie(int blacklog) = 0;virtual std::shared_ptr<Socket> Accept(InetAddr* client) = 0;virtual void Close() = 0;virtual int Recv(std::string *out) = 0;virtual int Send(const std::string& message) = 0;virtual int Connect(const std::string &server_ip, uint16_t server_port) = 0;public:void BuildTcpSocketMethod(uint16_t port, int blacklog = gbacklog){SocketOrDie();BindOrDie(port);ListenOrDie(blacklog);}void BuildTcpClientSocketMethod(){SocketOrDie();}};const static int defaultfd = -1;class TcpSocket : public Socket{public:TcpSocket():_sockfd(defaultfd){}TcpSocket(int fd):_sockfd(fd){}~TcpSocket() {}void SocketOrDie() override{_sockfd = ::socket(AF_INET, SOCK_STREAM, 0); // ::表示默认使用更外部(全局)的socket函数if(_sockfd < 0){LOG(LogLevel::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success";}void BindOrDie(uint16_t port) override{InetAddr localaddr(port);int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());if(n < 0){LOG(LogLevel::FATAL) << "bind error";exit(BIND_ERR);}LOG(LogLevel::INFO) << "bind success";}void ListenOrDie(int blacklog) override{int n = ::listen(_sockfd, blacklog);if(n < 0){LOG(LogLevel::FATAL) << "listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success";}std::shared_ptr<Socket> Accept(InetAddr* client) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);int fd = ::accept(_sockfd, CONV(peer), &len);if(fd < 0){LOG(LogLevel::WARNING) << "accept warning ...";return nullptr; //TODO}client->SetAddr(peer);return std::make_shared<TcpSocket>(fd);}int Recv(std::string *out) override //返回值等同read的返回值{// 流式读取,并不关心读到的是什么char buffer[4096*4];ssize_t n = ::recv(_sockfd,&buffer,sizeof(buffer)-1, 0);if(n > 0){buffer[n] = 0;*out += buffer;}return n;}int Send(const std::string& message) override{return ::send(_sockfd, message.c_str(), message.size(), 0);}void Close() override{if(_sockfd > 0)::close(_sockfd);}int Connect(const std::string &server_ip, uint16_t server_port) override{InetAddr server(server_ip, server_port);return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());}private:int _sockfd; // _sockfd,listensockfd,sockfd};
}
(6). TcpServer.hpp
#include "Socket.hpp"
#include "Log.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <sys/types.h>
#include <functional>using namespace SocketModule;
using namespace LogModule;using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;class TcpServer
{
public:TcpServer(uint16_t port) :_port(port),_listensockptr(std::make_unique<TcpSocket>()),_isrunning(false){_listensockptr->BuildTcpSocketMethod(_port);}void Start(ioservice_t callback){_isrunning = true;while(_isrunning){InetAddr client;auto sock = _listensockptr->Accept(&client); // 获得1.和client通信的sockfd 2.client网络地址if(sock == nullptr){continue; }LOG(LogLevel::DEBUG) << "accept success ..." << client.StringAddr();// 获得了:1.与客户端通信socket;2.客户端地址和端口号pid_t id = fork();if(id < 0){LOG(LogLevel::FATAL) << "fork error ...";exit(FORK_ERROR);}else if(id == 0){//子进程 ->关闭listen socket_listensockptr->Close();if(fork() > 0)exit(0);//孙子进程在执行任务,已经是孤儿进程了callback(sock,client);sock->Close();exit(OK);}else{//父进程 ->关闭clinet socket(即:auto sock)sock->Close();pid_t rid = ::waitpid(-1, nullptr, 0);(void)rid;}}_isrunning = false;}~TcpServer() {}
private:uint16_t _port;std::unique_ptr<Socket> _listensockptr;bool _isrunning;
};
(7). Util.hpp
#pragma once#include <iostream>
#include <fstream>
#include <string>// 工具类
class Util
{
public:Util() {}~Util() {}static bool ReadFileContent(const std::string &filename, std::string* out/*实际std::vector<char>常用*/){// version 1:默认以文本方式读取文件的.图片是二进制的不能用这种方式读.// std::ifstream in(filename, std::ios::out | std::ios::app);// if (!in.is_open())// {// return false;// }// std::string line;// while (std::getline(in,line))// {// *out += line;// }// in.close();// return true;// version 2:以二进制方式进行读取int filesize = FileSize(filename);if(filesize > 0){std::ifstream in(filename, std::ios::binary);if(!in.is_open())return false;out->resize(filesize);in.read(&(*out)[0], filesize); //或in.read((char *)out->c_str(), filesize);in.close();return true;}else{return false;}}static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep/*\r\n*/){auto pos = bigstr.find(sep);if(pos == std::string::npos)return false;*out = bigstr.substr(0, pos);bigstr.erase(0, pos+sep.size());return true;}static int FileSize(const std::string& filename){std::ifstream in(filename,std::ios::binary);if(!in.is_open())return -1;in.seekg(0, in.end);int filesize = in.tellg();in.seekg(0, in.beg);in.close();return filesize;}private:};
(8). Http.hpp
#pragma once
#include "TcpServer.hpp"
#include "Util.hpp"
#include <sstream>
#include <memory>
#include <functional>
#include <unordered_map>
#include <cctype>const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";const std::string webroot = "./wwwroot";
const std::string homepage = "index.html";
const std::string page_404 = "/404.html";class HttpRequest
{
public:HttpRequest():_is_interact(false),_has_header(false),_has_body(false){}// 服务端浏览器写好了std::string Serialize(){return std::string();}// 获取请求行void ParseReqLine(std::string& reqline){// GET / HTTP/1.1std::stringstream ss(reqline);ss >> _method >> _uri >> _version;}// 获取请求报头与正文bool ParseReqHeadersAndBody(std::string& reqline){std::string line;int content_len = 0;// 读取并解析 Header,直到空行while (true){bool ret = Util::ReadOneLine(reqline, &line, glinespace);if (!ret){LOG(LogLevel::DEBUG) << "请求报头为空";return true;}if (line.empty()) break; // 空行:头结束(因为ReadOneLine已去掉\r\n)auto sep = line.find(glinesep);if (sep != std::string::npos){std::string key = line.substr(0, sep);std::string value = line.substr(sep + glinesep.size());_headers[key] = value;if(!_has_header) _has_header = true;if (key == "Content-Length" || key == "content-length"){content_len = std::stoi(value);}}}// 按 Content-Length 读取正文到 _text_text.clear();if (content_len > 0){if ((int)reqline.size() >= content_len){_has_body = true;_text = reqline.substr(0, content_len);reqline.erase(0, content_len);}else{LOG(LogLevel::FATAL) << "报文异常";return false;}}return true;}// 实现(我们今天认为,reqstr是一个完整的http,没有写decode)bool Deserialize(std::string& reqstr){// 1.提取请求中的请求行std::string reqline;bool res = Util::ReadOneLine(reqstr, &reqline,glinespace);LOG(LogLevel::DEBUG) << reqline;// 2.对请求行进行反序列化// 获得请求行ParseReqLine(reqline);if(_uri=="/")_uri = webroot + _uri + homepage;else_uri = webroot + _uri;// 获得请求报头与正文ParseReqHeadersAndBody(reqstr);/*日志打印请求信息*/LOG(LogLevel::DEBUG) << "_method: " << _method;LOG(LogLevel::DEBUG) << "_uri: " << _uri;LOG(LogLevel::DEBUG) << "_version: " << _version;if(_has_header){for(const auto &header : _headers){LOG(LogLevel::DEBUG) << "_header: " << header.first << glinesep << header.second;}}if(_has_body)LOG(LogLevel::DEBUG) << "_text: " << _text;/*日志打印请求信息*/// (1).POST特殊处理:if (_method == "POST" || _method == "post"){_args = _text; //参数由请求正文发送_is_interact = true;return true;}// (2).GET特殊处理:// 注:可能有这种_uri: ./wwwroot/login?username=zhangsan&password=123456if (_method == "GET" || _method == "get"){const std::string temp = "?";auto pos = _uri.find(temp);if(pos == std::string::npos){return true;}// _uri解析:// _args: username=zhangsan&password=123456// _uri: ./wwwroot/login_args = _uri.substr(pos + temp.size());_uri = _uri.substr(0, pos);_is_interact = true;return true;}// 其他请求方法(PUT、DELETE、HEAD等)return true;}std::string Uri() { return _uri; }bool isInteract() { return _is_interact; }std::string Args() { return _args; }// 检查是否支持长连接bool KeepAlive(){auto iter = _headers.find("Connection");if(iter != _headers.end()){// Connection字段:conn_valstd::string conn_val = iter->second;// 转换为小写进行比较for(char& c : conn_val){c = std::tolower(c);}if(conn_val == "keep-alive")return true;}// 检查 HTTP 版本,HTTP/1.1 默认支持长连接if(_version == "HTTP/1.1" || _version == "http/1.1"){// HTTP/1.1 如果没有明确指定 Connection: close,则默认支持 keep-aliveauto iter2 = _headers.find("Connection");if(iter2 != _headers.end()){std::string conn_val = iter2->second;for(char& c : conn_val){c = std::tolower(c);}if(conn_val == "close")return false;}return true;}return false;}~HttpRequest(){}
private:std::string _method;std::string _uri;std::string _version;std::unordered_map<std::string, std::string> _headers; //请求报头std::string _blankline; //空行std::string _text; //正文bool _has_header;bool _has_body;std::string _args; //uri后面跟的参数bool _is_interact; //是否需要交互
};class HttpResponse
{
public:HttpResponse():_blankline(glinespace),_version("HTTP/1.0"),_keep_alive(false){}// 实现:成熟的http,应答做序列化,不需要依赖任何第三方库!std::string Serialize(){std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;std::string resp_header;for(auto& header : _headers){std::string line = header.first + glinesep + header.second + glinespace;resp_header += line;}return status_line + resp_header + _blankline + _text;}// 服务端浏览器写好了bool Deserialize(){return true;}void SetTargetFile(const std::string& target){_targetfile = target;}void SetCode(int code){_code = code;switch(_code){case 200:_desc = "OK";break;case 404:_desc = "Not Found";break;case 301:_desc = "Moved Permanently";break;case 302:_desc = "See Other";break;default:break;}}void SetHeader(const std::string& key, const std::string& value){auto iter = _headers.find(key);if(iter != _headers.end())return;_headers.emplace(key, value);}void SetText(const std::string & t){_text = t;}// 设置是否保持连接void SetKeepAlive(bool keep_alive){_keep_alive = keep_alive;if(_keep_alive){SetHeader("Connection", "keep-alive");// HTTP/1.1 版本以支持长连接_version = "HTTP/1.1";}else{SetHeader("Connection", "close");}}std::string Uri2Suffix(const std::string& targetfile){// targetfile: ./wwwroot/a/b/c.htmlauto pos = targetfile.rfind(".");if(pos == std::string::npos){return "text/html"; //应该报错的,简写默认是网页了}std::string suffix = targetfile.substr(pos);if(suffix == ".html" || suffix == ".htm")return "text/html";else if (suffix == ".jpg")return "image/jpeg";else if (suffix == ".png")return "image/png";else if (suffix == ".mp4")return "video/mpeg4";elsereturn "text/html";//应该填完Content-Type整张表的,简写默认是网页了}bool MakeResponse(){if(_targetfile == "./wwwroot/favicon.ico"){LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";return false;}// 临时重定向if(_targetfile == "./wwwroot/redir_test"){SetCode(302);SetHeader("Location", "https://www.qq.com/");return true;}int filesize = 0;bool res = Util::ReadFileContent(_targetfile, &_text); //ReadFileContent给_targetfile加好了./wwwrootif(!res){// 法一:_text = "";LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";SetCode(404);_targetfile = webroot + page_404;Util::ReadFileContent(_targetfile, &_text);std::string suffix = Uri2Suffix(_targetfile);SetHeader("Content-Type", suffix);// 法二:// SetCode(302);// SetHeader("Location", "http://115.190.2.155:8080/404.html"); //注意:这里没有域名,端口写死的,要注意!!!// return true;}else{LOG(LogLevel::DEBUG) << "读取文件: " << _targetfile;SetCode(200);std::string suffix = Uri2Suffix(_targetfile);SetHeader("Content-Type", suffix);}filesize = Util::FileSize(_targetfile);SetHeader("Content-Length", std::to_string(filesize));return true;}~HttpResponse(){}
// private:
public:std::string _version;int _code; //404std::string _desc; //"Not Found"std::unordered_map<std::string, std::string> _headers; //请求报头std::string _blankline; //空行std::string _text; //正文// 其他属性std::string _targetfile; //要获取资源的地址bool _keep_alive; //是否保持连接
};// Http要做到:
// 1.返回静态资源
// 2.提供动态交互的能力using http_func_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;class Http
{
public:Http(uint16_t port):tsvrp(std::make_unique<TcpServer>(port)){}// 从缓冲区中提取一个完整的 HTTP 请求// 返回值: true 表示提取到完整请求, false 表示数据不完整bool ExtractOneRequest(std::string& buffer, std::string& request){// 先检查是否至少有一个完整的请求行size_t first_line_end = buffer.find(glinespace);if(first_line_end == std::string::npos){// 还没有收到完整的请求行return false;}// 查找请求头结束标志 \r\n\r\nstd::string header_end = glinespace + glinespace; // "\r\n\r\n"size_t header_end_pos = buffer.find(header_end);if(header_end_pos == std::string::npos){// 还没有收到完整的请求头return false;}// 提取请求头部分(用于解析 Content-Length)std::string header_part = buffer.substr(0, header_end_pos);// 检查是否有 Content-Lengthint content_len = 0;size_t content_pos = header_part.find("Content-Length:");if(content_pos == std::string::npos){content_pos = header_part.find("content-length:");}if(content_pos != std::string::npos && content_pos < header_end_pos){// 找到 Content-Length 头,解析其值size_t len_start = header_part.find(":", content_pos) + 1;// 跳过空格while(len_start < header_part.size() && (header_part[len_start] == ' ' || header_part[len_start] == '\t'))len_start++;size_t len_end = header_part.find(glinespace, len_start);if(len_end == std::string::npos)len_end = header_part.size();if(len_start < len_end){std::string len_str = header_part.substr(len_start, len_end - len_start);try {content_len = std::stoi(len_str);} catch(...) {content_len = 0;}}}// 计算完整请求的结束位置// header_end_pos 是 \r\n 的位置,header_end.size() 是 \r\n 的长度// 所以请求头结束后的位置是 header_end_pos + header_end.size()size_t header_end_offset = header_end_pos + header_end.size();size_t request_end = header_end_offset + content_len;if(buffer.size() < request_end){// 数据不完整,还需要继续接收return false;}// 提取完整的请求request = buffer.substr(0, request_end);buffer.erase(0, request_end);return true;}void HandlerHttpRequest(std::shared_ptr<Socket> &sock, InetAddr &client){ // 接收缓冲区,用于处理粘包std::string recv_buffer;bool should_close = false;// 循环处理多个请求(支持长连接)while(!should_close){// 尝试从缓冲区提取完整请求std::string httpreqstr;bool has_complete_request = ExtractOneRequest(recv_buffer, httpreqstr);// 如果没有完整请求,尝试接收更多数据if(!has_complete_request){std::string new_data;int n = sock->Recv(&new_data);if(n <= 0){// 连接已关闭或出错should_close = true;break;}recv_buffer += new_data;// 再次尝试提取完整请求has_complete_request = ExtractOneRequest(recv_buffer, httpreqstr);}if(!has_complete_request){// 如果还是没有完整请求,继续接收continue;}std::cout << std::endl << "##########################" << std::endl;std::cout << httpreqstr;std::cout << "##########################" << std::endl << std::endl;// 对字符串请求反序列化HttpRequest req;if(!req.Deserialize(httpreqstr)){LOG(LogLevel::WARNING) << "请求解析失败,跳过该请求,继续处理下一个";// 跳过这个有问题的请求,继续处理缓冲区中的下一个请求continue;}// 构建http应答HttpResponse resp;// 根据请求决定是否保持连接bool keep_alive = req.KeepAlive();resp.SetKeepAlive(keep_alive);if(req.isInteract()){// 1.交互// _args: username=zhangsan&password=123456// _uri: ./wwwroot/loginif(_route.find(req.Uri()) == _route.end()){// (1).无对应方法resp.SetTargetFile(webroot + page_404);if (resp.MakeResponse()){std::string response_str = resp.Serialize();sock->Send(response_str);}}else{// (2).有对应方法_route[req.Uri()](req, resp);std::string response_str = resp.Serialize();sock->Send(response_str);}}else{// 2.静态resp.SetTargetFile(req.Uri());if (resp.MakeResponse()){// 所以我们就不在担心,用户访问一个服务器上不存在的资源了(html,css,js,图片,视频这种资源--静态资源!)std::string response_str = resp.Serialize();sock->Send(response_str);}}// 如果不保持连接,处理完这个请求后关闭if(!keep_alive){should_close = true;}}}void Start(){tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client){this->HandlerHttpRequest(sock, client);});}void RegisterService(const std::string name, http_func_t h){std::string key = webroot + name;auto iter = _route.find(key);if(iter == _route.end()){_route.emplace(key, h);}}~Http(){}
private:std::unique_ptr<TcpServer> tsvrp;std::unordered_map<std::string, http_func_t> _route;
};
(9). Main.cc
#include "Common.hpp"
#include "Http.hpp"void Login(HttpRequest &req, HttpResponse &resp)
{LOG(LogLevel::DEBUG) << req.Args() << ",我们成功进入到了处理数据的逻辑";std::string text = "hello: " + req.Args();resp.SetCode(200);resp.SetHeader("Content-Type", "text/plain"); //文字类型resp.SetHeader("Content-Length", std::to_string(text.size()));resp.SetText(text);
}void Register(HttpRequest &req, HttpResponse &resp)
{LOG(LogLevel::DEBUG) << req.Args() << ",我们成功进入到了处理数据的逻辑";std::string text = "hello: " + req.Args();resp.SetCode(200);resp.SetHeader("Content-Type", "text/plain"); //文字类型resp.SetHeader("Content-Length", std::to_string(text.size()));resp.SetText(text);
}void VipCheck(HttpRequest &req, HttpResponse &resp)
{LOG(LogLevel::DEBUG) << req.Args() << ",我们成功进入到了处理数据的逻辑";std::string text = "hello: " + req.Args();resp.SetCode(200);resp.SetHeader("Content-Type", "text/plain"); //文字类型resp.SetHeader("Content-Length", std::to_string(text.size()));resp.SetText(text);
}void Search(HttpRequest &req, HttpResponse &resp)
{LOG(LogLevel::DEBUG) << req.Args() << ",我们成功进入到了处理数据的逻辑";std::string text = "hello: " + req.Args();resp.SetCode(200);resp.SetHeader("Content-Type", "text/plain"); //文字类型resp.SetHeader("Content-Length", std::to_string(text.size()));resp.SetText(text);
}void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " port" << std::endl;
}// http port
int main(int argc, char* argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}// std::cout << "服务器已经启动,已经是一个守护进程了" << std::endl;// 守护进程化// daemon(1, 0);// Enable_File_Log_Strategy();uint16_t port = std::stoi(argv[1]);std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);httpsvr->RegisterService("/login",Login);httpsvr->RegisterService("/register", Register);httpsvr->RegisterService("/vip_check", VipCheck);httpsvr->RegisterService("/s", Search);httpsvr->Start();return 0;
}
(10). Makefile
myhttp:Main.ccg++ -o $@ $^ -std=c++17
.PHONY:clean
clean:rm -rf myhttp #/var/log/my.log
(11). 项目结构
Http/
├── Common.hpp
├── html
│ └── 20251027A041HW00
├── Http.hpp
├── Inet_Addr.hpp
├── Log.hpp
├── Main.cc
├── Makefile
├── Mutex.hpp
├── myhttp
├── Socket.hpp
├── TcpServer.hpp
├── Util.hpp
└── wwwroot├── 404.html├── About.html├── board1.html├── Contact.html├── favicon.ico├── image├── index.html├── landscape.html├── Login.html├── Register.html├── robots.txt├── test.html└── video
(12). 结果



十一.附录:
1. HTTP历史及版本核心技术与时代背景
HTTP(Hypertext Transfer Protocol,超文本传输协议)作为互联网中浏览器和服务器间通信的基石,经历了从简单到复杂、从单一到多样的发展过程。以下将按照时间顺序,介绍HTTP的主要版本、核心技术及其对应的时代背景。
2. HTTP/0.9
(1).核心技术:
- 仅支持GET请求方法。
- 仅支持纯文本传输,主要是HTML格式。
- 无请求和响应头信息。
(2).时代背景:
- 1991年,HTTP/0.9版本作为HTTP协议的最初版本,用于传输基本的超文本HTML内容。
- 当时的互联网还处于起步阶段,网页内容相对简单,主要以文本为主。
3. HTTP/1.0
(1).核心技术:
- 引入POST和HEAD请求方法。
- 请求和响应头信息,支持多种数据格式(MIME)。
- 支持缓存(cache)。
- 状态码(status code)、多字符集支持等。
(2).时代背景:
- 1996年,随着互联网的快速发展,网页内容逐渐丰富,HTTP/1.0版本应运而生。
- 为了满足日益增长的网络应用需求,HTTP/1.0增加了更多的功能和灵活性。
- 然而,HTTP/1.0的工作方式是每次TCP连接只能发送一个请求,性能上存在一定局限。
4. HTTP/1.1
(1).核心技术:
- 引入持久连接(persistent connection),支持管道化(pipelining)。
- 允许在单个TCP连接上进行多个请求和响应,提高了性能。
- 引入分块传输编码(chunked transfer encoding)。
- 支持Host头,允许在一个IP地址上部署多个Web站点。
(2).时代背景:
- 1999年,随着网页加载的外部资源越来越多,HTTP/1.0的性能问题愈发突出。
- HTTP/1.1通过引入持久连接和管道化等技术,有效提高了数据传输效率。
- 同时,互联网应用开始呈现出多元化、复杂化的趋势,HTTP/1.1的出现满足了这些需求。
5. HTTP/2.0
(1).核心技术:
- 多路复用(multiplexing),一个TCP连接允许多个HTTP请求。
- 二进制帧格式(binary framing),优化数据传输。
- 头部压缩(header compression),减少传输开销。
- 服务器推送(server push),提前发送资源到客户端。
(2).时代背景:
- 2015年,随着移动互联网的兴起和云计算技术的发展,网络应用对性能的要求越来越高。
- HTTP/2.0通过多路复用、二进制帧格式等技术,显著提高了数据传输效率和网络性能。
- 同时,HTTP/2.0还支持加密传输(HTTPS),提高了数据传输的安全性。
6. HTTP/3.0
(1).核心技术:
- 使用QUIC协议替代TCP协议,基于UDP构建的多路复用传输协议。
- 减少了TCP三次握手及TLS握手时间,提高了连接建立速度。
- 解决了TCP中的线头阻塞问题,提高了数据传输效率。
(2).时代背景:
- 2022年,随着5G、物联网等技术的快速发展,网络应用对实时性、可靠性的要求越来越高。
- HTTP/3.0通过使用QUIC协议,提高了连接建立速度和数据传输效率,满足了这些需求。
- 同时,HTTP/3.0还支持加密传输(HTTPS),保证了数据传输的安全性。
7.爬虫
爬虫,本质就是用http客户端,来模拟浏览器行为,获取指定链接下的网页!
比如我们使用:wget+网页,就可以获取该网页的所有内容。
