15.http协议

再谈 “协议”

  • 协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?

image-20230921144415830

网络版计算器

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

  • 约定方案一:

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;
  • 约定方案二:
  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 “序列化” 和 “反序列化”

保证可以完整的读取一个报文

image-20230921185632218

方案一(自己写序列化)

makefile

cc=g++
LD=-DMYSELF
.PHONY:all
all:calserver calclient

calclient:calClient.cc
	$(cc) -o $@ $^ -std=c++11 

calserver:calServer.cc
	$(cc) -o $@ $^ -std=c++11 

.PHONY:clean
clean:
	rm -f calclient calserver

log.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char * to_levelstr(int level)
{
    switch(level)
    {
        case DEBUG : return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default : return nullptr;
    }
}

void logMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logprefix[NUM];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
        to_levelstr(level), (long int)time(nullptr), getpid());

    char logcontent[NUM];
    va_list arg;
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);

    std::cout << logprefix << logcontent << std::endl;
}

protocol.hpp

#pragma once

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

#define SEP " "
// 不敢使用sizeof(),因为其包含\0这个字符,而strlen()不会
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

// 将报头和报文的正文内容进行拼接,中间用\r\n进行分割
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{
    // 报头中存放的就是报文正文内容所占字节的大小
    std::string send_string = std::to_string(text.size());

    // 将报头和报文的正文部分进行拼接,它们之前只用分隔符进行分割
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    // 将其返回
    return send_string;
}

// 去掉完整报文的报头,通过输出参数std::string *text
// 返回报文的正文部分
// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
    // "content_len"\r\n"exitcode result"\r\n
    // package.find(LINE_SEP)拿到报头右侧的分隔符的位置
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    // package.substr(0, pos)拿到报头字符串
    std::string text_len_string = package.substr(0, pos);
    // 将报头字符串转化为数字,其对应的就是报文正文的长度,即text_len
    int text_len = std::stoi(text_len_string);

    // package.substr(pos + LINE_SEP_LEN, text_len)拿到报文正文的内容
    *text = package.substr(pos + LINE_SEP_LEN, text_len);

    // 去掉报头成功,则返回真
    return true;
}

// 当服务器或者客户端收到请求消息时,需要将网络中的消息先进行反序列化
// 再进行处理
class Request
{
public:
    // 无参构造
    Request() : x(0), y(0), op(0)
    {
    }

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

    // 序列化
    // 1. 自己写序列化
    // 2. 用现成的系统提供的序列化或者反序列化的接口
    bool serialize(std::string *out)
    {
        // 将*out进行清空
        *out = "";

        // 结构化 -> "x op y";
        // x是数字,使用接口to_string()将数字转化为字符串
        // op本身就是char类型,因此不需要进行转化
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);

        // 将所有的字符串进行拼接,且字符之间使用SEP(空格)进行分割
        *out = x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;
        return true;
    }

    // 反序列化
    // "x op y";
    // const std::string &in, in里面存放的去掉报头的报文正文
    // 将其进行反序列化
    bool deserialize(const std::string &in)
    {
        // "x op y" -> 结构化
        // in.find(SEP),找到的是左操作数x右边的分隔符
        auto left = in.find(SEP);

        // in.rfind(SEP),找到的是右操作数y左边的分隔符
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;

        // SEP_LEN 是分隔符SEP的长度
        if (right - (left + SEP_LEN) != 1)
            return false;

        // in.substr(0, left),分割的字符串是左闭右开的区间
        // [0, 2) [start, end) , start(起始位置),
        // end - start(从起始位置分割end - start个字符串)
        std::string x_string = in.substr(0, left);         // 分割出来的做操作数
        std::string y_string = in.substr(right + SEP_LEN); // 分割出来的右操作数

        // 保证x_string不为空串
        if (x_string.empty())
            return false;
        if (y_string.empty())
            return false;

        // 将左操作数和右操作数,从字符串转化为数字
        x = std::stoi(x_string);
        y = std::stoi(y_string);

        // 操作符op的下标为left + SEP_LEN
        op = in[left + SEP_LEN];

        return true;
    }

public:
    // "x op y"
    int x;
    int y;
    char op;
};

// 当服务器或者客户端处理完收到的请求的数据之后,
// 需要将处理后的结果构建成可以发送给网络的响应
class Response
{
public:
    Response() : exitcode(0), result(0)
    {}

    Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
    {}

    // 序列化
    bool serialize(std::string *out)
    {
        *out = "";
        // 将退出码,和处理结果转化为字符串
        std::string ec_string = std::to_string(exitcode);
        std::string res_string = std::to_string(result);

        // 将退出码,和处理结果转化的字符串进行拼接
        *out = ec_string;
        *out += SEP;
        *out += res_string;
        return true;
    }

    // 反序列化
    bool deserialize(const std::string &in)
    {
        // "exitcode result"
        auto mid = in.find(SEP);
        if (mid == std::string::npos)
            return false;
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        // 保证退出码和处理结果不是空字符串
        if (ec_string.empty() || res_string.empty())
            return false;

        // 将退出码和处理结果从字符串转化为数字
        exitcode = std::stoi(ec_string);
        result = std::stoi(res_string);

        // 反序列化成功,则返回真
        return true;
    }

public:
    // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int exitcode;
    int result; // 计算结果
};


// 从套接字对应的文件描述符sock中进行读取
// 如下是多个报文的字节流
// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
    // 定义应用层的缓冲区,来存放报文
    char buffer[1024];
    while (true)
    {
        // 从sock读取sizeof(buffer) - 1个字节并将其存放到buffer中
        // 经过多次循环,就可以将sock中的内容全部读取
        // 并返回读取内容的字节个数
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            // 给字符串的末尾添加结束符,即\0
            buffer[n] = 0;
            // 将读取的所有数据放入到string类型的inbuffer中
            inbuffer += buffer;

            // 分析处理
            // "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n
            // inbuffer.find(LINE_SEP)就是报头"content_len"右侧的分隔符的位置
            auto pos = inbuffer.find(LINE_SEP);
            if (pos == std::string::npos)
                continue;  // 如果没有找到,那么就返回继续读取,再重新找

            // inbuffer.substr(0, pos),获取到报头
            // 报头存放就是报文正文内容所占的字节数,即text_len_string
            std::string text_len_string = inbuffer.substr(0, pos);

            // 将报文正文内容所占的字节数,由字符串转为数字
            int text_len = std::stoi(text_len_string);

            // total_len 报文的总长度
            // text_len_string.size()是报头的长度
            // 2 * LINE_SEP_LEN + text_len 报文正文和两个分割符长度的总和
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            
            // text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();
            std::cout << "处理前#inbuffer: \n"<< inbuffer << std::endl;

            // inbuffer.size()小于total_len,说明inbuffer没有一个完整的报文
            // 那么就返回,继续从sock读取,直到inbuffer.size() > total_len
            if (inbuffer.size() < total_len)
            {
                std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
                continue;
            }

            // 至少有一个完整的报文
            // 分割出一个完整报文
            *text = inbuffer.substr(0, total_len);

            // 从inbuffer删除掉分割出的报文
            inbuffer.erase(0, total_len);

            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;

            break;
        }
        else
            // 如果n小于等于0说明对方关闭了sock文件描述符对应的文件,因此返回错误
            return false;
    }
    return true;
}

calServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <functional>

#include "log.hpp"
#include "protocol.hpp"

using namespace std;

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    // 默认缺省的端口号为8080
    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    // const Request &req: 输入型
    // Response &resp: 输出型
    typedef std::function<bool(const Request &req, Response &resp)> func_t;

    void handlerEntery(int sock, func_t func)
    {
        std::string inbuffer;
        while (true)
        {
            // 1. 读取:"content_len"\r\n"x op y"\r\n
            // 1.1 你怎么保证你读到的消息是 【一个】完整的请求
            // req_text代表收到的完整请求报文(输出型参数)
            // req_str代表收到的请求报文的正文(输出型参数)
            std::string req_text, req_str;

            // 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n
            if (!recvPackage(sock, inbuffer, &req_text))
                return;
            std::cout << "带报头的请求:\n"<< req_text << std::endl;
            
            if (!deLength(req_text, &req_str))
                return;
            std::cout << "去掉报头的正文:\n"<< req_str << std::endl;


            // 2. 对请求Request,反序列化
            // 2.1 得到一个结构化的请求对象
            // 这个结构化的请求对象为报文的正文部分,需要将其进行反序列化
            // 我们才能够对正文中的数据做处理
            Request req;
            // req.deserialize(req_str),反序列化成功,则返回值为真
            if (!req.deserialize(req_str))
                return;

            // 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑
            // 3.1 得到一个结构化的响应
            Response resp;
            // resp就是将收到的请求做处理后的响应,发回给请求方
            // req的处理结果,全部放入到了resp
            func(req, resp);

            // 4.对响应Response,进行序列化
            // 4.1 得到了一个"字符串",也就是数据处理后得到的响应结果
            // 需要将响应结果进行序列化,才能够发送给网络
            std::string resp_str;
            resp.serialize(&resp_str);

            // 5. 然后再发送响应
            // 5.1 构建成为一个完整的报文
            std::string send_string = enLength(resp_str);
            std::cout << "构建完成完整的响应\n"<< send_string << std::endl;

            // 将其发送
            send(sock, send_string.c_str(), send_string.size(), 0);
        }
    }

    class calServer
    {
    public:
        // tcp服务器的构造函数
        calServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
        {
        }

        // 初始化服务器
        void initServer()
        {
            // 1. 创建socket文件套接字对象
            // tcp也是网络连接,因此第一个参数为AF_INET
            // tcp是面向字节流的,因此第二个参数为SOCK_STREAM
            // 确定好前两个参数,第三个参数默认为0就可以
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                // 套接字的文件描述符小于0,那么创建套接字失败
                // 则我们通过日志,打印对应的错误信息
                logMessage(FATAL, "create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "create socket success: %d", _listensock);

            // 2. bind绑定自己的网络信息
            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;

            // bind小于0,代表绑定失败
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                // 通过日志,打印绑定失败的错误信息
                logMessage(FATAL, "bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "bind socket success");

            // tcp是建立连接的协议,因此客户端和服务器是要建立连接的
            // 所谓的连接,也就是服务器实时知道客户端向其发出了请求
            // tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
            // 然后才可以向服务器发送消息
            // 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
            // 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
            //          等待客户的电话,这样才可以实时与客户建立连接

            // 3. 设置socket 为监听状态
            // 参数_listensock,就是要将这个参数设置为监听状态
            // gbacklog,后续再说其用处,目前暂时先设置为5
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
            {
                // // 通过日志,打印设置监听状态失败的错误信息
                logMessage(FATAL, "listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "create socket success: %d", _listensock);
        }

        // 开始启动服务器
        void start(func_t func)
        {
            // 4. server 获取新链接
            // _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
            // _listensock只负责监听用户请求,新的套接字sock只负责通信
            // sock, 和client进行通信的fd(文件描述符)
            for (;;)
            {
                // 参数
                // struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
                // socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    // 获取连接失败,并不是致命的错误,因此日志等级为ERROR
                    logMessage(ERROR, "accept error, next");
                    // 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
                    continue;
                }
                logMessage(NORMAL, "accept a new link success, get new sock: %d", sock);

                // version 2 多进程版(2)
                pid_t id = fork();
                if (id == 0) // child
                {
                    close(_listensock);
                    // if(fork()>0) exit(0);
                    handlerEntery(sock, func);
                    close(sock);
                    exit(0);
                }
                close(sock);

                // father
                pid_t ret = waitpid(id, nullptr, 0);
                if (ret > 0)
                {
                    logMessage(NORMAL, "wait child success");
                }
            }
        }

        ~calServer()
        {
        }

    private:
        // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
        int _listensock;
        uint16_t _port;
    };

} // namespace server

calServer.cc

 #include "calServer.hpp"
#include <memory>

using namespace server;
using namespace std;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

 
// req: 在进行任务处理时,此时req里面一定是处理好的报文的正文,可以直接进行数据处理
// resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等任何细节
bool cal(const Request &req, Response &resp)
{
    // exitcode,和结果默认设置为0
    resp.exitcode = OK;
    resp.result = OK;

    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 (req.y == 0)
        // 发生除0错误,返回对应的错误码
            resp.exitcode = DIV_ZERO;
        else
            resp.result = req.x / req.y;
    }
    break;
    case '%':
    {
        if (req.y == 0)
            // 发生模0错误,返回对应的错误码
            resp.exitcode = MOD_ZERO;
        else
            resp.result = req.x % req.y;
    }
    break;
    default:
        // 运行到这里,说明操作符出错,则返回对应的错误码
        resp.exitcode = OP_ERROR;
        break;
    }

    return true;
}

// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{ 
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<calServer> tsvr(new calServer(port));
    tsvr->initServer();
    tsvr->start(cal);

    return 0;
}

calClient.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include "protocol.hpp"

#define NUM 1024

class calClient
{
public:
    // 客户端的构造函数
    calClient(const std::string &serverip, const uint16_t &serverport)
        : _sock(-1), _serverip(serverip), _serverport(serverport)
    {
    }

    // 初始化客户端
    void initClient()
    {
        // 1. 创建套接字socket
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            // 创建套接字失败,打印错误信息
            std::cerr << "socket create error" << std::endl;
            exit(2);
        }

        // 2. tcp的客户端要不要bind?要的!
        // 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
        // 3. 要不要listen?不要!
        // 4. 要不要accept? 不要!
        // 5. 要什么呢? 要发起链接!
    }

    // 启动客户端
    void start()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());

        if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            // 客户端向服务器发起链接失败,则打印错误信息
            std::cerr << "socket connect error" << std::endl;
        }
        else
        {
            // 链接成功,客户端和服务端进行通信
            std::string line;
            std::string inbuffer;
            while (true)
            {
                std::cout << "mycal>>> ";
                // 将输入流的数据,放入到应用层的缓冲区line中
                std::getline(std::cin, line);  // 1+1

                // ParseLine(line)拿到了输入数据构建的请求
                Request req = ParseLine(line); // "1+1"

                // 将数据序列化之后,通过输出参数std::string content返回
                // 此时content也就是要发送的报文的正文
                std::string content; // 输出型参数
                req.serialize(&content);

                // 给报文添加报头并返回
                std::string send_string = enLength(content);
                std::cout << "sendstring:\n"<< send_string << std::endl;

                // 将序列化之后的报文发送
                send(_sock, send_string.c_str(), send_string.size(), 0); 

                // 读取服务器计算数据后的返回结果
                // std::string package,输出型参数,返回完整的报文
                // std::string text,输出型参数,返回报文正文
                std::string package, text;

                // 从套接字的文件描述符sock中读取一个完整的报文
                //  "content_len"\r\n"exitcode result"\r\n
                if (!recvPackage(_sock, inbuffer, &package))
                    continue;

                // 分离报文报头和报文正文
                if (!deLength(package, &text))
                    continue;

                // 报文正文包含如下内容
                // "exitcode result"
                // 从网络中拿到的数据,因此需要先进行反序列化
                Response resp;
                resp.deserialize(text);
                std::cout << "exitCode: " << resp.exitcode << std::endl;
                std::cout << "result: " << resp.result << std::endl;
            }
        }
    }

    // 客户端的析构函数
    ~calClient()
    {
        // 关闭客户端套接字对应的文件描述符
        if (_sock >= 0)
            close(_sock);
    } 

    Request ParseLine(const std::string &line)
    {
        // 建议版本的状态机!
        //"1+1" "123*456" "12/0"
        int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
        int i = 0;

        // line.size()是输入左操作数、右操作数、操作符和分隔符的总长度
        int cnt = line.size();
        std::string left, right;
        char op;
        while (i < cnt)
        {
            switch (status)
            {
            case 0:
            {
                // 状态码为0,表明当前位置是在操作符之前
                // isdigit(line[i])判断line[i]是不是数字,是数字则返回真
                if (!isdigit(line[i]))
                {
                    // 此时line[i]一定是操作符
                    op = line[i];
                    // 1:碰到了操作符,因此将状态码改为1
                    status = 1;
                }
                else
                    // 此时line[i] 一定是左操作数
                    left.push_back(line[i++]);
            }
            break;
            case 1:
                // 状态码为1,说明line[i]一定是操作符,因此进行++
                i++;
                // ++ 之后line[i]一定为右操作数,因此将状态码变为2
                status = 2;
                break;
            case 2:
                // 此时line[i]一定为右操作数
                right.push_back(line[i++]);
                break;
            }
        }
        std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;
        
        // 将拿到的数据,构建为一个请求并返回
        return Request(std::stoi(left), std::stoi(right), op);
    }

private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

calClient.cc

#include "calClient.hpp"
#include <memory>

using namespace std;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<calClient> tcli(new calClient(serverip, serverport));
    tcli->initClient();
    tcli->start();
    return 0;
}

演示结果

image-20230922210027773

方案二(系统接口写序列化)

安装jsoncpp库

sudo yum install -y jsoncpp-devel
  • 成功安装后就可以进行如下查看

image-20230922211921371

json格式示例


// json 内部存储的是键对值,是key,value形式的
{
    "name":"张三",
    "age":18,
    "身高":1.81,
    "生日":"2002-01-02",
    "学校":"XXXX",
    "专业":["厨师","挖掘机"],
    "单身":true,
    "地址":null,
    "好友":{...}
}

makefile

cc=g++
LD=-DMYSELF    # 定义了MYSELF ,就使用自己写的序列化和反序列化,如果没有定义那么就用系统接口写的序列化和反序列化
.PHONY:all
all:calserver calclient

calclient:calClient.cc
	$(cc) -o $@ $^ -std=c++11 -ljsoncpp #${LD}

calserver:calServer.cc
	$(cc) -o $@ $^ -std=c++11  -ljsoncpp  #${LD}

.PHONY:clean
clean:
	rm -f calclient calserver

protocol.hpp

#pragma once

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

#define SEP " "
// 不敢使用sizeof(),因为其包含\0这个字符,而strlen()不会
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

// 将报头和报文的正文内容进行拼接,中间用\r\n进行分割
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)
{
    // 报头中存放的就是报文正文内容所占字节的大小
    // 需要将其转化为字符串,才可以与正文内容进行拼接
    std::string send_string = std::to_string(text.size());

    // 将报头和报文的正文部分进行拼接,它们之前只用分隔符进行分割
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;

    // 将其返回
    return send_string;
}

// 去掉完整报文的报头,通过输出参数std::string *text
// 返回报文的正文部分
// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{
    // "content_len"\r\n"exitcode result"\r\n
    // package.find(LINE_SEP)拿到报头右侧的分隔符的位置
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    // package.substr(0, pos)拿到报头字符串
    std::string text_len_string = package.substr(0, pos);
    // 将报头字符串转化为数字,其对应的就是报文正文的长度,即text_len
    int text_len = std::stoi(text_len_string);

    // package.substr(pos + LINE_SEP_LEN, text_len)拿到报文正文的内容
    *text = package.substr(pos + LINE_SEP_LEN, text_len);

    // 去掉报头成功,则返回真
    return true;
}

