网络协议原理

news2025/1/15 8:31:27

文章目录

  • TCP通信原理
  • TCP与UDP的对比
  • 应用层
    • 应用层协议 --- tcp
    • 协议定制
      • 直接传递对象
      • 自定义协议
      • 现在要解决的问题
      • 业务处理
  • json的使用
    • 使用json进行序列化和反序列化操作
  • 总结

TCP通信原理

tcp是面向字节流的
同时他也是面向连接的
所以TCP的服务器编写代码如图所示:
客户端的编写代码如图:
在这里插入图片描述

tcp在进行connect的时候, 会先进行三次链接, 交换报文, 这一过程叫做TCP的三次握手
这一过程就是客户端的SYN_SENT到服务器的RSTABLISHED过程

而TCP_server在进行, accept的时候, 前提必须是把获取好的连接传输给应用层, 得到新的文件描述符这样才能进行使用

在server的ESTABLISED这一步中, accept会进行返回, 分配新的文件描述符connfd和客户端进行通信
而也在这时表示是握手成功, 需要注意

在connect时只发起一次, 这是一次系统调用, 另外两次由双方的OS自动完成握手的操作
connect与accept可能会发生阻塞, 主要是因为, 他在等待三次握手的链接完成
建立链接, 实际是建立共识, 后续展开~~~~
不完成三次握手, 就不会有后续的整个过程
建立链接完成, 实际上是双方为了维护一条链接, 创建的描述链接的结构体字段

而在udp 的客户端, 没有connect的这一步, 而是直接发送数据

TCP在断开链接的时候, 实际上就是完成了四次挥手.

在这里插入图片描述
其中在client的FIN_WAIT_1的字段表示, 客户端首次发起请求断开的链接
CLOSR_WAIT表示服务器做出相应, 同一断开链接的请求
LAST_ACK表示, 服务端也向客户端发送请求断开链接

而他们的挥手都是close()引起的
这一过程是全双工的
整个过程像是离婚, 双方都要进行确认

TCP与UDP的对比

说到底, 他们无非就是
可靠传输VS不可靠传输
有连接VS无连接
字节流VS数据报

应用层

应用层协议 — tcp

协议本身是一种约定, TCP是面向字节流的, 传输需要正确书写, 现在将重点放在读取的位置处, 我们进行网络版本的计算器的完成(client进行数据的请求, 实际的数据的处理工作由server进行处理)

  1. 在我们的日常通信中, 给对方发送消息包含性别时间, 消息内容等的相关的信息, 但是不能都发送, 而是打包成一体进行发送, 这一过程就会进行序列化的操作
  2. 到我们发送过去的时候, 会将这个一体的信息进行解析, 这是又会变为初始状态的信息的状态. 从一体的状态变成单个的信息状态, 这个过程就是反序列化

那么协议概念也就如图所示了:

在这里插入图片描述
也就是说, 这样的结构化的字段就能被双方进行识别, 双方使用同一种数据类型, 能对指定的字段进行解释, 这是一种约定, 也就是协议
当然, 在应用层不推荐使用结构化字段进行数据的存储和转发到对应的机器, 因为目的机器可能OS不同, 像结构体的内存对齐, 不同类型的指针等的大小都会不同, 所以一般使用序列化处理之后的结果进行转发和处理
那么TCP就是经过序列化处理之后形成的字节流进行转发, 发送过去之后, 进行反序列化得到结构化字段再进行读取数据
序列化:就是将协议对应的结构化字段, 转化成"字符串"的字节流
反序列化: 将"字符串"的字节流, 转化为结构化数据
为什么要这样做呢?
为了方便网络发送
为什么不直接使用传struct的方式呢?而却要在应用层, 使用序列化的方式传递字节流?

  1. 应用层变化快, 因为机器不同导致的问题比较多, 所以更适合使用序列化转化的方式.
  2. 比较好扩展, 未来如果修改协议字段, 序列化的传送也不影响.
  3. Linux内核的各种协议字段都是控制好的, 所以该OS内的进程之间通信不需要使用序列化
  4. 可以跨语言进行使用, 客户端和服务端在未来可能是不同的语言进行写的, 不同的语言使用序列化API之后, 可以响应为对应语言正确的结构化字段

协议定制

