【Linux】Tcp协议的通讯流程,浅谈三次握手四次挥手

news2024/11/25 11:53:30

文章目录

  • Tcp协议的通讯流程
  • 一、协议定制与网络版计算器的实现
  • 二、json的使用
  • 总结


Tcp协议的通讯流程

上一篇文章我们讲解了如何实现Tcp服务器,Tcp的接口也用了,下面我们就看一下Tcp协议的通讯流程:

 在服务端,我们首先要创建一个套接字,这个套接字被称为监听套接字,这个时候服务端处理关闭状态。有套接字后我们开始绑定服务端的ip和端口号,然后我们调用listen接口,一旦调用成功我们的服务器由close状态就变为监听状态,一旦变成监听状态看允许客户端来连我们的服务器了,只不过这个时候不获取客户端的链接。监听后我们就可以通过accept接口获取客户端的链接了,这个时候accept接口会给我们返回一个套接字,这个套接字才是真正用于和客户端通信的套接字。

客户端首先创建套接字,然后在发起链接请求的时候会帮我们绑定,connect这个接口是用来建立链接的,而我们只是向服务器发起建立链接的请求,中间三次握手的过程是由双方的操作系统自动来完成的。因为connect是链接建立的发起方,所以我们用accept获取链接的时候一定是链接已经成功建立了我们才能获取。

服务器初始化 :
调用 socket, 创建文件描述符 ;
调用 bind, 将当前的文件描述符和 ip/port 绑定在一起 ; 如果这个端口已经被其他进程占用了 , 就会 bind 失败;
调用 listen, 声明当前这个文件描述符作为一个服务器的文件描述符 , 为后面的 accept 做好准备 ;
调用 accecpt, 并阻塞 , 等待客户端连接过来
建立连接的过程 :
调用 socket, 创建文件描述符 ;
调用 connect, 向服务器发起连接请求 ;
connect 会发出 SYN 段并阻塞等待服务器应答 ; ( 第一次 )
服务器收到客户端的 SYN, 会应答一个 SYN-ACK 段表示 " 同意建立连接 "; ( 第二次 )
客户端收到 SYN-ACK 后会从 connect() 返回 , 同时应答一个 ACK ; ( 第三次 )
注意:三次握手的过程我们会在讲tcp原理的时候详细的讲解,现在只是一些流程和概念。
数据传输的过程:
建立连接后 ,TCP 协议提供全双工的通信服务 ; 所谓全双工的意思是 , 在同一条连接中 , 同一时刻 , 通信双方可以同时写数据; 相对的概念叫做半双工 , 同一条连接在同一时刻 , 只能由一方来写数据 ;
服务器从 accept() 返回后立刻调用 read(), socket 就像读管道一样 , 如果没有数据到达就阻塞等待 ;
这时客户端调用 write() 发送请求给服务器 , 服务器收到后从 read() 返回 , 对客户端的请求进行处理 , 在此期间客户端调用read() 阻塞等待服务器的应答 ;
服务器调用 write() 将处理结果发回给客户端 , 再次调用 read() 阻塞等待下一条请求 ;
客户端收到后从 read() 返回 , 发送下一条请求 , 如此循环下去

断开连接的过程:

如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN(第一次);

此时服务器收到 FIN , 会回应一个 ACK, 同时 read 会返回 0 ( 第二次 );
read 返回之后 , 服务器就知道客户端关闭了连接 , 也调用 close 关闭连接 , 这个时候服务器会向客户端发送一个FIN; ( 第三次 )
客户端收到 FIN, 再返回一个 ACK 给服务器 ; ( 第四次 )

总结:

1.建立链接是双方操作系统自动完成的(三次握手)。

2.建立链接是为了双方维护链接而创建的数据结构,这个数据结构是有成本的,这个成本主要体现在创建要花时间和空间。

3.断开链接(四次挥手)的目的是将曾经建立好的链接信息释放掉


一、协议定制与网络版计算器的实现

