程序员平时写的代码都是在应用层,为了解决日常所需,但是不同的网络进程之间还需要有不同的通信协议,每个应用层协议都是为了解决某一类问题,而问题的解决又必须通过位于不同主机中的多个应用进程之间的通信和协同工作来完成。应用进程之间的这种通信必须遵守严格的规则,应用进程的具体内容就是精确定义这些通信规则。
应用层的协议应当定义:
协议是一种 “约定”。socket API
的接口, 在读写数据时,都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
什么是序列化和反序列化?
从一个结构化的数据转化为一个长字符串的过程,我们称之为“序列化”的过程!
长字符串到达对方机器后,会被分析算法将字符串里面的字段信息解析出来,然后再填充信息,“还原”结构化的信息,最终完成数据的传输。其中我们将长字符串再转化为“结构化”数据的过程叫做“反序列化”!
为什么要进行序列化和反序列化?
1️⃣:为了应用层网络通信的方便,结构化的数据是不便于网络传输的。
2️⃣:为了方便上层进行使用内部成员,将应用和网络进行了解耦!(上层用户根本就不关心,数据在网络中的转化过程)
我们之前的UDP、TCP套接字通信,有没有进行序列化和反序列化?
——没有,我们根本就没有实际的应用场景,就没有结构化的数据来供我们传输。
结构化的数据:本质就是协议的表现!
怎么序列化和反序列化?
造轮子:就是我们自己写一个。
用轮子:就是我们使用别人写好的组件(xml,json,protobuff)。
所以接下来我们来一个实际的应用场景:网络版本的计算器。
约定方案一:
- 客户端发送一个形如"1+1"的字符串;
- 这个字符串中有两个操作数, 都是整形;
- 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
- 数字和运算符之间没有空格;
模拟一下序列化和反序列化的过程:
序列化和反序列化都由我们自己做的话,太过于繁琐和麻烦了。
约定方案二:
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
- 这个过程叫做 “序列化” 和 “反序列化”
定制协议,这是最主要的部分
定制协议的过程,就是定制结构化数据的过程
业务逻辑:做一个短服务,并不是死循环的长服务
客户端发送一个request ->分析处理->服务器构建response->send(response)发回去->close(sock)
没有明显的序列化和反序列化
Protocol.hpp
#pragma once
#include
#include
using namespace std;
// 我们自己定制的协议,客户端和服务器都要遵守!!!
// 这就叫做自定义协议!!!
// 请求格式
typedef struct request
{
int x;
int y;
char op; // +-*/%
} request_t;
// 响应格式
typedef struct response
{
int code; // server运算完毕的状态code(0:success -1:div 0)
int result; // 计算结果 要能区分出正常的计算结果,还是异常的退出结果
} response_t;
Sock.hpp
// 对套接字接口进行封装,方便使用
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "create socket failed!" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock, 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)) < 0)
{
cerr << "bind failed!" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if (listen(sock, 5) < 0)
{
cerr << "listen failed!" << endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr *)&peer, &len);
if (fd >= 0)
{
return fd;
}
return -1;
}
static void Connect(int sock, std::string ip, 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());
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == 0)
{
cout << "connect sucess!" << endl;
}
else
{
cout << "connect failed!" << endl;
exit(-5);
}
}
};
CalServer.cc
#include "Protocol.hpp"
#include "Sock.hpp"
#include
// 我们想这样运行:./CalServer port
static void Usage(string proc)
{
cout << "./" << proc << " port" << endl;
exit(1);
}
void *HandlerRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
//接下来就是业务逻辑处理
// 1.读取求情,从客户端读
request_t req;
ssize_t s = read(sock, &req, sizeof(req));
// 2. 分析请求 && 3. 计算结果
// 4. 构建响应,并进行返回
if (s == sizeof(req))
{
response_t resp = {0, 0};
switch (req.op)
{
case '+':
resp.result = req.x + req.y;
break;
case '-':
resp.result = req.x - req.y;
break;
case '*':
resp.result = req.x * req.y;
break;
case '/':
if (req.y == 0)
resp.code = -1;
else
resp.result = req.x / req.y;
break;
case '%':
if (req.y == 0)
resp.code = -2;
else
resp.result = req.x % req.y;
break;
default:
resp.code = -3;
break;
}
cout << "request: " << req.x << req.op << req.y << endl;
// 数据处理完毕,写进socket中,写回对端
write(sock, &resp, sizeof(resp));
cout<<"服务结束!"<<endl;
}
// 5. 关闭链接
close(sock);
}
int main(int argc, char *argv[])
{
if (argc != 2)
Usage(argv[0]);
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for (;;)
{
int sock = Sock::Accept(listen_sock);
if (sock >= 0)
{
cout << "get a new client..." << endl;
int *pram = new int(sock);
pthread_t tid;
pthread_create(&tid, nullptr, HandlerRequest, pram);
}
}
return 0;
}
CalClient.cc
#include "Protocol.hpp"
#include "Sock.hpp"
// 我们想要这样运行:./CalClient server_ip server_port
void Usage(string proc)
{
cout << "Usage:" << proc << " server_ip server_port" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[1]);
exit(-1);
}
int sock = Sock::Socket();
Sock::Connect(sock, argv[1], atoi(argv[2]));
// 业务逻辑
request_t req;
memset(&req, 0, sizeof(req));
cout << "please enter data one# ";
cin >> req.x;
cout << "please enter data two# ";
cin >> req.y;
cout << "please enter operator# ";
cin >> req.op;
// 将数据写到对端
ssize_t s = write(sock, &req, sizeof(req));
// 读回处理结果
response_t resp;
s = read(sock, &resp, sizeof(resp));
if (s == sizeof(resp))
{
cout << "code[0:success]: " << resp.code << endl;
cout << "result: " << resp.result << endl;
}
return 0;
}
上一个版本,我们定义原生结构体,然后利用
write/read
接口以二进制方式读写,显得太过于麻烦了,这样的方式并不是最佳的。所以我们要引入系列化和反序列化,json
的使用。
安装json:sudo yum install -y jsoncpp-devel
testjson.cc
#include
#include
#include
// 结构化的数据
typedef struct request
{
int x;
int y;
char op; // +-*/%
} request_t;
// 注意编译时要链接json库 -ljsoncpp
int main()
{
// // 序列化
// request_t req={10,20,'*'};
// Json::Value root;//这是一个万精油的对象,可以承装任何对象,json是一个KV式的序列化方案!
// root["datax"]=req.x;
// root["datay"]=req.y;
// root["operator"]=req.op;
// // 一共有两种Writer
// // FasterWriter StyledWriter
// //Json::StyledWriter writer;
// Json::FastWriter writer;
// std::string json_string =writer.write(root);
// std::cout<
// 反序列化的过程
std::string json_string = R"({"datax":10,"datay":20,"operator":42})";
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
request_t req;
req.x = root["datax"].asInt();
req.y = root["datay"].asInt();
req.op = (char)root["operator"].asUInt();
std::cout << req.x << req.y << req.op << std::endl;
return 0;
}
Protocol.hpp
#pragma once
#include
#include
#include
using namespace std;
// 我们自己定制的协议,客户端和服务器都要遵守!!!
// 这就叫做自定义协议!!!
// 请求格式
typedef struct request
{
int x;
int y;
char op; // +-*/%
} request_t;
// 响应格式
typedef struct response
{
int code; // server运算完毕的状态code(0:success -1:div 0)
int result; // 计算结果 要能区分出正常的计算结果,还是异常的退出结果
} response_t;
//
//序列化和反序列化函数
//
//
// request_t -> string
std::string SerializeRequest(const request_t &req)
{
Json::Value root;
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
Json::FastWriter writer;
std::string json_string = writer.write(root);
return json_string;
}
// string -> request_t
void DeserializeRequest(const std::string &json_string, request_t &out)
{
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.x = root["datax"].asInt();
out.y = root["datay"].asInt();
out.op = (char)root["operator"].asInt();
}
std::string SerializeResponse(const response_t & resp)
{
Json::Value root;
root["code"]=resp.code;
root["result"]=resp.result;
Json::FastWriter writer;
std::string res=writer.write(root);
return res;
}
void DeserializeResponse(const std::string &json_string,response_t &out)
{
Json::Reader reader;
Json::Value root;
reader.parse(json_string,root);
out.code=root["code"].asInt();
out.result=root["result"].asInt();
}
CalServer.cc
#include "Protocol.hpp"
#include "Sock.hpp"
#include
// 我们想这样运行:./CalServer port
static void Usage(string proc)
{
cout << "./" << proc << " port" << endl;
exit(1);
}
void *HandlerRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
//接下来就是业务逻辑处理
char buffer[1024];
request_t req;
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
cout << "get a new request..." << buffer << endl;
std::string str = buffer;
DeserializeRequest(str, req);
response_t resp = {0, 0};
switch (req.op)
{
case '+':
resp.result = req.x + req.y;
break;
case '-':
resp.result = req.x - req.y;
break;
case '*':
resp.result = req.x * req.y;
break;
case '/':
if (req.y == 0)
resp.code = -1;
else
resp.result = req.x / req.y;
break;
case '%':
if (req.y == 0)
resp.code = -2;
else
resp.result = req.x % req.y;
break;
default:
resp.code = -3;
break;
}
cout << "request: " << req.x << req.op << req.y << endl;
std::string send_string = SerializeResponse(resp);
write(sock, send_string.c_str(), send_string.size());
cout << "服务结束: " << send_string << endl;
}
// 5. 关闭链接
close(sock);
}
int main(int argc, char *argv[])
{
if (argc != 2)
Usage(argv[0]);
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for (;;)
{
int sock = Sock::Accept(listen_sock);
if (sock >= 0)
{
cout << "get a new client..." << endl;
int *pram = new int(sock);
pthread_t tid;
pthread_create(&tid, nullptr, HandlerRequest, pram);
}
}
return 0;
}
CalClient.cc
#include "Protocol.hpp"
#include "Sock.hpp"
// 我们想要这样运行:./CalClient server_ip server_port
void Usage(string proc)
{
cout << "Usage:" << proc << " server_ip server_port" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[1]);
exit(-1);
}
int sock = Sock::Socket();
Sock::Connect(sock, argv[1], atoi(argv[2]));
// 业务逻辑
request_t req;
memset(&req, 0, sizeof(req));
cout << "please enter data one# ";
cin >> req.x;
cout << "please enter data two# ";
cin >> req.y;
cout << "please enter operator# ";
cin >> req.op;
// 将数据写到对端
std::string json_string = SerializeRequest(req);
ssize_t s = write(sock, json_string.c_str(), json_string.size());
// 读回处理结果
char buffer[1024];
s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
response_t resp;
buffer[s] = 0;
std::string str = buffer;
DeserializeResponse(str, resp);
cout << "code[0:success]: " << resp.code << endl;
cout << "result: " << resp.result << endl;
}
return 0;
}
结果展示:
总结
我们写的CS模式的网络版本的计算器,本质就是一个应用层网络服务:
1️⃣其中的基本通信代码:是我们自己实现的。
2️⃣序列和反序列化是:利用json组件完成的。
3️⃣业务逻辑是我们自己设定的。
4️⃣请求、结果的格式、code的含义等约定是我们自己定义的。
统一资源定位符(Uniform Resource Locator)
我们请求的图片、html、css、js、视频、音频、标签、文档等这些都称之为"资源"!服务器后台,是用Linux做的。IP + Port唯一的确定一个进程。
但是我们无法唯一的确认一个资源!公网IP地址是唯一确认一台主机的,而我们所谓的网络"资源"都一定是存在于网络中的一台Linux机器上!Linux或者传统的操作系统,保存资源的方式,都是以文件的方式保存的。单Linux系统,标识一个唯一资源的方式,通过路径!
所以:IP+Linux路径,就可以唯一的确认一个网络资源!(这是URL存在的意义)
ip通常是以域名的方式呈现的。路径可以通过目录名+分隔符/确认。
像 / ? :
等这样的字符,已经被url当做特殊意义理解了,因此这些字符不能随意出现.。比如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%
,编码成%XY
格式。这就叫做urlencode
,其次urldecode
是urlencode
的逆过程。
无论是请求还是响应,基本上http都是按照行\n
为单位进行构建请求或者构建响应的。无论是请求还是响应,几乎都是由3或者4部分组成。
如何理解普通用户的上网行为?(此处简化处理)
1️⃣从目标服务器拿到数据
2️⃣向目标服务器上传数据
本质:这个过程对于计算机来说就是IO,人的所有上网行为不就是进程间通信吗?
思考一:http如何封装、解包?
空行是一个特殊字符,用空行可以做到,将长字符串一分为2。
思考二:http如何分用?
不是http解决的,是具体的应用代码解决的,http需要有接口来帮助上层获取参数。
思考三:http请求或者响应是如何被读取的?那么它又是如何被发送的?
换言之:http request 和 http response是如何被看待的?
可以将请求和响应整体看做是一个大的字符串!!!
接下来就是,我想要查看http报文的格式,如何查看?它是什么样的呢?
recv函数与read函数几乎是一样的!只是recv有最后一个参数flags,默认设为0,即可。
send函数与write函数几乎也是一样的,只是send有最后一个参数flags,默认设为0,即可。
这两个函数是Linux特有的,针对TCP协议开发出来的函数。
Http.cc
// 服务器
#include "Sock.hpp"
#include
// 我们想要这样运行:./Http 8080
void Usage(string proc)
{
cout << "Usage: " << proc << "port" << endl;
exit(1);
}
void *HanderHttpRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
#define SIZE 1024 * 10
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = 0;
cout << buffer; //仅仅查看http的请求格式! for test
}
close(sock);
return nullptr;
}
int main(int argc, char *argv[])
{
if (argc != 2)
Usage(argv[0]);
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for (;;)
{
int sock = Sock::Accept(listen_sock);
if (sock > 0)
{
pthread_t tid;
int *pram = new int(sock);
pthread_create(&tid, nullptr, HanderHttpRequest, pram);
}
}
return 0;
}
我们现在想要回显一点内容,返回给客户端,我们只是单纯的返回一个字符串就可以了吗?
答案:不是的,我们还需要手动构建响应报文的状态行,响应报头才行,因为我们使用的是HTTP协议,必须要遵守大佬制定的规则。
Http.cc
#include "Sock.hpp"
#include
// 我们想要这样运行:./Http 8080
void Usage(string proc)
{
cout << "Usage: " << proc << "port" << endl;
exit(1);
}
void *HanderHttpRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
#define SIZE 1024 * 10
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = 0;
cout << buffer; //仅仅查看http的请求格式! for test
string http_response = "http/1.0 200 OK\n";
http_response += "Content-Type: text/plain\n"; // text/plain,正文是普通的文本
http_response += "\n";//以空行来作为报头与有效载荷的分界线
// 接下来构建一个响应 有效载荷 再发送回去,显示在浏览器上面
http_response += "hello world!"; // 不仅仅是正文,还需要构建响应报头和状态行,因为要满足http协议
send(sock, http_response.c_str(), http_response.size(), 0);
}
close(sock);
return nullptr;
}
int main(int argc, char *argv[])
{
if (argc != 2)
Usage(argv[0]);
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
for (;;)
{
int sock = Sock::Accept(listen_sock);
if (sock > 0)
{
pthread_t tid;
int *pram = new int(sock);
pthread_create(&tid, nullptr, HanderHttpRequest, pram);
}
}
return 0;
}
其中常见的媒体格式类型如下:
总结:
HTTP协议,如果我们要自己写的话,本质是:我们要根据协议的内容,来进行文本分析!
服务器的这种recv的读法是不正确的:
#define SIZE 1024 * 10
char buffer[SIZE];
memset(buffer, 0, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
客户端可能会以某种方式,向服务器发送多个请求,我们的缓冲区的大小是固定的,有没有情况下,可能回多读一些报文,甚至少读了一些报文,这都会造成问题。
所以我们要保证两点:
1️⃣我们保证每次都是读取的完整的http_request
2️⃣每次都不要把下一个http请求给读到(涉及到TCP,后面细讲)
如何判定将我们的报头部分读完了?
读到空行——“\n\n”,之后我们就能正确提取报头中的各种属性包括: Content-Length!
如何决定后面还有没有正文?
这和请求方法有关
如果有正文,如何保证把正文全部读取完成呢?而且不要把下一个http的部分数据读到呢?
如果有正文,报头部分有一个属性:Content-Length: len
,表明正文部分有多少个字节!决定读取多少个len字节的正文!
总结:
1️⃣Content-Length帮助我们读取到完整的http请求或者响应(不存在Content-Length的情况,就是你没有正文的时候)。
2️⃣同时,根据空行能够做到将报头和有效载荷进行分离(解包)。
所以我们可以引出connection:keep-alive/close
1.0
:较老的版本,是短链接的:一个请求,对应一响应,然后关闭套接字,其中的一次请求,一般就是请求一个资源,完毕后关闭链接。
1.1
:支持长链接,循环请求与响应,1.1
是现在常用的版本。
一个大网页中一般有很多个资源,每个资源都要发起http请求。当要访问这个大网页时,http/1.0就需要多次进行http请求,由于http协议是基于tcp协议的,所以tcp要通信,我们就要建立连接->传送数据->关闭链接,每一个请求都要进行一次上述的过程,这样请求一个资源使用http/1.0就显得比较耗时了。所以后来有了http/1.1,它是支持长链接的,它主要是为了解决1.0中的耗时问题,通过减少频繁建立tcp链接的方式,来提高效率!
方法 | 说明 | 支持的http版本 |
---|---|---|
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体资源 | 1.0、1.1 |
HEAD | 获得报文首部 | 1.0、1.1 |
我们关注一下 /
我们可以增加一些路径观察一下:
其中的/
有点像我们Linux下的根目录,其实不是的,这个叫做web根目录。
/
:我们一般要请求的一定是一个具体的资源,但如果请求是/
,意味着我们要请求该网站的首页!(内部会进行判断:if(method=='/'),path=/wwwroot;
,注意并不是根目录下的所有资源)首页一般叫做index.html 或者 index.htm。一般的网站,都要有默认首页。
我们接下来可以做测试:
创建一个wwwroot的web根目录,其下存放文件就是一个一个的资源。
stat
函数
功能:根据文件路径,获得文件的指定的属性
int stat(const char *path, struct stat *buf);
index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<h5>hello 首页!h5>
<h5>hello 表单!h5>
<form action="/a/b/hande_form" method="GET">
姓名:<input type="text" name="name"><br />
密码:<input type="password" name="passwd"><br />
<input type="submit">
form>
body>
html>
运行结果:
GET 方法如果提交参数,是通过URL的方式提交的,问号?
作为分割符,提取出用户名和密码,这样前端的数据,就可以被后端服务器拿到了,进而进行数据处理。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<h5>hello 首页!h5>
<h5>hello 表单!h5>
<form action="/a/b/hande_form" method="POST"> //POST方法
姓名:<input type="text" name="name"><br />
密码:<input type="password" name="passwd"><br />
<input type="submit">
form>
body>
html>
运行结果:
POST 方法是通过正文提交参数的,这里的参数就是你输入框中输入的信息。
1️⃣(概念问题)
①GET
方法叫做——获取,是最常用的方法,默认一般获取所有的网页,都是GET
方法,但是如果GET
要提交参数(它能提交参数的,不能只看字面意思获取),通过URL
来进行参数拼接从而提交给Server端。
②POST
方法叫做——推送,是提交参数比较常用的方法,但是如果提交参数,一般是通过正文部分提交的,但是你不要忘记,有Content -Length:X
表示参数的长度。
2️⃣(区别)
①参数提交的位置不同POST
方法比较私密(私密!=安全),不会回显到浏览器的URL输入框!GET
方法不私密,会将重要信息回显到URL的输入框中,增加了被盗取的风险。
②GET
是通过URL传参的,而URL
是有大小限制的!这和具体的浏览器有关!POST
方法,是由正文部分传参的,一般大小没有限制!
3️⃣(如何选择)
①GET
:如果提交的参数,不敏感,数量非常少,可以采用GET
。
②POST
:否则,就使用POST
方法。
4️⃣http协议处理,本质是文本分析,所谓的文本分析:
①http协议本身的字段(请求行,请求报头)
②提取参数,如果有的话GET
或者POST
其实是前后端交互的一个重要方式。
注意:这里的参数就是你输入框中输入的信息。
状态码 | 类别 | 原因短语 |
---|---|---|
1xx | 表示成功 | 如请求收到了或正在进行处理 |
2xx | 表示成功 | 如接收或者知道了 |
3xx | 表示重定向 | 如要完成请求还必须采取进一步行动 |
4xx | 表示客户端的差错 | 如请求中有错误的语法或不能完成 |
5xx | 表示服务器的差错 | 如服务器失效无法完成请求 |
3xx重定向
1️⃣永久重定向 301
2️⃣临时重定向 302 、307
重定向概念:
1️⃣(永久重定向)当访问某一个网站的时候,会让我们跳转到另一个网址。
2️⃣(临时重定向)等我访问某种资源的时候,提示我登录,跳转到了登录页面,输入完毕密码,登录的时候,会自动跳转回来(登录,美团下单)。
浏览器会做特殊处理,把老旧的一些缓存更新为新网站的信息,例如书签。永久性重定向通常用来网站搬迁、域名替换。
重定向是需要浏览器给我们提供支持的,浏览器必须识别301,302,307
服务器要告诉浏览器,我应该再去哪里?
所以有了这个字段Location
:新的地址!
测试:
Header | 说明 |
---|---|
Content-Type | 数据类型(text/html等) |
Content-Length | Body的长度 |
Host | 客户端告知服务器,所请求的资源是在哪个主机的哪个端口号上 |
User-Agent | 声明用户的操作系统和浏览器版本信息 |
referer | 当前页面是从哪个页面跳转过来的 |
location | 搭配3xx使用,告诉客户端接下来要去哪里访问 |
Cookie | 用于在客户端存储少量信息,通常用于实现session的会话功能 |
connection | keep-alive:长连接 close:短连接 |
在网站中,网站是认识我们的,在进行各种页面跳转的时候,本质其实就是进行各种http请求,网站还是照样认识我们,不用重复输密码也能认识我们。
HTTP是无状态的协议:也就是说,同一个客户第二次访问同一个服务器上的页面时,服务器的响应与第一次被访问时相同,因为服务器并不记得曾经访问过的这个用户,也不记得为该客户服务过多少次。HTTP无状态特性简化了服务器的设计,使服务器更容易支持大量并发的HTTP请求。
HTTP照样认识我这个特性,并不是HTTP协议本身要解决的问题,但是HTTP可以提供一些技术支持,来保持网站具有会话保持的功能。
这个技术就叫做“cookie”
!在浏览器和HTTP协议两个角度来看待cookie
!
1️⃣浏览器:cookie是一个文本文件,该文件里面保存的使我们的用户私密信息。
2️⃣HTTP:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在请求报文中携带该cookie信息!
基本理解:
验证观察:添加cookie信息
可以观察到,首次访问并未携带cookie信息,但是后续访问时,都携带了cookie信息,如此网站就可以认识我们了。
如果别人盗取了我们的cookie文件,他就可以
1、以我们的身份认证登录特定的资源。
2、如果保存是账号密码,那么情况就会变得非常糟糕!
所以单纯的使用cookie是具有一定的安全隐患的,所以我们要引出session,但是我们这里讲session并不代表我们不用cookie了,现在我们市面上是cookie+session!
核心思路:将用户的私密信息保存在服务器端!!!
构建http响应时会形成一个唯一的会话ID——session_id,凭借这个ID号可以验证用户,其中服务器磁盘上的session文件保存了用户的私密信息。
后续所有的http请求,都会由浏览器自动携带cookie内容——就是标识唯一性的当前用户的session_id,后续服务器依旧可以认识客户端浏览器,这也是一种会话保持的功能。
但是cookie文件还有被泄露的风险啊!——是的!!但是这个我们没有办法杜绝,因为这些文件是存储在用户的电脑上的,因为用户对于安全防范的意识和计算机方面的知识涉猎较为少,所以我们无法杜绝,但是衍生出了很多的防御方案!
总结:cookie+session本质是为了提升用户访问或者登录平台的体验!
现在几乎100%都不用HTTP,使用的是HTTPS。
HTTPS=HTTP+TLS/SSL
TLS/SSL是HTTP数据的解密加密层
有一对秘钥(公钥和私钥),一般而言公钥是向全世界公开的,而私钥是必须自己私有保存的。
可以用公钥加密,但是只能用私钥来解密
可以用私钥加密,但是只能用公钥来解密
如何防止文本中的内容被篡改,以及识别是否被篡改?
首先先形成数字签名!
发送文本时,添加上数字签名!
所以接下来就要进行校验:
1、如何选择加密算法?
如果采用对称加密,假设客户端用秘钥X,那么服务端也要用X,那么对方是怎么知道X的呢?
我们可以在电脑中预装(成本高,不现实),可以提前协商秘钥(但是第一次通信的过程是没有加密的,数据是在裸奔),所以综上,单一的对称加密不可取。
其他人即使拿到了加密的数据也是没有办法解密的,因为其他人没有私钥。
所以现在能够保证从客户端到服务器单向的数据安全了,因为前文说明了,可以用公钥加密,但是只能用私钥来解密;可以用私钥加密,但是只能用公钥来解密。
所以我们再次升级改进,双方都分别生成自己的公钥和私钥,在双方通信阶段,就提前交换双方的公钥!!!
既然一对非对称加密能够保证单向通信的安全,那么两个非对称加密是不是就能保证数据双向传输的安全性呢!!!但是事实并非如此:
①依旧有非法窃取的风险。
②非对称加密算法,十分耗时。(对称算法是比较节省时间的)
实际做法:非对称+对称方案
服务器端形成对称秘钥X,用公钥S对X进行加密形成X+,此时客户端发送给服务端X+
什么叫做安全?
——不是让别人拿不到,就叫做安全,而是别人拿到了,也没法处理。
在网络环节中,随时都可能存在中间人来偷窥、修改我们的数据信息。所以返回给客户端公钥S的时候,是会存在风险的:
client并不知道server发送给自己的报文被 篡改了。
本质问题:client无法判定发来的秘钥协商报文是不是从合法的服务方发来的!
所以网络中就出现了一个非常重要的CA证书机构!
只要一个服务商,经过权威机构认证,该机构就是合法的!
CA机构:1.权威 2.有自己的公钥A和私钥A’(公钥私钥只是算法)
CA的公钥是全世界都知道的,但是CA的私钥只有CA自己知道,换言之世界上,只有CA机构能重新形成对应的数字签名!
一般,一个正规的服务商要先向CA证书机构申请证书,同时需要提交企业的基本信息(域名、公钥),然后CA机构就可以为之创建证书,证书是由企业的基本信息和基本信息形成的数字签名两部分组成,申请好证书之后,就可以颁发给用户,此时再次进行秘钥协商的时候,就不用直接发送公钥S了,而是发送证书,中间人如果想要截取证书并且修改里面的内容,这是不可以的,因为证书里面携带了数字签名,即使修改了数字签名,中间人没有CA机构的私钥,所以无法生成新的数字签名,当客户端收到证书,会把内容和数字签名拆分出来做校验,这样就可以知道信息是否被篡改过了。
要求:client必须知道CA机构的公钥信息。
如何获得公钥信息?
1️⃣一般是内置的。
2️⃣访问网址的时候,浏览器可能会提示用户进行安装。