直接传递对象

		// 封装一个基类, 用于表示一个socket接口类
		class Socket {
		public:
		    Socket();
		    virtual  ~Socket(){}
		    virtual void CreateSocketOrDie() = 0;// 创建或者死亡
		    virtual void BindSocketOrDie(uint16_t port) = 0;// 绑定或者死亡
		    virtual void ListenSocketOrDie(int backlog) = 0;// 监听或者死亡 这个参数为listen函数的backlog参数,后续讲
		    virtual Socket* AcceptSocketOrDie(std::string *peerip, uint16_t *peerport) = 0;// 接受或者死亡 输出型参数, 获得远端ip和端口
		    virtual void ConnectSocketOrDie(const std::string& serverip, uint16_t serverport) = 0;// 连接或者死亡
		    virtual void CloseSocketOrDie() = 0;// 关闭或者死亡
		    virtual int GetSocketFd() const = 0;// 获取socket文件描述符 
		    virtual void SetSocketFd(int sockfd) = 0;// 设置socket文件描述符
		public:
		    void BuildListenSocket(uint16_t port, int backlog)// 创建一个监听socket
		    {
		        CreateSocketOrDie();// 创建socket
		        BindSocketOrDie(port);// 绑定端口
		        ListenSocketOrDie(backlog);// 监听
		    }
		    void BuildConnectSocket(const std::string& serverip, uint16_t serverport)// 创建一个连接socket
		    {
		        CreateSocketOrDie();// 创建socket
		        ConnectSocketOrDie(serverip, serverport);// 连接
		    }
		    void BuildNormalSocket(int sockfd)// 创建一个普通socket
		    {
		        SetSocketFd(sockfd);
		    }
		};

TcpServerMain.cc

#include "Protocol.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include <memory>
#include <unistd.h>
#include <iostream>

using namespace Net_Work;

void HandlerRequest(Socket *sockp)
{
    std::cout << "in handler_request" << std::endl;
}

// ./TcpServer port
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
        return 1;
    }
    uint16_t localport = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));
    svr->Loop();

    return 0;
}

TcpServer.hpp

#pragma once

#include "Socket.hpp"
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
#include <functional>

using func_t = std::function<void(Net_Work::Socket* sockp)>;

class TcpServer;

class THreadData
{
public:
    THreadData(TcpServer *tcp_this, Net_Work::Socket *sockp) : _this(tcp_this), _sockp(sockp)
    {}
public:
    TcpServer *_this;
    Net_Work::Socket *_sockp;
};

class TcpServer{
public:
    TcpServer(uint16_t port, func_t handler_request) : _port(port), _listenSock(new Net_Work::TcpSocket()), _handler_request(handler_request)
    {
        _listenSock->BuildListenSocketMethod(_port, Net_Work::backlog);
    }
    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self());// 分离线程
        THreadData *td = static_cast<THreadData*>(args);
        td->_this->_handler_request(td->_sockp);
        
        td->_sockp->CloseSocket();// 关闭套接字
        delete td->_sockp;
        delete td;
        return nullptr;
    }
    void Loop()// 服务器的主循环
    {
        while(true)
        {
            std::string peerip;
            uint16_t peerport;
            // 获取套接字
            Net_Work::Socket* newsock = _listenSock->AcceptSocketOrDie(&peerip, &peerport);// 接受一个连接
            if(newsock == nullptr)// 如果接受失败, 则继续循环
            {
                std::cout << "AcceptSocketOrDie failed" << std::endl;
                continue;
            }
            std::cout << "Get a new connection, sockfd is : " << newsock->GetSocketFd() << ", peer ip: " << peerip << ", peer port: " << peerport << std::endl;
            // 使用线程来处理连接
            pthread_t tid;
            THreadData *td = new THreadData(this, newsock);
            pthread_create(&tid, nullptr, ThreadRun, (void*)td);
        }
    }
    ~TcpServer()
    {
        delete _listenSock;
    }
private:
    int _port;
    Net_Work::Socket *_listenSock;// 创建一个监听socket
public:
    func_t _handler_request;// 处理连接的函数
};

// 至此, 服务端的代码已经完成, 主要功能在HandlerRequest函数中, 该函数需要用户自己实现
// 服务端只是一个框架, 用户需要自己实现HandlerRequest函数来处理连接

TcpClientMain.cc