之前我们说过,协议是一种约定。那么socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?结构化的数据我们可以理解为对象,就像我们的qq,发送消息的时候除了消息还有头像昵称时间,这些数据肯定是不能分开发送给服务器的,因为一旦用户基数很大的时候服务器根本无法区分哪个是哪个的消息。那么如何将这些数据打包成一个发送给服务器呢?要想将多个字节流变成一个字节流,这个过程就叫做序列化。当我们的服务器将这个数据收到后,我们还需要将这个数据恢复为原来没有整合的样子,此时恢复的过程就叫做反序列化。所以:业务结构数据在发送到网络的时候,先序列化再发送收到的一定是序列字节流,要先进行反序列化然后才能使用,这是实现网络协议的第一个细节。我们刚刚说数据发送到网络,但实际上还有一个细节我们没有提到,我们如何确定服务器会完整的收到我们发送的数据呢?这就是我们实现协议的第二个细节。

网络版计算器的实现:

例如 , 我们需要实现一个服务器版的计算器 . 我们需要客户端把要计算的两个加数发过去 , 然后由服务器进行计算 , 最后再把结果返回给客户端。
约定方案:
客户端发送一个形如 "1+1" 的字符串 ;
这个字符串中有两个操作数 , 都是整形 ;
两个数字之间会有一个字符是运算符
数字和运算符之间有间隔
首先我们创建所需要的文件:

之前的Tcp客户端和服务端我们直接拿过来用即可,还有log.hpp等文件,然后我们的Tcp服务端只保留多进程版本:

现在我们要修改的是,如何对数据做处理,首先我们先把函数名改为handerEnter表示处理进入的逻辑,我们将以前的任务文件改名为protocol,也就是协议定制:

 准备工作做完后我们开始进行协议定制,首先我们的协议中包含请求与响应,请求就是客户让我们计算的内容,响应就是我们给出的结果和返回码:

pragma once
#include <iostream>

class Request
{
public:

public:
    //   "x op y"
    int x;
    int y;
    char op;
};

class Response
{
public:
    int exitcode;   //返回码,0表示计算成功,非0表示计算失败
    int result;     //计算结果
};

 然后我们写个构造函数初始化一下:

 我们先默认初始化为0,然后开始编写服务器中的回调函数hander:

    void handerEnter(int sock)
    {
        // 1.读取
        // 1.1 如何保证读到的消息是一个完整的请求


        // 2.对请求Request 反序列化
        // 2.1 得到一个结构化的请求对象
        Request req =    

        // 3. 计算机处理req.x  req.y   req.op
        // 3.1 得到一个结构化的响应
        Response resp;


        // 4. 对响应Response进行序列化
        // 4.1 得到了一个字符串

        // 5.然后我们再发送响应
    }

 我们有5个大步骤,首先服务端收到客户端发来的请求(就是让服务器计算某个表达式),我们第一个要保证的是如何能完整的接收客户端发来的数据,而要保证这一点可以有三个步骤(1.数据定长2.特殊符号3.自描述)这三点我们后面会讲到,能保证第一步后我们再对这个请求做去掉报头,然后做反序列化操作,一旦进行反序列化操作我们就能拿到单个的x,y,op,然后我们利用回调函数开始做计算,这个函数我们在server.cc中实现直接传入start函数,计算出来结果后我们还要发回给客户端,所以还需要将结果进行序列化操作,序列化后得到一个“x op y”的字符串,我们将这个字符串发送给客户端即可。因为我们的第3步要得到一个响应,所以我们直接搞一个回调函数:

 

这个回调函数的第一个参数是一个输入型参数,就是客户端发给我们的请求经过反序列化的对象,第二个参数是一个输出型参数,未来我们处理req的时候得到req.x,req.y,req.op然后用他们做计算,将计算结果直接放到resp中。

  void handerEnter(int sock, func_t func)
    {
        // 1.读取
        // 1.1 如何保证读到的消息是一个完整的请求


        // 2.对请求Request 反序列化
        // 2.1 得到一个结构化的请求对象
        Request req =    

        // 3. 计算机处理req.x  req.y   req.op
        // 3.1 得到一个结构化的响应
        Response resp;
        func(req,resp); //req的处理结果全部放入了resp中

        // 4. 对响应Response进行序列化
        // 4.1 得到了一个字符串

        // 5.然后我们再发送响应
    }

 有了这个回调函数,那么我们在server.cc中就直接可以根据处理好的req去填充resp:

 那么如何保证读到的消息是一个完整的请求呢?因为tcp是面向字节流的,所以我们可以明确报文和报文的边界,明确报文和报文边界有三种方式:1.定长 2.特殊符号 3.自描述,如下图:

 tcp内部是有自己的发送缓冲区和接收缓冲区的,所以我们只需要规定每个报文的大小的都是一样的,这样就明确了报文和报文边界,当然我们也可以在报文中加一个特殊的符号,用这些符号去表示报文边界,那么自描述是什么呢,其实就是我们可以规定报文的前四个字节是什么什么,后面是什么什么。(我们今天所演示的自描述就是添加报头,这个报头可以是符号也可以代表一些含义)

