再谈“协议”

ops/2024/10/21 5:34:27/

1.认识协议

之前我们使用TCP的方式实现了一个服务器,而TCP是面向字节流的,而UDP是面向数据报的,接下来通过一个例子区分两种的区别。

  • UDP面向数据报:就如同发快递,你发多少个快递,对面就收到多少个快递(不考虑丢包情况),并且每个快递之间都是有明显的分割的。你很容易就能区别出不同的快递。
  • TCP面向字节流:数据在传输或处理时都会被拆分成一系列的字节,然后按照特定的顺序进行读取或写入。就像水流一样,你打开水龙头时,你并不知道哪些水是自来水厂第一次给你传的,哪些是第二次给你传的。

所以我们之前写的TCP服务器有个很大的问题,就是你并不能保证读取到的就是一个完整的数据。例如我们的英汉互译词典,如果用户输入的是:carriage,但是我们接受到的是car,最后返回的就是car的汉文,那这肯定是不对的,如何解决这个问题呢,我们需要制定协议。

协议就是一种约定,网络中的通讯双方都必须遵守。

2.结构化数据

网络传输中,我们难免会遇到需要传输结构化数据的情况,例如网络聊天中,我们不仅仅需要传输别人发的消息,还包含头像,名称,时间等消息。那我们直接传送一个结构体吗。

我们使用send,recv等IO函数时是可以直接传递一个结构化的数据的,但是在实际中我们选择将这个结构体转化成一个字符串再发送给对方,为什么呢?

  1. 跨平台兼容性:字符串是一种非常基础且通用的数据类型,几乎所有的编程语言和平台都支持字符串。相比之下,结构体可能因编程语言和平台的差异而具有不同的表示方式,导致在不同的系统上传输和解析时出现问题。使用字符串作为传输介质可以避免这类跨平台兼容性问题
  2. 简化编码与解码过程:在网络传输中,需要将数据编码为可以在网络上传输的格式。对于字符串,编码和解码过程通常相对简单,因为字符串本身就是一种序列化的数据形式。而结构体在编码为可传输的格式时可能需要考虑字段顺序、大小端字节序等问题,增加了编码和解码的复杂性。

当我们知道了网络传输中应该使用字符串的方式,那我们如何构建这个字符串呢,我们以网络版本计算器举例子。

struct request
{int _data_x;char _oper;int _data_y;
};

方案一:直接发送

例如:“12+32”,但是这种方式就会出现我们之前说的问题,我们无法保证读取到的是一个完整的报文,并且报文与报文之间没有分割,所以可能实际独到的是“12+3224/12”(其中24/12是第二次发送的)。

方法二:制定协议,序列化与反序列化

  • 序列化的意思就是把一个结构化的数据转化成可以存储或传输的形式(字符串)。
  • 反序列化就是把一个字节序列转化成结构化的数据

然后我们可以制定一个协议,比如每次发送都已\n结尾,我们就可以通过\n将两个报文分开了,例如当我们收到"12+32\n24/12\n"就知道这是两次报文了。

因为是协议,双方都要遵守,所以,发送端会在每次发送的报文后面加一个\n,接收端知道读到了\n就是这个报文的结尾,这就是协议的作用。

接下来,通过一个代码,带大家更加深刻的了解协议。

3.网络版本计算器

3.1准备工作

3.1.1封装socket

在每次使用socket时,我们写的代码都差不多,所以我们可以封装一下socket。

我们可以使用一个模板方法设计模式。

class Socket
{
public:virtual ~Socket() {}virtual void CreateSocket() = 0;virtual void BindSocket(uint16_t port) = 0;virtual void ListenSocket() = 0;virtual Socket *AcceptConnect(InetAddr& addr) = 0;virtual bool ConnectServer(InetAddr& addr) = 0;virtual int Recvfrom(std::string& buffer, InetAddr* addr) = 0;virtual int Sendto(std::string buffer, InetAddr& addr) = 0;virtual int GetSockfd() = 0;virtual void SetSockfd(int _sockfd) = 0;virtual void CloseSockfd() = 0;public:void CreateBindListenSocket(uint16_t port){CreateSocket();BindSocket(port);ListenSocket();}bool CreateSocketAndConnect(InetAddr& addr){CreateSocket();return ConnectServer(addr);}
};

