网络基础「HTTP」

🔭个人主页: 北 海
🛜所属专栏: Linux学习之旅、神奇的网络世界
💻操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

    • 1.再谈协议
      • 1.1.认识URL
      • 1.2.Encode 和 Decode
    • 2.HTTP 协议
      • 2.1.协议格式
      • 2.2.见一见请求
      • 2.3.见一见响应
    • 3.模拟实现响应
      • 3.1.简单实现
      • 3.2.重要属性
        • 3.2.1.Content-Length
        • 3.2.2.Content-Type
      • 3.3.请求分析
        • 3.3.1.路径处理
        • 3.3.2.类型处理
        • 3.3.3.请求方法
        • 3.3.4.状态码
        • 3.3.5.Cookie 缓存
        • 3.3.6.补充


1.再谈协议

在上一篇文章中,我们了解了 协议 的制定与使用流程,不过太过于简陋了,真正的 协议 会复杂得多,也强大得多,比如在网络中使用最为广泛的 HTTP/HTTPS 超文本传输协议

但凡是使用浏览器进行互联网冲浪,那必然离不开这个 协议HTTP/HTTPS 不仅支持传输文本,还支持传输图片、音频、视频等 资源

客户端/浏览器上传资源的大小称为 上行流量,获取资源的大小称为 下行流量,网速则是单位时间内所能传输的流量大小,所以网速越快,上传/下载的体验就会越好

可以在浏览器中根据 CSDN服务器的 IPPort,以及资源路径,基于 HTTPS 协议,获取我们所需要的资源

比如 https://blog.csdn.net/weixin_61437787?type=blog 就表示我的个人主页

1.1.认识URL

诸如上面的网址称为 URL -> Uniform Resource Locator 统一资源定位符,也就我们熟知的 超链接/链接URL 中包含了 协议、IP地址、端口号、资源路径、参数 等信息

注:登录信息现在已经不使用了,因为不够安全

IP地址在哪呢?

  • blog.csdn.net 叫做 域名,可以通过 域名 解析为对应的IP地址
  • 使用 域名 解析工具解析后,下面就是 CSDN 服务器的IP地址

那端口号呢?

  • 为了给用户提供良好的体验,一个成熟的服务是不会随意改变端口号的
  • 只要是使用了 HTTP 协议,默认使用的都是 80 端口号,而 HTTPS 则是 443
  • 如果我们没指明端口号,浏览器就会使用 协议 的默认端口号

现在大多数网站使用的都是 HTTPS 协议,更加安全,默认端口为 443

至于资源路径,这是 Linux 中的文件路径,比如下面这个 URL

https://csdnnews.blog.csdn.net/article/details/136575090?spm=1000.2115.3001.5926

其资源路径为 /article/details/136575090,与 Linux 中的路径规则一致,这里的路径起源于 web 根目录(不一定是 Linux 中的根目录)

Linux 机器中存放资源(服务器),客户端访问时只需要知晓目标资源的存储路径,就能访问了,除了上面这些信息外,URL 中还存在特殊的 分隔符

  • :// 用于分隔 协议IP地址
  • : 用于分隔 IP地址端口号
  • / 表示路径,同时第一个 / 可以分隔 端口号资源路径
  • ? 则是用来分隔 资源路径参数

这些特殊 分隔符 很重要,这是属于 协议 的一部分,就像我们之前定义的 两正整数运算协议 中的 一样,如果没有 分隔符,那就无法获取 URL 中的信息

如果 资源路径 或者后面的 参数 中不小心携带了 分隔符 会怎么样?

  • 最好不要出现,即使出现,服务器在传输之前也会将其进行特殊化处理,比如将 (空格)解释为 %20

至于 参数 是一组 K=V 结构,浏览器可以从 参数 中获取到重要数据

1.2.Encode 和 Decode

Encode 就是将诸如 分隔符中文其他非英语语言 等转换成计算机能认识的符合,比如在浏览器搜索框中输入 //?:: 请求相关资源,实际 URL 中的 参数q=%2F%2F%3F%3A%3A

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


即便输入的是 中文,也能进行转码

所以为什么有的 URL 很长?就是因为在转换后字符数会增多

转码这个工作也需要 服务器 完成,基于之前的 ServiceIO() 函数,相对完整的请求处理流程如下

void ServiceIO(const int& sock, const std::string ip, const uint16_t& port)
{while(true){// 1.读取数据// 2.移除报头// 3.反序列化// 4.Decode 解码// 5.业务处理// 6.Encode 编码// 7.序列化// 8.添加报头// 9.发送数据}
}

2.HTTP 协议

2.1.协议格式

HTTP 协议由 Request 请求Response 响应 两部分组成

从宏观角度来看,HTTP 请求 分为这几部分:

  • 请求行,包括请求方法(GET / POST)、URL、协议版本(http/1.0 http/1.1 http/2.0
  • 请求报头,表示请求的详细细节,由多组 k: v 结构所组成
  • 空行,区分报头和有效载荷
  • 有效载荷(可以没有)

HTTP 协议中是使用 \r\n 作为 分隔符

如何分离 协议报头有效载荷

  • 以空行 \r\n 进行分隔,空行之前为协议报头,空行之后为有效载荷

如何进行 序列化与反序列

  • 序列化:使用 \r\n 进行拼接
  • 反序列化:根据 \r\n 进行读取

至于 HTTP 响应 分为这几部分:

  • 状态行,协议版本、状态码、状态码描述
  • 响应报头,表示响应的详细细节,由多组 k: v 结构所组成
  • 空行,区分报头和有效载荷
  • 有效载荷,即客户端请求的资源

HTTP 响应 中关于 协议报头与有效载荷的分离序列化与反序列化 等问题和 HTTP 请求 中的处理方式一致

如何理解协议版本?

  • 客户端和服务器可能使用了不同的 HTTP 版本
  • 服务器可以根据协议版本的匹配情况进行功能响应

什么是状态码?

  • 状态码类似于 C/C++ 中的错误码,可以反应请求的情况
  • 常见的状态码:404,状态码的描述为 No Found

2.2.见一见请求

将浏览器视为客户端,编写服务器,浏览器通过 IP+Port 访问服务器时,就会发出 HTTP 请求,服务器在接收后可以进行打印,也就可以看到 HTTP 请求了

首先完成 HTTP 服务器的编写

所需文件:

  • Err.hpp 错误码文件
  • Log.hpp 日志输出
  • Sock.hpp 套接字接口封装
  • HttpServer.hpp 服务器头文件
  • HttpServer.cc 服务器源文件
  • Makefile 自动化编译脚本

Err.hpp 错误码文件

#pragma onceenum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR,SETSID_ERR,CHDIR_ERR,OPEN_ERR,READ_ERR,
};

Log.hpp 日志输出

#pragma once#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>using namespace std;enum
{Debug = 0,Info,Warning,Error,Fatal
};static const string file_name = "log/TcpServer.log";string getLevel(int level)
{vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};//避免非法情况if(level < 0 || level >= vs.size() - 1)return vs[vs.size() - 1];return vs[level];
}string getTime()
{time_t t = time(nullptr);   //获取时间戳struct tm *st = localtime(&t);    //获取时间相关的结构体char buff[128];snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);return buff;
}//处理信息
void logMessage(int level, const char* format, ...)
{//日志格式:<日志等级> [时间] [PID] {消息体}string logmsg = getLevel(level);    //获取日志等级logmsg += " " + getTime();  //获取时间logmsg += " [" + to_string(getpid()) + "]";    //获取进程PID//截获主体消息char msgbuff[1024];va_list p;va_start(p, format);    //将 p 定位至 format 的起始位置vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取va_end(p);logmsg += " {" + string(msgbuff) + "}";    //获取主体消息// 直接输出至屏幕上cout << logmsg << endl;// //持久化。写入文件中// FILE* fp = fopen(file_name.c_str(), "a");   //以追加的方式写入// if(fp == nullptr) return;   //不太可能出错// fprintf(fp, "%s\n", logmsg.c_str());// fflush(fp); //手动刷新一下// fclose(fp);// fp = nullptr;
} 

