网络:HTTP协议

目录

序列化与反序列化

守护进程

网络计算器的实现

HTTP协议

http的代码演示

HTTPS


初步理解三次握手,四次挥手

①tcp是面向连接的通信协议,在通信之前,需要进行3次握手,来进行连接的建立(谁connect谁握手)

②当tcp在断开链接的时候,需要释放链接,4次挥手(双方都close关闭连接)


从此篇开始,会自顶向下讲解各个层的协议,首先是应用层

序列化与反序列化

例如, 我们需要实现一个服务器版的+ - * /计算器,我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端

方法是:首先定义结构体来表示我们需要交互的信息

发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体

上述的过程就叫做序列化反序列化

一个结构化的数据发送之前需要先序列化,变成字符串,对方再把字符串数据反序列化变成结构化数据

用户端发给服务端的是字符串,服务器收到后需要进行反序列化

我们只要保证, 一端发送时构造的数据,,在另一端能够正确的进行解析, 这种就是约定,,也就是应用层协议


一般来说,在函数参数中,不同的参数代表不同的类型,例如:

输入型参数:        const std::string &
输出型参数:        std::string *
输入输出型参数: std::string &


守护进程

服务器全部都是在前台运行的
1.前台进程:和终端关联的进程就是前台进程。
2.任何xshell登陆,只允许一个前台进程和多个后台进程
3.进程除了有自己的pid, ppid, 还有一个组ID(PGID)
4.在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash,因此可以用匿名管道来进行通信
5.而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程
6.任何一次登陆,登陆的用户,需要有多个进程(组),来给这个用户提供服务的(bash),用户自己可以启动很多进程(组)。我们把给用户提供服务的进程或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。会话ID就是SID
7.使用setsid()可以将自己变成自成会话  
8. 需要注意:setsid要成功被调用,必须保证当前进程不是进程组的组长,通过执行fork()可以保证自己不是组长
9.守护进程不能直接向显示器打印消息,一旦打印,会被暂停、终止
 

命令行中启动的进程属于当前会话,想让他自成一个会话,此时在用户退出登陆时,只去掉自己会话内部的资源,不会影响该进程,所以该进程就不会受到用户的再次登陆和注销的影响,这样自成一个会话的进程称为守护进程


如何在Linux中正确的写一个让进程守护进程化的一个代码?
想通过自己写一个函数,让我们的进程调用这个函数,自动变成守护进程

此时执行Calserver后,查看时,会发现这个进程的会话ID(SID)是以自己的PID命名的
TTY表示是否和终端有关,?就表示无关
且PPID为1,守护进程的父进程就是1号进程,说明被领养了,本质是孤儿进程的一种


序列化反序列化使用自己实现的方案便于理解,但是更多还是使用的现有方案,json

json是一个用花括号括起来的key/value式的结构,{"key" : "value", "key" : "value", "key" : "value"}

使用时需要包含头文件<jsoncpp/json/json.h>

同时也需要在makefile中加上-ljsoncpp

网络计算器的实现

运行加过如下所示:

具体代码:

makefile:

.PHONY:all
all:client Calserver

client:CalClient.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp
Calserver:CalServer.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread

.PHONY:clean
clean:
	rm -f client Calserver

Protocol.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

namespace ns_protocol
{
// #define MYSELF 1
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

#define SEP "\r\n"
#define SEP_LEN strlen(SEP)

    class Request
    {
    public:
        //序列化
        // 1. 自主实现 -> "x_ op_ y_" 中间有两个空格
        // 2. 使用现成的方案

