HTTP/HTTPS协议(一)

常见的应用层协议

HTTP/HTTPS称为超文本传输协议,用于传输浏览器使用的普通文本、超文本、音频和视频等数据。

HTTP协议

认识URL

url也就是网址,主要是由协议方案名,登录信息,服务器地址,服务器端口号,带层次的路径,查询字符串以及片段标识符组成。

当然现在有些信息在浏览器中被隐藏起来,并且会对一些字符类似于+,?,/等字符进行转义,如下图所示,c++的加号被转义成%2B,转义规则一般是将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式。

 HTTP协议格式

http是怎么样分离报头跟有效载荷的,主要是通过空行来决定的,http的协议格式主要如下所示,分为请求端以及响应端;

请求端:

请求行,请求报头,空行以及正文部分组成,其中请求行中包含了[方法]+[url]+[版本],其中方法主要是包括GET方法(获取资源)以及POST方法(传输实体主题)。请求报头也称为header,主要包括请求的属性,类似于长度,主机地址等等,在接下来一个空行分隔报头和正文内容,正文内容也叫做body,body允许为空字符串,如果body存在,则header中会有一个Content-Length属性标识Body的长度。

响应端:

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

相应端的状态码

类别    原因短语
1xxinformational(信息状态码)正在处理请求
2xxSuccess(成功状态码)请求处理完毕
3xxRedirection(重定向状态码)需要进行附加操作以完成请求
4xxClient Error(客户端错误状态码)服务器无法处理请求
5xxServer Error(服务端错误状态码)服务器处理请求出错

 最常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向), 504(Bad Gateway)。

上诉的请求和响应的细节:

1、请求和响应如何保证应用层完整读取完毕了?

首先c++保证了可以完整的读取一行,使用while(getline(request,line))就可以读到所有的请求行+请求报头全部读完直到空行,根据报头中的content-Length属性,就可以确定正文的长度,通过解析出来的内容长度,根据内容长度读取正文即可。

2、请求和响应怎么做到序列化和反序列化的?

hhtp自己实现的,第一行+请求/响应报头,只需要按照\r\n将字符串1->n既可以,正文不用做。

当然对于直接调用http协议的话,就不需要做序列化和反序列化。

示例1:网络计算器,自己实现的序列化以及反序列化