有了以上概念我们就可以写序列化与反序列化函数了,未来这两个函数一定是能让req和resp使用的,所以我们先给req这个类实现序列化与反序列化函数:

#define SEP " "
#define SEP_LEN strlen(SEP)

bool serialize(std::string *out)
    {
        *out = "";
        std::string x_string = std::to_string(x);
        std::string y_string = std::to_string(y);
        *out += x_string;
        *out += SEP;
        *out += op;
        *out += SEP;
        *out += y_string;
        return true;
    }

首先我们的规定是序列化后的字符串是: "x op y"的形式,所以我们将分隔符定义为空格,然后定义空格符的长度,这样以后更改会很方便。首先对out指针做初始化,因为我们不能保证给我们传的字符串是空字符串,然后我们将x和y转化为string类型让out字符串先+x,再加分隔符,再加运算符,再加分隔符最后再加y即可,这样外部的out参数就得到了序列化的数据,然后我们返回true。

bool deserialize(const std::string& in)
    {
        auto left = in.find(SEP);
        auto right = in.rfind(SEP);
        if (left==std::string::npos || right==std::string::npos)
        {
            return false;
        }
        if (left == right)
        {
            return false;
        }
        if (right - (left+SEP_LEN) != 1)
        {
            return false;
        }
        std::string x_string = in.substr(0,left);
        std::string y_string = in.substr(right+SEP_LEN);
        if (x_string.empty()) return false;
        if (y_string.empty()) return false;
        x = stoi(x_string);
        y = stoi(y_string);
        op = in[left+SEP_LEN];
        return true;
    }

反序列化也很简单,我们首先找到字符串中左边的空格和右边的空格,然后判断是否满足我们要求的格式,不满足直接返回false。满足后我们就获取序列化字符串中的x,y,op得到后保存在类内的成员变量中,然后返回true。

有了对请求的序列化和反序列化我们就可以计算了:

bool cal(const Request& req,Response& resp)
{
    //req已经有结构化完成的数据了,可以直接使用
    resp.exitcode = OK;
    resp.result = OK;
    switch (req.op)
    {
    case '+':
        resp.result = req.x + req.y;
        break;
    case '-':
        resp.result = req.x - req.y;
        break;
    case '*':
        resp.result = req.x * req.y;
        break;
    case '/':
    {
        if (req.y==0)
        {
            resp.exitcode = DIV_ZERO;
        }
        else 
        {
            resp.result = req.x / req.y;
        }
    }
    break;
    case '%':
    {
        if (req.y==0)
        {
            resp.exitcode = MOD_ZERO;
        }
        else 
        {
            resp.result = req.x % req.y;
        }
    }
    break;
    default:
        resp.exitcode = OP_ERROR;
        break;
    }
    return true;
}

 计算也很简单,首先将回应类中的返回码和结果初始化,然后利用switch判断运算符(注意:switch语句中是可以在case语句后面加大括号的,但是我们的break一定要写在大括号外面,否则就犯了C语言的错误),对于除0错误我们可以在协议头文件中设置一个错误码:

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

上面我们只是完成了请求的序列化和反序列化,我们还需要对结果的相应也做序列化与反序列化:

    //"exitcode result"
    bool serialize(std::string* out)
    {
        *out = "";
        *out+= to_string(exitcode);
        *out += SEP;
        *out+= to_string(result);
        return true;
    }
    bool deserialize(const std::string& in)
    {
        auto pos = in.find(SEP);
        if (pos==string::npos)
        {
            return false;
        }
        exitcode = stoi(in.substr(0,pos));
        result = stoi(in.substr(pos+SEP_LEN));
        return true;
    }

因为我们规定序列化的结果必须是"exitcode result"的形式,所以响应的序列化中只需要把返回码和空格和结果拼接起来即可。反序列化也很简单,就是先找到空格,然后通过空格分割出返回码和结果的字符串,然后将字符串转化为int赋值给响应类的私有成员即可,这样响应类中就保存了计算出的结果和返回码。

下面我们再写添加报头的函数,由于这个函数响应和请求都需要,所以我们直接写在协议头文件中:

#define SEP " "
#define SEP_LEN strlen(SEP)

#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) 


//  自定义报头:"x op y" -> content_len\r\n"x op y"\r\n
//             "exitcode result" -> content_len(正文长度)\r\n"exitcode result"\r\n
const string enLength(const std::string& text)   //添加报头
{
     string send_string = to_string(text.size());
     send_string+=LINE_SEP;
     send_string+=text;
     send_string+=LINE_SEP;
     return send_string;
}

我们规定一个完整的报文必须是:"x op y" -> content_len\r\n"x op y"\r\n这样的格式,就是正文长度 + \r\n(特殊符号)+正文 + \r\n,而这个函数给我们传的参数一定是正文"x op y"这样的字符串,所以我们只需要计算正文长度,然后将正文长度转化为字符串,将正文长度加上特殊符号加上正文加上特殊符号,这样就是我们规定的报文了,然后返回即可。

有了添加报头我们还需要去掉报头的函数:

bool deLength(const std::string& package,string* text)
{
     auto pos = package.find(LINE_SEP);
     if (pos==string::npos)
     {
        return false;
     }
     int len = stoi(package.substr(0,pos));
     *text = package.substr(pos+LINE_SEP_LEN,len);
     return true;
}

package参数就是一个完整报文,text是输出型参数,就是我们将去掉报头的结果放在text中返回给外界。去掉报头也很简单,先找到特殊符号,然后我们获取正文的长度,注意:我们规定完整的报文开头到第一个\r\n就是报文长度。有了报文长度后直接用substr函数就拿到了正文。拿到后返回true即可。

下面我们完善hander函数:

 void handerEnter(int sock, func_t func)
    {
        while (true)
        {
            // 1.读取
            // 1.1 如何保证读到的消息是一个完整的请求
            std::string req_str;
            //?????????
            cout<<"处理前请求的完整报文:\n"<<req_str<<endl;
            // 如果走到这里,那么可以保证req_str得到的是一个完整的请求
            string ret;
            if (!deLength(req_str, &ret))
            {
                return;
            }
            cout<<"去掉报头的请求报文:\n"<<ret<<endl;
            // 2.对请求Request 反序列化
            // 2.1 得到一个结构化的请求对象
            Request req;
            if (!req.deserialize(ret))
            {
                // 反序列化失败就return
                return;
            }
            // 3. 计算机处理req.x  req.y   req.op
            // 3.1 得到一个结构化的响应
            Response resp;
            func(req, resp);
            // 4. 对响应Response进行序列化
            // 4.1 得到了一个字符串
            std::string resp_str;
            resp.serialize(&resp_str);
            cout<<"进行序列化后的响应的字符串:\n"<<resp_str<<endl;
            // 5.然后我们再发送响应
            // 5.1构建成为一个完整的报文
            string send_string = enLength(resp_str);
            cout<<"对序列化后的响应添加报头的字符串:\n"<<send_string<<endl;
            send(sock, send_string.c_str(), send_string.size(), 0);
        }
    }

因为我们是多次给客户端发送响应,所以这里应该是个循环。然后我们除了没有保证读到的消息是一个完整的报文,其他的阶段都搞定了。假设我们已经读到一个完整的报文了,然后对请求做去掉报头操作,去掉报头后再进行反序列化操作,这样就拿到了x,y,op并且保存在req中,然后我们创建一个响应对象,调用回调函数将req中x,y,op的结果计算出来,把结果和返回码填到resp中。然后我们对resp中的返回码和结果进行序列化操作,序列化后添加报头就可以发送给客户端了,下面我们编写保证得到的是完整报文的函数:

bool recvPackage(int sock, string& inbuffer, string *text)
{
    //因为是从sock中读取数据放到text中,所以为了保险先对text清空
    (*text).clear();
    char buffer[1024];
    while (true)
    {
        ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0);
        if (n>0)
        {
           buffer[n] = 0;
           inbuffer+=buffer;
           //分析处理得到一个完整的报文
           auto pos = inbuffer.find(LINE_SEP);
           if (pos==string::npos)
           {
              //没找到分隔符,一定不是完整的报文,需要继续往inbuffer中读
              continue;
           }
           int text_len = stoi(inbuffer.substr(0,pos));
           int connumber_len = to_string(text_len).size();
           int total_len = connumber_len + text_len + 2*LINE_SEP_LEN;
           if (total_len > inbuffer.size())
           {
               continue;
           }
           //inbuffer.size()大于一个完整报文长度,那么inbuffer中至少有一个完整的报文
           *text = inbuffer.substr(0,total_len);
           inbuffer.erase(0,total_len);
           //读到一个完整的报文就退出
           break;
        }
        else
        {
            return false;
        }
    }
    return true;
}

