基于tcp实现自定义应用层协议

认识协议 

协议(Protocol) 是一种通信规则或标准,用于定义通信双方或多方之间如何交互和传输数据。在计算机网络和通信系统中,协议规定了通信实体之间信息交换的格式、顺序、定时以及有关同步等事宜的约定。简易来说协议就是通信双方所约定好的结构化字段。

 ​​​​​

 序列化与反序列化的认识

但在网络传输中由于不同主机和不同系统对于数据存储的大小端差异,所以在传输结构化字段的时候,并不能保证每个结构化的成员数据都能够准确的对应上。

所以一般会采用将结构化的字段内容进行序列化成一个字符串,然后再通过网络发送出去,接收端再将数据反序列化接收,也就是将各个序列化的结构字段数据提取出来。

自定义协议实现网络版本计算器 

在自定义协议时必须让客户端与服务器都能够看到同一份协议字段,这样才能在接收数据的时候按照规定的格式进行准确无误的接收。

对于序列化的字段格式采用空格作为分隔符。而反序列化的时候就可以通过空格分隔符将数据提取出来。

自定义协议(序列化与反序列化和报头)

对于我们实现的协议不仅仅有序列化结构体字段,其实还有包头的封装,因为tcp是有连接的,数据是流式传输的,所以传输过程并不是一发一收的形式,所以报头数据就可以分离每一次的收发,因为报头中有一个字段是存放序列化字段的长度。

#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;

// 模拟定协议

const string sepa = " ";
const string line_break = "\n";

// 添加报头数据和解报头
void Encode(string &mes)
{
    int len = mes.size();
    string ret = to_string(len) + line_break + mes + line_break;
    mes = ret;
}

bool Decode(string &package, string &ret)
{
    //"len\n123 wuwu\n"
    // 先判断收到的数据是否完整
    int len = package.size();
    int pos = package.find(line_break); // 指向第一个换行符
    if (pos == string::npos)
        return false;

    int pos1 = package.find(line_break, pos + line_break.size()); // 指向第二个换行符
    if (pos1 == string::npos)
        return false;

    // 解包后的数据
    ret = package.substr(pos + line_break.size(), pos1 - pos - 1);
    // 去掉被读走的数据
    package = package.substr(pos1 + line_break.size());

    return true;
}

class Request
{
    friend class Cal;

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

    void info()
    {
        cout << _x << ' ' << _oper << ' ' << _y << " = ?" << endl;
    }

    void Serialize(string &out) // 序列化
    {
        out = to_string(_x) + sepa + _oper + sepa + to_string(_y);
    }

    //"x + y"
    void Deserialize(const string s) // 反序列化
    {

        int begin = 0;
        int end = s.find(sepa, begin);
        _x = stoi(s.substr(begin, end - begin));
        begin = end + sepa.size(); // 加的1其实就是' '的长度

        end = s.find(sepa, begin);
        _oper = s.substr(begin, end - begin)[0];
        begin = end + sepa.size();

        _y = stoi(s.substr(begin));

    }

private:
    int _x;
    int _y;
    char _oper;
};

class Response
{
public:
    Response()
    {
    }
    Response(int re, string ret_info)
        : _result(re), _ret_info(ret_info)
    {
    }
    void Info()
    {
        cout << "result = " << _result << " (" << _ret_info << ')' << endl;
    }

    void Serialize(string &out) // 序列化
    {
        out = to_string(_result) + sepa + _ret_info;

    }

    //"_result _ret_info"
    void Deserialize(const string s) // 反序列化
    {

        int begin = 0;
        int end = s.find(sepa, begin);
        _result = stoi(s.substr(begin, end - begin));
        begin = end + sepa.size(); // 加的1其实就是分隔符的长度

        _ret_info = s.substr(begin);

    }

private:
    int _result;      // 保存结果
    string _ret_info; // 结果信息
};

封装套接字

封装套接字就是实现代码分离,使得可读性更高,还有就是省的以后再写。

#pragma once
#include <iostream>
#include <cstdint>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <thread>
#include <functional>
#include <memory>
using namespace std;

#define default_backlog 5

// 设计模式:模版方法类
class my_socket // 抽象类
{
public:
    virtual void Creat_socket() = 0; // 纯虚函数,必须重写
    virtual void Bind(int port) = 0;
    virtual void Listen(int backlog) = 0;
    virtual my_socket *Accept(string &ip, uint16_t &port) = 0;
    virtual void Connect(string ip, uint16_t port) = 0;
    virtual int Get_sockfd() = 0;
    virtual void Close() = 0;

