【Linux 网络】基于TCP的Socket编程:通过协议定制,实现网络计算器

前言:

上文我们讲到了基于TCP的Socket编程:实现了一个远程命令执行的功能【Linux 网络】TCP Socket 编程实战:手把手实现远程命令执行(附实操要点 + 完整代码)-CSDN博客

本文我们再来进阶一下,同时定制协议,来实现更复杂的功能!


为了让我们的代码结构划分更加清晰,我们先对部分功能进行封装

Socket封装

对Socket中经常需要使用到的接口进行封装。

采用模板方法模式 进行设计,通过子类继承并重写父类方法来实现。

Socket.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Comment.hpp"
using namespace LogModule;

const int BACK = 16;

namespace SocketModule
{
    // 模板方法模式
    // 基类大部分函数都是虚函数
    class Socket
    {
    public:
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t prot) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void ConnectOrDie(InetAddr &addr) = 0;
        virtual ssize_t Recv(string &buffer) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual void Close() = 0;

        void BuildTcpSocket(uint16_t prot, int backlog = BACK)
        {
            SocketOrDie();
            BindOrDie(prot);
            ListenOrDie(backlog);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() {}

        TcpSocket(int fd)
            : _sockfd(fd)
        {
        }

        void SocketOrDie() override
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "socket error";
                exit(SOCK_ERR);
            }
        }

        void BindOrDie(uint16_t prot) override
        {
            InetAddr addr(prot);
            int ret = bind(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "bind error";
                exit(BIND_ERR);
            }
        }

        void ListenOrDie(int backlog) override
        {
            int ret = listen(_sockfd, backlog);
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "listen error";
                exit(LISTEN_ERR);
            }
        }

        // 客户端
        std::shared_ptr<Socket> Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (fd < 0)
            {
                LOG(LogLevel::ERROR) << "accept error";
                exit(ACCP_ERR);
            }

            // 将客户端信息带出
            InetAddr addr(peer);
            *client = addr;
            // accept成功后返回一个新的套接字,用于服务
            // 可以直接返回fd,也可以返回新对象(的指针)
            return std::make_shared<TcpSocket>(fd);
        }

        void ConnectOrDie(InetAddr &addr) override
        {
            int ret = connect(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "connect error";
                exit(CONN_ERR);
            }
        }

        ssize_t Recv(string &buffer) override
        {
            char str[1024];
            ssize_t n = recv(_sockfd, str, sizeof(str), 0);
            if (n)
            {
                // 首先,str并不是空白的,其空间可以被其他函数使用过,保存有垃圾数据
                // 其次,revc函数读取后,并不会主动的为我们读取到的信息末尾加上'\0'
                // 所以,我们必须自己显示的加上!
                // 不然当buffer+=的时候,str作为C风格的字符串,遇到'\0'才会停止
                // 导致程序就会不知道读到哪停,把后面的内存垃圾也一并收进去了!
                // 最终导致json串解析的时候失败!

                str[n] = 0;
                buffer += str; // 一定是+=,缓冲区信息是累加,而不是覆盖
            }
            return n;
        }

        int Send(const std::string &message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }

        void Close() override
        {
            close(_sockfd);
        }

        int Sockfd()
        {
            return _sockfd;
        }

    private:
        int _sockfd;
    };
}

协议定制

协议定制主要分为:序列化与反序列化、编码与解码

序列化与反序列化

在之前的文章中,我们曾讲过:协议在实现上,其本质是结构体!大家都遵守协议,那么涉及网络通信的数据都必须以结构体的形式存放!

而序列化,就是将"结构体"变成"字节流"!虽然协议的本质是结构体,网络通信的数据都是用结构体存放的,但是想要把数据通过网络发送给对方,其数据就必须是**"字节流"**。

将结构体转化为"字节流"有两种主流流派:

流派一:

直接发送结构体本身!既发送二进程数据。

很高效,很简单,速度快!但是!这就意味着发送方和接收方都必须是C++环境,并且结构体的内布局必须一模一样!

不推荐!

流派二:

结构化数据文本化!

直接将结构体的数据按照一定规则,进行文本化。仅发送文本、接收文本。接收的文件再按照规则进行反序列化!再次得到结构化数据。

推荐!

我们这里采用结构化数据文本化!那么具体改如何做呢?通过Json库实现!

认识JsonCpp库

JsonCpp库是C++中最通用的JSON处理库之一。主要功能就是进行序列化、反序列化。

序列化 (Serialization):将结构化数据,打包成一个标准的JSON字符串

