网络版计算器

        本次我们实现一个服务器版本的简单的计算器,通过自己在应用层定制协议来将数据传输出去。

协议代码

        此处我们为了解耦,采用了两个类,分别表示客户端的请求和服务端的响应。

Request
class Request
{
public:
    Request()
    {
    }
    Request(int x, int y, char op)
        : _x(x), _y(y), _op(op)
    {
    }

    // 序列化
    bool serialize(string &out)
    {
        out += to_string(_x);
        out += SEP;
        out += _op;
        out += SEP;
        out += to_string(_y);
        return true;
    }
    // 反序列化 x + y
    bool deserialize(const string &in)
    {
        string x_str, y_str;
        int left = in.find(SEP);
        if (left == string::npos)
            return false;

        int right = in.rfind(SEP);
        if (right == string::npos)
            return false;

        x_str = in.substr(0, left);
        y_str = in.substr(right + 1);
        _x = stoi(x_str);
        _y = stoi(y_str);
        _op = in[left + SEP_LEN];
        return true;
    }

    int _x;
    int _y;
    char _op;
};
Response
class Response
{
public:
    Response()
        : exitcode(0), result(0)
    {
    }
    // 序列化
    bool serialize(string &out)
    {
        out.clear();
        out += to_string(exitcode);
        out += SEP;
        out += to_string(result);
    }
    // 反序列化
    bool deserialize(const string &in)
    {
        // exitcode result
        string exit_str, result_str;
        int pos = in.find(SEP);

        if (pos == string::npos)
            return false;

        exit_str = in.substr(0, pos);
        result_str = in.substr(pos + SEP_LEN);
        if (exit_str.empty() || result_str.empty())
            return false;

        exitcode = stoi(exit_str);
        result = stoi(result_str);
    }

    int exitcode;
    int result;
};

        而对于两个类来说,它们最重要的就是序列化和反序列化的过程。

        对于请求类,它需要将所输入的数据序列化转化成为字符串转化的字符串就是一整个报文,便于后续添加报头;而它的反序列化就需要将报文重新转化为数据,便于后续的计算任务进行。

        对于响应类,它需要将所得到的结果序列化转化为字符串;反序列化则是将报文转化为数据。

        当数据序列化后就需要添加报头,而反序列化的任务则需要在去除报头后进行。

添加和去除报头

#define SEP " "
#define SEP_LEN strlen(SEP)
#define SEP_LINE "\\r\\n"
#define SEP_LINE_LEN strlen(SEP_LINE)

// 报头格式 text_len/r/ntext/r/n
// 添加报头

bool Enlength(const string &in, string &out)
{
    out.clear();
    out += to_string(in.size());
    out += SEP_LINE;
    out += in;
    out += SEP_LINE;
    return true;
}
// 去掉报头
// 去掉后 只剩 text
bool Delength(const string &in, string &out)
{
    int pos = in.find(SEP_LINE);
    if (pos == string::npos)
        return false;
    int text_len = stoi(in.substr(0, pos));
    out = in.substr(pos + SEP_LINE_LEN, text_len);
    return true;
}

        此处我们的报头的格式就是 "text_len\r\ntext\r\n" 的格式。 

        这Enlength函数可以将报文转化为数据包,Delength可以将数据包转化为报文

 接收函数

// 接收的是一个数据 是 text_len/r/ntext/r/n的形式
bool Recv(int socket, string &inbuffer, string &pacakge)
{
    char buffer[1024];
    while (true)
    {
        int n = recv(socket, buffer, sizeof(buffer) - 1, 0);
        if (n > 0)
        {
            // 开始接收
            buffer[n] = 0;
            inbuffer += buffer;
            int pos = inbuffer.find(SEP_LINE);
            if (pos == string::npos)
                continue;
            int text_len = stoi(inbuffer.substr(0, pos));
            string len_string = inbuffer.substr(0, pos);
            int total_len = text_len + len_string.size() + SEP_LINE_LEN * 2;
            if (inbuffer.size() < total_len)
                continue; // 说明没有一个完整的报文

            // 此时就说明至少有一个完整的报文
            pacakge = inbuffer.substr(0, total_len + 1);
            inbuffer.erase(total_len);

            break;
        }
        else
        {
            return false;
        }
    }
    return true;
}

        此外,由于TCP是字节流的协议,我们需要自定义函数来保证收到了至少一个完整的报文。 

计算函数 

void CalHandler(const Request &rq, Response &rp)
{
    switch (rq._op)
    {
    case '+':
        rp.result = rq._x + rq._y;
        break;
    case '-':
        rp.result = rq._x - rq._y;
        break;
    case '*':
        rp.result = rq._x * rq._y;
        break;
    case '/':
        if (rq._y == 0)
        {
            rp.exitcode = 1;
        }
        else
        {
            rp.result = rq._x / rq._y;
        }
        break;
    case '%':
        if (rq._y == 0)
        {
            rp.exitcode = 1;
        }
        else
        {
            rp.result = rq._x % rq._y;
        }
        break;
    }
}

