• Linux知识点 -- 网络基础(二)-- 应用层


    Linux知识点 – 网络基础 – 应用层


    在这里插入图片描述

    一、使用协议来实现一个网络版的计算器

    1.自定义协议

    定义结构体来表示我们需要交互的信息;
    发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
    这个过程叫做"序列化"和"反序列化”;

    在这里插入图片描述

    Sock.hpp
    将套接字封装成对象,其中包含套接字的创建与连接成员函数

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "Log.hpp"
    
    class Sock
    {
    private:
        const static int gbacklog = 20;
    
    public:
        Sock() {}
    
        int Socket()
        {
            int listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (listensock < 0)
            {
                logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
                exit(2);
            }
            logMessage(NORMAL, "create socket success, listensock: %d", listensock);
            return listensock;
        }
    
        void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof local);
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
            if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
            {
                logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
                exit(3);
            }
        }
    
        void Listen(int sock)
        {
            if (listen(sock, gbacklog) < 0)
            {
                logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
                exit(4);
            }
            logMessage(NORMAL, "init server success");
        }
    
        // 一般经验:
        // const string& 输入型参数
        // string* 输出型参数
        // string& 输入输出型参数
    
        int Accept(int listensock, std::string *ip, uint16_t *port)
        {
            struct sockaddr_in src;
            socklen_t len = sizeof src;
            int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                return -1;
            }
            if(port)
            {
                *port = ntohs(src.sin_port);
            }
            if(ip)
            {
                *ip = inet_ntoa(src.sin_addr);
                return servicesock;
            }
        }
    
        bool Connect(int sock, const std::string& server_ip, const uint16_t& server_port)
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof server);
            server.sin_family = AF_INET;
            server.sin_port = htons(server_port);
            server.sin_addr.s_addr = inet_addr(server_ip.c_str());
            if(connect(sock, (struct sockaddr*)&server, sizeof server) == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    
        ~Sock() {}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    TcpServer.hpp
    封装TCP服务接口的类;
    注意:类内回调函数由于参数有this指针,无法正-常回调,因此需要设置成static成员,再通过参数传进this指针,来访问类内非静态成员;

    #pragma once
    
    #include "Sock.hpp"
    #include 
    #include 
    #include 
    
    namespace ns_tcpserver
    {
        using func_t = std::function<void(int)>;
        class TcpServer;
        class ThreadData // 传入回调函数的参数
        {
        public:
            ThreadData(int sock, TcpServer *server)
                : _sock(sock), _server(server)
            {
            }
            ~ThreadData() {}
    
        public:
            int _sock;
            TcpServer *_server; // 里面有TcpServer对象的指针,由于回调函数是静态成员函数,无法访问非静态成员
                                // 这里的TcpServer对象指针是用来在回调函数中访问非静态成员的
        };
    
        class TcpServer
        {
        private:
            //如果是类内成员函数,参数中是有this指针的,多线程回调会出问题
            //因此需设置成静态成员,才可以回调
            static void* ThreadRoutine(void* args)
            {
                pthread_detach(pthread_self());//线程分离
                ThreadData* td = static_cast<ThreadData*>(args);//类型转换
                td->_server->Excute(td->_sock);//通过对象this指针调用成员函数
                close(td->_sock);
                return nullptr;
            }
        public:
            TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
            {
                // 创建套接字,绑定并监听
                _listensock = _sock.Socket();
                _sock.Bind(_listensock, port, ip);
                _sock.Listen(_listensock);
            }
    
            // 将服务请求放入函数队列
            void BindService(func_t func)
            {
                _func.push_back(func);
            }
    
            // 执行服务
            void Excute(int sock)
            {
                for (auto &f : _func)
                {
                    f(sock);
                }
            }
    
            void Start()
            {
                for (;;)
                {
                    std::string cli_ip;
                    uint16_t cli_port;
                    int sock = _sock.Accept(_listensock, &cli_ip, &cli_port);
                    if (sock == -1)
                    {
                        continue;
                    }
                    logMessage(NORMAL, "create new link succsee, sock: %d", sock);
    
                    // 多线程处理请求
                    pthread_t tid;
                    ThreadData *td = new ThreadData(sock, this);
                    pthread_create(&tid, nullptr, ThreadRoutine, td);
                }
            }
    
            ~TcpServer()
            {
                if (_listensock >= 0)
                {
                    close(_listensock);
                }
            }
    
        private:
            int _listensock;
            Sock _sock;
            std::vector<func_t> _func; // 回调函数列表
        };
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    Protocol.hpp
    定制协议:
    分别有计算请求的序列化和计算结果的序列化;

    • TCP协议的读写接口(read和write)都是将数据拷贝到缓冲区或者从缓冲区拷贝出来,并不是直接发送到对方主机;发送给对方主机是由TCP传输控制协议决定的
    • 由于TCP是面向字节流的协议,因此,发送和接受的次数,每次发送多少字符,都不受控制(UDP协议每次发送和接受的都是完整的报文),有可能每次接收到的不一定是完整的报文,也有可能一次读取了多个报文,所以需要自己定制协议解包代码;在读取时不能简单地receive,而需要对读取的数据进行解析;
    • 自主定制的协议使用"length\r\nx_ op_ y_\r\n"协议,前面加上数据长度;
      在这里插入图片描述
    #pragma once
    #include 
    #include 
    #include 
    #include "Sock.hpp"
    
    namespace ns_protocol
    {
    #define MYSELF 1
    #define SPACE " "
    #define SPACE_LEN strlen(SPACE)
    
    #define SEP "\r\n"
    #define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0
    
        class Request // 计算请求序列
        {
        public:
            Request() {}
            Request(int x, int y, char op)
                : _x(x), _y(y), _op(op)
            {
            }
    
            ~Request() {}
    
            std::string Serialize() // 序列化
            {
    #ifdef MYSELF
                // 使用自定义序列化方案
                // 将请求传换成string:_x _op _y的形式
                std::string str;
                str = std::to_string(_x);
                str += SPACE;
                str += _op;
                str += SPACE;
                str += std::to_string(_y);
                return str;
    #else
                // 使用现成方案
                std::cout << "to do" << std::endl;
    #endif
            }
    
            bool Deserialized(const std::string &str) // 反序列化
            {
    #ifdef MYSELF
                std::size_t left = str.find(SPACE);
                if (left == std::string::npos)
                {
                    return false;
                }
                std::size_t right = str.rfind(SPACE);
                if (right == std::string::npos)
                {
                    return false;
                }
                _x = atoi(str.substr(0, left).c_str());
                _y = atoi(str.substr(right + SPACE_LEN).c_str());
                if (left + SPACE_LEN > str.size())
                {
                    return false;
                }
                else
                {
                    _op = str[left + SPACE_LEN];
                }
                return true;
    
    #else
                std::cout << "to do" << std::endl;
    #endif
            }
    
        public:
            int _x;
            int _y;
            char _op; // + - * / %
        };
    
        class Response // 计算结果响应序列
        {
        public:
            Response() {}
    
            Response(int result, int code)
                : _result(result), _code(code)
            {
            }
    
            ~Response() {}
    
            std::string Serialize() // 序列化:_code _result
            {
    #ifdef MYSELF
                // 使用自定义序列化方案
                // 将请求传换成string:_x _op _y的形式
                std::string str;
                str = std::to_string(_code);
                str += SPACE;
                str += std::to_string(_result);
                return str;
    #else
                // 使用现成方案
                std::cout << "to do" << std::endl;
    #endif
            }
    
            bool Deserialized(const std::string &str) // 反序列化
            {
    #ifdef MYSELF
                std::size_t pos = str.find(SPACE);
                if (pos == std::string::npos)
                {
                    return false;
                }
    
                _code = atoi(str.substr(0, pos).c_str());
                _code = atoi(str.substr(pos + SPACE_LEN).c_str());
                return true;
    
    #else
                std::cout << "to do" << std::endl;
    #endif
            }
    
        public:
            int _result; // 计算结果
            int _code;   // 计算结果的状态码:运算是否成功
        };
    
        // 临时方案
        // 期望返回的是一个完整地报文
        bool Recv(int sock, std::string* out)
        {
            //TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求
            //因此需要解析协议,查看数据是否完整
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof buffer - 1, 0);
            if (s > 0)
            {
                buffer[s] = 0;
                *out += buffer;
            }
            else if(s == 0)
            {
                //客户端退出
                return false;
            }
            else
            {
                //读取错误
                return false;
            }
            return true;
        }
    
        void Send(int sock, const std::string str)
        {
            send(sock, str.c_str(), str.size(), 0);
        }
    
        //添加报文
        // "XXXXXX"
        // "123\r\nXXXXXX\r\n"
        std::string Encode(std::string &s)
        {
            std::string new_package = std::to_string(s.size());
            new_package += SEP;
            new_package += s;
            new_package += SEP;
            return new_package;
        }
    
    
        //解析报文
        //规定报文的格式为:"length\r\nx_ op_ y_\r\n..."
        std::string Decode(std::string& buffer)
        {
            std::size_t pos = buffer.find(SEP);
            if(pos == std::string::npos)
            {
                return "";//如果没找到分隔符,返回空串
            }
            int size = atoi(buffer.substr(0, pos).c_str());
            int surplus = buffer.size() - pos - 2*SEP_LEN;
            if(surplus >= size)
            {
                //至少有一份合法的报文,可以手动提取了
                buffer.erase(0, pos + SEP_LEN);
                std::string s = buffer.substr(0, size);
                buffer.erase(0, size + SEP_LEN);
                return s;
            }
            else
            {
                return "";//没有完整地报文,继续接收
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201

    CalServer.cc
    计算服务

    • 服务器运行时,对端如果直接关闭,我们收到的就是空的信息,send的也是已经关闭的文件描述符,就可能导致服务器关闭;
      方案一:对SIGPIPE信号忽略,这样即使正在发送信息时对方关闭,也可以保证服务器不退出;

      在这里插入图片描述
      方案二:接收到信息时,需要判断信息的完整性,读取是否成功
    • 一般经验:在server编写的时候,要有较为严谨的判断逻辑;
      一般服务器都是要忽略SIGPIPE信号的,防止在运行过程中出现非法写入的问题;
    #include "TcpServer.hpp"
    #include "Protocol.hpp"
    #include 
    #include 
    
    using namespace ns_protocol;
    using namespace ns_tcpserver;
    
    static void Usage(const std::string &process)
    {
        std::cout << "\nUsage: " << process << " port\n"
                  << std::endl;
    }
    
    // 进行计算
    static Response calculatorHelper(const Request &req)
    {
        Response resp(0, 0);
        switch (req._op)
        {
        case '+':
            resp._result = req._x + req._y;
            break;
        case '-':
            resp._result = req._x - req._y;
            break;
        case '*':
            resp._result = req._x * req._y;
            break;
        case '/':
            if (0 == req._y)
                resp._code = 1;
            else
                resp._result = req._x / req._y;
            break;
        case '%':
            if (0 == req._y)
                resp._code = 2;
            else
                resp._result = req._x % req._y;
            break;
        default:
            resp._code = 3;
            break;
        }
        return resp;
    }
    
    void calculator(int sock)
    {
        std::string inbuffer;//每次读取到的缓冲区
        while (true)
        {
            //1.读取成功
            bool res = Recv(sock, &inbuffer); // 读到了一个请求
            if(!res)
            {
                break;
            }
            //2.协议解析,保证得到一个完整的报文
            std::string package = Decode(inbuffer);
            if(package.empty())
            {
                continue; //如果读到的报文不完整,继续读取
            }
            logMessage(NORMAL, "%s", package.c_str());
            //3.保证该报文是一个完整的报文
            Request req;
            //4.反序列化,字节流->结构化
            req.Deserialized(package); // 反序列化
            //5.业务逻辑
            Response resp = calculatorHelper(req);
            //6.序列化
            std::string respString = resp.Serialize();//对计算结果进行序列化
            //7.添加长度信息,形成一个完整的报文
            respString = Encode(respString);
            //8.发送
            Send(sock, respString);//将结果序列发回给客户端
        }
    }
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
            exit(1);
        }
    
        signal(SIGPIPE, SIG_IGN);
    
        std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
        server->BindService(calculator);
        server->Start();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    CalClient.cc
    客户端

    #include 
    #include "Sock.hpp"
    #include "Protocol.hpp"
    
    using namespace ns_protocol;
    static void Usage(const std::string &process)
    {
        std::cout << "\nUsage: " << process << " serverIp serverPort\n"
                  << std::endl;
    }
    // ./client server_ip server_port
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            Usage(argv[0]);
            exit(1);
        }
        std::string server_ip = argv[1];
        uint16_t server_port = atoi(argv[2]);
        Sock sock;
        int sockfd = sock.Socket();
        if (!sock.Connect(sockfd, server_ip, server_port))
        {
            std::cerr << "Connect error" << std::endl;
            exit(2);
        }
        bool quit = false;
        std::string buffer;
        while (!quit)
        {
            // 1. 获取需求
            Request req;
            std::cout << "Please Enter # ";
            std::cin >> req._x >> req._op >> req._y;
            // 2. 序列化
            std::string s = req.Serialize();
            // std::string temp = s;
            // 3. 添加长度报头
            s = Encode(s);
            // 4. 发送给服务端
            Send(sockfd, s);
            // 5. 正常读取
            while (true)
            {
                bool res = Recv(sockfd, &buffer);
                if (!res)
                {
                    quit = true;
                    break;
                }
                std::string package = Decode(buffer);
                if (package.empty())
                    continue;
                Response resp;
                resp.Deserialized(package);
                std::string err;
                switch (resp._code)
                {
                case 1:
                    err = "除0错误";
                    break;
                case 2:
                    err = "模0错误";
                    break;
                case 3:
                    err = "非法操作";
                    break;
                default:
                    std::cout << resp._result << " [success]" << std::endl;
                    break;
                }
                if(!err.empty()) std::cerr << err << std::endl;
                // sleep(1);
                break;//完整读取一个报文就退出
            }
        }
        close(sockfd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    运行结果:
    在这里插入图片描述

    2.守护进程

    • (1)前台进程:和终端关联的进程;在终端下能读取输入并作出反应(如bash);
      (2)任何xshell登陆,只允许一个前台进程和多个后台进程;
      (3)进程除了有自己的pid, ppid, 还有一个组ID;
      (4)在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash ->可以用匿名管道来进行通信;
      (5)而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程
      (6)任何一次登陆,登陆的用户,需要有多个进程(组),来给这个用户提供服务的(bash),用户自己可以启动很多进程,或者进程组。我们把给用户提供服务的进程,或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。
      (7)当用户退出登陆的时候,整个会话中的进程组都会结束;
      想让一个进程不再属于用户的会话,而是自成一个会话,这个进程称为守护进程
      (8)如何将进程变为守护进程->setsid()接口;
      (9)setsid要成功被调用,必须保证当前进程不是进程组的组长,可以通过fork创建的子进程实现;
      (10)守护进程不能直接向显示器打印消息,一旦打印,会被暂停,终止;

    • 如何在Linux正确的写一个让进程守护进程化的代码:
      写一个函数,让进程调用这个函数,自动变成守护进程;

    • /dev/null文件
      可以理解为一个文件黑洞,可以向里面打印数据,也可以从里面读取,但都不会有实际的数据输入输出;
      因此可以将标准输入,标准输出,标准错误重定向到devnull文件中;

    Daemon.hpp

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void MyDaemon()
    {
        //1.忽略信号,SIPPIPE, SIGCHID
        signal(SIGPIPE, SIG_IGN);
        signal(SIGCHLD, SIG_IGN);
        //2.不要让自己成为组长
        if(fork() > 0)
        {
            exit(0);//父进程退出,剩下子进程其实是孤儿进程
        }
        //3.调用setsid
        setsid();
        //4.标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
        int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
        if(devnull > 0)
        {
            dup2(0, devnull);
            dup2(1, devnull);
            dup2(2, devnull);
            close(devnull);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    CalServer.cc
    在服务器进程中调用守护进程函数,让服务器进程成为守护进程;

    #include "TcpServer.hpp"
    #include "Protocol.hpp"
    #include 
    #include 
    #include "Daemon.hpp"
    
    using namespace ns_protocol;
    using namespace ns_tcpserver;
    
    static void Usage(const std::string &process)
    {
        std::cout << "\nUsage: " << process << " port\n"
                  << std::endl;
    }
    
    // 进行计算
    static Response calculatorHelper(const Request &req)
    {
        Response resp(0, 0);
        switch (req._op)
        {
        case '+':
            resp._result = req._x + req._y;
            break;
        case '-':
            resp._result = req._x - req._y;
            break;
        case '*':
            resp._result = req._x * req._y;
            break;
        case '/':
            if (0 == req._y)
                resp._code = 1;
            else
                resp._result = req._x / req._y;
            break;
        case '%':
            if (0 == req._y)
                resp._code = 2;
            else
                resp._result = req._x % req._y;
            break;
        default:
            resp._code = 3;
            break;
        }
        return resp;
    }
    
    void calculator(int sock)
    {
        std::string inbuffer;//每次读取到的缓冲区
        while (true)
        {
            //1.读取成功
            bool res = Recv(sock, &inbuffer); // 读到了一个请求
            if(!res)
            {
                break;
            }
            //2.协议解析,保证得到一个完整的报文
            std::string package = Decode(inbuffer);
            if(package.empty())
            {
                continue; //如果读到的报文不完整,继续读取
            }
            logMessage(NORMAL, "%s", package.c_str());
            //3.保证该报文是一个完整的报文
            Request req;
            //4.反序列化,字节流->结构化
            req.Deserialized(package); // 反序列化
            //5.业务逻辑
            Response resp = calculatorHelper(req);
            //6.序列化
            std::string respString = resp.Serialize();//对计算结果进行序列化
            //7.添加长度信息,形成一个完整的报文
            respString = Encode(respString);
            //8.发送
            Send(sock, respString);//将结果序列发回给客户端
        }
    }
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
            exit(1);
        }
    
        signal(SIGPIPE, SIG_IGN);
        MyDaemon();//让该进程成为守护进程,自成一个会话
    
        std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
        server->BindService(calculator);
        server->Start();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    运行结果:
    在这里插入图片描述
    注:
    在这里插入图片描述
    守护进程实际上是孤儿进程,但是没有被系统领养,而是自成会话

    这样下来,服务器进程成为了守护进程,自成一个会话,即使用户退出登录,该进程也不会退出;

    3.使用json来完成序列化

    json:网络通信的格式

    • 在Linux上安装json:
      在这里插入图片描述
    • json实际上是一个结构化数据格式,里面是很多的kv结构:
      在这里插入图片描述
    • json库的使用:
      在这里插入图片描述
      StyleWriter对象,两个kv对象之间有换行符;
      StyleWriter对象的write函数会将root中的kv内容直接转换为对应的string;

      运行结果:
      在这里插入图片描述
      在这里插入图片描述
      FastWriter对象,中间没有换行符
      运行结果:
      在这里插入图片描述
      json里面是可以套json的
      在这里插入图片描述

    使用json协议完成序列化和反序列化:
    由于使用的是非cpp官方库,因此需要添加编译选项:
    makefile

    .PHONY:all
    all:CalClient CalServer
    
    CalClient:CalClient.cc
    	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
    CalServer:CalServer.cc
    	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
    
    .PHONY:clean
    clean:
    	rm -f CalClient CalServer
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Protocol.hpp

    #pragma once
    #include 
    #include 
    #include 
    #include "Sock.hpp"
    #include 
    
    namespace ns_protocol
    {
    //#define MYSELF 1
    #define SPACE " "
    #define SPACE_LEN strlen(SPACE)
    
    #define SEP "\r\n"
    #define SEP_LEN strlen(SEP) // 不能是sizeof,会统计\0
    
        class Request // 计算请求序列
        {
        public:
            Request() {}
            Request(int x, int y, char op)
                : _x(x), _y(y), _op(op)
            {
            }
    
            ~Request() {}
    
            std::string Serialize() // 序列化
            {
    #ifdef MYSELF
                // 使用自定义序列化方案
                // 将请求传换成string:_x _op _y的形式
                std::string str;
                str = std::to_string(_x);
                str += SPACE;
                str += _op;
                str += SPACE;
                str += std::to_string(_y);
                return str;
    #else
                // 使用现成方案
                Json::Value root;
                root["x"] = _x;
                root["y"] = _y;
                root["op"] = _op;
                Json::FastWriter writer;
                return writer.write(root);  
    #endif
            }
    
            bool Deserialized(const std::string &str) // 反序列化
            {
    #ifdef MYSELF
                std::size_t left = str.find(SPACE);
                if (left == std::string::npos)
                {
                    return false;
                }
                std::size_t right = str.rfind(SPACE);
                if (right == std::string::npos)
                {
                    return false;
                }
                _x = atoi(str.substr(0, left).c_str());
                _y = atoi(str.substr(right + SPACE_LEN).c_str());
                if (left + SPACE_LEN > str.size())
                {
                    return false;
                }
                else
                {
                    _op = str[left + SPACE_LEN];
                }
                return true;
    
    #else
                Json::Value root;
                Json::Reader reader;
                reader.parse(str, root);//parse函数能够将序列化的json字符串直接读取到Value对象中
                _x = root["x"].asInt();
                _x = root["y"].asInt();
                _x = root["op"].asInt();//char类型实质也是int
                return true;
    #endif
            }
    
        public:
            int _x;
            int _y;
            char _op; // + - * / %
        };
    
        class Response // 计算结果响应序列
        {
        public:
            Response() {}
    
            Response(int result, int code)
                : _result(result), _code(code)
            {
            }
    
            ~Response() {}
    
            std::string Serialize() // 序列化:_code _result
            {
    #ifdef MYSELF
                // 使用自定义序列化方案
                // 将请求传换成string:_x _op _y的形式
                std::string str;
                str = std::to_string(_code);
                str += SPACE;
                str += std::to_string(_result);
                return str;
    #else
                // 使用现成方案
                Json::Value root;
                root["code"] = _code;
                root["result"] = _result;
                Json::FastWriter writer;
                return writer.write(root);
    #endif
            }
    
            bool Deserialized(const std::string &str) // 反序列化
            {
    #ifdef MYSELF
                std::size_t pos = str.find(SPACE);
                if (pos == std::string::npos)
                {
                    return false;
                }
    
                _code = atoi(str.substr(0, pos).c_str());
                _result = atoi(str.substr(pos + SPACE_LEN).c_str());
                return true;
    
    #else
                Json::Value root;
                Json::Reader reader;
                reader.parse(str, root);
                _code = root["code"].asInt();
                _result = root["result"].asInt();
                return true;
    #endif
            }
    
        public:
            int _result; // 计算结果
            int _code;   // 计算结果的状态码:运算是否成功
        };
    
        // 临时方案
        // 期望返回的是一个完整地报文
        bool Recv(int sock, std::string* out)
        {
            //TCP是面向字节流的,无法保证独到的inbuffer是一个完整地请求
            //因此需要解析协议,查看数据是否完整
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            if (s > 0)
            {
                buffer[s] = 0;
                *out += buffer;
            }
            else if(s == 0)
            {
                //客户端退出
                return false;
            }
            else
            {
                //读取错误
                return false;
            }
            return true;
        }
    
        void Send(int sock, const std::string str)
        {
            int n = send(sock, str.c_str(), str.size(), 0);
            if(n < 0)
            {
                std::cout << "send error" << std::endl;
            }
        }
    
        //添加报头
        // "XXXXXX"
        // "123\r\nXXXXXX\r\n"
        std::string Encode(std::string &s)
        {
            std::string new_package = std::to_string(s.size());
            new_package += SEP;
            new_package += s;
            new_package += SEP;
            return new_package;
        }
    
    
        //解析报文
        //规定报文的格式为:"length\r\nx_ op_ y_\r\n..."
        std::string Decode(std::string& buffer)
        {
            std::size_t pos = buffer.find(SEP);
            if(pos == std::string::npos)
            {
                return "";//如果没找到分隔符,返回空串
            }
            int size = atoi(buffer.substr(0, pos).c_str());
            int surplus = buffer.size() - pos - 2*SEP_LEN;
            if(surplus >= size)
            {
                //至少有一份合法的报文,可以手动提取了
                buffer.erase(0, pos + SEP_LEN);
                std::string s = buffer.substr(0, size);
                buffer.erase(0, size + SEP_LEN);
                return s;
            }
            else
            {
                return "";//没有完整地报文,继续接收
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226

    运行结果:
    在这里插入图片描述

    二、HTTP协议

    1.概念

    • 应用层:就是程序员基于socket接口之上编写的具体逻辑,有很多和文本处理相关的工作;http协议一定会有大量的文本分析和处理;

    • URL:我们平时说的网址,其结构如下;
      在这里插入图片描述
      其中,服务器地址IP就是域名,用来标识唯一的主机;冒号后面是端口号,标识特定主机上的特定进程;
      端口号后面是带层次的文件路径,其中第一个文件夹叫做web根目录;文件路径标识客户想访问的资源路径;
      URL:union resource local统一资源定位符,代表本次访问请求的资源位置,定位互联网中唯一的一份资源;
      在用户访问网络资源时,先通过url找到服务器上的特定文件资源,在进行读取或写入;

    • 如果用户想在url中包含url本身作为特殊字符使用的字符时,浏览器会自动对该字符进行编码,在服务端收到后,需要转回特殊字符;
      在这里插入图片描述在这里插入图片描述

    2.HTTP协议请求和响应的报文格式

    在这里插入图片描述
    单纯在报文角度,http可以是基于行的文本协议;

    • 请求报文:
      请求行:方法 URL 协议版本
      http的方法为:
      在这里插入图片描述
      请求报头Header:多行kv结构,都是属性;
      空行:用来区分报头和有效载荷;
      请求正文(可以没有);

    • 响应报文:
      状态行:协议版本 状态码 状态码描述;
      响应报头;
      空行;
      响应正文;

    3.使用HTTP协议进行网络通信

    Log.hpp

    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // 日志是有日志级别的
    #define DEBUG   0
    #define NORMAL  1
    #define WARNING 2
    #define ERROR   3
    #define FATAL   4
    
    const char *gLevelMap[] = {
        "DEBUG",
        "NORMAL",
        "WARNING",
        "ERROR",
        "FATAL"
    };
    
    #define LOGFILE "./http.log"
    
    // 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
    void logMessage(int level, const char *format, ...)
    {
    #ifndef DEBUG_SHOW
        if(level== DEBUG) return;
    #endif
        // va_list ap;
        // va_start(ap, format);
        // while()
        // int x = va_arg(ap, int);
        // va_end(ap); //ap=nullptr
        char stdBuffer[1024]; //标准部分
        time_t timestamp = time(nullptr);
        // struct tm *localtime = localtime(×tamp);
        snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
    
        char logBuffer[1024]; //自定义部分
        va_list args;
        va_start(args, format);
        // vprintf(format, args);
        vsnprintf(logBuffer, sizeof logBuffer, format, args);
        va_end(args);
    
        FILE *fp = fopen(LOGFILE, "a");
        // printf("%s%s\n", stdBuffer, logBuffer);
        fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
        fclose(fp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    Sock.hpp

    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "Log.hpp"
    
    class Sock
    {
    private:
        const static int gbacklog = 20;
    
    public:
        Sock() {}
        int Socket()
        {
            int listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (listensock < 0)
            {
                logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
                exit(2);
            }
            logMessage(NORMAL, "create socket success, listensock: %d", listensock);
            return listensock;
        }
        void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof local);
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
            if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
                exit(3);
            }
        }
        void Listen(int sock)
        {
            if (listen(sock, gbacklog) < 0)
            {
                logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
                exit(4);
            }
    
            logMessage(NORMAL, "init server success");
        }
        // 一般经验
        // const std::string &: 输入型参数
        // std::string *: 输出型参数
        // std::string &: 输入输出型参数
        int Accept(int listensock, std::string *ip, uint16_t *port)
        {
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                return -1;
            }
            if(port) *port = ntohs(src.sin_port);
            if(ip) *ip = inet_ntoa(src.sin_addr);
            return servicesock;
        }
        bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(server_port);
            server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    
            if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
            else return false;
        }
        ~Sock() {}
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    Usage.hpp

    #pragma once
    #include 
    #include 
    void Usage(std::string proc)
    {
        std::cout << "\nUsage: " << proc <<  " port\n" << std::endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Util.hpp
    工具类,分割字符串

    #pragma once
    
    #include 
    #include 
    
    class Util
    {
    public:
        // aaaa\r\nbbbbb\r\nccc\r\n\r\n
        static void cutString(std::string s, const std::string &sep, std::vector<std::string> *out)
        {
            std::size_t start = 0;
            while (start < s.size())
            {
                auto pos = s.find(sep, start);
                if (pos == std::string::npos) break;
                std::string sub = s.substr(start, pos - start);
                // std::cout << "----" << sub << std::endl;
                out->push_back(sub);
                start += sub.size();
                start += sep.size();
            }
            if(start < s.size()) out->push_back(s.substr(start));
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    HttpServer.hpp

    #pragma once
    
    #include 
    #include 
    #include 
    #include "Sock.hpp"
    
    class HttpServer
    {
    public:
        using func_t = std::function<void(int)>;
    private:
        int listensock_;
        uint16_t port_;
        Sock sock;
        func_t func_;
    public:
        HttpServer(const uint16_t &port, func_t func): port_(port),func_(func)
        {
            listensock_ = sock.Socket();
            sock.Bind(listensock_, port_);
            sock.Listen(listensock_);
        }
        void Start()
        {
            signal(SIGCHLD, SIG_IGN);
            for( ; ; )
            {
                std::string clientIp;
                uint16_t clientPort = 0;
                int sockfd = sock.Accept(listensock_, &clientIp, &clientPort);
                if(sockfd < 0) continue;
                if(fork() == 0)
                {
                    close(listensock_);
                    func_(sockfd);
                    close(sockfd);
                    exit(0);
                }
                close(sockfd);
            }
        }
        ~HttpServer()
        {
            if(listensock_ >= 0) close(listensock_);
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    HttpServer.cc
    这里是主要的对http协议进行解析的代码,逐行解析,提取首行url,访问目标资源;

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "HttpServer.hpp"
    #include "Usage.hpp"
    #include "Util.hpp"
    // 一般http都要有自己的web根目录
    #define ROOT "./wwwroot" // ./wwwroot/index.html
    // 如果客户端只请求了一个/,我们返回默认首页
    #define HOMEPAGE "index.html"
    void HandlerHttpRequest(int sockfd)
    {
        // 1. 读取请求 for test
        char buffer[10240];
        ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            // std::cout << buffer << "--------------------\n" << std::endl;
        }
        std::vector<std::string> vline; // 取出http请求的每一行
        Util::cutString(buffer, "\n", &vline);
        std::vector<std::string> vblock; // 取出第一行的每一个子串
        Util::cutString(vline[0], " ", &vblock);
        std::string file = vblock[1]; // 请求的资源
        std::string target = ROOT;
        if(file == "/") file = "/index.html";
        target += file; //请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录
        std::cout << target << std::endl;
        std::string content;
        std::ifstream in(target); // 打开文件
        if(in.is_open())
        {
            std::string line;
            while(std::getline(in, line))
            {
                content += line;
            }
            in.close();
        }
        std::string HttpResponse;
        if(content.empty()) HttpResponse = "HTTP/1.1 404 NotFound\r\n";
        else HttpResponse = "HTTP/1.1 200 OK\r\n";
        HttpResponse += "\r\n";
        HttpResponse += content;
            // std::cout << "####start################" << std::endl;
            // for(auto &iter : vblock)
            // {
            //     std::cout << "---" << iter << "\n" << std::endl;
            // }
            // std::cout << "#####end###############" << std::endl;
            // 2. 试着构建一个http的响应
        send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
    }
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
            exit(0);
        }
        std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
        httpserver->Start();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    在目录下创建web根目录wwwroot,里面创建首页index.html;
    在这里插入图片描述
    index.html
    在vscode下装插件,!table会出现网页模板;

    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>lmxtitle>
    head>
    <body>
        <h3>这个一个Linux课程h3>
        <p>我是一个Linux的学习者,我正在进行http的测试工作!!p>
        <p>我是一个Linux的学习者,我正在进行http的测试工作!!p>
        <p>我是一个Linux的学习者,我正在进行http的测试工作!!p>
        <p>我是一个Linux的学习者,我正在进行http的测试工作!!p>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果:
    在这里插入图片描述

    4.HTTP协议的方法

    在这里插入图片描述
    其中最常用的是GET和POST方法;

    • 用户数据提交到服务器的流程:
      用户发起申请,形成表单,指明提交方法,表单中的数据,会被转成http request的一部分,之后收集用户数据,并把用户数据推送给服务器;

    • GET方法可以将数据从服务器端拿到客户端,也可以将客户端的数据提交到服务器;
      使用GET方法提交请求
      web目录结构:
      在这里插入图片描述
      index.html
      使用GET方法将进行提交
      在这里插入图片描述
      **input是按钮,其中的action是点击按钮后访问的文件,method是方法,这里是GET;
      下面的Username和Password是kv结构输入框,type是内容,name是标签;
      **
      运行结果:
      在这里插入图片描述
      使用浏览器访问建立好的网页,这是一个可以登陆的界面;
      在这里插入图片描述
      输入好用户名和密码后,点击登录;
      在这里插入图片描述
      跳转到如上界面,登陆的时候其实就是把用户信息提交给服务器;
      在这里插入图片描述
      在上面的网址栏可以看到自己输入的用户名和密码,?后面是参数,前面是提交的地址,就是将参数提交到目标文件中;
      服务器收到的请求:
      在这里插入图片描述
      这是因为get方法通过url传参,并将参数回显到url中;

    • POST方法用于将客户端的数据提交到服务器;
      使用POST方法提交请求
      insex.html
      在这里插入图片描述
      运行结果:
      在这里插入图片描述
      点击登录:
      在这里插入图片描述
      服务器收到的请求:
      在这里插入图片描述
      POST是不会通过URL传参的,它通过正文传参;

    总结

    • GET方法通过URL传参,回显输入的私密信息,不够私密;
    • POST方法通过正文传参,不会回显私密信息,私密性有保证;
    • 私密性不是安全性;
    • 登录和注册一般常用的是POST方法;
      内容较大也建议使用POST方法,因为POST方法里面有正文长度,方便整段读取;

    5.HTTP协议的状态码

    在这里插入图片描述

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

    • 重定向当网页进行请求时,需要跳转到其他网页;
      301:永久移动,直接重定向到另一个网也,不会返回原来的网页,影响用户后续的请求策略;
      302:临时移动,临时重定向到另一个网页,比如登陆界面,处理好后再返回原始网页,不影响用户后续的请求策略;
      307:临时重定向;

    • 重定向过程
      客户端向服务器发起http请求 -> 服务器返回30X重定向状态码,并携带新的网页地址信息 -> 客户端浏览器拿到新的地址后,自动向新的地址发起请求;
      在这里插入图片描述

    重定向实验
    HttpServer.cc
    在这里插入图片描述
    如果读取的文件不存在,返回的状态码为301,会进行重定向操作;
    其中Location属性就是重定向后的目标文件地址;

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "HttpServer.hpp"
    #include "Usage.hpp"
    #include "Util.hpp"
    // 一般http都要有自己的web根目录
    #define ROOT "./wwwroot" // ./wwwroot/index.html
    // 如果客户端只请求了一个/,我们返回默认首页
    #define HOMEPAGE "index.html"
    void HandlerHttpRequest(int sockfd)
    {
        // 1. 读取请求 for test
        char buffer[10240];
        ssize_t s = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << "\n--------------------\n"
                      << std::endl;
        }
    
        std::vector<std::string> vline; // 取出http请求的每一行
        Util::cutString(buffer, "\n", &vline);
    
        std::vector<std::string> vblock; // 取出第一行的每一个子串
        Util::cutString(vline[0], " ", &vblock);
    
        std::string file = vblock[1]; // 请求的资源
        std::string target = ROOT;
    
        if (file == "/")
            file = "/index.html";
    
        target += file; // 请求的资源从web根目录下开始,如果不指定web根目录,就会访问Linux根目录
        std::cout << target << std::endl;
    
        std::string content;      // 文件中的内容
        std::ifstream in(target); // 打开文件
        if (in.is_open())
        {
            std::string line;
            while (std::getline(in, line))
            {
                content += line;
            }
            in.close();
        }
    
        std::string HttpResponse;
        if (content.empty())
        {
            HttpResponse = "HTTP/1.1 301 NotFound\r\n";
            HttpResponse += "Location: http://47.115.213.66:8080/a/b/404.html\r\n";
        }
        else
            HttpResponse = "HTTP/1.1 200 OK\r\n";
        HttpResponse += "\r\n";
        HttpResponse += content;
        // std::cout << "####start################" << std::endl;
        // for(auto &iter : vblock)
        // {
        //     std::cout << "---" << iter << "\n" << std::endl;
        // }
        // std::cout << "#####end###############" << std::endl;
        // 2. 试着构建一个http的响应
        send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
    }
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
            exit(0);
        }
        std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
        httpserver->Start();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    index.html
    客户端点击登陆后,会跳转到/a/b/notexit.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>lmx</title>
    </head>
    
    <body>
        <h3>Hello Guests!</h3>
        <form name="input" action="/a/b/notexit.html" method="POST">
            Username: <input type="text" name="user"> <br/>
            Password: <input type="password" name="pwd"> <br/>
            <input type="submit" value="登陆">
        </form>
    </body>
    
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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>
        <h2>你访问的页面不存在</h2>
    </body>
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果:
    客户端访问网页HOME地址:
    在这里插入图片描述
    点击登陆后,访问/a/b/notexit.html这个地址的文件,但是这个文件是不存在的,文件读取返回结果为空,状态码为301,触发重定向;
    在这里插入图片描述
    重定向到了a/b/404.html这个文件;

    6.HTTP协议的报头

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

    • Content-Type、Content-Length
      添加内容类型及正文长度报头;
      在这里插入图片描述
    • Cookie会话管理
      http的特征:
      a.简单快速;
      b.无连接,指http不维护连接,连接是由TCP维护的;
      c.无状态,http不会记录用户曾经请求的网页,不会对用户的行为做记录;

      http协议是无状态的,但是我们平常在浏览器进行访问网页时,一般网站是会记录下我们的状态的,这是因为http协议为了支持常规用户的会话管理,支持两个报头属性Cookie(请求)、Set-Cookie(响应)
      用户登录后,曾经输入的用户名和密码等信息会保存为一个文件,在今后每次的http请求中,每次都会携带这个文件中的账户密码内容,这个文件就是cookie文件;
      cookie文件的创建与使用流程:
      当用户访问网站后,在网站上输入用户密码信息,之后服务器会将用户信息返回给客户端,客户端的浏览器会将用户信息保存,形成cookie文件,之后用户每次访问该网站,都会将cookie文件再次上传到服务器,进行用户星系比对,不用每次都重新输入信息了;
      在这里插入图片描述
      但是cookie文件中是将用户信息明文保存的,如果被黑客注入木马病毒,是能够盗取用户的私密信息;
      现在的新cookie方案:在网站认证用户信息后,服务端会形成一个用户唯一ID,session id,并返回给用户端,保存到cookie文件中;这样每次用户访问网站,上传的cookie文件都是用户在网站形成的唯一session id,就算被盗取,也不会暴露用户的私密信息;

    验证cookie
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    7.connetion选项

    在这里插入图片描述
    keep-alive:长连接,网页该有的资源通过一个连接全部拿到;
    close:短连接,处理完一个http请求后,就将连接关掉,每次都要建立连接获取图片等资源;

  • 相关阅读:
    《计算机校招之路》1.0.0 震撼发布
    OSG笔记:OSG实现文字倾斜
    普通人在当前大环境下——少看宏观,多看具体
    挖矿宝藏之TCP/IP
    Open3D (C++) 泊松重建
    世界互联网大会领先科技奖发布 百度知识增强大语言模型关键技术获奖
    mybatis 14: 多对一关联查询
    ref和reactive的区别?
    POI报表的高级应用
    JavaScript -- 03. 运算符介绍
  • 原文地址:https://blog.csdn.net/kissland96166/article/details/132752955