目录

一、认识URL

二、urlencode和urldecode 

三、HTTP协议格式 

 1.HTTP请求 

2.HTTP响应 

3.简单获取一个http响应

4.简单使用抓包工具 fiddler

四、简单的http代码 

1.小知识点

(1)recv

(2)首页 

2.Http代码书写vision1

 3.http代码vision2

 Socket.hpp

 Log.hpp

 Makefile

 HttpServer.hpp

HttpServer.cc

wwwroot/index.html

wwwroot/a/b/hello.html

wwwroot/a/b/word.html

五、HTTP中的方法

六、HTTP中的状态码 

七、HTTP常见Header 

八、https协议

0.预备知识 

 1.为什么要加密 -- 臭名昭著的运营商劫持

2.常见的加密方式

(1)对称加密 

(2)非对称加密 

3.数据摘要(数据指纹) 

4.数字签名 

九、https 的工作过程探究

方案一、只使用对称加密 

方案二、只使用非对称加密

方案三、双方都使用非对称加密 

方案四、对称加密+非对称加密

方案二三四的安全问题 --中间人攻击MITM攻击

引入证书

(1)CA认证

(2)CSR文件 

(3)数据签名 

(4)为什么不直接通过加密数据形成签名 ,而是先形成摘要

(5)客户端认证证书

方案五非对称加密+对称加密+证书认证 


一、认识URL

        平时我们俗称的 "网址" 其实就是说的 URL,Uniform Resource Locator(统一资源定位符)

https://www.baidu.com/

        这是百度首页的URL,域名经过域名解析后会转化成ip地址 。所以实际上我们访问这个网址还是需要ip地址和端口号的。在浏览器访问我们用的基本上是http和https协议,这些协议它们的端口号几乎是固定的,http是80,https是443.因此只需要域名解析得到ip地址即可访问。

        以下是一个完整url的构成。

        第一部分是协议,第二部分是登录信息,不过现在已经很少用了,第三部分服务器地址,也即域名,第四部分端口号,这部分可以由协议得知,可以省略,第五部分是文件路径,从web根目录开始,第六部分查询字符串,是一个key value结构,可以供我们传参,各参数之间由&分隔最后一部分片段标识符,不太常见 

二、urlencode和urldecode 

        像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
        转义的规则如下:将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式

        "+" 被转义成了 "%2B"
        urldecode就是urlencode的逆过程;

三、HTTP协议格式 

 1.HTTP请求 

(1) 首行: [方法] + [url] + [版本]
(2)Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
(3)Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;(我们无法保证读到的是一个完整的http请求,但是由于有空行,我们借此可以保证读到完整的首行和报头,因此如果有正文存在,我们可以通过报头里的长度kv值来读)

2.HTTP响应 

 

(1) 首行: [版本号] + [状态码] + [状态码解释]
(2)Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
(3)Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.

3.简单获取一个http响应

可以用telnet工具简单获取一个http响应。 

我们可以看到Content-Length: 29506 

4.简单使用抓包工具 fiddler

上面是请求,下面是响应,不过由于经过加密,看得不是很清楚 

        其实本质上就是一个代理,左边的大框内,左边的框代表浏览器,右边的代表fiddler,原本是由我们的浏览器把请求给服务器,但是现在启动fiddler后,浏览器会先将请求交给fiddler,由fiddler转交。而同样的,收消息也是如此,服务器返回响应也会先交给fiddler。

四、简单的http代码 

1.小知识点

(1)recv

        我们之前进行网络读取的时候都用的read,这里使用一下recv,其中前三个参数以及返回值都是一样的,多了一个flags,代表的是读取的方式,如非阻塞读等等,我们这里默认设为0,这样它和read的作用就一模一样了。

 

(2)首页 

如果我们之间用ip地址(+端口号)访问,那么浏览器会默认给我们拼接一个/,即web根目录,此时我们访问的就是该网站的首页。 

2.Http代码书写vision1

粗略写完一个http服务器,启动后用  netstat -nltp 查看

我们用浏览器访问后,通过服务器的打印功能,可以看到请求报文

        例如我们可以看到这里面的UserAgent内容,里面包含了我们的客户端信息。这个User Agent的检验也能作为我们 反爬策略,如果服务器检测到请求里不包含user agent或者其格式有误,那么就不进行http响应。

        另外我们如果要下载某个软件,用浏览器去检索的话,我们检索出来的首先都是符合本机系统的下载渠道,这也和这个User Agent有关。