    virtual void Recv(string &ret, int len) = 0;

public:
    void tcpserver_socket(uint16_t port, int backlog = default_backlog)
    {
        Creat_socket();
        Bind(port);
        Listen(backlog);
        // 因为服务会返回的执行accept获取连接,所以选择分离
    }
    void tcpclient_socket(string ip, uint16_t port)
    {
        Creat_socket();
        Connect(ip, port);
    }
};

class tcp_socket : public my_socket // 继承并重写虚函数
{
public:
    tcp_socket()
    {
    }
    tcp_socket(int sockfd)
        : _sockfd(sockfd)
    {
    }
    virtual void Creat_socket()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            cerr << "创建套接字失败" << endl;
            exit(-1);
        }
    }
    virtual void Bind(int port)
    {
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        int n = bind(_sockfd, (sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            cerr << "绑定套接字失败" << endl;
            exit(-1);
        }
    }
    virtual void Listen(int backlog)
    {
        int n = listen(_sockfd, backlog);
        if (n == -1)
        {
            cerr << "监听套接字失败" << endl;
            exit(-1);
        }
    }
    virtual my_socket *Accept(string &ip, uint16_t &port)
    {
        while (1)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int newsockfd = accept(_sockfd, (sockaddr *)&client, &len); // 监听套接字不关闭,可以用来接收多个客户端的连接
            if (newsockfd < 0)
            {
                cerr << "获取连接失败" << endl;
            }

            port = ntohs(client.sin_port);
            char buffer[64];
            inet_ntop(AF_INET, &client.sin_addr, buffer, sizeof(buffer)); // 1.网络转本机 2.4字节ip转字符串ip
            ip = buffer;

            if (newsockfd < 0)
            {
                cerr << "接收套接字失败" << endl;
            }
            else
                cout << "接收套接字成功" << endl;

            return new tcp_socket(newsockfd);
        }
    }
    virtual void Connect(string ip, uint16_t port)
    {
        struct sockaddr_in server;
        server.sin_family = AF_INET;   // socket inet(ip) 协议家族,绑定网络通信的信息
        server.sin_port = htons(port); // 将主机端口号转成网络
        // server.sin_addr.s_addr = inet_addr(ip.c_str()); // 转成网络序列的四字节ip
        inet_pton(AF_INET, ip.c_str(), &server.sin_addr); // 转成网络序列的四字节ip

        int n = connect(_sockfd, (sockaddr *)&server, sizeof(server)); // 自动bind
        if (n != 0)
        {
            cerr << "连接失败" << endl;
            exit(-1);
        }
        else
            cout << "连接成功" << endl;
    }

    virtual int Get_sockfd()
    {
        return _sockfd;
    }

    virtual void Close()
    {
        if (_sockfd > 0)
            close(_sockfd);
    }

    virtual void Recv(string &ret, int len)
    {
        char stream_buffer[len];
        int n = recv(_sockfd, stream_buffer, len - 1, 0);

        if (n > 0)
        {
            stream_buffer[n] = 0;
            ret += stream_buffer; // ret在读取之前可能还有内容残留
        }
        else
        {
            exit(0);
        }
    }

private:
    int _sockfd;
};

计算器代码

计算器类实现的功能就是服务于服务端的,将客户端发送的请求进行计算,并且同时将计算出的结果与返回信息都存到协议中的response类中,所以服务端就可以直接进行序列化,从而将数据发送给客户端。

#pragma once
#include "protocol.h"

class Cal
{
public:
    Cal(Request req)
        : _x(req._x), _y(req._y), _oper(req._oper)
    {
    }

    Response cal()
    {
        switch (_oper)
        {
        case '+':
            _result = _x + _y;
            break;
        case '-':
            _result = _x - _y;
            break;
        case '*':
            _result = _x * _y;
            break;
        case '/':
        {
            if (_y == 0)
                _retinfo = "除数为0,结果无意义";
            else
                _result = _x / _y;
        }
        break;
        case '%':
        {
            if (_y == 0)
                _retinfo = "模0,结果未定义";
            else
                _result = _x % _y;
        }
        break;

        default:
            _retinfo = "结果无效,未录入该运算符";
            break;
        }
        return {_result, _retinfo};
    }

    string Answer()
    {
        return "result = " + to_string(_result) + " ret_info = " + _retinfo;
    }

private:
    int _x;
    int _y;
    char _oper;
    int _result;
    string _retinfo = "结果无误";
};

服务端代码