Sock.hpp 套接字接口封装

#pragma once#include "Log.hpp"
#include "Err.hpp"#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>class Sock
{const static int default_sock = -1;const static int default_backlog = 32;
public:Sock():sock(default_sock){}// 创建套接字void Socket(){sock = socket(AF_INET, SOCK_STREAM, 0);if(sock == -1){logMessage(Fatal, "Creater Socket Fail! [%d]->%s", errno, strerror(errno));exit(SOCKET_ERR);}logMessage(Debug, "Creater Socket Success");}// 绑定IP与端口号void Bind(const uint16_t& port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if(bind(sock, (struct sockaddr*)&local, sizeof(local)) == -1){logMessage(Fatal, "Bind Socket Fail! [%d]->%s", errno, strerror(errno));exit(BIND_ERR);}logMessage(Debug, "Bind Socket Success");}// 进入监听状态void Listen(){if(listen(sock, default_backlog) == -1){logMessage(Fatal, "Listen Socket Fail! [%d]->%s", errno, strerror(errno));exit(LISTEN_ERR);}}// 尝试处理连接请求int Accept(std::string* ip, uint16_t* port){struct sockaddr_in client;socklen_t len = sizeof(client);int retSock = accept(sock, (struct sockaddr*)&client, &len);if(retSock < 0)logMessage(Warning, "Accept Fail! [%d]->%s", errno, strerror(errno));else{*ip = inet_ntoa(client.sin_addr);*port = ntohs(client.sin_port);logMessage(Debug, "Accept [%d -> %s:%d] Success", retSock, ip->c_str(), *port);}return retSock;}// 尝试进行连接int Connect(const std::string& ip, const uint16_t& port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);server.sin_addr.s_addr = inet_addr(ip.c_str());return connect(sock, (struct sockaddr*)&server, sizeof(server));}// 获取sockint GetSock(){return sock;}// 关闭sockvoid Close(){if(sock != default_sock)close(sock);logMessage(Debug, "Close Sock Success");}~Sock(){}
private:int sock; // 既可以是监听套接字,也可以是连接成功后返回的套接字
};

在实现 HTTP 服务器时,我们可以假设服务器一次就将 请求 全部读完了

HttpServer.hpp 服务器头文件

#pragma once#include "Sock.hpp"#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>namespace HttpServer
{class Server;// 线程信息类class ThreadData{public:ThreadData(const int& sock, const uint16_t& port, const std::string& ip, Server* psvr):_sock(sock), _port(port), _ip(ip), _psvr(psvr){}~ThreadData(){close(_sock);}public:int _sock;uint16_t _port;std::string _ip;Server* _psvr; // 回指指针};class Server{const static uint16_t default_port = 8888;using func_t = function<string(const string&)>;public:Server(const func_t& func, const uint16_t port = default_port):_func(func), _port(port){}void Init(){_listen_sock.Socket();_listen_sock.Bind(_port);_listen_sock.Listen();logMessage(Debug, "Init Server Success");}void Start(){while(true){uint16_t clientPort;std::string clientIP;int clientSock = _listen_sock.Accept(&clientIP, &clientPort);// 接受连接失败,重新尝试if(clientSock < 0)continue;ThreadData* td = new ThreadData(clientSock, clientPort, clientIP, this);pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, td);}}static void* threadRoutine(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);// 假设一次都读完了char buff[1024];ssize_t test = -1;ssize_t s = recv(td->_sock, buff, sizeof(buff) - 1, 0);if(s > 0){buff[s] = 0;std::string response = td->_psvr->_func(buff);send(td->_sock, response.c_str(), response.size(), 0);}else{logMessage(Debug, "Cilent [%d -> %s:%d] Quit", td->_sock, td->_ip.c_str(), td->_port);}delete td;return nullptr;}~Server(){_listen_sock.Close();}private:uint16_t _port;Sock _listen_sock;func_t _func;};
}

HttpServer.cc 服务器源文件

#include "HttpServer.hpp"#include <iostream>
#include <string>
#include <memory>using namespace std;string HttpHandler(const string& request)
{// 打印请求cout << request << endl;return "";
}int main()
{unique_ptr<HttpServer::Server> psvr(new HttpServer::Server(HttpHandler));psvr->Init();psvr->Start();return 0;
}

Makefile 自动化编译脚本

HttpServer : HttpServer.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -rf HttpServer

编译并启动服务器

现在服务器已经准备好了,浏览器输入 IP+Port 发出 HTTP 请求,因为当前服务器并未进行任何响应,所以浏览器无法显示页面


这就是 HTTP 请求

其中包含了 请求行、请求报头、空行