接下来我们简单硬编码了一个响应。我们用浏览器访问就i可看到这段hello word 的文本了

  //返回相应的过程
            std::string text = "hello word";
            std::string response_line = "Http/1.0 200 OK\r\n";
            std::string response_header = "Content Length: ";
            response_header += std::to_string(text.size());
            response_header += "\r\n";
            std::string blank_line = "\r\n";//空行

            std::string response = response_line;
            response += response_header;
            response += blank_line;
            response += text;

            send(sockfd, response.c_str(), response.size(), 0);

 

        而我们在正文部分,其实是可以返回一个简单的网页的。 网页其实就是文本文件,它通常以文件的形式呈现出来,我们如果在网络发送时把这个文件的内容读出来,并且作为响应的正文部分,那么客户端就能读到网页了。

我们硬编码一段网页文本看看效果

 //返回相应的过程
            std::string text = "<html><body><h3>hello word<h3></body></html>";
            std::string response_line = "Http/1.0 200 OK\r\n";
            std::string response_header = "Content Length: ";
            response_header += std::to_string(text.size());
            response_header += "\r\n";
            std::string blank_line = "\r\n";//空行

            std::string response = response_line;
            response += response_header;
            response += blank_line;
            response += text;

            send(sockfd, response.c_str(), response.size(), 0);
        }

 3.http代码vision2

 Socket.hpp

#pragma

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"

enum
{
    SocketErr = 2,
    BindErr,
    ListenErr
};

const int backlog = 10;

class Sock
{
public:
    Sock()
    {
    }
    ~Sock()
    {
    }

public:
    void Socket()
    {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
            exit(SocketErr);
        }
        int opt = 1;
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR,&opt,sizeof(opt));
    }
    void Bind(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(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
            exit(SocketErr);
        }
    }
    void Listen()
    {
        if (listen(sockfd_, backlog) < 0)
        {
            lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
            exit(ListenErr);
        }
    }

    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(sockfd_, (struct sockaddr*)& peer, &len);
        if(newfd < 0)
        {
            lg(Warning, "listen error, %s: %d", strerror(errno), errno);
            return -1;
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }
    int Connect(const std::string &ip, const uint16_t &port)
    {
        struct sockaddr_in peer;
        memset(&peer,0,sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
        if(n == -1)
        {
            std::cerr << "connect to " << ip << ";" << port << "error" << std::endl;
            return false;
        }
        return true;
    }
    void Close()
    {
        close(sockfd_);
    }
    int Fd()
    {
        return sockfd_;
    }
private:
    int sockfd_;
};

 Log.hpp

#pragma once

#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.h>
#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

class Log
{
public:

    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch(level)
        {
            case Info: return "Info";
            case Debug: return "Debug";
            case Warning: return "Warning";
            case Error :return "Error";
            case Fatal :return "Fatal";
            default: return "None";
        }
    }
    // void logmessage(int level,const char *format, ...)
    // {
    //     time_t t = time(nullptr);//时间戳
    //     struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //         ctime->tm_year+1900, ctime->tm_mon+1,ctime->tm_mday,
    //         ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//把字符串存到leftbuffer里面

    //     va_list s;
    //     va_start(s, format);
    //     char rightbuffer[SIZE];
    //     vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
        

    //     //格式 默认部分(左)+自定义部分(右)
    //     char logtxt[SIZE*2];
    //     snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);

    //     printLog(level, logtxt);//暂时打印
    // }
    void printLog(int level, std::string logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std:: endl;
            break;
        case Onefile:
            printOneFile("LogFile" ,logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
        default:
            break;
        }
    }
    void printOneFile(const std:: string logname, const std::string logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY|O_CREAT|O_APPEND, 0666);//LogFile
        if(fd < 0)return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string logtxt)
    {
        std::string filename = "LogFile";
        filename += ".";
        filename += levelToString(level);//LogFile.Debug/Warning/Fatal
        printOneFile(filename, logtxt);
    }
    ~Log()//这里析构只是为了让类看起来完整
    {

    }
    void operator()(int level,const char *format, ...)
    {
        time_t t = time(nullptr);//时间戳
        struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
            ctime->tm_year+1900, ctime->tm_mon+1,ctime->tm_mday,
            ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//把字符串存到leftbuffer里面

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
        

        //格式 默认部分(左)+自定义部分(右)
        char logtxt[SIZE*2];
        snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }
private:
    int printMethod;
    std :: string path;
};

Log lg;

 Makefile