协议以及打印错误信息的的代码,这里请求和响应必须同时带有serialize和diserialize的功能,进行序列化和反序列化,然后需要计算正文的长度。这里的协议采用两种方式,一种是普通的字符串方式,一种是JSON的方式

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 " "
#define SEP_LEN strlen(SEP) // sizeof 计算长度的时候会加上/n   而strlen 只计算字符的个数
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)
enum
{
    PERICOUS = 0,
    DIV_ZERO,
    MOD_ZERO,
    INPUT_ERROR
};
// 增加字长去区分正文和标记
// 发送字节流的格式是  “content_len\r\ncontent\r\n”
std::string enLength(const std::string &str)
{
    std::string res_text = std::to_string(str.size());
    res_text+=LINE_SEP;
    res_text+=str;
    res_text+=LINE_SEP;
    return res_text;
}
//去掉前面的“content_len\r\ncontent\r\n” -->content_len,将内容存储在req_str中
bool deLength(std::string &str, std::string *req_str)
{
    auto pos = str.find(LINE_SEP);
    if(pos ==std::string::npos) return false;
    std::string content_len = str.substr(0,pos);
    ssize_t len = std::stoi(content_len);
    //这里求出来的是内容的长度
    *req_str=str.substr(pos+LINE_SEP_LEN,len);
    return true;

}
//没有人规定网络通信的时候,规定一种协议
//怎么让系统知道我们使用的是哪一种协议 呢
// x + y ------->res
// 因为TCP是全双工的,所以对于string的数据需要把他转化成 x op y的形式
class Request
{
public:
    Request()
        : _x(0), _op('+'), _y(0)
    {
    }
    Request(int x, char op, int y)
        : _x(x), _op(op), _y(y)
    {
    }
    // 1. 序列化---》1.自己写  2. 用现成的
    //  "x + y" 需要转换成这样的形式,然后转成字节流传送给服务端   --序列化
    bool serialize(std::string *expression) // 输入输出型参数
    {
#ifdef MYSELF
        *expression = std::to_string(_x);
        *expression += SEP;
        *expression += _op;
        *expression += SEP;
        *expression += std::to_string(_y);
#else
    Json::Value root;
    root["first"] = _x;
    root["op"] = _op;
    root["second"]=_y;

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

    // "x + y /t/n" 的string 类型的对象,需要将他拆成 _x _op _y的格式   --反序列化
    bool deserialize(const std::string &str)
    {
#ifdef MYSELF
        size_t left = str.find(SEP);
        size_t right = str.rfind(SEP);
        if (left == std::string::npos || right == std::string::npos)
            return false;
        if (left == right)
            return false;
        if (right - (left + SEP_LEN) != 1)
            return false;
        std::string x_string = str.substr(0,left);
        std::string y_string = str.substr(right+SEP_LEN);
        if(x_string.empty() || y_string.empty()) return false;
        _x = std::stoi(x_string);
        _y = std::stoi(y_string);
        _op = str[left + SEP_LEN];
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(str,root);

        _x=root["first"].asInt();
        _y=root["second"].asInt();
        _op=root["op"].asInt();

#endif
        return true;
    }

    ~Request() {}

public:
    int _x;
    char _op;
    int _y;
};

//
class Response
{
public:
    Response()
        : _exitcode(0), _exitrest(0)
    {
    }
    Response(const int &exitrest, const int &exitcode)
        : _exitcode(exitcode), _exitrest(exitrest)
    {
    }
    // 把response 的exitcode 和exitresult 包装成字符串传出去,用于网络层的传输
    //"exitcode exitresult"
    bool serialize(std::string *expression)
    {
#ifdef MYSELF
        *expression += std::to_string(_exitcode);
        *expression += SEP;
        *expression += std::to_string(_exitrest);
#else
        Json::Value root;
        root["_exitrest"] = _exitrest;
        root["_exitcode"] = _exitcode;

        Json::FastWriter writer;
        *expression = writer.write(root);

#endif
        return true;
    }
    //把"exitcode exitresult"的字符串进行分割
    bool deserialize(std::string &str)
    {
#ifdef MYSELF
        auto pos = str.find(SEP);
        if(pos == std::string::npos)    return false;
        std::string ec_string =str.substr(0,pos);
        std::string res_string =str.substr(pos+SEP_LEN);
        if(ec_string.empty() || res_string.empty()) return false;

        _exitcode = std::stoi(ec_string);
        _exitrest = std::stoi(res_string);
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(str,root);

        _exitrest=root["_exitrest"].asInt();
        _exitcode=root["_exitcode"].asInt();
#endif
        return true;
    }
    ~Response() {}

public:
    int _exitrest;  //计算的结果
    int _exitcode; // 0 表示计算结果正确   1 表示除法除数为0 或者%运算有0
};


//content_len\r\ncontent\r\n content_len\r\ncontent\r\ncontent_len\r\ncontent\r\ncontent_len\r\ncontent\r\n
bool recvRequest(const int sock,std::string &inbuffer,std::string *req_text)
{
    char buffer[1024];
    while(true)
    {
        ssize_t num = recv(sock,buffer,sizeof(buffer)-1,0);
        if(num>0)
        {
            buffer[num]=0; //给尾部置0
            inbuffer+=buffer; 
            std::cout << "读取到的inbuffer: " << inbuffer << std::endl;
            auto pos = inbuffer.find(LINE_SEP);
            if(pos == std::string::npos) continue;
            //就是读到了第一个/r/n
            //首先计算出正文的长度
            std::string len_str = inbuffer.substr(0,pos);
            int len_content = std::stoi(len_str);
            int total_len = len_str.size()+2*LINE_SEP_LEN+len_content;
            std::cout << "处理前#inbuffer: " << inbuffer << std::endl;
            if(inbuffer.size()<total_len)
            {
                std::cout<<"输入的消息未遵守协议,正在等待后续内容"<<std::endl;
                continue;
            }
            //至少由一个完整的报文  //已经将正文内容读取到了text_content中
            *req_text= inbuffer.substr(0,total_len);
            //抹去已经读取到的一个完整的报文
            std::cout<<"从inbuffer中读取到的数据"<<*req_text<<std::endl;
            inbuffer.erase(0,total_len);

            std::cout<<"处理后#inbuffer:"<<inbuffer<<std::endl;
            break;
        }
        else
        {
            return false;
        }
            
    }
    return true;
}

客户端代码:客户端需要进行tcp链接,首先需要socket通信,tcp的客户端需要bind,但是不用显示的bind,os这里尤其是client port要让OS自定随机指定!不需要进行listen和accept操作,需要向服务端发起connect连接,在connect之后,需要将自己的需求发送给服务端send操作,在服务端计算完成之后,进行recv操作。

calClient.hpp

#pragma once
#include <iostream>
#include <cstring>
#include "data.hpp"
#include <unordered_map>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
namespace client
{
    using namespace std;
    enum
    {
        USAGE_ERROR = 0,
        SOCKET_ERROR,
        BIND_ERROR
    };
    class calclient
    {
    public:
        calclient(const string &ip, const uint16_t port)
            : _sockfd(-1), _serverip(ip), _serverport(port)
        {
        }
        void initClient()
        {
            // 1.跟udp一样 首先进行socket设置
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                cout << "SOCKET error" << endl;
                exit(SOCKET_ERROR);
            }
            cout << "SOCKET success" << endl;
            // 2. tcp的客户端要不要bind?要的! 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
            // 3. 要不要listen?不要!
            // 4. 要不要accept? 不要!
            // 5. 要什么呢??要发起链接
        }
        void run()
        {
            // 这里需要connect链接
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            peer.sin_family = AF_INET;
            peer.sin_port = htons(_serverport);
            peer.sin_addr.s_addr = inet_addr(_serverip.c_str());
            if (connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer)) != 0)
            {
                cerr << "socket connect error" << endl;
            }
            else
            {
                string message;
                string inbuffer;
                while(true)
                {
                    cout << "mycal <<<<<";
                    getline(cin, message);  //1+1 -->用来初始化req的格式
                    Request req = ParseLine(message); 
                    std::string content;
                    req.serialize(&content);
                    cout<<"序列化之后的content: "<<content<<endl;
                    std::string send_message = enLength(content);
                    cout<<"加上分割符的send_message: "<<send_message<<endl;
                    send(_sockfd,send_message.c_str(),send_message.size(),0);
                    // 说明数据发送过去了
                    std::string package,text;
                    if(!recvRequest(_sockfd,inbuffer,&package))
                        continue;
                    cout<<"package= "<<package<<endl;
                    if(!deLength(package,&text)) 
                    {
                        continue;
                    }
                    //exit_code exit_result
                    Response resp;
                    resp.deserialize(text);
                    std::cout<<"exitcode: "<<resp._exitcode<<"_exitrest: "<<resp._exitrest<<endl;
                }
            }
        }
        ~calclient() {}
        //
    Request ParseLine(const std::string &line)
    {
        // 建议版本的状态机!
        //"1+1" "123*456" "12/0"
        int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
        int i = 0;
        int cnt = line.size();
        std::string left, right;
        char op;
        while (i < cnt)
        {
            switch (status)
            {
            case 0:
            {
                if(!isdigit(line[i]))
                {
                    op = line[i];
                    status = 1;
                }
                else left.push_back(line[i++]);
            }
            break;
            case 1:
                i++;
                status = 2;
                break;
            case 2:
                right.push_back(line[i++]);
                break;
            }
        }
        std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;
        return Request(std::stoi(left),op, std::stoi(right));
    }
    private:
        int _sockfd;
        string _serverip;
        uint16_t _serverport;
        char _op[5] ={'+','-','*','/','%'};
    };
}