        //自主实现改为"length\r\nx_ op_ y_\r\n",前面的length可以作为此次需要读取的长度
        //这样就能够保证读到一个完整的请求
        const std::string Serialize()
        {
#ifdef MYSELF
            std::string str;
            str = std::to_string(x_);
            str += SPACE;
            str += op_; //
            str += SPACE;
            str += std::to_string(y_);
            return str;
#else
            // 使用现成的方案 序列化
            Json::Value root;
            root["x"] = x_;
            root["y"] = y_;
            root["op"] = op_;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        // 反序列化  "x_ op_ y_" -> x_ op_ y_ -> 字符串转为特定的x_ op_ y_
        bool Deserialize(const std::string &str)
        {
#ifdef MYSELF
            std::size_t left = str.find(SPACE);
            if (left == std::string::npos)
                return false;
            std::size_t right = str.rfind(SPACE);
            if (right == std::string::npos)
                return false;
            x_ = atoi(str.substr(0, left).c_str());
            y_ = atoi(str.substr(right + SPACE_LEN).c_str());
            if (left + SPACE_LEN > str.size())
                return false;
            else
                op_ = str[left + SPACE_LEN];
#else
            // 使用现成的方案 反序列化
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);
            x_ = root["x"].asInt();
            y_ = root["y"].asInt();
            op_ = root["op"].asInt();
            return true;
            
#endif
        }

    public:
        Request()
        {}

        Request(int x, int y, char op) : x_(x), y_(y), op_(op)
        {}

        ~Request()
        {}

    public:
        //约定了是 x_ op_ y_? 还是 y_ op_ x_?
        int x_;
        int y_;
        char op_; // '+' '-' '*' '/' '%'
    };

    class Response
    {
    public:
        // 序列化 "code_ result_" 中间有一个空格
        const std::string Serialize()
        {
#ifdef MYSELF
            std::string str;
            str = std::to_string(code_);
            str += SPACE;
            str += std::to_string(result_);
            
            return str;
#else
            // 使用现成的方案 序列化
            Json::Value root;
            root["code"] = code_;
            root["result"] = result_;
            root["xx"] = x_;
            root["yy"] = y_;
            root["zz"] = op_;
            Json::FastWriter writer;
            return writer.write(root);
#endif
        }
        // 反序列化
        bool Deserialize(const std::string &str)
        {
#ifdef MYSELF
            std::size_t pos = str.find(SPACE);
            if(pos == std::string::npos) return false;
            code_ = atoi(str.substr(0, pos).c_str());
            result_ = atoi(str.substr(pos+SPACE_LEN).c_str());
            
#else
            // 使用现成的方案 反序列化
            Json::Value root;
            Json::Reader reader;
            reader.parse(str, root);
            code_ = root["code"].asInt();
            result_ = root["result"].asInt();
            x_ = root["xx"].asInt();
            y_ = root["yy"].asInt();
            op_ = root["zz"].asInt();
            return true;
#endif
        }

    public:
        Response()
        {}

        Response(int result, int code, int x, int y, char op) 
        : result_(result), code_(code), x_(x), y_(y), op_(op)
        {}

        ~Response()
        {}

    public:
        int result_; // 计算结果
        int code_;   // 计算结果状态码(计算可能出错,0表示正常)

        int x_;
        int y_;
        char op_;
    };

    // 临时方案
    // 调整方案2:我们期望,你必须给我返回一个完整的报文
    bool Recv(int sock, std::string* out)
    {
        // UDP是面向数据报的:发一个,调用一次recvfrom,获取的就是一个完整的报文
        // TCP是面向字节流的:对方发好几次,可能一次就读完,不一定获取到的是完整的报文
        // recv:你怎么保证,你读到的inbuffer,是一个完整完善的请求呢? ->并不能保证
        // 例如12 + 1314 + 15,可能读取的是12 + 13 14 +;可能会混在一起
        // 所以单纯的recv是无法解决这个问题的,需要对协议进一步定制
        // 改为"length\r\nx_ op_ y_\r\n"

        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        else if(s == 0) //客户端退出
        {
            // std::cout << "client quit" << std::endl;
            return false;
        }
        else
        {
            // std::cout << "recv error" << std::endl;
            return false;
        }
            
        return true;
    }

    void Send(int sock, const std::string str)
    {
        // std::cout << "send in" << std::endl;
        int n = send(sock, str.c_str(), str.size(), 0);
        if(n < 0)//发送失败
            std::cout << "send quit" << std::endl;
    }