从请求行中可以看到当前使用的是 GET 请求,基于 HTTP/1.1 版本,且请求的资源路径为 /,如果我们在浏览器中指定资源路径,那么服务器则会得到该路径

在请求报头中包含多组属性

  • Host 表示当前请求的服务器 IP+Port
  • Connection 表示当前连接模式为长连接还是短连接
  • Cache-Control 表示双方在通信时缓存的最大生存时间
  • Upgrade-Insecure-Requests 表示是否将 HTTP 连接方式升级为 HTTPS 连接
  • User-Agent 表示用户端(也就是浏览器)的信息
  • Accept 表示客户端(浏览器)能接受的响应类型
  • Accept-Encoding 表示客户端(浏览器)能接受的 Encoding 类型
  • Accept-Language 表示客户端(浏览器)能接受的编码符号

User-Agent 很有意思,它能让服务器根据不同的设备,提供不同的 标签,比如下载微信客户端,使用 Windows 电脑访问,默认显示的下载方式为 电脑下载,但如果使用 iPhone 访问,下载方式则会变为 App Store

2.3.见一见响应

可以通过 telnet 这个工具获取服务器的响应,比如获取 百度 服务器的响应

telnet www.baidu.com 80


输入 ^] 连接服务器(ctrl + ]

^]

此时就表示已经和 百度 的服务器建立了连接


接着发出一个最简单的请求,看看 百度 服务器的响应结果

注意: 需要先按回车后,再发出请求,请求发出后需要再次回车表示空行,同时回车发送

GET / HTTP/1.0

下面这个就是 百度 服务器对于请求资源路径为 / 时的响应结果,也就是前端页面信息


将响应结果中的有效载荷部分作为前端页面代码,就可以得到这样一个页面:

而这就是 百度 的默认页面,它的响应结果也得遵循 HTTP 协议的响应格式


状态行中包括了 HTTP 版本、状态码、状态描述,响应报头中是各种 属性,重要字段后面再谈,有效载荷中则是请求的 资源


3.模拟实现响应

了解了 HTTP 响应的格式后,可以根据该格式实现一个简单的响应,发送给客户端(浏览器)

3.1.简单实现

在之前实现的 HTTP 服务器中,只需要对 HttpHandler() 方法中的返回值进行修改即可

  1. 添加状态行
  2. 添加响应报头(可省略)
  3. 添加空行
  4. 添加有效载荷

HttpHandler() 业务处理函数 — 位于 HttpServer.cc 服务器源文件

const static string SEP = "\r\n";string HttpHandler(const string& request)
{// 打印请求cout << request << endl;string response = "HTTP/1.0 200 OK" + SEP; // 状态行response += SEP; // 空行response += "Hello HTTP!"; // 有效载荷return response;
}

编译并启动服务器,浏览器发出请求,就能得到服务器的简单响应

如果将 有效载荷 部分替换成前端代码,就可以得到一个更为美观的响应页面(浏览器识别 有效载荷HTML 代码,自动解释为网页)

关于前端页面的学习:HTML

HttpHandler() 业务处理函数 — 位于 HttpServer.cc 服务器源文件

const static string SEP = "\r\n";string HttpHandler(const string &request)
{// 打印请求cout << request << endl;string response = "HTTP/1.0 200 OK" + SEP; // 状态行response += SEP;                           // 空行response += "<html> <body> <h1> TEST </h1> <p> Hello HTTP! </p> </body> </html>"; // 有效载荷return response;
}

使用 telnet 获取我们的服务器响应

telnet 47.106.166.108:8888^]GET / HTTP/1.0

除了 telnet 外,还可以使用 Postman 等工具在 Windows 中获取服务器响应

3.2.重要属性

客户端/服务器在解析响应/请求时,必须要知道 有效载荷 的长度,避免多个响应/请求粘在一起而导致无法解析

3.2.1.Content-Length

HTTP 中通过 Content-Length: xxx 来表示 有效载荷 的长度为 xxx,但是在我们上面模拟实现的响应中,并没有添加 Content-Length 属性,浏览器又是如何知道 有效载荷 的长度呢?

这是因为 现代浏览器的功能都十分强大,即使你不指明 Content-Length 它也能通过 边读取边解释 等策略读取 有效载荷,但从 协议 角度来看,无论浏览器是否使用,我们都应该注明 Content-Length 属性

浏览器的编写难度堪比操作系统,是一款十分智能、强大的工业级软件

比如我们给百度服务器发送请求时,它所响应的内容中就包含了 Content-Length 属性

HTTP 服务器的响应中加上该属性

HttpHandler() 业务处理函数 — 位于 HttpServer.cc 服务器源文件

const static string SEP = "\r\n";string HttpHandler(const string &request)
{// 打印请求cout << request << endl;string body = "<html> <body> <h1> TEST </h1> <p> Hello HTTP! </p> </body> </html>";// 状态行string response = "HTTP/1.0 200 OK" + SEP;// 响应报头response += "Content-Length: " + to_string(body.size()) + SEP; // 有效载荷的长度// 空行response += SEP;// 有效载荷response += body;return response;
}

使用 Postman 发起请求,可以看到 Content-Length 属性值为 66,表示 有效载荷 长度为 66

注意: 当主动添加 Content-Length 属性后,部分浏览器可能不会主动解析有效载荷,转而直接输出有效载荷的内容

为了让浏览器更好的解析 有效载荷,还需要注明 有效载荷 的类型

3.2.2.Content-Type

Content-Type: xxx 表示当前响应的资源类型为 xxx(网页、文本、图片、音频、视频等),可以通过不同的后缀来表征不同的资源,比如 .avi 格式的视频,可以使用 Content-Type: video/avi 来注明

Content-Type 对照表


如果我们将类型指定为 .txt,浏览器再访问 HTTP 服务器时,就会直接显示 有效载荷,而非解释为网页

HttpHandler() 业务处理函数 — 位于 HttpServer.cc 服务器源文件

const static string SEP = "\r\n";string HttpHandler(const string &request)
{// 打印请求cout << request << endl;string body = "<html> <body> <h1> TEST </h1> <p> Hello HTTP! </p> </body> </html>";// 状态行string response = "HTTP/1.0 200 OK" + SEP;// 响应报头response += "Content-Length: " + to_string(body.size()) + SEP; // 有效载荷的长度response += "Content-Type: text/plain" + SEP; // 有效载荷的类型为 文本// 空行response += SEP;// 有效载荷response += body;return response;
}