// 当服务器或者客户端收到请求消息时,需要将网络中的消息先进行反序列化
// 再进行处理
class Request
{
public:
    // 无参构造
    Request() : x(0), y(0), op(0)
    {
    }

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

    // 序列化
    // 1. 自己写序列化
    // 2. 用现成的系统提供的序列化或者反序列化的接口
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        // 将*out进行清空
        *out = "";

        // 结构化 -> "x op y";
        // x是数字,使用接口to_string()将数字转化为字符串
        // op本身就是char类型,因此不需要进行转化
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);

        // 将所有的字符串进行拼接,且字符之间使用SEP(空格)进行分割
        *out = x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;
#else
        // 定义一个json对象
        Json::Value root;
        // x是数字,将x与first配对,存储到json中
        // 那么josn会将x转化为字符串
        root["first"] = x;
        root["second"] = y;
        root["oper"] = op;

        // 两种方法都可以将root中的数据进行序列化
        Json::FastWriter writer;
        // Json::StyledWriter writer;

        // 将root传入,writer.write(root)会自动将root中的内容进行序列化
        // writer.write(root)返回的是一个string类型的对象
        *out = writer.write(root);
#endif
        return true;
    }


    // 反序列化
    // "x op y";
    // const std::string &in, in里面存放的去掉报头的报文正文
    // 将其进行反序列化
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        // "x op y" -> 结构化
        // in.find(SEP),找到的是左操作数x右边的分隔符
        auto left = in.find(SEP);

        // in.rfind(SEP),找到的是右操作数y左边的分隔符
        auto right = in.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;

        // SEP_LEN 是分隔符SEP的长度
        if (right - (left + SEP_LEN) != 1)
            return false;

        // in.substr(0, left),分割的字符串是左闭右开的区间
        // [0, 2) [start, end) , start(起始位置),
        // end - start(从起始位置分割end - start个字符串)
        std::string x_string = in.substr(0, left);         // 分割出来的做操作数
        std::string y_string = in.substr(right + SEP_LEN); // 分割出来的右操作数

        // 保证x_string不为空串
        if (x_string.empty())
            return false;
        if (y_string.empty())
            return false;

        // 将左操作数和右操作数,从字符串转化为数字
        x = std::stoi(x_string);
        y = std::stoi(y_string);

        // 操作符op的下标为left + SEP_LEN
        op = in[left + SEP_LEN];

#else
        Json::Value root;
        Json::Reader reader;

        // 将in这个流中读取的数据放入到root中
        reader.parse(in, root);

        // 根据键对值,拿到相应的操作数和操作符
        // asInt()的返回值是一个数字
        x = root["first"].asInt();
        y = root["second"].asInt();

        // 整数赋值给char类型的op,会自动转化为一个字符
        op = root["oper"].asInt();
#endif

        return true;
    }

public:
    // "x op y"
    int x;
    int y;
    char op;
};




// 当服务器或者客户端处理完收到的请求的数据之后,
// 需要将处理后的结果构建成可以发送给网络的响应
class Response
{
public:
    Response() : exitcode(0), result(0)
    {
    }

    Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
    {
    }

    // 序列化
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        *out = "";
        // 将退出码,和处理结果转化为字符串
        std::string ec_string = std::to_string(exitcode);
        std::string res_string = std::to_string(result);

        // 将退出码,和处理结果转化的字符串进行拼接
        *out = ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["exitcode"] = exitcode;
        root["result"] = result;

        Json::FastWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    // 反序列化
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        // "exitcode result"
        auto mid = in.find(SEP);
        if (mid == std::string::npos)
            return false;
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        // 保证退出码和处理结果不是空字符串
        if (ec_string.empty() || res_string.empty())
            return false;

        // 将退出码和处理结果从字符串转化为数字
        exitcode = std::stoi(ec_string);
        result = std::stoi(res_string);

#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

        exitcode = root["exitcode"].asInt();
        result = root["result"].asInt();
#endif

        // 反序列化成功,则返回真
        return true;
    }

public:
    // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int exitcode;
    int result; // 计算结果
};

// 从套接字对应的文件描述符sock中进行读取
// 如下是多个报文的字节流
// "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
    // 定义应用层的缓冲区,来存放报文
    char buffer[1024];
    while (true)
    {
        // 从sock读取sizeof(buffer) - 1个字节并将其存放到buffer中
        // 经过多次循环,就可以将sock中的内容全部读取
        // 并返回读取内容的字节个数
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            // 给字符串的末尾添加结束符,即\0
            buffer[n] = 0;
            // 将读取的所有数据放入到string类型的inbuffer中
            inbuffer += buffer;

            // 分析处理
            // "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n
            // inbuffer.find(LINE_SEP)就是报头"content_len"右侧的分隔符的位置
            auto pos = inbuffer.find(LINE_SEP);
            if (pos == std::string::npos)
                continue; // 如果没有找到,那么就返回继续读取,再重新找

            // inbuffer.substr(0, pos),获取到报头
            // 报头存放就是报文正文内容所占的字节数,即text_len_string
            std::string text_len_string = inbuffer.substr(0, pos);

            // 将报文正文内容所占的字节数,由字符串转为数字
            int text_len = std::stoi(text_len_string);

            // total_len 报文的总长度
            // text_len_string.size()是报头的长度
            // 2 * LINE_SEP_LEN + text_len 报文正文和两个分割符长度的总和
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;

            // text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();
            std::cout << "处理前#inbuffer: \n"
                      << inbuffer << std::endl;

            // inbuffer.size()小于total_len,说明inbuffer没有一个完整的报文
            // 那么就返回,继续从sock读取,直到inbuffer.size() > total_len
            if (inbuffer.size() < total_len)
            {
                std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
                continue;
            }

            // 至少有一个完整的报文
            // 分割出一个完整报文
            *text = inbuffer.substr(0, total_len);

            // 从inbuffer删除掉分割出的报文
            inbuffer.erase(0, total_len);

            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;

            break;
        }
        else
            // 如果n小于等于0说明对方关闭了sock文件描述符对应的文件,因此返回错误
            return false;
    }
    return true;
}

演示结果

image-20230922221044682

HTTP协议

认识URL

image-20230923103702760

urlencode和urldecode

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

image-20230923105739840

urlencode工具

http协议格式

image-20230923114338116

通过代码来验证(基本框架)

什么是web