HttpServer:HttpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f HttpServer

 HttpServer.hpp

#pragma once

#include <iostream>
#include "Socket.hpp"
#include "Log.hpp"
#include <string>
#include <pthread.h>
#include<fstream>
#include<vector>
#include<sstream>

const std::string wwwroot="./wwwroot";//web根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";

static const int defaultport = 12345;

class HttpServer;

class ThreadData
{
public:
    ThreadData(int fd):sockfd(fd)
    {}
public:
    int sockfd;
};
class HttpRequest
{
public:
    void Deserialize(std::string req)
    {
        while(true)
        {
            std::size_t pos = req.find(sep);
            if(pos == std::string::npos)break;
            std::string temp = req.substr(0,pos);
            if(temp.empty())break;//遇到空行跳出
            req_header.push_back(temp);
            req.erase(0, pos+sep.size());//找一行移走一行 移走的时候把""\r\n"也一起移走
        }
        text = req;//剩下的内容都交给正文 本来是应该根据Content length从空行后截取相应文本的,这里也从简粗糙处理
    }
    void Parse()
    {
        std::stringstream ss(req_header[0]);
        ss >> method >> url >> http_version;
        file_path = wwwroot;//一开始就是./wwwroot
        if(url == "/" || url == "/index.html")
        {
            file_path += "/";
            file_path += homepage;//./wwwtoot/index.html
        }
        else file_path += url;// /a/b/c/d.html ->./wwwroot/a/b/c/d.html;
    }
    void DebugPrint()
    {
        std::cout << "-----------------------------------" << std::endl;
        for(auto &line :req_header)
        {
            std::cout << line << "\n\n";
        }
        std::cout << text << std::endl;
    }
public:
    std::vector<std::string> req_header;
    std::string text;

    //解析之后的结果
    std::string method;
    std::string url;
    std::string http_version;
    std::string file_path;
};
class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport):port_(port)
    {
    }
    bool Start()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        while(true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            if(sockfd < 0)continue;
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd);
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }
    static std::string ReadHtmlContent(const std::string &htmlpath)
    {
        //坑
        std::ifstream in(htmlpath);
        if(!in.is_open())return "404";

        std::string content;
        std::string line;
        while(std::getline(in,line))
        {
            content += line;
        }
        in.close();
        return content;
    }
    static void HandlerHttp(int sockfd)
    {
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer)-1, 0);//这里这样写其实也有bug,因为我们也无法保证一次取得的就是一个完整报文,最好也进行报文检验
        if(n > 0)//这里我们暂时认为我们读到的是一个完整独立的http请求
        {
            buffer[n] = 0;
            std::cout << buffer;

            HttpRequest req;
            req.Deserialize(buffer);
            req.Parse();
            //req.DebugPrint();
            std::string text = ReadHtmlContent(req.file_path);

            std::string response_line = "Http/1.0 200 OK\r\n";
            std::string response_header = "Content Length: ";
            response_header += std::to_string(text.size());
            response_header += "\r\n";
            std::string blank_line = "\r\n";//空行

            std::string response = response_line;
            response += response_header;
            response += blank_line;
            response += text;

            send(sockfd, response.c_str(), response.size(), 0);
        }
        close(sockfd);
    }
    static void *ThreadRun(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData*>(args);
        HandlerHttp(td->sockfd);
        delete td;
        return nullptr;
    }
   

    ~HttpServer()
    {}
private:
    Sock listensock_;
    uint16_t port_;
};

HttpServer.cc

#include "HttpServer.hpp"
#include <iostream>
#include <memory>
#include "Log.hpp"

using namespace std;

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);
    //HttpServer *svr = new HttpServer();
    std::unique_ptr<HttpServer> svr(new HttpServer());
    svr->Start();

    return 0;
}

wwwroot/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>hello hello</h1>
    <h1>皮皮你好 火蜥蜴你好</h1>
    <h1>这是第一个网页</h1>
    <a href="http://120.55.47.126:12345/a/b/hello.html">到第二张网页</a>
    <a href="http://120.55.47.126:12345/x/y/word.html">到第三张网页</a>
</html>

wwwroot/a/b/hello.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>hello hello</h1>
    <h1>这是第二个网页</h1>
    <a href="http://120.55.47.126:12345">回到首页</a>
    <a href="http://120.55.47.126:12345/x/y/word.html">到第三张网页</a>
</body>
</html>