    // Decode函数用于保证得到一个完整的报文
    // "length\r\nx_ op_ y_\r\n"
    std::string Decode(std::string& buffer)
    {
        std::size_t pos = buffer.find(SEP);
        if(pos == std::string::npos) return "";
        // size是length的长度
        int size = atoi(buffer.substr(0, pos).c_str());
        // surplus是除去length和2个\r\n,剩下的内容
        int surplus = buffer.size() - pos - 2*SEP_LEN;
        if(surplus >= size)
        {
            // 至少具有一个合法报文
            buffer.erase(0, pos + SEP_LEN);// 将前面的length\r\n删去
            std::string s = buffer.substr(0, size);
            buffer.erase(0, size + SEP_LEN);// 将内容及后面的\r\n删去
            return s;
        }
        else
        {
            // 不具有一个合法报文
            return "";
        }
    }

    //Encode用于添加length\r\n等内容
    std::string Encode(std::string& s)
    {
        std::string new_package = std::to_string(s.size());
        new_package += SEP;
        new_package += s;
        new_package += SEP;
        return new_package;
    }
}

Daemon.hpp:(守护进程)

#pragma once

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void MyDaemon()
{
    // 1.忽略信号,SIGPIPE, SIGCHLD
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    // 2.不要让自己成为组长
    if (fork() > 0)
        exit(0);
    // 3.调用setsid
    setsid();
    // 4.标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
    // 所以都重定向到devnull中去,直接丢弃掉
    int devnull = open("dev/null", O_RDONLY | O_WRONLY);
    if(devnull > 0)
    {
        //重定向,将012对应的标准输入/输出/错误都重定向到devnull中去
        dup2(0, devnull);
        dup2(1, devnull);
        dup2(2, devnull);
        close(devnull);
    }
}

Sock.hpp:

#pragma once

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

class Sock
{
private:
    const static int gbacklog = 20;

public:
    Sock()
    {}

    // 创建套接字
    int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "create socket error,%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 文件描述符012默认打开,所以再创建就是3
        logMessage(NORMAL, "create socket success, listensock: %d", listensock);
        return listensock;
    }

    // bind
    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        // bind  文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 4字节ip->转为网络
        // binf失败
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));
            exit(3);
        }
    }

    //监听
    void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            // 监听失败
            logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    //获取连接(server端)
    int Accept(int listensock, std::string *ip, uint16_t *port)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int serversock = accept(listensock, (struct sockaddr *)&src, &len);
        if (serversock < 0)
        {
            // 获取连接失败
            logMessage(ERROR, "accept error,%d : %s", errno, strerror(errno));
            return -1;
        }
        //拿到客户端的IP和port
        if(port) *port = ntohs(src.sin_port);
        if(ip) *ip = inet_ntoa(src.sin_addr);
        return serversock;
    }

    //连接函数(client端,发起连接请求)
    bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);//主机->网络
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());//inet_addr->点分十进制->4字节IP

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }

    ~Sock()
    {}
};

TcpServer.hpp:

#pragma once

#include "Sock.hpp"
#include <functional>
#include <vector>
#include <pthread.h>

namespace ns_tcpserver
{
    using func_t = std::function<void(int)>;

    class TcpServer;
    class ThreadData
    {
    public:
        ThreadData(int sock, TcpServer *server) : sock_(sock), server_(server)
        {}

        ~ThreadData()
        {}

    public:
        int sock_;
        TcpServer *server_;
    };

    class TcpServer
    {
    private:
        static void *ThreadRoutine(void *args)
        {
            // 不想执行join,直接分离线程
            pthread_detach(pthread_self());
            ThreadData *td = static_cast<ThreadData *>(args);
            td->server_->Excute(td->sock_);
            close(td->sock_);
            // delete td;
            return nullptr;
        }