反序列化 (Deserialization):把收到是JSON字符串,解析回结构化数据

使用样例:

由于JsonCpp库的第三方库,编译时一定要带路径:g++ -o @ ^ -l jsoncpp

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <string>
#include <jsoncpp/json/json.h> //头文件注意!!
using namespace std;

// 序列化
int main()
{
    // Json::Value 对象
    Json::Value root;
    root["one"] = "yi";
    root["two"] = "er";
    root["three"] = "san";

    string str = root.toStyledString();
    cout << str << endl;

    // Json::FastWriter 对象,不添加额外空格、换行符
    Json::FastWriter fast;
    str = fast.write(root);
    cout << str << endl;

    // Json::StreamWriterBuilder
    Json::StreamWriterBuilder stream;
    std::unique_ptr<Json::StreamWriter> writer(stream.newStreamWriter());
    stringstream ss;
    writer->write(root, &ss);
    cout << ss.str() << endl;
}
bash 复制代码
hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
{
        "one" : "yi",
        "three" : "san",
        "two" : "er"
}

{"one":"yi","three":"san","two":"er"}

{
        "one" : "yi",
        "three" : "san",
        "two" : "er"
}
cpp 复制代码
// 反序列化
int main()
{
    // string str = {"\" one ":\" yi ",\" three ":3,\" two ":" er \""};
    string str = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";
    Json::Value root;
    Json::Reader read;
    read.parse(str, root);

    cout << root["name"].asString() << endl;
    cout << root["city"].asString() << endl;
    cout << root["age"].asInt() << endl;
}
bash 复制代码
hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
张三
北京
30

通过使用JsonCpp库,我们就可以实现序列化与反序列化了!

编码与解码

为什么要编码与解码

封包与解包是为了解决 TCP 的"粘包"和"半包"问题,明确消息的边界。

如果说序列化 是为了让数据能"顺着网线爬过去",那么编码就是为了让接收方知道"这一坨数据爬到哪里才算是个头"。

根本原因:TCP是面向字节流的,因此TCP发送数据,是不能像UDP一样保证其数据是完整的数据报。它的传输方式像流水 一样,而不是像快递包裹一样。因为 TCP 这种"流水"特性,在接收端会发生两种灾难性的情况:

粘包:当发送方快速的发送了多条消息时,在底层这多条数据会被全部交给发送缓冲区中,由OS负责进行发送!

那么就有可能出现OS将发送缓冲区中的内容全部发送过去!而如果我们没有特定的规则,就无法判断数据的边界在哪里!无法正确的读取数据!

半包:当发送方发送了一条很长的数据,由于 TCP 的 MSS (最大报文段长度) 限制,或者网络拥塞窗口限制,这数据可能被拆成了两个包发送!

而这就可能导致其中有数据报是不完整 的!如果我们没有规定数据的边界,就无法判断数据报到底是否完整!

所以,编码与解码的作用:人为制造边界!保证读取数据的完整性!

如何进行编码与解码

我们这里规定编码 规则为:报文长度 + \r\n + 报文 + \r\n

解码就是反过来:去掉报文长度、\r\n,获取报文

协议具体实现

结合我们要设计的:网络计算器。具体实现思路如下:

1.要有一个序列与反序列化的模块,负责处理运算式(运算符与操作树)。

2.要有一个序列与反序列化的模块,负责处理运算式的结果(结果与状态)。

3.要有一个编码与解码模块,负责对要发送的序列化报文进程编码,对接收的报文进行解码。

Protocol.hpp

cpp 复制代码
#pragma once
#include <string>
#include <cstring>
#include <functional>
#include "Socket.hpp"
#include <jsoncpp/json/json.h> //注意头文件
using namespace SocketModule;

// 协议定制:网络计算器
// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)

//

// 处理运算式 -> 序列化与反序列化
class Request
{
public:
    Request() {}

    Request(int x, string op, int y)
        : _x(x), _y(y), _op(op)
    {
    }

    // 序列化
    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["op"] = _op;

        Json::FastWriter fast;
        return fast.write(root);
    }

    // 反序列化
    bool Deserialize(string &str)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(str, root);
        if (ok == true)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _op = root["op"].asString();
        }
        return ok;
    }

    int X() { return _x; }
    int Y() { return _y; }
    string Oper() { return _op; }

private:
    int _x;
    int _y;
    std::string _op; // 运算符(+ - * /)
};

// 处理运算式的结果 -> 序列与反序列化
class Respond
{
public:
    Respond() {}

    void Set(int result, int code)
    {
        _result = result;
        _code = code;
    }