#include "Protocol.hpp"
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
// ./TcpClient Serverip Serverport
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " <Serverip> <Serverport>" << std::endl;
        return 1;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    Net_Work::Socket* sock = new Net_Work::TcpSocket();
    if(!sock->BuildConnectSocketMethod(serverip, serverport))
    {
        std::cerr << "Connect " << serverip << ":" << serverport << " failed" << std::endl;
        return 1;
    }
    std::cout << "Connect " << serverip << ":" << serverport << " success" << std::endl;
    
    std::string message = "Hello, Server"; 
    write(sock->GetSocketFd(), message.c_str(), message.size());
    
    sock->CloseSocket();
    return 0;
}

Socket.hpp

#pragma once 
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
    
#define Convert(addrptr) ((struct sockaddr*)addrptr) // 这个宏用于将sockaddr_in 转换为struct sockaddr*

namespace Net_Work
{
    const static int defaultSockfd = -1;
    const static int backlog = 5;

    enum{
        Socket_Error = 1,
        Bind_Error,
        Listen_Error
    };

    // 封装一个基类, 用于表示一个socket接口类
    // 设计模式: 模板方法模式, 未来不管是什么socket, 都继承这个类, 实现这些接口, 这样就可以复用这些接口, 这个流程是固定的
    class Socket {
    public:
        virtual  ~Socket(){}
        virtual void CreateSocketOrDie() = 0;// 创建或者死亡
        virtual void BindSocketOrDie(uint16_t port) = 0;// 绑定或者死亡
        virtual void ListenSocketOrDie(int backlog) = 0;// 监听或者死亡 这个参数为listen函数的backlog参数,后续讲
        virtual Socket* AcceptSocketOrDie(std::string *peerip, uint16_t *peerport) = 0;// 接受或者死亡 输出型参数, 获得远端ip和端口
        virtual bool ConnectSocketOrDie(const std::string& serverip, uint16_t serverport) = 0;// 连接或者死亡
        virtual int GetSocketFd() const = 0;// 获取socket文件描述符 
        virtual void SetSocketFd(int sockfd) = 0;// 设置socket文件描述符
        virtual void CloseSocket() = 0;// 关闭socket
    public:
        void BuildListenSocketMethod(uint16_t port, int backlog)// 创建一个监听socket
        {
            CreateSocketOrDie();// 创建socket
            BindSocketOrDie(port);// 绑定端口
            ListenSocketOrDie(backlog);// 监听
        }
        bool BuildConnectSocketMethod(const std::string& serverip, uint16_t serverport)// 创建一个连接socket
        {
            CreateSocketOrDie();// 创建socket
            return ConnectSocketOrDie(serverip, serverport);// 连接
        }
        void BuildNormalSocketMethod(int sockfd)// 创建一个普通socket
        {
            SetSocketFd(sockfd);
        }
    };

    // 继承类Socket, 用于表示一个tcp socket
    class TcpSocket : public Socket {
    public:
        TcpSocket(int sockfd = -1)
            :_sockfd(sockfd)
        {}
        
        ~TcpSocket()
        {}
        void CreateSocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0); 
            if (_sockfd < 0)
            {
                exit(Socket_Error);
            }

        }
        void BindSocketOrDie(uint16_t port)
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;
            int n = ::bind(_sockfd, Convert(&local), sizeof(local));
            if (n < 0)
            {
                exit(Bind_Error);
            }
        }
        void ListenSocketOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
            {
                exit(Listen_Error);
            }
        }
        Socket* AcceptSocketOrDie(std::string *peerip, uint16_t *peerport)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int newsockfd = ::accept(_sockfd, Convert(&peer), &len);
            if (newsockfd < 0)
            {
                return nullptr;
            }
            *peerport = ntohs(peer.sin_port);
            *peerip = inet_ntoa(peer.sin_addr);
            Socket* newSock = new TcpSocket(newsockfd);
            return newSock;
        }
        bool ConnectSocketOrDie(const std::string& serverip, uint16_t serverport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(serverport);
            server.sin_addr.s_addr = inet_addr(serverip.c_str());
            int n = ::connect(_sockfd, Convert(&server), sizeof(server));
            if (n == 0)
            {
                return true;
            }
            return false;
        }

        int GetSocketFd() const override
        {
            return _sockfd;
        }
        void SetSocketFd(int sockfd) override
        {
            _sockfd = sockfd;
        }
        void CloseSocket() override
        {
            if (_sockfd > defaultSockfd)
            {
                ::close(_sockfd);
            }
        }
    private:
        int _sockfd;
    };
}