calClient.cc:主要是启动服务端

#include "calClient.hpp"
#include <memory>
using namespace client;
static void Usage(char* proc)
{
    cout<<"/n/t"<<proc<<" local port error"<<endl;
}
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    //对输入的参数进行转化
    uint16_t port = atoi(argv[2]);
    //创建指针
    unique_ptr<calclient> tclt(new calclient(argv[1],port));
    //开始初始化
    tclt->initClient();
    tclt->run();
    return 0;
}

服务端的代码:

首先需要创建套接字,并且绑定固定的端口,在进行监听操作,这里就是和udp不同的地方需要监听的,然后进行accept获取新连接,获取新连接之后,返回对应的文件描述符,通过文件描述符与对应客户端进行链接,对于实际情况中,可能多个客户端与服务端相连,这是就需要采取多线程或者多进程的解决方案,尽量不要在服务端的内部完成业务函数,尽量写在外面,完成解耦,一贯原则:低耦合,高内聚。

calServer.hpp

#pragma once
#include "logMessage.hpp"
#include "data.hpp"
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <functional>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>
namespace server
{
    using namespace std;
    enum
    {
        USAGE_ERROR = 0,
        SOCKET_ERROR,
        BIND_ERROR,
        LISTEN_ERROR
    };
     #define NUM 1024
    static int backlog = 5;
    static uint16_t severport = 8080;
    //using func_t = function<bool()>;
    typedef function<bool(const Request& resq,Response& resp)> func_t;  //这里resp是个输出型参数 
    //保证解耦
    void calHandle(int sock,func_t func)
    {
        std::string inbuffer;
        while(true)
        {
            //1.读取
            //1.1 保证读到的消息是 【一个】 完整的请求
            std::string req_text,req_str;
            //1.2 保证req_text里面肯定是一个完整的请求 "content_len"\r\n"x op y"\r\n
            if(!recvRequest(sock,inbuffer,&req_text))  
                return;
            std::cout << "带报头的请求:" << req_text << std::endl;
            if(!deLength(req_text, &req_str))  
                return;   //
            std::cout << "去掉报头的正文:" << req_str << std::endl;    
            //2. 反序列化
            //2.1 得到一个结构化的请求对象
            Request resq;
            if(! resq.deserialize(req_str)) 
                return ;   
            //3. 计算机处理req.x req.op req.y   --业务逻辑
            //3.1 得到一个结构化的响应
            Response resp;
            func(resq,resp);      //resq的处理结果,全部放入到了re

            //4.对响应Response 进行序列化
            //4.1 得到一个字符串
            std::string resp_str;
            resp.serialize(&resp_str); 
            std::cout << "计算完成, 序列化响应: " <<  resp_str << std::endl;
            // 5.发送响应
            //5.1 首先需要构建一个完整的报文
            std::string send_string = enLength(resp_str);
            std::cout<<"构成完整的响应 :"<<send_string<<std::endl;
            // send()
            send(sock,send_string.c_str(),send_string.size(),0);
        }
    }
    