服务端就是负责接收客户端的信息并进行处理,然后将处理结果发送回去,为了满足多客户端的请求,服务端会采用创建线程的方式来进行与客户端对接,而服务端的主线程就负责实现accept,接受客户端发送的连接请求。

#pragma once
#include "socket.h"
#include "calculate.h"

using func_t = function<void(my_socket *)>; // 执行任务的方法

class tcp_server
{
public:
    tcp_server(uint16_t port, func_t f)
        : _port(port), _sv(new tcp_socket()), _func(f)
    {
        _sv->tcpserver_socket(port);
    }

    void thread_run(my_socket *socket) // 线程执行区域
    {
        _func(socket);

        socket->Close(); // 运行完毕就直接关闭accept返回的套接字描述符
    }
    
    void loop()
    {
        while (1)
        {
            string client_ip;
            uint16_t client_port;
            my_socket *socket = _sv->Accept(client_ip, client_port); // 接收套接字
            cout << "获取新连接,ip= " << client_ip << " port= " << client_port << endl;

            // sleep(3);
            // _sv->Close();//监听套接字就是用来接收多个客户端的连接

            // 创建线程执行任务
            thread t(std::bind(&tcp_server::thread_run, this, placeholders::_1), socket);
            t.detach(); // 线程分离
            // t.join();
        }
    }

    ~tcp_server()
    {
        delete _sv;
    }

private:
    my_socket *_sv;
    uint16_t _port;
    func_t _func;
};
#include "tcp_server.h"
#include "protocol.h"

void deal(my_socket *socket) // 存的套接字描述符就是accept返回值
{
    string buffer;
    while (1)
    {
        // 1.数据读取
        socket->Recv(buffer, 100); // 将每一次序列化的数据都读进buffer里

        string msg;
        string total_info;

        // 2.解包装(将所有独读到的数据都解包,最后完成后一起再发送出去)
        while (Decode(buffer, msg)) // 此时buffer会存在残留数据
        {
            // 3.反序列化buffer,
            Request rq;
            rq.Deserialize(msg);

            // 4.数据读取完毕可以进行处理
            Cal c(rq);
            Response rsp = c.cal(); // 计算结果存到rsp里


            // 5.将处理结果返回给客户端(需要进行序列化和加包)
            string s;
            rsp.Serialize(s);
            Encode(s);
            total_info += s;
        }
        send(socket->Get_sockfd(), total_info.c_str(), total_info.size(), 0); // 任务发送给服务器

    }
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "格式错误\n正确格式:" << argv[0] << " port"
             << endl;
    }
    uint16_t port = atoi(argv[1]);

    // tcp_server tsv(port);
    unique_ptr<tcp_server> tsv(new tcp_server(port, deal));
    tsv->loop(); // accept客户端套接字
}

客户端代码

客户端也同样采用创建线程的方式来进行发送数据与接收数据,这其中一个线程专门发送数据,一个线程专门接收数据,这其中的好处就是不会受到干扰,如果都通过一个线程来完成的话就会导致数据必须是一发一收的方式,并不满足数据流式传输。

#include "socket.h"
#include "protocol.h"

string ops = "+-*/%&|^";

void thread_run(my_socket *clt)
{
    while (1)
    {
        // 1.读取服务端处理后的信息
        string buffer;
        string msg;
        clt->Recv(buffer, 100); // 将每一次序列化的数据都读进buffer里

        // 2.解包装
        Decode(buffer, msg);

        // 3.反序列化msg,
        Response rsp;
        rsp.Deserialize(msg);
        rsp.Info();

        sleep(3);
    }
}
int main(int argc, char *argv[])
{
    srand((unsigned int)time(nullptr));
    if (argc != 3)
    {
        cout << "格式错误\n正确格式:" << argv[0] << " ip"
             << " port" << endl;
    }
    string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    my_socket *clt = new tcp_socket();
    clt->tcpclient_socket(ip, port); // 连接服务端套接字

    //创建线程专门负责接收信息
    thread reciver(thread_run,clt);
    reciver.detach();

    while (1)
    {
        int x = rand() % 100;
        int y = rand() % 100;
        char oper = ops[rand() % ops.size()];
        Request rq(x, y, oper);

        rq.info(); // 向客户端打印任务

        // 1.进行序列化并打包 发送数据
        string s;
        rq.Serialize(s);
        Encode(s);
        send(clt->Get_sockfd(), s.c_str(), s.size(), 0); // 任务发送给服务器

        sleep(1);
    }


    delete clt;
}

认识JSON

JSON是一种成熟的序列化反序列化方案。需要使用的话要安装JSON库

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;
// using namespace Json;