协议定制文件, Protocol.hpp

#pragma once

#include <iostream>

// 定制协议
class Request {

public:
    Request(int x, int y);
    ~Request();

private:
    int _data_x;// 参数x
    int _data_y;// 参数y
    char _oper;// 操作符 + - * / %
};

class Response {
private:
    int _result;// 结果
    int _code;// 状态码
};


自定义协议

首先进行举例说明
在这里插入图片描述
在这个过程中,

  1. write/send只是拷贝函数, 将数据从缓冲区拷贝到内核或者是将数据从缓冲区拷贝到内核
  2. 发送缓冲区数据, 是什么时候发?发多少?出错了怎么办?

都是由内核决定 — TCP控制协议
TCP协议叫做传输控制协议, 传输和控制的就是数据的拷贝内容

  1. TCP在实际的通信是双方OS之间的通信, 将数据从一台OS拷贝给另一台OS
  2. 像之前的recv/read也只是与这些功能类似
  3. 而read/write/send/recv…这些功能, 与当时文件的IO过程一模一样, 在内核到用户发送数据的时候, 用户级缓冲区都是处于阻塞等待状态, 当内核的数据到来时, 才会唤醒用户级缓冲区, 进行内容的写入
  4. 而这个从一台机器发送到另一台机器的过程就像是生产者消费者模型, 一边进行发送, 一边进行拿取, 这也是最经典的生产者消费者模型
  5. 一台在发送时, 同时也能完成接收, 这就是全双工协议的TCP, UDP虽然是单工通信, 但是内部与之类似
  6. 数据的粘包问题

用户输入的字符, 发送到对端时, 可能会变少, 这是因为什么呢?
首先, TCP是面向字节流, 没有边界, 而OS在发送TCP数据时, 会通过缓冲区来进行优化, 例如缓冲区的大小为1024字节. 如果一次请求发送的数据量比较少, 没达到缓冲区大小, TCP则会将多个请求合并为一个请求进行发送, 这就形成了粘包问题, 如果一次请求发送的数据量比较大, 超过了缓冲区的大小, TCp就会将其拆分为多次进行发送, 这就是拆包.
实际用户发送多少字节, 并不一定收到多少字节就是这个原因
但是UDP是数据报, 要么都过不去, 要么全部发过去

  1. 除了要定制协议, 还要明确报文与报文之间的边界, 目的就是为了防止数据包的粘包问题(后期代码体现)
  2. UDP是不需要的, 他是面向数据报的协议, 报文与报文之间有明确边界, 而TCp需要自己添加边界, 分为一个一个的数据包, 从用户(上层)看来就是一个一个的字节流, 因此他也叫做面向字节流

现在要解决的问题

  1. 结构化数据的序列化和反序列化
  1. 首先这边传入的数据是 x y 和op操作符, 那么传过去的时候, 最合适的方式就是"x op y\n", 但是为了以后数据序列化的通用性, 这边引入数据头, 那么这个数据就会变成"len\nx op y\n", 我们只需要找到第一个\n就可以知道len的长度是多少, len代表一个数据包的长度, 由此将来传过来的数据流的格式可能是"len\nx op y\n"“len\nx op y\n”"len\nx op y\n"但是可以通过上述的寻找方式都找到
  2. \n都不属于是报文的一部分, 这是一种约定
  3. 这些很多工作都是在做字符串处理
  4. 对Response做序列化和反序列化
bool Serialize(std::string &out)
			    {
			        out.append(std::to_string(_result));
			        out.append(ProtSep);
			        out.append(std::to_string(_code));
			        return true;
			    }
			    bool Deserialize(std::string &in)// 将"_result _code" 反序列化成对象   
			    {
			        auto pos = in.find(ProtSep);
			        if(pos == std::string::npos)
			        {
			            std::cerr << "Response::Deserialize failed! pos == std::string::npos" << std::endl;
			            return false;
			        }
			        _result = std::stoi(in.substr(0, pos));// 刚好是[)区间
			        _code = std::stoi(in.substr(pos+ProtSep.size()));
			        return true;
			    }

  1. 对Request做序列化和反序列化
