四.再谈协议_网络计算器

序言

在之前章节中,我们认识了一些网络的接口,并简单体验了一下网络编程,成功实现了字符串数据的收发,模拟实现了最基本的一个多人聊天室,但是网络编程就仅仅局限于此吗?那还远远不够!
最基本的一点在于,我们之前收发的都仅仅是一个字符串,那我们可以发送一个结构体给对面的服务器吗?不如说,发送一个结构体(类),才是生活中更为普遍遇到的情况.
比如我们登录某个网站,实际就是向对面的服务器,发送我们的个人用户信息,从逻辑上看,其实就是发送一个结构体(类),里面包含不同的信息,诸如性别,账号,密码等等
但是在底层,所有数据都是按照0,1二进制序列进行传输的,计算机只认识二进制序列!
那我们现在有一段32位的二进制序列,发送给对方
对方应该如何解读这个数据呢?
我们可以将它看作是4个char(8bit)的数据,也可以看作1个int类型数据
不同的解读方式,有着不同的结果!就像藏头诗一样,你照旧读,这是一首普通的诗,但是假如双方已经约定好解读诗的方式,就能解读出真实的结果.
现在回想起,我们体验的网络编程,成功实现字符串数据的收发,其实默认的前提就是,我们都把我们收发的数据看作是一个字符串数据,对面收到后,按照字符串格式来对二进制序列进行解读!
我们把网络通信的参与方必须遵循的相同规则,或者说对数据的相同解读方式,称作协议
协议的本质就是双方约定好的某种格式的数据,常见的就是用结构体或者类来进行表达
它并不是计算机工程师们脑袋一拍,瞬间就想出来的,而是设计用来解决会遇到的数据解读问题!
而不同的操作系统,其实结构体存储方式可能并不会一样,占据的大小也不同,如何解决这个问题呢?
我们人类所擅长的就是知识迁移的能力,将复杂未解决的问题,往已经解决过,已有的知识上去靠拢
为什么我们不把结构体先转变为字符串,再进行发送呢?
对端接收到字符串数据,再按照双方约定好的格式,转变为结构体
不就解决了我们的问题吗?

我们把
对象的状态信息转换为可以存储或传输的形式(字节序列)的过程(结构体转字符串),称作为序列化
将把字节序列恢复为对象的过程(字符串转结构体),称作为反序列化

所以序列化,反序列化的技术出现,也不是凭空出现的,而是为了便于网络通信!
序列化保证了无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列
而反序列化其实就是序列化的逆过程,将二进制序列转变为我们真正想要的结构体数据.
接下来,我们将自己手写一个协议,并用一下别人已经写好的协议,来加深这个过程的理解,最后再做一个简单的总结.

手写体验协议(网络计算器)

下面从零到有实现一个网络版的计算器,主要目的是感受一下什么是协议

1.封装Sock.hpp

单独封装一个sock.hpp文件,将我们之前用到的网络系统接口函数,诸如socket,bind,listen,accept,connect全部进行封装,并包含log.hpp,err.hpp头文件,这样以后我们往上再进一步编写的时候,就不用重复进行编写了.

#pragma once#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include "Log.hpp"
#include "err.hpp"static const int gbacklog = 32;
static const int default_fd = -1;class Sock
{
public:Sock() : _sock(default_fd) {}~Sock() {}// 获取套接字void Socket(){_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){LogMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}}// 绑定 需要外部提供端口号void Bind(const 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; // 随机指定ipif (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0){LogMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));exit(BIND_ERR);}}// 监听void Listen(){if (listen(_sock, gbacklog) < 0){LogMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));exit(LISTEN_ERR);}}// 接收连接int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(_sock, (struct sockaddr *)&client, &len);if (sock < 0){LogMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));}else{*clientip = inet_ntoa(client.sin_addr);*clientport = ntohs(client.sin_port);}return sock;}// 连接int Connect(const std::string &serverip, const std::uint16_t &serverport){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());return connect(_sock, (struct sockaddr *)&server, sizeof(server));}int Fd(){return _sock;}void Close(){if (_sock != default_fd){close(_sock);}}private:int _sock; // 套接字
};

2.编写tcp_server.hpp

完成Sock.hpp头文件编写后,服务器的代码编写就变得简单很多了,只要创建一个Sock对象,然后调用相应的方法即可.(这也是我们的目的,便于编程!)
整体编写的逻辑和我们之前编写的tcp服务器完全相同,这里不再赘述,具体参考之前的tcp文章
并进行多线程改造,这样能够使多个客户端,都能连接我们编写的服务器
每一个线程启动的时候,都会自动运行ThreadRoutine函数,调用其中的ServiceIO方法(实际多线程对数据进行处理的函数)