int main()
{
    // Json::Value 万能类型
    Json::Value root;
    root["a"] = 10;
    root["b"] = 20;
    root["哈哈哈"] = "嘎嘎嘎";

    Json::Value tmp;
    tmp["who"] = "cr";
    tmp["age"] = 20;
    root["id"] = tmp;

    // Json::FastWriter writer;//行式风格序列化
    Json::StyledWriter writer;     // 样式风格序列化
    string s = writer.write(root); // 将root结构化字段进行序列化操作
    cout << s << endl;

    cout << "-------------------------------------" << endl;

    // 反序列化
    Json::Value rets;
    Json::Reader reader;
    bool ret = reader.parse(s, root); // 调用反序列化方法,将序列化的数据s反序列到root里
    if (ret)                          // 解析root
    {
        int a = root["a"].asInt();
        int b = root["b"].asInt();
        string st = root["哈哈哈"].asString();
        tmp = root["id"];
        cout << a << ' ' << b << ' ' << st << ' ' << tmp << endl;
    }
}

 ​

目录

认识协议 

 序列化与反序列化的认识

自定义协议实现网络版本计算器 

自定义协议(序列化与反序列化和报头)

封装套接字

计算器代码

服务端代码

客户端代码

认识JSON


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

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

相关文章

分库分表最全详解(图文全面总结)

分库分表 分库分表是数据库设计、和管理中的一种策略&#xff0c;主要解决随着数据量、和并发访问量的增加而带来的性能、和扩展性问题。 分库分表&#xff0c;主要就是两种常用手段&#xff1a;“分库”、和“分表”。 如下图所示&#xff1a; 分库&#xff08;Database S…

牛客NC67 汉诺塔问题【中等 递归 Java/Go/PHP/C++】 lintcode 169 · 汉诺塔

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/7d6cab7d435048c4b05251bf44e9f185 https://www.lintcode.com/problem/169/ 思路 相传在古印度圣庙中&#xff0c;有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上&#xff0c;有三根杆(编号A、B、C…

discuzX2.5的使用心得 札记一

从开始接受php论坛的开发任务&#xff0c;对php感兴趣的我开始迷恋上discuz这个产品了&#xff0c; 像戴志康这样的创新人才&#xff0c;是我们这代人的骄傲和学习的榜样 应该是了解一下&#xff0c;啥事discuzX2.5&#xff0c;百度看一下 discuz x2.5_百度百科 看完百度词条…

国密协议网关与IPSec VPN技术:保障数据安全传输的新途径

国密协议网关IPSec VPN隧道技术是一种结合了国家密码管理局&#xff08;简称国密&#xff09;的加密算法和IPSec VPN隧道技术的安全通信解决方案。 IPSec&#xff08;Internet Protocol Security&#xff09;是互联网协议安全的一种标准&#xff0c;用于保护网络通信的安全性和…

linux系统常用压缩和解压命令

文章目录 Ubuntu 系统中的文件压缩与解压指南一、常用的压缩和解压工具二、tar 工具三、gzip 工具四、bzip2 工具五、zip 和 unzip 工具六、7z 工具乱码批量解压脚本七、总结 Ubuntu 系统中的文件压缩与解压指南 在 Ubuntu 系统中&#xff0c;文件压缩与解压是日常操作中非常常…

vue脚手架与创建vue项目

一、前言 vue脚手架的安装与创建vue项目需要先行安装配置node与npm&#xff0c;详情可以看node、npm的下载、安装、配置_node 下载安装-CSDN博客 二、vue脚手架的使用 1、vue与vue脚手架的版本 Vue脚手架&#xff08;Vue CLI&#xff09;是Vue.js官方提供的一个命令行工具&…

四大策略,五大优势!麒麟信安云助力用户实现VMware替换无忧

2023 年 12 ⽉ 11 ⽇&#xff0c;VMware 正式官宣“所有 VMware by Broadcom 解决⽅案向订阅许可证的过渡&#xff0c;并停⽌销售永久许可证、永久产品的⽀持和订阅&#xff08;SnS&#xff09;续订以及混合购买计划/订阅购买计划积分&#xff08;HPP/SPP&#xff09;”。 202…

2024年电子、电气与信息科学国际会议(EEIS 2024)

2024年电子、电气与信息科学国际会议&#xff08;EEIS 2024&#xff09; 2024 International Conference on Electronics, Electrical and Information Science 【重要信息】 大会地点&#xff1a;昆明 大会官网&#xff1a;http://www.iceeis.com 投稿邮箱&#xff1a;iceeis…