首先我们要从文件描述符中读到完整的报文放在text中,所以我们保险起见先将text清空。inbuffer参数是一个支持我们持续存放报文的字符串,因为我们读取是有可能读不到一个完整的报文的,所以这些暂时用不到的报文都会放在inbuffer中。所以我们保险起见先将text清空。然后我们定义一个

缓冲区,这个缓冲区会存放我们从文件描述符中读出来的数据,这里我们用recv接口读取,返回值与read一样都是读到的字节数。如果n==0说明客户端退出,这个时候我们返回false即可。如果n大于0,说明读取成功,然后我们在缓冲区最后一个位置加上\0然后把数据加到inbuffer中,注意是+=因为会持续的读取。然后我们在inbuffer中查找特殊字符\r\n,如果没找到那么一定不会有完整的报文,所以我们continue继续读取,读取到特殊符号后,我们首先保存正文长度和正文数字的长度(比如 4\r\nhoow\r\n),首先正文的长度是4,前面这个4的长度是1,1就是正文数字的长度。有了这两个长度我们就能计算一个完整报文的长度,完整报文 = 正文数组长度 + 2*特殊字符长度 + 正文长度。然后我们判断inbuffer中的长度是否大于完整报文的长度,如果不大于则说明inbuffer没有完整报文需要继续读取,如果有完整报文我们就用substr函数拿到完整报文,并且将inbuffer中刚刚拿掉的报文删除,这样就不会影响下一个报文。注意:我们读到一个报文就退出处理。

 然后我们的hander方法就全部搞定了,注意在hander中定义一个inbuffer用来存放循环读取到的客户端报文。

服务端我们搞定后,现在去搞定客户端,让客户端发送请求:

void start()
      {
         struct sockaddr_in server;
         bzero(&server,sizeof(server));
         server.sin_family = AF_INET;
         server.sin_port = htons(_serverport);
         server.sin_addr.s_addr = inet_addr(_serverip.c_str());
         //connet的时候操作系统会帮客户端bind   返回值等于0成功
         if (connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0)
         {
             cerr<<"socket connect error"<<endl;
         }
         else 
         {
            string msg;
            string inbuffer;
            while (true)
            {
               cout<<"mycal>> ";
               getline(cin,msg);
               //将字符串中转换为一个请求
            }
         }
      }

我们客户端肯定是输入要计算的字符串,那么我们如何将字符串转化为一个响应呢?其实很简单,我们规定只是"1+1" 或者 "10/12"这样的格式:

 Request ParseLine(const string& line)
      {
         //规定表达式  "1+1" " "12/0"
         int status = 0;  //status是一个标志,0表示操作符之前,1表示操作符,2表示操作符之后
         int i = 0;
         int cnt = line.size();
         string left,right;
         char op;
         while (i<cnt)
         {
            switch (status)
            {
               case 0:
               {
                  if (!isdigit(line[i]))
                  {
                     op = line[i];
                     status = 1;
                  }
                  else 
                  {
                     left.push_back(line[i++]);
                  }
               }
               break;
               case 1:
                  i++;
                  status = 2;
                  break;
               case 2:
                  right.push_back(line[i++]);
                  break;
            }
         }
         return Request(stoi(left),stoi(right),op);
      }

我们实现的原理很简单,首先大循环是遍历字符串,在遍历的过程中我们设置一个标志,标志为0代表这个字符属于左操作数,标志为1代表属于运算符,标志为2代表属于右操作数。然后我们通过switch语句去判断,最后拿到左操作数和右操作数和运算符,然后我们要想构造请求对象还必须给request类多加一个构造函数:

 同样如果响应类也需要的话,我们也加一个。

有了构造函数我们直接匿名对象返回即可,这样我们就将一个字符串转换为请求对象了。

