【Linux网络编程】第十弹---打造初级网络计算器:从协议设计到服务实现

46 阅读12分钟

1、Protocol.hpp

该文件实现序列化与反序列使用到的类和相关函数(加报头解报头)!

1.1、Request类

该类是向服务器发送请求的类,需要三个成员变量,_x,_y,_oper(运算符号) ,统一的计算格式是 _x _oper _y,内部主要是序列化(发送)和反序列化(接受)函数

1.1.1、基本结构 

该类有三个成员变量,_x,_y,_oper(运算符号),序列化,反序列化,构造,析构及其他获取成员变量与打印的函数!

class Request
{
public:
    Request()
    {}
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out);
    // 反序列化 将字符串转成结构化
    bool Deserialize(const std::string &in);
    void Print();
    ~Request();
    int X();
    int Y();
    char Oper();
private:
    int _x;
    int _y;
    char _oper; // + - * / %   // x oper y
};

1.1.2、构造析构函数

为了方便后面的使用,此处实现两个构造函数,一个无参,一个带参函数,析构函数无需处理!

Request()
{}
Request(int x,int y,char oper):_x(x),_y(y),_oper(oper)
{}
 
~Request()
{}

1.1.3、序列化函数

序列化将结构化转成字符串,并将字符串以输出型参数传出

// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
    // 1.自己做: "x oper y" (麻烦)
    // 2.使用现成的库, xml,json(jsoncpp库), protobuf
    Json::Value root;
    root["x"] = _x;
    root["y"] = _y;
    root["oper"] = _oper;
    Json::FastWriter writer;
    std::string s = writer.write(root);
    *out = s;
    return true;
}

 

1.1.4、反序列化函数

反序列化将字符串转成结构化,参数传入字符串

// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
    Json::Value root;
    Json::Reader reader;
    bool res = reader.parse(in,root);
 
    _x = root["x"].asInt();
    _y = root["y"].asInt();
    _oper = root["oper"].asInt();
 
    return true;
}

打印函数

void Print()
{
    std::cout << _x << std::endl;
    std::cout << _y << std::endl;
    std::cout << _oper << std::endl;
}

1.1.5、获取成员变量函数

因为成员变量是私有的,外部访问使用成员函数!

int X()
{
    return _x;
}
int Y()
{
    return _y;
}
char Oper()
{
    return _oper;
}

1.1.6、设置成员变量值函数

void SetValue(int x, int y, char oper)
{
    _x = x;
    _y = y;
    _oper = oper;
}

1.2、Response类

该类是向客户端发送结果的类,需要三个成员变量,_result(计算结果),_code(自定义错误码[ 0: success 1: div error 2: 非法操作 ]),_desc(错误码描述) ,内部主要是序列化(发送)和反序列化(接受)函数

1.2.1、基本结构 

该类有三个成员变量,_result(计算结果),_code(自定义错误码 ),_desc(错误码描述),序列化,反序列化,构造,析构和打印函数!

注意:为了方便类外访问该类成员变量,该成员变量是公有的! 

class Response
{
public:
    Response();
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out);
    // 反序列化 将字符串转成结构化
    bool Deserialize(const std::string &in);
    ~Response();
public:
    int _result;
    int _code; // 0: success 1: div error 2: 非法操作
    std::string _desc; 
};

1.2.2、构造析构函数

构造函数直接手动初始化(结果和错误码初始化为0,描述默认初始化为success),析构函数无需处理

Response():_result(0),_code(0),_desc("success")
{}
 
~Response()
{}

1.2.3、序列化函数

序列化将结构化转成字符串,并将字符串以输出型参数传出

// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
    // 使用现成的库, xml,json(jsoncpp库), protobuf
    Json::Value root;
    root["result"] = _result;
    root["code"] = _code;
    root["desc"] = _desc;
    Json::FastWriter writer;
    std::string s = writer.write(root);
    *out = s;
    
    return true;
}

1.2.4、反序列化函数

反序列化将字符串转成结构化,参数传入字符串

// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
    Json::Value root;
    Json::Reader reader;
    bool res = reader.parse(in,root);
 
    _x = root["x"].asInt();
    _y = root["y"].asInt();
    _oper = root["oper"].asInt();
 
    return true;
}