makefile

cc=g++
httpserver:HttpServer.cc
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f httpserver

Protocol.hpp

#pragma once
#include <iostream>
#include <string>

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

public:
    std::string inbuffer;
};

class HttpResponse
{
public:
    std::string outbuffer;
};

HttpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#include "Protocol.hpp"

using namespace std;

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    // 默认缺省的端口号为8080
    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;

    class HttpServer
    {
    public:
        // 服务器的构造函数
        HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
        {
        }

        // 初始化服务器
        void initServer()
        {
            // 1. 创建socket文件套接字对象
            // tcp也是网络连接,因此第一个参数为AF_INET
            // tcp是面向字节流的,因此第二个参数为SOCK_STREAM
            // 确定好前两个参数,第三个参数默认为0就可以
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                // 套接字的文件描述符小于0,那么创建套接字失败
                // 则我们通过日志,打印对应的错误信息
                exit(SOCKET_ERR);
            }

            // 2. bind绑定自己的网络信息
            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;

            // bind小于0,代表绑定失败
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                exit(BIND_ERR);
            }

            // tcp是建立连接的协议,因此客户端和服务器是要建立连接的
            // 所谓的连接,也就是服务器实时知道客户端向其发出了请求
            // tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
            // 然后才可以向服务器发送消息
            // 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
            // 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
            //          等待客户的电话,这样才可以实时与客户建立连接

            // 3. 设置socket 为监听状态
            // 参数_listensock,就是要将这个参数设置为监听状态
            // gbacklog,后续再说其用处,目前暂时先设置为5
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
            {
                // // 通过日志,打印设置监听状态失败的错误信息
                exit(LISTEN_ERR);
            }
        }

        void HandlerHttp(int sock)
        {
            // 1. 读到完整的http请求
            // 2. 反序列化
            // 3. httprequst, httpresponse, _func(req, resp)
            // 4. resp序列化
            // 5. send
            char buffer[4096];

            // http的请求协议
            HttpRequest req;

            // http的响应协议
            HttpResponse resp;

            // 大概率我们直接就能读取到完整的http请求
            size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); 
            if (n > 0)
            {
                buffer[n] = 0;
                req.inbuffer = buffer;
                // req.parse();
                _func(req, resp); // req -> resp
                send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
            }
        }

        // 开始启动服务器
        void start()
        {
            // 4. server 获取新链接
            // _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
            // _listensock只负责监听用户请求,新的套接字sock只负责通信
            // sock, 和client进行通信的fd(文件描述符)
            for (;;)
            {
                // 参数
                // struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
                // socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    continue;
                }

                // version 2 多进程版(2)
                pid_t id = fork();
                if (id == 0) // child
                {
                    close(_listensock);
                    if (fork() > 0)
                        exit(0);
                    HandlerHttp(sock);
                    close(sock);
                    exit(0);
                }
                close(sock);

                // father
                pid_t ret = waitpid(id, nullptr, 0);
            }
        }

        ~HttpServer()
        {
        }

    private:
        // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
        int _listensock;
        uint16_t _port;
        func_t _func;
    };

} // namespace server

HttpServer.cc

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

using namespace std;
using namespace server; 

void Usage(std::string proc)
{
    cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}


bool Get(const HttpRequest &req, HttpResponse &resp)
{
    // for test
    cout << "----------------------http start---------------------------" << endl;
    cout << req.inbuffer << std::endl;
    cout << "----------------------http end---------------------------" << endl;

    return true;
}

// ./httpServer 8080
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));
    httpsvr->initServer();
    httpsvr->start();

    return 0;
}

演示结果

  • 将浏览器的网页作为客户端,Linux服务器作为服务端

  • 浏览器将是如下场景(因为此时浏览器并没有向服务器请求任何的文件资源)

image-20230923151627486

  • 服务器是如下的场景

image-20230923151744850

  • 解释

image-20230923153832462

验证代码2

telnet命令

在Linux中,可以使用telnet命令来演示telnet的功能。首先,确保你的Linux系统已经安装了telnet客户端。

  1. 打开终端(Terminal)。

  2. 输入以下命令来安装telnet客户端(如果尚未安装):

    sudo yum install telnet
    

    或者使用适合你的Linux发行版的包管理器来安装telnet。

  3. 输入以下命令来连接到远程主机:

    telnet <远程主机IP地址> <端口号>
    

    替换 <远程主机IP地址><端口号> 为实际的远程主机IP地址和端口号。例如,要连接到IP地址为192.168.0.100的远程主机的23号端口,可以输入:

    telnet 192.168.0.100 23
    
  4. 如果连接成功,你将看到一个类似于以下的提示符:

    Trying <远程主机IP地址>...
    Connected to <远程主机IP地址>.
    Escape character is '^]'.
    
  5. 可以按下Ctrl + ],然后输入http协议响应首行(【版本】【状态码】【状态码描述】)

请注意,telnet协议是明文传输数据的,因此不建议在不受信任的网络中使用telnet。在安全性要求较高的环境中,应使用更安全的协议,如SSH。

HttpServer.cc

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

using namespace std;
using namespace server;

void Usage(std::string proc)
{
    cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}

bool Get(const HttpRequest &req, HttpResponse &resp)
{
    // for test
    cout << "----------------------http start---------------------------" << endl;
    cout << req.inbuffer << std::endl;
    cout << "----------------------http end---------------------------" << endl;

    // 响应首行,分别对应http版本, 状态码, 状态码描述
    std::string respline = "HTTP/1.1 200 OK\r\n";

    // 分割行
    std::string respblank = "\r\n";

    // "<html></html>" 代表开始和结尾
    // "<head></head>" 代表标题的开始和结尾
    // "<body></body>" 代表正文内容的开始和结尾
    // "<title></title>" 代表标签,就是浏览器上方显示的这个网页的标签
    // std::string body = "<html><head><title></head><body></body></html>"
    // \"en\" : 使用 \ 将 " 进行转义
    std::string body = "<html lang=\"en\"><head><title>for test</title><h1>hello word</h1></head><body><p>I dedicate my whole life to loving you</body></html>";

    resp.outbuffer += respline;
    resp.outbuffer += respblank;
    resp.outbuffer += body;

    return true;
}

// ./httpServer 8080
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));
    httpsvr->initServer();
    httpsvr->start();

    return 0;
}

演示结果

image-20230923162320572

演示结果

image-20230923163055318

验证代码3

stringstream的用法

  1. 服务器和网页分离,html

  2. url -> / : web根目录