    public:
        TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
        {
            listensock_ = sock_.Socket();
            sock_.Bind(listensock_, port, ip);
            sock_.Listen(listensock_);
        }

        // 自己给服务器绑定一个服务
        void BindService(func_t func)
        {
            func_.push_back(func);
        }

        // 执行func_
        void Excute(int sock)
        {
            for(auto& f : func_)
            {
                f(sock);
            }
        }

        // 启动服务器
        void Start()
        {
            for (;;)
            {
                std::string clientip;
                uint16_t clientport;
                int sock = sock_.Accept(listensock_, &clientip, &clientport);
                if (sock == -1)
                    continue;
                logMessage(NORMAL, "create new link success, sock: %d", sock);
                pthread_t tid;
                ThreadData *td = new ThreadData(sock, this);
                pthread_create(&tid, nullptr, ThreadRoutine, td); 
            }
        }

        ~TcpServer()
        {
            if (listensock_ >= 0)
                close(listensock_);
        }

    private:
        int listensock_;
        Sock sock_;
        std::vector<func_t> func_;
    };
}

Calclient.cc:

#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"

using namespace ns_protocol;

static void usage(const std::string& proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}

//./client server_ip server_port
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    Sock sock;
    int sockfd = sock.Socket();
    if(!sock.Connect(sockfd, server_ip, server_port))
    {
        std::cerr << "Connect error" << std::endl;
        exit(2);
    }
    //连接成功
    bool quit = false;
    std::string buffer;
    while(!quit)
    {
        // 1. 获取需求
        Request req;
        std::cout << "Please Enter # ";
        std::cin >> req.x_ >> req.op_ >> req.y_;
        // 2.序列化
        std::string s = req.Serialize();
        // std::string temp = s;
        // 3.添加长度报头
        s = Encode(s);
        // 4.发送给服务端
        Send(sockfd, s);//发送给服务器

        // 5.正常读取
        while(true)
        {
            bool res = Recv(sockfd, &buffer);
            if(!res)
            {
                quit = true;
                break;
            }
            std::string package = Decode(buffer);
            if(package.empty())
                continue;
            Response resp;
            resp.Deserialize(package);
            std::string err;
            switch(resp.code_)
            {
            case 1:
                err = "除0错误";
                break;
            case 2:
                err = "模0错误";
                break;
            case 3:
                err = "非法操作";
                break;
            default:
                std::cout << resp.x_ << resp.op_ << resp.y_ << "=" << resp.result_ << " [success]" << std::endl;
                break;
            }
            if(!err.empty())
                std::cout << err << std::endl;
            break;
        }
    }
    close(sockfd);
    return 0;
}

Calserver.cc:

#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>
#include "Daemon.hpp"

using namespace ns_tcpserver;
using namespace ns_protocol;

static void usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " Port\n"
              << std::endl;
}

// 结构化的响应,设置为static,不让外部使用
static Response calculatorHelper(const Request req)
{
    Response resp(0, 0, req.x_, req.y_, req.op_);
    switch (req.op_)
    {
    case '+':
        resp.result_ = req.x_ + req.y_;
        break;
    case '-':
        resp.result_ = req.x_ - req.y_;
        break;
    case '*':
        resp.result_ = req.x_ * req.y_;
        break;
    case '/':
        if (0 == req.y_)
            resp.code_ = 1;
        else
            resp.result_ = req.x_ / req.y_;
        break;
    case '%':
        if (0 == req.y_)
            resp.code_ = 2;
        else
            resp.result_ = req.x_ % req.y_;
        break;
    default:
        resp.code_ = 3;
        break;
    }
    return resp;
}