1.2.5、打印函数

将成员变量以字符串形式打印出来即可!

void PrintResult()
{
    std::cout << "result: " << _result << ", code: " << _code 
    << ", desc: " << _desc << std::endl;
}

1.3、Factory类

因为Request类和Response类可能频繁创建,因此我们可以设计一个工厂类内部设计两个创建类的静态函数(没有this指针,外部直接调用函数即可)

class Factory
{
public:
    static std::shared_ptr<Request> BuildRequestDefault()
    {
        return std::make_shared<Request>();
    }
    static std::shared_ptr<Response> BuildResponseDefault()
    {
        return std::make_shared<Response>();
    }
};

1.4、添加报头

在实际的网络通信中,传的不仅仅是序列化之后的字符串,还会有报头信息,此处我们也设计一下报头信息,格式如下:

1、 "len"\r\n"{json}"\r\n --- 完整的报文

2、len 有效载荷的长度

3、\r\n(第一个): 区分len 和 json 串

4、\r\n(第二个): 暂时没有其他用,打印方便,debug

static const std::string sep = "\r\n"; // 分隔符
 
// 添加报头
std::string Encode(const std::string &jsonstr)
{
    int len = jsonstr.size();
    std::string lenstr = std::to_string(len);
    return lenstr + sep + jsonstr + sep;
}

1.5、解析报头

将发送过来的有报头的信息解析成有效信息,即去掉前面的长度和分割符与有效信息后面的分隔符

注意:可能没有一个有效信息或者有多个有效信息!

static const std::string sep = "\r\n"; // 分隔符
 
// 不能带const
// "le
// "len"\r\n"{j [)
// "len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
 
std::string Decode(std::string &packagestream)
{
    // 分析
    auto pos = packagestream.find(sep); // 报文流
    if (pos == std::string::npos)
        return std::string(); // 没找到返回空
    std::string lenstr = packagestream.substr(0, pos);
    int len = std::stoi(lenstr); // json长度
    // 计算一个完整的报文应该是多长
    int total = lenstr.size() + len + 2 * sep.size();
    // 传进来的字符串长度小于报文总长,说明没有一个完整的有效信息,返回空
    if (packagestream.size() < total)
        return std::string();
        
    // 提取
    std::string jsonstr = packagestream.substr(pos + sep.size(), len);
    packagestream.erase(0, total); // 从0位置删除total长度
 
    return jsonstr;
}

2、Service.hpp

Service.hpp中的 IOService类 是用于通信的类,而且内部需要执行传入的回调函数,因此该类需要加一个执行方法的成员!

执行方法的声明:

参数是请求类的指针,返回值是应答类的指针!

using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;

2.1、构造析构函数

构造函数需要传入函数对象,用于初始化成员变量,析构函数无需处理!

IOService(process_t process) :_process(process)
{}
 
~IOService()
{}

2.2、IOExcute()

IOExcute() 函数进行客户端与服务端的通信,并处理发送过来的信息(调用执行方法) ,有以下7个主要步骤!

1、接收消息

2、报文解析(保证获取至少获得一条有效信息,没有则继续接受消息)

3、反序列化(将字符串转成结构化)

4、业务处理(调用构造函数传入的回调函数)

5、序列化应答

6、添加len长度(报头)

7、发送回去