wwwroot/a/b/word.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>hello hello</h1>
    <h1>这是第三个网页</h1>
    <a href="http://120.55.47.126:12345">回到首页</a>
    <a href="http://120.55.47.126:12345/x/y/word.html">到第三张网页</a>

</body>
</html>

五、HTTP中的方法

下面表格中有很多的方法,但是日常中用到的百分之九十五以上的方法只有GET和POST。

百分之八十以上的方法是GET。 而如果我们想要把对应的数据交给服务器,那么就需要POST

 

 

        GET方法提交参数时会回显在url,因此不私秘一些。至于安全性,其实两种方法都不安全,安全性问题是通过加密来进行处理的 

六、HTTP中的状态码 

        一旦出现http错误,网络又没问题,无外乎就是客户端错误和服务器错误了 ,因此这些都比较好理解,最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)。

            而重定向状态码需要结合Http的常见报头来讲,其中的location状态码,location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问。我们来看下图,例如我们浏览器向某服务器发起请求,但是这个服务器暂时停止服务了,给我们返回一段报文,携带location字段,此时我们的客户端就会发起二次访问,再去访问别的服务器。

        重定向有永久重定向和临时重定向。

  1. 永久重定向(301)

    • 浏览器会缓存重定向结果,后续对原 URL 的请求会直接使用新 URL,无需再次向服务器确认。
    • 示例:若网站从 http://old.com 永久迁移到 https://new.com,返回 301 后,浏览器下次访问 old.com 会自动跳转到 new.com
  2. 临时重定向(302/307)

    • 浏览器不会缓存重定向结果,每次请求原 URL 时都会向服务器重新确认是否需要重定向。
    • 示例:用户访问登录页面时,未登录则返回 302 跳转到登录页,登录完成后下次访问仍从原 URL 发起请求。

七、HTTP常见Header 

 Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

        除了以上的一些Header我们再谈谈Connection,在此之前我们还需要补充短链接和长链接的知识。

        例如此时我们需要访问一个很大的网页,里面包含着十张图片(一个巨大的网页会包含非常多的元素,每个元素都是一个资源,这里以图片为例子),那么此时我们要获取这些图片也需要发起请求。如果我们使用短链接的话,每次请求得到响应后就会关闭链接,我们就需要重复十一次这样的过程。而如果使用长链接,我们建立一次链接后,一次性把十一个请求都发过去,得到响应后才关闭链接,也就是说我们只需要建立,断开一次链接。

        http/1.0只支持且只支持短链接的方式,http/1.1默认采用的则是长链接的方式。客户端和服务器的版本可能会不同,因此在前几次的请求和响应的过程中会进行沟通,协商http的版本来决定采用哪种链接。

        如果Connection为keep-alive,那么说明客户端和服务器采取长链接的方式进行通信。

        了解了以上知识,我们向网页中插入图片来进行验证一下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>hello hello</h1>
    <h1>这是第一个网页</h1>
    <img src="/image/1.jpg" alt="这是夏弥">
    <img src="/image/2.jpg" alt="这是楚子航和夏弥">
    <a href="http://120.55.47.126:12345/a/b/hello.html">到第二张网页</a>
    <a href="http://120.55.47.126:12345/x/y/word.html">到第三张网页</a>
</html>

但是我们发现,图片却加载不出来。

        这是因为我们必须告诉浏览器,我们的图片是什么格式的,浏览器才能识别出来。 此时就涉及到了 Content-Type: 数据类型(text/html等)

我们在解析报头的时候加一个变量来记录后缀,然后再在报头中加入Content_Type即可

#pragma once

#include <iostream>
#include "Socket.hpp"
#include "Log.hpp"
#include <string>
#include <pthread.h>
#include<fstream>
#include<vector>
#include<sstream>
#include<unordered_map>

const std::string wwwroot="./wwwroot";//web根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";

static const int defaultport = 12345;

class HttpServer;

