网络版计算器

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

协议代码

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

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 指令才行。

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/39793.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity 任意数据在Scene窗口Debug

任意数据在Scene窗口Debug &#x1f354;效果&#x1f96a;食用方法 &#x1f354;效果 如下所示可以很方便的把需要Debug的数据绘制到Scene中&#xff08;普通的Editor脚本只能够对MonoBehaviour进行Debug&#xff09; &#x1f96a;食用方法 &#x1f4a1;. 新建脚本继承Z…

实例018 类似windows xp的程序界面

实例说明 在Windows XP环境下打开控制面板&#xff0c;会发现左侧的导航界面很实用。双击展开按钮&#xff0c;导航栏功能显示出来&#xff0c;双击收缩按钮&#xff0c;导航按钮收缩。下面通过实例介绍此种主窗体的设计方法。运行本例&#xff0c;效果如图1.18所示。 ​编辑…

如何顺势而为,让ChatGPT为教育所用?

恐惧和回避无法阻挡科技的浪潮&#xff0c;教育与AI的深度融合时代已经到来&#xff0c;如何把AI当做工具&#xff0c;把其成为教育的机会而非威胁&#xff0c;是教育体系未来不得不得面对的新变化。 接受ChatGPT作为一种教学辅助工具&#xff0c;成为教师的朋友或者帮手&…

Leetcode每日一题:979. 在二叉树中分配硬币(2023.7.14 C++)

目录 979. 在二叉树中分配硬币 题目描述&#xff1a; 实现代码与解析&#xff1a; dfs&#xff08;后序遍历&#xff09; 原理思路&#xff1a; 979. 在二叉树中分配硬币 题目描述&#xff1a; 给定一个有 N 个结点的二叉树的根结点 root&#xff0c;树中的每个结点上都对…

5.postgresql--COALESCE

在 PostgreSQL 中&#xff0c; COALESCE函数返回第一个非空参数。它通常与 SELECT 语句一起使用以有效处理空值。 COALESCE函数接受无限数量的参数。它返回第一个不为空的参数。如果所有参数都为 null&#xff0c;则 COALESCE函数将返回 null。 COALESCE函数从左到右计算参数&a…

doris恢复库恢复表

今天眼疾手快 不小心删了公司生产环境的表 而且碰巧这个数据没有备份的 当时哥们就呆住 还好doris升级过1.2 刚推出了恢复数据的功能~~~~~这里给老天爷磕一个了~~~~~~ 数据删除恢复 Doris为了避免误操作造成的灾难&#xff0c;支持对误删除的数据库/表/分区进行数据恢复&…

MongoDB初体验-安装使用教程2023.7

前言&#xff1a;博主第一次接触MongoDB&#xff0c;看了一圈网上现有的教程&#xff0c;不是缺少细节就是有问题没交代清楚&#xff0c;特整理了一下自己安装运行的过程&#xff0c;从下载安装到开机自启&#xff0c;全程细节齐全、图文并茂、简单易懂。 目录 1. 从官网下载2…

JavaWeb课程设计项目实战(03)——开发准备工作

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 在正式进入项目开发之前请先完成以下准备工作。 数据库语句 请创建数据库和表并完成数据初始化工作。 初始化数据库 请在MySQL数据库中创建名为studentinformationmanag…

Nacos服务注册和配置中心(Config,Eureka,Bus)1

SCA(Spring Cloud Alibaba)核心组件 Spring Cloud是若干个框架的集合&#xff0c;包括spring-cloud-config、spring-cloud-bus等近20个子项目&#xff0c;提供了服务治理、服务网关、智能路由、负载均衡、断路器、监控跟踪、分布式消息队列、配置管理等领域的解决方案,Spring C…

ADB 命令结合 monkey 的简单使用,超详细