void IOExcute(SockSPtr sock, InetAddr &addr)
{
    std::string packagestreamqueue; // 写在while循环外,存储信息
    while (true)
    {
        // 1.负责读取
        ssize_t n = sock->Recv(&packagestreamqueue);
        if(n <= 0)
        {
            LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
            break;
        }
 
        std::cout << "--------------------------------------------" << std::endl;
        std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;
 
        // 我们能保证读到的是完整的报文? 不能!
        // 2.报文解析,提取报头和有效载荷
        std::string package = Decode(packagestreamqueue);
        if(package.empty()) continue;
        // 我们能保证读到的是一个完整的报文!!!
        auto req = Factory::BuildRequestDefault();
        std::cout << "package: \n" << package << std::endl;
 
        // 3.反序列化
        req->Deserialize(package); // 反序列化 将字符串转成结构化
 
        // 4.业务处理
        auto resp = _process(req); // 业务处理(通过请求,得到应答)
 
        // 5.序列化应答
        std::string respjson;
        resp->Serialize(&respjson); // 序列化
        std::cout << "respjson: \n" << respjson << std::endl;
 
        // 6.添加len长度
        respjson = Encode(respjson);
        std::cout << "respjson add header done: \n" << respjson << std::endl;
 
        // 7.发送回去
        sock->Send(respjson);
    }

此处有一个问题,如果第一次接收消息没有读到完整的报文就会继续接受消息,但是以我们前面写的接收消息函数会清空内容,因此我们需要做稍微的修改! 

2.3、Recv()

Recv()函数是Socket.hpp文件中TcpServer类的成员函数,接收消息成功之后需要该为拼接旧的内容

// 接收消息
ssize_t Recv(std::string *out) override
{
    char inbuffer[4096];
    ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
    if (n > 0)
    {
        inbuffer[n] = 0;
        // *out = inbuffer;
        *out += inbuffer; // 调整(可能一次读取不成功 | 读取多次)
    }
    return n;
}

3、NetCal.hpp

NetCal.hpp文件中的NetCal类包含回调函数的具体实现

3.1、构造析构函数

该类没有成员变量,构造析构函数无需处理!

NetCal()
{}
 
~NetCal()
{}

3.2、Calculator()

Calculator() 函数用于网络计算器的计算逻辑!

std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req)
{
    auto resp = Factory::BuildResponseDefault();
    switch (req->Oper())
    {
    case '+':
        resp->_result = req->X() + req->Y();
        break;
    case '-':
        resp->_result = req->X() - req->Y();
        break;
    case '*':
        resp->_result = req->X() * req->Y();
        break;
    case '/':
        {
            if(req->Y() == 0)
            {
                resp->_code = 1;
                resp->_desc = "divc zero";
            }
            else
            {
                resp->_result = req->X() / req->Y();
            }
        }
        break;
    case '%':
        {
            if(req->Y() == 0)
            {
                resp->_code = 2;
                resp->_desc = "mod zero";
            }
            else
            {
                resp->_result = req->X() % req->Y();
            }
        }
        break;
    default:
        {
            resp->_code = 3;
            resp->_desc = "illegal operation";
        }
        break;
    }
    return resp;
}

4、ClientMain.cc

该文件用户创建TcpServer类对象,并调用执行函数运行客户端

通信操作主要包括以下七步:

1、序列化

2、添加长度报头字段

3、发送数据

4、读取应答,response

5、报文解析,提取报头和有效载荷

6、反序列化

7、打印结果

#include <iostream>
#include <ctime>
#include "Socket.hpp"
#include "Protocol.hpp"
 
using namespace socket_ns;
 
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
 
    SockSPtr sock = std::make_shared<TcpSocket>();
    if (!sock->BuildClientSocket(serverip, serverport))
    {
        std::cerr << "connect error" << std::endl;
        exit(1);
    }
 
    srand(time(nullptr) ^ getpid());
    const std::string opers = "+-*/%&^!";
 
    std::string packagestreamqueue;
    while (true)
    {
        // 构建数据
        int x = rand() % 10;
        usleep(x * 1000);
        int y = rand() % 10;
        usleep(x * y * 100);
        char oper = opers[y % opers.size()];
 
        // 构建请求
        auto req = Factory::BuildRequestDefault();
        req->SetValue(x, y, oper);
 
        // 1.序列化
        std::string reqstr;
        req->Serialize(&reqstr);
 
        // 2.添加长度报头字段
        reqstr = Encode(reqstr);
 
        std::cout << "####################################" << std::endl;
 
        std::cout << "requset string: \n" << reqstr << std::endl;
 
        // 3.发送数据
        sock->Send(reqstr);
 
        while (true)
        {
            // 4.读取应答,response
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                break;
            }
            // 我们能保证读到的是完整的报文? 不能!
            // 5.报文解析,提取报头和有效载荷
            std::string package = Decode(packagestreamqueue);
            if (package.empty())
                continue;
 
            std::cout << "package: \n" << package << std::endl;
 
            // 6.反序列化
            auto resp = Factory::BuildResponseDefault();
            resp->Deserialize(package);
 
            // 7.打印结果
            resp->PrintResult();
 
            break;
        }
 
        sleep(1);
    }
 
    sock->Close();
 
    return 0;
}

