【Linux网络】定制协议

这篇文章我们就来实现一个自定义协议,应用层我们来写一个计算器,将计算的操作数和操作符等结构化的数据使用我们自己定义的协议序列化为字符串流,发送给服务端,然后服务端将字符串流反序列化为原来的结构化数据,进行运算,接着再将结果序列化为字符串流发给对应的客户端,最后客户端再反序列化拿到计算结果。这其实就是一个网络版的计算器

文章目录

  • [1. 定制协议](#1. 定制协议)
  • [2. 应用层封装计算器](#2. 应用层封装计算器)
  • [3. 服务端主程序](#3. 服务端主程序)
  • [4. 客户端主程序](#4. 客户端主程序)

1. 定制协议

1.1 基本结构

要实现一个网络版的计算器,客户端首先需要将两个操作数和一个操作符这种结构化的数据序列化为一条请求报文发送给服务端,然后服务端将请求报文反序列化为原来的结构化数据,根据两个操作数和操作符计算结果,服务端将运算结果序列化为一条应答报文发送给客户端,最后客户端将应答报文反序列化为结构化数据,从而拿到运算结果

框架如下:

cpp 复制代码
#pragma once

#include "Socket.hpp"
#include <jsoncpp/json/json.h>

using namespace SocketModule;

// client -> server
class Request
{
public:
    Request(){}
    Request(int x, int y, char oper) 
        : _x(x), _y(y), _oper(oper)
    {}
    ~Request(){}
private:
    int _x;
    int _y;
    char _oper; // + - * /
};

// server -> client
class Response
{
public:
    Response(){}
    Response(int result, int code) 
        :_result(result), _code(code)
    {}
    ~Response(){}
private:
    int _result; // 运算结果异常,无法区分_result是正常的结果,还是异常值
    int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
};

我们运算结果可能是运算错误的结果,比如除0错误,那么该结果是错误的,所以一个_result不够,还需要再引入一个变量_code,表示运算结果是正常结果还是异常值


1.2 序列化和反序列化

对于序列化和反序列化,我们已经在上篇文章中介绍了JSONCPP的方案,这里就不多介绍了,不过需要提一点,就是我们网络通信肯定选择紧凑格式的JSON字符串

代码如下:

cpp 复制代码
// client -> server
class Request
{
public:
    Request(){}
    Request(int x, int y, char oper) 
        : _x(x), _y(y), _oper(oper)
    {}

    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper; 

        Json::FastWriter writer;
        std::string s = writer.write(root);
        return s;
    }

    // {"x": 10, "y" : 20, "oper" : '+'}
    bool Deserialize(std::string &in)
    {
        // "10" "20" '+' -> 以空格作为分隔符 -> 10 20 '+'
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
        }
        return ok;
    }

    ~Request(){}
private:
    int _x;
    int _y;
    char _oper; // + - * /
};

// server -> client
class Response
{
public:
    Response(){}
    Response(int result, int code) 
        :_result(result), _code(code)
    {}

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

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

    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
        }
        return ok;
    }
    
    ~Response(){}
private:
    int _result; // 运算结果异常,无法区分_result是正常的结果,还是异常值
    int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
};

注意 :我们将操作符反序列化时,使用了asInt方法,其实就是操作符的ASCII码值,所以根据ASCII码值也能拿到操作符


1.3 自定义协议

现在我们结构化的数据已经具备了序列化和反序列化的能力,但是对于自定义协议来说还是不够,为什么这么说呢?

在TCP通信中,由于TCP是流式协议,没有消息边界,因此当我们通过TCP发送多个JSON报文时,接收方可能一次读取到多个报文粘在一起,或者一个报文被拆分成多次接收。

也就是:

  • 粘包:多个 JSON 报文被合并到一个接收缓冲区中

  • 拆包:一个 JSON 报文被拆分成多次接收

那有什么解决办法码?

下面我们分别介绍两种方法在JSON传输中的应用:

方法一:使用长度前缀

发送方先计算JSON字符串的长度,然后将长度转换为固定格式(例如4字节的整数)放在JSON字符串的前面,接收方先读取4字节得到长度,再读取指定长度的JSON字符串。

方法二:使用分隔符

在每个JSON字符串的末尾加上一个特殊的分隔符,例如换行符'\n'。这样,接收方可以一直读取直到遇到换行符,然后解析一个JSON对象。

如果使用长度前缀的话,你读取报文都不能确定是否完整,那你怎么知道你读取的长度前缀就是完整的呢?所以我们可以结合长度前缀和分隔符一起使用,只要读到分隔符,我就能确定读到了一个完整的长度前缀,同时为了保证可读性,我们在报文结尾也加上一个分隔符

封装报文

那么序列化之后的JSON字符串就需要进行封装,报头为JSON字符串的长度加上分隔符,报尾则是分隔符,这样就能封装一个完整的报文

代码如下:

cpp 复制代码
class Protocol
{
public:
    Protocol() {}
    ~Protocol() {}
    