服务器

        首先直接看看服务器的代码。

calsever.hpp

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include "protocol.hpp"

using namespace std;

#define gbacklog 5

class Sever;

class Sever
{
public:
    Sever(const uint16_t &port)
        : _port(port), _listensocket(-1)
    {
    }

    void InitSever()
    {
        _listensocket = socket(AF_INET, SOCK_STREAM, 0); // TCP是面向字节流的协议

        // bind该socket
        struct sockaddr_in peer;
        peer.sin_family = AF_INET;
        peer.sin_port = htons(_port);
        peer.sin_addr.s_addr = INADDR_ANY;
        if (bind(_listensocket, (sockaddr *)&peer, sizeof(peer)) < 0)
        {
            cout << "bind err!" << endl;
            exit(-1);
        }
        cout << "bind success!" << endl;

        // 监听该socket
        if (listen(_listensocket, gbacklog) < 0)
        {
            cout << "listen err!" << endl;
            exit(-1);
        }
        cout << "listen success!" << endl;
    }

    void start()
    {
        while (true)
        {
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer);
            int socket = accept(_listensocket, (sockaddr *)&peer, &len);

            if (socket < 0)
            {
                cout << "socket err" << endl;
                exit(-1);
            }

            cout << "accept success socket : " << socket << endl;

            // version 2.1 多进程版
            pid_t id = fork();
            if (id == 0) // 子进程内部
            {
                close(_listensocket); // 子进程不用监听,父进程监听即可
                if (fork() > 0)
                    exit(-1); // 直接让子进程创建孙子进程,然后将子进程退出,让孙子进程被领养
                handlerEnter(socket);
                close(socket);

            } // 但是父进程不用等待,否则会造成串行
        }
    }

    void handlerEnter(int socket)
    {
        string inbuffer;
        while (true)
        {

            // 读取数据
            string recv_text,recv_pacakge;
            while(!Recv(socket,recv_text,recv_pacakge))
            {
            }
            //去报头
            if(!Delength(recv_pacakge,recv_text))
            return;
            Request rq;
            // 反序列化
            if(!rq.deserialize(recv_text))
            {
                return;
            }
            // 计算公式
            Response rp;
            CalHandler(rq,rp);
            // 将结果序列化
            string send_text,send_pacakge;
            rp.serialize(send_text);
            //添加报头
            Enlength(send_text,send_pacakge);
            // 发送结果
            send(socket,send_pacakge.c_str(),send_pacakge.size(),0);
        }
    }

private:
    uint16_t _port;
    int _listensocket;
};

calsever.cc 

#include "calSever.hpp"
#include <memory>

using namespace std;

int main(int args, char *argv[])
{
    if (args != 2) // 在运行时必须带有端口号
    {
        cout << " ./Sever port" << endl;
        exit(-1);
    }

    uint16_t port = atoi(argv[1]);

    unique_ptr<Sever> TCPSever(new Sever(port));
    
    TCPSever->InitSever();
    TCPSever->start();

    return 0;
}

        对于服务器的代码,并没有太大的改动,主要是服务器所需要进行的任务的代码需要了解。

        

         首先是接收客户端发过来的数据,由于此处使用的是TCP协议进行传输,所以可能接受的数据包不是一个完整的数据包,因此需要利用循环和自定义的函数进行检测。

        而该函数的逻辑大致为 接收到数据包——去掉报头——对报文进行反序列化——对获取的数据进行计算——将结果序列化——对报文添加报头——发送数据包。

        整体逻辑十分简单,但前提是函数需要正确使用。

客户端

calclient.hpp

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include"protocol.hpp"

using namespace std;

class Client
{

public:
    Client(const uint16_t &port, const string &ip)
        : _severport(port), _severip(ip), _socket(-1)
    {
    }

    void InitClient()
    {
        _socket = socket(AF_INET, SOCK_STREAM, 0);
        if (_socket < 0)
        {
            cout << "socket failed" << endl;
        }
    }