5、完整代码

5.1、Protocol.hpp

#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <jsoncpp/json/json.h>
 
static const std::string sep = "\r\n"; // 分隔符
 
// 设计一下协议的报头和报文的完整格式
// "len"\r\n"{json}"\r\n --- 完整的报文
// len 有效载荷的长度
// \r\n(第一个): 区分len 和 json 串
// \r\n(第二个): 暂时没有其他用,打印方便,debug
 
// 添加报头
std::string Encode(const std::string &jsonstr)
{
    int len = jsonstr.size();
    std::string lenstr = std::to_string(len);
    return lenstr + sep + jsonstr + sep;
}
 
// 不能带const
// "le
// "len"\r\n"{j [)
// "len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
 
std::string Decode(std::string &packagestream)
{
    // 分析
    auto pos = packagestream.find(sep); // 报文流
    if (pos == std::string::npos)
        return std::string(); // 没找到返回空
    std::string lenstr = packagestream.substr(0, pos);
    int len = std::stoi(lenstr); // json长度
    // 计算一个完整的报文应该是多长
    int total = lenstr.size() + len + 2 * sep.size();
    // 传进来的字符串长度小于报文总长,说明没有一个完整的有效信息,返回空
    if (packagestream.size() < total)
        return std::string();
 
    // 提取
    std::string jsonstr = packagestream.substr(pos + sep.size(), len);
    packagestream.erase(0, total); // 从0位置删除total长度
 
    return jsonstr;
}
 
// 协议
class Request
{
public:
    Request()
    {
    }
    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
    {
    }
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out)
    {
        // 1.自己做: "x oper y" (麻烦)
        // 2.使用现成的库, xml,json(jsoncpp库), protobuf
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;
        Json::FastWriter writer;
        std::string s = writer.write(root);
        *out = s;
        return true;
    }
    // 反序列化 将字符串转成结构化
    bool Deserialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool res = reader.parse(in, root);
 
        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();
 
        return true;
    }
    void Print()
    {
        std::cout << _x << std::endl;
        std::cout << _y << std::endl;
        std::cout << _oper << std::endl;
    }
    ~Request()
    {
    }
    int X()
    {
        return _x;
    }
    int Y()
    {
        return _y;
    }
    char Oper()
    {
        return _oper;
    }
    void SetValue(int x, int y, char oper)
    {
        _x = x;
        _y = y;
        _oper = oper;
    }
 
private:
    int _x;
    int _y;
    char _oper; // + - * / %   // x oper y
};
 
// class request resp = {30,0}
class Response
{
public:
    Response() : _result(0), _code(0), _desc("success")
    {
    }
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out)
    {
        // 使用现成的库, xml,json(jsoncpp库), protobuf
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        root["desc"] = _desc;
        Json::FastWriter writer;
        std::string s = writer.write(root);
        *out = s;
 
        return true;
    }
    // 反序列化 将字符串转成结构化
    bool Deserialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool res = reader.parse(in, root);
        if (!res)
            return false;
        _result = root["result"].asInt();
        _code = root["code"].asInt();
        _desc = root["desc"].asString();
 
        return true;
    }
    void PrintResult()
    {
        std::cout << "result: " << _result << ", code: " << _code << ", desc: " << _desc << std::endl;
    }
    ~Response()
    {
    }
 
public:
    int _result;
    int _code; // 0: success 1: div error 2: 非法操作
    std::string _desc;
};
 
class Factory
{
public:
    static std::shared_ptr<Request> BuildRequestDefault()
    {
        return std::make_shared<Request>();
    }
    static std::shared_ptr<Response> BuildResponseDefault()
    {
        return std::make_shared<Response>();
    }
};

5.2、Service.hpp

#pragma once
#include <iostream>
#include <functional>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"
 
using namespace socket_ns;
using namespace log_ns;
 
using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;
 