    class calserver
    {
    public:
        calserver(const uint16_t &port = severport)
            : _listenSockfd(-1), _port(port)
        {
        }
        void initServer()
        {
            // 1.首先创建套接字
            _listenSockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_listenSockfd == -1)
            {
                debugLog(FATAL, "socket error");
                exit(SOCKET_ERROR);
            }
            debugLog(NORMAL, "socket success %d",_listenSockfd);
            // 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; // 不需要绑定特定的端口号,所有的底层网络收到的消息都往上传

            if (bind(_listenSockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                debugLog(FATAL, "bind error");
                exit(BIND_ERROR);
            }
            debugLog(NORMAL, "bind success");
            // 这里tcp与udp最大的不同之处在于,tcp是需要建立链接的,也就是需要监听的
            int sockfd = listen(_listenSockfd, backlog); // 这里先把第二个参数设置为5
            // 3.这里开始进行监听操作
            if (sockfd < 0) // 建立链接失败
            {
                debugLog(FATAL, "listen error");
                exit(LISTEN_ERROR);
            }
            debugLog(NORMAL, "listen sucess %d",sockfd);
        }

        void runServer(func_t func)
        {
            for (;;)
            {
                // 4.sever 获取新链接 accept函数
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                // 这里accept返回的sock也是一个文件描述符,通过这个与对应客户端进行链接
                int sock = accept(_listenSockfd, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    debugLog(ERROR, "accept sock error");
                    continue;
                }
                debugLog(NORMAL, "accept a new link");
                //version2使用多进程
                pid_t id = fork();
                if(id==0)
                {
                   //child子进程
                   //对于子进程而言,需要父进程进行回收,父进程回收的话有两种方式
                   //阻塞等待和非阻塞等待,但是执行任务时死循环,如果阻塞等待跟串行运行没有区别
                   //所以选择再次fork()出一个孙子进程,再把子进程回收,然孙子进程去OS操作系统下
                    close(_listenSockfd); //对于每个子进程而言,都有父进程的资源,所以可以选择把listensockfd关闭
                    if(fork()>0) exit(-1);
                    calHandle(sock,func);
                    close(sock);
                    exit(0);  //执行完成自动交给父进程退出
                }
                //father
                waitpid(id,nullptr,0);
            }
        }
        ~calserver()
        {
        }