创建目录wwwroot(客户端默认目录)

makefile

cc=g++
httpserver:HttpServer.cc
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f httpserver

Util.hpp

#pragma once

#include <iostream>
#include <string>

class Util
{
public:
    // XXXX XXX XXX\r\nYYYYY  -->首行对应:XXXX XXX XXX
    // 请求行的首行: 【方法】【url】【版本】\r\n
    static std::string getOneLine(std::string &buffer, const std::string &sep)
    {
        // 查找首行的第一个分隔符(这个分割符时\r\n,定义的宏是sep)
        auto pos = buffer.find(sep);

        if(pos == std::string::npos) 
        {
            return "";
        }
        std::string sub = buffer.substr(0, pos);

        // 将字符串和分割符删除
        buffer.erase(0, sub.size()+sep.size());
        return sub;
    }
};

Protocol.hpp

#pragma once
#include <iostream>
#include <string>
#include <sstream>
#include "Util.hpp"

const std::string sep = "\r\n";

// 默认目录,就是web客户端的根目录
const std::string default_root = "./wwwroot";

// 当客户端没有请求资源时,那么就会将默认路径下主页资源(index.html)返回给客户端
// 默认路径下的主页资源就是我们平时打开浏览器的首页,我们刚打开浏览器时,还没有请求任何资源
const std::string home_page = "index.html";

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

    void parse()
    {
        // 1. 从inbuffer中拿到第一行,分隔符\r\n
        std::string line = Util::getOneLine(inbuffer, sep);
        if (line.empty())
            return;

        // 2. 从请求行中提取三个字段
        // std::cout << "line: " << line << std::endl;

        std::stringstream ss(line);
        // stringstream 默认是以空格来分割字符串的
        // method url httpversion 对应【方法】【url】【版本】
        ss >> method >> url >> httpversion;

        // 3. 添加web默认路径
        path = default_root; // 默认路径为:./wwwroot

        // 当url为/a/b/c.html,那么就是请求./wwwroot/a/b/c.html路径下的文件资源
        // 当客户端没有请求资源时,url就为 / 那么path即使./wwwroot/,
        // 当path为./wwwroot/时,我们就将默认路径下的主页资源返回给客户端
        path += url;
        if (path[path.size() - 1] == '/')
            path += home_page;
    }

public:
    std::string inbuffer;
    std::string method;
    std::string url;
    std::string httpversion;
    std::string path;
};

class HttpResponse
{
public:
    std::string outbuffer;
};

HttpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#include "Protocol.hpp"
#include "Util.hpp"

using namespace std;

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    // 默认缺省的端口号为8080
    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;

    class HttpServer
    {
    public:
        // 服务器的构造函数
        HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
        {
        }

        // 初始化服务器
        void initServer()
        {
            // 1. 创建socket文件套接字对象
            // tcp也是网络连接,因此第一个参数为AF_INET
            // tcp是面向字节流的,因此第二个参数为SOCK_STREAM
            // 确定好前两个参数,第三个参数默认为0就可以
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                // 套接字的文件描述符小于0,那么创建套接字失败
                // 则我们通过日志,打印对应的错误信息
                exit(SOCKET_ERR);
            }

            // 2. bind绑定自己的网络信息
            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;

            // bind小于0,代表绑定失败
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                exit(BIND_ERR);
            }

            // tcp是建立连接的协议,因此客户端和服务器是要建立连接的
            // 所谓的连接,也就是服务器实时知道客户端向其发出了请求
            // tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
            // 然后才可以向服务器发送消息
            // 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
            // 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
            //          等待客户的电话,这样才可以实时与客户建立连接

            // 3. 设置socket 为监听状态
            // 参数_listensock,就是要将这个参数设置为监听状态
            // gbacklog,后续再说其用处,目前暂时先设置为5
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
            {
                // // 通过日志,打印设置监听状态失败的错误信息
                exit(LISTEN_ERR);
            }
        }

        void HandlerHttp(int sock)
        {
            // 1. 读到完整的http请求
            // 2. 反序列化
            // 3. httprequst, httpresponse, _func(req, resp)
            // 4. resp序列化
            // 5. send
            char buffer[4096];

            // 定义http的请求协议
            HttpRequest req;

            // 定义http的响应协议
            HttpResponse resp;

            // 大概率我们直接就能读取到完整的http请求
            size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); 
            if (n > 0)
            {
                buffer[n] = 0;
                req.inbuffer = buffer;
                req.parse();
                _func(req, resp); // req -> resp
                send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
            }
        }

        // 开始启动服务器
        void start()
        {
            // 4. server 获取新链接
            // _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
            // _listensock只负责监听用户请求,新的套接字sock只负责通信
            // sock, 和client进行通信的fd(文件描述符)
            for (;;)
            {
                // 参数
                // struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
                // socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    continue;
                }

                // version 2 多进程版(2)
                pid_t id = fork();
                if (id == 0) // child
                {
                    close(_listensock);
                    if (fork() > 0)
                        exit(0);
                    HandlerHttp(sock);
                    close(sock);
                    exit(0);
                }
                close(sock);

                // father
                pid_t ret = waitpid(id, nullptr, 0);
            }
        }

        ~HttpServer()
        {
        }

    private:
        // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
        int _listensock;
        uint16_t _port;
        func_t _func;
    };

} // namespace server

HttpServer.cc

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

using namespace std;
using namespace server;

void Usage(std::string proc)
{
    cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}

bool Get(const HttpRequest &req, HttpResponse &resp)
{
    // for test
    cout << "----------------------http start---------------------------" << endl;
    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;
    cout << "----------------------http end---------------------------" << endl;

    // 响应首行,分别对应http版本, 状态码, 状态码描述
    std::string respline = "HTTP/1.1 200 OK\r\n";

    // 代表我的正文属于什么资源
    std::string respheader = "Content-Type: text/html\r\n";

    // 分割行
    std::string respblank = "\r\n";

    // "<html></html>" 代表开始和结尾
    // "<head></head>" 代表标题的开始和结尾
    // "<body></body>" 代表正文内容的开始和结尾
    // "<title></title>" 代表标签,就是浏览器上方显示的这个网页的标签
    // <meta charset="UTF-8">  是编码方式
    // std::string body = "<html><head><title></head><body></body></html>"
    // \"en\" : 使用 \ 将 " 进行转义
    std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello word</h1></head><body><p>you are right</body></html>";

    resp.outbuffer += respline;
     resp.outbuffer += respheader;
    resp.outbuffer += respblank;
    resp.outbuffer += body;

    return true;
}

