🔭个人主页: 北 海
🛜所属专栏: 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
服务器的 IP
和 Port
,以及资源路径,基于 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()
方法中的返回值进行修改即可
- 添加状态行
- 添加响应报头(可省略)
- 添加空行
- 添加有效载荷
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.请求方法
浏览器(客户端)与服务器间的交互行为可以分为这两类:
- 从服务器中获取资源
- 将资源上传至服务器
这两类行为分别对应着最常用的两个请求方法:GET
、POST
(GET
也能上传资源),除此之外,还存在其他请求方法,但最常用的就是 GET
和 POST
请求方法 | 作用 | 支持的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
中,那么文本框中的内容哪去了呢?
答案是 在有效载荷中
由此可以看出 GET
和 POST
这两种请求方法最大的区别:提参方式,GET
会将表单中的内容直接添加到 URL
中;POST
则会将表单中的内容添加到有效载荷中
这两种方法在传输表单内容时,都是明文传输,但 POST
相对 GET
而言更 私密,并且容量也会更大
注意: GET 和 POST 方法都不安全,只是 POST 更私密(存储在有效载荷中,用户不容易获取)
GET
和 POST
的应用场景:
GET
:搜索框,比如百度的搜索框使用的就是GET
方法,可以在URL
中找到搜索的关键字POST
:敏感字段,比如账号、密码等个人信息,可以提供一定的私密性- 针对敏感字段,除了
POST
外,还可以使用其他更安全的方法 - 如果需要传输的内容过长,也可以使用
POST
方法,因为有效载荷的容量理论上非常大(URL
有长度限制)
接下来演示使用 Fiddler
等抓包工具,截获 POST
请求,并从中获取账号和密码
- 大致原理:挟持浏览器,让浏览器先把请求发给它,然后它帮浏览器请求
所以就目前而言(使用 HTTP
协议),只要是没有经过加密的数据,发送到网络中都是不安全的!需要进行加密,随着信息安全的意识增强,会选择使用更加安全的 HTTPS
协议
3.3.4.状态码
状态码是服务器向浏览器(客户端)反映请求成功与否的一种方式,状态码可以分为这几类:
状态码 | 类型 | 解释 |
---|---|---|
1xx | Informational 信息性状态码 | 接收到的请求正在处理 |
2xx | Success 成功状态码 | 请求正常处理完毕 |
3xx | Redirection 重定向状态码 | 需要进行附加操作以完成请求 |
4xx | Client Error 客户端错误状态码 | 服务器无法处理请求 |
5xx | Server Error 服务器错误状态码 | 服务器处理请求出错 |
其中最常见的就是 404
错误码,表示 请求的资源不存在,关于 HTTP
服务器的 404
页面编写已经在 「类型处理」 部分完成了,当我们访问不存在的网页时,会得到这样一个页面
服务器发出的响应正文如下
HTTP
中浏览器(客户端)的状态码 形态各异,可能出现状态码与实际状态不相符的情况,主要是原因 不同浏览器(客户端)对协议的支持程度不同
重定向状态码
当浏览器(客户端)访问的目标网站地址发生改变时,浏览器会返回 3xx
重定向错误码,用于引导浏览器访问正确的网址,常见的重定向状态码如下:
- 永久重定向:
301
、308
- 临时重定向:
302
、303
、307
- 其他重定向:
304
最具有代表性的重定向状态码为 301
和 302
如何理解永久重定向和临时重定向?
永久重定向表示目标网址已经彻底改变,用户只有第一次访问需要跳转;而临时重定向表示目标网址暂时发生了改变,每次访问都需要跳转
注意: 无论是永久还是临时,站在服务器角度,都需要进行重定向,因为总会有新客户端连接,需要为其进行重定向引导
关于重定向状态的更多信息可以看看这篇文章 《彻底搞懂 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
可以通过哈希加密算法进行计算,比如MD5
、SHA256
通过 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 id
与 session
对象的映射关系持久化存储
也可以使用 Redis
这种关系型数据库存储映射关系,更加高效
3.3.6.补充
常见的 Header
如下表所示
key | value |
---|---|
Content-Type | 数据的类型 |
Content-Length | 有效载荷的长度 |
Connection | 当前连接模式为长连接还是短连接 |
Host | 客户端告诉服务器,请求的资源位于哪个主机中的哪个端口上 |
User-Agent | 用户的操作系统和浏览器版本信息 |
Referer | 当前页面由哪个页面跳转而来 |
Location | 配合重定向状态码使用,引导浏览器跳转至目标网址 |
Cookie | 在客户端存储少量信息,用于实现会话 |
关于 Connection
属性,一个网页中包含多份资源,每一份资源的都需要发起一个单独的 HTTP
请求,为了避免请求时与服务器的连接断开(也为了提高效率),可以设置 keep-alive
表示 长连接 默认,确保所有的资源都能请求完成
关于 /favicon.ico
资源,这是一个小图标,显示在网页标题的左侧,添加之后可以提高网页的辨识度,这个资源由浏览器自动发起请求
相关文章推荐
网络基础『 序列化与反序列化』
网络编程『简易TCP网络程序』
网络编程『socket套接字 ‖ 简易UDP网络程序』
网络基础『发展 ‖ 协议 ‖ 传输 ‖ 地址』