写在前面
这个博客我们要要讨论的是协议,主要是应用层.今天我们将正式认识HTTP和HTTPS,也要认识序列化和反序列化,内容比较多,但是不难
再谈协议
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层,我们要完成下面三个步骤.
-
sock的使用
-
定制协议,也就是报头如何封报和解包
-
编写业务
协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
例如 , 我们需要实现一个服务器版的加法器 . 客户端把要计算的两个加数发过去 , 然后由服务器进行计算 , 最后再把结果返回给客户端,此时我们就需要定制协议.我们有一下两种方案.
约定方案一
- 客户端发送一个形如 “1+1” 的字符串 ;
- 这个字符串中有两个操作数 , 都是整形 ;
- 两个数字之间会有一个字符是运算符 , 运算符只能是 + ;
- 数字和运算符之间没有空格
约定方案二
- 定义结构体来表示我们需要交互的信息 ;
- 发送数据时将这个结构体按照一个规则转换成字符串 , 接收到数据的时候再按照相同的规则把字符串转化回结构体
序列化 & 反序列化
其中我们把法案二的过程称之序列和反序列化,无论我们采用方案一 , 还是方案二 , 还是其他的方案 , 只要保证 , 一端发送时构造的数据 , 在另一端能够正确的进行解析, 就是 ok 的 . 这种约定 , 就是 应用层协议.下面我们要把方案二给实现一下.
下面我们说一下我们如何序列化和反序列化.很简单,所谓的序列化就是把我们结构体里面的内容变成一个字符串,反序列化就是把字符串按照某种格转换为结构体里面的属性.这里我们加上一个报头的封包和解包的动作,这里要确保我们一次行拿到的数据是一个结构体的数据,算是一个协议.
#pragma once
#include <iostream>
#include <string>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "util.hpp"
// 我们要在这里进行我们自己的协议定制!
// 网络版本的计算器
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define OPS "+-*/%"
// decode,整个序列化之后的字符串进行提取长度
// 1. 必须具有完整的长度
// 2. 必须具有和len相符合的有效载荷
// 我们才返回有效载荷和len
// 否则,我们就是一个检测函数!
// 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
std::string decode(std::string &in, uint32_t *len)
{
assert(len);
// 1. 确认是否是一个包含len的有效字符串
*len = 0;
std::size_t pos = in.find(CRLF);
if (pos == std::string::npos)
return ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)
// 2. 提取长度
std::string inLen = in.substr(0, pos);
int intLen = atoi(inLen.c_str());
// 3. 确认有效载荷也是符合要求的
int surplus = in.size() - 2 * CRLF_LEN - pos;
if (surplus < intLen)
return "";
// 4. 确认有完整的报文结构
std::string package = in.substr(pos + CRLF_LEN, intLen);
*len = intLen;
// 5. 将当前报文完整的从in中全部移除掉
int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN;
in.erase(0, removeLen);
// 6. 正常返回
return package;
}
// encode, 整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{
// "exitCode_ result_"
// "len\r\n""exitCode_ result_\r\n"
std::string encodein = std::to_string(len);
encodein += CRLF;
encodein += in;
encodein += CRLF;
return encodein;
}
// 定制的请求 x_ op y_
class Request
{
public:
Request()
{
}
~Request()
{
}
// 序列化 -- 结构化的数据 -> 字符串
// 认为结构化字段中的内容已经被填充
void serialize(std::string *out)
{
std::string xstr = std::to_string(x_);
std::string ystr = std::to_string(y_);
// std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->
*out = xstr;
*out += SPACE;
*out += op_;
*out += SPACE;
*out += ystr;
}
// 反序列化 -- 字符串 -> 结构化的数据
bool deserialize(std::string &in)
{
// 100 + 200
std::size_t spaceOne = in.find(SPACE);
if (std::string::npos == spaceOne)
return false;
std::size_t spaceTwo = in.rfind(SPACE);
if (std::string::npos == spaceTwo)
return false;
std::string dataOne = in.substr(0, spaceOne);
std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
if (oper.size() != 1)
return false;
// 转成内部成员
x_ = atoi(dataOne.c_str());
y_ = atoi(dataTwo.c_str());
op_ = oper[0];
return true;
}
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "x_: " << x_ << std::endl;
std::cout << "op_: " << op_ << std::endl;
std::cout << "y_: " << y_ << std::endl;
std::cout << "#################################" << std::endl;
}
public:
// 需要计算的数据
int x_;
int y_;
// 需要进行的计算种类
char op_; // + - * / %
};
// 定制的响应
class Response
{
public:
Response() : exitCode_(0), result_(0)
{
}
~Response()
{
}
// 序列化 -- 不仅仅是在网络中应用,本地也是可以直接使用的!
void serialize(std::string *out)
{
// "exitCode_ result_"
std::string ec = std::to_string(exitCode_);
std::string res = std::to_string(result_);
*out = ec;
*out += SPACE;
*out += res;
}
// 反序列化
bool deserialize(std::string &in)
{
// "0 100"
std::size_t pos = in.find(SPACE);
if (std::string::npos == pos)
return false;
std::string codestr = in.substr(0, pos);
std::string reststr = in.substr(pos + SPACE_LEN);
// 将反序列化的结果写入到内部成员中,形成结构化数据
exitCode_ = atoi(codestr.c_str());
result_ = atoi(reststr.c_str());
return true;
}
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "exitCode_: " << exitCode_ << std::endl;
std::cout << "result_: " << result_ << std::endl;
std::cout << "#################################" << std::endl;
}
public:
// 退出状态,0标识运算结果合法,非0标识运行结果是非法的,!0是几就表示是什么原因错了!
int exitCode_;
// 运算结果
int result_;
};
bool makeReuquest(const std::string &str, Request *req)
{
// 123+1 1*1 1/1
char strtmp[BUFFER_SIZE];
snprintf(strtmp, sizeof strtmp, "%s", str.c_str());
char *left = strtok(strtmp, OPS);
if (!left)
return false;
char *right = strtok(nullptr, OPS);
if (!right)
return false;
char mid = str[strlen(left)];
req->x_ = atoi(left);
req->y_ = atoi(right);
req->op_ = mid;
return true;
}
下面我们要做的非常简单,客户端拿到数据,根据数据构造一个请求的结构体,然后把这个结构体个序列化后进行封包,把最后的结构体发送过去就可以了.
//客户端
#include "util.hpp"
#include "Protocol.hpp"
#include <cstdio>
// 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!!
// 3. 需要listen吗?不需要的!
// 4. 需要accept吗?不需要的!
volatile bool quit = false;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
<< std::endl;
}
// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
// 1. 创建socket SOCK_STREAM
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽
// 2.1 先填充需要连接的远端主机的基本信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_aton(serverIp.c_str(), &server.sin_addr);
// 2.2 发起请求,connect 会自动帮我们进行bind!
if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect: " << strerror(errno) << std::endl;
exit(CONN_ERR);
}
std::cout << "info : connect success: " << sock << std::endl;
std::string message;
while (!quit)
{
message.clear();
std::cout << "请输入表达式>>> "; // 1 + 1
std::getline(std::cin, message); // 结尾不会有\n
if (strcasecmp(message.c_str(), "quit") == 0)
{
quit = true;
continue;
}
// message = trimStr(message); // 1+1 1 +1 1+ 1 1+ 1 1 +1 => 1+1 -- 不处理
Request req;
// 制作请求
if (!makeReuquest(message, &req))
continue;
std::string package;
req.serialize(&package); // 把请求序列化到这个package中
std::cout << "debug->serialize-> " << package << std::endl;
package = encode(package, package.size()); // 添加报头
std::cout << "debug->encode-> \n"
<< package << std::endl;
// 把请求发送走
ssize_t s = write(sock, package.c_str(), package.size());
if (s > 0)
{
char buff[1024];
size_t s = read(sock, buff, sizeof(buff) - 1);
if (s > 0)
buff[s] = 0;
std::string echoPackage = buff;
Response resp;
uint32_t len = 0;
std::string tmp = decode(echoPackage, &len); // 去掉报头
if (len > 0)
{
echoPackage = tmp;
// std::cout << "debug->decode-> " << echoPackage << std::endl;
resp.deserialize(echoPackage); //反序列化
printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);
}
}
else if (s <= 0)
{
break;
}
}
close(sock);
return 0;
}
服务端得到字符串,首先要做的就是解包,然后进行反序列化,得到结构体,把这个任务处理好好之后,拿到数据把数依次经行序列化和分包后把结果返回过去就可以了.
// 服务端
static Response calculator(const Request &req)
{
Response resp;
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 '/':
{ // x_ / y_
if (req.y_ == 0)
resp.exitCode_ = -1; // -1. 除0
else
resp.result_ = req.x_ / req.y_;
}
break;
case '%':
{ // x_ / y_
if (req.y_ == 0)
resp.exitCode_ = -2; // -2. 模0
else
resp.result_ = req.x_ % req.y_;
}
break;
default:
resp.exitCode_ = -3; // -3: 非法操作符
break;
}
return resp;
}
// 1. 全部手写 -- done
// 2. 部分采用别人的方案--序列化和反序列化的问题 -- xml,json,protobuf
void netCal(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
// 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
std::string inbuffer;
while (true)
{
Request req;
char buff[128];
ssize_t s = read(sock, buff, sizeof(buff) - 1);
if (s == 0)
{
logMessage(NOTICE, "client[%s:%d] close sock, service done", clientIp.c_str(), clientPort);
break;
}
else if (s < 0)
{
logMessage(WARINING, "read client[%s:%d] error, errorcode: %d, errormessage: %s",
clientIp.c_str(), clientPort, errno, strerror(errno));
break;
}
// read success
buff[s] = 0;
inbuffer += buff;
std::cout << "inbuffer: " << inbuffer << std::endl;
// 1. 检查inbuffer是不是已经具有了一个strPackage
uint32_t packageLen = 0;
std::string package = decode(inbuffer, &packageLen); // 去掉报头
if (packageLen == 0)
continue; // 无法提取一个完整的报文,继续努力读取吧
std::cout << "package: " << package << std::endl;
// 2. 已经获得一个完整的package
if (req.deserialize(package)) // 反序列化
{
req.debug();
// 3. 处理逻辑, 输入的是一个req,得到一个resp
Response resp = calculator(req); // resp是一个结构化的数据
// 4. 对resp进行序列化
std::string respPackage;
resp.serialize(&respPackage);
// 5. 对报文进行encode --
respPackage = encode(respPackage, respPackage.size());
// 6. 简单进行发送 -- 后续处理
write(sock, respPackage.c_str(), respPackage.size()); // 把结果写回去
}
}
}
下面我们说一下我们不用这么麻烦,关于序列化和反序列化已经有人帮助我们完成好了,这里推荐使用jsoncpp,注意这里我们只需要知道有这个工具就可以了,具体的等到我们的项目和大家分析.
#include <iostream>
#include <string>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "util.hpp"
// 我们要在这里进行我们自己的协议定制!
// 网络版本的计算器
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define OPS "+-*/%"
//#define MY_SELF 1
// decode,整个序列化之后的字符串进行提取长度
// 1. 必须具有完整的长度
// 2. 必须具有和len相符合的有效载荷
// 我们才返回有效载荷和len
// 否则,我们就是一个检测函数!
// 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
std::string decode(std::string &in, uint32_t *len)
{
assert(len);
// 1. 确认是否是一个包含len的有效字符串
*len = 0;
std::size_t pos = in.find(CRLF);
if (pos == std::string::npos)
return ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)
// 2. 提取长度
std::string inLen = in.substr(0, pos);
int intLen = atoi(inLen.c_str());
// 3. 确认有效载荷也是符合要求的
int surplus = in.size() - 2 * CRLF_LEN - pos;
if (surplus < intLen)
return "";
// 4. 确认有完整的报文结构
std::string package = in.substr(pos + CRLF_LEN, intLen);
*len = intLen;
// 5. 将当前报文完整的从in中全部移除掉
int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN;
in.erase(0, removeLen);
// 6. 正常返回
return package;
}
// encode, 整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{
// "exitCode_ result_"
// "len\r\n""exitCode_ result_\r\n"
std::string encodein = std::to_string(len);
encodein += CRLF;
encodein += in;
encodein += CRLF;
return encodein;
}
// 定制的请求 x_ op y_
class Request
{
public:
Request()
{
}
~Request()
{
}
// 序列化 -- 结构化的数据 -> 字符串
// 认为结构化字段中的内容已经被填充
void serialize(std::string *out)
{
#ifdef MY_SELF
std::string xstr = std::to_string(x_);
std::string ystr = std::to_string(y_);
// std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->
*out = xstr;
*out += SPACE;
*out += op_;
*out += SPACE;
*out += ystr;
#else
// json
// 1. Value对象,万能对象
// 2. json是基于KV
// 3. json有两套操作方法
// 4. 序列化的时候,会将所有的数据内容,转换成为字符串
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter fw;
// Json::StyledWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化 -- 字符串 -> 结构化的数据
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// 100 + 200
std::size_t spaceOne = in.find(SPACE);
if (std::string::npos == spaceOne)
return false;
std::size_t spaceTwo = in.rfind(SPACE);
if (std::string::npos == spaceTwo)
return false;
std::string dataOne = in.substr(0, spaceOne);
std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
if (oper.size() != 1)
return false;
// 转成内部成员
x_ = atoi(dataOne.c_str());
y_ = atoi(dataTwo.c_str());
op_ = oper[0];
return true;
#else
// json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "x_: " << x_ << std::endl;
std::cout << "op_: " << op_ << std::endl;
std::cout << "y_: " << y_ << std::endl;
std::cout << "#################################" << std::endl;
}
public:
// 需要计算的数据
int x_;
int y_;
// 需要进行的计算种类
char op_; // + - * / %
};
// 定制的响应
class Response
{
public:
Response() : exitCode_(0), result_(0)
{
}
~Response()
{
}
// 序列化 -- 不仅仅是在网络中应用,本地也是可以直接使用的!
void serialize(std::string *out)
{
#ifdef MY_SELF
// "exitCode_ result_"
std::string ec = std::to_string(exitCode_);
std::string res = std::to_string(result_);
*out = ec;
*out += SPACE;
*out += res;
#else
// json
Json::Value root;
root["exitcode"] = exitCode_;
root["result"] = result_;
Json::FastWriter fw;
// Json::StyledWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// "0 100"
std::size_t pos = in.find(SPACE);
if (std::string::npos == pos)
return false;
std::string codestr = in.substr(0, pos);
std::string reststr = in.substr(pos + SPACE_LEN);
// 将反序列化的结果写入到内部成员中,形成结构化数据
exitCode_ = atoi(codestr.c_str());
result_ = atoi(reststr.c_str());
return true;
#else
// json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
exitCode_ = root["exitcode"].asInt();
result_ = root["result"].asInt();
return true;
#endif
}
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "exitCode_: " << exitCode_ << std::endl;
std::cout << "result_: " << result_ << std::endl;
std::cout << "#################################" << std::endl;
}
public:
// 退出状态,0标识运算结果合法,非0标识运行结果是非法的,!0是几就表示是什么原因错了!
int exitCode_;
// 运算结果
int result_;
};
应用层
上面我们写了协议吗?写了,我们在有效数据上面加了长度,这就是我们我们协议.你会发现,我们自己写是很麻烦的,有没有非常成熟的场景,这让程序员自定义协议供所有人接受,也就是一套标准,也就是应用层的标准,特定协议的标准.是存在的.下面我们简绍两类协议,当然也存在其他的标准.
- HTTP 明文传输,默认端口是80端口
- HTTPS 加密传输,默认端口是443端口
HTTP协议
首先我们先说明一下HTTP协议我们现在不太常用了,主流的是HTTPS协议,不过之前HTTP使用太广泛了,直到现在仍旧存在HTTP协议的服务.,08年我国是属于草莽阶段,很多公司HTTP用的多,现在一般HTTPS用到多,HTTP(HyperText Transfer Protocol,超⽂本传输协议)它是⽆连接, ⽆状态, ⼯作在应⽤层的协议.
- 无连接理解为: HTTP协议本身是没有维护连接信息的, HTTP的数据会交给⽹络协议栈传输层的TCP协议, ⽽TCP是⾯向连接的.
- 无状态: HTTP 协议⾃身不对请求和响应之间的通信状态进⾏保存.也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Qr2t2VG-1679454666162)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230320211742939.png)]
URL认识
平时我们俗称的 “网址” 其实就是说的 URL
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iheBrJri-1679454666164)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230320211131231.png)]
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RHqfwJto-1679454666164)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230307212503084.png)]
HTTP协议格式
HTTP 协议规定,请求从客户端发出,最后服务器端响应该请求并返回.换句话说,肯定是先从客户端开始建⽴通信的,服务器端在没有接收到请求之前不会发送响应.
下面我简单的说一下HTTP协议的格式,这里要说一下一般在书中,他们会说他们分为三部分,那里是不错的,只不过我们这里按照四部分来分析,主要是让大家更好的理解.
请求报文,
- 首行 : [ 方法 ] + [url] + [ 版本 ]
- Header: 请求的属性 , 冒号分割的键值对 ; 每组属性之间使用 \n 分隔 ; 遇到空行表示 Header 部分结束
- 这是一个换行
- Body: 空行后面的内容都是 Body. Body 允许为空字符串 . 如果 Body 存在 , 则在 Header 中会有一个Content-Length属性来标识 Body 的长度
响应报文
- 首行 : [ 版本号 ] + [ 状态码 ] + [ 状态码解释 ]
- Header: 请求的属性 , 冒号分割的键值对 ; 每组属性之间使用 \n 分隔 ; 遇到空行表示 Header 部分结束
- 这是一个换行
- Body: 空行后面的内容都是 Body. Body 允许为空字符串 . 如果 Body 存在 , 则在 Header 中会有一个Content-Length属性来标识 Body 的长度 ; 如果服务器返回了一个 html 页面 , 那么 html 页面内容就是在body中 .
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kPEFNVNK-1679454666165)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230320213807927.png)]
这里有两个问题,我们是如何找报头和有效载荷的,这是我们换行的作用,我们以他为分割线.那么我们如何知道有效载荷的长度的?这里如果我们存在有效载荷,那么Header中将会存在一个属性保存我们有效载荷的长度.
你说了那么多,我们是不是应该看看是不是呢?这里先来看我们的请求的格式究竟是不是?这里先来看别人的截图,后面我们自己写一个来让大家看一看.
这里我们实现一下,主要是让大家看一下
void handlerHTTPRequest(int sock)
{
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if(s > 0) cout << buffer;
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port),
ip_(ip),
listenSock_(-1)
{
quit_ = false;
}
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind绑定
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5 /*后面再说*/) < 0)
{
exit(3);
}
// 运行别人来连接你了
}
void loop()
{
signal(SIGCHLD, SIG_IGN); // only Linux
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
// 获取链接失败
cerr << "accept error ...." << endl;
continue;
}
// 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
close(listenSock_); //建议
if(fork() > 0) exit(0);
//孙子进程
handlerHTTPRequest(serviceSock);
exit(0); // 进入僵尸
}
close(serviceSock);
wait(nullptr);
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 安全退出
bool quit_;
};
HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
HTTP请求方法
现在我们已经知道了我们HTTP协议的格式,这里我们要说一下请求方法.先说明一下,我们与计算机经行交互,那么我们要做的莫过于下面的两件事情.
- 向网络中发送数据
- 从网络中读取资源
下面是网络中为了支持不同的操作给我们提供的方法,我们重点看下面的两个.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rRLW1tKt-1679454666169)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/qkj/202303211046046.png)]
GET
GET方法会把我们的请求以明文方式将我们的参数信息拼接到我们url中,下面是我们的前端代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>104 期测试</title>
</head>
<body>
<h3>hello my server!</h3>
<p>我终于测试完了我的代码</p>
<form action="/a/b/c.html" method="get">
Username: <input type="text" name="user"><br>
Password: <input type="password" name="passwd"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
POST
下面我们说一下POST传参,这个方法会把我们的参数以明文的方式放在正文中.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>104 期测试</title>
</head>
<body>
<h3>hello my server!</h3>
<p>我终于测试完了我的代码</p>
<form action="/a/b/c.html" method="post">
Username: <input type="text" name="user"><br>
Password: <input type="password" name="passwd"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
这里我们需要对比一下我们的两个方法,注意,我们这里谈的私密不是安全,他们都不安全,要知道我们的参数 以明文的方式在裸奔,如果有一个中间人拿到了我们请求报文,那么无论是POST还是GET方法,我们参数都可以被拿到.[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PnrvvU6l-1679454666171)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230209155448643.png)]
这里我还想问大家一个问题,为何我们在Window环境中下载软件会默认是适配Windows版本的,在手机上是手机版本的?这是我们的请求报文中会携带我们硬件的基本信息.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-38SmOwcE-1679454666172)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230209133604086.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HV2zRybb-1679454666173)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230209134811664.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jx4DSaL1-1679454666173)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230209134834052.png)]
HTTP状态码
首先我们要说的所谓的状态码就可以理解为一个哈希表,我们用特定的数字来代表我们不同相应的含义,仅此而已.可是各大厂商对他的支持标准支持的不是很好,我们了解就行了.
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
临时重定向
302是临时重定向,临时重定向就是如果你想要升级一下你的软件,暂时把你的服务迁移到另外的一个地方,可是你有很多的用户,你不想让这些用户记住你的新的额IP地址,这里我们就可以用一个临时重定向.
void handlerHTTPRequest(int sock)
{
std::string response;
response = "HTTP/1.0 302 Temporarily Moved\r\n";
response += "Location: HTTPS://www.qq.com\r\n";
response += "\r\n";
send(sock, response.c_str(), response.size(), 0);
}
永久重定向
301是永久重定向,如果你想把你的功能完全迁移到另外一个IP地址,此时我们可以用永久重定向,注意我们的临时和永久这里不做区别,具体看要求吧.
void handlerHTTPRequest(int sock)
{
std::string response;
response = "HTTP/1.0 301 Temporarily Moved\r\n";
response += "Location: HTTPS://www.qq.com\r\n";
response += "\r\n";
send(sock, response.c_str(), response.size(), 0);
}
无状态
上面我们说HTTP是没有状态的,我们要求啥资源,HTTP就会给我们提供什么资源,不做记录,
Cookie
可是我们看到的好像不对啊,一般我们登录网站只需要一次密码,后面就可以直接登录了,这不就是把我们的状态记录了吗?首先我们先说明HTTP确实是没有状态的,但是浏览器会帮助我们做记录,这就是涉及到cookie机制.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HKdj1kbu-1679454666175)(HTTPS://qkj0302.oss-cn-beijing.aliyuncs.com/image-20230209194308864.png)]
浏览器在我们第一次访问一个网站时会把我们信息保存下来形成一个文件,等待下一次我们访问这个网站的时候浏览器会自动拿着这个文件去访问.
session
首先我想说上面我们cookie策略非常好,但是由于我们的HTTP是明文传送的,也就是我们的cookie文件也是明文的,里面我们的密码和账户都有可能被中间人拿到,这样我们的账户就被盗取了,因此我们cookie不安全,所以我们主流的浏览器现在采用的是cookie+session.这个策略很好理解,第一次我们登录一个网站时,服务端会在在自己的本地形成一个session文件,该文件的文件名是唯一的,同时会给浏览器返回一个session_id,我们浏览器的cookie文件中保存的就是这个id等到下一次我们访问同一个网站的时候这个就去拿到id去和服务端进行查找.
注意,上面的策略也有问题,中间人是可以拿到我们的id,但是就算是中间人拿到我们id,他只能登录我们的账号,不能修该我们的密码.注意cookice文件里信息里面不仅仅id,例如登录地址,我们会遇到如果我们换一个地区进行登录,一般会要求我们进行信心认证.
无连接的
再说一次,我们的HTTP是无连接.这里不久矛盾了吗?下面你还说是支持长连接,可能我没有表述好.说一下如果你的亲戚有钱,是你有钱吗?不是,最多用一下人家的能钱.这里也是如此,我们认为HTTP和tcp毫无关系,tcp是有链接的,这里的HTTP只是用下tcp的能力,仅此而已.
短连接
注意HTTP1.0版本的协议是短连接的,也就是请求一下,回应一下.由于我们每一次建立和断开连接都需要三次握手和四次挥手,这里有很大的问题,在早期我们的计算机使用人数较少,资源也比较少,采用短连接是可以的,但是现在我们看懂的网页是非常大的,一个完整的网页可能背后是无数次的HTTP请求,所以短连接效率有点低.因此1.1版本更新了,如果双发都同意长连接方案,那么就是长链接,其中我们header中存在一个Connection: keep-alive,如果我们不支持长连接,那么就是Connection: close.
HTTPS协议
下面我们继续往下说,我们的HTTP协议下的数据是明文传送的,也就是我们的数据在裸奔.请问对于我们一些无关紧要的数据明文还可以接受,那么对于一些机密文件我们绝对不能以明文的方式传送,那么我们就需要加密.所谓的加密我想大家都可以很好的理解,其中HTTPS协议就是帮助我们加密的.
HTTPS 也是⼀个应⽤层协议. 是在 HTTP 协议的基础上引⼊了⼀个加密层.HTTP 协议内容都是按照⽂本的⽅式明⽂传输的. 这就导致在传输过程中出现⼀些被篡改的情况
加密
我们说一下加密和解密的概念
- 加密就是把 明⽂ (要传输的信息)进⾏⼀系列变换, ⽣成 密⽂
- 解密就是把 密⽂ 再进⾏⼀系列变换, 还原成 明⽂
在这个加密和解密的过程中, 往往需要⼀个或者多个中间的数据, 辅助进⾏这个过程, 这样的数据称为 密钥
中间人问题
上面我们我们一直说中间人问题?那么请问什么是中间人问题?因为HTTP的内容是明⽂传输的,明⽂数据会经过路由器、wifi热点、通信服务运营商、代理服务 器等多个物理节点,如果信息在传输过程中被劫持,传输的内容就完全暴露了.劫持者还可以篡改传 输的信息且不被双⽅察觉,这就是 中间⼈攻击 ,所以我们才需要对信息进⾏加密.
加密方式
虾米嗯我们说一下常见的加密方式
- 对称加密 速度快,效率高
- 非对称加密 慢 (相对的)
- 数字指纹 不是加密的方式
数据摘要
数字指纹(数据摘要),其基本原理是利⽤单向散列函数(Hash函数)对信息进⾏运算,⽣成⼀串固定⻓度的数字摘要.数字指纹并不是⼀种加密机制,但可以⽤来判断数据有没有被窜改.
- 摘要常见算法:有MD5、SHA1、SHA256、SHA512等,算法把无限的映射成有限,因此可能会有碰撞(两个不同的信息,算出的摘要相同,但是概率非常低)
- 摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推原信息,通常用来进行数据对比
例如我们百度云盘在上传文件的时候,有可能出现妙传,这是由于我们百度网盘在传输文件的时候去先把我们文件进行数字摘要,看看生成的哈希值时候已经存在了,如果存在了就会把这个资源连接放在我们账户上面,这就是我们为何出现了秒传的情况.
对称加密
采⽤单钥密码系统的加密⽅法,同⼀个密钥可以同时⽤作信息的加密和解密,这种加密⽅法称为对称加密,也称为单密钥加密,特征:加密和解密所⽤的密钥是相同的.
- 常见对称加密算法(了解):DES、3DES、AES、TDEA、Blowfish、RC2等
- 特点:算法公开、计算量小、加密速度快、加密效率高
非对称加密
简单说就是有两把密钥,一把叫做公钥、一把叫私钥,用公钥加密的内容必须用私钥才能解开,同样,私钥加密的内容只有公钥能解开.
- 常见非对称加密算法(了解):RSA,DSA,ECDSA
- 特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快
注意公钥和私钥是配对的,可以相互匹配使用
- 通过公钥对明文加密, 变成密文
- 通过私钥对密文解密, 变成明文
也可以反着用
- 通过私钥对明文加密, 变成密文
- 通过公钥对密文解密, 变成明文
HTTPS 的工作过程探究
现在我们已经知道了加密的方式,我们需要探究一下我们HTTPS采用的是什么加密的方式,这样有有助于我们理解HTTPS.
方案一 对称加密
如果通信双方都各自持有同一个密钥X,且没有别人知道,这两方的通信安全当然是可以被保证的(除非密钥被破解)
引入对称加密之后, 即使数据被截获, 由于黑客不知道密钥是啥, 因此就无法进行解密, 也就不知道请求的真实内容是啥了.但事情没这么简单. 服务器同一时刻其实是给很多客户端提供服务的. 这么多客户端, 每个人用的秘钥都必须是不同的(如果是相同那密钥就太容易扩散了, 黑客就也能拿到了). 因此服务器就需要维护每个客户端和每个密钥之间的关联关系, 这也是个很麻烦的事情.因此比较理想的做法, 就是能在客户端和服务器建立连接的时候, 双方协商确定这次的密钥是啥.那么这里就会出现问题,我们再协商密钥的时候是明文传送,此时中间人就会拿到我们密钥,我们数据就不安全了
方案二: 一方非对称加密
鉴于非对称加密的机制,如果服务器先把公钥以明文方式传输给浏览器,之后浏览器向服务器传数据前都先用这个公钥加密好再传,从客户端到服务器信道似乎是安全的(有安全问题),因为只有服务器有相应的私钥能解开公钥加密的数据.但是服务器到浏览器的这条路怎么保障安全?如果服务器用它的私钥加密数据传给浏览器,那么浏览器用公钥可以解密它,而这个公钥是一开始通过明文传输给浏览器的,若这个公钥被中间人劫持到了,那他也能用该公钥解密服务器传来的信息了.
方案三 双方非对称加密
注意我们加上一组非对称密钥就可以保证一条路的安全,例如如果我有两组公钥私钥,那我就可以保证双向数据传输的安全了呢?
某网站服务器拥有公钥A与对应的私钥A’;浏览器拥有公钥B与对应的私钥B’.浏览器把公钥B明文传输给服务器.
服务器把公钥A明文给传输浏览器.之后浏览器向服务器传输的内容都用公钥A加密,服务器收到后用私钥A’解密.由于只有服务器拥有私钥A’,所以能保证这条数据的安全.同理,服务器向浏览器传输的内容都用公钥B加密,浏览器收到后用私钥B’解密.同上也可以保证这条数据的安全.好像成功了!但是这个方法并没有被大范围推广,并且也不可能被大范围推广.很重要的原因是非对称加密算法非常耗时,而对称加密快很多.
方案四 对称+非对称加密
某网站拥有用于非对称加密的公钥A、私钥A’.浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器.浏览器随机生成一个用于对称加密的密钥X,用公钥A加密后传给服务器.服务器拿到后用私钥A’解密得到密钥X.这样双方就都拥有密钥X了,且别人无法知道它.之后双方所有数据都通过密钥X加密解密即可.成功!HTTPS基本就是采用了这种方案.但是这并不是完美的,这仍然有漏洞喔!
中间人却完全不需要拿到私钥A’就能干坏事了,某网站有用于非对称加密的公钥A、私钥A’.浏览器向网站服务器请求,服务器把公钥A明文给传输浏览器.中间人劫持到公钥A,保存下来,把数据包中的公钥A替换成自己伪造的公钥B(它当然也拥有公钥B对应的私钥B’).浏览器生成一个用于对称加密的密钥X,用公钥B(浏览器以为是公钥A)加密后传给服务器.中间人劫持后用私钥B’解密得到密钥X,再用公钥A加密后传给服务器.服务器拿到后用私钥A’解密得到密钥X.这样在双方都不会发现异常的情况下,中间人通过一套“狸猫换太子”的操作,掉包了服务器传来的公钥,进而得到了密钥X.根本原因是浏览器无法确认收到的公钥是不是网站自己的,因为公钥本身是明文传输的.所以!我们只剩下最后一个问题,那就是怎么确保浏览器收到的公钥是网站的,而不是中间人的!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tOCu2iVJ-1679454666178)(https://qkj0302.oss-cn-beijing.aliyuncs.com/20230321_211421_3.gif)]
方案五 非对称加密 + 对称加密 + 证书认证
下面我们正式说一下HTTP采用的方式,这个是非常优秀的.
数字证书
下面我们需要引出数字证书,这个可以保证我们浏览器拿到的公钥是网站的,这里涉及到到数据摘要.
网站在使用HTTPS前,需要向CA机构申领一份数字证书,数字证书里含有证书持有者信息、公钥信息等.服务器把证书传输给浏览器,浏览器从证书里获取公钥就行了,证书就如身份证,证明“该公钥对应该网站”.只要我们的客户端拿到了证书,我们就可以判断公钥的合法性.
下面我说一下我们的流程服务端把公钥给CA机构,让后把私钥保存好,这样经过CA机构我们就得到了一个数字证书.
数字签名
下面我们继续,我们在浏览器访问服务端的时候我们把这个证书给浏览器就可以了,那么这里又有一个显而易见的问题,“证书本身的传输过程中,如何防止被篡改”?即如何证明证书本身的真实性?身份证运用了一些防伪技术,而数字证书怎么防伪呢?解决这个问题我们就真的接近胜利了!如何防止数字证书被篡改?
很简单,当服务端申请CA证书的时候,CA机构会对该服务端进行审核,并专门为该网站形成数字签名,过程如下:
- CA机构拥有非对称加密的私钥A和公钥A’
- CA机构对服务端申请的证书明文数据进行hash,形成数据摘要
- 然后对数据摘要用CA私钥A’加密,得到数字签名S,服务端申请的证书明文和数字签名S 共同组成了数字证书,这样一份数字证书就可以颁发给服务端了
在客户端和服务器刚一建立连接的时候, 服务器给客户端返回一个 证书,证书包含了之前服务端的公钥, 也包含了网站的身份信息
当客户端获取到这个证书之后, 会对证书进行校验(防止证书是伪造的).
- 判定证书的有效期是否过期
- 判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构).
- 验证证书是否被篡改: 从系统中拿到该证书发布机构的公钥, 对签名解密, 得到一个 hash 值(称为数据摘要), 设为 hash1. 然后计算整个证书的 hash 值, 设为 hash2. 对比 hash1 和 hash2 是否相等. 如果相等, 则说明证书是没有被篡改过的.
下面我们说两个问题,补充我们理解.
中间人有没有可能篡改该证书?
由于他没有CA机构的私钥,所以⽆法hash之后⽤私钥加密形成签名,那么也就没法办法对篡改后的证书形成匹配的签名,如果强⾏篡改,客⼾端收到该证书后会发现明⽂和签名解密后的值不⼀致,则说明证书已被篡改,证书不可信,从⽽终⽌向服务器传输信息,防⽌信息泄露给中间⼈ .
那么中间人整个掉包证书呢?
因为中间⼈没有CA私钥,所以⽆法制作假的证书,所以中间⼈只能向CA申请真证书,然后⽤⾃⼰申请的证书进⾏掉包.这个确实能做到证书的整体掉包,但是别忘记,证书明⽂中包含了域名等服务端认证信息,如果整体掉包,客⼾端依旧能够识别出来.永远记住:中间⼈没有CA私钥,所以对任何证书都⽆法进⾏合法修改,包括⾃⼰的 .
如何成为中间人
- ARP欺骗:在局域网中,hacker经过收到ARP Request广播包,能够偷听到其它节点的 (IP, MAC)地址.例, 黑客收到两个主机A, B的地址,告诉B (受害者) ,自己是A,使得B在发送给A 的数据包都被黑客截取
- ICMP攻击:由于ICMP协议中有重定向的报文类型,那么我们就可以伪造一个ICMP信息然后发送给局域网中的客户端,并伪装自己是一个更好的路由通路.从而导致目标所有的上网流量都会发送到我们指定的接口上,达到和ARP欺骗同样的效果
- 假wifi && 假网站等