vue数字翻盘,翻转效果

实现数字翻转的效果上面为出来的样子 下面为代码&#xff0c;使用的时候直接引入&#xff0c;还有就是把图片的路径自己换成自己或者先用颜色替代&#xff0c;传入num和numlength即可 <template><div v-for"(item, index) in processedNums" :key"in…

mysql-索引、存储引擎、事务、锁机制和优化

1. MySQL的索引 1.1 概述 索引是通过某种算法&#xff0c;构建出一个数据模型&#xff0c;用于快速找出在某个列中有以特定值的行&#xff0c;不使用索引&#xff0c;MySQL必须从一条记录开始读完整个表&#xff0c;直到找出相关的行&#xff0c;表越大查询数据所花的时间越多…

vue3 使用vant

使用前提&#xff1a; vite创建的vue3项目 vanthttps://vant-ui.github.io/vant/#/zh-CN/home npm i vant 引入样式&#xff1a; main.js import vant/lib/index.css vant封装 import { showLoadingToast,closeToast,showDialog,showConfirmDialog } from vant;export func…

OWASP top10--SQL注入(三、手工注入)

目录 access数据库 手工注入过程&#xff1a; 猜解数据库表名 猜解数据库表名里面的字段 猜解字段内容 SQL注入中的高级查询 mssql数据库 手工注入过程&#xff1a; sa权限 ​编辑dbowner权限 public权限 mysql数据库 1、对服务器文件进行读写操作(前提条件) 需要知…

安全阀检测要求标准:如何提高检测效率与准确性?

安全阀&#xff0c;作为承压设备的重要保护元件&#xff0c;其性能的稳定性和可靠性直接关系到设备的运行安全。 因此&#xff0c;对安全阀进行定期、规范的检测显得尤为重要。接下来&#xff0c;佰德将围绕安全阀的检测要求标准&#xff0c;从检测前准备工作到检测报告与记录…

第十二周 5.21面向对象的三大特性(封装、继承、多态)(二)

三、多态 1.理解: (1)多态:父类型的引用存储不同子类型的对象 父类类名 引用名 new 子类类名(); 引用 对象 父类型 子类型 …

创新融合,5G+工业操作系统引领未来工厂

为加速企业完成生产制造自动化和经营管理自动化&#xff0c;从而走向未来工厂&#xff0c;蓝卓不断探索supOS工业操作系统与前沿技术的的创新融合&#xff0c;而5G技术为工业操作系统提供了更多元化的赋能手段和想象空间。目前&#xff0c;supOS围绕生产、安全、质检、监控等领…

Python代码:十八、生成数字列表

1、描述 牛牛在牛客网系统录入了一连串数字&#xff0c;数字之间依靠逗号隔开&#xff0c;你能帮助他将这些数字存储在列表中吗&#xff0c;列表元素以int的形式。 输入描述&#xff1a; 输入一行整数&#xff0c;数字之间以空格间隔。 输出描述&#xff1a; 输出这些数字…

LuatOS学习

开发顺序 Lua是脚本语言中运行速度最快的语言 资源占用极低 脚本语言运行方式 脚本语言是从上往下一行一行运行的 变量 coun 123456 a,b,c 1,2,3交换 a,b b,a在测试环境中&#xff0c;用print(a,b)打印 nil类型 未声明的变量就是nil&#xff0c;nil用来表示此变量为空…

adb 启动app并查看启动时间

启动app adb shell am start -n 包名/界面名 获取app的启动时长 adb shell am start -W 包名/界面名 要启动一个app 就需要知道其包名与界面名,提前打开一个程序&#xff0c;然后执行以下程序 C:\Users\i5ba0>adb shell dumpsys window windows | findstr mFocusedAppm…

2024年6月PMP考试考前冲刺攻略

调整心态 考场就像战场一样&#xff0c;不仅是实力的较量&#xff0c;更是心理素质的较量。如果感到过于焦虑&#xff0c;可以通过运动等方式来缓解&#xff0c;也可以多与家人、朋友和老师沟通。只有稳定心态才能发挥出最大的实力&#xff01; 高效学习方法 课本是基础&…

vue+echart :点击趋势图中的某一点或是柱状图,出现弹窗,并传输数据

样式 在趋势图中点击某一个柱状图&#xff0c;出现下面的弹窗 代码实现 主要是在趋势图页面代码中&#xff0c;在初始化趋势图的设置中&#xff0c;添加对趋势图监听的点击方法 drawChart() {const chartData this.chartData;let option {};if (!chartData.xData?.len…