建站最好的公司排名,wordpress时钟插件,封丘县建设银行网站,腾讯企点是什么认识“协议” 文章目录 认识“协议”序列化和反序列化网络计算器引入Sock类设计协议编写服务端类启动服务端编写客户端类启动客户端程序测试 序列化和反序列化
在网络体系结构中#xff0c;应用层的应用程序会产生数据#xff0c;这个数据往往不是简单的一段字符串数据…认识“协议” 文章目录 认识“协议”序列化和反序列化网络计算器引入Sock类设计协议编写服务端类启动服务端编写客户端类启动客户端程序测试 序列化和反序列化
在网络体系结构中应用层的应用程序会产生数据这个数据往往不是简单的一段字符串数据而是具有一定意义的结构化数据应用层要想在网络中发送这个结构化数据就要将其转化成报文结构而这个将应用程序产生的结构化数据转化成报文的过程就是序列化。
数据被序列化成报文后应用程序进程就可以使用操作系统提供了网络接口将报文交付给操作系统操作系统就会完成后续的操作将数据封装然后让网卡硬件发送出去。
报文经过网络被接收方主机的网卡硬件接收后接收方主机的操作系统就会获取网卡硬件中接收到的报文并进行分用然后在进程调用操作系统提供的网络接口时将报文交给进程进程接收到报文后将报文转化成对应的结构化数据的过程就是反序列化。 程序产生的结构化数据要被序列化程序接收报文要将其反序列化不论是序列化和反序列化都要遵守一定规则这个规则就是双方的“协议”此外协议还包括如何给有效载荷添加报头成为一个完整报文如何给完整报文去除报头获得有效载荷、结构化数据的结构。一个结构化的数据按照一定的规则序列化封装后再根据序列化的规则进行封装操作的逆操作也就是反序列化一定能得到原始的结构化数据这就是协议的作用。
网络计算器
为了更好的体会序列化和反序列化的过程我们进行编码实现一个网络计算器。为了实现这个网络计算器我们需要实现一个进行计算的服务端 实现一个发送请求的客户端。我们使用客户端把要计算的两个操作数发过去, 然后由服务端进行计算, 最后再把结果返回给客户端。
引入Sock类
要实现的是网络计算器因此一定是需要使用套接字的因此我们封装一个Sock类类内部编写一些套接字操作函数。使用这个类方便后续代码的编写具体代码实现如下
static const int backlog 64;
static const int defaultfd -1;class Sock
{public:Sock() : _sock(defaultfd){}void Socket() // 创建套接字{_sock socket(AF_INET, SOCK_STREAM, 0); // TCPif (_sock 0){LogMessage(Fatal, create socket error:%s, strerror(errno)); // 打印信息到日志文件中exit(SOCKET_ERROR);}}void Bind(uint16_t port) // 绑定IP地址和端口号{struct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_addr.s_addr INADDR_ANY;local.sin_port htons(port);if (bind(_sock, (struct sockaddr *)local, sizeof(local)) 0){LogMessage(Fatal, bind socket error:%s, strerror(errno));exit(BIND_ERROR);}}void Listen(){if (listen(_sock, backlog) 0){LogMessage(Fatal, listen socket error:%s, strerror(errno));exit(LISTEN_ERROR);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in temp;memset(temp, 0, sizeof(temp));socklen_t len sizeof(temp);int sock accept(_sock, (struct sockaddr *)temp, len);if (sock 0){LogMessage(Warning, accept socket error:%s, strerror(errno));}else{*clientip inet_ntoa(temp.sin_addr);*clientport htons(temp.sin_port);}return sock;}int Connect(std::string serverip, uint16_t serverport){struct sockaddr_in server;memset(server, 0, sizeof(server));server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(serverip.c_str());server.sin_port htons(serverport);return connect(_sock, (struct sockaddr*)server, sizeof(server));}int Fd(){return _sock;}~Sock(){if (_sock ! defaultfd){close(_sock);}}private:int _sock;
};说明 想了解套接字编写的更多细节可以查看博主的另一篇博客网络套接字编程(二)-CSDN博客该Sock类内部使用了一个LogMessage函数该函数的作用是打印日志信息到日志文件中想了解该日志组件的更多细节可以查看博主的另一篇博客网络套接字编程(三)-CSDN博客。
设计协议
设计协议指定客户端和服务端序列化和反序列化的具体规则具体代码如下
#define SEP
#define SEP_LEN strlen(SEP)
#define HEAD_SEP \r\n
#define HEAD_SEP_LEN strlen(HEAD_SEP)std::string addHeader(const std::string send_string)//给有效载荷添加报头
{std::string s;s std::to_string(send_string.size());s HEAD_SEP;s send_string;s HEAD_SEP;return s;
}std::string removeHeader(int len, const std::string package)//去除报头还原有效载荷
{ std::string temp package.substr(package.size() - HEAD_SEP_LEN - len, len); return temp;
}int readPackage(int sock, std::string inbuffer, std::string *package)//读取报文函数
{ char buffer[1024];ssize_t s recv(sock, buffer, sizeof(buffer)-1, 0);//读取数据if (s 0)return -1;//读取数据出错inbuffer buffer;int pos inbuffer.find(HEAD_SEP, 0);if (pos std::string::npos) return 0;//读取到的数据中没有完整的报文std::string lenStr inbuffer.substr(0, pos);//获取有效载荷长度字符串int len toInt(lenStr);//计算有效载荷长度int targetPackageLen lenStr.size() len 2 * HEAD_SEP_LEN;//计算一个完整报文长度 if (targetPackageLen inbuffer.size()) return 0;//读取到的数据中没有完整的报文*package inbuffer.substr(0, targetPackageLen);//获取报文inbuffer.erase(0, targetPackageLen);//清除已读取的报文为下一次读取做准备return len;
}int stringSplit(const std::string inStr, const std::string sep, std::vectorstd::string *result)//对有效载荷字符切割
{size_t start 0;while(start inStr.size()){int pos inStr.find(sep, start);if(pos std::string::npos)//没找到对应字符串break;result-push_back(inStr.substr(start, pos - start));start pos sep.size();}if (start inStr.size())//将最后一个操作数写入result-push_back(inStr.substr(start));return result-size();
}int toInt(const std::string str)//将字符串转化成整形数据
{return atoi(str.c_str());
}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 std::to_string(_x);std::string y_string std::to_string(_y);*outStr x_string SEP _op SEP y_string;return true;}bool Deserialize(const std::string inStr) // 反序列化{// 将有效载荷字符串反序列化std::vectorstd::string result;int ret stringSplit(inStr, SEP, result);if (ret ! 3)//操作符和操作数的数量不正确return false;if (result[1].size() ! 1)//操作符错误return false;_x toInt(result[0].c_str());_y toInt(result[2].c_str());_op result[1][0];return true;}void Print()//打印数据{std::cout req.x: _x req.y: _y req.op _op std::endl; }public:int _x; // 左操作数int _y; // 右操作数char _op; // 操作符
};class Response // 相应结构体
{public:Response(){}Response(int result, int exitcode):_result(result), _exitcode(exitcode){}bool Serialize(std::string *outStr) // 序列化{*outStr ; // 清空//将结构化数据转化成有效载荷字符串std::string ret_string std::to_string(_result);std::string excd_string std::to_string(_exitcode);*outStr ret_string SEP excd_string;return true;}bool Deserialize(const std::string inStr) // 反序列化{std::vectorstd::string result;int ret stringSplit(inStr, SEP, result);if (ret ! 2)return false;_result toInt(result[0].c_str());_exitcode toInt(result[1].c_str());return true;}void Print()//打印数据{std::cout resp.result: _result req.exitcode: _exitcode std::endl; }public:int _result; // 计算结果int _exitcode; // 错误码
};报文的设计 为了让客户端和服务端读取数据后将每个有效载荷分离开来在发送数据前要为其添加应用层报头报文的结构有效载荷长度\r\n有效载荷\r\n在客户端和服务端读取数据时只有读到一个完整的报文结构才认为读到了一个有效载荷才会将该报文结构读取。
因此添加报头的方式如下 有效载荷 -有效载荷长度\r\n有效载荷\r\n对应上述代码中的addHeader函数参数是有效载荷字符串返回值是添加报头后报文结构字符串。
去除报头的方式如下 有效载荷长度\r\n有效载荷\r\n -有效载荷对应上述代码中的removeHeader函数参数是报文结构字符串返回值是去除报头后的有效载荷字符串。
由于实现的是计算器功能因此我们要处理的数据是类似于11的字符串将其转换成有效载荷后为1 1,中间用空格隔开。 读取报文的方式 由于要同读取一个完整报文结构的方式来将每个有效载荷分离开因此实现了readPackage函数用于将每个报文结构分割。
该函数有三个参数第一个参数是读取数据的文件描述符第二个参数是应用层级别的缓冲区第三个参数是获取一个完整报文结构输出型参数从文件中读取数据后先将数据加载到应用层级缓冲区然后使用该缓冲区内部的数据进行报文结构的查找如果当前缓冲区内没有一个完整的报文结构就退出该函数等待下一次读取文件。
报文结构查找的方式如下首先报文的结构是有效载荷长度\r\n有效载荷\r\n因此先寻找\r\n结构通过该结构找到有效载荷长度然后通过有效载荷长度计算该完整报文结构的长度然后判断缓冲区内部数据的大小是否超过该报文结构长度。最后将这个报文结构写入输出性参数中。 引入Request类和Response类 Request类是客户端向服务端发送请求所使用的结构化数据类内部实现了将Request类序列化成只包含有效载荷的字符串的Serialize成员函数还实现了将只包含有效载荷的字符串反序列化成Request类的Deserialize函数。
Response类服务端向客户端发送相应所使用的结构化数据类内部实现了将Response类序列化成只包含有效载荷的字符串的Serialize成员函数还实现了将只包含有效载荷的字符串反序列化成Response类的Deserialize函数。
Request类和Response类的序列化和反序列化都使用了stringSplit函数和toInt函数其中stringSplit函数是有效载荷切割函数toInt函数是将字符串转化成整形函数。stringSplit函数的实现方式如下由于有效载荷中操作数和操作符是由空格隔开的因此使用空格将左操作数、操作符、右操作数分离开来。落实到代码中就是设计了一个存储字符串的vector结构的result变量查找第一个空格将空格前的数据写入result[0],查找第二个空格将两个空格之间的数据写入result[1],将第二个空格后的数据写入result[2]由于是形如1 1的字符串因此result[0]为左操作数result[1]为操作数符result[2]为右操作数
编写服务端类
我们将计算器服务端封装成类类内部实现服务端的初始化服务端启动的函数计算器任务执行函数具体代码实现如下
using func_t std::functionResponse(const Request );class TcpServer;class ThreadData
{public:ThreadData(int sock, std::string clientip, uint16_t clientport, TcpServer *tsvr): _sock(sock), _clientip(clientip), _clientport(clientport), _tsvr(tsvr){}public:int _sock;std::string _clientip;uint16_t _clientport;TcpServer *_tsvr;
};class TcpServer
{public:TcpServer(func_t func, uint16_t port) : _func(func), _port(port){}void initServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();LogMessage(Info, Init server done,listensock:%d, _listensock.Fd());}void start(){std::string clientip;uint16_t clientport;while (true){pthread_t tid;int sock _listensock.Accept(clientip, clientport);ThreadData *td new ThreadData(sock, clientip, clientport, this);pthread_create(tid, nullptr, ThreadRoutine, td);}}static void *ThreadRoutine(void *args){pthread_detach(pthread_self());ThreadData *td static_castThreadData *(args);td-_tsvr-ServiceIO(td-_sock, td-_clientip, td-_clientport);delete td;return nullptr;}void ServiceIO(int sock, std::string clientip, uint16_t clientiport){std::string inbuffer; // 读取报文缓冲区while (true){// 1.读取报文并获得有效载荷std::string package;int n readPackage(sock, inbuffer, package);if (n -1)break;else if (n 0)continue;package removeHeader(n, package);// 2.请求反序列化Request req;req.Deserialize(package);req.Print();// 3.处理请求Response res _func(req); // 业务逻辑res.Print();// 4.相应序列化std::string send_string;res.Serialize(send_string);// 5.添加报头并发送报文send_string addHeader(send_string);send(sock, send_string.c_str(), send_string.size(), 0);}close(sock);}private:uint16_t _port;Sock _listensock;func_t _func; // 网络服务方法--进行计算
};任务执行实体 服务端主线程初始化进行初始化操作后服务端每获取一个客户端的连接后会为其创建一个新线程由这个新线程来担任任务执行的实体。主线程只做监听和获取连接的操作新线程来完成计算器的任务。其中新线程会调用ThreadRoutine函数执行任务。 ThreadData类的作用 由于完成任务的是新线程因此新线程需要得到完成任务所需的参数包括进行数据传输的套接字文件描述符、客户端IP地址、客户端端口号、服务端类的指针等。需要服务端类的指针的原因是任务执行函数ServiceIO函数是服务端类的成员函数需要使用服务端类的指针调用该函数。其中ServiceIO函数中的数据计算作为网络服务方法在创建类对象时传入。
启动服务端
首先启动服务端前对输入的命令行参数进行纠错处理其次创建服务端类后需要传入数据计算方法和要绑定的端口具体代码如下
Response calculate(const Request 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._exitcode 1;elseresp._result req._x / req._y;break;case %:if (req._y 0)resp._exitcode 2;elseresp._result req._x % req._y;break;default:resp._exitcode 3;break;}return resp;
}void Usage(std::string proc)
{std::cout Usage:\n\t proc port\n std::endl;
}int main(int argc, char *argv[])
{if (argc ! 2){Usage(argv[0]);exit(USAGE_ERROR);}uint16_t port atoi(argv[1]);std::unique_ptrTcpServer tsvr(new TcpServer(calculate, port));tsvr-initServer();tsvr-start();return 0;
}编写客户端类
和服务端相同将客户端也封装成类提供客户端初始化成员函数提供客户端运行函数具体代码实现如下
class TcpClinet
{
public:TcpClinet(std::string serverip, uint16_t serverport) : _serverip(serverip), _serverport(serverport){}void initClient(){_sock.Socket();int n _sock.Connect(_serverip, _serverport);if (n -1){LogMessage(Fatal, connect socket error:, strerror(errno));exit(-1);}}void start(){std::string buffer;while (true){Request req;// 1. 获取操作数和操作符int x;int y;char op;std::cout Pleas enter x;std::cin req._x;std::cout Pleas enter y;std::cin req._y;std::cout Pleas enter op;std::cin req._op;// 2. 将结构体数据序列化std::string req_string;req.Serialize(req_string);req.Print();// 3. 添加报头req_string addHeader(req_string);// 4. 发送报文send(_sock.Fd(), req_string.c_str(), req_string.size(), 0);// 5. 接收相应std::string package;int n 0;do{n readPackage(_sock.Fd(), buffer, package);if (n 0)break;} while (n 0);if (n 0)break;// 6. 去掉报头package removeHeader(n, package);// 7. 反序列化Response resp;resp.Deserialize(package);resp.Print();}}private:Sock _sock;std::string _serverip;uint16_t _serverport;
};客户端运行 使用start函数运行客户端后先接收用户输入的计算数据然后封装成Request类的结构化数据对其序列化发送接收数据后反序列化转换成Response类的结构体化数据输出。
启动客户端
和服务端相同首先启动服客户端前对输入的命令行参数进行纠错处理具体代码如下
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[0]);exit(USAGE_ERROR);}std::string serverip argv[1];uint16_t serverport atoi(argv[2]);std::unique_ptrTcpClinet tcvr(new TcpClinet(serverip, serverport));tcvr-initClient();tcvr-start();return 0;
}程序测试
首先启动服务端并绑定端口号为8083 启动客户端并输入服务端的IP地址和端口号 在客户端分别输入左操作数、右操作数、操作符查看结果 再尝试进行除法运算 由于除数为0因此返回码为1。
说明 该程序只有在用户输入的数据符合协议时才具有健壮性除此之外可能存在问题比如如果客户端和服务器分别在不同的平台下运行在这两个平台下计算出请求结构体和响应结构体的大小可能会不同此时就可能会出现一些问题。虽然当前代码存在很多潜在的问题但这个代码能够让我们直观地体会到协议的作用这里将其当作一份示意性代码就行了。