父类如上所示。

const int defaultbacklog = 10;
const int defaultSockfd = -1;class TcpSocket : public Socket
{
public:TcpSocket(int sockfd = defaultSockfd): _sockfd(sockfd){}~TcpSocket(){}void CreateSocket() override{if (_sockfd > 0)return;_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){lg.LogMessage(Error, "Create listensocket error");exit(CREATESOCKETERROR);}int opt = 0;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));lg.LogMessage(Normal, "Create listensocket success: %d", _sockfd);}void BindSocket(uint16_t port) override{sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = INADDR_ANY;if (::bind(_sockfd, (sockaddr *)&addr, sizeof(addr)) < 0){lg.LogMessage(Error, "Bind error");exit(BINDERROR);}lg.LogMessage(Normal, "Bind success");}void ListenSocket() override{if (::listen(_sockfd, defaultbacklog) < 0){lg.LogMessage(Error, "Listen error");exit(LISTENERROR);}lg.LogMessage(Normal, "Listen success");}Socket *AcceptConnect(InetAddr& addr) override{sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = ::accept(_sockfd, (sockaddr*)&peer, &len);if (sockfd < 0)return nullptr;addr.Init(peer);TcpSocket* newsock = new TcpSocket(sockfd);return newsock;}bool ConnectServer(InetAddr& addr) override{sockaddr_in peer = addr.GetAddr();int n = ::connect(_sockfd, (sockaddr*)&peer, sizeof(peer));if (n < 0)return false;else return true;}int Recvfrom(std::string& buffer, InetAddr* addr) override{sockaddr_in peer;socklen_t peerlen = sizeof(peer);char temp[4096];ssize_t n = recvfrom(_sockfd, temp, sizeof(temp) - 1, 0, (sockaddr*)&peer, &peerlen);if (n > 0){temp[n] = 0;buffer += temp;addr->Init(peer);}return n;}int Sendto(std::string buffer, InetAddr& addr) override{sockaddr_in peer = addr.GetAddr();ssize_t n = sendto(_sockfd, buffer.c_str(), buffer.size(), 0, (sockaddr*)&peer, sizeof(peer));return n;}int GetSockfd() override{return _sockfd;}void SetSockfd(int sockfd) override{_sockfd = sockfd;}void CloseSockfd() override{close(_sockfd);}private:int _sockfd;
};

子类TcpSocket继承父类,将来我们直接创建TcpSocket对象,即可调用创建套接字,绑定,监听等函数。

3.1.2封装sockaddr_in

每次创建sockaddr_in,我们都需要将ip转成点分十进制的字符串,port网络转主机等操作,我们可以封装一个InetAddr对象帮我们管理。

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class InetAddr
{
public:InetAddr(){}InetAddr(const sockaddr_in addr): _addr(addr){Init(addr);}void Init(const sockaddr_in& addr){_ip = inet_ntoa(addr.sin_addr);_port = ntohs(addr.sin_port);_user += _ip;_user += " : ";_user += std::to_string(_port);_addr = addr;}std::string &GetUser(){return _user;}std::string &GetIp(){return _ip;}uint16_t &GetPort(){return _port;}sockaddr_in &GetAddr(){return _addr;}private:std::string _ip;uint16_t _port;sockaddr_in _addr;std::string _user;
};

