“like you do when you lie and I know it’s my imagination” @_zing_photograph
1. 认识协议
一条QQ消息经历了如下过程:结构化数据 → 长“字符串” → 结构化数据
struct message
{
昵称:小边小边不秃头
头像:我愚蠢的理想主义.png
消息:GN ST
时间:2022-08-26 15:02:55
}msg;
为什么要进行序列化和反序列化?
- 为了应用层网络通信的方便。因为这种结构化的数据是不便于网络传输的,而字符串便于网络传输。
- 这种结构化的数据方便上层使用内部成员,例如图片界面显示。将应用和网络进行解耦。
之前我们做的udp、tcp通信,并没有进行任何序列化和反序列化,只是因为我们没去定义结构化数据。
事实上,这个结构化数据就是协议的表现。
怎么做到呢?我们可以自己造轮子,但其实真挺麻烦的;今天我们用轮子,就是用别人写好的组件(xml/json/protobuff)
网络计算器
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案
客户端发送一个形如1+1的结构体; 这个字符串中有两个操作数, 都是整形; 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格; 定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体…
订制协议
目前就是订制数据化结构的过程
1. 原生版本
没有明显的序列化/反序列化的过程的版本
Protocol.hpp
#pragma once
#include<iostream>
#include<string>
using namespace std;
typedef struct request
{
int x;
int y;
char op;
}request_t;
typedef struct response
{
int code;
int result;
}response_t;
Sock.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstdlib>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cerr << "socket error" << 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 error" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if(listen(sock, 5) < 0)
{
cerr << "listen error" << 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;
}
else
{
return -1;
}
}
static void Connect(int sock, string ip, uint16_t port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
CalServer.cc
#include"Protocol.hpp"
#include"Sock.hpp"
#include<pthread.h>
static void Usage(string proc)
{
cout << "Usage: " << proc << " port" << endl;
exit(1);
}
void* HandlerRequest(void* args)
{
int sock = *(int*)args;
delete (int*)args;
pthread_detach(pthread_self());
request_t req;
ssize_t s = read(sock, &req, sizeof(req));
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 << req.x << req.op << req.y << endl;
write(sock, &resp, sizeof(resp));
cout << "服务结束" << endl;
}
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"
void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
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;
}
Makefile
.PHONY:all
all:CalClient CalServer
CalClient:CalClient.cc
g++ -o $@ $^ -std=c++11
CalServer:CalServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf CalClient CalServer
我们自己定的协议,client 和 server都必须遵守,这就叫做自定义协议 ——
但这个方案很cuo
2. 序列化反序列化版本
理解json.cpp
安装,本质上是在/usr/include/jsoncpp/json 中添加头文件,在/usr/lib64/ 下添加libjson库 ——
sudo yum install -y jsoncpp-devel
编译时,需要带-ljsoncpp ~
仅仅是了解,我们目的是解释序列化和反序列化。
Protocal.hpp
#pragma once
#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>
using namespace std;
//订制协议
//请求格式
typedef struct request
{
int x;
int y;
char op; //"+-*/%"
}request_t;
//响应格式
typedef struct response
{
int code; //server运算完毕的计算状态:code(0:success) code(-1:div 0)...
int result; //计算结果,你能否区分是正常的计算结果 还是 异常的退出结果
}response_t;
//序列化 - 结构化数据→字符串
std::string SerializeRequest(const request_t& req)
{
Json::Value root; //万金油对象,可以橙装任何对象,json是一种kv式的序列化方案
// 盛装
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
//FastWriter, StyledWriter
// Json::StyledWriter writer;
Json::FastWriter writer;
//返回值为string,即序列化后的结果
return writer.write(root);
}
//反序列化 - 字符串→结构化数据
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"].asUInt();
}
//序列化
std::string SerializeReponse(const response_t& resp)
{
Json::Value root; //万金油对象,可以橙装任何对象,json是一种kv式的序列化方案
// 盛装
root["code"] = resp.code;
root["result"] = resp.result;
//FastWriter, StyledWriter
// Json::StyledWriter writer;
Json::FastWriter writer;
//返回值为string,即序列化后的结果
return writer.write(root);
}
//反序列化
void DeserializeReponse(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 <pthread.h>
static void Usage(string proc)
{
cout << "Usage: " << 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;
std::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 << req.x << req.op << req.y << endl;
std::string send_string = SerializeReponse(resp);
write(sock, send_string.c_str(), send_string.size());
cout << "服务结束" << send_string << endl;
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"
void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}
// ./CalClient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
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());
// ssize_t s = write(sock, &req, sizeof(req));
char buffer[1024];
s = read(sock, buffer,sizeof(buffer)-1);
if(s > 0)
{
response_t resp;
buffer[s] = 0;
std::string str = buffer;
//反序列化响应 至resp
DeserializeReponse(str, resp);
cout << "code[0:success]: " << resp.code << endl;
cout << "result: " << resp.result << endl;
}
return 0;
}
Makefile
.PHONY:all
all:CalClient CalServer
CalClient:CalClient.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp
CalServer:CalServer.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:
rm -rf CalClient CalServer
小总结
如上重点在于演示在网络中是互发字符串 ——
我们所写的cs模式的在线版本服务器,本质就是一个应用层网络服务 —— 基本通信代码;序列和反序列化(借助组件);业务逻辑是我们自己定的、请求结果格式 & code含义等约定是我们自己做的。
我们干的这些事儿,就完美对应了五层协议中的应用层 ——
2. http
虽然说, 上面的应用层协议是我们自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一. 但本质上和我们刚刚写的网络计算器没有区别,都是应用层协议
在http内部都已实现。
2.1 认识url - 网址
我们请求的图片、html、css、jx、视频、音频、标签、文档等这些都称之为“资源”
我们可以用IP+Port确认一个进程,但是无法唯一确认一个资源,公网IP地址是唯一一台主机的,而网络“资源”是存在于网络中的一台Linux机器上。Linux或者传统的操作系统,都是以文件的方式保存资源的。单Linux系统,表示一个唯一资源的方式是通过路径的。
所以,IP + Linux路径,就可以唯一的确认一个网络资源。
-
IP通常是以域名方式呈现的 -
路径可以通过目录名+/确认
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了,因此这些字符不能随意出现.
如果某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义 —— 转义的规则如下
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
encode 编码:
还需要decode 解码。
2.2 http格式
简化认识:
如何理解普通用户的上网行为 —— IO
思考 —— http request 和 http response被如何看待~
我们可以把请求/响应看做一个大的字符串:
我们接下来的demo中用recv来读,这种读法是不正确的,只不过现在没有被暴露出来罢了。。
我们需要保证 ——
- 每次读取都是读取完整的一个http request
- 如何判定我们将报头部分读完了呢?读到空行,分离报头和有效载荷(解包)。就能提取报头中的各种属性,包括**
Content-Length **,自描述字段。 - 决定后面还有没有正文?这和请求方法有关(我们接下来会验证);如果有正文,如何保证把正文全部读取完成呢? 上面那个字段表明正文部分有多少字节,帮助我们完整的读取http请求/响应。
- 会有不存在
Content-Length 的情况,就是没有正文 - 如上所说,是规定、协议!
- 每次读取都不要将下一个http请求的一部分读到(后面详谈)
HTTP请求
-
首行:[方法] + [url] + [版本] -
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束 -
Body:空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length 属性来标识Body的长度;
HTTP响应
-
首行: [版本号] + [状态码] + [状态码描述] -
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束 -
Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.
2.3 http的操作
为了方便测试,我们引入这两个为tcp订制的接口,面向字节流读取的函数 ——
#include <sys/types.h>
#include <sys/socket.h>
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);
对比read/write函数,它们几乎没有差别,我们也不关心flag 默认为0
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
http.cc
#include"Sock.hpp"
#include<pthread.h>
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, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer;
std::string http_response = "http/1.0 200 OK\n";
http_response +="Content-Type: text/plain\n";
http_response += "\n";
http_response += "hello beatles";
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)
{
std::cout << "a new link" << std::endl;
pthread_t tid;
int* parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
return 0;
}
这样就实实在在的看到了请求报头 ——
2.4 http的方法
早期http版本是短链接,一个请求,一次响应,close socket,一个请求一般就是请求一个资源,链接自动关闭。
大部分服务器为了安全只会暴露 GET,POST,HEAD方法
http请求中的/ 不是根目录,而叫做web根目录
准备工作 ——
http.cc
-
Content-Type 类型:正文类型 -
Content-Length :获取文件大小,我们今天用stat获取文件特定属性 - 大小 #include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
struct stat {
dev_t st_dev;
ino_t st_ino;
mode_t st_mode;
nlink_t st_nlink;
uid_t st_uid;
gid_t st_gid;
dev_t st_rdev;
off_t st_size;
blksize_t st_blksize;
blkcnt_t st_blocks;
time_t st_atime;
time_t st_mtime;
time_t st_ctime;
};
http.cc
#include"Sock.hpp"
#include<pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include<fstream>
#define WWWPORT "./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, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer;
std::string html_file = WWWPORT;
html_file += HOME_PAGE;
struct stat st;
stat(html_file.c_str(), &st);
std::string http_response = "http/1.0 200 OK\n";
http_response +="Content-Type: text/html; charset=utf8\n";
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);
http_response += "\n";
http_response += "\n";
std::ifstream in(html_file);
if(!in.is_open())
{
std::cerr << "open html error" << std::endl;
}
else
{
std::string content;
std::string line;
while(std::getline(in, line))
{
content += line;
}
http_response += content;
in.close();
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* parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
return 0;
}
Makefile 同上。如上只是把我们简陋的字符串版本改成了文件版。
wwwroot/index.html
- wwwroot称为web根目录,wwwroot下放置的内容都叫做资源
- wwwroot目录下的index.html就叫做网站的首页
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h3>小边's Home</h3>
</body>
</html>
接下来,就要验证GET和POST方法啦~
2.4.1 GET
HTML 教程 (w3school.com.cn)
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>long time no see!! I am missing you~</h5>
<h5>我是表单!您是谁</h5>
<!-- /a/b/handler_form 这个路径并不存在,我们目前也不处理,因为我们今天的重点在GET -->
<form action="/a/b/handler_form" method="GET">
姓名: <input type="text" name="name">
密码: <input type="password" name="password">
<input type="submit" value="提交">
</form>
</body>
</html>
会把表单内容拼接到url后面,这样前端的数据就被后端C++程序拿到了 ——
2.4.2 POST
index.html
只需将表单中的GET方法改为POST方法 ——
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>long time no see!! I missed you~</h5>
<h5>我是表单!您是谁</h5>
<!-- /a/b/handler_form 这个路径并不存在,我们目前也不处理,因为我们今天的重点在GET -->
<form action="/a/b/handler_form" method="POST">
姓名: <input type="text" name="name">
密码: <input type="password" name="password">
<input type="submit" value="提交">
</form>
</body>
</html>
2.4.3 小总结
概念
GET :获取方法,一般所有的网页默认的都是GET方法,但是GET也能提交参数 - 通过url参数拼接从而提交给server端。POST :推送方法,以比较常用的参数提交方法,但是一般是通过正文部分提交的,Content-Length 表示参数的长度。
区别
参数提交的位置不同
GET :GET方法不私密,会回显到浏览器url输入框,增加了被盗取的风险。GET通过url传参,而url是有大小限制的,这和具体的浏览器有关。POST :POST方法比较私密(!=安全),因为不会回显到浏览器url输入框。POST由正文部分传参,一般大小没有限制。
如何选择
GET :提交的参数不敏感,数量非常少;否则用POST
http协议处理,本质是文本分析 ——
- http协议本身的字段
- 提取参数,如果有的话。GET和POST其实是前后端交互的一个重要方式。
2.5 http的状态码
应用层是人要参与的,这里的人“水平”参差不齐,很多人根本就不清楚如何使用http的状态码,又因为浏览器种类太多,导致大家对状态码的支持比较混乱。这样类似于404 的状态码,对浏览器没什么指导意义,它就是“正常”显示网页。
1XX:100, 101表示请求正在被处理,服务器收到你的请求,但是处理你的请求需要很多时间,给你返回一个响应 2XX:200请求正常处理完毕 - OK,我们的刚刚自己构建的就是200 3XX:重定向。有301, 302, 303, 307, 308不同的浏览器,不同的版本对于这种重定向的处理机制是不一样的,我们接下来会测试 4XX:403 (Forbidden) 禁止访问;404 (Not Found)你访问的资源不存在,属于客户端问题. 5XX:服务器错误。比如来了一个请求,创建线程或者进程失败;处理请求时,做字符串分析时出现问题,程序崩溃。比如,500, 503,504(Bad Gateway)
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>
#define WWWPORT "./wwwroot/"
#define HOME_PAGE "index.html-bak"
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, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer;
std::string html_file = WWWPORT;
html_file += HOME_PAGE;
std::ifstream in(html_file);
if (!in.is_open())
{
std::cerr << "open html error" << std::endl;
std::string http_response = "http/1.0 404 not Found\n";
http_response += "Content-Type: text/html; charset=utf8\n";
http_response += "\n";
http_response += "<html><p>你访问的资源走丢了~~</p></html>";
send(sock, http_response.c_str(), http_response.size(), 0);
}
else
{
struct stat st;
stat(html_file.c_str(), &st);
std::string http_response = "http/1.0 200 ok\n";
http_response += "Content-Type: text/html; charset=utf8\n";
http_response += "Content-Length: ";
http_response += std::to_string(st.st_size);
http_response += "\n";
http_response += "\n";
std::string content;
std::string line;
while (std::getline(in, line))
{
content += line;
}
http_response += content;
in.close();
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 *parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
return 0;
}
我们重点来谈3xx 的状态码 —— 重定向
有时,我们访问某一个网站,可能会跳转到另一个网址:比如当我访问某种资源时,提示我登陆,于是我便跳转到了登录页面;当我输完密码,会自动再跳转回来。这种现象,都叫做重定向。
所谓永久和临时,永久重定向通常用于网站搬迁、域名更换;临时重定向,每次都要经历跳转,属于业务环节。
模拟301
注意,重定向时需要浏览器给我们提供支持的:必须能识别301/302/307,server要告诉浏览器(客户端),接下来应该去哪里?
报头属性Location :新的地址
http.cc
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>
#define WWWPORT "./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, sizeof(buffer));
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if(s > 0)
{
buffer[s] = 0;
std::cout << buffer;
std::string response = "http/1.1 301 Permantly moved\n";
response += "Location: https://new.qq.com/\n";
response += "\n";
send(sock, response.c_str(), 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 *parm = new int(sock);
pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
}
}
return 0;
}
于是我们自动跳转到了腾讯网 ——
2.6 http的常见header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- Referer: 当前页面是从哪个页面跳转过来的;
- Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
我们之前所有的实验,全部都是请求→响应→断开连接 —— http/1.0采用的网络请求方案是短链接,当我们访问一个由多个元素构成的一个大型的网页资源时,就要发起多次http请求(基于短链接),http协议是基于tcp协议的,所以每一次的http request都要执行 建立链接→ 传送数据→断开连接,但是这样效率较低。
这样http/1.1后支持长链接 ,需要设置Connection 属性:keep-alive,通过减少频繁建立tcp链接,来达到提高效率的目的。
2.7 cookie 和 session
我们有这样的经验,我们网站中,各种页面跳转时,本质就是进行各种http请求,网站照样认识我。但是http协议本身是一种无状态的协议(因为简单),并不记录请求的历史信息。这样看似是矛盾的,但是“让网站认识我”并不是http协议本身要解决的问题,而是做网络资源获取,但是http可以提供一些技术支持来保证网站具有“会话保持”的功能 —— cookie & session。
cookie
会话保持
- 浏览器角度:cookie其实是一个文件,该文件保存的是我们用户的私密信息
- http协议角度:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在request中携带该cookie信息
这个cookie就是在浏览器(客户端)中,首次登陆时浏览器会自动保存登录相关信息到cookie文件,后续的请求,浏览器会将每一个请求都会请求报头属性中,自动携带对应的cookie。
cookie有两种形式 ——
如果别人盗取了我的cookie文件,他就可以以我的身份进行认证,访问特定资源如果保存的是用户名和密码,那就很糟糕了~~
所以单纯使用cookie是有安全隐患的 —— session,但是不代表我们不用cookie~
session
将用户的私密信息,保存在服务端。
后序所有的http请求,都会由浏览器自动携带cookie内容 —— 当前用户的session_id,server依旧可以做到认识client,这也是一种会话保持的功能。
由于客户端的cookie文件中,不再直接保存用户私密信息,就不会直接泄漏啦~ 但是的确还有cookie文件被泄漏的可能,如果别人也去访问我们对应的网址,还是会去访问我们对应的网址,但我们也没办法啦~ 但是也衍生了很多的防御方案 —— 比如异地登录重新生成session_id、短信认证等等
cookie+session本质是提高用户访问网站或者平台的体验。
3. https (了解)
3.1 “加密”
http = http +TLS/SSL (数据的加密解密层)
加密方式
如何识别/防止文本中的内容被篡改?
回忆起了上学期一门写字儿就给分儿的课 —— 信息安全概论,第几章来着,密码学。。幸好逃离了。。言晨而应该懂这个,不过好像挺难?!
秘钥协商,采用对称的方式是有安全隐患的!
3.2 https通信过程
如何选择加密算法?
看起来有两对非对称秘钥,就能保证数据双向的安全,但事实并非如此 ——
实际上,采取的是非对称 + 对称方案 ——
-
用非对称的方式交换对称秘钥 -
用对称方案进行数据通信
事实上,第一次把公钥S给client会不会出现问题呢?
这样以来,中间人就拿到了就拿到了接下来用于通信的对称秘钥的私钥X . 然而client无法判断秘钥协商报文是不是从合法的服务方发来的。
那怎么办呢?于是有CA证书机构(Certificate Authority) —— 只有一个服务商经过权威机构认证,该机构才合法。
- 申请证书:提供企业信息;域名;公钥
- 创建证书:企业基本信息(域名,公钥);由这段文本(hash散列形成数据指纹)形成的数据签名
- 要求client必须知道CA机构的公钥信息用来解密数字签名。那它如何得知呢?这个一般是内置的。另外,证书颁发具有“传递性”,所以也有一部分在访问网址是,浏览器会提示用户进行安装
因为CA的公钥是全世界众所周知的,但是CA的私钥只有CA自己知道,换言之,只有CA机构能重新形成对应的数字签名,因此即便中间人可以改内容,但,无法更改数字签名,因为他没有CA机构的私钥,用公钥解密再更改也无法再加密。
那如果中间人也是一个合法的服务方呢。。不行~ 因为基本信息中有“域名”,你请求的域名和“合法中间人”的域名那一定是会变化而被察觉到的。。
今天才知道证书是这么回事儿啊~~
|