// 序列化 --- 将对象序列化成字符串
			    bool Serialize(std::string &out)// 序列化都是输出型参数
			    {
			        out.append(std::to_string(_data_x));
			        out.append(ProtSep);
			        out.append(std::to_string(_data_y));
			        out.append(ProtSep);
			        out.append(std::to_string(_oper));
			        return true;
			    }
			    // 反序列化 --- 将字符串反序列化成对象
			    bool Deserialize(std::string &in)// 反序列化传入的必须是 "x op y" 的格式
			    {
			        auto left = in.find(ProtSep);
			        if(left == std::string::npos)
			        {
			            std::cerr << "Request::Deserialize failed! left == std::string::npos" << std::endl;
			            return false;
			        }
			        auto right = in.rfind(ProtSep);
			        if(right == std::string::npos)
			        {
			            std::cerr << "Request::Deserialize failed! right == std::string::npos" << std::endl;
			            return false;
			        }
			        _data_x = std::stoi(in.substr(0, left));
			        _data_y= std::stoi(in.substr(right+ProtSep.size()));// 从当前位置开始截取到ProtSep.size()个字符, 截到结尾
			        std::string oper = in.substr(left+ProtSep.size(), right-(left+ProtSep.size()));
			        if(oper.size() != 1)
			        {
			            std::cerr << "Request::Deserialize failed! oper.size() != 1" << std::endl;
			            return false;
			        }
			        _oper = oper[0];
			        return true;
    }
  1. 上述操作只是完成业务方面的应用, len /n的处理是在网络发送方面先序列化, 换成"x op y"的格式, 然后调用encode, 变成"len\nx op y\n"

完成解析函数

std::string Encode(const std::string &message)// 先序列化, 变成"x op y"的格式, 然后调用Encode, 变成"len\nx op y\n"形式
			    {
			        std::string len = std::to_string(message.size());
			        std::string package = len + LineBreakSep + message + LineBreakSep;
			        return package;
			    }
			    // 解析的时候, 无法保证传进来的package一定是一个完整的协议, 所以需要传入一个指针, 指向一个字符串, 将解析出来的协议内容放入这个字符串中
			    bool Decode(std::string &package, std::string *message)// 先解码, 变成"len\nx op y\n"形式, 然后反序列化, 变成对象
			    {
			        auto pos = package.find(LineBreakSep);// 确保拿到的长度是一个完整的协议
			        if (pos == std::string::npos)
			        {
			            std::cerr << "Decode failed! pos == std::string::npos" << std::endl;
			            return "";
			        }
			        std::string lens = package.substr(0, pos);// 拿到协议的长度
			        int message_len = std::stoi(lens);// 将长度转换为整数
			        int total_len = message_len + pos + LineBreakSep.size() + 2 * LineBreakSep.size();// 确保长度是完整的协议的长度
			        if (package.size() < total_len)// 如果长度不够, 返回false
			        {
			            std::cerr << "Decode failed! package.size() < message_len + pos + LineBreakSep.size()" << std::endl;
			            return false;
			        }
			        // 走到这里, 说明长度至少是一个完整的协议的长度
			        *message = package.substr(pos + LineBreakSep.size(), message_len);// 将协议内容赋值给message
			        package.erase(0, total_len);// 删除已经解析出来的协议, 保证下一次解析的时候, 不会重复解析
			        return true;
			    }

业务处理

			void HandlerRequest(Socket *sockp)
			{
			    std::string inbufferstream;// 接收请求的缓冲区
			    // 1.创建一个工厂类, 用于创建请求对象
			    std::unique_ptr<Factory> factory = std::make_unique<Factory>();
			    auto req = factory->BuildRequest();
			    // 接收请求
				while(true)
				    {
				        // 1.读取报文
				        if (!sockp->Recv(&inbufferstream, 1024))
				        {
				            break;// 接收失败
				        }
				        // 2.分析收到的字节流, 是否有一个完整的报文
				        std::string message;
				        if(!Decode(inbufferstream, &message))
				        {
				            continue;
				        }
				        // 3.处理请求, 走到这里, 说明已经有一个完整的报文了, 可以进行反序列化
				        if(!req->Deserialize(message))
				        {
				            break;// 反序列化失败
				        }
				        // 4.业务处理, 计算器
				        auto resp = calculate.Cal(req);
				        // 5.将响应对象序列化
				        std::string send_string;
				        resp->Serialize(&send_string);// 序列化之后数据一定是"result code"
				        // 6.添加报头(构建完整的字符串级别的响应报文)
				        send_string = Encode(send_string);
				        // 上述的操作, 可以封装成一个类, 因为序列化的整体流程就是这样的, 封装成一个类可以直接调用, 然后进行后续的工作, 更整体化, 更清晰
				 
				 
				    }
}