    void run()
    {
        struct sockaddr_in peer;
        bzero(&peer, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(_severport);
        peer.sin_addr.s_addr = inet_addr(_severip.c_str());

        if (connect(_socket, (sockaddr *)&peer, sizeof(peer)) == -1)
        {
            cout << "connect failed" << endl;
            exit(-1);
        }
        else
        {
            // 输入所需要计算的公式
            string inbuffer;
            while (true)
            {
                int x, y;
                char op;
                cout << "请输入第一个数据 :";
                cin >> x;
                cout << "请输入第二个数据 :";
                cin >> y;
                cout << "请输入计算方式 :";
                cin >> op;

                string send_text,send_pacakge;
                Request rq(x,y,op);

                // 进行序列化以及添加报头
                rq.serialize(send_text);
                Enlength(send_text,send_pacakge);

                // 发送公式
                send(_socket,send_pacakge.c_str(),send_pacakge.size(),0);

                // 接收结果

                string recv_text,recv_pacakge;
                while(!Recv(_socket,inbuffer,recv_pacakge))
                {
                    //若是接收失败就重试
                }

                //此时已经接收到结果
                // 对结果进行反序列化并输出

                Response rp;
                //去掉报头
                Delength(recv_pacakge,recv_text);
                rp.deserialize(recv_text);

                if(rp.exitcode != 0)
                cout<<"err ! exitcode : "<<rp.exitcode<<endl;
                else 
                {
                    cout<<"The result : "<<rp.result<<endl;
                }
            }
        }
    }

private:
    uint16_t _severport;
    string _severip;
    int _socket;
};

calclient.cc

#include"calClient.hpp"
#include<memory>
using namespace std;

void Remind()
{
    cout<<"./Client Severport Severip"<<endl;
}

int main(int args,char* argv[])
{

    if(args!=3)
    {
        Remind();
        exit(-1);
    }

    uint16_t port = atoi(argv[1]);
    string ip = argv[2];

    unique_ptr<Client> TCPClient(new Client(port,ip));

    TCPClient->InitClient();
    TCPClient->run();

    return 0;
}

        对于客户端而言,它的工作和服务器大差不差。 

         具体逻辑是 获取数据——序列化——添加报头——传输数据包——接收数据包——去掉报头——反序列化——获取结果。       

        接下来就来直接看看结果。

        

 

         能看到我们确实成功的运行并计算出结果。

        但是我们可以看到,由我们自己定义协议十分麻烦,这里是协议很小,当我们的项目很大的时候,也许协议就不止这么点了,因此我们有更简单的方法来定制协议。 

更简单的方案:

  • json
  • protobuf
  • xml

json方案的序列化和反序列化 

class Request
{
public:
    Request()
    {
    }
    Request(int x, int y, char op)
        : _x(x), _y(y), _op(op)
    {
    }

    // 序列化
    bool serialize(string &out)
    {
        #ifdef MYSELF
        out += to_string(_x);
        out += SEP;
        out += _op;
        out += SEP;
        out += to_string(_y);
        #else
            Json::Value root;
            root["first"] = _x;
            root["second"] = _y;
            root["operator"] = _op;

            Json::FastWriter w;
            out = w.write(root);  
        #endif
        return true;
    }
    // 反序列化 x + y
    bool deserialize(const string &in)
    {
        #ifdef MYSELF
        string x_str, y_str;
        int left = in.find(SEP);
        if (left == string::npos)
            return false;

        int right = in.rfind(SEP);
        if (right == string::npos)
            return false;

        x_str = in.substr(0, left);
        y_str = in.substr(right + 1);
        _x = stoi(x_str);
        _y = stoi(y_str);
        _op = in[left + SEP_LEN];
        #else 
        Json::Value root;
        Json::Reader r;
        r.parse(in,root);
        _x = root["first"].asInt();
        _y = root["second"].asInt();
        _op = root["operator"].asInt();

        
        #endif
        return true;

    }

    int _x;
    int _y;
    char _op;
};

class Response
{
public:
    Response()
        : exitcode(0), result(0)
    {
    }
    // 序列化
    bool serialize(string &out)
    {
        #ifdef MYSELF
        out.clear();
        out+=to_string(exitcode);
        out+=SEP;
        out+=to_string(result);
        #else 
        Json::Value root;
        root["exitcode"] = exitcode;
        root["result"] = result;
        Json::FastWriter w;
        out = w.write(root);
        #endif  
        return true;
    }
    // 反序列化
    bool deserialize(const string &in)
    {
        #ifdef MYSELF
        //exitcode result
        string exit_str,result_str;
        int pos = in.find(SEP);

        if(pos == string::npos)
            return false;

        exit_str = in.substr(0,pos);
        result_str = in.substr(pos+SEP_LEN);
        if(exit_str.empty()||result_str.empty())
            return false;
        
        exitcode = stoi(exit_str);
        result = stoi(result_str);
        #else
        Json::Value root;
        Json::Reader r;
        r.parse(in,root);
        exitcode = root["exitcode"].asInt();
        result = root["result"].asInt();
 

        #endif
        return true;

    }

    int exitcode;
    int result;
};

         我们能够看到确实成功了。

        不过使用json需要一点小小的准备工作。

  • 下载对应的工具

        可以用root账号或者sudo进行下载。

        yun install -y jsoncpp-devel

        
  •  包含头文件

  •  编译添加新指令

        只要是使用了json的文件,就需要添加 -ljsoncpp 指令才行。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值