    // 序列化
    std::string Serialize()
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter fast;
        return fast.write(root);
    }

    // 反序列化
    bool Deserialize(string &str)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(str, root);
        if (ok)
        {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
        }
        return ok;
    }

    bool Ret()
    {
        if (!_code)
            return true;
        return false;
    }

    int Result() { return _result; }
    int Code() { return _code; }

private:
    int _result; // 运算结果,无法区分清楚应答是计算结果,还是异常值
    int _code;   // 0:sucess, 1,2,3,4->不同的运算异常的情况, 约定!
};

// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)

// 保证读取完整性:采用添加报头的形式
//  json串长度 + \r\n + json串 + \r\n

const string tep = "\r\n";
using Func_t = function<void(Respond &, int, int, string)>;

class Protocol
{
public:
    Protocol() {}

    Protocol(Func_t func)
        : _func(func)
    {
    }

    // 编码
    // 50\r\n{"x":10,"y":20,"op":"+"}\r\n
    string Encode(const std::string &jsonstr)
    {
        int json_len_int = jsonstr.size();
        string json_len = to_string(json_len_int);
        return json_len + tep + jsonstr + tep;
    }

    // 从缓冲区读取的数据,可能如下:
    //
    // 50\r\
    // 50\r\n{"x":10,"y":20,"op
    // 50\r\n{"x":10,"y":20,"op":"+"}\r\n
    // 50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"

    // 解码:必须保证读取上来的json串是完整的!!!
    bool Decode(std::string &buffer, std::string *package)
    {
        // 先找分隔符
        size_t pos = buffer.find(tep);
        if (pos == string::npos)
        {
            // 不可能是完整的json串
            return false;
        }

        int json_len = stoi(buffer.substr(0, pos));
        int tep_len = tep.size();
        // 错误点:sizeof(json_len)结果为4,因为是整数
        // 应该用pos表示数字的长度!
        int message_len = pos + tep_len + json_len + tep_len;
        if (message_len > buffer.size())
        {
            // 一个完整的信息长度 > 当前buffer的长度
            // 不可能是完整的json串
            return false;
        }

        // 走到这里,一定有一个完整的报文!

        // 读取一个完整的报文出来
        *package = buffer.substr(pos + tep_len, message_len);
        // 读取后,移除已经读取了的报文
        buffer.erase(0, message_len);

        return true;
    }

    // 获取客户端信息,处理后再发送回客户端
    void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        // 接收缓冲区
        string buffer;
        while (true)
        {
            ssize_t n = sock->Recv(buffer);
            if (n > 0)
            {
                // 将buffer中的完整Json串,一次性处理完
                string package;
                while (Decode(buffer, &package))
                {
                    // 读取成功,得到一个完整的json串!
                    // 反序列化
                    Request request;
                    request.Deserialize(package);

                    // 调用运算方法
                    Respond respond;
                    _func(respond, request.X(), request.Y(), request.Oper());

                    // 得到结果,序列化后发送给客户端
                    string ret = respond.Serialize();
                    // 修复:发送前需要编码
                    string final_ret = Encode(ret);
                    sock->Send(final_ret);
                }
            }
            else if (n == 0)
            {
                // 读取到文件末尾!
                LOG(LogLevel::INFO) << "读取到了文件末尾~";
                break;
            }
            else
            {
                // 读取失败!
                LOG(LogLevel::INFO) << "读取失败!";
                break;
            }
        }
    }

private:
    Func_t _func;
};

网络计算器计算模块

协议我们也以及定制好了,还差最后一个板块!

协议中我们拿到了完整报文,也就是得到了计算式,接下来就是如何进行计算了,那其实就很简单了

NetCal.hpp

cpp 复制代码
#pragma
#include <string>
#include <iostream>
#include "Protocol.hpp"

class NetCal
{
public:
    void cal(Respond &respond, int x, int y, const char *op)
    {
        switch (*op)
        {
        case '+':
            respond.Set(x + y, 0);
            break;

        case '-':
            respond.Set(x - y, 0);
            break;

        case '*':
            respond.Set(x * y, 0);
            break;

        case '/':
            if (y == 0)
            {
                respond.Set(INT32_MIN, 1); // 除零错误!
            }
            else
            {
                respond.Set(x / y, 0);
            }
            break;

        case '%':
            if (y == 0)
            {
                respond.Set(INT32_MIN, 2); // mod错误!
            }
            else
            {
                respond.Set(x % y, 0);
            }
            break;

        default:
            // 设置错误code
            respond.Set(INT32_MIN, 3); // 非法运算符
            LOG(LogLevel::ERROR) << "非法运算符!";
            break;
        }
    }
};