现在进行修改代码, 实际的输入内容由上层决定, 当前的回调只是将数据进行发送, 转发即可, 后续的操作就不再演示,省略即可

引入成熟的序列化和反序列化的操作

为了方便操作, 先将自己的序列化方式改为条件编译进行限制

在这里插入图片描述

json的使用

首先, 我们需要知道现如今常用的序列化方式
json — 常用
protobuf — 网络中常用的序列化和反序列化操作
xml – 比较慢, c++不进行考虑

使用json进行序列化和反序列化操作

java python都是可以直接使用json, 而c++要进行使用, 则必须要使用第三方sdk, 安装别人的库

sudo apt-get install -y jsoncpp-devel //devel表示这个版本是开发板

安装完成序列化的演示实现

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
    Json::Value root; // 万能类型, 接受任何数据类型
    root["k1"] = 100;
    root["k2"] = 200;
    root["k3"] = 300;
    Json::FastWriter writer;
    std::string str = writer.write(root); // 序列化
    std::cout << str << std::endl;
    return 0;
}

因为jsoncpp是第三方的库, 所以要使用, 必须在编译时进行编译选项的链接

在这里插入图片描述
运行结果演示:
在这里插入图片描述
同时json支持嵌套键值对的定义实现:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
    Json::Value root; // 万能类型, 接受任何数据类型
    root["k1"] = 100;
    root["k2"] = 200;
    root["k3"] = 300;
    Json::Value v;
    v["hello"] = "world";
    v["world"] = "nihao";
    root["k4"] = v;

    Json::FastWriter writer;
    std::string str = writer.write(root); // 序列化
    std::cout << str << std::endl;
    return 0;
}

运行结果演示:
在这里插入图片描述除了使用FastWrite方式还可以使用StyleWrite方式进行:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
    Json::Value root; // 万能类型, 接受任何数据类型
    root["k1"] = 100;
    root["k2"] = 200;
    root["k3"] = 300;
    Json::Value v;
    v["hello"] = "world";
    v["world"] = "nihao";
    root["k4"] = v;
    Json::StyledWriter writer;
    std::string str = writer.write(root); // 序列化
    std::cout << str << std::endl;
    return 0;
}

可以看到他生成的结果是具有格式演示的序列化, 这样只是可读性强一点, 除此之外没有区别
在这里插入图片描述对上述的业务代码完成序列化和反序列化操作:
序列化:

Json::Value root;
root["x"] = _data_x;
root["y"] = _data_y;
root["op"] = _oper; // 字符的本质就是整数, 所以可以直接赋值
Json::FastWriter writer;
*out = writer.write(root); // 也可以判断是否成功, 这边就不进行了
return true;

反序列化:

Json::Value root;
Json::Reader reader;
bool ret = reader.parse(in, root);
if (!ret) // 解析失败, 返回false, 参数为in, 解析结果为root
{
    std::cerr << "Request::Deserialize failed! Json::Reader::parse failed" << std::endl;
    return false;
}
_data_x = root["x"].asInt();
_data_y = root["y"].asInt();
_oper = root["op"].asInt(); // 字符的本质就是整数
return ret;

这边代码的encoed和decode和对我们自己的序列化和反序列化变成的"len\nx op y\n"的格式, 在使用json之后有什么区别呢?会有变化吗?

实际上, 不管是自定义序列化还是json序列化, 他们操作之后的结果或者是操作之前的数据格式都是对这两个方法没有影响的, 因为长度信息都在len这个字符当中, 所以未来不管使用哪种序列化的方法, 他都能进行正确的识别

现在已经完成了对自定义序列化和json序列化的条件编译代码, 要想实现对应的序列化操作还需要对他进行编译代码时的选择, 这边选择在makefile中进行修改

在这里插入图片描述在这里插入图片描述
在编译时加一个 -static表示静态编译, 这样编译出来的代码内部以静态链接的方式连接着其他的第三方库, 所以内存较大, 但是直接运行的效果较好
可以将这个程序改为守护进程的方式
在这里插入图片描述在这里插入图片描述