void start()
      {
         struct sockaddr_in server;
         bzero(&server,sizeof(server));
         server.sin_family = AF_INET;
         server.sin_port = htons(_serverport);
         server.sin_addr.s_addr = inet_addr(_serverip.c_str());
         //connet的时候操作系统会帮客户端bind   返回值等于0成功
         if (connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0)
         {
             cerr<<"socket connect error"<<endl;
         }
         else 
         {
            string msg;
            string inbuffer;
            while (true)
            {
               cout<<"mycal>> ";
               getline(cin,msg);
               //先有结构化的数据,然后序列化后发送给服务端
               Request req = ParseLine(msg);
               //得到序列化的结果
               string req_s;
               req.serialize(&req_s);
               //给序列化的结果添加报头
               string send_string = enLength(req_s);
               send(_sock,send_string.c_str(),send_string.size(),0);

               //读取服务器返回的序列化结果
               string total_text;
               if (!recvPackage(_sock,inbuffer,&total_text))
               {
                  continue;
               }
               //走到这total_text中一定是一个完整的报文
               Response resp;
               string text;
               if (!deLength(total_text,&text))
               {
                  continue;
               }
               //走到这text中一定是一个去掉报头的报文
               resp.deserialize(text);
               cout<<"exitCode: "<<resp.exitcode<<endl;
               cout<<"result: "<<resp.result<<endl;
            }
         }
      }

然后我们从刚刚那一步开始,有了请求对象后我们需要对这个请求做序列化操作,序列化后添加报头,然后发送给服务器。同时我们还要接受服务器发给我们的相应,所以我们继续使用recvPackage函数读取一个完整的报文,如果我们读取失败那么就继续读取,直到读取成功我们将这个报文去除报头,然后进行反序列化操作这样resp中就保存了返回码和结果,然后我们将结果打印出来,下面我们运行起来演示一下:

 可以看到我们的网页版计算器是没问题的,对于除0错误我们只关心错误码。这样我们就完成了网页版计算器。

二、json的使用

对于上面我们自己写的序列化和反序列化操作,实际上在日常生活中我们是不用的,一般都是用现成的,下面我们讲一下最简单的json c++版本的使用:

首先linux安装json:

yum install -y jsoncpp-devel

注意没有权限的用sudo提权。

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

    }
    bool serialize(std::string *out)
    {
        Json::Value root;
        root["first"] = x;
        root["second"] = y;
        root["oper"] = op;
        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool deserialize(const std::string& in)
    {
        Json::Value root;
        Json::Reader read;
        read.parse(in,root);
        x = root["first"].asInt();
        y = root["second"].asInt();
        op = root["oper"].asInt();
        return true;
    }
public:
    //   "x op y"
    int x;
    int y;
    char op;
};

json的使用非常简单,首先定义一个万能的Value,然后因为是做序列化操作,所以需要将我们的x,y,op和root中进行一个映射,json使用的是键值对方式,所以我们就让x和first做一个映射,y和second做一个映射,op同理,做好映射我们定义一个Fastwriter对象,注意writer就是json中用来序列化的,然后writer中的write函数可以直接将root进行序列化并且返回的是一个字符串。

反序列化同样定义万能的Value,然后json中reader是进行反序列化的,我们利用read中的parse函数,这个函数第一个参数是从哪个流进行反序列化,第二个参数就是我们的root。然后我们就可以将root中的x,y,op都拿出来,因为root中是字符串类型,而我们要的是整形,所以用asInt转化为整形,op并不会受影响,因为char的本质是ascll值。

response的序列化和反序列化同理,大家可以试试:

class Response
{
public:
    Response()
    :exitcode(0)
    ,result(0)
    {

    }
    Response(int exitcode_,int result_)
    :exitcode(exitcode_)
    ,result(result_)
    {

    }
    bool serialize(std::string* out)
    {
        Json::Value root;
        root["exitcode"] = exitcode;
        root["result"] = result;
        Json::FastWriter writer;
        *out = writer.write(root);
        return true;
    }
    bool deserialize(const std::string& in)
    {
        Json::Value root;
        Json::Reader read;
        read.parse(in,root);
        exitcode = root["exitcode"].asInt();
        result = root["result"].asInt();
        return true;
    }
public:
    int exitcode;   //返回码,0表示计算成功,非0表示计算失败
    int result;     //计算结果
};

下面我们运行起来看看json的效果:

 可以看到换上json我们的代码也是没有问题的,我们在json和自己写的直接可以用条件编译,这样的话就可以想用哪个用哪个:

 然后我们的makefile也可以修改一下,对于条件编译,我们可以直接编译的时候添加MYSELF,这样就会用我们自己写的序列化和反序列化代码。

 红框的部分效果是一样的,只不过用LD变量会方便切换json和我们自己写的序列化和反序列化代码。

