文章目录
目录
一、HTTP协议
1. HTTP简介
HTTP(Hyper Text Transfer Protocol)协议又叫做超文本传输协议,是互联网上应用最广泛的协议之一,它用于在 Web 浏览器和 Web 服务器之间传输数据。是一个简单的请求-响应协议,HTTP通常运行在TCP之上。
在编写网络通信代码时,我们可以自己进行协议的定制,但实际有很多优秀的工程师早就已经写出了许多非常成熟的应用层协议,其中最典型的就是HTTP协议。
HTTP 工作原理
客户端(例如 Web 浏览器)向服务器发送 HTTP 请求。
请求中包含以下信息:
- 请求方法(例如 GET、POST)、请求的 URL 、HTTP版本
- 请求头字段
- 请求主体(可选)
服务器接收请求,处理请求,并返回 HTTP 响应。
响应中包含以下信息:
- 状态码(例如 200 OK、404 Not Found)
- 响应头字段
- 响应主体
HTTP 请求方法
- GET: 从服务器获取资源。
- POST: 向服务器提交数据。
- PUT: 更新服务器上的资源。
- DELETE: 删除服务器上的资源。
- HEAD: 获取资源的头部信息。
- OPTIONS: 查询服务器支持的请求方法。
HTTP 状态码
- 200 OK: 请求成功。
- 404 Not Found: 资源未找到。
- 403 Forbidden: 访问被禁止。
- 500 Internal Server Error: 服务器内部错误。
HTTP 头字段
HTTP 头字段包含了关于请求和响应的额外信息,例如:
- Content-Type: 数据类型。
- Content-Length: 数据长度。
- User-Agent: 客户端信息。有些网站在防止爬虫爬取数据时,就会针对User-Agent进行检查
- Host: 服务器地址。
HTTP 版本
- HTTP/1.0: 第一个版本,支持持久连接。
- HTTP/1.1: 第二版本,支持流水线处理、缓存等功能。
- HTTP/2: 最新版本,使用二进制格式,支持多路复用,提高传输效率。
2. 认识URL
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。
URL构成
一个标准的 URL 由以下部分组成:
- 协议: 指定了访问资源的方式,例如
http
、https
、ftp
等。 - 服务器地址: 指示了资源所在的服务器,例如
www.google.com
、www.example.com
等。 - 服务器端口号: 指定了服务器上提供服务的端口,默认情况下,HTTP 协议使用 80 端口,HTTPS 协议使用 443 端口。
- 路径: 指定了资源在服务器上的位置,例如
/index.html
、/products/shoes
等。 - 查询参数: 用于传递额外的信息,以帮助服务器更准确地定位资源,例如
?q=搜索关键词
、?id=123
等。 - 片段标识符: 用于标识资源的特定部分,例如
#top
指示网页的顶部。
协议方案名
http:// 表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。
常见的应用层协议:
- DNS(Domain Name System)协议:域名系统。
- FTP(File Transfer Protocol)协议:文件传输协议。
- TELNET(Telnet)协议:远程终端协议。
- HTTP(Hyper Text Transfer Protocol)协议:超文本传输协议。
- HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer)协议:安全数据传输协议。
- SMTP(Simple Mail Transfer Protocol)协议:电子邮件传输协议。
- POP3(Post Office Protocol - Version 3)协议:邮件读取协议。
- SNMP(Simple Network Management Protocol)协议:简单网络管理协议。
- TFTP(Trivial File Transfer Protocol)协议:简单文件传输协议。
登录信息
usr:pass 表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器。
服务器地址
www.example.jp 表示的是服务器地址,也叫做域名,比如 www.alibaba.com,www.qq.com,www.baidu.com。
需要注意的是,我们用IP地址标识公网内的一台主机,但IP地址本身并不适合给用户看,因为即使是点分十进制的IP地址,用户也可能记不清楚,可以试想一下我们访问百度要输入110.242.68.4这么一串IP(百度域名解析有多个IP),显而易见对用户不方便,用户都不知道网站大致内容是什么,那么这个网站对应的用户数量也不会很多。
我们可以通过ping命令,分别获得 www.baidu.com 和 www.qq.com 这两个域名解析后的IP地址。
如果用户看到的是这两个IP地址,那么用户在访问这个网站之前并不知道这两个网站到底是干什么的,但如果用户看到的是www.baidu.com和www.qq.com这两个域名,那么用户至少知道这两个网站分别对应的是哪家公司,因此域名具有更好的自描述性。
实际我们可以认为域名和IP地址是等价的,在计算机当中使用的时候既可以使用域名,也可以使用IP地址。但URL呈现出来是可以让用户看到的,因此URL当中是以域名的形式表示服务器地址的。
服务器端口号
80
表示的是服务器端口号。HTTP协议和套接字编程一样都是位于应用层的,在进行套接字编程时我们需要给服务器绑定对应的IP和端口,而这里的应用层协议也同样需要有明确的端口号。
常见协议对应的端口号:
协议名称 | 对应端口号 |
---|---|
HTTP | 80 |
HTTPS | 443 |
SSH | 22 |
当我们使用某种协议时,该协议实际就是在为我们提供服务,对于这些常用的服务与端口号之间的对应关系都是明确的(熟知端口号),所以我们在使用某种协议时实际是不需要指明该协议对应的端口号的,因此在URL当中,服务器的端口号一般也是被省略的。
带层次的文件路径
/dir/index.htm
表示的是要访问的资源所在的路径。访问服务器的目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径。
比如我们打开浏览器输入百度的域名后,此时浏览器就帮我们获取到了百度的首页。
当我们发起网页请求时,本质是获得了这样的一张html网页信息,然后浏览器对这张网页信息进行解释,最后就呈现出了对应的网页。
我们可以将这种资源称为网页资源,此外我们还会向服务器请求视频、音频、网页、图片等资源。HTTP之所以叫做超文本传输协议,而不叫做文本传输协议,就是因为有很多资源实际并不是普通的文本资源。
因此在URL当中就有这样一个字段,用于表示要访问的资源所在的路径。此外我们可以看到,这里的路径分隔符是/,而不是\,这也就证明了实际很多服务都是部署在Linux上的。
查询字符串
uid=1
表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过&
符号分隔开的。
比如我们在百度上面搜索HTTP,此时可以看到URL中有很多参数,而在这众多的参数当中有一个参数wd(word),表示的就是我们搜索时的搜索关键字wd=HTTP
。
因此双方在进行网络通信时,是能够通过URL进行用户数据传送的。
片段标识符
ch1
表示的是片段标识符,是对资源的部分补充。比如我们在浏览器看组图的时候,URL当中就会出现片段标识符。
urlencode和urldecode
如果在搜索关键字当中出现了像 /?: 这样的字符,由于这些字符已经被URL当作特殊意义理解了,因此URL在呈现时会对这些特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为十六进制,然后从右到左,取4位(不足4位直接处理),每两位做一位,前面加上%,编码成%XY格式。
比如当我们搜索C++时,由于+
加号在URL当中也是特殊符号,而+
字符转为十六进制后的值就是0x2B
,因此一个+
就会被编码成一个%2B
。
补充: URL当中除了会对这些特殊符号做编码,对中文也会进行编码。
- 定位资源: 用于唯一标识互联网上的资源,例如网页、文件、图片等。唯一标识是因为URL中的服务器地址(DNS自动解析为IP)和服务器端口号可以在互联网上唯一标识一台主机上的一个进程,而带层次的文件路径又唯一标识了服务器上的资源,所以在共同作用下URL就可以唯一标识互联网上的资源
- 传递信息: 用于传递额外的信息,例如搜索关键词、用户 ID 等。
- 导航: 用于在不同的网页之间跳转。
URL中的域名和文件路径唯一标定了一台主机上的一个资源,协议和端口号标定了是什么样的服务将资源获取或发送。
在线编码工具
这里分享一个在线编码工具:
选中其中的URL编码/解码模式,在输入C++后点击编码就能得到编码后的结果。
再点击解码就能得到原来输入的C++。实际当服务器拿到对应的URL后,也需要对编码后的参数进行解码,此时服务器才能拿到你想要传递的参数,解码实际就是编码的逆过程。
URL 的类型
- 绝对 URL: 包含了完整的地址信息,可以直接访问资源。
- 相对 URL: 相对于当前页面地址的地址,需要根据当前页面地址进行解析才能访问资源。
3. HTTP协议格式
应用层常见的协议有HTTP和HTTPS,传输层常见的协议有TCP,网络层常见的协议是IP,数据链路层对应就是MAC帧了。其中传输层和网络层被封装在OS内核中,数据链路层在设备驱动中,物理层就是网卡设备。
传输层、网络层、数据链路层它们主要负责的是通信细节(不考虑物理层)。如果应用层不考虑下三层,在应用层自己的心目当中,它就可以认为自己是在和对方的应用层在直接进行数据交互。
下三层负责的是通信细节,而应用层负责的是如何使用传输过来的数据,两台主机在进行通信的时候,应用层的数据能够成功交给对端应用层,因为网络协议栈的下三层已经负责完成了这样的通信细节,而如何使用传输过来的数据就需要我们去定制协议,这里最典型的就是HTTP协议。
HTTP是基于请求和响应的应用层服务,作为客户端,你可以向服务器发起request,服务器收到这个request后,会对这个request做数据分析,得出你想要访问什么资源,然后服务器再构建response,完成这一次HTTP的请求。这种基于request&response这样的工作方式,我们称之为cs或bs模式,其中c表示client,s表示server,b表示browser。
由于HTTP是基于请求和响应的应用层访问,因此我们必须要知道HTTP对应的请求格式和响应格式,这就是学习HTTP的重点。
HTTP请求协议格式
HTTP请求协议格式如下:
HTTP请求由以下四部分组成:
- 请求行:[请求方法] [url] [http版本],以空格分隔
- 请求报头:请求的属性,这些属性都是以key: value的形式按行列举
- 空行:遇到空行表示请求报头结束
- 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。
其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串,即正文部分可以为空。
如何将HTTP请求的报头与有效载荷进行分离?
当应用层收到一个HTTP请求时,它必须想办法将HTTP的报头与有效载荷进行分离。对于HTTP请求来讲,这里的请求行和请求报头就是HTTP的报头信息,而这里的请求正文实际就是HTTP的有效载荷。
我们可以根据HTTP请求当中的空行来进行分离,当服务器收到一个HTTP请求后,就可以按行进行读取,如果读取到空行则说明已经将报头读取完毕,实际HTTP请求当中的空行就是用来分离报头和有效载荷的。
如果将HTTP请求想象成一个大的线性结构,此时每行的内容都是用\n隔开的,因此在读取过程中,如果连续读取到了两个\n,就说明已经将报头读取完毕了,后面剩下的就是有效载荷了。
获取浏览器的HTTP请求
在网络协议栈中,应用层的下一层叫做传输层,而HTTP协议底层通常使用的传输层协议是TCP协议,因此我们可以用套接字编写一个TCP服务器,然后启动浏览器访问我们的这个服务器。
由于我们的服务器是直接用TCP套接字读取浏览器发来的HTTP请求,此时在服务端没有应用层对这个HTTP请求进行过任何解析,因此我们可以直接将浏览器发来的HTTP请求进行打印输出,此时就能看到HTTP请求的基本构成。
方法一:使用telnet 进行HTTP报文获取
报头分析
方法二:编写一个简单的TCP服务器,这个服务器要做的就是把浏览器发来的HTTP请求进行打印即可。
#pragma once
#include "Socket.hpp"
#include "Log.hpp"
const static uint16_t defaultport = 8080;
struct ThreadData
{
int sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport) : _port(port)
{}
bool start()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
while (true)
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(&clientip, &clientport);
_log(Info, "get a new conect, sockfd: %d", sockfd);
ThreadData* td = new ThreadData;
td->sockfd = sockfd;
pthread_t tid;
pthread_create(&tid, nullptr, Handler, td);
}
}
static void* Handler(void* args)
{
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
char buffer[10240];
ssize_t n = read(td->sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer;
}
close(td->sockfd);
delete td;
return nullptr;
}
private:
Sock _listensock;
uint16_t _port;
};
#include <iostream>
#include <memory>
#include "Log.hpp"
#include "HttpServer.hpp"
using namespace std;
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << argv[0] << " port" << std::endl;
exit(1);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<HttpServer> svr(new HttpServer(port));
svr->start();
return 0;
}
运行服务器程序后,然后用浏览器进行访问(IP地址+端口号进行访问),此时我们的服务器就会收到浏览器发来的HTTP请求,并将收到的HTTP请求进行打印输出
说明一下:
- 浏览器向我们的服务器发起HTTP请求后,因为我们的服务器没有对进行响应,只是简单的在服务器端进行了cout输出,此时浏览器就会认为服务器没有收到HTTP请求,然后再不断发起新的HTTP请求,因此虽然我们只用浏览器访问了一次,但会受到多次HTTP请求。
- 由于浏览器发起请求时默认用的就是HTTP协议,因此我们在浏览器的 URL 框当中输入网址时可以不用指明HTTP协议(也有浏览器默认协议为HTTPS)。
- url当中的 / 不能称之为我们云服务器上根目录,这个/表示的是web根目录,这个web根目录可以是你的机器上的任何一个目录,这个是可以自己指定的,不一定就是Linux的根目录。
- 这里的User-Agent表示访问服务器的客户端的信息,有操作系统信息、内核信息,从中我们可以理解为什么我们使用搜索引擎搜索一款软件时,页面自动给我们推送适合我们的OS版本的软件,例如百度,这是因为百度先获取了我们的http请求,分析了User-Agent信息,智能化的从服务器获取信息。
- 请求行当中的url一般是不携带域名以及端口号的,因为在请求报头中的Host字段会进行指明,请求行当中的url表示你要访问这个服务器上的哪一路径下的资源。如果浏览器在访问我们的服务器时指明要访问的资源路径,那么此时浏览器发起的HTTP请求当中的url也会跟着变成该路径。
- 请求报头当中全部都是以
key: value
形式按行陈列的各种请求属性,请求属性陈列完后紧接着的就是一个空行,空行后的就是本次HTTP请求的请求正文,此时请求正文为空字符串,因此这里有两个空行。
HTTP响应协议格式
HTTP响应协议格式如下:
HTTP响应由以下四部分组成:
- 状态行:[http版本] [状态码] [状态码描述],中间以空格分隔。
- 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。
- 空行:遇到空行表示响应报头结束。
- 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个 Content-Length 属性来标识响应正文的长度。比如服务器返回了一个 html 页面,那么这个 html 页面的内容就是在响应正文当中的。
如何将HTTP响应的报头与有效载荷进行分离?
对于HTTP响应来讲,这里的状态行和响应报头就是HTTP的报头信息,而这里的响应正文实际就是HTTP的有效载荷。与HTTP请求相同,当应用层收到一个HTTP响应时,也是根据HTTP响应当中的空行来分离报头和有效载荷的。当客户端收到一个HTTP响应后,就可以按行进行读取,如果读取到空行则说明报头已经读取完毕。
HTTP为什么要交互版本?
HTTP请求当中的请求行和HTTP响应当中的状态行,当中都包含了http的版本信息。其中HTTP请求是由客户端发的,因此HTTP请求当中表明的是客户端的http版本,而HTTP响应是由服务器发的,因此HTTP响应当中表明的是服务器的http版本。
客户端和服务器双方在进行通信时会交互双方http版本,主要还是为了兼容性的问题。因为服务器和客户端使用的可能是不同的http版本,为了让不同版本的客户端都能享受到对应的服务,此时就要求通信双方需要进行版本协商。
客户端在发起HTTP请求时告诉服务器自己所使用的http版本,此时服务器就可以根据客户端使用的http版本,为客户端提供对应的服务,而不至于因为双方使用的http版本不同而导致无法正常通信。因此为了保证良好的兼容性,通信双方需要交互一下各自的版本信息。
例如我们手机上的APP版本,如果APP有新的版本,而我们没有升级,那么我们手机上旧版本的APP就不能收到新版本的APP服务端的响应报文吗?这公司显然是不想干了,所以服务器端会保留旧版本的响应服务,根据用户不同版本的请求进行对应的响应服务。
构建HTTP响应给浏览器
服务器读取到客户端发来的HTTP请求后,需要对这个HTTP请求进行各种数据分析,然后构建成对应的HTTP响应发回给客户端。而我们的服务器连接到客户端后,实际就只读取了客户端发来的HTTP请求就将连接断开了。
接下来我们可以构建一个HTTP请求给浏览器,鉴于现在还没有办法分析浏览器发来的HTTP请求,这里我们可以给浏览器返回一个固定的HTTP响应。正文为"hello world",然后构建一个请求行、content_length键值字段和空行,最后拼接上正文字符串,然后返回给浏览器,因为浏览器会自动解析HTTP响应报文,所以如果浏览器收到了我们的响应,那么浏览器上就会显示出我们构建的正文部分“hello world”。
#pragma once
#include "Socket.hpp"
#include "Log.hpp"
#include <sys/socket.h>
#include <sys/types.h>
const static uint16_t defaultport = 8080;
class ThreadData
{
public:
ThreadData(int fd) : sockfd(fd)
{}
public:
int sockfd;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport) : _port(port)
{}
bool start()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
while (true)
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(&clientip, &clientport);
if (sockfd < 0) continue;
_log(Info, "get a new conect, sockfd: %d", sockfd);
ThreadData* td = new ThreadData(sockfd);
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
// 单独封装出来
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer;
// 返回响应的过程
std::string text = "hello world";
std::string response_line = "HTTP/1.0 200 OK\r\n";
std::string response_header = "Content-Length";
response_header += std::to_string(text.size()) + "\r\n";
std::string blank_line = "\r\n"; // 空行
std::string response = response_line;
response += response_header + blank_line + text;
// write(sockfd, response.c_str(), response.size());
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void* ThreadRun(void* args)
{
// 线程分离
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
// 调用类内部的函数,直接把 HandlerHttp 改为 static 更方便
HandlerHttp(td->sockfd);
delete td;
return nullptr;
}
private:
Sock _listensock;
uint16_t _port;
};
recv
除了read可以向sockfd文件读取数据,还有 recv 函数,功能相同,并且 recv 参数多了一个flag,可以非阻塞的从sockfd文件 read 数据。
send
除了write还有send,flags表示发送策略
在浏览器访问服务器 IP:Port
服务器显示HTTP请求报文
我们只是回发了HTTP响应格式的字符串报文,浏览器就解析了服务器发送的内容,这是应用层HTTP协议作用的结果,浏览器会直接显示正文部分,报文内的键值信息并不显示
可以将HTML写在正文部分
前端就是在写HTML网页(前段有CSS、JavaScript、HTML这三种语言),而后端就是将HTML代码拼接起来,发送给浏览器,浏览器内置的有HTTP的客户端,并且自动解析带有标签的网页
HTML
HTML(HyperText Markup Language,超文本标记语言)是用于创建网页的标准标记语言。它定义了网页的结构、内容和样式。
HTML 的基本结构
一个基本的 HTML 页面包含以下部分:
- 文档类型声明: 告诉浏览器使用哪个版本的 HTML。
<!DOCTYPE html>
- HTML 根元素: 所有的 HTML 元素都包含在
<html>
元素中。
<html lang="en">
</html>
- 头部元素:
<head>
元素包含了页面的元数据,例如标题、字符集、样式表等。
<head>
<meta charset="UTF-8">
<title>My Web Page</title>
</head>
- 主体元素:
<body>
元素包含了页面的可见内容。
<body>
<h1>Welcome to My Website</h1>
<p>This is a paragraph of text.</p>
</body>
难道我们的网页只能是一个静态固定的硬编码形式吗?
并不是,我们可以创建一个html文件,在该文件内进行编写html,最后在后端将html文件构成字符串,追加到HTTP响应报文的正文部分,就可以直接修改html文件,不用重新启动服务器,浏览器就可以访问新修改的html文件,而不像我们之前写的固定的 “hello world”,如果想改变body内容,还要修改源文件重新编译,再次启动进程的步骤。
在当前路径下创建一个wwwroot目录,将HTML写在wwwroot目录下,即将wwwroot目录所在路径作为web的根目录
将HTML文件的每一行构成一个字符串返回,构成HTTP响应的正文部分
wwwroot/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>hello world</h1>
</body>
</html>
HttpServer.hpp
static std::string ReadHtmlContent(const std::string& htmlpath)
{
std::ifstream in(htmlpath);
if (!in.is_open()) return "404";
std::string line, content;
while (std::getline(in, line))
{
content += line;
}
in.close();
return content;
}
// 单独封装出来
static void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer;
// 返回响应的过程
std::string text = ReadHtmlContent("wwwroot/index.html");
std::string response_line = "HTTP/1.0 200 OK\r\n";
std::string response_header = "Content-Length";
response_header += std::to_string(text.size()) + "\r\n";
std::string blank_line = "\r\n"; // 空行
std::string response = response_line;
response += response_header + blank_line + text;
// write(sockfd, response.c_str(), response.size());
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void* ThreadRun(void* args)
{
// 线程分离
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
// 调用类内部的函数,直接把 HandlerHttp 改为 static 更方便
HandlerHttp(td->sockfd);
delete td;
return nullptr;
}
在浏览器进行访问
浏览器请求的资源是一个已经被指定路径的html文件(wwwroot / index.html),我们不用重新编译再启动服务器了,直接修改html文件然后保存,浏览器就可以直接访问新修改的html。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>hello world</h1>
<h1>hello world</h1>
<h1>hello world</h1>
<h1>hello world</h1>
<h1>hello world</h1>
<h1>hello world</h1>
<h1>hello world</h1>
</body>
</html>
浏览器访问
但是我们在构建HTTP响应时的正文部分是固定的wwwroot / index.html路径文件,所以不管浏览器用户访问什么路径的资源,我们都是返回同样的正文部分,我们想实现用户想访问哪个路径下的资源就访问哪个路径下的资源
服务器接收HTTP请求时,分割出请求行内的URL,这样就可以确定用户想访问哪一个路径的资源。我们可以简单的写一个路径,因为不想分割字符串了。构建一个路径 wwwroot/a/a/a/index.html将该路径字符串传递给ReadHtmlContent将该路径的html文件构建为一个字符串返回。
所以我们在浏览器请求的带有层次的文件路径前面的 / 表示的大概率不是Linux内的根目录,而是我们自定义的一个路径,我们也可以将这个web根目录的路径设置在配置文件内,在构建用户访问的URL路径时将两者组合,就可以达到修改配置文件就可以修改web根目录的效果,而不用再修改HttpServer.hpp文件再重新编译启动
stringstream
自动按空格分割
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
浏览器访问时不加文件路径
浏览器访问时带上文件路径
再加上一个成员变量file_path ,用来构建客户要访问的资源在服务器OS上的具体路径
如果浏览器要访问web根目录文件资源,显然服务器并不能把根目录下所有资源都响应给客户端,所以对于要访问web根目录的HTTP请求,我们要在服务器构建一个资源路径返回
例如,我们在浏览器要访问根目录资源
显然,根据我们的服务器代码,服务器会构建资源路径./wwwroot/index.html,并将 ./wwwroot/index.html 文件构建为HTTP响应报文的正文,返回给浏览器
再例如,浏览器要访问/a/a/a路径资源,服务器就会截取并构建客户要访问的文件路径,所以web的根目录都是服务器指定的一个路径,而不是绝对的是Linux的根目录
在wwwroot目录下创建路径/a/b/hello.html、/x/y/world.html两个路径
./wwwroot/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>man, what can i say?</h1>
<!-- <h1>你是一个好人</h1> -->
<a href="http://106.14.216.12:8080/a/b/hello.html">访问第二张网页</a>
<a href="http://106.14.216.12:8080/x/y/world.html">访问第三张网页</a>
</body>
</html>
./wwwroot/hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>这是第二张网页</h1>
<a href="http://106.14.216.12:8080/">访问首页</a>
<a href="http://106.14.216.12:8080/x/y/world.html">访问第三张网页</a>
</body>
</html>
./wwwroot/world.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>这是第三张网页</h1>
<a href="http://106.14.216.12:8080/">访问首页</a>
<a href="http://106.14.216.12:8080/a/b/hello.html">访问第二张网页</a>
</body>
</html>
可以去w3school找一个布局放到index.html中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#header {
background-color: black;
color: white;
text-align: center;
padding: 5px;
}
#nav {
line-height: 30px;
background-color: #eeeeee;
height: 300px;
width: 100px;
float: left;
padding: 5px;
}
#section {
width: 350px;
float: left;
padding: 10px;
}
#footer {
background-color: black;
color: white;
clear: both;
text-align: center;
padding: 5px;
}
</style>
</head>
<body>
<div id="header">
<h1>City Gallery</h1>
</div>
<a href="http://106.14.216.12:8080/a/b/hello.html">访问第二张网页</a>
<a href="http://106.14.216.12:8080/x/y/world.html">访问第三张网页</a>
<div id="nav">
London<br>
Paris<br>
Tokyo<br>
</div>
<div id="section">
<h2>London</h2>
<p>
London is the capital city of England. It is the most populous city in the United Kingdom,
with a metropolitan area of over 13 million inhabitants.
</p>
<p>
Standing on the River Thames, London has been a major settlement for two millennia,
its history going back to its founding by the Romans, who named it Londinium.
</p>
</div>
<div id="footer">
4. HTTP的方法
HTTP常见的方法如下:
序号 | 方法 | 描述 |
---|---|---|
1 | GET | 从服务器获取资源。用于请求数据而不对数据进行更改。例如,从服务器获取网页、图片等。 |
2 | POST | 向服务器发送数据以创建新资源。常用于提交表单数据或上传文件。发送的数据包含在请求体中。 |
3 | PUT | 向服务器发送数据以更新现有资源。如果资源不存在,则创建新的资源。与 POST 不同,PUT 通常是幂等的,即多次执行相同的 PUT 请求不会产生不同的结果。 |
4 | DELETE | 从服务器删除指定的资源。请求中包含要删除的资源标识符。 |
5 | PATCH | 对资源进行部分修改。与 PUT 类似,但 PATCH 只更改部分数据而不是替换整个资源。 |
6 | HEAD | 类似于 GET,但服务器只返回响应的头部,不返回实际数据。用于检查资源的元数据(例如,检查资源是否存在,查看响应的头部信息)。 |
7 | OPTIONS | 返回服务器支持的 HTTP 方法。用于检查服务器支持哪些请求方法,通常用于跨域资源共享(CORS)的预检请求。 |
8 | TRACE | 回显服务器收到的请求,主要用于诊断。客户端可以查看请求在服务器中的处理路径。 |
9 | CONNECT | 建立一个到服务器的隧道,通常用于 HTTPS 连接。客户端可以通过该隧道发送加密的数据。 |
其中最常用的就是GET方法和POST方法。
GET方法和POST方法
GET方法一般用于获取某种资源信息,而POST方法一般用于将数据上传给服务器。但实际我们上传数据时也有可能使用GET方法,比如百度提交数据时实际使用的就是GET方法。
GET方法和POST方法都可以带参:
- GET方法是通过 URL 传参的,以?作为分隔符。
- POST方法是通过正文传参的。
从GET方法和POST方法的传参形式可以看出,POST方法能传递更多的参数,因为url的长度是有限制的,POST方法通过正文传参就可以携带更多的数据。
此外,使用POST方法传参更加私密,因为POST方法不会将你的参数回显到url当中,此时也就不会被别人轻易看到。但是不能说POST方法比GET方法更安全,因为POST方法和GET方法实际都不安全,要做到安全只能通过加密来完成。
用GET方法提交表单信息,表单信息会追加到URL文件路径后面,以?作为分隔符。
举例,/wwwroot / index.html 是一个简单的登录页面,我们的登录信息要发送给服务器是通过URL发送的,即GET方法
例如,我们输入用户名:牛魔王,密码:123456,然后我们的表单信息就会被追加在URL中,发送给服务端
观察服务器recv到的HTTP请求报文内的各字段,可以看到请求头内部的URL携带了用户传递的name和password信息
在URL中以 ?作为分隔符,左边为资源路径,右边为资源所需参数。如果左边路径是一个可执行程序,那么服务器端底层就fork创建子进程,程序替换可执行程序,通过管道将参数传递给子进程
说明:
- 如果我们将提交表单的方法改为POST方法,此时当我们填充完用户名和密码进行提交时,对应提交的参数就不会在url当中体现出来,而会通过正文将这两个参数传递给了服务器。
- 虽然GET和POST都没有将数据进行加密,但是GET方法不私密,因为信息会在URL中直接显示,可能被别人窥屏看到
- GET方法适用于小量数据发送,因为URL长度是有限的
- POST方法适用大量数据发送,因为数据放在HTTP请求的正文部分
5. HTTP状态码
分类 | 分类描述 |
---|---|
1XX | Infomational(信息性状态码),服务器收到请求,需要请求者继续执行操作 |
2XX | Success(成功状态码),操作被成功接收并处理 |
3XX | Redirection(重定向状态码),需要进一步的操作以完成请求 |
4XX | Client Error(客户端错误),请求包含语法错误或无法完成请求 |
5XX | Server Error(服务器错误状态码),服务器在处理请求的过程中发生了错误 |
最常见的状态码,比如200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)。
ps:程序员最想看到的:200-OK。程序员不想看到的:500-Internal-Server-Error。用户不想看到的:401-Unauthorized、403-Forbidden、408-Request-Time-out、404-not-found。
Redirection(重定向状态码)
重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置,此时这个服务器相当于提供了一个引路的服务。
重定向又可分为临时重定向和永久重定向,其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向。
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时你访问的直接就是重定向后的网站。而如果某个网站是临时重定向,那么每次访问该网站时如果需要进行重定向,都需要浏览器来帮我们完成重定向跳转到目标网站。
演示临时重定向
重定向:分为临时和永久重定向,302状态码搭配键值Location,Location的value就是新网站,登录页面跳转到首页就是临时重定向。
进行临时重定向时需要用到Location字段,Location字段是HTTP报头当中的一个属性信息,该字段表明了你所要重定向到的目标网站。
我们这里要演示临时重定向,可以将HTTP响应当中的状态码改为307,然后跟上对应的状态码描述,此外,还需要在HTTP响应报头当中添加Location字段,这个Location后面跟的就是你需要重定向到的网页(Location: https://www.qq.com/),比如我们这里将其设置为QQ的首页。
void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer;
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
// 返回响应的过程
bool ok = true;
// 构建正文
std::string text = ReadHtmlContent(req.file_path);
if (text.empty())
{
ok = false;
// text为空,表明客户访问了不存在的资源,返回404页面
std::string ph = wwwroot + "/err.html";
text = ReadHtmlContent(ph);
}
// 状态行
std::string response_line;
// if (ok) response_line = "HTTP/1.0 200 OK\r\n";
if (ok) response_line = "HTTP/1.0 307 Temporary Redirect\r\n";
else response_line = "HTTP/1.0 404 Not Found!";
// 响应报头
// Content_Length
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size()) + "\r\n";
// Location
response_header += "Location: https://www.qq.com/";
response_header += "\r\n";
// 空行
std::string blank_line = "\r\n";
// 组装起来
std::string response = response_line;
response += response_header + blank_line + text;
// write(sockfd, response.c_str(), response.size());
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
访问服务器IP:port
使用telnet工具查看HTTP响应报文 ,响应的状态码为307,Location字段就是QQ首页网址
404 Not Found
客户端出错大致是因为访问的资源路径不存在,我们可能会认为资源不存在是服务端的问题,我们来简单举个例子,一天,小明直接向他的父亲打电话要一个亿,他爸没有,这是他父亲的错吗?显然并不是,按照社会正常逻辑来看这是小明的错,因为并不是每个人都是王健林,我们的服务器也不可能林罗万象,包含所有的资源
演示404访问不存在路径资源,如果资源不存在,那么服务器端文件打开就会失败失败,所以我们构建一个404的err.html页面,如果资源路径文件打开失败则将我们err.html构成HTTP响应的正文部分返回,然后浏览器就会显示一个我们自定义404的HTML页面。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>404</title>
<style>
html, body {
height: 100%;
min-height: 450px;
font-size: 32px;
font-weight: 500;
color: #5d7399;
margin: 0;
padding: 0;
border: 0;
}
.content {
height: 100%;
position: relative;
z-index: 1;
background-color: #d2e1ec;
background-image: -webkit-linear-gradient(top, #bbcfe1 0%, #e8f2f6 80%);
background-image: linear-gradient(to bottom, #bbcfe1 0%, #e8f2f6 80%);
overflow: hidden;
}
.snow {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 20;
}
.main-text {
padding: 20vh 20px 0 20px;
text-align: center;
line-height: 2em;
font-size: 5vh;
}
.main-text h1 {
font-size: 45px;
line-height: 48px;
margin: 0;
padding: 0;
}
.main-text-a {
height: 32px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.main-text-a a {
font-size: 16px;
text-decoration: none;
color: #0066CC;
}
.main-text-a a:hover {
color: #000;
}
.home-link {
font-size: 0.6em;
font-weight: 400;
color: inherit;
text-decoration: none;
opacity: 0.6;
border-bottom: 1px dashed rgba(93, 115, 153, 0.5);
}
.home-link:hover {
opacity: 1;
}
.ground {
height: 160px;
width: 100%;
position: absolute;
bottom: 0;
left: 0;
background: #f6f9fa;
box-shadow: 0 0 10px 10px #f6f9fa;
}
.ground:before, .ground:after {
content: '';
display: block;
width: 250px;
height: 250px;
position: absolute;
top: -62.5px;
z-index: -1;
background: transparent;
-webkit-transform: scaleX(0.2) rotate(45deg);
transform: scaleX(0.2) rotate(45deg);
}
.ground:after {
left: 50%;
margin-left: -166.66667px;
box-shadow: -340px 260px 15px #8193b2, -620px 580px 15px #8193b2, -900px 900px 15px #b0bccf, -1155px 1245px 15px #b4bed1, -1515px 1485px 15px #8193b2, -1755px 1845px 15px #8a9bb8, -2050px 2150px 15px #91a1bc, -2425px 2375px 15px #bac4d5, -2695px 2705px 15px #a1aec6, -3020px 2980px 15px #8193b2, -3315px 3285px 15px #94a3be, -3555px 3645px 15px #9aa9c2, -3910px 3890px 15px #b0bccf, -4180px 4220px 15px #bac4d5, -4535px 4465px 15px #a7b4c9, -4840px 4760px 15px #94a3be;
}
.ground:before {
right: 50%;
margin-right: -166.66667px;
box-shadow: 325px -275px 15px #b4bed1, 620px -580px 15px #adb9cd, 925px -875px 15px #a1aec6, 1220px -1180px 15px #b7c1d3, 1545px -1455px 15px #7e90b0, 1795px -1805px 15px #b0bccf, 2080px -2120px 15px #b7c1d3, 2395px -2405px 15px #8e9eba, 2730px -2670px 15px #b7c1d3, 2995px -3005px 15px #9dabc4, 3285px -3315px 15px #a1aec6, 3620px -3580px 15px #8193b2, 3880px -3920px 15px #aab6cb, 4225px -4175px 15px #9dabc4, 4510px -4490px 15px #8e9eba, 4785px -4815px 15px #a7b4c9;
}
.mound {
margin-top: -80px;
font-weight: 800;
font-size: 180px;
text-align: center;
color: #dd4040;
pointer-events: none;
}
.mound:before {
content: '';
display: block;
width: 600px;
height: 200px;
position: absolute;
left: 50%;
margin-left: -300px;
top: 50px;
z-index: 1;
border-radius: 100%;
background-color: #e8f2f6;
background-image: -webkit-linear-gradient(top, #dee8f1, #f6f9fa 60px);
background-image: linear-gradient(to bottom, #dee8f1, #f6f9fa 60px);
}
.mound:after {
content: '';
display: block;
width: 28px;
height: 6px;
position: absolute;
left: 50%;
margin-left: -150px;
top: 68px;
z-index: 2;
background: #dd4040;
border-radius: 100%;
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg);
box-shadow: -56px 12px 0 1px #dd4040, -126px 6px 0 2px #dd4040, -196px 24px 0 3px #dd4040;
}
.mound_text {
-webkit-transform: rotate(6deg);
transform: rotate(6deg);
}
.mound_spade {
display: block;
width: 35px;
height: 30px;
position: absolute;
right: 50%;
top: 42%;
margin-right: -250px;
z-index: 0;
-webkit-transform: rotate(35deg);
transform: rotate(35deg);
background: #dd4040;
}
.mound_spade:before, .mound_spade:after {
content: '';
display: block;
position: absolute;
}
.mound_spade:before {
width: 40%;
height: 30px;
bottom: 98%;
left: 50%;
margin-left: -20%;
background: #dd4040;
}
.mound_spade:after {
width: 100%;
height: 30px;
top: -55px;
left: 0%;
box-sizing: border-box;
border: 10px solid #dd4040;
border-radius: 4px 4px 20px 20px;
}
</style>
</head>
<body translate="no">
<div class="content">
<canvas class="snow" id="snow" width="1349" height="400"></canvas>
<div class="main-text">
<h1>404 天呐!出错了 ~<br><br>您好像去了一个不存在的地方! (灬ꈍ ꈍ灬)</h1>
<div class="main-text-a"><a href="#">< 返回 首页</a></div>
</div>
<div class="ground">
<div class="mound">
<div class="mound_text">404</div>
<div class="mound_spade"></div>
</div>
</div>
</div>
<script>
(function () {
function ready(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
function makeSnow(el) {
var ctx = el.getContext('2d');
var width = 0;
var height = 0;
var particles = [];
var Particle = function () {
this.x = this.y = this.dx = this.dy = 0;
this.reset();
}
Particle.prototype.reset = function () {
this.y = Math.random() * height;
this.x = Math.random() * width;
this.dx = (Math.random() * 1) - 0.5;
this.dy = (Math.random() * 0.5) + 0.5;
}
function createParticles(count) {
if (count != particles.length) {
particles = [];
for (var i = 0; i < count; i++) {
particles.push(new Particle());
}
}
}
function onResize() {
width = window.innerWidth;
height = window.innerHeight;
el.width = width;
el.height = height;
createParticles((width * height) / 10000);
}
function updateParticles() {
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#f6f9fa';
particles.forEach(function (particle) {
particle.y += particle.dy;
particle.x += particle.dx;
if (particle.y > height) {
particle.y = 0;
}
if (particle.x > width) {
particle.reset();
particle.y = 0;
}
ctx.beginPath();
ctx.arc(particle.x, particle.y, 5, 0, Math.PI * 2, false);
ctx.fill();
});
window.requestAnimationFrame(updateParticles);
}
onResize();
updateParticles();
}
ready(function () {
var canvas = document.getElementById('snow');
makeSnow(canvas);
});
})();
</script>
</body>
</html>
我们在浏览器访问一个不存在的资源路径,浏览器显示了我们自定义的404HTML页面
服务器错误
服务端错误可能是服务器创建新线程服务连接请求时 listen 失败或 accept 失败等服务端内部出错
6. HTTP常见Header
HTTP常见的Header如下:
- Content-Type:数据类型(text/html等)。
- Content-Length:正文的长度。
- Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
- User-Agent:声明用户的操作系统和浏览器的版本信息。
- Referer:当前页面是哪个页面跳转过来的。
- Location:搭配3XX状态码使用,告诉客户端接下来要去哪里访问。
- Connection:keep-alive表示连接不会在传输后关闭
- Set-Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能。
Host
Host字段表明了客户端要访问的服务的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口。但客户端不就是要访问服务器吗?为什么客户端还要告诉服务器它要访问的服务对应的IP和端口?
因为有些服务器实际提供的是一种代理服务(正向代理),也就是代替客户端向其他服务器发起请求,然后将请求得到的结果再返回给客户端。在这种情况下客户端就必须告诉代理服务器它要访问的服务对应的IP和端口,此时Host提供的信息就有效了。
User-Agent
User-Agent代表的是客户端对应的操作系统和浏览器的版本信息。
比如当我们用电脑下载某些软件时,它会自动向我们展示与我们操作系统相匹配的版本,这实际就是因为我们在向目标网站发起请求的时候,User-Agent字段当中包含了我们的主机信息,此时该网站就会向你推送相匹配的软件版本。
Referer
Referer代表的是你当前是从哪一个页面跳转过来的。Referer记录上一个页面的好处一方面是方便回退,另一方面可以知道我们当前页面与上一个页面之间的相关性。
也就对应于浏览器后退箭头
Connection
HTTP/1.0是通过request&response的方式来进行请求和响应的,HTTP/1.0常见的工作方式就是客户端和服务器先建立链接,然后客户端发起请求给服务器,服务器再对该请求进行响应,然后立马端口连接。
但如果一个连接建立后客户端和服务器只进行一次交互,就将连接关闭,就太浪费资源了,并且效率低下,因此现在主流的HTTP/1.1是支持长连接的。所谓的长连接就是建立连接后,客户端建立一个Tcp连接后,可以向服务器一次写入多个HTTP请求,而服务器在上层依次读取这些请求就行了,此时一条连接就可以传送大量的请求和响应,这就是长连接。
如果HTTP请求或响应报头当中的Connect字段对应的值是Keep-Alive,就代表支持长连接。
例如淘宝网页,一个网页上有很多图片等资源,每一个资源在服务器上都是一个文件,所以一个网页上有100个图片,总的HTTP请求就是101个,显然对于该情况短连接的效率很低,超链接、图片等信息获取时浏览器会自动构建HTTP请求发送至服务器
举例,网站主页显示多个图片,观察是否是多个HTTP请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="header">
<h1>快乐老家</h1>
</div>
<div>
<a href="http://106.14.216.12:8080/a/b/hello.html">访问第二张网页</a>
<a href="http://106.14.216.12:8080/x/y/world.html">访问第三张网页</a>
</div>
<!-- 根据src自动向浏览器发送二次请求 -->
<!-- <img src="/image/1.jpg" alt="好图1" width="100" width="200"> -->
<div>
<img src="/image/4.gif" alt="图4" width="200" width="300">
</div>
<div>
<img src="/image/5.gif" alt="图5" width="300" width="300">
</div>
<div>
<img src="/image/6.gif" alt="图6" width="300" width="300">
</div>
</body>
</html>
Set-Cookie
HTTP实际上是一种无状态协议,HTTP的每次请求/响应之间是没有任何关系的,但你在使用浏览器的时候发现并不是这样的。
比如当你登录一次CSDN后,就算你把CSDN网站关了甚至是重启电脑,当你再次打开CSDN网站时,CSDN并没有要求你再次输入账号和密码,这实际上是通过cookie技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种cookie数据。
HTTP不会记录曾经访问过的服务,即上一次请求和本次请求是一个独立的事件。但是我们知道,如今各类网站、app都需要先登录用户才能更好的享受服务,那么访问第一个视频时登录用户,为什么刷第二个视频时,登录信息还存在?HTTP不是无状态的吗?
这些cookie数据实际都是对应的服务器方写的,如果你将对应的某些cookie删除,那么此时可能就需要你重新进行登录认证了,因为你删除的可能正好就是你登录时所设置的cookie信息。
cookie是什么呢?
因为HTTP是一种无状态协议,如果没有cookie的存在,那么每当我们要进行页面请求时都需要重新输入账号和密码进行认证,这样太麻烦了。
比如你是某个视频网站的VIP,这个网站里面的VIP视频有成百上千个,你每次点击一个视频都要重新进行VIP身份认证。而HTTP不支持记录用户状态,那么我们就需要有一种独立技术来帮我们支持,这种技术目前现在已经内置到HTTP协议当中了,叫做cookie。
当我们第一次登录某个网站时,需要输入我们的账号和密码进行身份认证,此时如果服务器经过数据比对后判定你是一个合法的用户,那么为了让你后续在进行某些网页请求时不用重新输入账号和密码,此时服务器就会进行Set-Cookie的设置。(Set-Cookie也是HTTP报头当中的一种属性信息)
当认证通过并在服务端进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时就会将这个Set-Cookie响应给浏览器。而浏览器收到响应后会自动提取出Set-Cookie的值,将其保存在浏览器的cookie文件当中,此时就相当于我的账号和密码信息保存在本地浏览器的cookie文件当中。
从第一次登录认证之后,浏览器再向该网站发起的HTTP请求当中就会自动包含一个cookie字段,其中携带的就是我第一次的认证信息,此后对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的cookie字段,而不会重新让你输入账号和密码了。
也就是在第一次认证登录后,后续所有的认证都变成了自动认证,这就叫做cookie技术。
内存级别&文件级别
cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件。
将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的,因为浏览器启动后也是一个进程。
将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的。
cookie被盗
如果你浏览器当中保存的cookie信息被非法用户盗取了,那么此时这个非法用户就可以用你的cookie信息,以你的身份去访问你曾经访问过的网站,我们将这种现象称为cookie被盗取了。
比如你不小心点了某个链接,这个链接可能就是一个下载程序,当你点击之后它就会通过某种方式把程序下载到你本地,并且自动执行该程序,该程序会扫描你的浏览器当中的cookie目录,把所有的cookie信息通过网络的方式传送给恶意方,当恶意方拿到你的cookie信息后就可以拷贝到它的浏览器对应的cookie目录当中,然后以你的身份访问你曾经访问过的网站。
SessionID
单纯的使用cookie是非常不安全的,因为此时cookie文件当中就保存的是你的私密信息,一旦cookie文件泄漏你的隐私信息也就泄漏。
所以当前主流的服务器还引入了SessionID这样的概念,当我们第一次登录某个网站输入账号和密码后,服务器认证成功后会生成一个对应的SessionID,这个SessionID与用户信息是不相关的,即不包含用户名和密码信息。系统会将所有登录用户的SessionID值统一维护起来。
此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后会自动提取出SessionID的值,将其保存在浏览器的cookie文件当中。后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个SessionID。
而服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合当中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了,然后就会正常处理你发来的请求,这就是我们当前主流的工作方式。
安全是相对的
引入SessionID之后,浏览器当中的cookie文件保存的是SessionID,此时这个cookie文件同样可能被盗取。此时用户的账号和密码虽然不会泄漏了,但用户对应的SessionID是会泄漏的,非法用户仍然可以盗取我的SessionID去访问我曾经访问过的服务器,相当于还是存在刚才的问题。
- 之前的工作方式就相当于把账号和密码信息在浏览器当中再保存一份,每次请求时都自动将账号和密码的信息携带上,但是账号和密码一直在网当中发送太不安全了。
- 因此现在的工作方式是,服务器只有在第一次认证的时候需要在网络中传输账号和密码,此后在网络上发送的都是SessionID。
这种方法虽然没有真正解决安全问题,但这种方法是相对安全的。互联网上是不存在绝对安全这样的概念的,任何安全都是相对的,就算你将发送到网络当中的信息进行加密,也有可能被别人破解。
不过在安全领域有一个准则:如果破解某个信息的成本已经远远大于破解之后获得的收益(说明做这个事是赔本的),那么就可以说这个信息是安全的。
引入SessionID后的好处
- 在引入SessionID之前,用户登录的账号信息都是保存在浏览器内部的,此时的账号信息是由客户端去维护的。
- 而引入SessionID后,用户登录的账号信息是有服务器去维护的,在浏览器内部保存的只是SessionID。
此时虽然SessionID可能被非法用户盗取,但服务器也可以使用各种各样的策略来保证用户账号的安全。
- IP是有归类的,可以通过IP地址来判断登录用户所在的地址范围。如果一个账号在短时间内登录地址发送了巨大变化,此时服务器就会立马识别到这个账号发生异常了,进而在服务器当中清除对应的SessionID的值。这时当你或那个非法用户想要访问服务器时,就都需要重新输入账号和密码进行身份认证,而只有你是知道自己的密码的,当你重新认证登录后服务器就可以将另一方识别为非法用户,进而对该非法用户进行对应的黑名单/白名单认证。
- 当操作者想要进行某些高权限的操作时,会要求操作者再次输入账号和密码信息,再次确认身份。就算你的账号被非法用户盗取了,但非法用户在改你密码时需要输入旧密码,这是非法用户在短时间内无法做到的,因为它并不知道你的密码。这也就是为什么账号被盗后还可以找回来的原因,因为非法用户无法在短时间内修改你的账号密码,此时你就可以通过追回的方式让当前的SessionID失效,让使用该账号的用户进行重新登录认证。
- SessionID也有过期策略,比如SessionID是一个小时内是有效的。所以即便你的SessionID被非法用户盗取了,也仅仅是在一个小时内有效,而且在功能上受约束,所以不会造成太大的影响。
任何事情都有两面性,如果不是这些非法用户的存在,现在的服务器肯定是漏洞百出,只有双方不断进行对抗双方才能不断进步。
实验演示
当浏览器访问我们的服务器时,如果服务器给浏览器的HTTP响应当中包含Set-Cookie字段,那么当浏览器再次访问服务器时就会携带上这个cookie信息。
因此我们可以在服务器的响应报头当中直接固定添加上一个Set-Cookie字段,看看浏览器第二次发起HTTP请求时是否会带上这个Set-Cookie字段。
#pragma once
#include "Socket.hpp"
#include "Log.hpp"
#include <sys/socket.h>
#include <sys/types.h>
#include <fstream>
#include <sstream>
#include <vector>
#include <unordered_map>
// web根目录
const std::string wwwroot = "./wwwroot";
const static uint16_t defaultport = 8080;
const std::string sep = "\r\n";
class HttpServer;
class ThreadData
{
public:
ThreadData(int fd) : sockfd(fd)
{}
public:
int sockfd;
HttpServer* svr;
};
class HttpRequest
{
public:
void Deserialize(std::string req)
{
while (true)
{
size_t pos = req.find(sep);
if (pos == std::string::npos) break;
std::string temp = req.substr(0, pos);
if (temp.empty()) break; // 读到空行退出
req_header.push_back(temp);
req.erase(0, pos + sep.size()); // 删除已经读完第一行
}
// 这里应该按键值字段的contentlength来截取正文部分,偷个懒
text = req;
}
void Parse()
{
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
// 在服务区端构建资源所在路径,web根目录就是./wwwroot,所以一般而言web根目录并不是Linux的根目录
file_path = wwwroot;
// 要访问例如 / 或者 /index.html,统一构建为./wwwroot/index.html
// 要访问例如 /a/b/c路径的,构建为 ./wwwroot + url,注意url路径自带 /
if (url == "/" || url == "index.html")
file_path += "/index.html";
else
file_path += url;
size_t pos = file_path.rfind(".");
if (pos == std::string::npos) suffix = ".html";
else suffix = file_path.substr(pos);
}
void DebugPrint()
{
for (auto& line : req_header)
{
std::cout << "---------------------------" << std::endl;
std::cout << line << "\n\n";
}
std::cout << "method " << method << std::endl;
std::cout << "url " << url << std::endl;
std::cout << "http_version " << http_version << std::endl;
std::cout << "file_path " << file_path << std::endl;
std::cout << text << std::endl;
}
public:
std::vector<std::string> req_header;
std::string text;
std::string method;
std::string url;
std::string http_version;
std::string file_path = wwwroot;
std::string suffix;
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport) : _port(port)
{
// 构建content_type映射
content_type[".html"] = "text/html";
content_type[".png"] = "image/png";
content_type[".jpg"] = "image/jpeg";
content_type[".gif"] = "image/gif";
}
bool start()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
while (true)
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(&clientip, &clientport);
if (sockfd < 0) continue;
_log(Info, "get a new conect, sockfd: %d", sockfd);
ThreadData* td = new ThreadData(sockfd);
// td->sockfd = sockfd;
td->svr = this;
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
// 将html文件构建为一个字符串返回,还要考虑二进制文件
static std::string ReadHtmlContent(const std::string& htmlpath)
{
// 打开path路径文件,打开失败表明文件不存在返回空串
std::ifstream in(htmlpath, std::ios::binary);
if (!in.is_open()) return "";
// 计算文件大小
in.seekg(0, std::ios_base::end);
auto len = in.tellg();
in.seekg(0, std::ios_base::beg);
std::string content;
content.resize(len);
in.read((char*)content.c_str(), content.size());
// std::string line, content;
// while (std::getline(in, line))
// {
// content += line;
// }
in.close();
return content;
}
std::string SuffixToDesc(const std::string& suffix)
{
auto iter = content_type.find(suffix);
if (iter == content_type.end()) return content_type["html"];
else return content_type[suffix];
}
// 单独封装出来
void HandlerHttp(int sockfd)
{
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer;
HttpRequest req;
req.Deserialize(buffer);
req.Parse();
// req.DebugPrint();
// std::string url = "a/a/a";
// std::string path = wwwroot + url; // 构建路径 wwwroot/a/a/a/index.html
// 返回响应的过程
bool ok = true;
// 构建正文
std::string text = ReadHtmlContent(req.file_path);
if (text.empty())
{
ok = false;
// text为空,表明客户访问了不存在的资源,返回404页面
std::string ph = wwwroot + "/err.html";
text = ReadHtmlContent(ph);
}
// 状态行
std::string response_line;
if (ok) response_line = "HTTP/1.0 200 OK\r\n";
// if (ok) response_line = "HTTP/1.0 307 Temporary Redirect\r\n";
else response_line = "HTTP/1.0 404 Not Found!";
// 响应报头
// Content_Length
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size()) + "\r\n";
// Location
// response_header += "Location: https://www.qq.com/";
// response_header += "\r\n";
// Content_Type
response_header += "Content-Type: " + SuffixToDesc(req.suffix) + "\r\n";
// Cookie
response_header += "Set-Cookie: name=ljs:passwd=123456" ;
response_header += "\r\n";
// 空行
std::string blank_line = "\r\n";
// 组装起来
std::string response = response_line;
response += response_header + blank_line + text;
// write(sockfd, response.c_str(), response.size());
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void* ThreadRun(void* args)
{
// 线程分离
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
// 调用类内部的函数,直接把 HandlerHttp 改为 static 更方便
td->svr->HandlerHttp(td->sockfd);
delete td;
return nullptr;
}
private:
Sock _listensock;
uint16_t _port;
std::unordered_map<std::string, std::string> content_type; // content_type的映射关系
};
访问后,可以看到浏览器记录的cookie
运行服务器后,用浏览器访问我们的服务器,此时通过Fiddler可以看到我们的服务器发给浏览器的HTTP响应报头当中包含了这个Set-Cookie字段。
如果要设置多个cookie,就在HTTP响应的响应报头添加多个Set-Cookie即可