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

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

文章目录


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

1.自定义协议

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

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

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"

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

public:
    Sock() {}

    int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        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() {}
};

TcpServer.hpp
封装TCP服务接口的类;

注意:类内回调函数由于参数有this指针,无法正-常回调,因此需要设置成static成员,再通过参数传进this指针,来访问类内非静态成员;

cpp 复制代码
#pragma once

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

namespace ns_tcpserver
{
    using func_t = std::function<void(int)>;
    class TcpServer;
    class ThreadData // 传入回调函数的参数
    {
    public:
        ThreadData(int sock, TcpServer *server)
            : _sock(sock), _server(server)
        {
        }
        ~ThreadData() {}

    public:
        int _sock;
        TcpServer *_server; // 里面有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; // 回调函数列表
    };

}

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

  • TCP协议的读写接口(read和write)都是将数据拷贝到缓冲区或者从缓冲区拷贝出来,并不是直接发送到对方主机;发送给对方主机是由TCP传输控制协议决定的
  • 由于TCP是面向字节流的协议,因此,发送和接受的次数,每次发送多少字符,都不受控制(UDP协议每次发送和接受的都是完整的报文),有可能每次接收到的不一定是完整的报文,也有可能一次读取了多个报文,所以需要自己定制协议解包代码;在读取时不能简单地receive,而需要对读取的数据进行解析;
  • 自主定制的协议使用"length\r\nx_ op_ y_\r\n"协议,前面加上数据长度;
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#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 "";//没有完整地报文,继续接收
        }
    }
}

CalServer.cc
计算服务

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


    方案二:接收到信息时,需要判断信息的完整性,读取是否成功
  • 一般经验:在server编写的时候,要有较为严谨的判断逻辑;
    一般服务器都是要忽略SIGPIPE信号的,防止在运行过程中出现非法写入的问题;
cpp 复制代码
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>

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;
}

CalClient.cc
客户端

cpp 复制代码
#include <iostream>
#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;
}

运行结果:

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

cpp 复制代码
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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);
    }
}

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

cpp 复制代码
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include <memory>
#include <signal.h>
#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;
}

运行结果:

注:

守护进程实际上是孤儿进程,但是没有被系统领养,而是自成会话

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

3.使用json来完成序列化

json:网络通信的格式

  • 在Linux上安装json:
  • json实际上是一个结构化数据格式,里面是很多的kv结构:
  • json库的使用:

    StyleWriter对象,两个kv对象之间有换行符;
    StyleWriter对象的write函数会将root中的kv内容直接转换为对应的string;

    运行结果:


    FastWriter对象,中间没有换行符
    运行结果:

    json里面是可以套json的

使用json协议完成序列化和反序列化:

由于使用的是非cpp官方库,因此需要添加编译选项:
makefile

bash 复制代码
.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

Protocol.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "Sock.hpp"
#include <jsoncpp/json/json.h>

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

#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是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 "";//没有完整地报文,继续接收
        }
    }

}

运行结果:

二、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

cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#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(&timestamp);
    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);
}

Sock.hpp

cpp 复制代码
#pragma once

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

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

public:
    Sock() {}
    int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        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() {}
};

Usage.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
void Usage(std::string proc)
{
    std::cout << "\nUsage: " << proc <<  " port\n" << std::endl;
}

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

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>

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));
    }
};

HttpServer.hpp

cpp 复制代码
#pragma once

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

class HttpServer
{
public:
    using func_t = std::function<void(int)>;
private:
    int listensock_;
    uint16_t port_;
    Sock sock;
    func_t func_;
public:
    HttpServer(const uint16_t &port, func_t func): port_(port),func_(func)
    {
        listensock_ = sock.Socket();
        sock.Bind(listensock_, port_);
        sock.Listen(listensock_);
    }
    void Start()
    {
        signal(SIGCHLD, SIG_IGN);
        for( ; ; )
        {
            std::string clientIp;
            uint16_t clientPort = 0;
            int sockfd = sock.Accept(listensock_, &clientIp, &clientPort);
            if(sockfd < 0) continue;
            if(fork() == 0)
            {
                close(listensock_);
                func_(sockfd);
                close(sockfd);
                exit(0);
            }
            close(sockfd);
        }
    }
    ~HttpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }
};

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

cpp 复制代码
#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#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;
}

在目录下创建web根目录wwwroot,里面创建首页index.html;

index.html
在vscode下装插件,!table会出现网页模板;

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>这个一个Linux课程</h3>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
    <p>我是一个Linux的学习者,我正在进行http的测试工作!!</p>
</body>
</html>

运行结果:

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属性就是重定向后的目标文件地址;

cpp 复制代码
#include <iostream>
#include <memory>
#include <cassert>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#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;
}

index.html
客户端点击登陆后,会跳转到/a/b/notexit.html这个地址的文件;

cpp 复制代码
<!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>

404.html
重定向的目标文件;

cpp 复制代码
<!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>

运行结果:

客户端访问网页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请求后,就将连接关掉,每次都要建立连接获取图片等资源;

相关推荐
Sumlll_1 小时前
Ubuntu系统下QEMU的安装与RISC-V的测试
linux·ubuntu·risc-v
猫头虎1 小时前
2025最新OpenEuler系统安装MySQL的详细教程
linux·服务器·数据库·sql·mysql·macos·openeuler
木子.李3472 小时前
ssh连接远程服务器相关总结
运维·服务器·ssh
晚风吹人醒.3 小时前
SSH远程管理及访问控制
linux·运维·ssh·scp·xshell·访问控制·远程管理
AI大模型应用之禅4 小时前
全球股市估值与可持续农业垂直种植技术的关系
网络·ai
掘根4 小时前
【仿Muduo库项目】HTTP模块2——HttpRequest子模块,HttpResponse子模块
网络·网络协议·http
Uncertainty!!4 小时前
Linux多用户情况下个别用户输入密码后黑屏
linux·远程连接
necessary6534 小时前
使用Clion查看linux环境中的PG源码
linux·运维·服务器
江湖有缘6 小时前
Jump个人仪表盘Docker化部署教程:从0到 搭建专属导航页
运维·docker·容器
小猪佩奇TONY6 小时前
Linux 内核学习(14) --- linux x86-32 虚拟地址空间
linux·学习