核心模块我们都实现了,接下来只需要在服务器端中的代码进行合理调用既可实现对应功能!

主要观察TcpServer.hpp TcpServer.cc TcpServer.hpp文件

完整代码实现

TcpServer.cc

cpp 复制代码
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "NetCal.hpp"

void Usage(string str)
{
    cout << "Please use: " << str << " prot!" << endl;
}

int main(int args, char *argv[])
{
    if (args != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    // 顶层
    NetCal netcal;

    // 协议层
    Protocol protocol([&netcal](Respond &respond, int x, int y, string op)
                      { return netcal.cal(respond, x, y, op.c_str()); });

    // 服务器层
    uint16_t prot = stoi(argv[1]);
    TcpServer ts(prot, [&protocol](shared_ptr<Socket> sock, InetAddr &client)
                 { protocol.GetRequest(sock, client); });
    ts.Start();
}

TcpServer.hpp

cpp 复制代码
#pragma once
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "Socket.hpp"
using namespace SocketModule;

class TcpServer
{
    using Func_t = function<void(shared_ptr<Socket>, InetAddr &)>;

public:
    TcpServer(uint16_t prot, Func_t func)
        : _prot(prot),
          _isrunning(false),
          _func(func)
    {
        // 完成tcp套接字的创建、绑定、监听
        ts.BuildTcpSocket(_prot);
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            auto sock = ts.Accept(&_client);
            if (sock == nullptr)
                continue;

            pid_t pid = fork();
            if (pid == 0)
            {
                // 关闭不需要的描述符
                ts.Close();
                // 子进程
                if (fork() > 0)
                    return;

                // 孙子进程执行任务
                _func(sock, _client);
                return;
            }
            else if (pid > 0)
            {
                // 父进程
                // 关闭不需要的描述符
                sock->Close();
                // 子进程会立刻退出,不会阻塞等待
                wait(&pid);
            }
            else
            {
                ts.Close();
                LOG(LogLevel::ERROR) << "fork error";
                return;
            }
        }
        _isrunning = false;
    }

private:
    uint16_t _prot;
    TcpSocket ts;
    bool _isrunning;

    InetAddr _client;
    Func_t _func;
};

TcpClient.cc

cpp 复制代码
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
using namespace SocketModule;

void Usage(string str)
{
    cout << "Please use: " << str << " prot!" << endl;
}