class ThreadData
{
public:
    ThreadData(int fd, HttpServer *ts):sockfd(fd), svr(ts)
    {}
public:
    int sockfd;
    HttpServer *svr;
};
class HttpRequest
{
public:
    void Deserialize(std::string req)
    {
        while(true)
        {
            std::size_t pos = req.find(sep);
            if(pos == std::string::npos)break;
            std::string temp = req.substr(0,pos);
            if(temp.empty())break;//遇到空行跳出
            req_header.push_back(temp);
            req.erase(0, pos+sep.size());//找一行移走一行 移走的时候把""\r\n"也一起移走
        }
        text = req;//剩下的内容都交给正文 本来是应该根据Content length从空行后截取相应文本的,这里也从简粗糙处理
    }
    void Parse()
    {
        std::stringstream ss(req_header[0]);
        ss >> method >> url >> http_version;
        file_path = wwwroot;//一开始就是./wwwroot
        if(url == "/" || url == "/index.html")
        {
            file_path += "/";
            file_path += homepage;//./wwwtoot/index.html
        }
        else file_path += url;// /a/b/c/d.html ->./wwwroot/a/b/c/d.html;

        auto pos = file_path.rfind(".");
        if(pos == std::string::npos) suffix = ".html";
        else suffix = file_path.substr(pos);
    }
    void DebugPrint()
    {
        std::cout << "-----------------------------------" << std::endl;
        for(auto &line :req_header)
        {
            std::cout << line << "\n\n";
        }
        std::cout << text << std::endl;
    }
public:
    std::vector<std::string> req_header;
    std::string text;

    //解析之后的结果
    std::string method;
    std::string url;
    std::string http_version;
    std::string file_path;

    std::string suffix;//用于记录文件后缀
};
class HttpServer
{
public:
    HttpServer(uint16_t port = defaultport):port_(port)
    {
        content_type.insert({".html","text/html"});
        content_type.insert({".jpg","image/jpeg"});
    }
    bool Start()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        while(true)
        {
            std::string clientip;
            uint16_t clientport;
            int sockfd = listensock_.Accept(&clientip, &clientport);
            if(sockfd < 0)continue;
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this);
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }
    static std::string ReadHtmlContent(const std::string &htmlpath)
    {
        //坑 这样写我们是无法完整读到图片的,因为图片是以二进制的方式呈现的
        // std::ifstream in(htmlpath);
        // if(!in.is_open())return "";

        // std::string content;
        // std::string line;
        // while(std::getline(in,line))
        // {
        //     content += line;
        // }
        // in.close();
        // return content;
        std::ifstream in(htmlpath, std::ios::binary);
        if(!in.is_open()) return "";

        in.seekg(0, std::ios_base::end);
        auto len = in.tellg();
        in.seekg(0, std::ios_base::beg);

        std::string content;
        content.resize(len);

        in.read((char*)content.c_str(), content.size());//这样写其实是不规范的 我们为了不修改太多代码才这么写
        in.close();
        return content;
    }
    std::string SuffixToDesc(const std::string &suffix)
    {
        auto iter = content_type.find(suffix);
        if(iter == content_type.end())return content_type[".html"];
        else return content_type[suffix];
    }
    void HandlerHttp(int sockfd)
    {
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer)-1, 0);//这里这样写其实也有bug,因为我们也无法保证一次取得的就是一个完整报文,最好也进行报文检验
        if(n > 0)//这里我们暂时认为我们读到的是一个完整独立的http请求
        {
            buffer[n] = 0;
            std::cout << buffer;

            HttpRequest req;
            req.Deserialize(buffer);
            req.Parse();
            //req.DebugPrint();
            //返回相应的过程
            std::string text;
            bool ok = true;
            text = ReadHtmlContent(req.file_path);
            if(text.empty())
            {
                ok = false;
                std::string err_html = wwwroot;
                err_html += "/";
                err_html += "err.html";
                text = ReadHtmlContent(err_html);
            }


            std::string response_line;
            if(ok)
                response_line = "Http/1.0 200 OK\r\n";
            else
                response_line = "Http/1.0 404 Not Found\r\n";
            std::string response_header = "Content Length: ";
            response_header += std::to_string(text.size());
            response_header += "\r\n";
            response_header += "Content-Type: ";
            response_header += SuffixToDesc(req.suffix);
            response_header += "\r\n";

            std::string blank_line = "\r\n";//空行

            std::string response = response_line;
            response += response_header;
            response += blank_line;
            response += text;

            send(sockfd, response.c_str(), response.size(), 0);
        }
        close(sockfd);
    }
    static void *ThreadRun(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData*>(args);
        td->svr->HandlerHttp(td->sockfd);
        delete td;
        return nullptr;
    }
   

    ~HttpServer()
    {}
private:
    Sock listensock_;
    uint16_t port_;
    std::unordered_map<std::string, std::string> content_type;
};

运行服务器并访问后,我们看到果然申请图片的时候也会发起请求。 