// ./httpServer 8080
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));
    httpsvr->initServer();
    httpsvr->start();

    return 0;
}

演示结果

image-20230923225554110

验证代码4

ifstream的使用方法

ifstream的构造方法

stat()

// 头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

// 函数
int stat(const char *path, struct stat *buf);

// 参数
const char *path:文件路径
struct stat *buf:结构体(包含文件属性),其中包含所读取文件的大小,即off_t st_size; 

struct stat
{
    dev_t st_dev;         /* ID of device containing file */
    ino_t st_ino;         /* inode number */
    mode_t st_mode;       /* protection */
    nlink_t st_nlink;     /* number of hard links */
    uid_t st_uid;         /* user ID of owner */
    gid_t st_gid;         /* group ID of owner */
    dev_t st_rdev;        /* device ID (if special file) */
    off_t st_size;        /* total size, in bytes */
    blksize_t st_blksize; /* blocksize for file system I/O */
    blkcnt_t st_blocks;   /* number of 512B blocks allocated */
    time_t st_atime;      /* time of last access */
    time_t st_mtime;      /* time of last modification */
    time_t st_ctime;      /* time of last status change */
};

RETURN VALUE
    On success,zero is returned.On error, -1 is returned, and errno is set appropriately.

创建wwwroot目录

image-20230925213517489

test/a.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>beautiful</title>
</head>
<body>
    <h1>a网页</h1>
    <img src="https://qwy.oss-cn-chengdu.aliyuncs.com/image_for_typora/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20230821185354.jpg" alt="天使">
    <img src="https://qwy.oss-cn-chengdu.aliyuncs.com/image_for_typora/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20230821185356.jpg" alt="天使">
    <a href="/"> 返回 </a> 
</body>
</html>

test/b.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>elegant</title>
</head>
<body>
    <h1>b网页</h1>
    <img src="https://qwy.oss-cn-chengdu.aliyuncs.com/image_for_typora/image-20230924222102670.png" alt="天使">
    <a href="/"> 返回 </a> 
</body>
</html>

wwwroot/404.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>资源不存在</title>
</head>
<body>
    <h1>你所访问的资源并不存在,404</h1>
</body>
</html>

wwwroot/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XXX专属</title>
</head>

<body>
    <h1>hello world</h1>

    <!--  这是html文件的注释符 -->

    <!--<a href="/test/a.html">新闻</a> 
    表示跳转到当前目录下/test/a.html这个文件 -->

    <a href="/test/a.html">a网页</a>
    <a href="/test/b.html">b网页</a>

    <!--  加载图片成功,显示图片,加载图片失败则返回alt存储的字符
     图片的位置时在当前目录下的image中-->
    <!--  这张图片由于图片过大,服务器配置太低,所以未能传输到客户端-->
    <!-- <img src="/image/1.jpg" alt="无人机"> -->

    <!-- 登录框 -->
    <!-- "/a/b/c.py" 代表未来将会把登录框的数据将传递给这个文件 -->
    <!-- "POST" 代表提取登录框数据的方法 -->
    <form action="/a/b/c.py" method="POST">
        姓名:<br>
        <input type="text" name="xname">
        <br>
        密码:<br>
        <input type="password" name="ypwd">
        <br><br>
        <input type="submit" value="登陆">
    </form>


    <!-- <br> 就是回车换行 -->
    <!-- submit n.按钮 -->
    <!--
    <form action="/a/b/c.py" method="GET">
        姓名:<br>
        <input type="text" name="xname">
        <br>
        密码:<br>
        <input type="password" name="ypwd">
        <br><br>
        <input type="submit" value="登陆">
    </form>
    -->


</body>

</html>

makefile

cc=g++
httpserver:HttpServer.cc
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f httpserver

Util.hpp

#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <fcntl.h>

class Util
{
public:
    // XXXX XXX XXX\r\nYYYYY  -->首行对应:XXXX XXX XXX
    // 请求行的首行: 【方法】【url】【版本】\r\n
    static std::string getOneLine(std::string &buffer, const std::string &sep)
    {
        // 查找首行的第一个分隔符(这个分割符时\r\n,定义的宏是sep)
        auto pos = buffer.find(sep);

        if(pos == std::string::npos) 
        {
            return "";
        }
        std::string sub = buffer.substr(0, pos);

        // 将字符串和分割符删除
        buffer.erase(0, sub.size()+sep.size());
        return sub;
    }


   static bool readFile(const std::string resource, char *buffer, int size)
    {
        std::ifstream in(resource, std::ios::binary);
        if(!in.is_open()) return false; // resource not found

        // 需要用二进制的方式读取
        in.read(buffer, size);

        // getline(in, line)只能读取字符串
        // std::string line;
        // while(std::getline(in, line))
        // {
        //     *out += line;
        // }

        in.close();
        return true;
    }
};

HttpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#include "Protocol.hpp"
#include "Util.hpp"

using namespace std;