    // 封装报文
    std::string Encode(const std::string &jsonstr)
    {
        // len\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n
        std::string len = std::to_string(jsonstr.size());
        return len + sep + jsonstr + sep;
    }
private:
};

解析报文

服务端拿到一条完整报文之后,肯定需要对报文进行解析,因为我们需要得到JSON字符串,然后对JSON字符串进行反序列化。

代码如下:

cpp 复制代码
const std::string sep = "\r\n";

class Protocol
{
public:
    Protocol() {}
    ~Protocol() {}
    
    // 封装报文
    std::string Encode(const std::string &jsonstr)
    {
        // len\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n
        std::string len = std::to_string(jsonstr.size());
        return len + sep + jsonstr + sep;
    }

    // 解析报文
    // 一条完整报文如: 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n
    // read读取到的内容可能如下:
    // 5
    // 50
    // 50\r
    // 50\r\n
    // 50\r\n{"x": 10, "
    // 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n
    // 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n50\r\n{"x": 10, "y" : 20, "ope
    //.....
    // 
    // 1. 判断报文完整性
    // 2. 如果包含至少一个完整请求,提取他,并移除它,方便处理下一个
    bool Decode(std::string &buffer, std::string *package)
    {
        size_t pos = buffer.find(sep);
        if(pos == std::string::npos)
            return false; // 长度不完整,让调用方继续从内核中读取数据
        // 此时得到json字符串的长度
        std::string package_len_str = buffer.substr(0, pos);
        int package_len = std::stoi(package_len_str);
        // 判断buffer是否有一条完整的报文
        int target_len = package_len_str.size() + package_len + sep.size() * 2;
        if(buffer.size() < target_len)
            return false; // buffer中没有一条完整的报文
        // 这时一定拿到了一条完整报文
        *package = buffer.substr(pos + sep.size(), package_len);
        buffer.erase(0, target_len);
        return true;
    }
private:
};

获取请求报文

我们知道服务端在接受连接之后,需要一边继续监听,一边执行任务(也就是把客户端发送的数据进行处理),这个时候服务端就需要回调出去处理。那在哪处理呢?

要知道我们服务端收到的数据是封装之后的,那我们就需要进行解析报文,然后反序列化,而这些就是我们自定义协议需要做的,所以我们可以在协议中进行处理。当我们处理完之后拿到结构化数据时,就需要交给应用层封装的计算器来执行,这个时候同样也需要回调去应用层处理,应用层处理完回调返回结果,然后将结果进行序列化,封装报文,再发送给客户端。

cpp 复制代码
	void GetRequest(std::shared_ptr<Socket>& socket, InetAddr& client)
    {
        // 从接收缓冲区读取数据
        std::string buffer;
        while(true)
        {
            int n = socket->Recv(&buffer);
            if(n > 0)
            {
                // 解析报文, 提取完整的json字符串,如果不完整,就让服务器继续读取
                std::string json_package;
                while(Decode(buffer, &json_package))
                {
                    LOG(LogLevel::DEBUG) << client.StringAddr() << " 请求: " << json_package;
                    // 将json字符串反序列化
                    Request req;
                    bool ok = req.Deserialize(json_package);
                    if(!ok)
                        continue;
                    // 回调到应用层进行处理
                    Response res = _func(req);

                    // 将结果序列化
                    std::string jsonstr = res.Serialize();

                    // 封装报文
                    std::string message = Encode(jsonstr);

                    socket->Send(message);
                }
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";
                break;
            }
        }
    }

获取应答报文

客户端同样需要对服务端发送的应答报文进行处理

cpp 复制代码
	bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp)
    {
        while (true)
        {
            int n = client->Recv(&resp_buff);
            if (n > 0)
            {

                // 成功
                std::string json_package;
                // 1. 解析报文,提取完整的json请求,如果不完整,就让服务器继续读取
                while (Decode(resp_buff, &json_package))
                {
                    // 2. 反序列化
                    resp->Deserialize(json_package);
                }
                return true;
            }
            else if (n == 0)
            {
                std::cout << "server quit " << std::endl;
                return false;
            }
            else
            {
                std::cout << "recv error" << std::endl;
                return false;
            }
        }
    }

构建请求报文

服务端要想获取请求报文,需要客户端先构建请求报文

cpp 复制代码
	std::string BuildRequestString(int x, int y, char oper)
    {
        // 1. 构建一个完整的请求
        Request req(x, y, oper);

        // 2. 序列化
        std::string json_req = req.Serialize();

        // 3. 添加长度报头
        return Encode(json_req);
    }

2. 应用层封装计算器

因为实现的计算器比较简单,就不多说了

代码如下:

cpp 复制代码
class Cal
{
public:
    Response Execute(Request &req)
    {
        Response resp(0, 0); // code: 0表示成功
        switch (req.Oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());

            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(1); // 1除零错误
            }
            else
            {
                resp.SetResult(req.X() / req.Y());
            }
        }
        break;
        case '%':
        {
            if (req.Y() == 0)
            {
                resp.SetCode(2); // 2 mod 0 错误
            }
            else
            {
                resp.SetResult(req.X() % req.Y());
            }
        }
        break;
        default:
            resp.SetCode(3); // 非法操作
            break;
        }

        return resp;
    }
};