int main(int args, char *argv[])
{
    if (args != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    string ip = argv[1];
    uint16_t prot = stoi(argv[2]);
    TcpSocket ts;
    ts.SocketOrDie();
    InetAddr addr(ip, prot);
    ts.ConnectOrDie(addr);

    while (true)
    {
        int a, b;
        string op;
        cout << "请输入参数一# ";
        cin >> a;
        cout << "请输入运算符# ";
        cin >> op;
        cout << "请输入参数二# ";
        cin >> b;
        getchar();

        Request request(a, op, b);
        Protocol protocol;
        // 序列化 && 编码
        string jsonstr = request.Serialize();
        string finalstr = protocol.Encode(jsonstr);

        // 发送编码后的信息
        ts.Send(finalstr);

        string buffer;
        // 接收信息,并序列化
        while (true)
        {
            int n = ts.Recv(buffer);
            if (n)
            {
                string package;
                // 2. 尝试解码(去除报头)
                // 必须 Decode,因为服务器发来的是 Encode 过的数据
                if (protocol.Decode(buffer, &package))
                {
                    Respond respond;
                    if (respond.Deserialize(package))
                    {
                        if (respond.Ret())
                            cout << "结果: " << respond.Result() << endl;
                        else
                            cout << "运算错误: " << respond.Code() << endl;
                    }
                    break; // 成功得到一个结果,跳出接收循环,进行下一次输入
                }
                else
                {
                    // 数据不完整,继续 Recv
                    continue;
                }
            }
            else
            {
                cout << "服务器连接断开" << endl;
                return 0;
            }
        }
    }
}

NetCal.hpp

cpp 复制代码
#pragma
#include <string>
#include <iostream>
#include "Protocol.hpp"

class NetCal
{
public:
    void cal(Respond &respond, int x, int y, const char *op)
    {
        switch (*op)
        {
        case '+':
            respond.Set(x + y, 0);
            break;

        case '-':
            respond.Set(x - y, 0);
            break;

        case '*':
            respond.Set(x * y, 0);
            break;

        case '/':
            if (y == 0)
            {
                respond.Set(INT32_MIN, 1); // 除零错误!
            }
            else
            {
                respond.Set(x / y, 0);
            }
            break;

        case '%':
            if (y == 0)
            {
                respond.Set(INT32_MIN, 2); // mod错误!
            }
            else
            {
                respond.Set(x % y, 0);
            }
            break;

        default:
            // 设置错误code
            respond.Set(INT32_MIN, 3); // 非法运算符
            LOG(LogLevel::ERROR) << "非法运算符!";
            break;
        }
    }
};

Comment.hpp

cpp 复制代码
#pragma once

// 禁止拷贝类
class NoCopy
{
public:
    NoCopy() {};
    // 拷贝构造
    NoCopy(const NoCopy &nocopy) = delete;
    // 赋值重载
    const NoCopy &operator=(const NoCopy &nocopy) = delete;
};

enum EXIT
{
    OK = 0,
    SOCK_ERR,
    BIND_ERR,
    LISTEN_ERR,
    ACCP_ERR,
    TCPSERVER_ERR,
    TCPCLIENT_ERR,
    CONN_ERR
};

Socket.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Comment.hpp"
using namespace LogModule;

const int BACK = 16;

namespace SocketModule
{
    // 模板方法模式
    // 基类大部分函数都是虚函数
    class Socket
    {
    public:
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t prot) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void ConnectOrDie(InetAddr &addr) = 0;
        virtual ssize_t Recv(string &buffer) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual void Close() = 0;

        void BuildTcpSocket(uint16_t prot, int backlog = BACK)
        {
            SocketOrDie();
            BindOrDie(prot);
            ListenOrDie(backlog);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() {}

        TcpSocket(int fd)
            : _sockfd(fd)
        {
        }

        void SocketOrDie() override
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "socket error";
                exit(SOCK_ERR);
            }
        }

        void BindOrDie(uint16_t prot) override
        {
            InetAddr addr(prot);
            int ret = bind(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "bind error";
                exit(BIND_ERR);
            }
        }

        void ListenOrDie(int backlog) override
        {
            int ret = listen(_sockfd, backlog);
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "listen error";
                exit(LISTEN_ERR);
            }
        }

        // 客户端
        std::shared_ptr<Socket> Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (fd < 0)
            {
                LOG(LogLevel::ERROR) << "accept error";
                exit(ACCP_ERR);
            }

            // 将客户端信息带出
            InetAddr addr(peer);
            client = &addr;
            // accept成功后返回一个新的套接字,用于服务
            // 可以直接返回fd,也可以返回新对象(的指针)
            return std::make_shared<TcpSocket>(fd);
        }

        void ConnectOrDie(InetAddr &addr) override
        {
            int ret = connect(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "connect error";
                exit(CONN_ERR);
            }
        }

        ssize_t Recv(string &buffer) override
        {
            char str[1024];
            ssize_t n = recv(_sockfd, str, sizeof(str), 0);
            if (n)
            {
                // 首先,str并不是空白的,其空间可以被其他函数使用过,保存有垃圾数据
                // 其次,revc函数读取后,并不会主动的为我们读取到的信息末尾加上'\0'
                // 所以,我们必须自己显示的加上!
                // 不然当buffer+=的时候,str作为C风格的字符串,遇到'\0'才会停止
                // 导致程序就会不知道读到哪停,把后面的内存垃圾也一并收进去了!
                // 最终导致json串解析的时候失败!

                str[n] = 0;
                buffer += str; // 一定是+=,缓冲区信息是累加,而不是覆盖
            }
            return n;
        }

        int Send(const std::string &message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }

        void Close() override
        {
            close(_sockfd);
        }

        int Sockfd()
        {
            return _sockfd;
        }

    private:
        int _sockfd;
    };
}

Protocol.hpp

cpp 复制代码
#pragma once
#include <string>
#include <cstring>
#include <functional>
#include "Socket.hpp"
#include <jsoncpp/json/json.h> //注意头文件
using namespace SocketModule;

// 协议定制:网络计算器
// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)

//

// 接收信息:处理模块 -> 客户端用
class Request
{
public:
    Request() {}

    Request(int x, string op, int y)
        : _x(x), _y(y), _op(op)
    {
    }

    // 序列化
    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["op"] = _op;

        Json::FastWriter fast;
        return fast.write(root);
    }

    // 反序列化
    bool Deserialize(string &str)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(str, root);
        if (ok == true)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _op = root["op"].asString();
        }
        return ok;
    }

    int X() { return _x; }
    int Y() { return _y; }
    string Oper() { return _op; }