    private:
        int _listenSockfd;
        uint16_t _port;
    };

}

上诉程序的多进程处可以进行如下的修改

 // version1 单进程主进程
 //  5. 这里就是一个sock,未来通信我们就用这个sock,面向字节流的,后续全部都是文件操作
 //  seveceIO(sock,clientport);
 //  close(sock);


 version3 多线程模式
// pthread_t pdt;
// threadData *td = new threadData(this, sock,clientport);
// pthread_create(&pdt, nullptr, startRoutine, td);
// // 这里也不需要阻塞式等待
// //pthread_join();


// //version4 线程池的方式
// ThreadPool<CalTask>::getInstace()->run();
// CalTask t(sock,seveceIO);
// ThreadPool<CalTask>::getInstace()->push(t);

calServer.cc代码:主要是业务逻辑代码以及服务端初始代码

#include "calServer.hpp"
#include "data.hpp"
#include <memory>
using namespace server;

static void Usage(char *proc)
{
    cout << "/n/t" << proc << " local port error" << endl;
}
bool calfunc(const Request &resq, Response &resp)
{
    resp._exitcode = PERICOUS;
    switch (resq._op)
    {
    case '+':
    {
        resp._exitrest = resq._x + resq._y;
        break;
    }
    case '-':
    {
        resp._exitrest = resq._x - resq._y;
        break;
    }
    case '*':
    {
        resp._exitrest = resq._x * resq._y;
        break;
    }
    case '/':
    {
        if (resq._y == 0)
            resp._exitcode = DIV_ZERO;
        else
            resp._exitrest = resq._x / resq._y;
        break;
    }
    case '%':
    {
        if (resq._y == 0)
            resp._exitcode = MOD_ZERO;
        else
            resp._exitrest = resq._x % resq._y;
        break;
    }
    default:
        resp._exitcode = INPUT_ERROR;
        break;
    }
    return true;
}

// 输入的形式为 ./tcpServer 8080的格式
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(-1);
    }
    unique_ptr<calserver> tser(new calserver(atoi(argv[1]))); // 智能指针
    tser->initServer();                                       // 初始化操作
    tser->runServer(calfunc);                                      // 启动服务端

    return 0;
}

打印日志的代码:其中引入了可变参数列表:可变参数列表,参数的压栈顺序从右向左的,形参实例化也是从右向左的。需要找到可变参数列表的前一个参数,作为分隔符进行分割。

#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <cstdio>
#include <time.h>
#include <unistd.h>
#define DEBUG 0
#define NORMAL 1 // 正常
#define WARNING 2
#define ERROR 3 // 普通错误,可以正常运行
#define FATAL 4 // 严重错误,不能正常运行

char *tostring_level(const int level)
{
    switch (level)
    {
    case DEBUG:
        return (char *)"DEBUG";
    case NORMAL:
        return (char *)"NORMAL";
    case WARNING:
        return (char *)"WARNING";
    case ERROR:
        return (char *)"ERROR";
    case FATAL:
        return (char *)"FATAL";
    default:
        return nullptr;
    }
}
// 可变参数列表
// void debugLog(DEBUG,"hello %f %d %c",3.14,10,'C')
void debugLog(const int level, const char *format, ...) // 可变参数列表,参数的压栈顺序从右向左的,形参实例化也是从右向左的
{
#define NUM 1024
    char logprofix[NUM];  // 得到[日志等级][2023-7-23][pid]等信息
    char logcontent[NUM]; // 得到后面的可变参数列表信息
    snprintf(logprofix, sizeof(logprofix), "[%s]-[%ld]-[%d]", tostring_level(level), (long int)time(nullptr), getpid());
    va_list arg;     //首先声明一个可变参数列表
    va_start(arg, format);   //这里的va_start会以format为分割线,将...里面的参数分到arg中
    vsnprintf(logcontent, sizeof(logcontent), format, arg);
    std::cout<<logprofix<<logcontent<<std::endl;
}