以上就是网页版计算器的全部内容了。


总结

自定义协议就是定义一个结构化的对象,有了结构化的对象未来客户端和服务端就可以来回进行发送和接收,约定在上面的代码中体现在规定必须是"x op y"这样的格式,这就是约定。注意:没有规定我们网络通信的时候只能有一种协议,实际上我们完全可以将上述代码中规定前面是正文长度替换为协议编码,通过协议编码就可以选择要执行哪种协议了。

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

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

相关文章

Django on_delete参数在sql级别操作中不生效问题

class AA(models.Model):name models.CharField(max_length128)class Meta:db_table aaclass BB(models.Model):name models.CharField(max_length128)aa models.ForeignKey(AA, nullTrue, on_deletemodels.CASCADE)class Meta:db_table bb 如上当使用ORM删除aa表中的数据…

数字孪生:未来科技的新前沿

数字孪生作为一项新兴的研究方向&#xff0c;正逐渐成为科技界的焦点。它是将现实世界中的实体、系统或过程通过数字化手段进行建模、仿真和分析&#xff0c;形成与实体相对应的数字化副本。数字孪生的发展为我们带来了无限的想象空间&#xff0c;以及解决现实问题的新途径。 在…

opencv-18 什么是色彩空间?

1.什么是色彩空间类型&#xff1f; 色彩空间类型&#xff0c;也称为颜色空间类型或色彩模型&#xff0c;是一种表示图像中颜色的方式。在计算机图形学和数字图像处理中&#xff0c;有许多种色彩空间类型&#xff0c;每种类型有不同的表达方式和特点。 常见的色彩空间类型包括&a…

Vector - CAPL - 诊断模块函数(回调函数信息)

目录 CanTpCopyDataReceived CAPL 调用返回的错误代码及其含义 CanTpGetReceiverAddress CanTpGetRecentAddressExtension CanTpGetSenderAddress 代码示例 CanTpProvideTxData 代码示例 CanTpSetRxBufferSize 代码示例 CanTpCopyDataReceived 功能&#xff1a;通过回…

​《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(16)-Charles其他骚操作之大结局​

1.简介 今天就说一些Charles的其他操作、以及抓包跨域的问题和常见的问题如何解决。到此Charles这一系列的文章也要和大家说再见了&#xff0c;其他什么小程序、Android7.0等等的问题可以查看宏哥的Fiddler系列文章&#xff0c;只不过是将Fiddler换成Charles而已。 2.模拟403…

vue中使用Base64转码(Tinymce富文本保留HTML标签)

在vue项目中&#xff0c;我们经常使用到富文本编辑器&#xff0c;例如博主的项目&#xff08;见上图&#xff09;&#xff0c;这里需要把富文本内的HTML结构&#xff0c;发放到Android端做混合应用的开发&#xff0c;因此HTML结构必不可少的&#xff01; 但是浏览器在向服务器…

【MySQL基础】

目录 一、概述 1.什么是数据库 2.数据库能干什么 2.1 企业应用 2.2 金融行业 2.3 电子商务 2.4 社交媒体 2.5物联网 3.为什么要用数据库&#xff0c;优势、特性&#xff1f; 3.1 可靠性和稳定性 3.2 数据管理能力 3.3 数据共享和集成 3.4 数据安全性和隐私保护 3…

新零售转型战略:打造数字化时代持续性盈利的商业模式

新零售达成什么样子&#xff0c;能够创造更大得客户价值&#xff0c;从企业得角度来看&#xff0c;他一定要提升企业得获利能力。新零售应该怎么做&#xff1f;以客户需求为本&#xff0c;找出需求&#xff0c;重新创造场景. 从而提升用户体验加强粘性&#xff0c;而蚓链数字化…

抖音seo账号矩阵系统源码如何开发布局?

目录 一、 抖音SEO账号矩阵系统源码的开发布局步骤如下&#xff1a; 二。 开发部署源码 三、 开发部署功能设计 1. 短视频AI智能创作 2. 托管式账号管理: 3. 数据分析 4. 智能营销获客 四。 抖音seo源码开发部署交付技术文档包含 五。 开发代码展示&#xff1a; 一、 抖…

Layui下拉多选框