private:
    int _x;
    int _y;
    std::string _op; // 运算符(+ - * /)
};

// 发送信息:发送模块 -> 服务器用
class Respond
{
public:
    Respond() {}

    void Set(int result, int code)
    {
        _result = result;
        _code = code;
    }

    // 序列化
    std::string Serialize()
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter fast;
        return fast.write(root);
    }

    // 反序列化
    bool Deserialize(string &str)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(str, root);
        if (ok)
        {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
        }
        return ok;
    }

    bool Ret()
    {
        if (!_code)
            return true;
        return false;
    }

    int Result() { return _result; }
    int Code() { return _code; }

private:
    int _result; // 运算结果,无法区分清楚应答是计算结果,还是异常值
    int _code;   // 0:sucess, 1,2,3,4->不同的运算异常的情况, 约定!
};

// 协议(基于TCP的)需要解决两个问题:
// 1. request和response必须得有序列化和反序列化功能
// 2. 你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)

// 保证读取完整性:采用添加报头的形式
//  json串长度 + \r\n + json串 + \r\n

const string tep = "\r\n";
using Func_t = function<void(Respond &, int, int, string)>;

class Protocol
{
public:
    Protocol() {}

    Protocol(Func_t func)
        : _func(func)
    {
    }

    // 编码
    // 50\r\n{"x":10,"y":20,"op":"+"}\r\n
    string Encode(const std::string &jsonstr)
    {
        int json_len_int = jsonstr.size();
        string json_len = to_string(json_len_int);
        return json_len + tep + jsonstr + tep;
    }

    // 从缓冲区读取的数据,可能如下:
    //
    // 50\r\
    // 50\r\n{"x":10,"y":20,"op
    // 50\r\n{"x":10,"y":20,"op":"+"}\r\n
    // 50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"y":20,"op":"+"}\r\n50\r\n{"x":10,"

    // 解码:必须保证读取上来的json串是完整的!!!
    bool Decode(std::string &buffer, std::string *package)
    {
        // 先找分隔符
        size_t pos = buffer.find(tep);
        if (pos == string::npos)
        {
            // 不可能是完整的json串
            return false;
        }

        int json_len = stoi(buffer.substr(0, pos));
        int tep_len = tep.size();
        // 错误点:sizeof(json_len)结果为4,因为是整数
        // 应该用pos表示数字的长度!
        int message_len = pos + tep_len + json_len + tep_len;
        if (message_len > buffer.size())
        {
            // 一个完整的信息长度 > 当前buffer的长度
            // 不可能是完整的json串
            return false;
        }

        // 走到这里,一定有一个完整的报文!

        // 读取一个完整的报文出来
        *package = buffer.substr(pos + tep_len, message_len);
        // 读取后,移除已经读取了的报文
        buffer.erase(0, message_len);

        return true;
    }

    // 获取客户端信息,处理后再发送回客户端
    void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        // 接收缓冲区
        string buffer;
        while (true)
        {
            ssize_t n = sock->Recv(buffer);
            if (n > 0)
            {
                // 将buffer中的完整Json串,一次性处理完
                string package;
                while (Decode(buffer, &package))
                {
                    // 读取成功,得到一个完整的json串!
                    // 反序列化
                    Request request;
                    request.Deserialize(package);

                    // 调用运算方法
                    Respond respond;
                    _func(respond, request.X(), request.Y(), request.Oper());

                    // 得到结果,序列化后发送给客户端
                    string ret = respond.Serialize();
                    // 修复:发送前需要编码
                    string final_ret = Encode(ret);
                    sock->Send(final_ret);
                }
            }
            else if (n == 0)
            {
                // 读取到文件末尾!
                LOG(LogLevel::INFO) << "读取到了文件末尾~";
                break;
            }
            else
            {
                // 读取失败!
                LOG(LogLevel::INFO) << "读取失败!";
                break;
            }
        }
    }

private:
    Func_t _func;
};

InetAddr.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>

// 实现网络地址与主机地址的转换

class InetAddr
{
public:
    InetAddr() {}

