HTTP协议
在上一节中,我们提到了协议的本质,其实是双方约定好的某种格式的数据,常见的就是用结构体或者类来进行表达
而上层的业务逻辑决定了我们协议的定制,有了协议,双方就可以按照同样的角度,去解读数据,这是一个自顶向下的过程
但是,虽然我们说应用层协议是我们程序猿自己定的.
实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用.
HTTP(超文本传输协议)就是其中之一.
平时我们在浏览器输入对应的网址,就能访问对应的网站,看到对应的图片等等,实际采用的就是我们的HTTP协议.
但是,我们又提到过网络通信的本质,其实是两个进程进行相互通信
用IP+PORT(端口号)来对进程的唯一性进行标识
所以我们在执行我们自己写的代码的时候,在linux系统下,用户端都需要提供对应服务器端的ip+端口号
./文件.cpp serverip serverport
但我们在浏览器中输入对应的网址,并没有提供对应想要访问的服务器端的ip和端口号啊?
进一步思考,网址究竟是什么呢?
这就是我们接下来需要探讨的东西.
URL(网址)
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法.
它的基本格式如下:
总共有7个特点
1)协议方案名
http://表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。
HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性
除了HTTP协议外,还有DNS(Domain Name System)协议,FTP(File Transfer Protocol)协议,TELNET远程终端协议等等
2)登录信息认证
usr:pass表示的是登录认证信息,包括登录用户的用户名和密码,但一般是被忽略的,我们平时输入网址的时候,也没有输入该内容
3)服务器地址与服务器端口号
www.example.jp表示的是服务器地址,也叫做域名,比如www.baidu.com.
我们需要指定的服务器端ip,其实就是域名,浏览器作为软件,会为我们提供对应的域名解析服务,所以在表面上看,我们输入的是baidu.com 但在底层实际会被解析为183.2.172.185(百度服务器的ip地址)
在linux系统下通过ping指令,也可以验证我们这一说法
那可以直接输IP地址来访问对应的服务器呢?
也是可以的(前提是你能记住的话)
那为什么不直接输IP地址来访问对应的服务器呢?
理由很简单,一来数字不好记忆,通过baidu.com域名(公司名字拼音)的方式就能访问对应的网址(资源),对用户更友好;二来ip地址本身也并不适合给用户看
那端口号呢?
Server服务器端的port端口号是不能随意指定的,假如随意指定,那就乱套了,一个公司说我要端口号80,另一个公司说我也要端口号80,那最终这个端口号应该分配给谁呢?
所以端口号必须是众所周知且不能随意更改的!
最简单解决的方法就是,端口号和成熟的应用层协议一一进行对应! 两者是1对1强相关的关系
对于HTTP协议而言,端口号为固定的80;而对于HTTPS协议而言,端口号为固定的443
而由于它是固定的,通常我们在输入网址的时候,也经常把它忽略掉,浏览器会自动帮我们进行填充
4)带层次的文件路径
有了IP+端口号,我们就可以访问到对应的服务器(唯一的进程),但具体要访问的是哪一份数据呢?
这就需要我们指定对应的文件路径,就像我们在XShell中通过cd指令跳转到不同的目录下,去访问对应服务器的不同资源
比如我们输入http://www.news.cn/,进入新华网的首页
鼠标随机点击一篇文章进去,可以看到后面就带上对应的文件路径
此外我们可以看到,路径分隔符是/,而不是\,这也就证明了实际很多服务都是部署在Linux系统上的,而不是Windows系统.
5)查询字符串
uid=1表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过&符号分隔开的,将用户数据传递给对应的服务器!
我们在浏览器搜索hello这个单词的时候,可以看到在URL中就会出现一堆以&进行分割的查询字符串,其中还有个字符串wd(word)=hello
urlencode和urldecode
在URL中(? / #)等等符号有着特殊的意义,那假如我们就是要搜索对应的这些特殊符号,又应该怎么办呢?
Url encode 编码针对的就是解决在url中出现特殊符号的问题(? / #),简称为urlencode.
比如我们在浏览器搜索?,在URL中我们可以看到它会被编码成我们的%3F
关于urlencode我们有几点需要学习
第一,它是我们浏览器自动做的,并不需要用户端自己做的,我们搜索一个问号,并不需要知道怎么编码
第二,转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
第三,服务器除了支持编码,也需要支持解码decode,简称为urldecode
第四,有一些在线网站,其实支持我们在线进行编码,比如https://tool.chinaz.com/Tools/urlencode.aspx
比如我们输入你好,然后按下URL编码键
就完成了对应"你好"的urlencode过程
HTTP请求与响应
在用户端指定对应想要访问的服务器端的ip+port端口号后,就会向对应的服务器端发送请求,申请对应的资源,随后服务器端接收到对应请求,给用户端返回对应的资源(图片,视频,运算结果等等)响应.
在我们协议一节中,我们设计的网络版本计算器协议,请求的格式非常简单,仅包含x,y两个操作数以及对应的运算操作符;响应的格式也非常简单,仅包含计算结果以及对应的错误码.
但大佬设计的HTTP协议请求与响应格式明显不会这么简单,这是我们接下来需要详谈的部分.
HTTP请求
先来看HTTP请求的格式
总共可以分为四个部分,分别是请求行,请求报头,空行以及有效载荷(可以没有),两两之间以\r\n作为分割符隔开
其中请求行又包括请求方法,我们刚刚所学的URL,以及对应的协议版本,两两之间以空格隔开
请求方法:
有时也叫“动作”,来表明Request-URL指定的资源不同的操作方式
常见的一般指定为GET(获取资源)或者POST(传输实体主体)
URL:
统一资源定位符,也被称为网址,用来定位服务器资源
协议版本:
协议并非一成不变的,会不断进行更新,这就像市面上存在不同的微信版本供我们下载
有HTTP1.0,1.1,2.0等等,通过指定协议版本,能让新老客户端很好的使用不同的功能
整个HTTP请求的格式设计其实与我们自主设计的网络版本计算器逻辑类似
我们的请求需要以\r\n作为分割,HTTP请求也同样是以\r\n进行数据的分割,实现序列化与反序列化提取对应数据的功能.
而HTTP请求中还单独多了一个空行,这是因为我们还需要将报头和有效载荷进行分离,读到空行,则报头就意味着读完了.
HTTP响应
HTTP响应是与请求一一进行对应的
总共也可以分为四个部分,分别是状态行,响应报头,空行以及有效载荷(可以没有),两两之间以\r\n作为分割符隔开
其中状态行又包括协议版本,状态码,以及对应的状态码描述,两两之间以空格隔开
协议版本很好理解,请求为HTTP1.0,则返回的响应也是HTTP1.0,防止因为双方使用的http版本不同而导致无法正常通信,保证通信双方良好的兼容性.
状态码我们也不陌生,有时候我们访问某些网站时,屏幕显示的404,就是我们对应的状态码
对状态码进行的解释,比如NOT FOUND,我们称之为状态码描述
与HTTP请求类似,HTTP响应也同样是以\r\n进行数据的分割,实现序列化与反序列化提取对应数据的功能.
最简单的HTTP服务器
下面,我们将编写一个最简单的HTTP服务器,来看看HTTP请求与响应.
看一看请求与响应
整体编写的逻辑和我们网络版本计算器类似(Sock.hpp,Log.hpp等头文件和上节的相同)
创建一个类Class HttpServer,成员变量包括我们的端口号, Sock,以及成员函数func_t ,这样上层同样只需要添加方法即可!
#pragma once
#include <iostream>
#include <cstring>
#include <pthread.h>
#include <functional>
#include "Sock.hpp"
#include "Log.hpp"namespace http_server
{using func_t = std::function<std::string(std::string &)>;static const uint16_t defaultport = 8888;class HttpServer;class HttpData{public:HttpData(int sock, const std::string &ip, const uint16_t &port, HttpServer *htsr): _sock(sock), _clientip(ip), _clientport(port), _htsr(htsr){}~HttpData() {}public:int _sock;std::string _clientip;uint16_t _clientport;HttpServer *_htsr;};class HttpServer{public:HttpServer(func_t func, int port = defaultport): _func(func), _port(port){}~HttpServer() {}void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();}// 实际处理服务的方法void HandlerHttpRequest(int sock){char buffer[4096]; //创建一个缓冲区std::string request;ssize_t s = recv(sock,buffer,sizeof(buffer) - 1,0);if(s > 0){buffer[s] = 0;request = buffer;std::string response = _func(request); //服务器进行业务处理send(sock,response.c_str(),response.size(),0); //向用户端发送响应}else{LogMessage(Info, "client quit...");}}static void *threadRoutine(void *args){pthread_detach(pthread_self());HttpData *td = static_cast<HttpData *>(args);td->_htsr->HandlerHttpRequest(td->_sock);close(td->_sock);delete td;return nullptr;}void Start(){while (true){std::string clientip;uint16_t clientport;int sock = _listensock.Accept(&clientip, &clientport);if (sock < 0)continue; // 假如连接失败,则继续重连pthread_t pid;HttpData *td = new HttpData(sock, clientip, clientport, this);pthread_create(&pid, nullptr, threadRoutine, td);}}private:Sock _listensock;int _port;func_t _func;};
}
对应的makefile文件如下:
httpServer:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f httpServer
继续完成我们main主函数的编写
#include "HttpServer.hpp"
#include <memory>using namespace http_server;
//用户使用手册
static void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" <<std::endl;std::cout << message << std::endl;return "";
}
int main(int argc,char* argv[])
{if(argc != 2){exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp,port));tsvr->InitServer();tsvr->Start();return 0;
}
允许我们对应的服务器程序,并打开我们任意一个浏览器访问我们的服务器,便可以看到HTTP请求输出在屏幕上,格式和我们之前讲的相同
而对于HTTP响应,我们则是在Xshell中直接访问百度服务器
telnet www.baidu.com 80
发送最简单的请求GET / HTTP/1.0
便能获取到百度服务器给我们对应的响应
但是返回的响应,信息实际上是非常多的,并不方便我们查看对应的内容!
我们可以下载一个叫做postman的软件,输入对应的网址,它就能输出比较精美的格式,供我们查看对应的HTTP响应
编写我们自己的响应
但是,单纯这样还不够,我们可以尝试编写我们自己的响应,实际上就是
HttpServer方法继续编写(HandlerHttp函数)
我们在浏览器访问时,响应一般都是以网页的形式呈现出来,我们编写的响应也应该以网页的形式呈现
具体内容可以参照w3school这个网站进行学习
我们今天编写的网站则没有这么复杂,只需要有显示对应文字即可
<html><header><h1>this is a test</h1></header>
</html>
#include "HttpServer.hpp"
#include "Util.hpp"
#include <memory>using namespace http_server;
// 用户使用手册
static void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverport\n"<< std::endl;
}const std::string SEP = "\r\n";
const std::string path = "./wwwroot/index.html";
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;std::string response = "HTTP/1.0 200 OK" + SEP;response += SEP; //序列化response += "<html><header> <h1>this is a test</h1></header></html>";return response;
}
int main(int argc, char *argv[])
{if (argc != 2){exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp, port));tsvr->InitServer();tsvr->Start();return 0;
}
对代码重新进行编译允许,在浏览器上访问,即可看到我们网页版显示内容.
进一步改造响应
但这还并不够,有一些问题我们其实一直在回避,比如说在网络版本计算器中,我们会在序列前面加上有效载荷的长度,用来区分不同报头,那HTTP协议又是如何区分不同报头呢? 还有我们响应的资源类型有很多种,可能是图片,也可能是视频等等,这也是HTTP被称为超文本传输协议的原因.
解决这些问题的答案都在报头中,报头中会蕴含不同的报头属性,用来解决诸如区分不同报头,指定资源类型等等问题.
通常,浏览器会给你自动处理进行解析,但是我们代码里面还是要自己编写的,所以我们上述的HandlerHttp函数还需要继续进行改造.
Content_Length
Content_Length是报头属性之一,它的作用就是用来区分不同的报头
const std::string SEP = "\r\n";
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;std::string response = "HTTP/1.0 200 OK" + SEP;response += "Content-length: " + std::to_string(body.size()) + SEP; //不要忘了加SEPresponse += SEP;response += body;return response;
}
Content-Type
Content-Type也是报头属性之一,它的作用就是指定Body(资源/有效载荷)的种类,它将决定浏览器将以什么形式、什么编码读取这个文件
那如何确定一个资源,它到底是什么类型呢?
无论是什么资源,比如说图片,网页,视频,音频等等,它的本质都是文件!是文件就都要有自己的后缀!
比如图片的后缀是<.jpg .png…> ,网页的后缀是<.html .htm> ,音频的后缀是<.mp3>等等
不同的后缀名对应不同的文件类型
具体不同的对应关系,可以自行上网搜Content-Type对照表就可以获得: 菜鸟教程
const std::string SEP = "\r\n";
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;std::string body = "<html><header> <h1>this is a test</h1></header></html>";std::string response = "HTTP/1.0 200 OK" + SEP;response += "Content-length: " + std::to_string(body.size()) + SEP; //不要忘了加SEPresponse += "Content-Type: text/html" + SEP; //不要忘了加SEPresponse += SEP;response += body;return response;
}
从文件中读取Body(有效载荷)
但是在实际操作中,并不会像我们上述这样,直接写一个body字符串,那假如一个网页的资源非常多,那代码就会显得很冗余,而且也不好修改.
实际操作中,程序员大多编写html等等网页代码,我们负责从里面读取相应的正文内容即可!
对此,我们要编写一个ReadFile函数,它的功能就是从对应的文件中,读取对应的内容,并存到我们的字符串中
整体函数可以分为四个部分:
1.调用stat函数来获取文件大小
2.resize调整string的大小
3.open函数读取文件内容
4.关闭文件
// 输入: const &
// 输出: *
// 输入输出: &
static bool ReadFile(const std::string &path, std::string *fileContent)
{// 1.stat函数 获取文件大小struct stat st;int n = stat(path.c_str(), &st);if (n < 0)return false; // 获取失败,返回falseint size = st.st_size;// 2.调整string的大小 resizefileContent->resize(size);// 3.读取 open函数int fd = open(path.c_str(),O_RDONLY);if(fd < 0) return false; //打开失败read(fd,(char*)fileContent->c_str(),size);if (n < 0) return false;// 4.关闭close(fd);LogMessage(Info,"read file %s done",path.c_str());return true;
}
创建一个wwwroot文件夹,并创建对应的index.html网页文件
<html><header><h1>this is a test</h1></header>
</html>
对应的路径,我们设定为当前路径,从wwwroot文件夹中的index.html网页文件中读取,则我们的HandlerHttp函数又可以进一步修改为
const std::string SEP = "\r\n";
const std::string path = "./wwwroot/index.html";
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;std::string body;Util::ReadFile(path,&body); //返回的是一张网页std::string response = "HTTP/1.0 200 OK" + SEP;response += "Content-length: " + std::to_string(body.size()) + SEP; // 不要忘了加SEPresponse += "Content-Type: text/html" + SEP; // 不要忘了加SEPresponse += SEP;response += body;return "";
}
反序列化
上述的路径,文件类型等等,我们其实都是自己给定的,在现实中,服务器应该根据用户端的请求,提取对应的信息,并给出对应的响应!
(读取请求------反序列化------分析请求)
我们再回顾一下HTTP请求的格式
现在我们要对其进行反序列化,构建出一个结构体,这样就可以轻松的调用不同的资源,给用户返回我们的响应.
结构体的成员变量设计,就是根据HTTP请求来设计
方法上,暂时没有特殊要求,可以添加一个Print函数,来调试我们反序列化是否成功
class HttpRequest
{
public:HttpRequest() {}~HttpRequest() {}void Print(){LogMessage(Debug, "method: %s, url: %s, version: %s",_method.c_str(), _url.c_str(), _httpversion.c_str());for (const auto &line : _body)LogMessage(Debug, "-%s", line.c_str());}public:std::string _method; // 方法std::string _url; // 资源路径std::string _httpversion; // 协议版本std::vector<std::string> _body;
};
现在的关键就是如何进行反序列化,将一个请求(字符串)转换为我们的HttpRequest结构体
永远不要忘记HTTP请求,是以\r\n进行切割内容
所以我们只要编写两个函数,一个我们称之为ReadOneLine函数,它能够一行一行(按照\r\n)读取对应的请求行,请求报头等等,然后压入vector中,方便我们操作
另一个我们称之为ParseRequestLine函数,它能够切割我们的字符串,进一步读取我们的内容,比如说我们通过调用ReadOneLine函数,可以读取到请求行,那我们便可以用ParseRequestLine函数进一步切割,获取到请求方法,URL以及协议版本.
我们先来编写ReadOneLine函数
它的目标就是找到对应的分割符(\r\n),剪切对应子串,并将对应的子串从原串中删除,直到原串(HTTP请求)全被提取完
static std::string ReadOneLine(std::string &message, const std::string &sep)
{auto pos = message.find(sep);if(pos == std::string::npos) return "";std::string s = message.substr(0,pos);message.erase(0,pos + sep.size()); //移除对应切割出的部分return s;
}
对于ParseRequestLine函数,我们也可以用find来提取,只不过此时分隔符变为了空格,但还有一个更简单的方法,那就是用Stringstream类,它可以用输出的方式,按照空格分割出不同的子串
static bool ParseRequestLine(const std::string &line, std::string *method, std::string *url, std::string *httpVersion)
{std::stringstream ss(line);ss >> *method >> *url >> *httpVersion;return true;
}
整体的所有方法代码可以整合到一个头文件中
// Util.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <cstring>
#include <sstream>
#include "Log.hpp"
#include <vector>
using namespace std;class Util
{
public:// 输入: const &// 输出: *// 输入输出: &static bool ReadFile(const std::string &path, std::string *fileContent){// 1.stat函数 获取文件大小struct stat st;int n = stat(path.c_str(), &st);if (n < 0)return false; // 获取失败,返回falseint size = st.st_size;// 2.调整string的大小 resizefileContent->resize(size);// 3.读取 open函数int fd = open(path.c_str(),O_RDONLY);if(fd < 0) return false; //打开失败read(fd,(char*)fileContent->c_str(),size);if (n < 0) return false;// 4.关闭close(fd);LogMessage(Info,"read file %s done",path.c_str());return true;}static std::string ReadOneLine(std::string &message, const std::string &sep){auto pos = message.find(sep);if(pos == std::string::npos) return "";std::string s = message.substr(0,pos);message.erase(0,pos + sep.size()); //移除对应切割出的部分return s;}static bool ParseRequestLine(const std::string &line, std::string *method, std::string *url, std::string *httpVersion){std::stringstream ss(line);ss >> *method >> *url >> *httpVersion;return true;}
};
有了上述的方法,我们就可以完成我们的反序列化函数编写了
按照分割符\r\n进行读取一行数据,并存入对应的vector中,需要用的时候,只需要用下标就可以提取出.
HttpRequest Desearialize(std::string message)
{HttpRequest rq;std::string line = Util::ReadOneLine(message, SEP); // 根据分隔符读出第一行Util::ParseRequestLine(line, &rq._method, &rq._url, &rq._httpversion);while (!message.empty()){line = Util::ReadOneLine(message, SEP);rq._body.push_back(line);}return rq;
}
那我们就可以从HTTP请求中提取我们想要的内容(反序列化),
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;HttpRequest rq = Desearialize(message);rq.Print(); //调试,看是否提取成功std::string body;Util::ReadFile(rq._url, &body);std::string response = "HTTP/1.0 200 OK" + SEP;response += "Content-length: " + std::to_string(body.size()) + SEP; // 不要忘了加SEPresponse += "Content-Type: text/html" + SEP; // 不要忘了加SEPresponse += SEP;response += body;return response;
}
Web根目录
但上述的代码还是有不足的,一般一个webserver服务器,不做特殊说明,如果用户之间默认访问’/‘,我们是绝对不能把整站数据给对方用户端的,一来有些数据不可以被访问,二来数据量太大了
所以我们需要添加默认首页与默认根目录,这个默认目录我们称作为Web根目录
它并不是实际的linux系统根目录,而是我们假定的默认根目录,就像我们打开百度,默认打开的是搜索主页面(默认根目录下的默认首页)
本次实验我们的Web根目录则是wwwroot,无论用户想要访问什么数据,都是从这个根目录下开始寻找对应的资源,并且我们保证不能让用户访问wwwtoot里面的任何一个目录本身,而只能是文件!
所以,我们对HttpRequest类进一步修改,增添真实路径这一成员变量,并在初始化时,就用默认构造进行初始化
const std::string defaultHomePage = "index.html"; // 默认首页
const std::string webRoot = "./wwwroot"; // web根目录class HttpRequest
{
public:HttpRequest() : _path(webRoot){}~HttpRequest() {}void Print(){LogMessage(Debug, "method: %s, url: %s, version: %s",_method.c_str(), _url.c_str(), _httpversion.c_str());LogMessage(Debug, "path: %s", _path.c_str());}public:std::string _method; // 方法std::string _url; // 资源路径std::string _httpversion; // 协议版本std::vector<std::string> _body;// 真实资源路径std::string _path;
};
对应的反序列化函数和HandlerHttp函数也可以进一步修改
HttpRequest Desearialize(std::string message)
{HttpRequest rq;std::string line = Util::ReadOneLine(message, SEP); // 根据分隔符读出第一行Util::ParseRequestLine(line, &rq._method, &rq._url, &rq._httpversion);while (!message.empty()){line = Util::ReadOneLine(message, SEP);rq._body.push_back(line);}rq._path += rq._url; // "wwwroot/a/b/c.html", "./wwwroot/"if (rq._path[rq._path.size() - 1] == '/')rq._path += defaultHomePage;return rq;
}
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;HttpRequest rq = Desearialize(message);rq.Print(); //调试std::string body;Util::ReadFile(rq._path, &body);std::string response = "HTTP/1.0 200 OK" + SEP;response += "Content-length: " + std::to_string(body.size()) + SEP; // 不要忘了加SEPresponse += "Content-Type: text/html" + SEP; // 不要忘了加SEPresponse += SEP;response += body;return response;
}
文件后缀名
假如显示的不是文本,而是图片呢?
使用下面的bash指令可以下载对应的图片到我们的Xshell中
mkdir image //创建名为image的文件夹
> cd image //进入image的文件夹
> wget +网络图片链接 //下载对应的图片
> mv 图片名字 //更改图片名字
> du -h //查看图片大小
编写GetContentType函数,可以根据我们的Content-Type对照表,给不同的文件类型加上对应不同的后缀名
std::string GetContentType(const std::string &suffix)
{std::string content_type = "Content-Type: ";if (suffix == ".html" || suffix == ".htm")content_type += "text/html";else if (suffix == ".css")content_type += "text/css";else if (suffix == ".js")content_type += "application/x-javascript";else if (suffix == ".png")content_type += "image/png";else if (suffix == ".jpg")content_type += "image/jpeg";else{}return content_type + SEP;
}
完整代码可以修改如下:
#include "HttpServer.hpp"
#include "Util.hpp"
#include <memory>using namespace http_server;
// 用户使用手册
static void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverport\n"<< std::endl;
}const std::string SEP = "\r\n";
const std::string path = "./wwwroot/index.html";const std::string defaultHomePage = "index.html"; // 默认首页
const std::string webRoot = "./wwwroot"; // web根目录class HttpRequest
{
public:HttpRequest() : _path(webRoot){}~HttpRequest() {}
public:std::string _method; // 方法std::string _url; // 资源路径std::string _httpversion; // 协议版本std::vector<std::string> _body;// 真实资源路径std::string _path;// 文件后缀std::string _suffix;
};HttpRequest Desearialize(std::string message)
{HttpRequest rq;std::string line = Util::ReadOneLine(message, SEP); // 根据分隔符读出第一行Util::ParseRequestLine(line, &rq._method, &rq._url, &rq._httpversion);while (!message.empty()){line = Util::ReadOneLine(message, SEP);rq._body.push_back(line);}rq._path += rq._url; // "wwwroot/a/b/c.html", "./wwwroot/"if (rq._path[rq._path.size() - 1] == '/')rq._path += defaultHomePage;auto pos = rq._path.rfind(".");if (pos == std::string::npos)rq._suffix = ".html"; // 没找到,默认后缀为网页elserq._suffix = rq._path.substr(pos);return rq;
}std::string GetContentType(const std::string &suffix)
{std::string content_type = "Content-Type: ";if (suffix == ".html" || suffix == ".htm")content_type += "text/html";else if (suffix == ".css")content_type += "text/css";else if (suffix == ".js")content_type += "application/x-javascript";else if (suffix == ".png")content_type += "image/png";else if (suffix == ".jpg")content_type += "image/jpeg";else{}return content_type + SEP;
}std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;HttpRequest rq = Desearialize(message);std::string body;Util::ReadFile(rq._path, &body);std::string response = "HTTP/1.0 200 OK" + SEP;response += "Content-length: " + std::to_string(body.size()) + SEP; // 不要忘了加SEPresponse += GetContentType(rq._suffix);response += SEP;response += body;return "";
}
int main(int argc, char *argv[])
{if (argc != 2){exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp, port));tsvr->InitServer();tsvr->Start();return 0;
}
PS:超链接跳转,本质其实就是让html中特定的标签被浏览器解释,重新发起HTTP请求
HTTP方法
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方法
浏览器客户端向服务器发起请求时,携带的方法一般就是GET或者POST
一般没有指定的话,用的都是GET方法
那两者的区别在哪呢?
提交参数的方式不同,对于GET方法而言,是直接通过URL的方式进行参数提交;而POST请求,提交数据的时候,URL不会发生变化 ,没有参数它是通过正文部分提交参数的!
我们可以用Postman软件对照GET和POST的区别,只需要运行我们的服务器,然后用Postman发出请求时,添加对应的参数,选择不同方式,进行发送即可
那GET和POST各自的应用场景是什么呢?
用GET方法提交参数,是不私密的(不是不安全),很容易被窃取到对应的信息
而POST提交参数比较私密一点,毕竟提交的参数是在正文,而不是直接在URL字符串中显示
一般而言,对于登录注册支付(QQ空间密码)等行为,都要使用POST方法提交参数
一来会相对更私密,二来对于Url请求行字符串,一般都会有大小的约束,正文理论上则可以非常大!
但无论是GET和POST方法都不要直接说是安全还是不安全!对于我们发送的信息而言,至少都要进行加密!
否则用诸如Fiddler等软件,很容易就能窃取到对应的消息
HTTP状态码
HTTP的状态码如下:
– | 类别 | 原因 |
---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
最常见的状态码,比如200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)
PS:404报错属于客户端报错,而不是服务器端的错,这就好比你去鱼店买菜,而卖鱼店里面没有菜卖,这不是卖鱼店的错,而是你的错,没事不去卖菜的店买菜,而去鱼店买菜,这不是自找苦吃吗?
但服务器端有必要提醒用户并没有对应的资源存在!
404 NOT FOUND
比如我们打开诸如京东等等外卖的网站,输入不存在的资源路径
京东网站会给我们输出对应的错误资源信息
想要做到显示404页面代码也很简单,只需要修改源代码,改成if else的逻辑进行页面显示即可
创建一个404显示报错页面(page_404.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
<style>
body {
text-align: center;
padding: 150px;
}
h1 {
font-size: 50px;
}
body {
font-size: 20px;
}
a {
color: #008080;
text-decoration: none;
}
a:hover {
color: #005F5F;
text-decoration: underline;
}
</style>
</head>
<body>
<div>
<h1>404</h1>
<p>页面未找到<br></p>
<p>
您请求的页面可能已经被删除、更名或者您输入的网址有误。<br>
请尝试使用以下链接或者自行搜索:<br><br>
<a href="https://www.baidu.com">百度一下></a>
</p>
</div>
</body>
</html>
将服务器代码修改为if else逻辑
#include "HttpServer.hpp"
#include "Util.hpp"
#include <memory>using namespace http_server;
// 用户使用手册
static void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverport\n"<< std::endl;
}const std::string SEP = "\r\n";
const std::string path = "./wwwroot/index.html";const std::string defaultHomePage = "index.html"; // 默认首页
const std::string webRoot = "./wwwroot"; // web根目录
const std::string page_404 = "./wwwroot/err_404.html"; //无法找到对应资源时,显示的页面class HttpRequest
{
public:HttpRequest() : _path(webRoot){}~HttpRequest() {}void Print(){LogMessage(Debug, "method: %s, url: %s, version: %s",_method.c_str(), _url.c_str(), _httpversion.c_str());// for (const auto &line : _body)// LogMessage(Debug, "-%s", line.c_str());LogMessage(Debug, "path: %s", _path.c_str());}public:std::string _method; // 方法std::string _url; // 资源路径std::string _httpversion; // 协议版本std::vector<std::string> _body;// 真实资源路径std::string _path;// 文件后缀std::string _suffix;
};
HttpRequest Desearialize(std::string message)
{HttpRequest rq;std::string line = Util::ReadOneLine(message, SEP); // 根据分隔符读出第一行Util::ParseRequestLine(line, &rq._method, &rq._url, &rq._httpversion);while (!message.empty()){line = Util::ReadOneLine(message, SEP);rq._body.push_back(line);}rq._path += rq._url; // "wwwroot/a/b/c.html", "./wwwroot/"if (rq._path[rq._path.size() - 1] == '/')rq._path += defaultHomePage;auto pos = rq._path.rfind(".");if (pos == std::string::npos)rq._suffix = ".html"; // 没找到,默认后缀为网页elserq._suffix = rq._path.substr(pos);return rq;
}std::string GetContentType(const std::string &suffix)
{std::string content_type = "Content-Type: ";if (suffix == ".html" || suffix == ".htm")content_type += "text/html";else if (suffix == ".css")content_type += "text/css";else if (suffix == ".js")content_type += "application/x-javascript";else if (suffix == ".png")content_type += "image/png";else if (suffix == ".jpg")content_type += "image/jpeg";else{}return content_type + SEP;
}
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;HttpRequest rq = Desearialize(message);//rq.Print();std::string body;std::string response;if (true == Util::ReadFile(rq._path, &body)){ response = "HTTP/1.0 200 OK" + SEP;response += "Content-length: " + std::to_string(body.size()) + SEP; // 不要忘了加SEPresponse += GetContentType(rq._suffix);response += SEP;response += body;}else{response = "HTTP/1.0 404 Not Found" + SEP;Util::ReadFile(page_404, &body);response += "Content-length: " + std::to_string(body.size()) + SEP; // 不要忘了加SEPresponse += GetContentType(".html");response += SEP;response += body;}return response;
}
int main(int argc, char *argv[])
{if (argc != 2){exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<HttpServer> tsvr(new HttpServer(HandlerHttp, port));tsvr->InitServer();tsvr->Start();return 0;
}
但需要注意的是,不同浏览器对于协议的支持其实不是那么强,不同公司其实有着自己的状态码规定,并非说2xx,对应的一定是成功状态码的信息
3xx 重定向
在状态码中,还存在重定向这一选项,它的功能就是通过各种方法将各种网络请求重新定个方向转到其它位置
它就好比我们出校园的南门吃沙县小吃,但沙县小吃刚好在装修,于是在门外贴了一张告示,让同学们去西门的临时店铺吃,这张告示就发挥着重定向的作用
重定向又分为临时重定向与永久重定向
临时重定向,不更改浏览器的任何地址信息
永久重定向,会更改浏览器的本地书签!
临时店铺那就是临时重定向,但假如老板发现西门临时店铺带来的生意其实更好,而装修后的原店铺反而生意一落千丈,老板将原店铺关闭,而直接全部转移到西门店铺做生意,这就是永久重定向!
不过无论是临时还是永久,都要求提供新的地址!
我们可以将我们的代码修改一下,重定向至百度浏览器页面
std::string HandlerHttp(std::string &message)
{std::cout << "---------------------------" << std::endl;std::cout << message << std::endl;HttpRequest rq = Desearialize(message);//重定向测试std::string response;response = "HTTP/1.0 301 Moved Permanently" + SEP;response += "Location: https://www.baidu.com/" + SEP;response += SEP;return response;
}
对于临时还是永久,都有各自的应用场景
临时重定向:
1.打开某个小的软件,直接打开的是广告,然后跳转到京东淘宝等软件
2.扫码登录注册,跳转到首页
永久重定向:
1.一个网站,不仅仅是人在访问,有可能其他的程序也在访问,比如爬虫 (搜索引擎)
2.搜出来一个条目,点击,这个网站过期了,打不开了 (永久更新到新的网址)
会话保持 Cookie && Session
我们应该都遇过一个情况,就是基于HTTP登录一个网站后,比如说b站,退出后,可以直接登录,而不用重新输入密码进行登录,甚至我们关机后重启,也不需要再重新输入密码登录.
HTTP不直接做这个工作,但是用户需要这个功能,我们把用户是否在线要持续的记录下来这个功能称作为会话保持(Cookie)
不要小瞧这个自动进行认证功能,在之前,client访问每一个资源的时候,都是需要认证的!
对于VIP用户来说,每点开一部电影,就要输入一次账号和密码,那大概率会很烦
而有了会话保持功能,那看VIP电影就不用多次登录,这就好比我们游乐场验票,我们不需要每玩一个项目,就要买票,直接买一张票,畅玩所有项目卡,大大提高用户的体验!
当然,这和HTTP本身协议是无关的,HTTP协议是一种无状态协议,每次请求/响应之间是没有任何关系的,这个功能和浏览器有关!
在第一次登录的时候,浏览器会将用户中response(响应)的cookie信息在本地进行保存,并且这种保存不是内存级,而是文件级的,理由就是把浏览器关了,再打开对应的网址,依旧可以直接登录
具体步骤如下图所示:
1.用户申请访问服务器,服务器返回的响应中包含表单属性,让用户输入账号和密码
2.服务器对用户输入的请求,其中包括账号和密码进行验证,假如验证成功,则返回对应的资源,返回的响应中还包括Set-cookie字段(用户输入的账号和密码)
3.通过返回的请求,浏览器可以将response(响应)的cookie信息在本地进行保存(文件级)
4.此后,对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的cookie字段,而不会重新让你输入账号和密码了
在Edge浏览器中,我们也可以看到我们保存的Cookie信息,并对其进行管理
但是这其实并不安全,假如我们电脑中了木马病毒,比如说不小心进了恶意网站,点了不该点的链接等等,黑客就很轻松把你所有的cookie信息全部盗取过来,这样它就能随意利用用户的信息访问不同网站!
所以现在大多是用当前用户的基本信息情况形成session对象 ,并且对应唯一的一个Session id
本地cookie文件只保存session id,以及其对应的期限
现在我们是通过session id的方式,来访问对应的资源
有人可能会疑问,这根本没有解决任何问题啊?
黑客同样可以盗取用户的session id来访问不同的服务器!
但是通过session id的方式来访问服务器,有两大优势所在
第一,用户信息不再被泄漏,而是被server服务器维护起来,原来则能够被黑客直接盗取
第二,Session id是会失效的 ,虽然不能彻底杜绝用户Cookie丢失问题,但我们可以提出相应的新解决方案,比如说验证ip地址,假如是异地登录,即便黑客盗取了相应的session id,也无法访问对应的服务器!
Tokens技术
但是Session技术虽然能够一定程度缓解黑客入侵盗取信息的问题,但是牺牲的却是我们服务器端的资源,毕竟在本质上,我们的用户信息是被我们的服务器端进行维护的.
因此当用户数量增加时,服务器的负担也会增加,这可能会影响到应用的扩展性
于是有人就提出了另外一种技术——Tokens技术
Tokens作为计算机术语时,是“令牌”的意思.
它本质上是服务器端生成的一串加密字符串,以作客户端进行请求的一个令牌
所有步骤都和之前的类似
1.用户首先向服务器发送登录请求,包含其凭证,如用户名和密码.
2.服务器验证这些凭证.如果凭证有效,服务器会生成一个token.
不同之处在于,对于Sesson技术来说,用户信息是我们的服务器端维护,客户端只需要Session ID即可访问对应的资源
但是Tokens技术不同,Token令牌是客户端负责存储的,通常是在本地存储或者cookie中
用户假如想访问对应的服务器资源时,Token会被附加在请求头中发送给服务器,即想要访问对应的资源,只需要出示自己的token令牌即可(有点对暗号的感觉)
当然,黑客同样也可以盗取我们用户的token,从而访问服务器资源,但是,依旧是那个道理,虽然我们不能彻底杜绝用户token丢失问题,但我们可以提出相应的新解决方案
比如给token加数字签名,以防止内容被篡改等等
基于tokens技术提出来的方案有很多,各式各样不同的Token令牌种类,这里就不再详细介绍,有兴趣的可以自行去其它博主那搜索了解
JSON Web Tokens (JWT):
这是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这个信息可以被验证和信任,因为它是数字签名的.JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公/私钥对进行签名.
OAuth2 Tokens:
OAuth2是一个授权框架,它允许应用程序获得有限的访问权限到用户帐户。它主要用于授权,而不是身份验证。OAuth2定义了四种授权方式来获取Access Token,这些Token可以用来访问受保护的资源。
Bearer Tokens:
Bearer Token是一种非常简单的安全令牌,没有签名和加密机制,但是在传输过程中需要通过HTTPS进行保护.Bearer Tokens在OAuth 2.0规范中被广泛使用。
Refresh Tokens:
在OAuth2中,Refresh Token用于在当前Access Token过期后获取新的Access Token,而无需用户重新认证。这对于那些需要长时间访问用户数据的应用程序非常有用。
API Keys:
API密钥通常用作服务的简单访问令牌。它们通常是长期有效的,并且与特定的应用程序或用户关联。然而,它们通常不包含任何用户信息,只是允许服务器识别请求的来源.
DNS服务
hosts文件
DNS(Domain Name System),是一整套从域名映射到IP的系统
我们说假如想要访问对应的服务器,需要目标服务器的IP+端口号Port访问网络中唯一一个进程
浏览器作为软件,在底层会为我们提供对应的域名解析服务,所以在表面上看,我们输入的是baidu.com ,但在底层实际会被解析为183.2.172.185(百度服务器的ip地址)
将域名转换为对应的IP系统,便被称为DNS服务
IP它就好比我们的手机号,数目一旦多起来,便很难记住,但是姓名很容易记住,域名也是如此,通过域名就能访问对应的服务器,这也是我们老百姓想要的,正所谓产品越简单,越容易推行.
它的本质是一个字符串, 并且使用hosts文件来描述主机名和IP地址的关系
在Linux系统下,我们输入下面的指令也可以查看我们对应的hosts文件
cat /etc/hosts
所以有的时候,我们QQ等其它软件可以用,但是浏览器就是上不了网,访问不了对应的网站,可能是域名解析DNS出了问题.
DNS服务器
最初, 我们通过互连网信息中心(SRI-NIC)来管理hosts文件
如果一个新计算机要接入网络, 或者某个计算机IP变更, 都需要到信息中心申请变更hosts文件.
其他计算机也需要定期下载更新新版本的hosts文件才能正确上网.
但是这样就会很麻烦,于是产生了DNS系统.
1.它是一个组织的系统管理机构, 维护系统内的每个主机的IP和主机名的对应关系.
2.如果新计算机接入网络, 将这个信息注册到数据库中;
3.用户输入域名的时候, 会自动查询DNS服务器, 由DNS服务器检索数据库, 得到对应的IP地址,当然并不是每次输入域名都要查询,它在浏览器中是有缓存功能的
查询过程
但是我们说,全球有这么多家公司,每家公司又有这么多台服务器,DNS服务器能够容纳那么多IP和主机名对应的关系吗?
我们说并没有,或者更进一步说,并不想要这样的结果
一是机器承担不起这样的负担
二是域名不断在增加与注销,要通知全球所有DNS服务器更新账本,这样的消耗是巨大的
所以,我们采取的是层级划分的方法
在域名当中,我们有一级域名,二级域名的概念
比如我们熟知的
com:一级域名,表示这是一个工商企业域名.
同级的还有.net(网络提供商)和.org(开源组织或非盈利组织)等,像是我们Linux内核源代码网站,对应的域名就是kernel.org,表示非盈利组织
baidu:二级域名,一般对应的就是公司名
例如,我们在浏览器中输入对应的域名www.example.com.cn
浏览器首先会在自己的缓存里面查找,看看有没有对应的域名和IP对应;有则直接返回
发现没有的话,就会去查询操作系统中的DNS缓存;有则直接返回
还是没有的话,就会去查找本地的hosts文件;有则直接返回
假如还是没有,此时才会去查找当地的DNS服务器,本地DNS服务器IP地址一般由本地网络服务商提供,如电信、移动等公司,一般通过DHCP自动分配,如果有对应的域名和IP对应关系,则直接返回,这个过程称之为本地DNS解析
但是假如本地DNS服务器都也没找到对应的域名和IP对应关系,我们只能去找对应的根DNS服务器了
然后我们从根DNS服务器开始,逐级查询对应的DNS服务器,直到找到具体的IP地址
比如在解析"www.example.com"时(假设本地DNS服务器没有找到),我们会先去问"老大哥"根DNS服务器,但它不会直接返回IP地址给我们,而是返回负责.com顶级域的"二哥"顶级域名服务器的地址
同样的,"二哥"它也不会直接返回IP地址,而是返回负责example.com的权限DNS服务器的地址
最后,本地DNS服务器接收到权限DNS服务器的地址后,向其发送查询请求.
权限DNS服务器会查找自己的记录,找到www.example.com对应的IP地址,然后返回给本地DNS服务器,本地DNS服务器再发回给我们.
在这个过程中,我们可以发现,全部都是由当地的DNS服务器全程参与负责,我们只需要等待结果就好,这样美滋滋的查询方式,我们称之为递归查询
假如客户端向DNS服务器发送查询请求,如果DNS服务器没有存储所需的信息,但是它自己不去查,而是将其他可能知道信息的DNS服务器的地址返回给客户端,然后要客户端自己,向这些服务器发送查询请求,然后不断重复这个过程,直到获取最终的查询结果
在这个过程中,客户端需要参与整个查询过程,这样的查询方式,我们称之为迭代查询
通过这样层级划分管理域名对应关系的方式(分而治之),各级DNS各司其职,统一将全球的域名成功解析,并完成去中心化的任务.
但是这里也会存在一个问题,根服务器应该设置到哪里呢?
毕竟一旦根服务器出故障,或者主动令某个国家无法访问对应的根服务器,导致无法使用域名解析服务,那造成的损失将是不可估计的,毕竟现在的一切事物都和互联网脱不开干系,而老百姓也不知道对应服务器的IP地址和端口号,一定会造成巨大的经济损失.
所以,全世界根域名服务器的个数和分布,其实也象征着国家的某种实力
全世界一共有13台根服务器,足足有9台在美国,也足以看出来问题.
中文域名
除了英文域名外,我们发现输入中文百度在搜索框里,同样可以访问对应的百度服务器,"百度"就是所谓的中文域名
在一开始的时候,中文域名其实并不支持,360杀毒软件的老板,28岁的周鸿祎(鸿祎教主)创建了北京三七二一科技有限公司(3721的名字由“三七二十一”而来,目标是“不管3721,中国人上网真容易”)
它能够提供对应的中文域名服务,使得用户可以直接在浏览器地址栏输入中文域名进行网站访问,而不需要记住复杂的英文域名或者IP地址
当然它的定位并不是一款搜索引擎,别人搜索引擎输入关键词,还能出现一大片与之相关的网站,而不是某个特定的网站
因此,后面李彦宏回国,创建了百度搜索引擎,并同样支持中文域名服务,没过多久,3721就在与百度竞争的过程中逐渐没落,最后被收购.