HTTP网络协议
虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一
理解网络协议
协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
首先我们可以将这种结构化数据存储在一个结构体中,通信双方都知道这种结构体,接收发送请求或者响应就使用结构体进行接收,从此达到传输结构化数据的目的。而结构体如何定义,结构体内部成员又是如何?这是客户端和服务端在通信前就已经约定好了的,通信的前提是双方都知道,并且愿意遵守这个约定
网络版简易计算器
全部代码详见码云
使用这两个结构体是我们自己定义的约定(协议)我们创建的client 和 server 都必须遵守!这就叫做自定义协议
我们所写的cs模式的在线版本计算器,本质是一个应用层网络服务,基本的通信代码是我们自己写的,序列化和反序列化(后文会讲)是通过组件完成的,请求,结果格式等约定时我们自己做的,业务逻辑也是我们自己写的
通过这个简易版计算器我们可以对OSI七层模型的上三层建立初步理解
//请求结构体
typedef struct request{ //请求结构体
int x; //数字1
int y; //数字2
char op; //运算符
}request_t;
//响应结构体
// response format 相应格式
typedef struct response{
int sign; //标志位,反应结果是否可靠
int result; //结果
}response_t;
部分服务器通信代码
// 服务器
// 1.Read request
request_t req; //创建请求结构体接收请求
memset(&req, 0, sizeof(req));
ssize_t s = read(sock, &req, sizeof(req)); //将网络数据传输给请求结构体对象
std::cout << "get a request, request size = " << sizeof(req) << std::endl;
std::cout << s << " " << req.x << req.op << req.y << std::endl;
if (s == sizeof(req)){
// Read full request success //若获取的请求完整
// 2.prase request //解析请求信息,构建响应结构体
std::cout << "prase request" << std::endl;
response_t resp{0 , 0};
switch (req.op){ //通过请求信息来构建响应
case '+':
resp.result = req.x + req.y;
break;
case '/':
if (req.y == 0) resp.sign = 1;
else resp.result = req.x / req.y;
break;
default:
resp.sign = 3; // request method errno
break;
}
// send response
std::cout << "return response" << std::endl;
write(sock, &resp, sizeof(resp)); //将构建的响应发送给客户端
}
部分客户端通信代码
// 客户端
request_t req; //从标准输入(客户)得到数据保存到结构体中
cout << "Please Enter Date One# ";
cin >> req.x;
cout << "Please Enter Date Two# ";
cin >> req.y;
cout << "Please Enter Operator";
cin >> req.op;
write(sock, &req, sizeof(req)); //将结构体发送给服务器
response_t resp; //创建响应结构体接收服务器响应
ssize_t s = read(sock, &resp, sizeof(resp)); //读取响应内容打印结果
if (s == sizeof(resp)){
if (resp.sign == 1){
std::cout << "除零错误" << std::endl;
}
else if (resp.sign == 3){
std::cout << "非法运算符" << std::endl;
}
else {
std::cout << "result = " << resp.result << std::endl;
}
}
通过上述代码我们确实通过自己定义的协议使用结构体完成了网络结构化数据的传输,但是这种方式却有着非常明显的弊端。首先,我们必须保证客户端和服务器的内存对齐方式必须相同,其次一旦服务器进行更新,对传输的结构体进行了修改,那么以前的所有客户端就用不了,因为两个结构体的格式不同,那么也就无法寄希望于传输后的数据可以原模原样的拿出来了。再者,有很多场景的某些数据大小是并不固定的,比如微信聊天,你怎么知道一个人发的信息内容有多大呢,对于信息我们结构体应该开多大的空间才合适??开大了浪费网络资源,开小了可能长一点的话发了就会出现截断或者乱码等问题
为了解决上述问题,以前的程序员们提出了序列化和反序列化
序列化和反序列化
当一台主机想要将数据上传到网络之前,将数据进行序列化然后再传入网络。而一台主机想要从网络读取数据后,需要将网络中的数据进行反序列化
JSON 是我们日常开发中常用的一种序列化和反序列化工具
sudo yum install -y jsoncpp-devel //安装json
JSON传输数据
JSON序列化
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
typedef struct request{
int x;
int y;
char op;
}request_t;
int main(){
request_t req{10, 20, '*'};
//序列化过程
Json::Value root; //可以承装任何对象,json是一种kv式的序列化方案
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
// Json::StyledWriter writer;
Json::FastWriter writer;
writer.write(root);
std::string json_string = writer.write(root);
std::cout << json_string << std::cout;
return 0;
}
Json::StyledWriter类型对象构建的json_string
{"datax":10,"datay":20,"operator":42}
Json::FastWriter 类型对象构建的json_string
{
"datax" : 10,
"datay" : 20,
"operator" : 42
}
[clx@VM-20-6-centos JsonTest]$ ldd a.out
linux-vdso.so.1 => (0x00007fffddfee000)
/$LIB/libonion.so => /lib64/libonion.so (0x00007f80236f2000)
libjsoncpp.so.0 => /lib64/libjsoncpp.so.0 (0x00007f80233a2000) // 这就是第三方组件,也就是一个动态库
libstdc++.so.6 => /home/clx/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6 (0x00007f8023021000)
libm.so.6 => /lib64/libm.so.6 (0x00007f8022d1f000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f8022b09000)
libc.so.6 => /lib64/libc.so.6 (0x00007f802273b000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f8022537000)
/lib64/ld-linux-x86-64.so.2 (0x00007f80235d9000)
JSON反序列化
int main(){
// 反序列化
std::string json_string = R"({"datax":10, "datay":20, "operator":42})";//R()可以防止内部字符部分字符被转义
Json::Reader reader; //使用Json::Reader类型对象反序列化 序列化数据,放入万能对象root中
Json::Value root;
reader.parse(json_string, root);
request_t req;
req.x = root["datax"].asInt();//root使用key来查找value数据,并使用asInt()函数转化成对应类型
req.y = root["datay"].asInt();
req.op = root["operator"].asUInt();
std::cout << req.x << " " << req.op << " "<< req.y << std::endl;
return 0;
}
使用JSON优化计算器
1.使用JSON对请求和响应结构体分别进行序列化和反序列化
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;
}
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 = root["operator"].asUInt();
}
std::string SerializeResponse(const response_t &resp){
Json::Value root;
root["sign"] = resp.sign;
root["result"] = resp.result;
Json::FastWriter writer;
std::string json_string = writer.write(root);
return json_string;
}
void DeserializeResponse(const std::string &json_string, response_t &out){
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.sign = root["sign"].asInt();
out.result = root["result"].asInt();
}
2.使用JSON字符串在网络内传输数据
//1.Method2 ReadRequest 从网络中读取Json字符串
char buffer[1024] = {0};
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
buffer[s] = 0;
if (s > 0){
request_t req;
DeserializeRequest(buffer, req);
// send response 将结构化数据进行序列化后再发送
std::string json_string = SerializeResponse(resp);
write(sock, json_string.c_str(), json_string.size());
std::cout << "return response successs" << std::endl;
正式认识HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. **HTTP(超文本传输协议)**就是其中之一
URL基本认识
平时我们所说的网址其实就是URL
我们请求的图片,视频等都称之为资源,这些资源都是存在网络中一台Linux机器上。IP + Port唯一确定一个进程,却不能唯一标识一个资源。而传统的操作系统保存资源的方式都是以文件保存的,单Linux系统,标识唯一资源的方式就是通过路径。
urlencode和urldecode
例如我们分别在百度中搜索 翻译和C++
我们可以通过这个转义工具来对自己的字符串进行转义,查看其在URL中的形态
HTTP协议格式
无论是请求还是响应,http都是按照行(\n)为单位进行构建请求或者响应的!无论是请求还是响应,几乎都是由3或者4部分组成的
如何理解普通用户的上网行为 1.从目标服务器拿到你要的资源 2.向目标服务器上传你的数据
HTTP请求
第一部分 请求行(第一行) 由 请求方法 + url(去掉域名后的内容) + http协议版本 + \n 构成
第二部分 请求报头(Header), 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
第三部分 空行 \n
第四部分 请求正文(Body)(如果有的话) 用户提交的数据,请求正文允许为空字符串. 如果正文存在, 则在Header中会有一个Content-Length属性来标识Body的长度
HTTP响应
第一部分 状态行(第一行) 由 http协议版本 + 状态码 + 状态码描述
第二部分 请求报头(Header), 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
第三部分 空行 \n
第四部分 响应正文(Body)(如果有的话) 用户提交的数据,响应正文允许为空字符串. 如果正文存在, 则在Header中会有一个Content-Length属性来标识响应正文的长度
HTTP方法
HTTP常见状态码
HTTP常见Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
Conection: 1.0只有短链接,HTTP1.1版本之后支持长链接
搭建简单HTTP服务器
简单前端页面设计
这个网站包含简单的前端也米娜HTML编写教程w3cschool ,我通过这个网站的表单,制作了一个简单的首页HTML界面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h3>hello net</h3>
<h5>hello 我是表单</h5>
<from action="/a" method = "POST">
姓名:<input type="text" name="name"><br/>
密码:<input type="password" name="passwd"><br/>
<input type="submit" value="登录"> <br/>
</from>
</body>
</html>
HTTP服务器搭建
//这里使用了专门的网络写入读取接口
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
#include "Sock.hpp"
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fstream>
#define WWWROOT "./wwwroot/" //根目录
#define HOME_PAGE "index.html" //首页
void Usage(std::string proc){
std::cout << "Usage " << proc << " port " << std::endl;
}
void* HandlerHttpRequest(void* args){
int sock = *(int*)args;
delete (int*)args;
pthread_detach(pthread_self());
#define SIZE 1024 * 10
char buffer[SIZE];
memset(buffer, 0, SIZE);
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0){
buffer[s] = 0;
std::cout << buffer << std::endl; //查看浏览器发来的HTTP请求
//构建HTTP响应
// std::string http_response = "http/1.0 200 OK\n"; //构建响应状态行
// http_response += "Content-Type: text/plain\n"; //正文的属性
// http_response += "\n"; //空行
// http_response += "hello net!"; //正文
std::string html_file = WWWROOT;
html_file += HOME_PAGE;
// 返回的时候不仅仅是返回正文网页信息,而是要包括http请求
std::string http_response = "http/1.0 200 OK\n";
// 正文部分的数据类型
http_response += "Content-Type: text/html; charset=utf8\n";
struct stat st;
stat(html_file.c_str(), &st);
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);
http_response += "\n";
http_response += "\n";
//std::cout << http_response << std::endl;
//响应正文
std::ifstream in(html_file);
if (!in.is_open()){
std::cerr << "open html error!" << std::endl;
}
else {
std::cout << "open success" << std::endl;
std::string content;
std::string line;
while (std::getline(in, line)){
content += line;
}
//std::cout << content << std::endl;
http_response += content;
//std::cout << http_response << std::endl;
}
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]); return 1;}
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 *psock = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, (void*)psock);
}
}
}
将我们的程序跑起来就可以接收网络发来的HTTP请求了,我们可以使用浏览器向我们的程序发送请求,只需要在浏览器中输入我们的公网IP:端口号
就可以了,可以看到浏览器给我们发来的http请求如下
GET / HTTP/1.1 //请求行
Host: 101.43.252.201:8888 //请求报头
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
//空行 到此HTTP请求报头结束,这个请求没有正文
GET / HTTP/1.1 //请求行
Content-Type常见类型可以查看这位博主的博客,整理的很完整的
GET和POST方法
GET方法:又称获取方法,是最常用的方法,默认一般获取所有网页,都是GET方法,但是如果要使用GET方法提交参数,会通过URL进行参数拼接从而提交给server端
POST方法:又称推送方法,是提交参数比较常用的方法,但是如果提交参数,一般是通过正文部分提交,但是你不要忘记,Content-Length: XXX表示正文的长度
Cookie与Session
在日常生活中,我们发现在各种页面跳转的时候,本质是进行各种http请求之后,网站照样认识我。比如我是B站大会员,不管我怎么在B站浏览视频,他都不会让我重新登录,但是HTTP协议本事是一种无状态协议,其只关心本次请求是否成功,对以往的请求并不做任何的记录
让网站认识我并不是HTTP协议本身要解决的问题,http可以提供一些技术支持来保证网站具有会话保持功能。cookie就是一种会话管理工具
- 浏览器:cookie其实是一个文件,改文件里面保存的是我们的用户私密信息
- http协议:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在request中携带cookie信息
//Set-Cookie: 服务器向浏览器设置一个cookie
http_response += "Set-Cookie: id=1111111111\n";
http_response += "Set-Cookie: password=2222\n";
我们在HTTP响应中添加两行,使用浏览器给服务器发送请求,服务器的响应报头中添加Set-Cookie属性,就可以设置浏览器的Cookie文件了
再次使用浏览器发送请求,就可以看到请求中带有Cookie信息
GET / HTTP/1.1
Host: 101.43.252.201:8889
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: id=1111111111; password=2222 //Cookie信息
Session核心思路:将用户的私密信息,保存在服务器端
浏览器将私密信息发送给服务器,服务器对这些私密信息进行认证,构建成session文件保存到服务器上,再通过session文件生成一个唯一的session_id ,将这个session_id设置到浏览器中的Cookie文件中。当我们再次使用登录时,浏览器会自动携带Cookie信息(session_id)发送请求,后续Server依旧可以做到认识Client
HTTPS
加密解密层
所有我们可以叫得上名字的网站都使用的是HTTPS协议,**HTTP协议和HTTPS有什么差别呢??加密 **
HTTPS = HTTP + TLS/SSL(数据的加密解密层)
加密解密层处于HTTP协议层的最底层,HTTP向下先访问TLS和SSL安全层接口,然后由安全层接口调用系统调用接口,socket接口,数据使用系统调用接口读取后会先进性加密/解密操作。所以HTTPS就是HTTP协议加上了一个加密解密层,这两层统称为HTTPS。并且大部分情况下只有用户数据(有效载荷)才会被加密,其他数据没有必要
两种加密方式
1.对称加密,密钥(只有一个)X
使用X密钥进行加密,也要使用X密钥进行解密,例如:
data ^ X = result; // 加密
result ^ X = data; // 解密
2.非对称加密 有一对密钥:公钥和私钥
可以用公钥加密,但是只能用私钥解密或者用私钥加密,只能用公钥解密。经典的非对称加密算法RSA一般而言,公钥是全世界公开的,私钥是必须自己进行私有保存的!
如何确认文本经过网络传输后保持原样,并检测其是否被篡改
我们可以对文本使用Hash散列算法进行处理,形成固定长度,唯一的字符串序列称为数据摘要 or 数据指纹。(Hash散列算法,只要文本只有一个标点符号差异,也会生成差异非常大的hash结果),再对数据摘要进行加密算法处理生成数据签名。将数据签名和文本一起通过网络发送给另一台主机。另一端接收到数据后对数据签名进行解密获取数据指纹1,并且对文本再进行Hash散列算法处理,生成数据指纹2,若两个数据指纹相同,则文本没有被篡改
加密的实际应用
那么日常生活中我们使用对称加密还是非对称加密呢??
如果我们使用对称加密,我们应该如何将密钥部署在两台服务器上呢??这一点我们可以采用预装来解决,将所有的对称密钥都事先预装到机器上,不过预装成本很高,如果每一台服务器都需要我们手动安装密钥那也太麻烦了?可以使用网络下载吗?下载等于网络通信,那么下载密钥是否需要加密呢??就算双方都有了密钥,应该如何协商使用哪一种密钥呢??协商密钥第一次有绝对没有加密。所以直接使用对称加密实际是不安全的
如果使用非对称加密方式,通常使用两对非对称密钥保证通信安全。客户端和服务器都有自己的公钥和私钥,在协商公钥阶段,服务器和客户端相互发送公钥给对方(数据不加密)。然后进入通信阶段,客户端的数据使用S的公钥S加密发送给Server,然后Server使用自己的私钥S`进行解密得到数据。同理Server使用Client的公钥C将响应加密,然后将加密数据传送给Client,Client再用自己的私钥进行解密,得到响应数据一对钥匙保证一个方向的通信安全,但是非对称加密方式任然存在被非法窃取的风险,并且非对称加密算法特别的耗费时间,效率非常低
在实际生活中采用的非对称+对称方案
协商密钥阶段: Server直接向Client发送自己的公钥S(不加密),Client自动生成一个对称加密的密钥X,使用公钥S加密发送给Server,Server使用自己的私钥S`进行解密,就获得了对称密钥X
通信阶段:Server和Client都知道了对称密钥X,双方通过对称密钥X进行通信
中间人攻击
在网络环节中,随时可能存在中间人来偷窥、修改我们的数据,当服务器和客户端正在处于协商密钥阶段时,可能会收到中间人的攻击
中间人截获服务器发送给客户端的未加密公钥S,将其修改成自己的公钥M,然后发送给Client,Client自动生成对称密钥X,然后对X使用M公钥进行加密,发送给服务器,此时即使服务器获取到加密X数据也无法完成解密,因为服务器没有M`。若加密X信息再次被中间人截获或偷窥,他就可以使用私钥进行解密,获取对称密钥X。这样中间人就和客户端建立了基于对称密钥X的通信,用户得到的响应由服务器构建变成了由中间人构建,用户的数据就完全泄露了
CA证书机构
只要一个服务商,经过权威机构认证,该机构就是合法的。而CA机构就是网络中权威的认证机构(其也有自己的公钥和私钥)
服务商必须提供自己的基本信息(如:域名,公钥等)用于申请CA证书,而CA机构会通过这些信息创建证书。而企业的基本信息就是一段文本,CA机构会使用Hash散列话数据生成数据指纹,然后**用CA机构的私钥进行加密(重点!重点!重点!)**生成该公司的数字签名,然后构建证书颁发给合法服务商
证书:CA机构给企业的生成的数字签名+企业提供申请证书所用的基本信息
所以,服务端和客户端进行协商密钥阶段,服务端只需要将CA证书发送给客户端就可以了,因为证书中企业基本信息包含服务端公钥S。而客户端接收到证书后,使用CA机构的公钥对数据签名进行解密,然后再对企业基本信息进行Hash散列,对比两个数据指纹是否相同,若相同则说明数据来源合法,进行下一步操作。
那么中间人能否截取你的证书信息呢??当然可以。
但中间人能否修改证书中的公钥呢??绝对不行,因为若企业基本信息被改生成的数据指纹就与原来不同,只要客户端对数据签名进行解密,对被修改信息Hash散列话就可以发现指纹不同,数据被修改。
那能否替换企业公钥信息并且替换数据签名呢??也不行,因为数据签名是由CA机构的私钥进行加密的,中间人绝对不知道CA机构私钥信息,若使用自己的私钥进行加密生成数据签名,则无法使用CA机构的公钥进行解密
如果中间人也是一个合法的服务方,也有自己的CA证书呢?? 在CA证书中不仅包含了企业公钥,还包含了类似域名等信息,客户端明明给www.baidu.com发送请求,结果回应它的竟然是www.qq.com,这先让你是非常不合理的,证书的存使中间人无法修改服务器发给客户端的数据,一旦修改就会被侦测到
解密,Client如何知道CA机构的公钥呢??
那么中间人能否截取你的证书信息呢??当然可以。
但中间人能否修改证书中的公钥呢??绝对不行,因为若企业基本信息被改生成的数据指纹就与原来不同,只要客户端对数据签名进行解密,对被修改信息Hash散列话就可以发现指纹不同,数据被修改。
那能否替换企业公钥信息并且替换数据签名呢??也不行,因为数据签名是由CA机构的私钥进行加密的,中间人绝对不知道CA机构私钥信息,若使用自己的私钥进行加密生成数据签名,则无法使用CA机构的公钥进行解密
如果中间人也是一个合法的服务方,也有自己的CA证书呢?? 在CA证书中不仅包含了企业公钥,还包含了类似域名等信息,客户端明明给www.baidu.com发送请求,结果回应它的竟然是www.qq.com,这先让你是非常不合理的,证书的存使中间人无法修改服务器发给客户端的数据,一旦修改就会被侦测到