最后我们再讲cookie

        讲之前我们先要补充一个概念,即http协议默认是无状态的。例如我们对一个网页不断刷新的时候,浏览器是会向服务器不断发起请求的。http协议不记录我们以及访问过这个网页了。所以请求一次和下次请求,是两个独立互不干扰的事件。

        那么如果我们访问某个网站中的一个视频,观看这个视频需要我登录,此时我登录了,然后我再进行观看下一个视频的时候,如何记录我的登录状态呢。

        所以我们现在来谈一下,http对登录用户的会话保持功能。

        1.我们先口头表达一下这种现象,例如我们登录blibili,然后我们关闭网页,再进入的时候发现它不需要我们再重新登录了,甚至我们关闭电脑重新启动,再访问的时候,它依然记录着我们的登录状态。

        2.再大致讲一下它的原理

        首先我们在某次访问的时候进行了登录,把账号和密码交给了服务器。

        服务器返回响应的时候会带有 set cookie字段,里面包含了一些参数,其中有我们的账号和密码等信息。

        浏览器接收到响应之后则会把set cookie中的账号密码数据存到它的cookie文件中。

        之后每次使用浏览器访问服务器的时候,我们的请求报文都会带上cookie内的信息,因此我们就不需要每次都进行登录了。 

        反之,如果我们退出登录,那么浏览器中的cookie文件也就会删除相应的数据。

        cookie文件的保存方式也有两种,一种是内存级,我们的浏览器也是一个进程,它其中也会new malloc空间,因此这些数据可以存在这里面,而一旦关闭浏览器,这些数据就消失了。

        第二种则是文件级,将相关的数据保存在浏览器相关的安装路径下,把数据刷新到磁盘,这样即使重新开机也能不用经过登录直接认证了。 

 3.再通过实验看一下现象

        我们把服务器发过去的响应报文写一个set-cookie(不过这里的数据不太严谨,&&是用于请求中的,而我们set-cookie位于响应中,所以其实不太合适这么写)

        之后访问浏览器就记录了这个cookie 

之后浏览器每次的请求都多带了一个Cookie的值 

但是这样显然有两个非常大的问题

1.是个人私有信息泄露

2. cookie信息被盗取

这两个问题也有交织的地方。

因此现在一般采用的是cookie+session的方式

        服务器在对客户端发送过来的数据进行认证之后,会为我们创建一个session文件,它有自己的session ID作为标识,里面存储的是用户的合法信息以及登录有关。

        有了session文件以及session ID之后,服务器响应的set-cookie就不需要是用户的具体私密信息了,而是有session ID即可,而浏览器之后发起请求的时候添加Session ID即可。

        如果session ID有效那么就无需重新登录,而如果失效那么会重定向返回登录页面进行登录。

而实际上为了管理如此多的用户信息,这些session文件实际上是会交给redis处理的

        虽然这种方式无法避免cookie信息被盗取,但是使用这种方式之后,即使有人取得了他人的cookie,也无法获得其他人的私密信息的。

        实际上,即使服务器做得再好,也无法防止用户泄露自己的cookie造成的问题。所以cookie被盗取问题是无法完全被解决的。

        不过由于session 信息是由服务器统一管理的,因此服务器也能通过一些方式操控session信息来比较大程度地避免cookie盗取问题,例如服务器发现客户端两次登录的位置不同,就会让你进行重新登录等等。

八、https协议

0.预备知识 

        通过了解http协议,以及之前的学习,我们知道,网络各层都只关注数据的传输,而并不会负责数据加密解密等工作 。所以为了保障我们的数据安全,有人引入了一层软件层,即ssl加密解密层。它与http协议共同构成https协议。

 下面提一点常识性的东西

1.对于加密解密的方案,并没有绝对的安全,只需要 攻破的成本> 攻破后的收益 那么其实这种加密解密方案就是一个合理的方案了。

2.ssl是一个权威的加密解密方案,但它也不一定任何时候都是安全的 。因为使用的人很多,因此来攻击的人也很多,所以需要ssl社区的人员进行更新维护,如果一些企业并没有及时更新ssl版本,那么就可能被现有的攻破方案进行攻破。

我们首先先大概了解一下一些相关概念

即明文,密钥和密文。

例如我们使用x ^ y ^ y  = x 为根据来作为加密解密策略。

  在下列图片中,7 是我们的明文,5是密钥,明文经过密钥加密 变成2,即密文。我们发送的时候发送的即是这个2,被另一端接收后密文再通过密钥解密就重新得到了明文7.

 1.为什么要加密 -- 臭名昭著的运营商劫持

        由于我们通过网络传输的任何数据包都会经过运营商的网络设备(路由器 交换机等),那么运营商的网络设备就可以解析出你传输的数据内容,并进行篡改。

         点击“下载”其实就是给服务器发送了一个HTTP请求,最终获取到的HTTP响应就会包含该app的下载链接,而运营商劫持之后,运营商发现请求中要求下载天天动听,此时运营商就会自动篡改服务器递交来的响应,将其改成“QQ浏览器”的下载地址了。