可变参数列表

首先声明一个函数,这个省略号就是可变参数列表,需要访问这个参数,首先需要了解一下偷吻中包含的va_list和三个宏va_start,va_arg和va_end。

void debugLog(const int level, const char *format, ...) 

首先需要声明一个va_list的变量,声明方式如下

 va_list arg; 

紧接者初始化这个变量,通过调用va_start来初始化这个变量,他的第一次参数是va_list变量,第二个参数是省略号前面的最后一个有名字的有效参数,因为可变参数列表,参数的压栈顺序从右向左的,形参实例化也是从右向左的,通过最后一个有效参数就可以拿到可变参数的起始压栈位置,从而对参数进行初始化

 vsnprintf(logcontent, sizeof(logcontent), format, arg);

这里的vsnprintf()原型如下,就可以按照指定的格式将可变参数列表里面的参数写入到logcontent变量中。

 其中第三个参数是参数列表中参数的类型。

当然如果想要一个参数一个参数的进行访问,可以使用va_arg()函数,va_arg可以返回va_list型参数当前指向的参数的值,并让va_list型参数指向可变参数列表中的下一个参数。

va_arg(arg,format);

最后,在函数末尾,还要用到va_end来“结束”这个va_list型变量,参数就是这个va_list型变量的名字。调用方式:

va_end(arg);

守护进程

什么是守护进程,就是当我们的服务端运行在云服务器上的时候,关闭链接窗口,依然可以在外部继续访问的进程叫做守护进程。

像xshell链接远程服务器的时候,会形成会话,会话中可以有多个后台进程,但是只能有一个前台进程,最常见的前台进程就是bash,可以通过jobs查看当前的任务数量以及任务编号,可以通过ctrl+z的方式将一个前台进程变成后台进程(bg+任务编号,也可以在运行任务的时候&+运行的任务也是放到后台执行)在输入fg+任务编号之后,由后台进程变成前台进程。这里面还涉及到一个一个进程组的概念:进程组:一个或多个进程的集合,进程组由进程组ID标识,进程组长的进程ID和进程组ID一致,并且进程组ID不会由于进程组长的退出而受到影响。

如何让一个进程守护进程化

1.让调用进程忽略掉异常信号;2、让自己不是任务组的组长;3、守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件;4、可选:执行路径(cwd)是否发生变化。

// 可以用系统提供的守护进程函数
// 也可以自己写
// 调用函数时setsid()
#pragma once
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define DEV "/dev/null"
void daemonSelf(const char* curPath = nullptr)
{
    // 守护进程化的流程
    // 1.让调用进程忽略异常的信号
    signal(SIGPIPE, SIG_IGN);
    // 2.如何让自己不是组长,setsid
    if (fork() > 0)
        exit(0); // 将父进程退出,子进程跟父进程的进程地址,页表一样
    // 由子进程来当组长
    int n = setsid();
    assert(n!=-1);
    // 3. 守护进程是脱离终端的 关闭或者重定向以前进程默认打开的文件,把默认打开的012文件描述符重定向到 /dev/null
    int fp = open(DEV,O_RDWR);
    if(fp!=-1)
    {
        dup2(fp,0);
        dup2(fp,1);
        dup2(fp,2);
        close(fp);
    }
    else
    {
        close(0);
        close(1);
        close(2);
    }
    // 4.可选:进程执行路径发生改变,主要是进程自带属性cwd 当前工作路径
    // cwd 属性可以在/proc/pid 文件夹中查看
    if(curPath) chdir(curPath);
}

示例2:HTTP请求网站

对于http请求网站只需要构建服务器的代码即可。

Protocol.hpp协议的代码,主要是包括了协议的一些

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "Util.hpp"

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