class IOService
{
public:
    IOService(process_t process) :_process(process)
    {
    }
    void IOExcute(SockSPtr sock, InetAddr &addr)
    {
        std::string packagestreamqueue; // 写在while循环外,存储信息
        while (true)
        {
            // 1.负责读取
            ssize_t n = sock->Recv(&packagestreamqueue);
            if(n <= 0)
            {
                LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
                break;
            }
 
            std::cout << "--------------------------------------------" << std::endl;
            std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;
 
            // 我们能保证读到的是完整的报文? 不能!
            // 2.报文解析,提取报头和有效载荷
            std::string package = Decode(packagestreamqueue);
            if(package.empty()) continue;
            // 我们能保证读到的是一个完整的报文!!!
            auto req = Factory::BuildRequestDefault();
            std::cout << "package: \n" << package << std::endl;
 
            // 3.反序列化
            req->Deserialize(package); // 反序列化 将字符串转成结构化
 
            // 4.业务处理
            auto resp = _process(req); // 业务处理(通过请求,得到应答)
 
            // 5.序列化应答
            std::string respjson;
            resp->Serialize(&respjson); // 序列化
            std::cout << "respjson: \n" << respjson << std::endl;
 
            // 6.添加len长度
            respjson = Encode(respjson);
            std::cout << "respjson add header done: \n" << respjson << std::endl;
 
            // 7.发送回去
            sock->Send(respjson);
        }
    }
    // 测试
    // void IOExcute(SockSPtr sock, InetAddr &addr)
    // {
    //     while (true)
    //     {
    //         std::string message;
    //         ssize_t n = sock->Recv(&message);
    //         if(n > 0)
    //         {
    //             LOG(INFO, "get message from client [%s],message: %s\n", addr.AddrStr().c_str(), message.c_str());
    //             std::string hello = "hello";
 
    //             sock->Send(hello);
    //         }
    //         else if(n == 0)
    //         {
    //             LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
    //             break;
    //         }
    //         else 
    //         {
    //             LOG(ERROR, "read error\n", addr.AddrStr().c_str());
    //             break;
    //         }
    //     }
    // }
    ~IOService()
    {
    }
private:
    process_t _process;
};

5.3、ClientMain.cc

#include <iostream>
#include <ctime>
#include "Socket.hpp"
#include "Protocol.hpp"
 
using namespace socket_ns;
 
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
 
    SockSPtr sock = std::make_shared<TcpSocket>();
    if (!sock->BuildClientSocket(serverip, serverport))
    {
        std::cerr << "connect error" << std::endl;
        exit(1);
    }
 
    srand(time(nullptr) ^ getpid());
    const std::string opers = "+-*/%&^!";
 
    std::string packagestreamqueue;
    while (true)
    {
        // 构建数据
        int x = rand() % 10;
        usleep(x * 1000);
        int y = rand() % 10;
        usleep(x * y * 100);
        char oper = opers[y % opers.size()];
 
        // 构建请求
        auto req = Factory::BuildRequestDefault();
        req->SetValue(x, y, oper);
 
        // 1.序列化
        std::string reqstr;
        req->Serialize(&reqstr);
 
        // 2.添加长度报头字段
        reqstr = Encode(reqstr);
 
        std::cout << "####################################" << std::endl;
 
        std::cout << "requset string: \n" << reqstr << std::endl;
 
        // 3.发送数据
        sock->Send(reqstr);
 
        while (true)
        {
            // 4.读取应答,response
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                break;
            }
            // 我们能保证读到的是完整的报文? 不能!
            // 5.报文解析,提取报头和有效载荷
            std::string package = Decode(packagestreamqueue);
            if (package.empty())
                continue;
 
            std::cout << "package: \n" << package << std::endl;
 
            // 6.反序列化
            auto resp = Factory::BuildResponseDefault();
            resp->Deserialize(package);
 
            // 7.打印结果
            resp->PrintResult();
 
            break;
        }
 
        sleep(1);
    }
 
    sock->Close();
 
    return 0;
}

5.4、Makefile

.PHONY:all
all:calserver calclient
 
calserver:ServerMain.cc 
	g++ -o $@ $^ -std=c++14 -ljsoncpp
 
calclient:ClientMain.cc 
	g++ -o $@ $^ -std=c++14 -ljsoncpp
 
.PHONY:clean 
clean:
	rm -rf calserver calclient