所以:因为http的内容是明文传输的,明文数据会经过路由器,WiFi热点,通信服务运营商,代理服务器等多个物理节点,如果信息在传输过程中被劫持,传输的内容就完全暴露了,劫持者可以篡改传输的信息且不被双方察觉,这就是中间人攻击,所以我们才需要对信息进行加密。 

2.常见的加密方式

(1)对称加密 

(2)非对称加密 

 非对称加密的数学原理比较复杂,涉及到数论相关的知识

下面是直观一些的图片

在下面的图中,对于非对称密钥,明文经过公钥A加密变成密文,密文通过私钥B解密变回明文

同时明文也以通过私钥B加密变成密文,密文在通过公钥A解密变成明文。

3.数据摘要(数据指纹) 

 

         也就是说我们用MD5,就可以把一段大文本转化为一段数据摘要。

        而这种数据摘要可以用于对比原文是否被篡改,即使原文只被修改了一点点,最后形成的数据摘要都会相差非常大。

   

关于数据摘要的应用我们小小的举两个例子

     1.我们上面有提到的位于服务器的session ID,其实就可以理解成用户的账号密码经过MD5算法处理后形成的数据摘要。 

2.百度网盘具有“秒传”,的功能,这是怎么实现的呢?

        例如两个人都想把同一部电影传到网盘,此时其中一个已经上传完毕了。此时百度网盘就把这个视频的二进制文件经过数据摘要算法形成对应的数据摘要。数据摘要对原数据本身就具有唯一性。此时另一名同学也想上传这部电影,此时百度网盘首先就先对这部电影的二进制文件也进行摘要,并且用其进行检索。此时如果检索到了,服务器可以给另一名同学一个软链接,来指向这同一个电影资源。因此这名同学就不需要再上传一次了。这样也极大减轻了网盘的负担。

4.数字签名 

 数据摘要经过加密就形成了数字签名。

九、https 的工作过程探究

 我们可以通过下列渐进式的方案来循序渐进逐步探究https的工作过程

方案一、只使用对称加密 

 

维护这样巨大的数据成本太大 。

比较理想的情况是在客户端和服务器建立链接的时候,双方 协商确定这次的密钥.

因此这种方案实际上很难实现。 

方案二、只使用非对称加密

1.首先我们的服务器拥有公钥P和私钥P‘

2.正式通信之前,客户端先向服务器发起请求, 服务器响应,并告知客户端它的公钥为P

3.客户端接收到公钥之后,再向服务器发消息时,就先用公钥进行加密

4.服务器接收到相应密文就能使用私钥P’进行解密,此时由于中间人不知道我们的私钥P‘,因此客户端向服务器的通信是安全的(不过再谈后面的方案会发现其实并不安全)

5.但是由于公钥是公开的因此服务器向客户端的通信并不安全。

因此这种方式也不可取。

方案三、双方都使用非对称加密 

双方都先把自己的那份公钥传递给对方。

然后向对方通信的时候都使用对方的公钥进行加密。

由于双方本身的私钥都只有自己知道,因此这样加密后的数据也只有对方能够解密。

但是这种方案效率太低,并且其实也仍然有安全问题

方案四、对称加密+非对称加密

服务器仍然是有一个公钥S和一个私钥S’,而客户端也在本地形成一个对称密钥C。

1.此时服务器 向客户端传递自己的公钥S,客户端得到了这个公钥

2.客户端把自己的对称密钥经过公钥S加密后发送给服务器

3.服务器接收到相应密文后可以用自己的私钥S‘将密文解密,由此就得到了客户端的对称密钥C

4.由此,客户端和服务器通过最开始的非对称加密交换了对称密钥C之后,后续就可以使用对称密钥C来进行加密解密,进行通信了。

这种方式已经很接近实际上的通信方式了,但是它还是有很大的安全问题

方案二三四的安全问题 --中间人攻击MITM攻击

        方案二方案三方案四都有一个最致命的问题,那就是如果最开始的时候中间人就已经开始攻击了呢? 

我们针对相对比较完善的方案四进行分析