    // 网络转主机
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _prot = ntohs(_addr.sin_port); // 网络地址转主机地址
        char buff[1024];
        inet_ntop(AF_INET, &addr.sin_addr, buff, sizeof(buff)); // 将4字节网络风格的IP -> 点分十进制的字符串风格的IP
        _ip = std::string(buff);
    }

    // 主机转网络(客户端)
    InetAddr(std::string ip, uint16_t prot)
        : _ip(ip),
          _prot(prot)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        //&addr.sin_addr 是一个指向 struct in_addr 的指针,其内存地址等价于 &(addr.sin_addr.s_addr)(因为结构体的起始地址就是第一个成员的起始地址)
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_prot);
    }

    // 主机转网络(服务端)
    InetAddr(uint16_t prot)
        : _prot(prot)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(prot);
        _addr.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
    }

    // 直接获取sockaddr_in
    struct sockaddr *Getaddr()
    {
        return (struct sockaddr *)&_addr;
    }

    socklen_t Len()
    {
        socklen_t len = sizeof(_addr);
        return len;
    }

    uint16_t prot()
    {
        return _prot;
    }

    std::string ip()
    {
        return _ip;
    }

    // 运算符重载
    bool operator==(InetAddr &addr)
    {
        return _prot == addr._prot && _ip == addr._ip;
    }

    std::string Getname()
    {
        return _ip + ':' + std::to_string(_prot);
    }

private:
    struct sockaddr_in _addr;
    uint16_t _prot;
    std::string _ip;
};

Mutex.hpp

cpp 复制代码
// 封装锁接口
#pragma once
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&mutex, nullptr);
        }

        ~Mutex()
        {
            pthread_mutex_destroy(&mutex);
        }

        void Lock()
        {
            pthread_mutex_lock(&mutex);
        }

        void Unlock()
        {
            pthread_mutex_unlock(&mutex);
        }

        pthread_mutex_t *Get()
        {
            return &mutex;
        }

    private:
        pthread_mutex_t mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
            : _Mutex(mutex)
        {
            _Mutex.Lock();
        }

        ~LockGuard()
        {
            _Mutex.Unlock();
        }

    private:
        // 为了保证锁的底层逻辑,锁是不能够拷贝的,并且也是没有拷贝构造函数的
        //  避免拷贝,应该引用
        Mutex &_Mutex;
    };
}

Log.hpp

cpp 复制代码
// 实现日志模块

#pragma once
#include <iostream>
#include <sstream>    // 包含stringstream类
#include <filesystem> //C++17文件操作接口库
#include <fstream>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"
using namespace std;
using namespace MutexModule;

// 补充:外部类只能通过内部类的实例化对象,来访问内部类中的方法与成员,且受修饰符限制
//       内部类可以直接访问外部类的方法以及成员,没有限制

namespace LogModule
{
    const string end = "\r\n";

    // 实现刷新策略:a.向显示器刷新 b.向指定文件刷新

    // 利用多态机制实现
    // 包含至少一个纯虚函数的类称为抽象类,不能实例化,只能被继承
    class LogStrategy // 基类
    {
    public:
        //"=0"声明为纯虚函数。纯虚函数强制派生类必须重写该函数
        virtual void SyncLog(const string &message) = 0;
    };

    // 向显示器刷新:子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        void SyncLog(const string &message) override
        {
            // 加锁,访问显示器,显示器也是临界资源
            LockGuard lockguard(_mutex);
            cout << message << end;
        }