浏览器访问 HTTP 服务器就会得到一个文本,也就是 有效载荷 中的内容


通过 Postman 也可以看到 Content-Type: text/plain 这个属性


正常情况下,响应的资源是说明类型,Conten-Type 就得注明其类型,确保浏览器能正确解析

3.3.请求分析

3.3.1.路径处理

正常情况下,在访问网页时,用户知道自己要访问的是哪个资源,浏览器会通过该资源在服务器中对应的 资源路径 发出请求,所以说 HTTP 服务器需要具备根据不同的 资源路径,给出不同的响应的能力,这也就意味着我们需要在服务器中创建一个资源目录 webRoot,其中存放各种资源

如果用户请求的资源不存在则返回 404 页面


此时我们就不能直接在 HttpServer.cc 中硬编码了(直接写出有效载荷),而是需要根据 资源路径,去 webRoot 目录中查找资源文件并读取,读取文件内容需要用到下面这个工具类

注意: 需要按照文件中的大小进行读取,避免因读取到 0 而提前停止(二进制文件中存在 0

Util.hpp 工具类

#pragma once#include "Log.hpp"#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>class Util
{
public:static bool ReadFile(const std::string& path, std::string* outStr){// 获取文件信息struct stat st;if(stat(path.c_str(), &st) < 0)return false;// 扩出足够大的空间int n = st.st_size;outStr->resize(n + 1);// 打开文件int fd = open(path.c_str(), O_RDONLY);if(fd < 0)return false;// 读取文件int size = read(fd, (char*)outStr->c_str(), n);close(fd);logMessage(Info, "Read file %s done", outStr->c_str());// 实际读取到的大小,应该与文件的大小一致return size == n;}
};

index.html 文件中设置一个默认页面

index.html 默认页面

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test</title>
</head><body><h1>Test</h1><p>Hello HTTP!Hello HTTP!Hello HTTP!Hello HTTP!Hello HTTP!Hello HTTP!</p>
</body></html>

HttpHandler() 函数在处理请求时,就可以通过 资源路径 读取资源了

HttpHandler() 函数 — 位于 HttpServer.cc 源文件

const static string SEP = "\r\n";
const static string defaultPath = "./webRoot/index.html";string HttpHandler(const string &request)
{// 打印请求cout << request << endl;string body;// 读取资源文件Util::ReadFile(defaultPath, &body);// 状态行string response = "HTTP/1.0 200 OK" + SEP;// 响应报头response += "Content-Length: " + to_string(body.size()) + SEP; // 有效载荷的长度response += "Content-Type: text/html" + SEP; // 有效载荷的类型为 文本// 空行response += SEP;// 有效载荷response += body;return response;
}

现在我们需要一个 结构体 来存储请求中的各种信息,比如 资源路径,同时需要借助 反序列化 进行解析

注意:

  • 如果用户直接请求 "/" 根目录,不能将目录中的所有资源都响应,而是需要响应一个默认显示页面
  • URL 中的资源路径,需要加上 web 根目录,才是一个完整的路径

Protocol.hpp 请求处理相关头文件

#pragma once#include "Util.hpp"#include <string>
#include <vector>
#include <sstream>
#include <unordered_map>const static string SEP = "\r\n";
const static string webRoot = "./webRoot";
const static string defaultHomePage = "/index.html"; // 默认页面class Request
{
public:Request(){}// 反序列化bool Deserialization(const std::string& url){// 根据 url 进行解析int n = 0;std::vector<std::string> vstr;while(true){std::string line;n = Util::ReadLine(url, n, SEP, &line) + SEP.size();if(line.empty())break;vstr.push_back(line);}// 解析请求行ParseFirstLine(vstr[0]);// 解析报头部分ParseHeaderLine(vstr);// 读取并解析有效载荷(可能没有)Util::ReadLine(url, n, SEP, &_body);return true;}// 解析请求行bool ParseFirstLine(const std::string& str){// 读取方法、资源路径、协议版本std::stringstream ss(str);ss >> _method >> _path >> _version;// 解析出后缀if(_path == "/")_path = defaultHomePage;// 实际路径 = web根目录 + 请求资源路径int pos = _path.find_last_of(".");if(pos == std::string::npos)_suffix = ".html";else_suffix = _path.substr(pos);_path = webRoot + _path;return true;}// 解析报头部分bool ParseHeaderLine(const std::vector<std::string>& vstr){for(int i = 1; i < vstr.size(); i++){const std::string& str = vstr[i];int pos = str.find(':');std::string key = str.substr(0, pos);std::string value = str.substr(pos + 2);_headers[key] = value;}return true;}~Request(){}public:std::string _method; // 请求方法std::string _path;   // 资源路径std::string _suffix; // 资源后缀std::string _version;// 协议版本std::unordered_map<std::string, std::string> _headers; // 请求报头std::string _body;   // 有效载荷
};

ReadLine() 读取行函数 — 位于 Util.hpp 工具类头文件的 Request 请求类中

static int ReadLine(const std::string& url, int i, const std::string& SEP, std::string* line)
{int pos = url.find(SEP, i);*line = url.substr(i, pos - i);return pos;
}

此时 HttpHandler() 函数中的处理方式就要发生改变了

HttpHandler() 业务处理函数 — 位于 HttpServer.cc 服务器源文件

string HttpHandler(const string &url)
{// 解析请求Request req;req.Deserialization(url);// 读取资源文件string body;Util::ReadFile(req._path, &body);cout << "path: " << req._path << endl;// 状态行string response = "HTTP/1.0 200 OK" + SEP;// 响应报头response += "Content-Length: " + to_string(body.size()) + SEP; // 有效载荷的长度response += "Content-Type: text/html" + SEP; // 有效载荷的类型为 文本// 空行response += SEP;// 有效载荷response += body;return response;
}

经过以上修改后,我们的 HTTP 服务器就支持根据不同的 资源路径,响应不同的资源了,现在在 webRoot 这个网页根目录中再添加两个测试文件

file1.html — 位于 webRoot 网页根目录中

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test file1</title>
</head>
<body><h1>FILE1</h1><p>This is file1This is file1This is file1This is file1This is file1</p>
</body>
</html>

file2.html — 位于 webRoot 网页根目录中

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test file1</title>
</head>
<body><h1>FILE1</h1><p>This is file1This is file1This is file1This is file1This is file1</p>
</body>
</html>

编译并启动服务器,通过浏览器发出不同的请求



现在已经具备一个服务器的雏形了,接下来就是处理请求不同资源的问题

3.3.2.类型处理

在进行响应时,需要知晓请求的资源类型,并在响应报头中通过 Content-Type 注明,关于资源路径中的文件后缀提取,已经在 Request 类中完成了,现在只需要根据 Content-Type 对照表进行转换,并赋值至 Response 类中即可

Content-Type 对照表


构建 Response 响应类,成员有:状态行(协议版本、状态码、状态码信息)、响应报头、有效载荷,函数有:根据请求加载响应对象、反序列化

Response 响应类 — 位于 Protocol.hpp 协议相关头文件

class Response
{
public:Response(){}// 根据请求对象,构建出响应对象bool LoadInfo(const Request& req){_version = req._version;// 读取资源std::string path = req._path;if(Util::ReadFile(path, &_body) == false){_st_code = "404";_st_msg = "No Found";path = webRoot + errPage_404;Util::ReadFile(path, &_body);}else{_st_code = "200";_st_msg = "OK";}std::cout << "path: " << path << std::endl;// 设置报头_headers["Content-Length: "] = std::to_string(_body.size());_headers["Content-Type: "] = Util::GetSuffix(req._suffix);return true;}// 序列化bool Serialization(std::string* outStr){outStr->clear();*outStr = _version + " " + _st_code + " " + _st_msg + SEP;for(auto &kv : _headers)*outStr += kv.first + kv.second + SEP;*outStr += SEP;*outStr += _body;return true;}~Response(){}public:std::string _version; // 协议版本std::string _st_code; // 状态码std::string _st_msg;  // 状态码信息std::unordered_map<std::string, std::string> _headers; // 响应报头std::string _body;    // 有效载荷
};

新增根据后缀获取资源类型的工具函数

GetSuffix() 获取资源类型 — 位于 Util.hpp 工具相关头文件

static std::string GetSuffix(const std::string& suffix)
{// 建表unordered_map<string, string> hash = {{".txt", "text/plain"},{".htm", "text/html"},{".html", "text/html"},{".jpg", "image/jpeg"},{".jpeg", "image/jpeg"},{".png", "image/png"},{".mp3", "audio/mp3"},{".avi", "video/avi"},{".mp4", "video/mpeg4"}};if(hash.count(suffix) == 0)return "text/html";return hash[suffix];
}

HttpHandler() 函数中不再需要主动处理请求,而是交给 Response 对象完成

HttpHandler() 业务处理函数 — 位于 HttpServer.cc 服务器源文件

string HttpHandler(const string &url)
{// 解析请求Request req;req.Deserialization(url);// 构建响应Response res;res.LoadInfo(req);// 响应string ret;res.Serialization(&ret);return ret;
}

因为用户可能请求不存在的资源,所以需要准备一个 404 网页

404 网页代码来源:HTML 和 JavaScript 编写简单的 404 界面

err404.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>

当前服务器支持请求不同的资源,所以我们可以在 webRoot 网页根目录下添加图片,并内嵌到其他资源文件中

注意: 如果一个网页中包含多份资源,每一份资源都需要发起一次 HTTP 请求

file1.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test file1</title>
</head>
<body><h1>这是一个牛肉饼</h1><p><img src="/image/1.jpg" alt="牛肉饼3元一个"></p>
</body>
</html>

现在可以请求不同的资源了

请求不存在的网页

请求 file1.html 文件


当然也可以单纯的请求图片(资源路径必须合法)

可以在网页中内嵌其他网页的 URL,配合 HTML 语法,实现网页跳转

file1.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test file1</title>
</head>
<body><h1>这是一个牛肉饼</h1><p><img src="/image/1.jpg" alt="牛肉饼3元一个"><br><a href="http://www.baidu.com">百度一下</a><a href="/index.html">回到首页</a></p>
</body>
</html>

分别点击 百度一下回到首页

3.3.3.请求方法

浏览器(客户端)与服务器间的交互行为可以分为这两类:

  1. 从服务器中获取资源
  2. 将资源上传至服务器

这两类行为分别对应着最常用的两个请求方法:GETPOSTGET 也能上传资源),除此之外,还存在其他请求方法,但最常用的就是 GETPOST

请求方法作用支持的HTTP版本
GET获取资源1.0、1.1
POST传输实体主体1.0、1.1
PUT传输文件1.0、1.1
HEAD获得报文首部1.0、1.1
DELETE删除文件1.0、1.1
OPTIONS询问支持的方法1.1
TRACE追踪路径1.1
CONNECT要求用隧道协议连接代理1.1
LINK建立和资源之间的联系1.0
UNLINK断开连接关系1.0

我们之前发出的请求使用的都是 GET 请求,如何让浏览器发出 POST 请求呢?

需要使用 HTML 中的 表单,语法如下

<form action="action_page.php" method="GET" target="_blank" accept-charset="UTF-8"
ectype="application/x-www-form-urlencoded" autocomplete="off" novalidate>
.
form elements.
</form> 

表单 中比较重要的两个属性

  • action 向何处发送表单
  • method 表单请求的方法

表单 中可以指定 method(使用 GET 或者 POST),在网页中看到的绝大多数输入框,都是通过 表单 实现的


在我们的 index.html 默认页面文件中实现一个 表单,并指定请求方法为 GET

注意: 此时的请求可能会导致服务器崩溃,因为我们没有做请求读取的处理工作,可能出现只读取了一半,从而导致读取错误

index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test</title>
</head><body><h1>主页</h1><form action="/a/b/c.php" method="get">请输入你的姓名: <input type="text" name="myname"><br>请输入你的密码: <input type="password" name="mypasswd"><br><input type="submit" value="提交"></form> 
</body></html>

访问网页,可以看到在提交 表单 后,URL 会发生变化


可以看出,如果使用 GET 方法提交 表单 的话,请求的资源以及文本框中的内容将会以 明文 的形式添加到 URL

为什么提交后会出现 404 页面?
因为请求的 /a/b/c.php 资源不存在,自动跳转到了 404 页面

服务器中获取的请求详情如下:

如果将 表单 中的请求方法改为 POST

index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>test</title>
</head><body><h1>主页</h1><form action="/a/b/c.php" method="post">请输入你的姓名: <input type="text" name="myname"><br>请输入你的密码: <input type="password" name="mypasswd"><br><input type="submit" value="提交"></form> 
</body></html>

表单提交前后 URL 的变化如下



可以看到只有请求的资源路径被添加到了 URL 中,那么文本框中的内容哪去了呢?

答案是 在有效载荷中

由此可以看出 GETPOST 这两种请求方法最大的区别:提参方式GET 会将表单中的内容直接添加到 URL 中;POST 则会将表单中的内容添加到有效载荷中

这两种方法在传输表单内容时,都是明文传输,但 POST 相对 GET 而言更 私密,并且容量也会更大

注意: GET 和 POST 方法都不安全,只是 POST 更私密(存储在有效载荷中,用户不容易获取)


GETPOST 的应用场景:

  • GET搜索框,比如百度的搜索框使用的就是 GET 方法,可以在 URL 中找到搜索的关键字
  • POST敏感字段,比如账号、密码等个人信息,可以提供一定的私密性
  • 针对敏感字段,除了 POST 外,还可以使用其他更安全的方法
  • 如果需要传输的内容过长,也可以使用 POST 方法,因为有效载荷的容量理论上非常大(URL 有长度限制)

接下来演示使用 Fiddler 等抓包工具,截获 POST 请求,并从中获取账号和密码

  • 大致原理:挟持浏览器,让浏览器先把请求发给它,然后它帮浏览器请求


所以就目前而言(使用 HTTP 协议),只要是没有经过加密的数据,发送到网络中都是不安全的!需要进行加密,随着信息安全的意识增强,会选择使用更加安全的 HTTPS 协议

3.3.4.状态码

状态码是服务器向浏览器(客户端)反映请求成功与否的一种方式,状态码可以分为这几类:

状态码类型解释
1xxInformational 信息性状态码接收到的请求正在处理
2xxSuccess 成功状态码请求正常处理完毕
3xxRedirection 重定向状态码需要进行附加操作以完成请求
4xxClient Error 客户端错误状态码服务器无法处理请求
5xxServer Error 服务器错误状态码服务器处理请求出错

其中最常见的就是 404 错误码,表示 请求的资源不存在,关于 HTTP 服务器的 404 页面编写已经在 「类型处理」 部分完成了,当我们访问不存在的网页时,会得到这样一个页面

服务器发出的响应正文如下


HTTP 中浏览器(客户端)的状态码 形态各异,可能出现状态码与实际状态不相符的情况,主要是原因 不同浏览器(客户端)对协议的支持程度不同


重定向状态码

当浏览器(客户端)访问的目标网站地址发生改变时,浏览器会返回 3xx 重定向错误码,用于引导浏览器访问正确的网址,常见的重定向状态码如下:

  • 永久重定向:301308
  • 临时重定向:302303307
  • 其他重定向:304

最具有代表性的重定向状态码为 301302

如何理解永久重定向和临时重定向?
永久重定向表示目标网址已经彻底改变,用户只有第一次访问需要跳转;而临时重定向表示目标网址暂时发生了改变,每次访问都需要跳转

注意: 无论是永久还是临时,站在服务器角度,都需要进行重定向,因为总会有新客户端连接,需要为其进行重定向引导

关于重定向状态的更多信息可以看看这篇文章 《彻底搞懂 HTTP 3XX 重定向状态码和浏览器重定向》

如何在代码中实现重定向?
设置错误码为 3xx,并在响应报头中加上 Location: URL

HTTP 服务器进行修改(临时重定向)

LoadInfo() 根据请求创建响应对象 — 位于 Protocol.hpp 中的 Response 响应类中

// 根据请求对象,构建出响应对象
bool LoadInfo(const Request& req)
{_version = req._version;// 读取资源std::string path = req._path;if(Util::ReadFile(path, &_body) == false){_st_code = "404";_st_msg = "No Found";path = webRoot + errPage_404;Util::ReadFile(path, &_body);}else{_st_code = "200";_st_msg = "OK";}// 重新设置状态行(临时重定向)_st_code = "301";_st_msg = "Moved Permanently";// 设置报头_headers["Content-Length: "] = std::to_string(_body.size());_headers["Content-Type: "] = Util::GetSuffix(req._suffix);_headers["Location: "] = "http://www.baidu.com"; return true;
}

编译并启动服务器,通过浏览器发出请求,请求发出后,直接跳转到了百度首页

通过 telnet 获取服务器响应如下

关于重定向的使用场景:

  • 永久重定向:网站更新,比如搜索引擎会定期访问网站,可以根据永久重定向进行更新
  • 临时重定向:跳转至指定页面,后续可能改变(登录页面、广告等)
3.3.5.Cookie 缓存

HTTP 协议本身是无状态的(不保存数据),主要的工作是完成 超文本传输,实际上用户在登录网站时,除了第一次需要手动登录外,后续一段时间内都不需要登录


这个现象称为 会话保持,可以大大提高提升用户使用体验,那么无状态的 HTTP 是如何实现 会话保持 的呢?

答案是使用 Cookie,用户在第一次登录时,服务器的响应中会包含 Set-Cookie: 账号&密码 这个报头,浏览器会保存 Cookie 相关的信息,后续再访问该网站时,在请求中自动添加 Cookie 报头,服务器完成验证后即可实现自动登录


用户后续一段时间内再访问该网站时,看似不需要登录,实际每次都在使用 Cookie 登录,不过这个工作是由浏览器自动完成的,用户几乎感知不到,可以查看浏览器中保存的 Cookie 信息

注意: Cookie 可以保存为内存级(只有本次使用浏览器期间有效,安全),也可以保存为文件级(关闭浏览器后仍然有效,方便)


前面说过,无论是 GET 还是 POST 方法,都是不安全的,如果 HTTP 中关于 Cookie 的设计真这么简单(直接在报头中携带 账号&密码),那么账号早被盗用了

木马病毒
这是一种植入性病毒,如果我们下载了携带病毒的软件,或者是访问了不安全的网站,就有可能导致 Cookie 泄漏,当其他人掌握 Cookie 时,就可以利用该 Cookie 直接登录网站,窃取关键信息

真正的 Cookie 使用了这样一个解决方案:

  • 根据 账号&密码 生成 session 对象,将 session 对象 id 作为 Set-Cookie 的值传给浏览器
  • 登录时,只需要判断 id 是否存在
  • session id 具有唯一性

使用了 seesion id 就能避免 Cookie 泄漏吗?

  • 不能,照样会发生泄漏,但至少此时泄漏的不是敏感信息

服务器可以制定安全策略,识别是否为异常登录

  • IP比对:识别登录用户的IP在短时间内是否发生了改变
  • 行为检测:识别用户是否存在异常信息,比如QQ突然大面积发生消息、添加好友

当服务器判定异常登录后,就会释放服务器中存储的 session id,这就意味着原本的 session id 失效了,需要重新输入密码登录

  • 如果是用户,重新使用 账号&密码 登录后,获取服务器重新生成的 session id 即可
  • 其他人则无法登录,因为没有 账号&密码

session id 对比直接存储 账号&密码 最大的优势在于 session id 更新成本低,且更加安全

如何生成唯一的 session id
可以通过哈希加密算法进行计算,比如 MD5SHA256

通过 HTTP 服务器验证 浏览器在请求时会自动加上 Cookie 报头

LoadInfo() 根据请求构建响应对象函数 — 位于 Protocol.hpp 中的 Response 响应类

// 根据请求对象,构建出响应对象
bool LoadInfo(const Request& req)
{_version = req._version;// 读取资源std::string path = req._path;if(Util::ReadFile(path, &_body) == false){_st_code = "404";_st_msg = "No Found";path = webRoot + errPage_404;Util::ReadFile(path, &_body);}else{_st_code = "200";_st_msg = "OK";}// // 重新设置状态行(临时重定向)// _st_code = "301";// _st_msg = "Moved Permanently";// 设置报头_headers["Content-Length: "] = std::to_string(_body.size());_headers["Content-Type: "] = Util::GetSuffix(req._suffix);_headers["Set-Cookie: "] = "sessionID=12345668"; // 服务器设置 session id// _headers["Location: "] = "http://www.baidu.com"; return true;
}

启动服务器,并使用浏览器进行访问,首先可以看到 浏览器已经存储了 Cookie 信息,也就是服务器响应的 session id


此时的请求是这样的

再次进行请求,请求就会变成这样,浏览器自动携带了 Cookie 报头,服务器就可以通过 session id 进行验证了

如何在服务器中实现 session

伪代码实现如下:

class Response
{// ...class Session{public:Session(const std::string& id, const std::string& passwd):_id(id), _passwd(passwd){}~Session(){}public:std::string _id;std::string _passwd;std::string _login_time; // 登录时间std::string _status;     // 用户状态};// 首次登录int Login(const Request& req){// 获取 账号&密码 等关键字段std::string id, passwd;GetValue(req, &id, &passwd);// 判断用户是否存在、密码是否正确if(Check(id, passwd) == true){// 根据关键字段构建 session 对象Session* ss = new Session(id, passwd);// 根据 session 对象生成 session idint ssID = SessionMD5(ss);// 存储映射关系_sessions[ssID] = ss;// 返回 ssIDreturn ssID;}return 0;}std::unordered_map<int, Session*> _sessions;// ...
};

当浏览器获取到 session id 后,会使用 session id 进行判断,可以将 _sessions 中的数据写入文件中,将 session idsession 对象的映射关系持久化存储

也可以使用 Redis 这种关系型数据库存储映射关系,更加高效

3.3.6.补充

常见的 Header 如下表所示

keyvalue
Content-Type数据的类型
Content-Length有效载荷的长度
Connection当前连接模式为长连接还是短连接
Host客户端告诉服务器,请求的资源位于哪个主机中的哪个端口上
User-Agent用户的操作系统和浏览器版本信息
Referer当前页面由哪个页面跳转而来
Location配合重定向状态码使用,引导浏览器跳转至目标网址
Cookie在客户端存储少量信息,用于实现会话

关于 Connection 属性,一个网页中包含多份资源,每一份资源的都需要发起一个单独的 HTTP 请求,为了避免请求时与服务器的连接断开(也为了提高效率),可以设置 keep-alive 表示 长连接 默认,确保所有的资源都能请求完成

关于 /favicon.ico 资源,这是一个小图标,显示在网页标题的左侧,添加之后可以提高网页的辨识度,这个资源由浏览器自动发起请求



星辰大海

相关文章推荐

网络基础『 序列化与反序列化』

网络编程『简易TCP网络程序』

网络编程『socket套接字 ‖ 简易UDP网络程序』

网络基础『发展 ‖ 协议 ‖ 传输 ‖ 地址』

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/764483.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenAI GPT商店面临质量与合规问题;黄仁勋预测:十年内AI将实时生成游戏画面

&#x1f989; AI新闻 &#x1f680; OpenAI GPT商店面临质量与合规问题 摘要&#xff1a;OpenAI旗下的GPT商店因存在大量涉嫌侵权内容、助长学术不诚实行为及违规内容等问题而引起关注。其中包括未经授权使用迪士尼、漫威角色生成内容的GPT模型&#xff0c;以及声称能绕过剽…

HCIP的学习(3)

网络类型及数据链路层协议 网络类型分类 P2P网络----点到点网络类型MA网络-----多点接入网络 BMA----广播型多点接入网络NBMA—非广播型多点接入网络&#xff08;快淘汰了&#xff09; 数据链路层协议 MA网络 以太网协议 特点&#xff1a;需要使用MAC地址对设备进行区分…

Java安全 反序列化(5) CC6链原理分析

Java安全 反序列化(5) CC6链原理分析 CC6学习的目的在于其可以无视jdk版本&#xff0c;这条链子更像CC1-LazyMap和URLDNS链子的缝合版 文章目录 Java安全 反序列化(5) CC6链原理分析前言一.CC6的原理和实现以及易错点我们如何实现调用LazyMap.get()方法一个易错点 二.完整CC6P…

【超全详解】Maven工程配置与常见问题解决指南

Maven工程 目录 Maven工程一、如何检查Maven工程是否配置正确&#xff1f;1、检查路径2、检查基本配置3、其他配置 二、Maven的基本操作基本操作install和package的区别 三、获取别人的Maven工程之后如何修改&#xff1f;四、如何正确写好配置文件&#xff1f;1.寻找配置资源2.…

Gitlab的流水线任务【实现每小时自动测试 dev分支的更新】

背景 在现代软件开发实践中&#xff0c;持续集成&#xff08;Continuous Integration, CI&#xff09;是确保代码质量和快速响应软件缺陷的关键策略。GitLab 提供了强大的 CI/CD 功能&#xff0c;允许开发者自动化测试和部署流程。本文将介绍如何设置 GitLab 流水线计划任务&a…

Java22重磅发布!!!!卷不动了,真的卷不动了。。。。

就在3月19日&#xff0c;Java22重磅发布。Java22新增了12项增强功能&#xff0c;其中包括七个预览特性和一个孵化器特性&#xff0c;这些功能都显著到足以引起JDK增强提案&#xff08;JEPs&#xff09;的关注。它们涵盖了Java语言、其API、性能以及JDK中包含的工具的改进。 真…

【网络基础】VRRP虚拟路由冗余协议介绍与配置

目录 一、VRRP的概述 1.1 VRRP的由来 1.2 作用 1.3 基本结构 1.4 状态机流程 1.5 设备类型 二、 实例演示 一、VRRP的概述 1.1 VRRP的由来 局域网中的用户终端通常采用配置一个默认网关的形式访问外部网络&#xff0c;如果此时默认网关设备发生故障&#xff0c;将中断…

设计模式 --4:工厂方法模式

总结 &#xff1a; 个人理解&#xff1a; 工厂方法模式就是在简单工程模式的基础下将工厂类抽象出来。如果不抽象工厂类 &#xff0c;每一次创建一个新的算法&#xff0c;都要修改原来的工厂类&#xff0c;这不符合 开放–封闭原则 将工厂类给抽象出来&#xff0c;让具体的算法…

使用zabbix自动发现规则监控oracle分区表的分区键的合法

一、 创建从数据库查询原始数据脚本 编写脚本query_table.sh vim query_table.sh #!/bin/bash temp_outfile"/tmp/table.source" outfile"/tmp/table.txt" source /home/oracle/.bash_profilesqlplus -s zabbix/zabbix>${temp_outfile}<<EOF c…

贪心算法入门

简介 贪心算法&#xff08;Greedy Algorithm&#xff09;是一种在每一步选择中都采取在当前状态下最好或最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最好或最优的算法。也就是首先选取局部最优&#xff0c;从局部最优推出全局最优。 举例…

【python开发】安装配置启动+数据库管理+表管理+数据行管理+python操作Mysql及相关安全的问题

Mysql入门 一、安装&启动1、安装2、测试3、设置和修改root密码 二、数据库管理1、内置客户端操作&#xff08;1&#xff09;查看当前所有的数据库(show databases);&#xff08;2&#xff09;创建数据库&#xff1a;create database learn&#xff08;数据库名&#xff09;…

北京中科富海低温科技有限公司确认出席2024第三届中国氢能国际峰会

会议背景 随着全球对清洁能源的迫切需求&#xff0c;氢能能源转型、工业应用、交通运输等方面具有广阔前景&#xff0c;氢能也成为应对气候变化的重要解决方案。根据德勤的报告显示&#xff0c;到2050年&#xff0c;绿色氢能将有1.4万亿美元市场。氢能产业的各环节的关键技术突…

四川宏博蓬达法律咨询有限公司:法律服务安全的新标杆

在这个法治社会&#xff0c;法律服务行业扮演着越来越重要的角色。四川宏博蓬达法律咨询有限公司&#xff0c;作为行业内的佼佼者&#xff0c;始终坚持以客户为中心&#xff0c;为客户提供专业、高效、安全的法律服务。 一、公司背景与实力展示 四川宏博蓬达法律咨询有限公司自…

C语言例3-26:逗号表达式的例子

逗号表达式&#xff1a; 表达式1&#xff0c;表达式2 表达式可以是算术表达式、关系表达式、逻辑表达式、条件表达式、赋值表达式和逗号表达式。 代码如下&#xff1a; #include<stdio.h> int main(void) {int i1,j;float f2.0f;char chb; //b(98)// printf(&…

罗格朗逸景PLUS IOT智能系统发布,为您提供更智能的生活体验!

罗格朗全新推出的逸景PLUS IOT智能系统现已正式上市,采用纤薄纯平的设计,功能丰富全面,支持灯光/温度/场景控制、背景音乐等多种功能,整合罗格朗IOT2.0系统,集成可视对讲,为用户打造更舒适、安全的智能生活。 罗格朗智能家居 罗格朗是全球电气与智能建筑系统专家,创立于1865年…

【3D reconstruction 学习笔记 第二部】

三维重建 3D reconstruction 4. 三维重建与极几何三角化&#xff08;线性解法&#xff09;三角化&#xff08;非线性解法&#xff09;多视图几何极几何极几何约束基础矩阵估计 5. 双目立体视觉重建6. 多视图重建7. SFM 系统设计8. SLAM系统设计 4. 三维重建与极几何 三角化&…

Orbit 使用指南 08 | 登记注册环境 | Isaac Sim | Omniverse

如是我闻&#xff1a; 在上一个指南中&#xff0c;我们学习了如何创建一个自定义的车杆环境。我们通过导入环境类及其配置类来手动创建了一个环境实例 # create environment configurationenv_cfg CartpoleEnvCfg()env_cfg.scene.num_envs args_cli.num_envs# setup RL envir…

在 Linux/Ubuntu/Debian 上安装 SQL Server 2019

Microsoft 为 Linux 发行版&#xff08;包括 Ubuntu&#xff09;提供 SQL Server。 以下是有关如何执行此操作的基本指南&#xff1a; 注册 Microsoft Ubuntu 存储库并添加公共存储库 GPG 密钥&#xff1a; sudo wget -qO- https://packages.microsoft.com/keys/microsoft.as…

jeect-boot queryFieldBySql接口RCE漏洞(CVE-2023-4450)复现

jeect-boot积木报表由于未授权的 API /jmreport/queryFieldBySql 使用了 freemarker 解析 SQL 语句从而导致了 RCE 漏洞的产生。 1.漏洞级别 高危 2.漏洞搜索 fofa app"Jeecg-Boot 企业级快速开发平台"3.影响范围 JimuReport < 1.6.14.漏洞复现 这个漏洞的…

C/C++代码性能优化——编程实践

1. 编程实践 在一些关键的地方&#xff0c;相应的编程技巧能够给性能带来重大提升。 1.1. 参数传递 传递非基本类型时&#xff0c;使用引用或指针&#xff0c;这样可以避免传递过程中发生拷贝。参数根据是否需要返回&#xff0c;相应加上const修饰&#xff0c;代码更安全&am…