class Request
{
public:
    Request() {}
    ~Request() {}
    void parse()
    {
        // 1. 从outbuffer里面获取第一行的,分割符是""\r\n"
        std::string line = Util::getOneLine(inbuffer, sep); // 静态方法可以这样调用
        if (line.empty())
            return;
        // 2.从请求字段种提取三个字段
        std::stringstream ss(line);
        ss >> method >> url >> httpversion; // 这里会自动以空格为分割符,流输入到这三个方法中
        // 2.1 a/b/c.py?name=zhangsan&pwd=12345
        // 通过问号可以把左右两部分分开
        // 如果是POST方法,本来就是分离的!
        // 左边是PATH,右边是参数parm

        // 3. 添加web默认路径
        path = default_root; // ./wwwroot
        path += url;         // ./wwwroot/a/b/c.html
        if (path[path.size() - 1] == '/')
            path += home_page;
        // 4. 获取path对应的资源后缀
        //  ./wwwroot/index.html
        auto pos = path.rfind(".");
        if (pos == std::string::npos)
            suffix = nullptr;
        else
            suffix = path.substr(pos); //.html   .jpg
        // 5. 获取文件的大小
        // 可以通过stat函数获取
        struct stat st_stat;
        stat(path.c_str(),&st_stat);
        size = st_stat.st_size;
    }

public:
    std::string inbuffer;

    std::string method;
    std::string url;
    std::string httpversion;
    std::string path;
    std::string suffix;
    int size;

    std::string parm;
};

class Response
{

public:
    std::string outbuffer; // 用来接受输出
};

Util.hpp

#pragma once

#include <iostream>
#include <string>
#include <fstream>

class Util
{
public:
    // XXXX XXX XXX\r\nYYYYY
    static std::string getOneLine(std::string &buffer, const std::string &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 path, char *body,int size)   //文本文件读取到body中
    {
        std::ifstream in(path);
        if(!in.is_open()) return false;   //统一没找到就是404
        
        in.read(body,size);
        //对于文本文件是可以的,但是音频是二进制文件
        // std::string line;
        // while(getline(in,line))
        // {
        //     *body+=line;
        // }
        in.close();
        return true;
    }


};

httpServer.cc

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

using namespace server;
void USAGE(std::string proc)
{

    cout << "USAGE\r\n"
         << proc << "Usage error\r\n"
         << endl;
}

std::string suffixtoDes(std::string suffix)
{
    // 通过url的后缀给与不同的content-type
    string respheader = "Content-Type: ";
    if (suffix == ".html")
        respheader += "text/html";
    else if (suffix == ".jpg")
        respheader += "application/x-jpg";
    respheader += "\r\n";
    return respheader;
}
// 1. 服务器和网通分离,html
// 2. url-》 / :
// 3. 需要正确的给客户端返回资源类型,首先需要自己知道!所有的资源都有后缀
bool Get(const Request &resq, Response &resp)
{
    // if(req.path == "test.py")
    // {
    //     //建立进程间通信,pipe
    //     //fork创建子进程,execl("/bin/python", test.py)
    //     // 父进程,将req.parm 通过管道写给某些后端语言,py,java,php等语言
    // }
    // if(req.path == "/search")
    // {
    //     // req.parm
    //     // 使用我们自己写的C++的方法,提供服务
    // }
    // for test
    cout << "-------------------------http start---------------------" << endl;
    cout << resq.inbuffer << endl;
    cout << "method: " << resq.method << endl;
    cout << "url: " << resq.url << endl;
    cout << "httpversion: " << resq.httpversion << endl;
    cout << "path: " << resq.path << endl;
    cout << "suffix: " << resq.suffix << endl;
    cout << "size: " << resq.size << endl;
    cout << "-------------------------http end---------------------" << endl;

    string respline = "HTTP/1.1  200 OK\r\n";
    string respheader = suffixtoDes(resq.suffix); //"Content-Type: text/html\r\n";,这里需要通过不同的后缀名选择不同的content-type的类型
    // 把长度也加上
    if (resq.size >= 0)
    {
        // respheader += "Content-Length: ";
        // respheader += std::to_string(resq.size);
        // respheader += "\r\n";
    }
    respheader += "Set-Cookie: name=12345678abcdg;Max-Age=120\r\n";
    // 往后每次http请求,都会自动携带曾经设置的所有cookie,帮服务器进行鉴权行为 ---http会话保持

    string respblank = "\r\n";
    // std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>王应登,我宣你啊,baby</p></body></html>";
    // 通过读取的方式的方式来获取网页

    string body;
    body.resize(resq.size + 1);
    if (!Util::ReadFile(resq.path, (char *)body.c_str(), resq.size)) // 如果读不到,就是资源不存在
    {
        const std::string html_404 = "./wwwroot/404.html";
        Util::ReadFile(html_404, (char *)body.c_str(), resq.size);
    }
    respheader += "Content-Length: ";
    respheader += std::to_string(body.size());
    respheader += "\r\n";
    resp.outbuffer += respline;
    resp.outbuffer += respheader;
    resp.outbuffer += respblank;
    cout << "-------------------------http response start---------------------" << endl;
    cout << resp.outbuffer << endl;
    cout << "-------------------------http response end---------------------" << endl;

    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->registerCb("/", Get); // 功能路由!
    // httpsvr->registerCb("/search", Search);
    // httpsrv->registerCb("/test.py", Other);
    httpsvr->initServer();
    httpsvr->runServer();

    return 0;
}