总结

  1. 上述用到的协议只有一层, Response Request, 未来如果想引入多个协议以供不同的应用场景, 可以将这些协议写好之后保存在一个vector里面, 再在序列化的时候在序列化字段最前面加上protocal_code\nlen\n..., 其中protocal_code就表示使用哪一个协议.
  2. 像是Socket.hpp的整个功能就能表示一个会话层, 用于通信的连接和管理
  3. 我们写的protocal添加报头, 对象转字符串等的操作都是在表示层
  4. 而这个应用层就是我们写的calculate.hpp
  5. 这些我们自己写的三层最终都会汇总到应用层(从OSI模型到TCP/IP模型也是这样)当中, 未来我们写网络服务就一定会用到着三层.

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

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

相关文章

Scala入门基础(10)高级函数

一.什么是高阶函数 二.map函数 三.foreach函数 四.filter函数 五.flatten函数 正文&#xff1a; 一.什么是高阶函数 高阶函数&#xff1a;是一个特殊的函数&#xff0c;特殊之处在于&#xff1a;它指使用其他函数作为参数或返回值 &#xff08;演示&#xff09; 二.map函…

maven项目打jar包之后如何指定外部配置文件运行java类

在maven项目中,常常会用到一些配置文件,一旦打成jar包之后,想要用外部的配置文件运行,怎么做呢? 一、配置文件config.ini 在maven项目中的src/main/resources目录下存放了一个配置文件config.ini。这个文件是默认的配置文件。 db.url=jdbc:mysql://localhost:3306/qyxx?u…

【JavaScript】LeetCode:66-70

文章目录 66 组合总和67 括号生成68 单词搜索69 分割回文串70 N皇后 66 组合总和 回溯sum&#xff1a;当前组合的数字和。递归终止条件&#xff1a;sum > target。收集结果条件&#xff1a;sum target&#xff0c;找到了满足条件的组合。注意&#xff1a;因为可以重复取数&…

亚洲最具影响力人物颜廷利:心理健康对身体健康的重要影响

在当代社会&#xff0c;面对疾病与痛苦&#xff0c;人们往往在西医与中医之间做出选择。21世纪世界上知名度最高的人物颜廷利教授的精辟见解指出了这两种医学体系的根本差异&#xff1a;西医以其高昂的费用&#xff0c;针对生理上的疾苦提供快速而直接的解决之道&#xff1b;相…

Python应用指南:利用高德地图API获取公交可达圈

参考文章&#xff1a;城市公交可达圈绘制方法&#xff08;一&#xff09; - 知乎 (zhihu.com) 本篇文章我们聚焦于通过公共交通出行方式&#xff08;包括公交、地铁、公交地铁的组合&#xff09;来获取一定时间内可以到达的范围。为了实现这一目标&#xff0c;我们将使用高德地…

在 Android 应用程序中实现与WebSocket 服务器的实时通信

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。 &#x1f60a; 座右铭&#xff1a;不…

spring-boot学习(2)

上次学习截止到拦截器 1.构建RESfun服务 PathVariable通过url路径获取url传递过来的信息 2.MyBatisPlus 第三行的mydb要改为自己的数据库名 第四&#xff0c;五行的账号密码改成自己的 MaooerScan告诉项目自己的这个MyBatisPlus是使用在哪里的&#xff0c;包名 实体类的定义…

PL/SQL Developer15和Oracle Instant Client安装配置详细图文教程

一、下载介质 1、Oracle Instant Client Oracle Instant Client Downloads | Oracle 中国 2、PL/SQL DEVELOPER PL/SQL Developer - Allround Automations Free trial - Allround Automations 二、安装介质。 1、安装plsqldev1504x64.msi。 一路默认下一步。 选择输入许可信…

实跑 YOLO V11在 OAK内部运行的效果

哈喽&#xff0c;各位OAK中国的朋友们! 大家好我是张伯生 今天&#xff0c;我想给大家演示一下最新发布的Yolo V11神经网络 下面我将演示的一个程序是&#xff1a;同时在我们的OAK相机上跑Yolo V11和RGB-D&#xff0c;也就是彩色相机和深度图的一个叠加的一个效果 RGB-D和Yo…

java_for循环

