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

news2024/12/26 22:56:50

认识协议 

协议(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


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络工程师---第三十八天

ISIS&#xff1a; ISIS含义&#xff1a;中间系统到中间系统IS-IS。 ISIS特点&#xff1a;①内部网关协议IGP&#xff08;Interior Gateway Protocol&#xff09;&#xff0c;用于自治系统内部&#xff1b; ②IS-IS也是一种链路状态协议&#xff0c;使用最短路径优先SPF算法进…

电子阅览室在管理时需注意什么

关于如今的绝大多数人来说&#xff0c;想必都听说过“电子阅览室”这一概念。它首要运用在校园中&#xff0c;给学生们供给愈加丰厚的常识储藏。它也是一个独立的局域网&#xff0c;在校园网络中作为重要的一个组成部分而存在。但是&#xff0c;一个好的电子阅览室是需求满意运…

python文件IO基础知识

目录 1.open函数打开文件 2.文件对象读写数据和关闭 3.文本文件和二进制文件的区别 4.编码和解码 读写文本文件时 读写二进制文件时 5.文件指针位置 6.文件缓存区与flush()方法 1.open函数打开文件 使用 open 函数创建一个文件对象&#xff0c;read 方法来读取数据&…

Docker学习(4):部署web项目

一、部署vue项目 在home目录下创建项目目录 将打包好的vue项目放入该目录下&#xff0c;dist是打包好的vue项目 在项目目录下&#xff0c;编辑default.conf 内容如下&#xff1a; server {listen 80;server_name localhost; # 修改为docker服务宿主机的iplocation / {r…

[JAVASE] 类和对象(六) -- 接口(续篇)

目录 一. Comparable接口 与 compareTo方法 1.1 Comparable接口 1.2 compareTo方法的重写 1.2.1 根据年龄进行比较 1.2.2 根据姓名进行比较 1.4 compareTo 方法 的使用 1.3 compareTo方法的缺点(重点) 二. Comparator接口 与 compare方法 2.1 Comparator接口 2.2 compare 方法…

使用AWR对电路进行交流仿真---以整流器仿真为例

使用AWR对电路进行交流仿真—以整流器仿真为例 生活不易&#xff0c;喵喵叹气。马上就要上班了&#xff0c;公司的ADS的版权紧缺&#xff0c;主要用的软件都是NI 的AWR&#xff0c;只能趁着现在没事做先学习一下子了&#xff0c;希望不要裁我。 本AWR专栏只是学习的小小记录而…

2024.5.25期末测试总结

成绩&#xff1a; 配置&#xff1a; 可能与实际有些出入 题目&#xff1a; 第一题&#xff1a; 代码思路&#xff1a; 一道模拟题&#xff0c;按照公式计算出sumpow(2,i)&#xff0c;判断sum>H&#xff0c;输出 代码&#xff1a; #include<bits/stdc.h> using name…

LiveGBS流媒体平台GB/T28181用户手册-基础配置:信令服务配置、流媒体服务配置、白名单、黑名单、更多配置

LiveGBS流媒体平台GB/T28181用户手册-基础配置:信令服务配置、流媒体服务配置、白名单、黑名单、更多配置 1、基础配置1.1、信令服务配置1.2、白名单1.3、黑名单1.4、流媒体服务配置 2、搭建GB28181视频直播平台 1、基础配置 LiveGBS相关信令服务配置和流媒体服务配置都在这里…

Spark运行模式详解

Spark概述 Spark 可以在多种不同的运行模式下执行&#xff0c;每种模式都有其自身的特点和适用场景。 部署Spark集群大体上分为两种模式&#xff1a;单机模式与集群模式。大多数分布式框架都支持单机模式&#xff0c;方便开发者调试框架的运行环境。但是在生产环境中&#xff…

机器人支持回调接口配置(详细教程)

大家伙&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。 一、前言 今天&#xff0c;给大家介绍一下&#xff0c;如何在机器人中配置回调地址和接口编写。很多时候我们可能有这样的场景&#xff0c;收到消息后&#xff0c;想自己处理一下消息的内…

【微服务】springboot 构建镜像多种模式使用详解

目录 一、前言 二、微服务常用的镜像构建方案 3.1 使用Dockerfile 3.2 使用docker plugin插件 3.3 使用docker compose 编排文件 三、环境准备 3.1 服务器 3.2 安装JDK环境 3.2.1 创建目录 3.2.2 下载安装包 3.2.3 配置环境变量 2.2.4 查看java版本 3.3 安装maven …

MySQL中, 自增主键和UUID作为主键有什么区别?

首先我们来看看, 存储自增主键和uuid的数据类型 我们知道, mysql中作为主键的通常是int类型的数据, 这个 数据从第一条记录开始, 从1开始主键往后递增, 例如我有100条数据, 那么根据主键排序后, 里面的记录从上往下一次就是1, 2, 3 ... 100, 但是UUID就不一样了, UUID是根据特殊…

HTTP协议、URL、HTTPS协议 ----- 讲解很详细

本章重点 理解应用层的作用, 初识HTTP协议 了解HTTPS协议 一、HTTP协议 1.认识url 虽然我们说&#xff0c;应用层协议是我们程序猿自己定的&#xff0c;但实际上&#xff0c;已经有大佬们定义了一些现成的&#xff0c;又非常好用的应用层协议&#xff0c;供我们直接参考使…

明星IP切片带货爆单营,0基础搞定IP切片带货短视频(69节课)

把握带货趋势&#xff0c;了解切片流程&#xff0c;剪辑带货创收营 课程目录&#xff1a; 01第一章实操链路-第一节IP选择.mp4 02第一章实操链路-第二节账号准备.mp4 03第一章实操链路-第四节开通权限.mp4 04第一章实操链路-第五节货品准备.mp4 05第一章实操链路-第六节素…

AI重塑保险业未来:机器学习在风险评估、欺诈检测与客户服务中的深度应用

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

3d数字化虚拟交互展厅让您紧跟时代的步伐

虚实融合打破边界&#xff0c;北京VR虚拟数字展厅搭建让体验者彷如置身于一部三维电影中&#xff0c;可以对场景中的物体、角色、模型进行自由参观和体验&#xff0c;并且系统支持随时更新&#xff0c;让您紧跟时代的步伐&#xff0c;领略更新的展览风采。 除了常见的科普培训&…

OpenStack平台Nova管理

1. 规划节点 使用OpenStack平台节点规划 IP主机名节点192.168.100.10controller控制节点192.168.100.20compute计算节点 2. 基础准备 部署的OpenStack平台 1. Nova运维命令 &#xff08;1&#xff09;Nova管理安全组规划 安全组&#xff08;security group&#xff09;是…

Neural Networks and Deep Learning环境搭建

1.进入Anaconda prompt 2.创建虚拟环境 &#xff08;1&#xff09;最简单的创建 python 虚拟环境的命令是&#xff1a; conda create -n your_env_name # your_env_name 为你虚拟环境名&#xff08;2&#xff09;我在这里创建一个名为&#xff1a;deep_study的 python2.7版…

NextJs 初级篇 - 安装 | 路由 | 中间件

NextJs 初级篇 - 安装 | 路由 | 中间件 一. NextJs 的安装二. 路由2.1 路由和页面的定义2.2 布局的定义和使用2.3 模板的定义和使用① 模板 VS 布局② 什么是 use client 2.4 路由跳转的方式2.5 动态路由2.6 路由处理程序① GET 请求的默认缓存机制② 控制缓存或者退出缓存的手…

IntelliJ IDEA实用插件:轻松生成时序图和类图

IntelliJ IDEA生成时序图、类图 一、SequenceDiagram1.1 插件安装1.2 插件设置1.3 生成时序图 二、PlantUML Integration2.1 插件安装2.2 插件设置2.3 生成类图 在软件建模课程的学习中&#xff0c;大家学习过多种图形表示方法&#xff0c;这些图形主要用于软件产品设计。在传统…