目录
- 实现网络版本计算器
- 自己定协议实现
- 用json协议实现
- 重谈OSI七层模型
- HTTP协议
- 域名介绍
- url介绍
- HTTP请求和响应
- 实现一个简易的HTTP服务器
- 实现简易Http服务器初级版
- 实现简易Http服务器中级版
- 实现一个简易的HTTP服务器最终版
- 请求方法
- HTTP状态码
- HTTP常见的Header
实现网络版本计算器
tcp通信时是全双工的,意思就是我发送消息的时候也可以接收消息,这两个动作可以同时进行
如何做到的呢?因为tcp有两个缓冲区,一个是接收缓冲区和发送缓冲区,这两个缓冲区是独立的,资源是独立的,所以不会相互影响
因为tcp叫传输层控制协议,所以什么时候发?发多少?发送出错了怎么办?全部都由Tcp自己解决。如果对端一直不读取,tcp是面向数据流,对方接收缓冲区就会多个数据粘在一起,如果发送端发现对方接收缓冲区的空间不多了,则发送端可能会将完整的数据分段,只发送一段过去。所以在读端将一流数据读取上来后,如何处理呢?所以就需要有应用层协议的定制
现在我们实现网络版本计算器(
+-*/%
),我们需要客户端把要计算的表达式发过去,然后由服务器进行计算,最后再把结果返回给客户端
用自己的协议实现
我们定协议(协议就是约定):
约定一:客户端发送一个形如“1 + 1”的字符串,数字和运算符之间有一个空格
约定二:定义结构体来表示我们需要交互的信息。
发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符转回结构体----这个过程叫做“序列化”和“反序列化”
请求协议:
struct resquest
{
int x;
int y;
char op;//运算符
}
应答协议
struct response
{
int result;//结果
int code;//结果是否可靠
}
每一个字段都是双方约定好的
问题:为什么发送的时候不直接发送结构体?为什么要将结构体转为字符串进行发送?因为同一个结构体在不同的编译器下编译结果体大小是可能会不一样的。默认对齐数不同,这就会导致结构体的大小不一样
在qq群里发送信息时,不止有发送的内容,还有头像,昵称,时间,是一个一个发送还是打个包一起发送呢?如果是一个一个发送,那接收端都不知道哪些是来自同一个发送端的。所以实际上是把三个字符串转换成一个字符串,收到后再把一个字符串解析成三个字符串
网络版本计算器代码逻辑链
Socket.hpp文件
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"Log lg;
class Socket
{
public:Socket(){}~Socket(){}void CreateSocket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){lg(Fatal, "create socket failed:%s", strerror(errno));exit(1);}}void Bind(uint16_t serverPort){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(serverPort);server.sin_addr.s_addr = INADDR_ANY;int n = bind(_sockfd, (struct sockaddr*)&server, sizeof(server));if (n < 0){lg(Fatal, "bind socket failed:%s", strerror(errno));exit(2);}}void Listen(){int n = listen(_sockfd, 5);if (n < 0){lg(Fatal, "listen socket failed:%s", strerror(errno));exit(3);}}int Accept(string* clientIp, uint16_t* clinetPort){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newsocket = accept(_sockfd, (struct sockaddr*)&peer, &len);if (newsocket < 0){lg(Error, "accept error:%s", strerror(errno));return -1;}*clientIp = inet_ntoa(peer.sin_addr);*clinetPort = ntohs(peer.sin_port);return newsocket;}int Connect(const string& serverIp, uint16_t serverPort){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverIp.c_str());server.sin_port = htons(serverPort);int n = connect(_sockfd, (struct sockaddr*)&server, sizeof(server));if (n < 0){lg(Error, "connect error:%s", strerror(errno));return -1;}return 0;}void Close(){close(_sockfd);}int Fd(){return _sockfd;}
private:int _sockfd;
};
Protocol.hpp文件
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>using namespace std;const string blank_space_sep = " ";
const string protocol_sep = "\n";
//添加报头:"内容" 转为 "长度\n内容\n"
bool Encode(string *package, const string& content)
{package->clear();*package += to_string(content.size());*package += protocol_sep;*package += content;*package += protocol_sep;return true;
}
//去除报头:"长度\n内容\n" 转为 "内容"
bool Decode(string &package, string *content)
{content->clear();int pos = package.find(protocol_sep);if (pos == string::npos) return false;int len = stoi(package.substr(0, pos));int totalLen = pos + len + 2;if (package.size() < totalLen) return false;//如果报文不完整,说明*content = package.substr(pos + protocol_sep.size(), len);package.erase(0, totalLen);return true;
}//提取字符串
bool Extract(const string& in, int& x, int& y, char& oper)
{int pos = in.find(blank_space_sep);if (pos == string::npos) return false;x = stoi(in.substr(0, pos));oper = *in.substr(pos + blank_space_sep.size(), 1).c_str();int pos2 = in.rfind(blank_space_sep);if (pos2 == string::npos) return false;y = stoi(in.substr(pos2 + blank_space_sep.size()));return true;
}//请求协议
class Request
{
public:Request(int x = 0, int y = 0, char oper = '+'):_x(x), _y(y), _oper(oper){}//序列化 -- 将结构体转为"_x + _y"bool Serialize(string* out){out->clear();*out += to_string(_x);*out += blank_space_sep;*out += _oper;*out += blank_space_sep;*out += to_string(_y);return true;}//反序列化 -- 将"_x + _y"转为结构体bool Deserialize(const string& in){return Extract(in, _x, _y, _oper);}
public:int _x;int _y;char _oper;
};//响应协议
class Response
{const string blank_space_sep = " ";
public:Response(int result = 0, int code = 0):_result(), _code(code){}//序列化 -- 将结构体转为"_result _code"bool Serialize(string* out){*out = to_string(_result) + blank_space_sep + to_string(_code);return true;}//反序列化 -- 将"_result _code"转为结构体bool Deserialize(const string& in){int pos = in.find(blank_space_sep);if (pos == string::npos) return false;_result = stoi(in.substr(0, pos));_code = stoi(in.substr(pos + blank_space_sep.size()));return true;}
public:int _result;int _code; // 0,可信,否则!0具体是几,表明对应的错误原因
};
ServerCal.hpp文件
#include "Protocol.hpp"//服务器的计算服务
class ServerCal
{
public:ServerCal(){}Response CalculatorHelper(const Request &req){int x = req._x;char oper = req._oper;int y = req._y;Response rsp(0, 0);switch (oper){case '+':{rsp._result = x + y;rsp._code = 0;break;}case '-':{rsp._result = x - y;rsp._code = 0;break;}case '*':{rsp._result = x * y;rsp._code = 0;break;}case '/':{if (y == 0){rsp._result = 0;rsp._code = -1;break;}rsp._result = x / y;rsp._code = 0;break;}case '%':{if (y == 0){rsp._result = 0;rsp._code = -1;break;}rsp._result = x % y;rsp._code = 0;break;}default:break;}return rsp;}string Calculator(string& s){string content;if(!Decode(s, &content))//将"长度/n内容/n" -> "内容"return "";Request rq;rq.Deserialize(content);//将"内容"反序列化Response rsp = CalculatorHelper(rq);//得到答案内容content.clear();rsp.Serialize(&content);//序列化答案内容string ret;Encode(&ret, content);//将"内容" -> "长度/n内容/n"return ret;}
};
TcpServer.hpp文件
#include <iostream>
#include <unistd.h>
#include <functional>
#include <signal.h>
#include "Socket.hpp"using namespace std;#define defaultIp "0.0.0.0"class TcpServer
{using func_t = function<string(string&)>;
public:TcpServer(const uint16_t serverPort, const string& serverIp = defaultIp):_serverIp(serverIp),_serverPort(serverPort){}void Init()//初始化{_listensock.CreateSocket();_listensock.Bind(_serverPort);_listensock.Listen();}void Start(func_t func){signal(SIGCHLD, SIG_IGN);//父进程不再关系子进程是否退出,即父进程不管子进程也不会资源泄漏_func = func;while (1){string clientIp;uint16_t clienPort;int socket = _listensock.Accept(&clientIp, &clienPort);if (fork() == 0){_listensock.Close();string streamBuffer;//Tcp发送完整的报文的时候可能只发送一部分,为了保证获得一个完整的报文,我们就可以这样处理while (1){char readBuffer[4096];int n = read(socket, readBuffer, sizeof(readBuffer) - 1);if (n > 0){readBuffer[n] = 0;streamBuffer += readBuffer;while (1){string ret = _func(streamBuffer);//服务器对客户端的字符串进行处理if (ret.empty())//说明没有要处理的报文break;write(socket, ret.c_str(), ret.size());}}else if (n == 0){lg(Info, "[clientIp:%s, clientPort:%d] closed", clientIp.c_str(), clienPort);break;}else{lg(Warning, "server read failed:%s", strerror(errno));break;}}exit(0);}close(socket);}}
private:Socket _listensock; //监听套接字string _serverIp; //服务器Ip地址,一般设置为全0uint16_t _serverPort;//服务器端口号func_t _func; //回调函数
};
TcpServer.cc文件
#include "TcpServer.hpp"
#include "ServerCal.hpp"//使用手册
static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}// ./TcpServer 8080
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::atoi(argv[1]);TcpServer *tsvp = new TcpServer(port);tsvp->Init();ServerCal cal;tsvp->Start(bind(&ServerCal::Calculator, &cal, std::placeholders::_1));//C++的bind函数return 0;
}
TcpClient.cc文件
#include "Protocol.hpp"
#include "Socket.hpp"static void Usage(const std::string &proc)
{std::cout << "\nUsage: " << proc << "\tserverIp\tport\n" << std::endl;
}
int main(int argc, char* argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverIp = argv[1];uint16_t serverPort = atoi(argv[2]);Socket skt;skt.CreateSocket();skt.Connect(serverIp, serverPort);string streamBuffer;while (1){cout << "Enter#";fflush(stdout);string content;getline(cin, content);//提取字符串,得到”请求反序列化“Request rq;Extract(content, rq._x, rq._y, rq._oper);string s;rq.Serialize(&s);string ret;Encode(&ret, s);write(skt.Fd(), ret.c_str(), ret.size());char readBuffer[4096];int n = read(skt.Fd(), readBuffer, sizeof(readBuffer) - 1);if (n > 0){readBuffer[n] = 0;streamBuffer += readBuffer;string ret;Decode(streamBuffer, &ret);Response rsp;rsp.Deserialize(ret);cout << "code: " << rsp._code << " result: " << rsp._result << endl;}else if (n == 0){cerr << "Server closed..." << endl;break;}else {cerr << "read failed:" << strerror(errno) << endl;exit(3);}}return 0;
}
运行结果
用json实现
我们也可以看到,如果自己定协议的话,还要序列化和反序列化,写起来太麻烦,这是不合理的,所以我们用别人已经写好的协议,如json(使用起来很简单),protobuf(纯二进制流,使用起来比较麻烦).
我们使用json则要安装这个库
yum install -y jsoncpp-devel
有这些.h和lib软链接文件说明已经安装好了
json的基本使用
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>using namespace std;int main()
{Json::Value root;root["x"] = 100;root["y"] = 200;root["op"] = '+';root["dect"] = "this is a + oper";Json::FastWriter w;string s = w.write(root);//将序列化root 反序列化cout << s << endl;
}
记得使用第三方库的时候编译时要加第三方库名
运行结果:
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>using namespace std;int main()
{Json::Value root;root["x"] = 100;root["y"] = 200;root["op"] = '+';root["dect"] = "this is a + oper";Json::StyledWriter w;string s = w.write(root);//将root 反序列化cout << s << endl;
}
运行结果:
StyleWriter可读性更好,FasterWriter序列化的更快
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>using namespace std;int main()
{Json::Value root;root["x"] = 100;root["y"] = 200;root["op"] = '+';root["dect"] = "this is a + oper";Json::StyledWriter w;string s = w.write(root);//将root反序列化cout << s << endl;Json::Value v;Json::Reader r;r.parse(s, v);//将s反序列化为v//将序列化的内容提取出来int x = v["x"].asInt();int y = v["y"].asInt();char op = v["op"].asInt();string dect = v["dect"].asString();cout << x << endl;cout << y << endl;cout << op << endl;cout << dect << endl;
}
运行结果
用json实现网络版本计算器
更新Protocol.hpp文件
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>using namespace std;const string blank_space_sep = " ";
const string protocol_sep = "\n";
//添加报头:"内容" 转为 "长度\n内容\n"
bool Encode(string *package, const string& content)
{package->clear();*package += to_string(content.size());*package += protocol_sep;*package += content;*package += protocol_sep;return true;
}
//去除报头:"长度\n内容\n" 转为 "内容"
bool Decode(const string &package, string *content)
{content->clear();int pos = package.find(protocol_sep);if (pos == string::npos) return false;int len = stoi(package.substr(0, pos));*content = package.substr(pos + protocol_sep.size(), len);return true;
}//提取字符串
bool Extract(const string& in, int& x, int& y, char& oper)
{int pos = in.find(blank_space_sep);if (pos == string::npos) return false;x = stoi(in.substr(0, pos));oper = *in.substr(pos + blank_space_sep.size(), 1).c_str();int pos2 = in.rfind(blank_space_sep);if (pos2 == string::npos) return false;y = stoi(in.substr(pos2 + blank_space_sep.size()));return true;
}//请求协议
class Request
{
public:Request(int x = 0, int y = 0, char oper = '+'):_x(x), _y(y), _oper(oper){}//序列化 -- 将结构体转为"_x + _y"bool Serialize(string* out){
#ifdef MySelfout->clear();*out += to_string(_x);*out += blank_space_sep;*out += _oper;*out += blank_space_sep;*out += to_string(_y);return true;
#elseJson::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::StyledWriter w;*out = w.write(root);return true;
#endif}//反序列化 -- 将"_x + _y"转为结构体bool Deserialize(const string& in){
#ifdef MySelfreturn Extract(in, _x, _y, _oper);
#elseJson::Reader r;Json::Value root;r.parse(in, root);_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;
#endif}
public:int _x;int _y;char _oper;
};//响应协议
class Response
{const string blank_space_sep = " ";
public:Response(int result = 0, int code = 0):_result(), _code(code){}//序列化 -- 将结构体转为"_result _code"bool Serialize(string* out){
#ifdef MySelf*out = to_string(_result) + blank_space_sep + to_string(_code);return true;
#else Json::Value root;root["code"] = _code;root["result"] = _result;Json::StyledWriter w;*out = w.write(root);return true;
#endif}//反序列化 -- 将"_result _code"转为结构体bool Deserialize(const string& in){
#ifdef MySelfint pos = in.find(blank_space_sep);if (pos == string::npos) return false;_result = stoi(in.substr(0, pos));_code = stoi(in.substr(pos + blank_space_sep.size()));return true;
#elseJson::Reader r;Json::Value root;r.parse(in, root);_result = root["result"].asInt();_code = root["code"].asInt();return true;
#endif}
public:int _result;int _code; // 0,可信,否则!0具体是几,表明对应的错误原因
};
gcc/g++编译器的条件编译
-D[宏定义]
不用json
用json
有了条件编译就能使我们在两种版本中轻易的切换
重谈OSI七层模型
下面给出定义
应用层功能:正对特定应用协议
表示层功能:设备固有数据格式和网络标准数据格式的转换
会话层功能:通信管理。负责建立和断开通信连接(数据流动的逻辑通路)。管理传输层以下的
分层
会话层主要负责连接管理,获取客户端连接,创建socketfd,创建子进程,最后关闭socketfd,对socketfd管理的生命周期
在代码中的体现:
表示层主要负责将固有的格式字符串转化为文字、图片、视频。我们定的协议"len\ncontent\n"
,其实还可以定成这样type\nlen\n\content\n
type可表示后面是什么内容(文字、图片、视频),这样就会有不同的解析方式来解析这个字符串
代码中的体现:
应用层主要负责处理数据,对解析后的请求数据进行处理的逻辑
代码中的体现:
这样就知道了,为什么教材里面说为5 层,OSI为7层,因为这3层被压为了一层,不同的场景有不同的应用协议,如我们只发送文字信息和只发送图片信息,协议报头必然是不同的,所以我们用户编程的需求是不同的,不能够把应用协议统一起来,所以不能放在操作系统里面。
HTTP协议
域名介绍
我们平时在浏览器的url输入框中可以搜索www.baidu.com,会直接跳到百度的首页,www.baidu.com这被称为域名,访问服务器的时候,在技术上我们只需要知道IP地址和端口号就可以了,但在日常生活中,我们使用的是域名,而非IP地址,为什么呢?因为域名容易被用户记住,用IP地址会导致用户的体验变差,浏览器里面内置了这个功能,使域名被解析成IP地址
用ping指令,ping一下百度的域名
可以看到域名被解析成了110.242.68.3,我们也可以用这个IP来访问百度
为什么我们访问的时候不用指定端口号呢?我们写IP地址的时候会默认拼接上http/https,而http/https应用层协议默认绑定了知名端口号,http绑定的是80,https绑定的是443
url介绍
当我们访问网站资源的时候,可以看到https://www.helloworld.net/p/4790426995,这被叫做url(Uniform Resource Locator统一资源定位符)所有网络上的资源,都可以用唯一的一个“字符串”标识,比如你同学要你分享一个网站,你可以直接给个链接过去,为什么呢?因为这个链接在全网中具有唯一性
下面是完整的url介绍
网络的行为可以分为两种
1.把别人的的东西拿下来。把别人曾经写好的网页拿到自己的浏览器上
2.把自己的东西传上去。可能在url中传递自己的信息,如上面的?uid=1。?后面带key/value结构的参数
在百度搜索引擎上搜索
这个url本质就是访问百度服务器,url上面有我们提供给百度服务器的参数。&作为分隔符,支持多参数提交
在url中,我们也能看到有很多特殊符号,我们如果也输入特殊符号,会不会有问题呢?
在少量的情况下,提交或者获取数据本身可能包含和url中与特殊的字符冲突的字符,则要求BS双方进行编码(encode)和解码(decode)
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
例如:
HTTP请求和响应
http请求和响应的格式:以行的形式来陈列,由多行构成
http request
报头=请求行+请求报头
第一部分(第一行)叫做请求行,
请求方法Method,95%都用这两种方法GET/POST,空格作为分隔符:一般为一个空格
URL:代表你要请求的资源是谁,url一般不包括域名,一般只有/web根目录及后面的内容
HTTP Version,有1.0 1.1 2.0,最常见的是1.1
以\r\n为结尾,也可以只用\n结尾
第二部分叫做请求报头,
每一行都是请求属性,格式为key:value
每一行以\r\n结尾
你如何将报头和有效载荷分离呢?你怎么保证哪里是报头(请求行+请求报头)和正文呢?
这里和我们的之前网络版本计算器一样,我们之前用的是\n就能把报头和有效载荷(正文)分开,有一个空行\r\n。我们一直按行读取,如果我们读到了空行,则说明前半部分是报头,后面是有效载荷
最后一部分叫做请求正文,http的请求可以带参数,也可以不带参数。
如何看待请求报文呢?我们看待这个请求报文的时候可以直接看成一个大字符串
通过循环读\r\n,你读完了报头,你怎么保证将正文完整的读取上来呢?报头里面有一个属性,叫Content-Length。这个属性代表正文的长度为多少,根据这个属性就能完整的读取上来了
http response
在网页发送的就是request,响应回来的就是response
介绍一个工具telnet,用这个工具构建一个最简单的请求报文(必须要有请求行和空行,其它的可以没有)
为什么都要有HTTP Version呢?浏览器用http协议的哪个版本,服务器用哪个版本,双方需要交互一下,就好像微信的客户端,有的是v1.0,有的是v2.0,微信升级的时候,不可能直接将所有人全部升级,万一有bug呢?会造成很大的损失,一般都是先升级一部分,没bug过一段时间再升级一部分那损失注定每个人用的会有不同的版本,所以客户端和服务器需要知道对方的版本,这样才能知道该给你发哪个版本对应的功能
请求一个不存在的资源
我们平常访问资源的时候也经常看到404,就如我们写的网络版本计算器的结果code表示结果是否可行。200就表示可信,404表示你向服务器请求的资源不存在
我们平头老百姓不知道这个状态码是什么意思,就有了状态码描述,字符串版本的描述,会在浏览器里面显示
下面我用两个工具来抓包看一下,报文是不是和我们所说的是一样的
Fidder抓包,启动后会自动帮我们在本地抓包,点Raw就会显示响应报文和请求报文
Fidder的原理:浏览器并不是直接和服务器进行交互,中间有第三方:Fidder。发送和接收都会经过Fidder。这个过程也叫做代理
用postman抓包
postman原理本质是模拟浏览器的行为
下面我们来自己写一个HTTP服务器,则会对其理解的更加深刻
实现一个简易的HTTP服务器
Socket.hpp文件和Log.hpp文件在往期文章实现过,如果没写也不要紧,Log.hpp就用printf代替,Socket.hpp就是对原函数的封装
Socket.hpp文件
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include "Log.hpp"Log lg;
class Socket
{
public:Socket(){}~Socket(){}void CreateSocket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){lg(Fatal, "create socket failed:%s", strerror(errno));exit(1);}}void Bind(uint16_t serverPort){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(serverPort);server.sin_addr.s_addr = INADDR_ANY;int n = bind(_sockfd, (struct sockaddr*)&server, sizeof(server));if (n < 0){lg(Fatal, "bind socket failed:%s", strerror(errno));exit(2);}}void Listen(){int n = listen(_sockfd, 5);if (n < 0){lg(Fatal, "listen socket failed:%s", strerror(errno));exit(3);}}int Accept(string* clientIp, uint16_t* clinetPort){struct sockaddr_in peer;socklen_t len = sizeof(peer);int newsocket = accept(_sockfd, (struct sockaddr*)&peer, &len);if (newsocket < 0){lg(Error, "accept error:%s", strerror(errno));return -1;}*clientIp = inet_ntoa(peer.sin_addr);*clinetPort = ntohs(peer.sin_port);return newsocket;}int Connect(const string& serverIp, uint16_t serverPort){struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverIp.c_str());server.sin_port = htons(serverPort);int n = connect(_sockfd, (struct sockaddr*)&server, sizeof(server));if (n < 0){lg(Error, "connect error:%s", strerror(errno));return -1;}return 0;}void Close(){close(_sockfd);}int Fd(){return _sockfd;}
private:int _sockfd;
};
Log.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>using namespace std;#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define fileName "log.txt"
//使用前需要创建log目录
class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}void printOneFile(string logname, const string& logtxt){logname = path + logname;int fd = open(logname.c_str(), O_WRONLY | O_APPEND | O_CREAT, 0666);//open只会创建文件不会创建目录if (fd < 0){perror("open failed");return;}write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const string& logtxt){string filename = fileName;filename += ".";filename += leveltoString(level);printOneFile(filename, logtxt);}void printLog(int level, const string& logtxt){if (printMethod == Screen){cout << logtxt << endl;return;}else if (printMethod == Onefile){printOneFile(fileName, logtxt);return;}else if (printMethod == Classfile){printClassFile(level, logtxt);return;}}const char* leveltoString(int level){if (level == Info) return "Info";else if (level == Debug) return "Debug";else if (level == Error) return "Error";else if (level == Fatal) return "Fatal";else return "default";}void operator()(int level, const char* format, ...){time_t t = time(nullptr);struct tm* st = localtime(&t);char leftbuffer[4096];snprintf(leftbuffer, sizeof(leftbuffer), "year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d\n\[%s]:",st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec, leveltoString(level));char rightbuffer[4096];va_list start;va_start(start, format);vsnprintf(rightbuffer, sizeof(rightbuffer), format, start);va_end(start);char logtxt[4096 * 2];snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);printLog(level, logtxt);}
private:int printMethod;string path;//路径与文件名解耦,最后将路径和文件粘合起来,再用open打开即可
};
HttpServer.hpp文件
#include <pthread.h>
#include <cstring>
#include "Socket.hpp"
#include "Log.hpp"class HttpServer;
struct ThreadData
{ThreadData(int sockfd, HttpServer* hs = nullptr):_ts(hs),_sockfd(sockfd){}int _sockfd;HttpServer* _ts;
};class HttpServer
{
public:HttpServer(){}static void* ThreadRun(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);char readBuffer[4096];int n = recv(td->_sockfd, readBuffer, sizeof(readBuffer) - 1, 0);if (n > 0){readBuffer[n] = 0;cout << readBuffer << endl;}else if (n == 0) {cout << "client closed..." << endl;}else{cout << "read Error:" << strerror(errno) << endl;}close(td->_sockfd);delete td;}bool start(uint16_t port){_listensock.CreateSocket();_listensock.Bind(port);lg(Info, "Bind socket success\n");_listensock.Listen();lg(Info, "listen socket success\n");while (1){string clientIp;uint16_t clientPort;int sockfd = _listensock.Accept(&clientIp, &clientPort);pthread_t tid;ThreadData* td = new ThreadData(sockfd, this);pthread_create(&tid, nullptr, ThreadRun, td);}}
private:Socket _listensock;
};
HttpServer.cc文件
#include <memory>
#include "HttpServer.hpp"void Usage(char* HttpServer)
{cout << "\n\t" << HttpServer << "\tServerPort\n";
}
int main(int argc, char* argv[])
{if (argc != 2){Usage(argv[0]);exit(1);}unique_ptr<HttpServer> up(new HttpServer);uint16_t serverPort = atoi(argv[1]);up->start(serverPort);return 0;
}
用浏览器进行访问
可以看到收到了来自浏览器客户端的请求报文
User-Agent:为客户端的某些信息,如自己是哪个操作系统,是用哪个浏览器来访问的
这也就是为什么用电脑去浏览器搜索和手机去浏览器搜索的结果是不一样的
让浏览器能收到响应
实现简易Http服务器初级版
更新HttpServer.hpp文件
class HttpServer
{const string sep = "\r\n";
public:HttpServer(){}static void* ThreadRun(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);td->_ts->HandlerHttp(td->_sockfd);delete td;}void HandlerHttp(int sockfd){char readBuffer[4096];int n = recv(sockfd, readBuffer, sizeof(readBuffer) - 1, 0);if (n > 0){readBuffer[n] = 0;cout << readBuffer << endl;//构建响应的过程string response;string responseLine = "HTTP/1.1 200 ok";string responseHeader = "Content-Length: ";//响应报头设置的时候格式一定要正确 "Key:Value: 长度\r\n" 有空格!string blankLine = "\r\n";string text = "hello world";responseHeader += to_string(text.size());response += responseLine;response += sep;response += responseHeader + sep;response += blankLine;response += text;write(sockfd, response.c_str(), response.size());}else if (n == 0) {cout << "client closed..." << endl;}else{cout << "read Error:" << strerror(errno) << endl;}close(sockfd);}bool start(uint16_t port){_listensock.CreateSocket();_listensock.Bind(port);lg(Info, "Bind socket success\n");_listensock.Listen();lg(Info, "listen socket success\n");while (1){string clientIp;uint16_t clientPort;int sockfd = _listensock.Accept(&clientIp, &clientPort);pthread_t tid;ThreadData* td = new ThreadData(sockfd, this);pthread_create(&tid, nullptr, ThreadRun, td);}}
private:Socket _listensock;
};
用postman进行访问
可以看到只有我们曾经设置过的Content-Length
用浏览器进行访问
当浏览器收到响应报文的时候,会自动对该报文进行解析。
可以在url中带路径/参数,请求服务器服务器资源
可以看到url中的请求信息保存到了请求行中,我们可以对请求行进行分析并处理,返回给客户端相应的资源
实现简易Http服务器中级版
为了能够分析客户端request传来的信息,我们对该报文进行反序列化,并对这些信息进行相应处理
新增网页内容
wwwroot/a/b/hello.html文件
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>第一张网页</h1><a href="http://192.168.214.128:8080/">回到首页</a>
</body>
</html>
wwwroot/x/y/world.html文件
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>第二张网页</h1><a href="http://192.168.214.128:8080/">回到首页</a>
</body>
</html>
wwwroot/index.html文件
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>这是首页</h1><a href="http://192.168.214.128:8080/a/b/hello.html">点我跳转下一个网页hello</a><a href="http://192.168.214.128:8080/x/y/world.html">点我跳转下一个网页world</a>
</body>
</html>
更新HttpServer.hpp文件
#include <vector>
#include <string>
#include <fstream>
#include <sstream>
#include <pthread.h>
#include <cstring>
#include "Socket.hpp"
#include "Log.hpp"//web根目录
const string wwwroot = "./wwwroot";
const string sep = "\r\n";
const string homepage = "index.html";class HttpServer;
struct ThreadData
{ThreadData(int sockfd, HttpServer* hs = nullptr):_ts(hs),_sockfd(sockfd){}int _sockfd;HttpServer* _ts;
};class HttpRequest
{
public:void Deserialize(string req){while (1){int pos = req.find(sep);if (pos == string::npos) break;string tmp = req.substr(0, pos);if (tmp.empty()) break;reqHeader.push_back(tmp);req.erase(0, pos + sep.size());}text = req;}void Parse(){//我们用stringstream 对字符串进行流式分割,长字符串,默认就是以空格作为分隔符stringstream ss(reqHeader[0]);ss >> method >> url >> http_version;filePath += wwwroot;//wwwrootif (url == "/" || url == "/index.html"){cout << "i love you" << endl;filePath += "/";filePath += homepage;}else{filePath += url;}}vector<string> reqHeader;string text;string method;string url;string http_version;string filePath;
};class HttpServer
{
public:HttpServer(){}static void* ThreadRun(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);td->_ts->HandlerHttp(td->_sockfd);delete td;}static string ReadHtmlContent(const string& htmlpath){ifstream in(htmlpath, ios::in | ios::binary);if (!in.is_open()){return "404";}string ret;string s;while (getline(in, s)){ret += s;}in.close();return ret;}void HandlerHttp(int sockfd){char readBuffer[4096];int n = recv(sockfd, readBuffer, sizeof(readBuffer) - 1, 0);if (n > 0){readBuffer[n] = 0;cout << readBuffer << endl;HttpRequest htq;htq.Deserialize(readBuffer);htq.Parse();//构建响应的过程string response;string responseLine = "HTTP/1.1 200 ok";string responseHeader = "Content-Length: ";//响应报头设置的时候格式一定要正确 "Key:Value: 长度\r\n" 有空格!string blankLine = "\r\n";//将网页内容给读取下来,构建在response里面,然后发回给客户端cout << htq.filePath << endl;string text = ReadHtmlContent(htq.filePath);responseHeader += to_string(text.size());response += responseLine;response += sep;response += responseHeader + sep;response += blankLine;response += text;write(sockfd, response.c_str(), response.size());}else if (n == 0) {cout << "client closed..." << endl;}else{cout << "read Error:" << strerror(errno) << endl;}close(sockfd);}bool start(uint16_t port){_listensock.CreateSocket();_listensock.Bind(port);lg(Info, "Bind socket success\n");_listensock.Listen();lg(Info, "listen socket success\n");while (1){string clientIp;uint16_t clientPort;int sockfd = _listensock.Accept(&clientIp, &clientPort);pthread_t tid;ThreadData* td = new ThreadData(sockfd, this);pthread_create(&tid, nullptr, ThreadRun, td);}}
private:Socket _listensock;
};
运行结果
实现一个简易的HTTP服务器最终版
请求方法
请求方法 | 说明 | 支持的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方法:也可以获取远端资源,但更多的是要上传自己的东西
我们只讲POST和GET方法,剩下的不讲,了解即可,因为95%的情况下都是用的这两种请求方法,
HEAD方法:你不用把正文给我,只用把报头给我。DELETE方法:url表示你要删除服务器上的哪个文件,很显然在大部分情况下,是不允许客户端删除服务器的内容的,所以一般服务器屏蔽了这个方法。OPTIONS方法:询问服务器支持那些方法。
日常生活中,我们是使用网站(http/https),是如何把我们的数据提交给服务器的呢?
数据都是通过表单来提交的,如在百度网页中我们通过的搜索框进行提交数据
更新index.html文件,用get方法
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>这是首页</h1><!-- action:把这些表单里的数据提交给服务器里的哪个程序 带路径即可--><!-- method:你想以什么方式提交这个表单 --><form action="/a/b/hello.html" method="get"><!-- type代表输入框的类型 name代表将来提交的时候,url后面的key=value key的名字 value:缺省值-->name: <input type="text" name="name" value=""><br>password: <input type="password" name="passwd"><br><!-- submit为按钮类型 --><input type="submit" value="提交"></form>
</body>
</html>
美丑的问题不是我们后端工程师要考虑的,是前端工程师要考虑的事情
action的意义你要将参数提交给哪个可执行程序
你输入信息,提交后,浏览器把我们的参数拼接到了url的后面
请求报文
作为服务器来讲,通过?分隔符,把左右分开,如果左边的路径访是一个可执行程序,代码逻辑就可以fork程序替换执行这个可执行程序,通过管道/进程减通信的技术把我们的参数交给这个程序
结论:用GET方法URL进行提参,参数数量受限制,不私密
方法改为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>Document</title>
</head>
<body><h1>这是首页</h1><!-- action:把这些表单里的数据提交给服务器里的哪个程序 带路径即可--><!-- method:你想以什么方式提交这个表单 --><form action="/a/b/hello.html" method="POST"><!-- type代表输入框的类型 name代表将来提交的时候,url后面的key=value key的名字 value:缺省值-->name: <input type="text" name="name" value=""><br>password: <input type="password" name="passwd"><br><!-- submit为按钮类型 --><input type="submit" value="提交"></form>
</body>
</html>
用fidder进行抓包
POST方法也支持参数提交,采用请求的正文提交参数
GET和POST进行参数提交的时候就只有这一个区别。平时我们上网时大部分用GET方法,因为大部分你是请求网页,下载音频,很少提交参数。
可以看到百度搜索时,参数在url上面,所以我们可以断定它表单传参用的是GET方法
前端知识:如果表单没有写method方法,则默认是get方法,可以看到它提交给了路径为/s的程序
提交参数的时候可以用get和post,问题是我们什么时候用get什么时候用post呢?
get通过url进行提参,也就意味着它的参数的数量是受限制的。get会把参数会先到url框,有人在我电脑后面看,就能知道我的秘密了,而post不会,有的人就会说了get是不安全的,无论是get还是post都不安全,上面测试post方法的时候,用fidder抓包很轻松就看到了参数。安不安全是是否加密来解决的
GET方法提参有两个特点:1.参数长度受限2.不私密
HTTP状态码
状态码 | 类别 | 原因短语 |
---|---|---|
1XX | Informational(信息性状态码) | 接受的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirecion(重定向状态码) | 需要进行附加操作已完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
1XX很少见,你发起了一个请求,服务器处理的时间可能会很长,http服务器会及时给你的浏览器进行发送响应,告诉你,你的请求我已经收到了,你别急,我处理完了再发给你
2XX,比如200才是http通信最常用的,表示已成功处理
4XX,叫做客户端错误,最典型的就是404,通常是我们要找的资源不存在,403一般是,你没有访问权限或者你被禁止访问了
5XX,服务器错误,什么情况下算服务器错误呢?比如你请求服务器,服务器创建一个线程,但创建失败了,这就是服务器错误
3XX的有很多,http状态码,规定的不严格,主要还是浏览器对状态码的审核不严格,为什么会这样呢?
因为浏览器的历史原因。我们在写代码的时候也发现了,有的时候http响应是残缺的,浏览器也能显示出页面,比如http服务器初级版,响应的就是一个字符串不是网页也能显示。现在的浏览器很强大了,有网络功能,有内存管理,能够h5、css代码解析,设计浏览器内核的成本不必操作系统的成本低。一个软件变强大的重点是1.本身写的很好2.有很强的容错性。浏览器有360浏览器、猎豹浏览器、edge浏览器、火狐浏览器,为什么客户端这么多呢?十几年前,我国最大的互联网公司,百度,国外的互联网谷歌,为什么呢?因为他们的流量大,当时中国网名80%都要用百度,因为基本上一个人上网,大概率会访问浏览器,谁把浏览器拿到了,谁就拿到了整个互联网的流量入口,因为流量大,所以公司赚钱。所以有公司愿意花大价钱做浏览器,谁做得好,谁就能挣钱。你做我也做,所以在浏览器的定制标准上就很难达成一致。这也就是为什么前端开发的人,写了一个页面,他需要在各种各样常见的浏览器上面都要做测试,因为代码可能这个支持了另一个不支持。总结一下,浏览器的标准是有的,但是大家遵守的不是那么好
301为永久重定向,302为临时重定向
什么是重定向呢?
让服务器知道浏览器,让浏览器访问新的地址–这就叫做重定向
写代码,看一下重定向的现象
更新HttpServer.hpp的HandlerHttp函数
void HandlerHttp(int sockfd){char readBuffer[4096];int n = recv(sockfd, readBuffer, sizeof(readBuffer) - 1, 0);if (n > 0){readBuffer[n] = 0;cout << readBuffer << endl;HttpRequest htq;htq.Deserialize(readBuffer);htq.Parse();bool ok = true;//将网页内容给读取下来,构建在response里面,然后发回给客户端string text = ReadHtmlContent(htq.filePath);cout << text << endl;//构建响应的过程string response;string responseLine;responseLine = "HTTP/1.0 302 Redirect";string blankLine = "\r\n";string responseHeader = "Content-Length: ";responseHeader += to_string(text.size());responseHeader += sep;responseHeader += "Location: ";responseHeader += "https://www.qq.com/";responseHeader += sep;response += responseLine;response += sep;response += responseHeader;response += blankLine;response += text;write(sockfd, response.c_str(), response.size());}else if (n == 0) {cout << "client closed..." << endl;}else{cout << "read Error:" << strerror(errno) << endl;}close(sockfd);}
运行结果
什么情况下适用于永久重定向呢?上面展示的那种情况就很合适,老用户访问的还是我的老服务器(老域名),但是我的服务器(新域名)已经更新了,怕别人找不到我这个新服务,就会在老服务器上部署永久重定向
什么情况下适用于临时重定向呢?比如我们在某一网站登录的时候,登录成功了浏览器会自动跳转到首页,这就是临时重定向
HTTP常见的Header
常见的Header | 描述 |
---|---|
Content-Type | 数据类型 |
Content-Length | Body的长度 |
Host | 客户端告知服务器,所请求的资源是在哪个主机的端口上 |
User-Agent: | 声明用户的操作系统和浏览器版本信息 |
referer | 当前页面是从哪个页面跳转过来的 |
location | 搭配3xx状态码使用,告诉客户端接下来要去哪里访问 |
Cookie | 用于在客户端存储少量信息.通常用于实现会话的功能 |
一个页面是包含很多元素的,而一个元素就是一个资源
早期的网络资源比较小,采用的就是短连接的方式,一个连接就只请求一个资源,如果一个页面有100张图片,则我们请求时就要发起101次请求,第一次请求网页本身,网页里的100张图片都要请求一次,全部请求到后,浏览器会将TM组合并渲染,就成为了我们看到的样子
请求100次就要建立100次连接–这就叫做短连接
无疑,这种方案是低效的。就有了长连接
这样就能建立一次连接,传递多个请求
HTTP/1.0就只支持短连接,HTTP/1.1支持短连接长连接
这也就是为什么报头里面最开始就要写上对方的http版本。双方若都支持长连接
请求报头加上Connection: keep-alive
如果请求和响应时都携带这个选项,就说明协商完成,都采用长连接的方式进行通信,否则我们就用短连接
因为长连接很复杂,我们服务器就不实现了
Content-Type:数据类型。响应一个资源给浏览器的时候,能告诉浏览器,这个资源是什么格式,什么类型的,这样浏览器才好给你显示对应的资源
前面代码为什么没有带Content-Type也能正确显示呢?因为浏览器性能比较强了,加上前面的特征点还是比较明显的,浏览器是能识别出你是个网页。但如果给性能低的浏览器就不一定能解释了。
对应数据类型的格式可以搜索content-type对照表
ctri + F可以在该网页进行搜索
添加图片到网页里
注意:图片一般是二进制文件,要用二进制的方式读取
在wwwroot目录下添加图片
添加404页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>你要访问的资源不存在!!!</h1>
</body>
</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>Document</title>
</head>
<body><h1>这是首页</h1><!-- action:把这些表单里的数据提交给服务器里的哪个程序 带路径即可--><!-- method:你想以什么方式提交这个表单 --><form action="/a/b/hello.html" method="POST"><!-- type代表输入框的类型 name代表将来提交的时候,url后面的key=value key的名字 value:缺省值-->name: <input type="text" name="name" value=""><br>password: <input type="password" name="passwd"><br><!-- submit为按钮类型 --><input type="submit" value="提交"></form><a href="http://60.205.245.92:8082/a/b/hello.html">点我跳转下一个网页hello</a><a href="http://60.205.245.92:8082/x/y/world.html">点我跳转下一个网页world</a><br><!-- 根据src向我们的服务器,浏览器会自动发起二次请求 --><img src="./image/2.jfif" alt="小猪"><br></body>
</html>
更新HttpServer.hpp文件的class HttpServer类
class HttpServer
{
public:HttpServer(){contentType[".jfif"] = "image/jpeg";contentType[".html"] = "text/html";contentType[".png"] = "image/png";contentType[".jpg"] = "image/jpeg";}static void* ThreadRun(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);td->_ts->HandlerHttp(td->_sockfd);delete td;}static string ReadHtmlContent(const string& htmlpath){ifstream in(htmlpath, ios::in | ios::binary);if (!in.is_open()){return "404";}//获取文件大小struct stat filestat;stat(htmlpath.c_str(), &filestat);size_t fileSize = filestat.st_size;//读取一个文件的大小string ret;ret.resize(fileSize);in.read((char*)ret.c_str(), fileSize);return ret;}string SuffixToDesc(const string& suffix){auto iter = contentType.find(suffix);if (iter == contentType.end())return contentType[".html"];else return contentType[suffix];}void HandlerHttp(int sockfd){char readBuffer[4096];int n = recv(sockfd, readBuffer, sizeof(readBuffer) - 1, 0);if (n > 0){readBuffer[n] = 0;cout << readBuffer << endl;HttpRequest htq;htq.Deserialize(readBuffer);htq.Parse();bool ok = true;//将网页内容给读取下来,构建在response里面,然后发回给客户端string text = ReadHtmlContent(htq.filePath);cout << text << endl;if (text == "404"){ok = false;string errHtml = wwwroot;errHtml += "/";errHtml += "err.html";text = ReadHtmlContent(errHtml);}//构建响应的过程string response;string responseLine;if (ok){responseLine = "HTTP/1.0 200 OK";}else {responseLine = "HTTP/1.0 404 Not Found";}string blankLine = "\r\n";string responseHeader = "Content-Length: ";responseHeader += to_string(text.size());responseHeader += sep;responseHeader += "Content-Type: ";string s = SuffixToDesc(htq.suffix);responseHeader += s;responseHeader += sep;response += responseLine;response += sep;response += responseHeader;response += blankLine;response += text;write(sockfd, response.c_str(), response.size());}else if (n == 0) {cout << "client closed..." << endl;}else{cout << "read Error:" << strerror(errno) << endl;}close(sockfd);}bool start(uint16_t port){_listensock.CreateSocket();_listensock.Bind(port);lg(Info, "Bind socket success\n");_listensock.Listen();lg(Info, "listen socket success\n");while (1){string clientIp;uint16_t clientPort;int sockfd = _listensock.Accept(&clientIp, &clientPort);pthread_t tid;ThreadData* td = new ThreadData(sockfd, this);pthread_create(&tid, nullptr, ThreadRun, td);}}
private:Socket _listensock;unordered_map<string, string> contentType;
};
运行结果
请求一个不存在的资源
cookie:能实现http对登录用户的会话保持功能
什么是会话保持?当你第一次访问b站的时候,需要登录才能看视频,你登录后,关闭了浏览器,再进入浏览器再访问b站,发现你不需要再次登录了。
这是怎么做到的呢?
保存cookie文件通常由两种保存方法
1.内存级:浏览器也是进程,可以new/malloc,直接保存信息,丹尼关闭浏览器后,需要重新登录
2.文件级:即便把浏览器关掉了,关机了,仍然再次访问,不用再次登录
如何证明浏览器会保存信息到cookie文件?
可以看到有到期时间,这就是为什么过一段时间后,需要我们重新登录
我现在删除所有cookie
再进入就需要重新登录了
重新登录后,登录信息就会自动被浏览器保存到cookie中
我们自己写一个cookie,想看到我服务器给你一个set-cookie信息,浏览器会将该信息保存到cookie里面,浏览器每发起一次请求,会自动携带cookie给我(服务器)
更新HttpServer.hpp文件的HandlerHttp函数
void HandlerHttp(int sockfd){char readBuffer[4096];int n = recv(sockfd, readBuffer, sizeof(readBuffer) - 1, 0);if (n > 0){readBuffer[n] = 0;cout << readBuffer << endl;HttpRequest htq;htq.Deserialize(readBuffer);htq.Parse();bool ok = true;//将网页内容给读取下来,构建在response里面,然后发回给客户端string text = ReadHtmlContent(htq.filePath);if (text == "404"){ok = false;string errHtml = wwwroot;errHtml += "/";errHtml += "err.html";text = ReadHtmlContent(errHtml);}//构建响应的过程string response;string responseLine;if (ok){responseLine = "HTTP/1.0 200 OK";}else {responseLine = "HTTP/1.0 404 Not Found";}string blankLine = "\r\n";string responseHeader = "Content-Length: ";responseHeader += to_string(text.size());responseHeader += sep;responseHeader += "Content-Type: ";string s = SuffixToDesc(htq.suffix);responseHeader += s;responseHeader += sep;responseHeader += "Set-Cookie: name=wf1234";responseHeader += sep;responseHeader += "Set-Cookie: passwd=abc123";responseHeader += sep;responseHeader += "Set-Cookie: view=hello.html";responseHeader += sep;response += responseLine;response += sep;response += responseHeader;response += blankLine;response += text;write(sockfd, response.c_str(), response.size());}else if (n == 0) {cout << "client closed..." << endl;}else{cout << "read Error:" << strerror(errno) << endl;}close(sockfd);}
cookie会产生两个问题
1.cookie被盗取
2.个人信息被盗取
假如你访问了不好的网站,被植入了木马病毒,会自动扫描你的cookie文件,别人就会有了你的cookie文件,别人访问服务器就拿着你的cookie文件去访问,这样就等同于你了。这也是一般盗号的原理。有可能你的cookie信息里面有你的账号密码,别人就能拿着你的账号去诈骗等违法操作
如何解决个人信息被盗呢?
这种session+cookie的方案和上面只用cookie的方案有什么区别?
如果我再次把你的cookie文件盗走了,我也拿到了你的session id我也和你一样去访问b站,此时在服务器能否甄别出来你这个用户是非法的呢?很显然。黑客照样能和你一样访问
虽然不能解决这个问题,但是能解决你的私人信息不被泄露,引入session技术,最重要的点就是在于把曾经要让浏览器去维护私人的信息,统一放在了服务器去维护,哪有人又要说了,那服务器不会收到攻击吗?答案是确实会存在这种情况,一般企业里会有安全攻防工程师,并且我国信息安全的法律也是很完善的,所以攻击企业端一般没人做。比如说你敢攻击支付宝吗,里面有顶级的攻防工程师,比如说它们会故意漏出破绽,你去扫,一旦中陷阱,就会被反向追踪
cookie信息被盗取的问题能否解决呢?因为大多数用户是小白,你再怎么放,也防不了客户端的信息被盗走,答案是解决不了。我要知道一点session id是服务器统一管理分配的,它可以给你分配,那也可以销毁你的session id,比如说盗取你的黑客在缅北,上一秒你还在西安访问,下一秒你就在缅北访问,此时服务器就能甄别到你账号异常,就会销毁你的session id,你就需要重新登录了,这就是为什么你放假回老家从西安到成都,你再访问服务器就会显示账号异常,需要重新登录