    private:
        Mutex _mutex;
    };

    // 向指定文件刷新:子类
    const string defaultpath = "./log";
    const string defaultfile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const string &path = defaultpath, const string &file = defaultfile)
            : _path(path), _file(file)
        {
            LockGuard lockguard(_mutex);
            // 判断路径是否存在,如果不存在就创建对应的路径
            if (!(filesystem::exists(_path)))
                filesystem::create_directories(_path);
        }

        void SyncLog(const string &message) override
        {
            // 合成最后路径
            string Path = _path + (_path.back() == '/' ? "" : "/") + _file;
            // 打开文件
            ofstream out(Path, ios::app);
            out << message << end;
        }

    private:
        string _path;
        string _file;
        Mutex _mutex;
    };

    //

    // 日志等级
    // enum class:强类型枚举。1.必须通过域名访问枚举值 2.枚举值不能隐式类型转化为整型
    enum class LogLevel
    {
        DEBUG,   // 调试级
        INFO,    // 信息级
        WARNING, // 警告级
        ERROR,   // 错误级
        FATAL    // 致命级
    };

    //

    // 将等级转化为字符串
    string LevelToStr(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "DEBUG";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKOWN";
        }
    }

    // 获取时间
    string GetTime()
    {
        // time函数:获取当前系统的时间戳
        // localtime_r函数:将时间戳转化为本地时间(可重入函数,localtime则是不可重入函数)
        // struct tm结构体,会将转化之后的本地时间存储在结构体中
        time_t curr = time(nullptr);
        struct tm curr_time;
        localtime_r(&curr, &curr_time);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                 curr_time.tm_year + 1900, // 年份是从1900开始计算的,需要加上1900才能得到正确的年份
                 curr_time.tm_mon + 1,     // 月份了0~11,需要加上1才能得到正确的月份
                 curr_time.tm_mday,        // 日
                 curr_time.tm_hour,        // 时
                 curr_time.tm_min,         // 分
                 curr_time.tm_sec);        // 秒

        return buffer;
    }

    //

    // 实现日志信息,并选择刷新策略
    class Logger
    {
    public:
        Logger()
        {
            // 默认选择显示器刷新
            Strategy = make_unique<ConsoleLogStrategy>();
        }

        void EnableConsoleLogStrategy()
        {
            Strategy = make_unique<ConsoleLogStrategy>();
        }

        void EnableFileLogStrategy()
        {
            Strategy = make_unique<FileLogStrategy>();
        }

        // 日志信息
        class LogMessage
        {
        public:
            LogMessage(const LogLevel &level, const string &name, const int &line, Logger &logger)
                : _level(level),
                  _name(name),
                  _logger(logger),
                  _line_member(line)
            {
                _pid = getpid();
                _time = GetTime();
                // 合并:日志信息的左半部分

                stringstream ss; // 创建输出流对象,stringstream可以将输入的所有数据全部转为为字符串
                ss << "[" << _time << "] "
                   << "[" << LevelToStr(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _name << "] "
                   << "[" << _line_member << "] "
                   << " - ";

                // 返回ss中的字符串
                _loginfo = ss.str();
            }

            // 日志文件的右半部分:可变参数,重载运算符<<

            // e.g. <<"huang"<<123<<"dasd"<<24
            template <class T>
            LogMessage &operator<<(const T &message) // 引用返回可以让后续内容不断追加
            {
                stringstream ss;
                ss << message;
                _loginfo += ss.str();

                // 返回对象!
                return *this;
            }

            // 销毁时,将信息刷新
            ~LogMessage()
            {
                // 日志文件
                _logger.Strategy->SyncLog(_loginfo);
            }

        private:
            string _time;
            LogLevel _level;
            pid_t _pid;
            string _name;
            int _line_member;
            string _loginfo; // 合并之后的一条完整信息

            // 日志对象
            Logger &_logger;
        };

        // 重载运算符(),便于创建LogMessage对象
        // 这里返回临时对象:当临时对象销毁时,调用对应的析构函数,自动对象中创建好的日志信息进行刷新!
        // 其次局部对象也不能传引用返回!
        LogMessage operator()(const LogLevel &level, const string &name, const int &line)
        {
            return LogMessage(level, name, line, *this);
        }

    private:
        unique_ptr<LogStrategy> Strategy;
    };

    // 为了用户使用更方便,我们使用宏封装一下
    Logger logger;

// 切换刷新策略
#define Enable_Console_LogStrategy() logger.EnableConsoleLogStrategy();
#define Enable_File_LogStrategy() logger.EnableFileLogStrategy();
// 创建日志,并刷新
//__FILE__ 和 __LINE__ 是编译器预定义的宏,作用是获取当前代码所在的文件名、行号
#define LOG(level) logger(level, __FILE__, __LINE__) // 细节:不加;
};
相关推荐
刘家炫1 小时前
Linux 基于 Epoll 的主从 Reactor 多线程模型
linux·服务器·reactor·项目·多路转接
真正的醒悟2 小时前
图解网络8
开发语言·网络·php
郝学胜-神的一滴2 小时前
Linux信号集操作函数详解
linux·服务器·开发语言·c++·程序人生
eggrall2 小时前
Linux 基础开发工具 —— 解锁高效开发的底层密钥
linux·运维·服务器
全栈工程师修炼指南2 小时前
Nginx | 负载均衡策略:ip_hash / hash 会话保持实践
运维·tcp/ip·nginx·负载均衡·哈希算法
哇哈哈&2 小时前
rabbitmq最简单的安装方法
linux·运维·centos
虎头金猫2 小时前
从杂乱到有序,Paperless-ngx 加个cpolar更好用
linux·运维·人工智能·docker·开源·beautifulsoup·pandas
赖small强2 小时前
【Linux 内存管理】Linux 低内存平台文件缓存导致的虚假内存不足问题分析与解决方案
linux·缓存·oom·水位线
慾玄2 小时前
ce复习--Chrony服务器
linux