一&#xff1a;ADB简介 1&#xff0c;什么是adb&#xff1a; ADB 全称为 Android Debug Bridge&#xff0c;起到调试桥的作用&#xff0c;是一个客户端-服务器端程序。其中客户端是用来操作的电脑&#xff0c;服务端是 Android 设备。ADB 也是 Android SDK 中的一个工具&…

unity背景缓动动效

这算是一个很常见的小功能&#xff0c;比如我们在玩横版游戏的时候&#xff0c;背景动画会以一定的频率运动&#xff0c;其实现方式也有很多种。 比如&#xff0c;使用UGUI的imageanimtion动画的方式&#xff0c;自己k桢实现。 还可以使用材质球本身的功能来实现&#xff0c;关…

【MySQL】查询进阶

查询进阶 数据库约束约束类型NULL , DEFAULT , UNIQUE 约束主键约束外键约束 聚合查询聚合函数group by子句HAVING 联合查询内连接外连接自连接子查询单行子查询多行子查询 数据库约束 约束类型 NOT NULL #表示某行不能储存空值 UNIQUE #保证每一行必须有唯一的值 DEFAULT #规…

UnxUtils工具包,Windows下使用Linux命令

1. 前言 最近写批处理多了&#xff0c;发现Windows下的bat批处理命令&#xff0c;相比Linux的命令&#xff0c;无论是功能还是多样性&#xff0c;真的差太多了。但有时候又不得不使用bat批处理&#xff0c;好在今天发现了一个不错的工具包&#xff1a;UnxUtils&#xff0c;这个…

【Java/大数据】Kafka简介

Kafka简介 Kafka概念关键功能应用场景 Kafka的原理Kafka 的消息模型早期的队列模型发布-订阅模型Producer、Consumer、Broker、Topic、PartitionPartitionoffsetISR Consumer Groupleader选举Controller leaderPartition leader producer 的写入流程 多副本机制replicas的同步时…

Godot实用代码-存取存档的程序设计

1. Settings.gd 全局变量 用于保存玩家设置 对应Settings.json 2. Data.gd 全局变量 用于保存玩具数据 对应Data.json 实践逻辑指南 1.在游戏开始的时候&#xff08;游戏场景入口的_ready()处&#xff0c; Settings.gd

基于linux下的高并发服务器开发(第一章)- 模拟实现 ls-l 命令

这一小节会用到上面两张图的红色框里面的变量 任务&#xff1a; 模拟实现 ls -l 指令 -rw-rw-r-- 1 nowcoder nowcoder 12 12月 3 15:48 a.txt #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <p…

keepalived 实现双机热备

文章目录 一、说明二、概念解释三、环境准备四、操作过程五、验证 一、说明 我们经常听说 nginx keepalived 双机热备&#xff0c;其实在这里&#xff0c;双机热备只是利用 keepalived 实现两个节点的故障切换&#xff0c;当主节点挂了&#xff0c;备用节点顶上&#xff0c;保…

基于51单片机和proteus的电流采集系统

此系统是基于51单片机和proteus的仿真设计&#xff0c;功能如下&#xff1a; 1. LCD1602实时显示获取到电流值及设定值。 2. 按键可调整电流设定值。 3. 电流值过高则蜂鸣器报警。 4. 指示灯指示电流及系统状态。 5. 系统信息可通过串口实时更新。 功能框图如下&#xff1…

javaee jstl表达式

jstl是el表达式的扩展 使用jstl需要添加jar包 package com.test.servlet;import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;import javax.servlet.ServletException; import javax.servlet…

【Java基础教程】Java学习路线攻略导图——史诗级别的细粒度归纳,持续更新中 ~

Java学习路线攻略导图 上篇 前言1、入门介绍篇2、程序基础概念篇3、包及访问权限篇4、异常处理篇5、特别篇6、面向对象篇7、新特性篇8、常用类库篇 前言 &#x1f37a;&#x1f37a; 各位读者朋友大家好&#xff01;得益于各位朋友的支持和关注&#xff0c;我的专栏《Java基础…