当前位置: 首页 > news >正文

网络:5.应用层协议HTTP

应用层协议HTTP

  • (http是一个应用层协议,底层用的tcp)

一.HTTP补充知识:

域名和IP之间的关系

image-20251030225006927

二.HTTP协议

虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。

在互联网世界中,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。
它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如HTML文档)。

HTTP协议是客户端与服务器之间通信的基础。客户端通过HTTP协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP协议是一个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。

三.认识URL

平时我们俗称的 “网址” 其实就是说的 URL(统一资源定位符)也就是超链接

image-20251102185917685

四.urlencode和urldecode(了解)

/ ? : 等这样的字符, 已经被url 当做特殊意义理解了. 因此这些字符不能随意出现.

比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.

转义的规则如下:

将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式

例如:

image-20251102190112535

“+” 被转义成了 “%2B”

urldecode就是urlencode的逆过程;

urlencode工具

浏览器urlencode;服务器urldecode;-- 这种模式叫作B/S模式

五.HTTP协议请求与响应格式

1. HTTP请求

  • http协议,序列和反序列化用的是特殊字符(空格或换行符)进行子串拼接,且不依赖任何第三方库

image-20251102190555284

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

image-20251102190939738

2. HTTP响应

完整请求获取方法:没法直接确定读到原完整报文,但是我有方法读到当前请求的完整报头!

  1. 读取到完整的请求报头[空行];
  2. 对报头进行反序列化,提取一个属性:Content-Length:有效载荷的长度;
  3. 在从剩余的字符串内容中,提取content-length个字符。

image-20251102191259044

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

image-20251102191342953

基本的应答格式

image-20251102191457253

六.HTTP的方法

image-20251102191539279

其中最常用的就是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

image-20251103044046170

image-20251103044214326

(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的状态码

image-20251102194049908

最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)

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

以下是仅包含重定向相关状态码的表格:

状态码含义是否为临时重定向应用样例
301Moved Permanently否(永久重定向)网站换域名后,自动跳转到新域名;
搜索引擎更新网站链接时使用(最大意义)
302Found 或 See Other是(临时重定向)用户登录成功后,重定向到用户首页
307Temporary Redirect是(临时重定向)临时重定向资源到新的位置(较少使用)
308Permanent Redirect否(永久重定向)永久重定向资源到新的位置(较少使用)

重定向的htp的请求,至少是三部分:状态行,报头,空行;正文常常没有.

image-20251103143932410

关于重定向的验证,以301为代表

**HTTP状态码301(永久重定向)和302(临时重定向)都依赖Location选项。**以下是关于两者依赖Location选项的详细说明:

  1. HTTP状态码301(永久重定向)
  • 网站更换域名,或者更换网址
  • 当服务器返回HTTP 301状态码时,表示请求的资源已经被永久移动到新的位置。
  • 在这种情况下,服务器会在响应中添加一个Location头部,用于指定资源的新位置。这个Location头部包含了新的URL地址,浏览器会自动重定向到该地址。
  • 例如,在HTTP响应中,可能会看到类似于以下的头部信息:
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n
  1. 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连接。

    image-20251103203754537

下面附上一张关于HTTP常见header的表格

字段名含义样例
Accept客户端可接受的响应内容类型Accept:
text/html,application/xhtml+xml,application/xm
l;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, like
Gecko) Chrome/91.0.4472.124 Safari/537.36
Cookie客户端发送给服务器的 HTTP cookie信息Cookie: session_id=abcdefg12345; user_id=123
Referer请求的来源URLReferer:
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: Basic
QWxhZGRpbjpvcGVuIHNlc2FtZQ== (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”

image-20251103210649212

image-20251103210709585

备注:
此处我们使用 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). 结果

image-20251103213716233

image-20251103213625104

image-20251103213634760

十一.附录:

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+网页,就可以获取该网页的所有内容。

http://www.dtcms.com/a/613789.html

相关文章:

  • python 要如何快速拥有可用python的编程能力
  • 网站跳出率房地产的未来趋势分析
  • 家庭网络建站广告设计公司服务不到位
  • SAP FICO工单成本分析报表
  • 破解版网站建设营销运营推广服务
  • 告别“在我电脑上能跑”:Docker入门与核心概念解析
  • 2. YOLOv5 搭建一个完整的目标检测系统核心步骤
  • discuz培训网站模板下载wordpress直接显示文章
  • R语言编程基础与应用 | 探索数据分析的无限可能
  • 北京网站设计网站设计公司价格工商做年报网站
  • 商丘网站建设网站推广镇江做网站的
  • Nginx安全策略
  • NumPy 从数值范围创建数组
  • 网站备案贵州电话学产品设计的可以找什么工作
  • 刷网站排名 优帮云企业所得税什么时候申报缴纳
  • P5736 【深基7.例2】质数筛题解(重置版!)
  • Qt--通过JLinkARM.dll实现Jlink自动烧写
  • gitlab cicd 模块解释
  • 如何实现带历史记录功能的控制台2048游戏
  • gitlab cicd首次操作
  • 建设学院网站意义比价 wordpress 插件下载
  • 电子毕业设计代做网站上海雷蒙威手表网站
  • LangChain Memory
  • 【ZeroRange WebRTC】NACK(Negative Acknowledgment)技术深度分析
  • 物联网架构
  • 网站推广公司兴田德润在哪儿wordpress 手机支付
  • 如何在 VSCode 中创建 Vue 项目
  • 【ZeroRange WebRTC】PLI(Picture Loss Indication)技术深度分析
  • 神马影视 8.8 源码 2025 版,HDR + 杜比音效 + 零卡顿
  • MFC编程实战:全面掌握Combo Box(组合框)控件的高级应用