【Linux网络】深入理解HTTP/HTTPS协议:原理、实现与加密机制全面解析
协议是通信双方必须遵守的规则,确保数据能够正确传输和解析,它规定了数据格式、传输顺序、错误处理等细节。应用层的协议一般都是我们自己进行定义的,但是有很多程序员前辈已经写出来了很哇塞的协议,我们直接进行学习和使用即可
HTTP协议
认识URL
初识URL
URL(Uniform Resource Locator,统一资源定位符)就是我们口中的网址,是用来标识和定位互联网上的资源的字符串,这里的资源包括文本、图片视频等等。
URL的结构
服务器地址其实就是IP地址,IP地址标定主机的唯一性,通过IP地址和特定端口号就可以和服务器进行建立链接,通过文件路径就可以申请访问服务器特定路径下的资源信息
http就是通过http协议进行访问服务器的资源,我们能够在网络上能够进行看到的就是资源,既然是资源就需要进行存储,服务器中的资源当然也不例外,服务器中的资源当然存在于服务器的磁盘上,所以要想进行访问服务器中的资源就是要找到资源的特定路径进行访问。
urlencode和urldecode
enlencode
在URL中 ‘ / ’和 ‘ ?’是有特殊含义的分隔符,对于斜杠“/”,应该说明它在路径中的分层作用,比如如何表示资源的位置结构,还可能提到根目录和相对路径的区别;关于问号“?”,需要明确它是查询参数的起始符,后面跟着键值对,多个参数用“&”连接。当我们需要进行使用这些特殊符号进行当作我们的内容怎么办?岂不是会将这个内容当作分隔符进行处理然后造成内容出现差错??当然不会,URL会进行将特殊字符进行转义。
转义的规则如下: 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY
URL 中某些字符(如空格、中文)需转义为 %
后跟十六进制值(称为 百分号编码)。
示例:空格 → %20
,中文字符“你” → %E4%BD%A0
。
目的是确保 URL 在传输中无歧义,兼容所有系统和协议。
urldecode
http请求和响应的宏观格式
http以行为单位的协议
http 请求的宏观格式
- 请求行中的数据:请求的方法 请求的URL 请求的版本(1.0 1.1 2.0)
- 请求报头:存放各种属性(多行)大多数是key -- val的格式
- http请求的空行
- 请求正文,请求正文是可以为空的
上述结构整体称为http的报头
http响应的宏观格式
- 状态行:响应的版本 状态码(请求的正确性) 状态码描述(404->资源不存在)
- 响应报头:存放服务器的各种属性
- 空行:
- 响应正文:一般是服务器中的资源
基于宏观格式的细节问题
- http请求和响应是如何进行通信的?
http请求是通过tcp进行建立链接,然后向服务器进行发送请求;http响应也是通过tcp进行链接socket,向客户端进行返回响应。
- 请求和响应是如何保证应用层读取完毕了呢??
写个读一行的函数
通过while循环,进行读取请求行+请求报头,知道读取到空行为止
分析读取报头中的属性信息,其中有一条属性信息是正文内容的长度,
根据解析出来的正文内容,进行读取正文中的属性信息
- 请求和响应是如何做到序列化和反序列化的??
这并不需要我们自己进行实现,这是http自己进行实现的,请求行/状态行+请求报头/响应报头只需要进行按照\r\n进行1->即可!
进行编码实现验证
HTTP服务端代码的封装
//HTTP_server.hpp#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include "protocol.hpp"
#include "util.hpp"static int gbacklog = 5;
int sockid = -1;
#define SIZE 1024
typedef std::function<bool(const HttpRequest &req, HttpResponse &resp)> func_t;class HttpServer
{
public:HttpServer(func_t func,uint16_t port): _listen_sockid(-1), _port(port),_func(func){}void InitServer(){// 进行创建套接字_listen_sockid = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockid < 0){exit(1);}std::cout << "listen_sockid:" << _listen_sockid << std::endl;// 进行bind绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(_port);socklen_t len = sizeof(local);int n = bind(_listen_sockid, (struct sockaddr *)&local, len);if (bind < 0){exit(2);}// 设置socket为监听状态 //哪些客户端申请进行访问“我”(服务器)int m = listen(_listen_sockid, gbacklog);if (m < 0){exit(3);};}void StartServer(){for (;;){// 进行获取链接struct sockaddr_in client;memset(&client, 0, sizeof(client));socklen_t len = sizeof(client);sockid = accept(_listen_sockid, (struct sockaddr *)&client, &len);if (sockid < 0){exit(4);}// 进行通信// 通过创建多进程的方式进行通信pid_t id = fork();if (id == 0){// 子进程close(_listen_sockid);if (fork() > 0){exit(0);}// http处理方法httpHandler(sockid);close(sockid);// exit(0);}close(sockid);waitpid(id, nullptr, 0);}}void httpHandler(int sockid){// 进行获取完整的数据// 进行反序列化// 将反序列化的数据进行处理// 将处理的数据进行序列化// 发送完整的数据// 定义一个缓冲区将数据进行存储char buffer[4096];int n = recv(sockid, buffer, sizeof(buffer) - 1, 0);if (n <= 0){if (n == 0){std::cerr << "对方关闭写通道" << std::endl;}else{std::cerr << "读取数据失败" << std::endl;}}else{buffer[n]=0;HttpRequest req;HttpResponse resp;req.inbuffer = buffer;req.prase();_func(req, resp);send(sockid,resp.outbuffer.c_str(),resp.outbuffer.size(),0);}}~HttpServer(){if (sockid > 0){close(sockid);}}private:uint16_t _port;int _listen_sockid;func_t _func;};
HTTP服务端的调用
//HTTP_server.cpp#include "HTTP_server.hpp"
#include <memory>void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}//通过suffix和Content-Type进行补充描述信息
std::string suffixToDesc(const std::string suffix)
{std::string ret="Content-Type: ";if(suffix==".html"){ret+="text/html";}else if(suffix==".jpg"){ret += "application/x-jpg";}ret+="\r\n";return ret;
}bool Get(const HttpRequest &req, HttpResponse &resp)
{// for teststd::cout << "----------------------http request start---------------------------" << std::endl;std::cout << req.inbuffer << std::endl;std::cout << "method: " << req.method << std::endl;std::cout << "url: " << req.url << std::endl;std::cout << "httpversion: " << req.httpversion << std::endl;std::cout << "path: " << req.path << std::endl;std::cout << "suffix: " << req.suffix << std::endl;std::cout << "size: " << req.size << std::endl;std::cout << "----------------------http request end---------------------------" << std::endl;std::string respline = "HTTP/1.1 200 OK\r\n";// 这里也不想进行硬编码// std::string respheader = "Content-Type: text/html\r\n";std::string respheader = suffixToDesc(req.suffix);std::string respblank = "\r\n";// 不要进行硬编码// std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>北京交通广播《一路畅通》“交通大家谈”节目,特邀北京市交通委员会地面公交运营管理处处长赵震、北京市公安局公安交通管理局秩序处副处长 林志勇、北京交通发展研究院交通规划所所长 刘雪杰为您解答公交车专用道6月1日起社会车辆进出公交车道须注意哪些?</p></body></html>";// 从文件中进行获得std::string body;body.resize(req.size+1);if (Util::readFile(req.path, (char*)body.c_str(),body.size()) == 0){Util::readFile(error_html, (char*)body.c_str(),body.size());}// 进行填充responseresp.outbuffer += respline;resp.outbuffer += respheader;resp.outbuffer += respblank;std::cout << "----------------------http response start---------------------------" << std::endl;std::cout << resp.outbuffer << std::endl;std::cout << "----------------------http response end---------------------------" << std::endl;resp.outbuffer += body;return true;
}int main(int args, char *argv[])
{if (args != 2){Usage(argv[0]);exit(0);}uint16_t port = atoi(argv[1]);std::unique_ptr<HttpServer> hs(new HttpServer(Get, port));hs->InitServer();hs->StartServer();return 0;
}
HTTP协议有关的封装
//prococal.hpp#pragma once
#include <iostream>
#include <sstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>const std::string sep = "\r\n";
const std::string default_root = "./wwwroot";
const std::string home_page = "index.html";
const std::string error_html = "./wwwroot/404.html";
#include "util.hpp"
class HttpRequest
{
public:HttpRequest(){}~HttpRequest(){}void prase(){// 1. 从inbuffer中拿到第一行,分隔符\r\nstd::string line = Util::getOneLine(inbuffer, sep);if (line.empty())return;// 2. 从请求行中提取三个字段// std::cout << "line: " << line << std::endl;std::stringstream ss(line);ss >> method >> url >> httpversion;// 3. 添加web默认路径path = default_root; // ./wwwroot,path += url; //./wwwroot/a/b/c.html, ./wwwroot/if (path[path.size() - 1] == '/')path += home_page;// 4.进行后缀的查询auto pos = path.rfind(".");if (pos == std::string::npos){suffix = ".html";}else{suffix = path.substr(pos);}// 5.进行获取资源大小struct stat st;int n = stat(path.c_str(), &st);if (n == 0)size = st.st_size;elsesize = -1;}public:std::string inbuffer;// std::string reqline;// std::vector<std::string> reqheader;// std::string body;std::string method;std::string url;std::string httpversion;std::string path;std::string suffix;int size;
};class HttpResponse
{
public:std::string outbuffer;
};
其他有关函数的封装
//util.hpp#pragma once #include<iostream>
#include<string>
#include<fstream>class Util
{
public:static std::string getOneLine(std::string& buffer,const std::string& sep){auto pos=buffer.find(sep);if(pos==std::string::npos){return "";}std::string ret=buffer.substr(0,pos);return ret;}static bool readFile(const std::string path,char* buffer,int size){//以二进制的形式进行打开读取pathstd::ifstream in(path);if(!in.is_open()){return false;}//error:// std::string line;// while(std::getline(in,line))// {// body+=line;// }in.read(buffer,size);in.close();return true;}
};
通过上面的代码我们可以清晰的得知,网页资源其实是通过多个资源进行整合而成的,我们要进行访问一整个网页资源,需要进行发送若干次的网络请求。
结合代码加强对宏观结构的影响
HTTP方法
http请求不但可以进行获取资源还是可以进行资源的交互的。我们通过http进行向服务端进行数据的上传时,本质事通过form表单进行提交的,浏览器会自动将form表单中的内容转化成http方法进行请求,http常用的两种方法分别是POST/GET方法
POST/GET两种方法的区别
- GET方法是将用户通过HTTP进行提交的数据直接进行拼接到URL上,也就是说GET方法是通过URL进行传递参数
- POST方法是将用户通过HTTP进行提交的数据进行放到正文部分,通过正文进行参数的提交
说明:
GET方法进行提交参数由于是通过URL进行提交的,所以说我们通过URL就可以看到我们进行通过HTTP提交的参数,POST由于是通过正文进行提交参数的,所以一般用户是看不到的,私密性正好,但是不敢说通过POST就比通过GET方法安全性更高,我们通过抓包工具都是可以进行看到参数的明文的,要想进行提高安全性,必须进行加密,https就是经过加密之后的。
HTTP状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
3XX是重定向状态码,重定向在网络中的是如何进行理解的呢?
上述这种先进行访问服务器,然后服务器通过返回一个3XX的错误码,并且附带一个新的URL,客户端通过新的URL进行访问的方式称为重定向
重定向分为临时重定向和永久重定向
临时重定向:当我们进行访问某个网站的时候,首先自动为我们进行跳转到广告商金主爸爸那里,之所以称为临时重定向是因为这种跳转URL并不是唯一的,当金主爸爸换人之后进行跳转的链接也就不一样了
永久重定向:与临时重定向不同的是,永久重定向进行跳转的目标每次都是唯一的,例如一个网站慢慢壮大之后由于服务器配置不够用了,需要进行将一些用户进行引流到新的网站,当老用户进行登陆后,通过URL 进行识别,然后跳转新的服务器进行使用,这种方式就是永久重定向。
长链接的概念
通过http进行请求网络资源,由于http网页中的资源是由若干部分进行组成的,按道理也就是说客户端需要多次通过http请求才能完整的获取网页中的资源,但是http协议是应用层的协议是基于传输层的TCP的,TCP是面向链接的,会出现频繁创建链接的问题。
所以说client如果和server都支持长链接,直接通过"一条长链接进行读取大份资源。"进行获取大份资源的理解其实本质上是通过一个TCP连接进行传输多个HTTP请求和响应。
Connection: keep-alive 支持长链接
Connection: close 不支持长连接
会话保持
会话保持的认识
当我们通过浏览器进行通过URL进行访问指定服务器的资源时,首次进行访问时需要进行信息的认证才可以进行访问服务器中的资源,当我们短时间内再次进行访问网站,继续需要用户认证,这对于用户来说体验感是非常差的;当服务器的资源并不是完全部署在一台服务器上的,而是网站文件放在一台服务器,资源(如图片、视频)存放在另一台服务器上,当我们进行访问资源时,每进行访问一个资源就首先开始进行用户认证登录,这是非常非常难受的。
由于HTTP本身是无状态的,为了解决上述用户体验问题,让用户进行登录后就可以按照自己的身份(因为有些资源是有权限的,例如VIP视频)进行随意访问,这种方式就叫做会话保持(保证用户长时间在线)。
会话保持的实现
老方法
cookie文件中存储的信息一般包括用户标识信息(用户ID:唯一标识用户的编号或字符串。用户名:用户的登录名或显示名。会话ID:用于识别用户当前会话的唯一标识符。认证信息(登录状态:标记用户是否已登录,令牌:用于身份验证的令牌)。偏好设置(用户选择的语言、用户选择的界面主题等等)。浏览行为信息(记录用户浏览历史)
补充:cookie是分为cookie文件级别和cookie内存级别,如何进行区分cookie是文件级别还是内存级别,通过以浏览器进行为例,当我们通过进行将浏览器进行访问特定服务器,当我们将浏览器这个进程进行杀掉后,在进行重新打开浏览器进行访问特定的服务器如果不需要重新进行认证登录则cookie就是内存级的,怎么理解呢??如果这个cookie是内存级别的,则这个cookie是存在在进程中的,当进程被杀掉后,则这个cookie就消失了,所以说需要进行重新认证登录。而文件级别的Cookie则被写入到用户电脑的某个文件夹中,比如在Windows系统里可能是特定的目录下。cookie技术分为cookie文件级别(关闭浏览器)和内存级别 ----cookie是内存级别还是文件级别可以在浏览器中进行配置。
老方法可能存在的问题
当一些黑客通过某些方式进行将木马散播出去(例如陌生邮件中的链接等等),当误点进入这个链接中时,木马就会浸入我们的浏览器中进行盗取我们浏览器中cookie文件中的信息,例如访问某些特定服务器中的身份信息(用户ID,账号密码等等),黑客通过拿到这些cookie信息直接模拟用户的身份验证,直接进行访问服务器。
新方法
新方法通过将用户的信息及使用缓存痕迹不在浏览器中进行存储了,直接进行放到服务端中session文件中,当用户首次进行申请该服务器中的资源时,服务器接收到申请后进行返回唯一的session id作为身份验证的信息,在会话保持中通过session id进行身份验证进行访问服务器中的资源。即使黑客进行拿到我们的cookie文件,cookie文件中只是验证信息并没有用户的数据,很大程度上减少了用户数据泄漏的风险。
新方法可能存在的问题
当黑客同样将木马进行散播进入我们的服务器中进行窃取我们的cookie文件,虽然减少了用户数据泄漏的风险,但还是无法进行阻止别人通过cookie文件中的信息进行绕开服务器中的验证进行访问服务器,但是这种方案极易配合其他策略进行增加安全性,例如当服务器进行检测到用户的IP发生短时间的长距离跨越时,会进行向绑定的设备进行推送警告信息,甚至进行强制下线。
HTTPS协议
通过上面两个工具的使用,我们可以看到通过HTTP进行数据的传输,无论GET方法还是POST方法,数据无异于进行裸奔,HTTPS就是通过加密的方式进行解决这种问题
简单认识HTTPS的加密过程
HTTP通过SSL/TLS握手过程中进行实现将HTTP的明文数据进行加密形成的密文数据通过协议栈进行传输到另一台主机,然后通过将数据进行解密进行拿到发送方进行网络通信的内容
数据指纹
数据指纹又称为数据摘要,其基本原理是利用单向散列函数(Hash函数)对信息进行运算,生成一串固定长度的字符串。数据指纹并不是一种加密机制,但是可以用来进行判断数据有没有被篡改。
常见的摘要算法:MD5,SHA1,SHA256,SHA512等,算法将无线映射成有限,因此也是有可能出现哈希碰撞,但是概率是非常非常的低的。
数据指纹是不属于加密过程的,因为加密过程是可逆的能够进行解密,但是数据指纹是通过映射性成的,不能进行可逆的解密过程。
应用:
进行判断两份数据是否是相同的,如果两份数据通过相同的算法进行哈希映射,将映射后的数据进行比对,如果相同则则是同一份数据,反之则是不同的数据
百度网盘的秒传功能就是通过这种方式进行实现的,当用户A进行传入某种资源后,当用户B在进行上传相同的资源时,通过查询数据摘要是否存在,如果数据存在,百度网盘并不会再将相同的资源进行重新上传一份,而是通过建立通过建立软连接的形式进行让用户B进行访问资源.
数据库中的表单行列中的密码行列也是通过某种算法经过Hash映射进行形成数据摘要,即使数据库进行泄漏也较大程度保护用户的隐私.
HTTPS的加密过程
在进行网络通信的过程中,往往会出现数据安全问题,例如数据被监听,数据被篡改,只有通过加密的方式解决这几种问题,通过下面几种方式进行理解对称加密和非对称加密,并且分析各种各种方案的优劣,找出最优加密方式
只采用对称加密
客户端在进行推送数据之前首先将自己密钥进行送,但是在进行推动密钥的过程中,由于密钥是没有进行机密的,容易被中间人进行拿到并拷贝,当服务端在进行通过密钥进行机密数据,让加密的数据在网络中进行传递时,中间人也是有密钥的,直接可以对数据进行监听和修改.
只采用非对称加密
采用非对称加密虽然能够继续宁确保客户端到服务端的数据安全,因为即使中间人进行窃取客户端向服务端的数据,但是无法对加密的数据进行解密,服务端到客户端的数据不安全由于服务端在进行推送自己的公钥给客户端时容易被中间人拿到并拷贝,导致服务端后续进行向客户端进行发送数据的时候容易被篡改,导致后续进行数据传输的时候造成数据不安全问题。
双方使用非对称加密
双方都采用非堆成的加密方式,每一方都将自己的自己的公钥进行推送给对方,之后在进行数据传输的过程中,都是用对方的公钥进行加密,这样只能通过对应的私钥才能进行解密,即使中间人拿到双发的公钥也无法进行解密,确保数据的安全。
但是这种方式加解密过程依赖复杂的数学运算(如大数分解或椭圆曲线运算),相比对称加密速度慢数百倍,且加密后的数据体积更大,不适合处理大量传输数据。而对称加密算法(如 AES)结构简单、加解密速度快、资源消耗低,适合高效的数据传输
采用非对称加密+对称加密
这种方式先通过非对称加密进行将客户端的对称密钥进行安全的交给服务端,然后后续直接通过对称加密即可,这种方式并没有将加密的对称密钥进行暴露给中间人,又可以进行高效传输。这种方式兼顾了安全性与性能。