void calculator(int sock)
{
    std::string inbuffer;
    while (true)
    {
        // 1.读取数据
        bool res = Recv(sock, &inbuffer); // 读到一个请求
        if (!res)
            break;
        // std::cout << "begin: inbuffer: " << inbuffer << std::endl;
        // 2.协议解析,Decode用于保证得到一个完整的报文
        std::string package = Decode(inbuffer);
        if (package.empty())
            continue;
        logMessage(NORMAL, "%s", package.c_str());
        // std::cout << "end: inbuffer: " << inbuffer << std::endl;
        // std::cout << "package: " << package << std::endl;
        // 读取的数据不为空再进行处理
        // 3.保证该报文是一个完整的报文
        Request req;
        // 4.反序列化,字节流->结构化
        req.Deserialize(package);
        // 5.业务逻辑
        Response resp = calculatorHelper(req);
        // 6.序列化
        std::string respString = resp.Serialize();
        // 7.添加长度信息,形成一个完整的报文
        // "length\r\ncode result\r\n"
        // std::cout << "respString: " << respString << std::endl;
        respString = Encode(respString);
        // std::cout << "Encode: respString: " << respString << std::endl;
        // 8.send暂时这样写
        Send(sock, respString);
    }
}

//./CalServer port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    // 一般经验: server在编写的时候,要有较为严谨性的判断逻辑
    // 一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入的问题

    // 向已经关闭的连接去写入,就会向当前的进程发送SIGPIPE信号
    // signal(SIGPIPE, SIG_IGN); // 可以对客户端退出发送的信号做忽略
    //上述功能放入守护进程中

    MyDaemon();
    std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
    server->BindService(calculator);
    server->Start();

    return 0;
}

HTTP协议

应用层:就是程序员基于socket接口之上编写的具体逻辑,做的很多工作都是和文本处理有关的 -> 叫做协议分析与处理

http协议,一定会具有大量的文本分析和协议处理

定位互联网中唯一的一个资源,url,统一资源定位符
全球范围内所有的资源,只要找到它的url就能访问该资源

如果用户想在urI中包含url本身用来作为特殊字符的字符, url形成的时候,浏览器会自动给我们进行编码encode,一般服务端收到之后,需要进行转回特殊字符


快速构建http请求和响应的报文格式

单纯在报文角度,http可以是基于行的文本协议

HTTP的请求分为请求行、请求报头、空行和请求正文:
请求行:包括方法url(/a/b/c/d.html)、http/1.1(协议版本) + \r\n
请求报头:有多行,每一行都是以\r\n结尾,每一行都是key:value的,表示属性
空行:\r\n
请求正文:可以没有

HTTP的响应分为状态行、响应报头、空行和响应正文:
状态行:包括http/1.1(协议版本) 、状态码状态码描述  + \r\n
响应报头:有多行,每一行都是以\r\n结尾,每一行都是key:value的,表示属性
空行:\r\n
响应正文:包括视频、音频、图片、html....

其中响应正文的大小,可以通过报头,在报头中有Cotent-Length属性,它后面对应的value,就表示的是正文的长度

在网页上访问时可以采用url的形式:[ip] : [port] / [index.html],这样访问


1.我们平时的上网行为其实就两种
a.从服务器端拿下来资源数据,一般采用GET方法
b.把客户端的数据提交到服务器,一般采用POST、GET方法

大部分情况下,获取网页/图片/视频这样资源的请求方法,基本都是GET方法

GET方法通过请求行的URL提交参数
POST方法通过http的正文部分提交参数

GET方法通过url提交参数,回显输入的私密信息,不够私密
POST方法通过正文提交参数,不会回显,一般私密性是有保证
私密性不代表着安全,加密解密才代表安全


状态码

1XX:Informational (信息性状态码),接收的请求正在处理
2XX:Suqsess (成功状态码),请求正常处理完毕
3XX:Redirection (重定向状态码),需要进行附加操作以完成请求
4XX:Client Error (客户端错误状态码),服务器无法处理请求
5XX:Server Error (服务器错误状态码),服务器处理请求出错

最常见的状态码,比如200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
括号中是状态码描述


http的代码演示

makefile:

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

.PHONY:clean
clean:
	rm -f HttpServer

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>测试</title>
</head>
<body>
    <h3>这是一个测试标题</h3>
    <p>这是测试段落!!!!!</p>
    <p>这是测试段落!!!!!</p>
    <p>这是测试段落!!!!!</p>
    <p>这是测试段落!!!!!</p>