下面补充一下获取结构数据的方法

cpp 复制代码
// client -> server
class Request
{
public:
    Request(){}
    Request(int x, int y, char oper) 
        : _x(x), _y(y), _oper(oper)
    {}

    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper; 

        Json::FastWriter writer;
        std::string s = writer.write(root);
        return s;
    }

    // {"x": 10, "y" : 20, "oper" : '+'}
    bool Deserialize(std::string &in)
    {
        // "10" "20" '+' -> 以空格作为分隔符 -> 10 20 '+'
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
        }
        return ok;
    }

    int X()
    {
        return _x;
    }

    int Y()
    {
        return _y;
    }

    char Oper()
    {
        return _oper;
    }

    ~Request(){}
private:
    int _x;
    int _y;
    char _oper; // + - * /
};

// server -> client
class Response
{
public:
    Response(){}
    Response(int result, int code) 
        :_result(result), _code(code)
    {}

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

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

    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
        }
        return ok;
    }

    void SetResult(int res)
    {
        _result = res;
    }

    void SetCode(int code)
    {
        _code = code;
    }

    void ShowResult()
    {
        std::cout << "计算结果是: " << _result << "[" << _code << "]" << std::endl;
    }
    
    ~Response(){}
private:
    int _result; // 运算结果异常,无法区分_result是正常的结果,还是异常值
    int _code; // 0:sucess, 1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
};

3. 服务端主程序

代码如下:

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

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " port" << std::endl;
}

// ./tcpserver port
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 应用层
    std::unique_ptr<Cal> cal = std::make_unique<Cal>();
    
    // 协议层
    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{
        return cal->Execute(req);
    });
    
    // 服务器层
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
        [&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){
            protocol->GetRequest(sock, client);
    });

    tsvr->Start();
    return 0;
}

所以这三层没有内置在OS中,就是因为这三层由用户决定,用户需要实现什么样的功能------应用层,用户要选择哪种数据格式转换------表示层,用户需要建立通信连接等------会话层。这三层我们已经自己定义实现了。


4. 客户端主程序

代码如下:

cpp 复制代码
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"

using namespace SocketModule;

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}

void GetDataFromStdin(int *x, int *y, char *oper)
{
    std::cout << "Please Enter x: ";
    std::cin >> *x;
    std::cout << "Please Enter y: ";
    std::cin >> *y;
    std::cout << "Please Enter oper: ";
    std::cin >> *oper;
}

// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
    client->BuildTcpClientSocketMethod();

    if (client->Connect(server_ip, server_port) != 0)
    {
        // 失败
        std::cerr << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }

    std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
    std::string resp_buffer;
    // 连接服务器成功
    while (true)
    {
        // 1. 从标准输入当中获取数据
        int x, y;
        char oper;
        GetDataFromStdin(&x, &y, &oper);

        // 2. 构建一个请求-> 可以直接发送的字符串
        std::string req_str = protocol->BuildRequestString(x, y, oper);
        
        // 3. 发送请求
        client->Send(req_str);

        // 4. 获取应答
        Response resp;
        bool res = protocol->GetResponse(client, resp_buffer, &resp);
        if(res == false)
            break;

        // 5. 显示结果
        resp.ShowResult();
    }
    client->Close();
    return 0;
}

运行结果:

可以看到code值为0时表示结果正确,非0表示结果异常。

至此,我们实现了一个简易的网络计算器

相关推荐
byte轻骑兵3 小时前
医疗信创标杆实践:浙人医 LIS 系统异构多活容灾架构深度解析(附 KingbaseES 实战)
网络·架构·1024程序员节
十五年专注C++开发4 小时前
Drogon: 一个开源的C++高性能Web框架
linux·c++·windows·后端开发·服务器开发
西门吹雪@1324 小时前
局域网手机/平板无数据线传输文件-通过网络传输LocalSend
网络·智能手机·电脑
搬砖的小码农_Sky4 小时前
如何从Windows 操作系统登录Linux(Ubuntu)操作系统
linux·windows·ubuntu·远程工作
搬砖的小码农_Sky4 小时前
如何在Linux(Ubuntu)操作系统上查看文件的MD5,SHA256等校验码
linux·运维·ubuntu
码住懒羊羊5 小时前
【Linux】操作系统&进程概念
java·linux·redis
Wang's Blog6 小时前
Linux小课堂: 基于 SSH 的安全文件传输与增量同步机制深度解析之从 wget 到 rsync 的全流程实战
linux·ssh·1024程序员节
yy7634966687 小时前
WPF 之 简单高效的Revit多语言支持方案
java·大数据·linux·服务器·wpf
Heavy sea10 小时前
Linux串口应用编程
linux·c语言·1024程序员节