3.2服务器

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>#include "InetAddr.hpp"
#include "LogMessage.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"using callback_t = std::function<bool(std::string &, std::string *)>;class TcpServer;struct ThreadData
{
public:ThreadData(TcpSocket *&serverSock, TcpServer *self): _serverSock(serverSock), _self(self){}public:TcpSocket *_serverSock;TcpServer *_self;
};class TcpServer
{
public:TcpServer(uint16_t port, callback_t cb): headler_request(cb){_listenSock = new TcpSocket();_listenSock->CreateBindListenSocket(port);}~TcpServer(){_listenSock->CloseSockfd();delete _listenSock;}void Start(){while (true){InetAddr addr;TcpSocket *serverSock = (TcpSocket *)_listenSock->AcceptConnect(addr);lg.LogMessage(Normal, "%s connect success, serverSock: %d", addr.GetUser().c_str(), serverSock->GetSockfd());pthread_t p;ThreadData *data = new ThreadData(serverSock, this);pthread_create(&p, nullptr, Routine, data);}}static void *Routine(void *arg){pthread_detach(pthread_self());ThreadData *data = static_cast<ThreadData *>(arg);std::string buffer;InetAddr addr;while (true){ssize_t n = data->_serverSock->Recvfrom(buffer, &addr);if (n > 0){// 对请求进行构建相应std::string out;data->_self->headler_request(buffer, &out);// 将数据发送回去data->_serverSock->Sendto(out, addr);}else if (n == 0){lg.LogMessage(Normal, "you close connect, me too");break;}else{lg.LogMessage(Warning, "recvfrom error");break;}}data->_serverSock->CloseSockfd();delete data->_serverSock;delete data;}private:TcpSocket *_listenSock;public:callback_t headler_request;
};

服务端整体逻辑。

  • 1.将来server.cc可以创建一个TcpServer对象,并传入一个端口和一个回调函数,TcpServer的构造函数会自动帮我们创建套接字,绑定,监听。
  • 2.外部通过调用Start函数就可以启动服务器了,Start会accept获取到一个新连接,就创建一个新线程,并且去执行Routine函数,主线程持续获得新连接。
  • 3.Routine函数中,我们先接受对端发来的消息,然后调用回调函数去对数据进行处理,再将数据发送回去。

3.3协议

我们需要一个请求,还需要响应。

//请求
class Request
{int _data_x;int _data_y;char _oper;
};//响应
class Response
{int _result;   //结果int _code;     //0成功,1除零错误,2模零错误,3未识别的操作符
};

我们可以再添加一些功能,例如我们需要将请求和响应进行序列化和反序列化,意味着,我们需要将他转化成字节序列,也需要将字节序列转化回结构化数据,这里有两种方案。

3.3.1自定义协议

  • 方案一:我们自己手动定制协议,明确报文与报文之间的间隔。

我们可以定制这样一个协议:len\nx op y\n

len表示x op y的长度,读取时我们先查找\n,如果找到了\n就说明\n之前的就是len,我们可以先将len提取出来。得知len之后就好办了,我们可以通过len查看当前报文是否是一个完整的报文,如果是就将他拆分出来,如果不是就继续读取。

void Serialization(std::string *out)
{*out = std::to_string(_data_x) + space + _oper + space + std::to_string(_data_y);return;
}// 添加报头
void EnCode(std::string &input)
{std::string str = std::to_string(input.size());str += linebreaks + input + linebreaks;input = str;
}

将来Serialization函数用于将_data_x,_oper,_data-y序列化成结构化数据,序列化之后我们就可以调用EnCode函数添加报头了。添加完之后就变成了len\nx op y\n的格式了。

bool Deserialization(std::string &inputbuffer)
{int left = inputbuffer.find(" ");if (left == std::string::npos)return false;int right = inputbuffer.rfind(" ");if (right == std::string::npos || right <= left)return false;_data_x = atoi(inputbuffer.substr(0, left).c_str());_data_y = atoi(inputbuffer.substr(right + space.size()).c_str());_oper = inputbuffer.substr(left + space.size(), right - left - space.size())[0];return true;
}bool DeCode(std::string &package, std::string *message)
{int pos = package.find(linebreaks);if (pos == std::string::npos)return false;std::string len = package.substr(0, pos);int messagelen = atoi(len.c_str());int total = len.size() + 2 * linebreaks.size() + messagelen;if (total > package.size()){return false;}*message = package.substr(pos + linebreaks.size(), messagelen);package.erase(0, total);return true;
}

Decode函数用于解析字符串,从package函数中提取出一个完整报文放入message中,如果package函数中没有一个完整报文,就返回false,也就是说,如果返回的是true就一定能保证读取到了一个完整的报文。

当读到了一个完整的报文(我们只需要x op y的这一部分,我们直接交给Deserialization函数,他会自动帮我们完成将每一段的数据拆分出来。

方案二:使用第三方库

3.3.2使用Json库

我们可以使用Json库帮我们完成上面的逻辑,由于是第三方库,所以在使用g++编译时,需要包含库名(-ljsoncpp)

我们先了解一下json库怎么使用

3.3.2.1序列化

1.我们先创建一个Value对象,类似于万能对象

#include <iostream>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;return 0;
}

2.将需要的参数以KV的形式保存。

#include <iostream>
#include <jsoncpp/json/json.h>int main()
{Json::Value root;root["d1"] = 1;root["d2"] = 3.14;root["d3"] = "hello world";return 0;
}

3.将root写入字符串中

#include <iostream>
#include <jsoncpp/json/json.h>int main()
{// 1.创建Value对象Json::Value root;// 2.以KV形式保存数据root["d1"] = 1;root["d2"] = 3.14;root["d3"] = "hello world";// 3.创建对象写入字符串中Json::FastWriter write;std::string res = write.write(root);std::cout << res << std::endl;return 0;
}

运行一下看看。

可以看到成功将我们的数据转化成了字符串。如果你觉得这种方式看着不太方便,也可以使用StyledWriter的方式来写.

Json::StyledWriter write;

序列化的工作完成了,反序列化呢。

3.3.2.3反序列化

1.创建Reader对象

// 1.创建Reader对象
Json::Reader reader;

2.解析字符串和Value

// 1.创建Reader对象
Json::Reader reader;
// 2.解析字符串和Value对象
reader.parse(res, root);

3.读取数据

// 1.创建Reader对象
Json::Reader reader;
// 2.解析字符串和Value对象
reader.parse(res, root);
// 3.读取
int d1 = root["d1"].asInt();
float d2 = root["d2"].asFloat();
std::string d3 = root["d3"].asCString();

我们再来运行一下试试。

最后也就能成功解析出来。

3.3.2.4Json库方案

我们可以使用条件编译的方式,区分是自定义协议还是Json库方案

void Serialization(std::string *out)
{
#ifdef Customize*out = std::to_string(_data_x) + space + _oper + space + std::to_string(_data_y);return;
#elseJson::Value root;root["datax"] = _data_x;root["datay"] = _data_y;root["oper"] = _oper;Json::FastWriter writer;*out = writer.write(root);
#endif
}bool Deserialization(std::string &inputbuffer)
{
#ifdef Customizeint left = inputbuffer.find(" ");if (left == std::string::npos)return false;int right = inputbuffer.rfind(" ");if (right == std::string::npos || right <= left)return false;_data_x = atoi(inputbuffer.substr(0, left).c_str());_data_y = atoi(inputbuffer.substr(right + space.size()).c_str());_oper = inputbuffer.substr(left + space.size(), right - left - space.size())[0];return true;
#elseJson::Value root;Json::Reader reader;auto res = reader.parse(inputbuffer, root);if (res == false)return false;_data_x = root["datax"].asInt();_data_y = root["datay"].asInt();_oper = (char)root["oper"].asInt();return true;
#endif
}

Result和Response都需要序列化和反序列化的函数,这里就不过多赘述了,逻辑都类似。来看看整体代码

#pragma once#include <iostream>
#include <memory>
#include <unistd.h>
#include <cstdlib>
#include <jsoncpp/json/json.h>
#include "LogMessage.hpp"//#define Customize = 1;namespace Protocol
{const std::string space = " ";const std::string linebreaks = "\n";enum Err_Des{Success = 0,Divide_Zero,Modulo_Zero,Not_Recognized};// 添加报头void EnCode(std::string &input){std::string str = std::to_string(input.size());str += linebreaks + input + linebreaks;input = str;}// 解析报头,并放在message中// le// len// len\n// len\nx// len\nx op// len\nx op// len\nx op y\n// len\nx op y\nlen..// 7\n12 + 32\nbool DeCode(std::string &package, std::string *message){int pos = package.find(linebreaks);if (pos == std::string::npos)return false;std::string len = package.substr(0, pos);int messagelen = atoi(len.c_str());int total = len.size() + 2 * linebreaks.size() + messagelen;if (total > package.size()){return false;}*message = package.substr(pos + linebreaks.size(), messagelen);package.erase(0, total);return true;}class Request{public:Request(){}Request(int x, char oper, int y): _data_x(x), _data_y(y), _oper(oper){}//_data_x _oper _data_yvoid Serialization(std::string *out){
#ifdef Customize*out = std::to_string(_data_x) + space + _oper + space + std::to_string(_data_y);return;
#elseJson::Value root;root["datax"] = _data_x;root["datay"] = _data_y;root["oper"] = _oper;Json::FastWriter writer;*out = writer.write(root);
#endif}//_data_x _oper _data_ybool Deserialization(std::string &inputbuffer){
#ifdef Customizeint left = inputbuffer.find(" ");if (left == std::string::npos)return false;int right = inputbuffer.rfind(" ");if (right == std::string::npos || right <= left)return false;_data_x = atoi(inputbuffer.substr(0, left).c_str());_data_y = atoi(inputbuffer.substr(right + space.size()).c_str());_oper = inputbuffer.substr(left + space.size(), right - left - space.size())[0];return true;
#elseJson::Value root;Json::Reader reader;auto res = reader.parse(inputbuffer, root);if (res == false)return false;_data_x = root["datax"].asInt();_data_y = root["datay"].asInt();_oper = (char)root["oper"].asInt();return true;
#endif}int &GetX(){return _data_x;}int &GetY(){return _data_y;}char &GetOper(){return _oper;}private:int _data_x = 0;int _data_y = 0;char _oper = 0;};class Response{public:Response(){}Response(int result, int code): _result(result), _code(code){}void Serialization(std::string *out){
#ifdef Customize*out = std::to_string(_result) + space + std::to_string(_code);return;
#elseJson::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;*out = writer.write(root);
#endif}//_data_x _oper _data_ybool Deserialization(std::string &inputbuffer){
#ifdef Customizeint pos = inputbuffer.find(" ");if (pos == std::string::npos)return false;_result = atoi(inputbuffer.substr(0, pos).c_str());_code = atoi(inputbuffer.substr(pos + space.size()).c_str());return true;
#elseJson::Value root;Json::Reader reader;auto res = reader.parse(std::cin, root);if (res == false)return false;_result = root["result"].asInt();_code = root["code"].asInt();
#endif}int &GetResult(){return _result;}int &GetCode(){return _code;}private:int _result = 0;int _code = 0;};// 工厂模式class Factory{public:std::shared_ptr<Request> BulidRequest(){std::shared_ptr<Request> res = std::make_shared<Request>();return res;}std::shared_ptr<Request> BulidRequest(int x, char ch, int y){std::shared_ptr<Request> res = std::make_shared<Request>(x, ch, y);return res;}std::shared_ptr<Response> BulidResponse(){std::shared_ptr<Response> res = std::make_shared<Response>();return res;}std::shared_ptr<Response> BulidResponse(int result, int code){std::shared_ptr<Response> res = std::make_shared<Response>(result, code);return res;}};
}

下面的Factory是工厂模式,用于返回一个堆上创建的对象,也可以不使用这个,未来直接在栈上创建也行。

3.5计算器

#include <iostream>
#include <memory>
#include "Protocol.hpp"
namespace Calculator
{class Calculator{public:std::shared_ptr<Protocol::Response> Compute(std::shared_ptr<Protocol::Request> req){int x = req->GetX();int y = req->GetY();char op = req->GetOper();Protocol::Factory fact;std::shared_ptr<Protocol::Response> resp = fact.BulidResponse();resp->GetCode() = Protocol::Success;switch (op){case '+':resp->GetResult() = x + y;break;case '-':resp->GetResult() = x - y;break;case '*':resp->GetResult() = x * y;break;case '/':{if (y == 0)resp->GetCode() = Protocol::Divide_Zero;elseresp->GetResult() = x / y;break;}case '%':{if (y == 0)resp->GetCode() = Protocol::Modulo_Zero;elseresp->GetResult() = x % y;break;}default:resp->GetCode() = Protocol::Not_Recognized;break;}return resp;}};
}

逻辑很简单,就是将Request计算出结果之后,再构建一个Response对象返回回去。

3.6回调函数

还记得服务器的整体逻辑吗,前面说过,服务器所做的就是读取用户的数据, 然后调用回调函数处理数据,最后再将数据写回给用户,所以我们的网络版本服务器最核心的内容就是回调函数,如何处理用户数据。

1.准备工作,先创建好对应的对象

bool Headler(std::string& instreambuffer, std::string* out)
{//计算器Calculator::Calculator cal;//构建相应对象Protocol::Factory fact;auto request = fact.BulidRequest(); return true;
}

2.读取用户数据

bool Headler(std::string& instreambuffer, std::string* out)
{//计算器Calculator::Calculator cal;//构建相应对象Protocol::Factory fact;auto request = fact.BulidRequest();std::string message;std::string temp;while (Protocol::DeCode(instreambuffer, &message) != false){if (request->Deserialization(message) == false)return false;//读到了一个完整的报文,可以开始处理了}return true;
}

Decode函数用于解析报文,从instreambuffer中读取一个完整报文放入message当中,所以当进入while函数内部,我们能保证一定读取到了一个完整报文,然后我们对这个报文进行反序列化,就一定能拿到用户传输给我们的x,oper,y这三个数据。

bool Headler(std::string& instreambuffer, std::string* out)
{//计算器Calculator::Calculator cal;//构建相应对象Protocol::Factory fact;auto request = fact.BulidRequest();std::string message;std::string temp;while (Protocol::DeCode(instreambuffer, &message) != false){//读到了一个完整的报文,可以开始处理了if (request->Deserialization(message) == false)return false;std::cout << "Request: " << message << std::endl;auto respose = cal.Compute(request);respose->Serialization(&temp);std::cout << "Response: " << temp << std::endl;Protocol::EnCode(temp);*out += temp;}return true;
}

现在我们可以开始计算了,计算的结果就放在response当中,我们再将response反序列化成字符串,并将反序列化的结果放入temp当中,再将temp添加报头,以上就是读到一个完整报文的操作。如果有多个完整报文,操作类似。

我们以一个数据为例.

instreambuffer = "7\n12 + 32\n9123 * 1";

经过Decode函数解析之后,发现前面是一个完整的报文,所以message在处理过后等于"12 + 32"。然后将他反序列化,我们就能拿到数据了,此时request内部。

class request
{int _data_x = 12;int _data_y = 34;char _oper = '+';
};

然后我们就可以使用cal开始计算了,计算后会给我们返回一个Response对象。

class response
{   int _result = 46;int _code = 0;
};

然后我们将他序列化之后保存进temp当中。temp = "46 0",然后我们就可以添加报头了

temp = "4\n46 0\n"

再将结果放入out中,我们再次使用DeCode函数判断是否能构成一个完整报文,发现不能,函数返回。

3.7客户端

#include <iostream>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Socket.hpp"
#include "LogMessage.hpp"
#include "Protocol.hpp"void Usage()
{std::cout << "Please enter: ./Client ip port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage();return 4;}std::string ip = argv[1];uint16_t port = atoi(argv[2]);TcpSocket sockfd;sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);InetAddr peer(addr);if (sockfd.CreateSocketAndConnect(peer) == false){return 5;}std::string oper = "+-*/%@!#$&=[]";srand(time(nullptr) ^ getpid());Protocol::Factory fact;std::string send;while (true){int x = rand() % 100;usleep(rand() % 1234);int y = rand() % 100;usleep(rand() % 321);char op = oper[rand() % oper.size()];auto req = fact.BulidRequest(x, op, y);std::string request;req->Serialization(&request);std::string total = std::to_string(request.size()) + "\n" + request + "\n";std::cout << total << std::endl;sockfd.Sendto(total, peer);std::string recv;
Start:sockfd.Recvfrom(recv, &peer);int left = recv.find("\n");if (left == std::string::npos){goto Start;}int right = recv.find("\n", left + 1);if (right == std::string::npos){goto Start;}//必须确保读到了一个完整的协议std::string result = recv.substr(left + 1, right - left - 1);std::cout << request << " = " << result << std::endl;sleep(1);}return 0;
}

整体逻辑:随机创建x,y和op,然后序列化并添加报文之后发送给客户端。然后就开始接受服务端消息。(Start后面的部分用于确定读到的是一个完整报文)。

4.运行结果

最终也是可以成功完成计算器功能,而且我们也不怕遇到数据粘包问题了。

我们再试试Json库的方案。

5.源码

5.1TcpServer.hpp

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>#include "InetAddr.hpp"
#include "LogMessage.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"using callback_t = std::function<bool(std::string &, std::string *)>;class TcpServer;struct ThreadData
{
public:ThreadData(TcpSocket *&serverSock, TcpServer *self): _serverSock(serverSock), _self(self){}public:TcpSocket *_serverSock;TcpServer *_self;
};class TcpServer
{
public:TcpServer(uint16_t port, callback_t cb): headler_request(cb){_listenSock = new TcpSocket();_listenSock->CreateBindListenSocket(port);}~TcpServer(){_listenSock->CloseSockfd();delete _listenSock;}void Start(){while (true){InetAddr addr;TcpSocket *serverSock = (TcpSocket *)_listenSock->AcceptConnect(addr);lg.LogMessage(Normal, "%s connect success, serverSock: %d", addr.GetUser().c_str(), serverSock->GetSockfd());pthread_t p;ThreadData *data = new ThreadData(serverSock, this);pthread_create(&p, nullptr, Routine, data);}}static void *Routine(void *arg){pthread_detach(pthread_self());ThreadData *data = static_cast<ThreadData *>(arg);std::string buffer;InetAddr addr;while (true){ssize_t n = data->_serverSock->Recvfrom(buffer, &addr);if (n > 0){// 对请求进行构建相应std::string out;data->_self->headler_request(buffer, &out);// 将数据发送回去data->_serverSock->Sendto(out, addr);}else if (n == 0){lg.LogMessage(Normal, "you close connect, me too");break;}else{lg.LogMessage(Warning, "recvfrom error");break;}}data->_serverSock->CloseSockfd();delete data->_serverSock;delete data;}private:TcpSocket *_listenSock;public:callback_t headler_request;
};

5.2Server.cc

#include <iostream>
#include <memory>#include "TcpServer.hpp"
#include "LogMessage.hpp"
#include "InetAddr.hpp"
#include "Calculator.hpp"
#include "Protocol.hpp"void Usage()
{std::cout << "./Server port" << std::endl;
}//
bool Headler(std::string& instreambuffer, std::string* out)
{//计算器Calculator::Calculator cal;//构建相应对象Protocol::Factory fact;auto request = fact.BulidRequest();std::string message;std::string temp;while (Protocol::DeCode(instreambuffer, &message) != false){//读到了一个完整的报文,可以开始处理了if (request->Deserialization(message) == false)return false;std::cout << "Request: " << message << std::endl;auto respose = cal.Compute(request);respose->Serialization(&temp);std::cout << "Response: " << temp << std::endl;Protocol::EnCode(temp);*out += temp;}return true;
}int main(int argc, char* argv[])
{if (argc != 2){Usage();exit(4);}uint16_t port = atoi(argv[1]);TcpServer* http = new TcpServer(port, Headler);http->Start();delete http;return 0;
}

5.3Client.cc

#include <iostream>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "InetAddr.hpp"
#include "Socket.hpp"
#include "LogMessage.hpp"
#include "Protocol.hpp"void Usage()
{std::cout << "Please enter: ./Client ip port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage();return 4;}std::string ip = argv[1];uint16_t port = atoi(argv[2]);TcpSocket sockfd;sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);InetAddr peer(addr);if (sockfd.CreateSocketAndConnect(peer) == false){return 5;}std::string oper = "+-*/%@!#$&=[]";srand(time(nullptr) ^ getpid());Protocol::Factory fact;std::string send;while (true){int x = rand() % 100;usleep(rand() % 1234);int y = rand() % 100;usleep(rand() % 321);char op = oper[rand() % oper.size()];auto req = fact.BulidRequest(x, op, y);std::string request;req->Serialization(&request);std::string total = std::to_string(request.size()) + "\n" + request + "\n";std::cout << total << std::endl;sockfd.Sendto(total, peer);std::string recv;
Start:sockfd.Recvfrom(recv, &peer);int left = recv.find("\n");if (left == std::string::npos){goto Start;}int right = recv.find("\n", left + 1);if (right == std::string::npos){goto Start;}//必须确保读到了一个完整的协议std::string result = recv.substr(left + 1, right - left - 1);std::cout << request << " = " << result << std::endl;sleep(1);}return 0;
}


http://www.ppmy.cn/ops/25405.html

相关文章

(学习日记)2024.04.29:UCOSIII第五十三节:User文件夹函数概览(uC-LIB文件夹)第三部分

之前的章节都是针对某个或某些知识点进行的专项讲解,重点在功能和代码解释。 回到最初开始学μC/OS-III系统时,当时就定下了一个目标,不仅要读懂,还要读透,改造成更适合中国宝宝体质的使用方式。在学完野火的教程后,经过几经思考,最后决定自己锦上添花,再续上几章。 这…

windows rabbitMq安装

一、Erlang 环境准备 下载安装包 跟我们跑java项目&#xff0c;要装jdk类似。rabbitMQ是基于Erlang开发的&#xff0c;因此安装rabbitMQ服务器之前&#xff0c;需要先安装Erlang环境。 官网直接下载windows直装版本&#xff1a;https://www.erlang.org/downloads 无脑安装&a…

【网站项目】个人需求和地域特色的外卖推荐系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

深入理解Java消息中间件-性能优化和调优

性能优化和调优是构建高性能系统的重要环节&#xff0c;这对于Apache Kafka这样的消息中间件尤为重要。下面我们将介绍一些具体的性能优化和调优操作方式&#xff0c;以帮助你更好地利用Kafka来构建高性能的消息系统。 合理配置Kafka集群&#xff1a;合理配置Kafka集群的节点数…

同一用户使用多个用户名对服务器或共享资源进行多个连接不允许

异常&#xff1a;使用 net use 指令 访问远程目录&#xff0c;提示错误 “同一用户使用多个用户名对服务器或共享资源进行多个连接 不允许。断开之前与服务器或共享资源的所有连接&#xff0c;然后 请再试一次。” 分析&#xff1a; 分析这可能是多个用户 请求这个目录&#x…

【QEMU系统分析之实例篇(六)】

系列文章目录 第六章 QEMU系统仿真的机器创建分析实例 文章目录 系列文章目录第六章 QEMU系统仿真的机器创建分析实例 前言一、QEMU是什么&#xff1f;二、QEMU系统仿真的机器创建分析实例1.系统仿真的命令行参数2.目标机器创建过程3.static void pc_machine_initfn(Object *o…

什么是scrum中的3355?

&#xff08;学校作业&#xff09; Scrum中的3355是指Scrum框架中的三个核心角色、三个工件、五个关键事件和五个价值观。 三个核心角色包括&#xff1a; 产品负责人&#xff08;Product Owner&#xff09;&#xff1a;主要负责确定产品的功能和达到要求的标准&#xff0c;指…

AI视频教程下载:用ChatGPT提示词开发AI应用和GPTs

在这个课程中&#xff0c;你将深入ChatGPT的迷人世界&#xff0c;学习如何利用其能力构建创新和有影响力的工具。你将发现如何创建不仅吸引而且保持用户参与度的应用程序&#xff0c;将流量驱动到你的网站&#xff0c;并开辟新的货币化途径。 **课程的主要特点&#xff1a;** …