</body>
</html>

HttpServer.hpp:

#pragma once

#include <iostream>
#include <signal.h>
#include <functional>
#include "Sock.hpp"

class HttpServer
{
public:
    using func_t = std::function<void(int)>;
private:
    int listensock_;
    uint16_t port_;
    Sock sock;
    func_t func_;
public:
    HttpServer(const uint16_t& port, func_t func):port_(port),func_(func)
    {
        listensock_ = sock.Socket();
        sock.Bind(listensock_, port_);
        sock.Listen(listensock_);
    }

    void Start()
    {
        signal(SIGCHLD, SIG_IGN);
        for( ; ;)
        {
            std::string clientIp;
            uint16_t clientPort = 0;
            int sockfd = sock.Accept(listensock_, &clientIp, &clientPort);
            if(sockfd < 0) continue;
            if(fork() == 0)
            {
                //子进程
                close(listensock_);
                func_(sockfd);
                close(sockfd);//执行完func_t方法后,再close套接字
                exit(0);
            }
            //父进程
            close(sockfd);
        }
    }

    ~HttpServer()
    {
        if(listensock_ >= 0)
            close(listensock_);
    }

};

HttpServer.cc:

#include <iostream>
#include <string>
#include <fstream>
#include "HeepServer.hpp"
#include "Util.hpp"

// 一般http 都要有自己的web根目录
#define ROOT "./wwwroot"
// 如果客户端只请求了一个/,我们返回默认首页,默认是./wwwroot/index.html
#define HOMEPAGE "index.html"

void HandlerHttpRequest(int sockfd)
{
    // 1.读取请求 for test
    char buffer[10240];
    ssize_t s = recv(sockfd, buffer, sizeof(buffer)-1, 0);
    if(s > 0)
    {
        buffer[s] = 0;
        // std::cout << buffer << "------------------\n" << std::endl;
    }

    //vline表示截取的是整个的请求
    std::vector<std::string> vline;
    Util::cutString(buffer, "\n", &vline);

    //vblock表示截取的是请求的第一行
    std::vector<std::string> vblock;
    Util::cutString(vline[0], " ", &vblock);
    //第一行请求所提取的vblock[1]是url,代表所请求的资源是什么,例如/index.html
    std::string file = vblock[1];
    std::string target = ROOT;
    //如果没有给定具体的路径,会返回/,则默认打开首页,即/index.html
    if(file == "/") file = "/index.html";
    target += file; //所以请求的资源都是wwwroot/.....
    std::cout << target << std::endl;

    //对网页文件打开后,进行内容读取,读取成功后拿出网页内容到content中
    std::string content;
    std::ifstream in(target);
    if(in.is_open())
    {
        //成功打开
        std::string line;
        //将读到的内容写入line中
        while(std::getline(in ,line))
        {
            content += line;
        }
        in.close();
    }

    // 2.构建一个http的响应
    std::string HttpResponse;
    if(content.empty()) HttpResponse = "HTTP/1.1 404 NotFound\r\n";
    else HttpResponse = "HTTP/1.1 200 OK\r\n";
    HttpResponse += "\r\n";
    HttpResponse += content;

    // std::cout << "####### start #######" << std::endl;

    // for(auto& iter : vblock)
    // {
    //     std::cout << "---" <<iter << "\n" << std::endl;
    // }
    // std::cout << "#######  end #######" << std::endl;

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

static void usage(const std::string &proc)
{
    std::cout << "\nUsage: " << proc << " Port\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }

    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();

    return 0;
}

Util.hpp:

#pragma once

#include <vector>
#include <string>
#include <iostream>

class Util
{
public:
    // 用于截取内容,s是传入的字符串,sep是分隔符
    // aaaaa\r\nbbbbbb\r\nccccc\r\n\r\n
    static void cutString(const std::string& s, const std::string& sep, std::vector<std::string> *out)
    {
        std::size_t start = 0;
        while(start < s.size())
        {
            auto pos = s.find(sep, start);
            if(pos == std::string::npos)
                break;
            std::string sub = s.substr(start, pos - start);
            out->push_back(sub);
            start += sub.size();
            start += sep.size();
        }
        if(start < s.size())
        {
            out->push_back(s.substr(start));
        }
    }
};

首先运行可执行程序HttpServer,输入端口号8080:

然后在浏览器输入当前云服务器的IP:port,如下所示:

结果如下:

执行了我们在当前路径下wwwroot文件夹中的index.html


HTTPS

HTTPS也是⼀个应⽤层协议,是在HTTP协议的基础上引⼊了⼀个加密层

HTTP协议内容都是按照⽂本的⽅式明⽂传输的,这就导致在传输过程中出现⼀些被篡改的情况,所以在互联网上明文传输是比较危险的事情,HTTPS就是在HTTP的基础上进⾏了加密,进⼀步的来保证用户的信息安全

常⻅的加密⽅式有对称加密和非对称加密

对称加密:采⽤单钥密码系统的加密⽅法,同⼀个密钥可以同时⽤作信息的加密和解密,这种加密⽅法称为对称加密,也称为单密钥加密,特征:加密和解密所⽤的密钥是相同的

特点:算法公开、计算量⼩、加密速度快、加密效率⾼

对称加密其实就是通过同⼀个"密钥",把明⽂加密成密⽂,并且也能把密⽂解密成明⽂

举个例子,⼀个简单的对称加密,按位异或


非对称加密:需要两个密钥来进⾏加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)