httpServer.hpp

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

using namespace server;
void USAGE(std::string proc)
{

    cout << "USAGE\r\n"
         << proc << "Usage error\r\n"
         << endl;
}

std::string suffixtoDes(std::string suffix)
{
    // 通过url的后缀给与不同的content-type
    string respheader = "Content-Type: ";
    if (suffix == ".html")
        respheader += "text/html";
    else if (suffix == ".jpg")
        respheader += "application/x-jpg";
    respheader += "\r\n";
    return respheader;
}
// 1. 服务器和网通分离,html
// 2. url-》 / :
// 3. 需要正确的给客户端返回资源类型,首先需要自己知道!所有的资源都有后缀
bool Get(const Request &resq, Response &resp)
{
    // if(req.path == "test.py")
    // {
    //     //建立进程间通信,pipe
    //     //fork创建子进程,execl("/bin/python", test.py)
    //     // 父进程,将req.parm 通过管道写给某些后端语言,py,java,php等语言
    // }
    // if(req.path == "/search")
    // {
    //     // req.parm
    //     // 使用我们自己写的C++的方法,提供服务
    // }
    // for test
    cout << "-------------------------http start---------------------" << endl;
    cout << resq.inbuffer << endl;
    cout << "method: " << resq.method << endl;
    cout << "url: " << resq.url << endl;
    cout << "httpversion: " << resq.httpversion << endl;
    cout << "path: " << resq.path << endl;
    cout << "suffix: " << resq.suffix << endl;
    cout << "size: " << resq.size << endl;
    cout << "-------------------------http end---------------------" << endl;

    string respline = "HTTP/1.1  200 OK\r\n";
    string respheader = suffixtoDes(resq.suffix); //"Content-Type: text/html\r\n";,这里需要通过不同的后缀名选择不同的content-type的类型
    // 把长度也加上
    if (resq.size >= 0)
    {
        // respheader += "Content-Length: ";
        // respheader += std::to_string(resq.size);
        // respheader += "\r\n";
    }
    respheader += "Set-Cookie: name=12345678abcdg;Max-Age=120\r\n";
    // 往后每次http请求,都会自动携带曾经设置的所有cookie,帮服务器进行鉴权行为 ---http会话保持

    string respblank = "\r\n";
    // std::string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>王应登,我宣你啊,baby</p></body></html>";
    // 通过读取的方式的方式来获取网页

    string body;
    body.resize(resq.size + 1);
    if (!Util::ReadFile(resq.path, (char *)body.c_str(), resq.size)) // 如果读不到,就是资源不存在
    {
        const std::string html_404 = "./wwwroot/404.html";
        Util::ReadFile(html_404, (char *)body.c_str(), resq.size);
    }
    respheader += "Content-Length: ";
    respheader += std::to_string(body.size());
    respheader += "\r\n";
    resp.outbuffer += respline;
    resp.outbuffer += respheader;
    resp.outbuffer += respblank;
    cout << "-------------------------http response start---------------------" << endl;
    cout << resp.outbuffer << endl;
    cout << "-------------------------http response end---------------------" << endl;

    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->registerCb("/", Get); // 功能路由!
    // httpsvr->registerCb("/search", Search);
    // httpsrv->registerCb("/test.py", Other);
    httpsvr->initServer();
    httpsvr->runServer();

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值