标题xmSelect插件&#xff1a; xmSelect文档 下载Layui第三方插件 下拉多选框效果&#xff1a; 实现方法(例子)&#xff1a; 将xmSelect插件的xm-select.js文件引入到layui中&#xff1a; <script src"public/js/xm-select/xm-select.js"></script> …

借助 Mybatis 的动态 SQL 解决传参不确定问题

在上一篇的&#xff1a;Mybatis 操作数据库的基本 CRUD 以及查询操作详析_糊糊涂涂的博客-CSDN博客中介绍了Mybatis使用固定SQL语句操作数据&#xff0c;本篇介绍 Mybatis 一个强大的特性&#xff1a;动态SQL。 动态 SQL 解决什么问题&#xff1f; 那当我们要执行的业务逻辑有…

carla与g29联合调试(一)

前言&#xff1a; 之前做的carla与g29的联合调试&#xff0c;现在记录一下carla控制g29的实现流程。 一、总体通讯流程 主要实现为carla中车辆的方向盘转动带着g29跟着一起转动&#xff0c;使用ros通讯来实现这个过程。 二、具体实现流程 2.1首先确定g29的力反馈和转动 使…

基于LoRA微调部署Stable Diffusion【免费试用阿里云】

文章目录 Stable Diffusion介绍环境及资源准备过程注交互式建模&#xff08;PAI-DSW&#xff09;的试用在创建的工作空间中创建实例安装 Diffusers Stable Diffusion介绍 Stable Diffusion 是一种文本到图像的潜在扩散模型&#xff0c;由 Runway 和慕尼黑大学合作构建&#xf…

学习day52

1.关于 error Component name "School" should always be multi-word vue/multi-word-component-names 这里是因为脚手架的规范原因&#xff0c; 解决办法&#xff1a; 我是在vue.comfig.js文件中加入了一条配置&#xff0c;即 lintOnSave:false 整个文件的完整…

Spring Boot单元测试入门指南

Spring Boot单元测试入门指南 JUnit是一个成熟和广泛应用的Java单元测试框架&#xff0c;它提供了丰富的功能和灵活的扩展机制&#xff0c;可以帮助开发人员编写高质量的单元测试。通过JUnit&#xff0c;开发人员可以更加自信地进行重构、维护和改进代码&#xff0c;同时提高代…

(学习笔记)如何理解TCP是面向字节流的协议,UDP是面向报文的协议?

如何理解字节流&#xff1f; 之所以会说TCP是面向字节流的协议&#xff0c;UDP是面向报文的协议&#xff0c;是因为操作系统对TCP和UDP协议的发送方的机制不同&#xff0c;也就是问题原因在发送方 为什么UDP协议是面向报文的协议&#xff1f; 当用户消息通过UDP协议传输时&…

从原理到实践,分析 Redisson 分布式锁的实现方案(二)

上篇讲解了如何用 Redis 实现分布式锁的方案&#xff0c;它提供了简单的原语来实现基于Redis的分布式锁。然而&#xff0c;Redis作为分布式锁的实现方式也存在一些缺点。本文将引入Redisson来实现分布式锁。 一、Redisson是什么 Redisson是一个基于Redis的分布式Java框架。它提…

flutter(01) windows桌面版 编译环境安装指南

1 flutter环境安装 flutter官网参考&#xff1a;Install | Flutter 先下载flutter SDK>&#xff1a;flutter sdk下载--官网&#xff0c;之后解压到C:\Users\XXX\data&#xff08;这里以该路径为例&#xff0c;但可以为其他自定义路径&#xff09;目录下&#xff0c;在这里…

5.python设计模式【单例模式】

内容&#xff1a;保证一个类只有一个实例&#xff0c;并提供一个访问它的全局访问点角色&#xff1a; 单例&#xff08;Singleton&#xff09; UML图 举个例子&#xff1a; 需求&#xff1a;一个类只能实例化一个对象&#xff0c;不能实例化多个对象 from abc import abstract…

MATLAB与ROS联合仿真——ROS环境搭建及相关准备工作(上)

本篇文章主要介绍在安装完ROS后&#xff0c;在进行MATLAB与ROS联合仿真之前&#xff0c;需要进行的一些环境搭建以及准备工作&#xff0c;主要分为 创建ROS工作空间及功能包、必备功能包安装、安装Gazebo11、导入实验功能包至工作空间、安装Visual_Studio_Code(选做)、常用便捷…