特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,⽽使得加密解密速度没有对
称加密解密的速度快。

通过公钥对明文加密,变成密文;通过私钥对密文解密,变成明文
也可以反着用,即通过私钥对明文加密,变成密文;通过公钥对密文解密,变成明文


关于数据摘要和数据指纹

数字指纹(数据摘要),其基本原理是利⽤单向散列函数(Hash函数)对信息进⾏运算,⽣成⼀串固定长度的数字摘要

即对原始的明文数据做哈希散列形成数据摘要

数字指纹并不是⼀种加密机制,但可以⽤来判断数据有没有被篡改

摘要特征:和加密算法的区别是,摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推
原信息,通常⽤来进⾏数据对⽐


数字签名

将数据摘要经过CA加密,就得到数字签名


HTPPS的工作过程

在服务器和客户端进行通信时,可能会存在中间人(例如黑客),通过窃取用户的数据信息,从而造成安全问题 

无论是使用对称密钥,非对称密钥,或是对称密钥+非对称密钥结合使用,都会存在安全问题,所以下面引入证书的概念

CA认证

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

这个证书可以理解成是一个结构化的字符串,里面包含了以下信息:
证书发布机构、证书有效期、公钥、签名、证书的所属者.....

关于数字签名的过程

CA机构拥有非对称加密的私钥A和公钥A'
CA机构对服务端申请的证书明文数据进行hash,形成数据摘要
然后对数据摘要用CA私钥A'加密,得到数字签名S

服务端申请的证书明文和数字签名S共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了


有了上面证书的概念,下面采取   对称密钥+非对称密钥+证书认证  的方法,就可以安全通信,不用担心被中间人篡改数据了

在客户端和服务器刚⼀建⽴连接的时候,服务器给客户端返回⼀个证书,证书包含了之前服务端的公钥,也包含了网站的身份信息

在上面没有证书认证时,只采用对称密钥和非对称密钥的方式进行数据通信,中间人可能会篡改明文中的公钥,替换为自己的公钥,并发送给客户端
接着客户端会采取中间人发送的公钥进行加密,加密后发送给服务端,中间人又截取到这个使用自己公钥加密的信息,于是使用自己的私钥解密后得到该信息,完成数据窃取
中间人再使用保存的服务器的公钥改变数据,并加密在发送给服务器,造成网络安全的问题