基本语法 for 关键字&#xff0c;表示循环控制for 有四要素: (1)循环变量初始化(2)循环条件(3)循环操作(4)循环变量迭代循环操作 , 这里可以有多条语句&#xff0c;也就是我们要循环执行的代码如果 循环操作(语句) 只有一条语句&#xff0c;可以省略 {}, 建议不要省略 流程图 …

电气学习知识点

文章目录 NPN和PNP输出 NPN和PNP输出 NPN和PNP&#xff08;两种不同类型的三极管&#xff09;都是集电极输出。&#xff08;集电极开路输出&#xff09; 下图b:基极、c集电极、e发射极 NPN示意图&#xff08;集电极连接负载 — 正方形&#xff09; NPN的电流流向是从集电极…

Elasticsearch设置 X-Pack认证,设置账号和密码

前言 以下Elasticsearch版本&#xff1a;7.9.3 ES自带的X-Pack密码验证&#xff1a; X-Pack是elasticsearch的一个扩展包&#xff0c;将安全&#xff0c;警告&#xff0c;监视&#xff0c;图形和报告功能捆绑在一个易于安装的软件包中&#xff0c;所以我们想要开启账号密码验证…

Scala入门基础(10.1)高阶函数2

一.reduce 二.reduceLeft-reduceRight 三.flod 四.sorter函数 五.sortWith 一.reduce 作用&#xff1a;reduce是一种集合操作&#xff0c;用于对集合中的元素进行聚合操作&#xff0c;返回一个单一的结果。它通过指定的二元操作(即取两个元素进行操作)对集合中的所有元素进…

力扣刷题-算法基础

hello各位小伙伴们,为了进行算法的学习,小编特意新开一个专题来讲解一些算法题 1.移除元素. - 力扣(LeetCode) 本题大概意思是给定一个数组和一个数val删除与val相同的元素,不要改变剩余元素的顺序,最后返回剩余元素的个数。 我们在这里使用双指针,这里的双指针并不是…

npm 加速,命令行修改国内镜像源【附带国内最新几个镜像】超简约版~

为什么要配置国内镜像源&#xff1f; npm 的官方源服务器在国外&#xff0c;对于国内开发者来说&#xff0c;下载速度可能会比较慢&#xff0c;甚至可能会出现下载失败的情况。而国内的镜像源服务器通常会对官方源的包进行同步&#xff0c;并且在国内部署&#xff0c;这样可以…

使用OneAPI+Ollama+Dify搭建一个兼容OpenAI的API发布及AI应用开发系统(三)Dify的安装及配置

在GitHub中的AI工作流短代码平台中&#xff0c;Dify获星一直名列前茅&#xff0c;目前已达48K星&#xff0c;其工作稳定性也是非常的高&#xff0c;在这里我们介绍一下Dify的安装。 由于Dify的结构非常的复杂&#xff0c;我们这里介绍Docker的方式进行安装&#xff0c;硬件的最…

Oracle+11g+笔记(7)-数据库空间管理

Oracle11g笔记(7)-数据库空间管理 7、数据库空间管理 存储空间是数据库系统中非常重要的资源&#xff0c;无论是数据库中的对象还是数据库中的数据都需要空间进行存储&#xff0c;一旦 数据库空间被全部占用&#xff0c;那么该数据库系统就不能再接受任何对象和数据&#xf…

浙大数据结构:09-排序2 Insert or Merge

这道题我们采用先判断是不是insert如果不是再用merge算一下 机翻 1、条件准备 首先存元素个数n&#xff0c;然后oldnum存原始数组&#xff0c;newnum存新数组 #include <iostream> #include<vector> #include<algorithm> using namespace std; #define e…

SSD | (三)NAND闪存(上)

文章目录 &#x1f4da;闪存基本原理&#x1f407;存储单元及相关操作&#x1f407;闪存类型&#x1f407;闪存组织结构&#x1f407;擦、写、读操作&#x1f407;阈值电压分布图 &#x1f4da;闪存基本原理 &#x1f407;存储单元及相关操作 闪存是一种非易失性存储器&#x…

ArkTS中的几个易错问题

问题1&#xff1a;copyWithin的乱用问题 由于鸿蒙开发者很多可能是安卓转的&#xff0c;在安卓侧尤其是kotlin写手觉得copyOfRange很好用&#xff0c;复制数组的某一段数据就用copyOfRange&#xff0c;而copyWithin其实不是同等作用。 下面是AI对copyWithin的解释&#xff1a…