#pragma once#include <functional>
#include <pthread.h>
#include "Sock.hpp"
#include "Protocol.hpp"namespace tcpserver_ns
{using namespace protocol_ns;using func_t = function<Response(const Request &)>;class tcpServer;class ThreadData{public:ThreadData(int sock,uint16_t port,std::string ip,tcpServer *td):_sock(sock),_port(port),_ip(ip),_td(td){}~ThreadData(){}public:int _sock;uint16_t _port;std::string _ip;tcpServer* _td;};class tcpServer{public:tcpServer(const uint16_t &port):_port(port){}~tcpServer(){_listensock.Close();}// 初始化服务器void InitServer(){_listensock.Socket(); //获取套接字_listensock.Bind(_port); //绑定_listensock.Listen();  //监听LogMessage(Info, "init server done, listensock: %d", _listensock.Fd());}// 启动服务器void Start(){while(true){std::string clientip;uint16_t clientport;int sock = _listensock.Accept(&clientip,&clientport);if(sock < 0)  //没有接收到连接,则继续重连continue;//连接成功LogMessage(Debug,"get a new client, client info : [%s:%d]",clientip.c_str(),clientport);pthread_t tid;ThreadData* td = new ThreadData(sock,clientport,clientip,this);pthread_create(&tid,nullptr,ThreadRoutine,td);}}static void* ThreadRoutine(void* args){pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);td->_td->ServiceIO(td->_sock,td->_ip,td->_port);LogMessage(Debug, "thread quit, client quit ...");delete td;return nullptr;}void ServiceIO(int sock, const std::string &ip, const uint16_t &port){//...}private:uint16_t _port;   // 服务器端口号Sock _listensock; // 监听套接字func_t _func;};
}

3.简单验证tcp_server.hpp

编写对应的makefile代码

.PHONY:all
all: client serverclient:calculatorclient.ccg++ -o $@ $^ -std=c++11 -lpthread
server:calculatorserver.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f client server

并完成calculatorserver.cc的调用逻辑

#include "tcp_server.hpp"
#include <memory>using namespace tcpserver_ns;int main()
{uint16_t port = 8888;std::unique_ptr<tcpServer> tsvr(new tcpServer(port));tsvr->InitServer(); //初始化服务器tsvr->Start(); //运行服务器return 0;
}

编译代码后,在Xshell上输入telnet 127.0.0.1 + 服务器端口号,看能否成功运行,即可验证代码是否发生出错

4.Protocol.hpp编写

剩下的就是编写我们的ServiceIO方法
不过在这之前,我们还需要完成Protocol.hpp的编写,即完成我们协议的定制
(协议的本质就是双方约定好的某种格式的数据,常见的就是用结构体或者类来进行表达)
协议头文件里面我们需要编写两个类(用结构体也行,不过是C++,入乡随俗,那就用类吧)
一个是我们的Request(请求)类,是用户端发送给我们服务器端的 另一个则是Response类,是服务器端计算好结果后,返回给我们用户端的类
每一个类里面,都必须包含序列化,反序列化两大方法
这其中的逻辑也很好理解
对于用户端来说,用户端既要接收到服务器端的数据,也要能向服务器端发数据(请求)
而对于服务器端来说,也是同理,服务器端既要能接收到用户端的数据,也要能向用户端发数据(响应)
这样两者才能进行协商沟通.

Serialize方法 ------- struct->string
Deserialize 方法 -------- string->struct

这是两个类的成员变量设计,其中response类我们除了结果外,还有多一个code成员变量,用来指示结果的可信度
假如正确获得结果,就设为0;除0,就设为1;模上0,就设为2;遇到非法运算符(除了+-×÷%外的其它运算符),就设为3

#pragma once//请求
struct request{int _x; //左操作数int _y; //右操作数char _op; //操作符
};//响应
struct response{int _code; //计算状态int _result; //计算结果
};

那如何将一个结构体变成一个字符串呢?
调用string类里面的to_string方法,将类成员转成字符串,再拼接起来即可,这个难度不大.
那如何将一个字符串变成一个结构体呢?
第一,实现StringSplit函数,它的功能是将字符串按照我们的分隔符(空格)进行切割,然后存到vector中
第二,实现ToInt函数,它的功能是将字符串转成int
有了这两个函数,我们编写反序列的代码就变得比较轻松,将字符串进行切割,再逐一提取,转成类成员属性即可.
由于反序列化都要用到,我们就干脆将这两个方法封装到Util.hpp,包含对应头文件,则结构体直接调用完成即可

//Util.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <cstring>
#include <vector>
using namespace std;class Util
{
public:// 输入: const &// 输出: *// 输入输出: &static bool SplitString(const string &str, const string &sep, vector<std::string> *result){//10 + 20ssize_t start = 0;while(start < str.size()){auto pos = str.find(sep,start);  //找出空格的位置if(pos == string::npos)  break;  //假如已经到了尽头,则跳出循环result->push_back(str.substr(start,pos - start));start = pos + sep.size();  //加上分隔符长度}if(start < str.size())  result->push_back(str.substr(start));return true;}static int ToInt(const std::string &str){return atoi(str.c_str());}
};

包含Util.hpp,即可完成协议的编写

#define SEP " "
#define SEP_LEN strlen(SEP)
class Request
{
public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _op(op){}// 结构体转换为字符串bool Serialize(std::string *outstr){*outstr = "";std::string x_string = to_string(_x);std::string y_string = to_string(_y);*outstr = x_string + SEP + _op + SEP + y_string;std::cout << "Request Serialize:\n"<< *outstr << std::endl;return true;}// 字符串转换为结构体bool Deserialize(std::string &instr){// inStr : “10 + 20” => [0]=>10, [1]=>+, [2]=>20// string -> vectorstd::vector<std::string> v;Util::SplitString(instr, SEP, &v); // 将字符串进行切割,然后存放到vector中// 判错if (v.size() != 3)return false; // 假如没有分割出三个字符,则返回错误if (v[1].size() != 1)return false;_x = Util::ToInt(v[0]);_y = Util::ToInt(v[2]);_op = v[1][0];return true;}~Request() {}public:int _x;int _y;char _op;
};class Response
{
public:Response() {}Response(int result, int code): _result(result), _code(code){}// 结构体转换为字符串~Response() {}bool Serialize(std::string *outstr){//_result _code*outstr = "";std::string res_string = std::to_string(_result);std::string code_string = std::to_string(_code);*outstr = res_string + SEP + code_string;std::cout << "Response Serialize:\n"<< *outstr << std::endl;return true;}// string->structbool Deserialize(const std::string &instr){std::vector<std::string> v;Util::SplitString(instr, SEP, &v); // 将字符串进行切割,然后存放到vector中// 判错if (v.size() != 2)return false; // 假如没有分割出两个字符,则返回错误_result = Util::ToInt(v[0]);_code = Util::ToInt(v[1]);return true;}

协议完成后,我们就可以继续完成ServiceIO接口的编写
总共可以分为五大步骤

第一步,从网络文件中读出字符串
第二步,Request结构体,反序列化字符串
第三步,用户上层业务逻辑处理,将Response结构体,处理完数据后,返回对应的Response结构体
第四步,序列化,形成可发送字符串
第五步,网络发送

假如先不考虑从网络文件中读出字符串这一问题,则上述的五大步骤对应的代码如下:

void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
{//从网络文件中接收到数据std::string message;//假如成功读出字符串数据Request req;req.Deserialize(message);   //反序列化字符串,转成结构体//业务处理Response res = _func(req);//序列化转成字符串std::string send_string;res.Serialize(&send_string);//网络发送send(sock, send_string.c_str(), send_string.size(), 0);
}

现在回到我们第一个步骤,即从网络文件中读出字符串
有一个问题,我们无法避免,即如何保证读出来的是一个完整的字符串呢?
"10 + 20"这个字符串,服务器端也可以收到"10 + 2"的时候,就认为已经全部读取完毕,所以单纯的直接接收,并不能保证我们读取的是完整的一个报文!

这里补充一个小的知识点
我们调用read,write(send,recv)等系统接口函数,并不是直接发送给对端的,而是将数据拷贝到我们内核维护的缓冲区中
就像你发快递,不是你直接发快递,而是你把包裹放到菜鸟驿站,由菜鸟驿站帮你发送快递

所以,为了保证我们读取的是完整的一个报文,在这里还需要设计协议
其实就是对字符串进行一点改造,比如在有效数据之前加上有效数据的长度,没有读取到对应的长度,就说明没有完整读取!
还有两个字符串之间还要加上\r\n,不然又会回到我们最开始的问题!——无法区分两个字符串
但其实仔细思考,为什么要添加长度呢?直接有效载荷+\r\n不就可以区分了吗?
那假如发送的就是\r\n呢?有效载荷里面还有其它字符呢?所以有效数据的长度还是非常有必要添加的!

“10 + 20”=====>”7”\r\n””10 + 20””\r\n” ====>报头+有效载荷
比如说“10 + 20”,它的长度就是7;则可以变成”7”\r\n””10 + 20”的这种形式,而”7”\r\n”其实就是我们所说的报头

协议中还需要编写两个新的函数

AddHeader函数
“10 + 20”=====>”7”\r\n””10 + 20””\r\n”

RemoveHeader函数
”7”\r\n””10 + 20””\r\n” =====>“10 + 20”

宏定义Header_Sep,Header_Sep_Len
这样以后直接修改宏定义即可,并非一定限定\r\n作为报头分隔符

// "10 + 20" => "7"\r\n""10 + 20"\r\n => 报头 + 有效载荷
// 请求/响应 = 报头\r\n有效载荷\r\n
std::string AddHeader(const std::string &str)
{std::cout << "AddHeader 之前:\n"<< str << std::endl;std::string s = to_string(str.size());s += HEADER_SEP;s += str;s += HEADER_SEP;std::cout << "AddHeader 之后:\n"<< s << std::endl;return s;
}
// "7"\r\n""10 + 20"\r\n => "10 + 20"
// 从后往前提取
std::string RemoveHeader(const std::string &str,int len)
{std::cout << "RemoveHeader 之前:\n"<< str << std::endl;std::string res = str.substr(str.size() - HEADER_SEP_LEN - len,len);std::cout << "RemoveHeader 之后:\n"<< res << std::endl;return res;
}

完成上述两个函数编写后,就可以继续编写ServiceIO函数第一步
记住这一定是一个循环读取的过程!边读取,边检测测试,只有读到完整的所有字符串,才能进行我们后序的第二,三,四,五操作.

函数参数类型设计
Const &:输入型参数
*:输出型参数(函数内部改变后,外部也跟着改变)
&:输入输出型参数

对于读取这个功能,我们需要编写ReadPackage函数,两个参数sock(用于函数内部调用系统接口recv,从哪个网络文件中读取数据);package(函数内部改变后,外部也要跟着改变,设计为指针,最后我们实际要操作的字符串);
还要一个参数inbuffer,作为缓冲区,将每次读到的信息存起来

// 读取消息
int ReadMessage(int sock,std::string &inbuffer,std::string *message)
{std::cout << "ReadPackage inbuffer 之前:\n"<< inbuffer << std::endl;// 边读取char buffer[1024];ssize_t s = recv(sock,buffer,sizeof(buffer) - 1,0);if(s <= 0)return -1;  //没有成功读取buffer[s] = 0;inbuffer += buffer;std::cout << "ReadPackage inbuffer 之中:\n"<< inbuffer << std::endl;// 边分析, "7"\r\n""10 + 20"\r\nsize_t pos = inbuffer.find(HEADER_SEP);if(pos == std::string::npos)   return 0;std::string len_str = inbuffer.substr(0,pos);int len = Util::ToInt(len_str);  //得到有效字符的长度int targetPackageLen = len_str.size() + 2*HEADER_SEP_LEN + len;   //得到子串的真实长度if(inbuffer.size() < targetPackageLen) //假如现在得到的字符长度比真实长度还小,重新读取return 0;//读取到一个完整的字符串*message = inbuffer.substr(0,targetPackageLen);inbuffer.erase(0,targetPackageLen);std::cout << "ReadPackage inbuffer 之后:\n"<< inbuffer << std::endl;return len;
}

返回的参数为成功读取到的字符长度,这样便于我们后续添加报头等操作
假如返回值为0(报文没有读完,读取时出错,则外部循环会一直调用读取);返回值为-1(这说明网络文件都读取失败,调用系统接口时出现问题,直接退出循环)
最后完整的ServiceIO函数如下:

void ServiceIO(int sock, const std::string &ip, const uint16_t &port)
{std::string inbuffer;while (true){std::string message;int len = ReadMessage(sock, inbuffer, &message);if (len == 0){continue;  //循环继续读取,直到读完整个字符串}else if (len == -1){break;    //读取发生错误,直接退出循环}else{// 去掉报头message = RemoveHeader(message, len);// 假如成功读出字符串数据Request req;req.Deserialize(message); // 反序列化字符串,转成结构体// 业务处理Response res = _func(req);// 序列化转成字符串std::string send_string;res.Serialize(&send_string);// 添加报头send_string = AddHeader(send_string);// 网络发送send(sock, send_string.c_str(), send_string.size(), 0);}}
}

完整版的服务器tcp_server.hpp的代码如下:

//tcp_server.hpp
#pragma once#include <functional>
#include <pthread.h>
#include "Sock.hpp"
#include "Protocol.hpp"namespace tcpserver_ns
{using namespace protocol_ns;using func_t = function<Response(const Request &)>;class tcpServer;class ThreadData{public:ThreadData(int sock, uint16_t port, std::string ip, tcpServer *td): _sock(sock), _port(port), _ip(ip), _td(td){}~ThreadData(){}public:int _sock;uint16_t _port;std::string _ip;tcpServer *_td;};class tcpServer{public:tcpServer(func_t func, const uint16_t &port): _func(func), _port(port){}~tcpServer(){_listensock.Close();}// 初始化服务器void InitServer(){_listensock.Socket();    // 获取套接字_listensock.Bind(_port); // 绑定_listensock.Listen();    // 监听LogMessage(Info, "init server done, listensock: %d", _listensock.Fd());}// 启动服务器void Start(){while (true){std::string clientip;uint16_t clientport;int sock = _listensock.Accept(&clientip, &clientport);if (sock < 0) // 没有接收到连接,则继续重连continue;// 连接成功LogMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);pthread_t tid;ThreadData *td = new ThreadData(sock, clientport, clientip, this);pthread_create(&tid, nullptr, ThreadRoutine, td);}}static void *ThreadRoutine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_td->ServiceIO(td->_sock, td->_ip, td->_port);LogMessage(Debug, "thread quit, client quit ...");delete td;return nullptr;}void ServiceIO(int sock, const std::string &ip, const uint16_t &port){std::string inbuffer;while (true){std::string message;int len = ReadMessage(sock, inbuffer, &message);if (len == 0){continue;  //已经读完了}else if (len == -1){break;    //读取发生错误,直接退出循环}else{// 去掉报头message = RemoveHeader(message, len);// 假如成功读出字符串数据Request req;req.Deserialize(message); // 反序列化字符串,转成结构体// 业务处理Response res = _func(req);// 序列化转成字符串std::string send_string;res.Serialize(&send_string);// 添加报头send_string = AddHeader(send_string);// 网络发送send(sock, send_string.c_str(), send_string.size(), 0);}}}private:uint16_t _port;   // 服务器端口号Sock _listensock; // 监听套接字func_t _func;};
}

5.编写calculatorserver.cc

tcp_server.hpp完成后,即可完成源文件的编写
其实与之前测试的源文件编写没有什么区别,但是我们需要完成上层业务处理的部分,具体来说,输入一个request结构体,需要返回我们的Response结构体

#include "tcp_server.hpp"
#include <memory>using namespace tcpserver_ns;
// ./calserver 8888
//调用逻辑
Response Calculate(const Request &req)
{// 走到这里,一定保证req是有具体数据的!Response resp(0, 0);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._code = 1;elseresp._result = req._x / req._y;break;case '%':if (req._y == 0)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}
//用户使用手册
static void Usage(string proc)
{std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}
int main(int argc,char* argv[])
{   //假如传入的参数不是两个,而不是指定了对应的端口号,则打出对应的使用列表,并退出if(argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<tcpServer> tsvr(new tcpServer(Calculate, port));tsvr->InitServer();tsvr->Start();return 0;
}

6.calculatorclient.cc编写

和之前tcp编写的整体逻辑一样,由于用户端并不需要绑定,监听等环节,所以整体编写难度,其实会比服务器端简单不少,这里也是和tcp一样,没有进行封装,全部代码都在源文件内了.
第一步,创建套接字

// 1.创建套接字,连接   Close方法(Server.hpp)
Sock sock;
sock.Socket();
// 不需要bind,也不需要listen
int n = sock.Connect(serverip, serverport);
if (n != 0)
{return 1; // 连接失败
}

第二步,就是完成和服务器端一样的五大步骤编写,依旧是不断循环,边读取,边执行,两者是一一对应的

第一步,用户录入数据(服务器端则是从网络文件中读取)
第二步,Request结构体,序列化字符串,并添加报头
第三步,向服务器端发送(此时服务器端接收,并进行业务处理)
第四步,获取响应,并去掉报头(序列化,形成可发送字符串,网络发送)
第五步,反序列化,获得对应结构体

我们可以把前三步,划分为向服务器端发送;把后两步,划分为服务器端处理完结果后,用户端获取对应服务器端的响应

向服务器端发送

这里有两个部分需要注意
第一个录入数据时,不要直接cin,毕竟我们输入的格式类型应该是"10 + 20",字符之间是以空格进行划分,我们需要的是整行直接进行读取,所以要用getline函数
第二个则是将一个字符串(如"10 + 20")进行切割,并返回一个Request结构体,这里我们可以直接调用我们之前实现的SplitString函数,也可以直接编写一个对应的切割函数

enum
{LEFT,MID,RIGHT
};
//"10 + 20"
Request ParseLine(const std::string &line)
{std::string left, right;char op;int status = LEFT;int i = 0;while (i < line.size()){switch (status){case LEFT:if (isdigit(line[i])) // 假如是数字,则压入左字符串left.push_back(line[i++]);elsestatus = MID;break;case MID:op = line[i++];status = RIGHT;break;case RIGHT:if (isdigit(line[i])) // 假如是数字,则压入左字符串right.push_back(line[i++]);break;}}Request req;req._x = std::stoi(left);req._y = std::stoi(right);req._op = op;return req;
}

完成切割字符串函数的编写后,即可完成接收的逻辑

// 2.录入数据
std::cout << "Enter# ";
std::string line;
getline(std::cin, line);// 3.序列化,并添加报头
Request req = ParseLine(line); // 将得到的数据进行切割,并返回一个结构体
std::string SendString;
req.Serialize(&SendString); //序列化
SendString = AddHeader(SendString); // 添加报头// 4.send
send(sock.Fd(), SendString.c_str(), SendString.size(), 0);

获取对应服务器端的响应

获取响应的整体部分逻辑和服务器端是相同的,也是不断循环,边读取,边检测测试,只有读到完整的所有字符串,才能进行我们后序的操作

//接收std::string package;int n = 0;
START:  // 5.获取响应n = ReadMessage(sock.Fd(),buffer,&package);if(n == 0){goto START;}else if(n == -1){break;}else{   // 6.去掉报头package = RemoveHeader(package,n);// 7.反序列化Response rep;rep.Deserialize(package);//输出最终结果std::cout << "result: " << rep._result << "[code: " << rep._code << "]" << std::endl;}

整体代码如下:

#include <iostream>
#include <string>
#include <cstdio>
#include <ctype.h>
#include "Sock.hpp"
#include "Log.hpp"
#include "Protocol.hpp"
#include "err.hpp"using namespace protocol_ns;enum
{LEFT,MID,RIGHT
};
//"10 + 20"
Request ParseLine(const std::string &line)
{std::string left, right;char op;int status = LEFT;int i = 0;while (i < line.size()){switch (status){case LEFT:if (isdigit(line[i])) // 假如是数字,则压入左字符串left.push_back(line[i++]);elsestatus = MID;break;case MID:op = line[i++];status = RIGHT;break;case RIGHT:if (isdigit(line[i])) // 假如是数字,则压入左字符串right.push_back(line[i++]);break;}}Request req;req._x = std::stoi(left);req._y = std::stoi(right);req._op = op;return req;
}
// 用户使用手册
static void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[1]);exit(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);// 1.创建套接字,连接   Close方法(Server.hpp)Sock sock;sock.Socket();// 不需要bind,也不需要listenint n = sock.Connect(serverip, serverport);if (n != 0){return 1; // 连接失败}std::string buffer;while (true){   //发送// 2.录入数据std::cout << "Enter# ";std::string line;getline(std::cin, line);// 3.序列化Request req = ParseLine(line); // 将得到的数据进行切割,并返回一个结构体std::string SendString;req.Serialize(&SendString);// 4.添加报头SendString = AddHeader(SendString);// 5.sendsend(sock.Fd(), SendString.c_str(), SendString.size(), 0);//接收std::string package;int n = 0;
START:  // 6.获取响应n = ReadMessage(sock.Fd(),buffer,&package);if(n == 0){goto START;}else if(n == -1){break;}else{   // 7.去掉报头package = RemoveHeader(package,n);// 8.反序列化Response rep;rep.Deserialize(package);std::cout << "result: " << rep._result << "[code: " << rep._code << "]" << std::endl;}}//9.关闭对应的网络文件sock.Close();return 0;
}

用用别人写好的协议

序列化和反序列化这个过程其实是有些复杂的,更不用说我们一个结构体里面包含的成员变量可能是很多的,难不成每一次都要我们自己编写对应的序列化和反序列化代码,并制定对应的协议吗?
实际上并不需要,我们可以直接用别人写的,诸如json,xml,protobuf,都是为了解决对应序列化和反序列的问题,我们只需要直接调用即可.
我们定义一个宏 #define MYSELF 1
即可用条件编译的方式来对比复现代码

#IFDEF

#ELSE

#ENDIF

我们本次采用的是json,在使用前需要提权升为管理员下载对应的json库

sudo yum install -y jsoncpp-devel

使用的时候,需要包含对应的头文件

#include <jsoncpp/json/json.h>

Value 万能对象,接受任意的kv类型
FastWriter:是用来进行序列化的 struct->string write方法
StyledWriter:也是用来进行序列化的 不过显示数据的时候,会比FastWriter输出更好看,格式更规范
Reader: 反序列化 parse方法
asINT就类似我们的ToInt函数,能够将字符串转成int类型

makefile也要对应修改,在后面加上-ljsoncpp才能进行编译,否则会报错

.PHONY:all
all: client serverclient:calculatorclient.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
server:calculatorserver.ccg++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:rm -f client server

修改后的完整代码如下:

//calculatorserver.cc
#pragma once#include <iostream>
#include <cstring>
#include <vector>
#include <jsoncpp/json/json.h>
#include "Sock.hpp"
#include "Util.hpp"
#include "Log.hpp"//#define MYSELF 1
// const &: 输入
// *: 输出
// &: 输入输出
namespace protocol_ns
{
#define SEP " "
#define SEP_LEN strlen(SEP)
#define HEADER_SEP "\r\n"
#define HEADER_SEP_LEN strlen("\r\n")// "长度"\r\n""_x _op _y"\r\n// "10 + 20" => "7"\r\n""10 + 20"\r\n => 报头 + 有效载荷// 请求/响应 = 报头\r\n有效载荷\r\nstd::string AddHeader(const std::string &str){std::cout << "AddHeader 之前:\n"<< str << std::endl;std::string s = to_string(str.size());s += HEADER_SEP;s += str;s += HEADER_SEP;std::cout << "AddHeader 之后:\n"<< s << std::endl;return s;}// "7"\r\n""10 + 20"\r\n => "10 + 20"std::string RemoveHeader(const std::string &str, int len){std::cout << "RemoveHeader 之前:\n"<< str << std::endl;std::string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);std::cout << "RemoveHeader 之后:\n"<< res << std::endl;return res;}// 读取消息int ReadMessage(int sock, std::string &inbuffer, std::string *message){std::cout << "ReadPackage inbuffer 之前:\n"<< inbuffer << std::endl;// 边读取char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s <= 0)return -1; // 没有成功读取buffer[s] = 0;inbuffer += buffer;std::cout << "ReadPackage inbuffer 之中:\n"<< inbuffer << std::endl;// 边分析, "7"\r\n""10 + 20"\r\nsize_t pos = inbuffer.find(HEADER_SEP);if (pos == std::string::npos)return 0;std::string len_str = inbuffer.substr(0, pos);int len = Util::ToInt(len_str);                                   // 得到有效字符的长度int targetPackageLen = len_str.size() + 2 * HEADER_SEP_LEN + len; // 得到子串的真实长度if (inbuffer.size() < targetPackageLen)                           // 假如现在得到的字符长度比真实长度还小,重新读取return 0;*message = inbuffer.substr(0, targetPackageLen);inbuffer.erase(0, targetPackageLen);std::cout << "ReadPackage inbuffer 之后:\n"<< inbuffer << std::endl;return len;}class Request{public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _op(op){}// 结构体转换为字符串bool Serialize(std::string *outstr){
#ifdef MYSELF*outstr = "";std::string x_string = to_string(_x);std::string y_string = to_string(_y);*outstr = x_string + SEP + _op + SEP + y_string;std::cout << "Request Serialize:\n"<< *outstr << std::endl;
#elseJson::Value root;root["x"] = _x;root["y"] = _y;root["op"] = _op;Json::StyledWriter writer;*outstr = writer.write(root);  //序列化
#endifreturn true;}// 字符串转换为结构体bool Deserialize(std::string &instr){
// inStr : 10 + 20 => [0]=>10, [1]=>+, [2]=>20
// string -> vector
#ifdef MYSELFstd::vector<std::string> v;Util::SplitString(instr, SEP, &v); // 将字符串进行切割,然后存放到vector中// std::cout << v[0] << " " << v[1] << " " << v[2] << std::endl;//  判错if (v.size() != 3)return false; // 假如没有分割出三个字符,则返回错误if (v[1].size() != 1)return false;_x = Util::ToInt(v[0]);_y = Util::ToInt(v[2]);_op = v[1][0];
#elseJson::Value root;Json::Reader reader;reader.parse(instr, root); //反序列化_x = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();#endifPrint();return true;}void Print(){std::cout << "_x: " << _x << std::endl;std::cout << "_y: " << _y << std::endl;std::cout << "_op: " << _op << std::endl;}~Request() {}public:int _x;int _y;char _op;};class Response{public:Response() {}Response(int result, int code): _result(result), _code(code){}// 结构体转换为字符串~Response() {}bool Serialize(std::string *outstr){
//_result _code
#ifdef MYSELF*outstr = "";std::string res_string = std::to_string(_result);std::string code_string = std::to_string(_code);*outstr = res_string + SEP + code_string;std::cout << "Response Serialize:\n"<< *outstr << std::endl;
#elseJson::Value root;root["result"] = _result;root["code"] = _code;Json::StyledWriter writer;*outstr = writer.write(root);  //序列化#endifreturn true;}// string->structbool Deserialize(const std::string &instr){
#ifdef MYSELFstd::vector<std::string> v;Util::SplitString(instr, SEP, &v); // 将字符串进行切割,然后存放到vector中// 判错if (v.size() != 2)return false; // 假如没有分割出两个字符,则返回错误_result = Util::ToInt(v[0]);_code = Util::ToInt(v[1]);
#elseJson::Value root;Json::Reader reader;reader.parse(instr, root); //反序列化_result = root["result"].asInt();_code = root["code"].asInt(); 
#endifPrint();return true;}void Print(){std::cout << "_result: " << _result << std::endl;std::cout << "_code: " << _code << std::endl;}public:int _result;int _code;};
}

可以看到对比我们自己实现的序列化和反序列化功能,完全可以无缝衔接,并且功能强大更多,编写更加简便,这就是别人大佬写好的协议!

总结

一张图概括整篇文章内容
上层业务逻辑决定了结构化字段每一份是什么内容,据此来定制我们的协议
序列化和反序列化,其实就是类(结构体)与字符串之间的互相转换,调用网络接口进行数据的收发(本质是数据的拷贝)
数据的收发对应其实就是用户端向服务器端请求对应资源,服务器端给用户端对应的资源(图片,视频,运算结果等等)响应!
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ChatGPT Edu版本来啦:支持GPT-4o、自定义GPT、数据分析等

5月31日&#xff0c;OpenAI在官网宣布&#xff0c;推出ChatGPT Edu版本。 据悉&#xff0c;这是一个专门为大学校园提供的ChatGTP&#xff0c;支持GPT-4o、网络搜索、自定义GPT、数据分析、代码生成等功能&#xff0c;可以极大提升学生、老师的学习质量和教学效率。 目前&…

Mac OS 用户开启 8080 端口

开启端口 sudo vim /etc/pf.conf # 开放对应端口 pass out proto tcp from any to any port 8080 # 刷新配置文件 sudo pfctl -f /etc/pf.conf sudo pfctl -e获取本机ip地址 ifconfig en0 | grep inet | grep -v inet6 | awk {print $2}访问指定端口

星创编辑器在投放业务中的落地|得物技术

搭建一个落地页需要涉及到多方合作&#xff0c;需要不断地进行沟通协调。繁杂的流程需要耗费很多的时间&#xff0c;因此我们推动产品重新搭建了一个专门服务于软广投放流程的编辑器——星创&#xff0c;完成广告搭建在投放业务各系统中的闭环。 一、落地页技术架构 名词解释…

贰[2],VisionMaster/.NetCore的WPF应用程序调用控件

1,环境 VisionMaster4.2 VisualStudio2022 WPF/.Net6.0 2,记录原因 .NetFrameWork的WPF应用程序调用添加例程.NetFrameWork的Winform应用程序相应的库,不会出现报错,界面也能正常显示操作,但是.NetCore的程序却总是报错。 2.1,.NetFrameWork的WPF应用程序 注:但是.…

AI绘画SD入门教程:ControlNet篇-Canny边缘检测预处理器

大家好&#xff0c;我是向阳 在本篇中&#xff0c;我来讲讲如何使用预处理器和辅助模型&#xff0c;分别都有些什么作用。 &#x1f4a1; 这里说明一下当你调用预处理器而辅助模型显示为无的几种原因&#xff1a; 当已载入SD1.5的模型时&#xff0c;CannyXL的辅助模型不会显示…

【笔记】Sturctured Streaming笔记总结(Python版)

目录 相关资料 一、概述 1.1 基本概念 1.2 两种处理模型 &#xff08;1&#xff09;微批处理 &#xff08;2&#xff09;持续处理 1.3 Structured Streaming和Spark SQL、Spark Streaming关系 二、编写Structured Streaming程序的基本步骤 三、输入源 3.1 File源 &a…

django 内置 JSON 字段 使用场景

Django 内置的 JSON 字段&#xff08;JSONField&#xff09;是在 Django 3.1 版本中引入的&#xff0c;用于处理 JSON 格式的数据。JSONField 允许在数据库表中存储和查询 JSON 数据&#xff0c;并且在与 Python 代码交互时自动转换为合适的 Python 数据类型。以下是一些常见的…

【golang学习之旅】Go中的cron定时任务

系列文章 【golang学习之旅】报错&#xff1a;a declared but not used 【golang学习之旅】Go 的基本数据类型 【golang学习之旅】深入理解字符串string数据类型 【golang学习之旅】go mod tidy 【golang学习之旅】记录一次 panic case : reflect: reflect.Value.SetInt using…

国产打印何去何从?汉印瞄准突破口,推进发展新质生产力

推动发展新质生产力&#xff0c;已成为当前时代的主题&#xff0c;代表着先进生产力的发展方向。 打印行业因其高门槛性和技术复杂性&#xff0c;以及在信息安全领域中的作用&#xff0c;使其在我国“新质生产力”发展中占据关键位置。同时&#xff0c;打印行业融合了高精尖产…

windows10镜像文件官网下载

官网 下载 Windows 10 光盘映像&#xff08;ISO 文件&#xff09; https://www.microsoft.com/zh-cn/software-download/windows10ISO/

QT开源 串口调式工具

都是基础的代码不详细解释&#xff0c;代码比较多福利链接

【C++练级之路】【Lv.24】异常

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《算法神殿》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、异常的概念及定义1.1 异常的概念1.2 异常的定义 二、异常的使用2.1 异常的栈展开匹配2.2 异常的重新…

Window10磁盘的分盘和合并

注意&#xff1a; 当我们c盘不够大需要扩大磁盘空间时&#xff0c;当c盘后面没有未划分的磁盘时候&#xff0c;我们是无法进行扩充c盘的&#xff0c;此时&#xff0c;我们可以先删除后面一个磁盘&#xff0c;再进行扩大。 如下&#xff1a;c盘后没有未分配的空间&#xff0c;…

6月4(信息差)

&#x1f30d;AI预测极端天气提速5000倍&#xff01;微软发布Aurora&#xff0c;借AI之眼预测全球风暴 &#x1f384;理解老司机&#xff0c;超越老司机&#xff01;LeapAD&#xff1a;具身智能加持下的双过程自驾系统&#xff08;上海AI Lab等&#xff09; 论文题目&#xf…

Flutter开发效率提升1000%,Flutter Quick教程之定义Api(三)

将tab键切换到Response&#xff0c;会出现这么一个界面 这是添加api返回的json数据。比如我们添加一个json数据。 添加完json数据后&#xff0c;右上角有一个删除按钮。要换json数据的话&#xff0c;可以点击清除再重新输入。 这时候&#xff0c;左边的面板上还会显示出 这个的…

Python实现PPT表格的编写包含新建修改插图(收藏备用)

自动创建一个ppt文件并创建好表格 代码要用到pptx库 pip install python-pptx 创建含有表格的ppt文件代码&#xff1a; from pptx import Presentation from pptx.util import Inches# 创建一个PPT对象 ppt Presentation()# 添加一个幻灯片 slide ppt.slides.add_slide(p…

用框架思维学Java:集合概览

集合这个词&#xff0c;耳熟能详&#xff0c;从小学一年级开始&#xff0c;每天早上做操时都会听到这两个字&#xff1a; 高中数学又学习到了新的集合&#xff1a; 那么Java中的集合是什么呢&#xff1f; 一&#xff0c;前言 1&#xff0c;什么是Java集合 数学集合是Java集…

Java 垃圾回收

文章目录 1 Java 垃圾回收1.1 JVM1.2 Java 对象生命周期 2 如何判断一个对象可被回收2.1 引用计数算法2.2 可达性分析算法 3 垃圾回收过程3.1 总体过程3.2 为什么要进行世代垃圾回收&#xff1f;3.3 分代垃圾回收过程 在 C 和 C 中&#xff0c;许多对象要求程序员声明他们后为其…

【第三节】C/C++数据结构之栈与队列

目录 一、数据结构-栈 1.1 栈的定义 1.2 栈的 ADT (Abstract Data Type) 1.3 栈的顺序存储结构及实现 二、数据结构-队列 2.1 队列的定义 2.2 队列的 ADT 2.3 队列的顺序存储结构与实现 2.4 优先队列 2.5 各种队列异同点 一、数据结构-栈 1.1 栈的定义 栈(Stack)可…

Web3设计风格和APP设计风格

Web3设计风格和传统APP设计风格在视觉和交互设计上有一些显著的区别。这些差异主要源于Web3技术和理念的独特性&#xff0c;以及它们在用户体验和界面设计中的具体应用。以下是Web3设计风格与传统APP设计风格的主要区别。北京木奇移动技术有限公司&#xff0c;专业的软件外包开…