namespace server
{
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR
    };

    // 默认缺省的端口号为8080
    static const uint16_t gport = 8080;
    static const int gbacklog = 5;

    using func_t = std::function<bool(const HttpRequest &, HttpResponse &)>;

    class HttpServer
    {
    public:
        // 服务器的构造函数
        HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
        {
        }

        // 初始化服务器
        void initServer()
        {
            // 1. 创建socket文件套接字对象
            // tcp也是网络连接,因此第一个参数为AF_INET
            // tcp是面向字节流的,因此第二个参数为SOCK_STREAM
            // 确定好前两个参数,第三个参数默认为0就可以
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                // 套接字的文件描述符小于0,那么创建套接字失败
                // 则我们通过日志,打印对应的错误信息
                exit(SOCKET_ERR);
            }

            // 2. bind绑定自己的网络信息
            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;

            // bind小于0,代表绑定失败
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                exit(BIND_ERR);
            }

            // tcp是建立连接的协议,因此客户端和服务器是要建立连接的
            // 所谓的连接,也就是服务器实时知道客户端向其发出了请求
            // tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
            // 然后才可以向服务器发送消息
            // 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
            // 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
            //          等待客户的电话,这样才可以实时与客户建立连接

            // 3. 设置socket 为监听状态
            // 参数_listensock,就是要将这个参数设置为监听状态
            // gbacklog,后续再说其用处,目前暂时先设置为5
            if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
            {
                // // 通过日志,打印设置监听状态失败的错误信息
                exit(LISTEN_ERR);
            }
        }

        void HandlerHttp(int sock)
        {
            // 1. 读到完整的http请求
            // 2. 反序列化
            // 3. httprequst, httpresponse, _func(req, resp)
            // 4. resp序列化
            // 5. send
            char buffer[4096];

            // 定义http的请求协议
            HttpRequest req;

            // 定义http的响应协议
            HttpResponse resp;

            // 大概率我们直接就能读取到完整的http请求
            size_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); 
            if (n > 0)
            {
                buffer[n] = 0;
                req.inbuffer = buffer;
                req.parse();
                _func(req, resp); // req -> resp
                send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
            }
        }

        // 开始启动服务器
        void start()
        {
            // 4. server 获取新链接
            // _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
            // _listensock只负责监听用户请求,新的套接字sock只负责通信
            // sock, 和client进行通信的fd(文件描述符)
            for (;;)
            {
                // 参数
                // struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
                // socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    continue;
                }

                // version 2 多进程版(2)
                pid_t id = fork();
                if (id == 0) // child
                {
                    close(_listensock);
                    if (fork() > 0)
                        exit(0);
                    HandlerHttp(sock);
                    close(sock);
                    exit(0);
                }
                close(sock);

                // father
                pid_t ret = waitpid(id, nullptr, 0);
            }
        }

        ~HttpServer()
        {}

    private:
        // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
        int _listensock;
        uint16_t _port;
        func_t _func;
    };

} // namespace server

HttpServer.cc

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

using namespace std;
using namespace server;

void Usage(std::string proc)
{
    cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}

std::string suffixToDesc(const std::string suffix)
{
    std::string ct = "Content-Type: ";
    if (suffix == ".html")
        ct += "text/html";
    else if (suffix == ".jpg")
        ct += "application/x-jpg";

    // "Content-Length: " 后也需要有"\r\n",此处不可以省略
    // 否则,服务器无法解析
    ct += "\r\n";
    return ct;
}

bool Get(const HttpRequest &req, HttpResponse &resp)
{
    // for test
    cout << "----------------------http start---------------------------" << endl;
    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;
    cout << "----------------------http end---------------------------" << endl;

    // 版本1
    // 响应首行,分别对应http版本, 状态码, 状态码描述
    // std::string respline = "HTTP/1.1 200 OK\r\n";

    // location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
    // 版本2(location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问)
    std::string respline = "HTTP/1.1 307 Temporary Redirect\r\n";

    // 传输资源的类型
    std::string respheader = suffixToDesc(req.suffix);
    // 如果size大于0,说明正文是存在内容的
    if (req.size > 0)
    {
        // Content-Length,正文的长度
        respheader += "Content-Length: ";
        respheader += std::to_string(req.size);

        // "Content-Length: " 后也需要有"\r\n",此处不可以省略
        // 否则,服务器无法解析
        respheader += "\r\n";
    }

    // 搭配版本2的respline使用
    // 都需要\r\n进行分割,以便客户端识别
    respheader += "location: https://www.snut.edu.cn/\r\n ";

    // 分割行(此处仅仅只是定义了,并没有将其合并给报头)
    std::string respblank = "\r\n";

    std::string body;
    // req.size+1,c语言字符串有结束符\0
    body.resize(req.size+1);
    if (!Util::readFile(req.path, (char*)body.c_str(), req.size))
    {
        // 如果读取失败,那么就给客户端返回404错误码
        // 这里假设读取html_404一定是可以成功的
        Util::readFile(html_404,(char*)body.c_str(), req.size);
    }

    // 响应的首行内容
    resp.outbuffer += respline;

    // 响应的报头
    resp.outbuffer += respheader;

    // 观察服务器给客户端响应的内容(由于图片是二进制,所以就不查看了)
    cout << "----------------------http response start---------------------------" << endl;
    std::cout << resp.outbuffer << std::endl;
    cout << "----------------------http response end---------------------------" << endl;

    // 分割行,分割报头和正文内容
    resp.outbuffer += respblank;

    // 响应的正文内容
    resp.outbuffer += body;

    return true;
}

// ./httpServer 8080
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));
    httpsvr->initServer();

    httpsvr->start();

    return 0;
}

GET方法与POST方法的区别

image-20230925223511184
------http start---------------------------" << endl;
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;
cout << “----------------------http end---------------------------” << endl;

// 版本1
// 响应首行,分别对应http版本, 状态码, 状态码描述
// std::string respline = "HTTP/1.1 200 OK\r\n";

// location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
// 版本2(location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问)
std::string respline = "HTTP/1.1 307 Temporary Redirect\r\n";

// 传输资源的类型
std::string respheader = suffixToDesc(req.suffix);
// 如果size大于0,说明正文是存在内容的
if (req.size > 0)
{
    // Content-Length,正文的长度
    respheader += "Content-Length: ";
    respheader += std::to_string(req.size);

    // "Content-Length: " 后也需要有"\r\n",此处不可以省略
    // 否则,服务器无法解析
    respheader += "\r\n";
}

// 搭配版本2的respline使用
// 都需要\r\n进行分割,以便客户端识别
respheader += "location: https://www.snut.edu.cn/\r\n ";

// 分割行(此处仅仅只是定义了,并没有将其合并给报头)
std::string respblank = "\r\n";

std::string body;
// req.size+1,c语言字符串有结束符\0
body.resize(req.size+1);
if (!Util::readFile(req.path, (char*)body.c_str(), req.size))
{
    // 如果读取失败,那么就给客户端返回404错误码
    // 这里假设读取html_404一定是可以成功的
    Util::readFile(html_404,(char*)body.c_str(), req.size);
}

// 响应的首行内容
resp.outbuffer += respline;

// 响应的报头
resp.outbuffer += respheader;

// 观察服务器给客户端响应的内容(由于图片是二进制,所以就不查看了)
cout << "----------------------http response start---------------------------" << endl;
std::cout << resp.outbuffer << std::endl;
cout << "----------------------http response end---------------------------" << endl;

// 分割行,分割报头和正文内容
resp.outbuffer += respblank;

// 响应的正文内容
resp.outbuffer += body;

return true;

}

// ./httpServer 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr httpsvr(new HttpServer(Get, port));
httpsvr->initServer();

httpsvr->start();

return 0;

}


## GET方法与POST方法的区别

[外链图片转存中...(img-blAqSqt1-1743662363073)]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值