1.在最开始的时候中间人就已经开始监视这次通信了,并且持有自己的公钥M与私钥M’。

2.客户端向服务器发起请求后,服务器响应了它的公钥S,此时中间人将其截取,获取了公钥S

 同时中间人把原本服务器发送给客户端的公钥S替换成了自己的公钥M,这样客户端就拿到了公钥M并且以为这就是服务器发送给自己的公钥。(因为这个公钥并没有自证的能力! 它不能证明自己是谁的公钥)

3.这时客户端把自己的对称密钥X使用中间人公钥M进行加密,并且将密文发送给服务器。

4.中间人此时再截取由客户端发送的密文,并且使用自己的私钥M‘进行解密,由此它就得到了客户端的对称密钥X。并且由于它已经获取了服务器的公钥S,它也就可以使用这个公钥S对X进行加密后再发送给服务器。

5.此时服务器收到密文后解密得到了X,此后客户端和服务器就正常开始通信。

6.但此时他们的对称密钥已经被中间人获取了,中间人可以窥探并修改客户端和服务器之间的通信数据,但是客户端和服务器双方不不会发觉。

        这种攻击方案根本就是中间人在客户端服务器双方互换公钥的时候将公钥替换成自己的 公钥,这样双方传输的数据是用中间人的公钥加密的,那么中间人自然可以通过自己手里的私钥解密

        

         出现上述问题最根本的原因是:双方都无法验证对方发来的公钥是否是合法的!此时我们就要引入证书的概念

引入证书

(1)CA认证

         服务端在使用HTTPS之前,需要向CA机构申领一份数字证书,数字证书里含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书里获取公钥就行了,证书就如身份证,证明服务端公钥的权威性。

        证书总体分为明文信息和签名。

      这里证书内的公钥就是服务器的公钥。 而私钥信息不需要放在证书内,由服务器自行保管。因此图中还提了申请认证的时候生成的csr文件中不含有私钥信息。

        

        由此我们知道,服务器在与客户端协商的时候,提供的是含有公钥的证书,而不是单单的公钥。那么此时要证明公钥的可靠性就变成了证明证书的可靠性了。

        那么我们怎么证明我们的证书是:1.权威机构颁发 2.没有被修改过呢? 

        为了解决这个问题我们先来认识一下csr文件和签名

(2)CSR文件 

        我们可以在网上在线获取CSR文件。我们填写域名企业等一系列本就可以公开的信息,然后通过网站在线生成,就可以给我们生成对应的CSR文件和私钥了(公钥也会一并形成)。

        这样获得csr文件之后我们就可以向CA机构提交了。 

(3)数据签名 

        数据签名是由数据摘要经过非对称加密形成的。这个过程由CA机构进行,也就是给数据摘要进行加密的私钥是CA机构自身的密钥 .

        CA机构处理的时候,这个数据签名就是由我们公开的数据通过摘要算法处理后(也就是散列函数处理) ,再通过CA机构自己的私钥加密形成的。

        最后这段数字签名就会附在公开数据边上,形成证书。

 

而证书进行验证也需要这个数字签名。

我们将公开的数据通过数据摘要算法形成 数据摘要(即一段散列值)

同时将数据签名用CA机构的公钥解密,也形成一段数据摘要。

通过这两段数据摘要的对比,我们就可以判断数据是否被篡改,或者说数字签名是否有效。

(4)为什么不直接通过加密数据形成签名 ,而是先形成摘要

主要是缩小签名密文的长度,加快签名的验证 

(5)客户端认证证书

方案五非对称加密+对称加密+证书认证 

其中最重要的一点就是,客户端使用CA的公钥来对数据签名解密。

第一这说明这一定是CA机构颁发的证书。

第二这说明客户端之认CA的公钥,即只有CA机构能进行证书的颁发,因为只有它有对应的私钥。 

因为第二点,所以中间人是没资格重新形成证书的,CA的私钥并不会进行传输,中间人也无计可施。

1.如果中间人只对数据进行修改,那么在两份数据签名比对的时候就会被发现。

2.如果中间人对数据和 数字签名都修改,那么因为中间人不知道CA机构的密钥,因此无法重新形成数据摘要,最终也会被发现

3..只修改数字签名,这没有什么意义,并且同样在两份数据签名比对的时候会被发现。

4.如果中间人替换了整个证书呢?

对于这种情况,由于域名的不完全相同,以及网页的不同,我们可以比较容易地发现。而如果有人刻意将两个网站做的很相似,误导访问者,那么这就是法律问题了。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