此时有了证书认证,就不会担心上述情况的发生了,因为此时服务器会发送一个证书给客户端,这个证书包括明文数据和数字签名,中间人只能够改变证书中的明文公钥,无法改变数字签名中的公钥(因为中间人没有CA密钥,所以无法改变签名),如果中间人改变了明文公钥,此时证书发送到客户端的手中后,客户端就可以进行验证证书是否被篡改


验证证书是否被篡改:从系统中拿到该证书发布机构的公钥,对数字签名解密,得到一个hash值(称为数据摘要),设为hash1;然后计算整个证书的明文数据的hash值,设为hash2,对比hash1和hash2是否相等,如果相等,则说明证书是没有被篡改过的;否则就是被篡改了

由于中间人没有CA机构的私钥,所以无法hash之后用私钥加密形成签名,那么也就没法办法对篡改后的证书形成匹配的数字签名,如果中间人强行篡改,客户端收到该证书后会发现明文和签名解密后的值不一致,则说明证书已被篡改,证书不可信,从而终止向服务器传输信息,防止信息泄露给中间人

当然也存在一种可能,即中间人直接掉包整个证书,这种情况中间人可以向CA申请真证书,然后用自己申请的证书进行掉包,这个确实能做到证书的整体掉包,但是别忘记,证书明文中包含了域名等服务端认证信息,如果整体掉包,客户端依旧能够识别出来。

所以永远记住:中间人没有CA私钥,所以对任何证书都无法进行合法修改,包括自己的

之所以签名不直接加密,而是要先hash形成摘要,是因为为了缩小签名密文的长度,加快数字签名的验证签名的运算速度


总结一下:HTTPS工作过程中涉及到的密钥有三组

第一组(非对称加密):用于校验证书是否被篡改,服务器持有私钥(私钥在形成CSR文件与申请证书时获得),客户端持有公钥(操作系统包含了可信任的CA认证机构有哪些,同时持有对应的公钥)
服务器在客户端请求时,返回携带签名的证书,客户端通过这个公钥进行证书验证,保证证书的合法性,进一步保证证书中携带的服务端公钥权威性。

第二组(非对称加密):用于协商生成对称加密的密钥,客户端用收到的CA证书中的公钥(是可被信任的)给随机生成的对称加密的密钥加密,传输给服务器,服务器通过私钥解密获取到对称加密密钥

第三组(对称加密):客户端和服务器后续传输的数据都通过这个对称密钥加密解密

其实上述一切都是围绕这个对称加密的密钥,其他的机制都是辅助这个密钥工作的
第二组非对称加密的密钥是为了让客户端把这个对称密钥传给服务器
第一组非对称加密的密钥是为了让客户端拿到第二组非对称加密的公钥


相关推荐

  1. 计算机网络——HTTP协议

    2024-04-06 13:18:02       47 阅读
  2. 【计算机网络HTTP协议

    2024-04-06 13:18:02       44 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-06 13:18:02       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-06 13:18:02       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-06 13:18:02       82 阅读
  4. Python语言-面向对象

    2024-04-06 13:18:02       91 阅读

热门阅读

  1. Tkinter 1

    Tkinter 1

    2024-04-06 13:18:02      33 阅读
  2. WebKit结构简介

    2024-04-06 13:18:02       36 阅读
  3. 关于K8S集群中maste节点r和worker节点的20道面试题

    2024-04-06 13:18:02       27 阅读
  4. git lfs使用(huggingface下载大模型文件)

    2024-04-06 13:18:02       43 阅读
  5. loopvar 改动不同版本的影响-大循环的执行时间

    2024-04-06 13:18:02       36 阅读
  6. ETCD备份方案制定

    2024-04-06 13:18:02       35 阅读
  7. ubuntu23设置kibana后台启动服务

    2024-04-06 13:18:02       29 阅读
  8. netty+websocket实现简易聊天

    2024-04-06 13:18:02       32 阅读