网站建设 每年收费,陕西seo关键词优化外包,科技展厅效果图设计图,平面广告设计作品集1. 协议 在之前我们谈到#xff0c;协议就是一种约定#xff0c;socket api接口#xff0c;在读写数据时#xff0c;都是按照字符串的方式来发送接收的#xff0c;那么我们要传输一些结构化数据时怎么办呢#xff1f;,比如说一个结构…1. 协议 在之前我们谈到协议就是一种约定socket api接口在读写数据时都是按照字符串的方式来发送接收的那么我们要传输一些结构化数据时怎么办呢,比如说一个结构体 eg:
struct message{string url;string time;string id;string msg;
};
我们可以将数据变为一个字符串(有效载荷)并为其添加报头(包含数据的一些属性)最后形成一个报文这个过程就是序列化的过程 再将这个报文发送到网络中另一台主机从网络中接收到该数据将其取报头并且将字符串转换为我们上面的结构化数据这个过程就是反序列化。
业务结构化数据在发送到玩过中时先序列化再发送收到的数据一定是序列字节流要先进行反序列化然后才能使用。
2. 自定义协议
下面我们实现一个网络版本的服务端和客户端你并且自定义一个协议实现序列化反序列化的过程。
makefile文件
ccg
LD-DMYSELF
.PHONY:all
all:calServer calClientcalServer:calServer.cc$(cc) -o $ $^ -stdc11 -ljsoncpp ${LD}
calClient:calClient.cc$(cc) -o $ $^ -stdc11 -ljsoncpp ${LD}.PHONY:clean
clean:rm -f calClient calServer
log.hpp(日志)
#pragma once#include iostream
#include cstring
#include string
#include stdarg.h
#include ctime
#include unistd.husing namespace std;#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3 // 出错可运行
#define FATAL 4 // 致命错误const char* to_levelStr(int level)
{switch ((level)){case DEBUG: return DEBUG;case NORMAL: return NORMAL;case WARNING: return WARNING;case ERROR: return ERROR;case FATAL: return FATAL;default: return nullptr;}
}void logMessage(int level, const char* format, ...) // ... 可变参数列表
{
#define NUM 1024char logPreFix[NUM];snprintf(logPreFix, sizeof(logPreFix), [%s][%ld][pid: %d], to_levelStr(level), (long int)time(nullptr), getpid());char logContent[NUM];va_list arg;va_start(arg, format);vsnprintf(logContent, sizeof(logContent), format, arg);cout logPreFix logContent endl;} protocol.hpp
在这个文件中定义了请求和响应类并在类中实现了请求和响应的序列化(serialize)和反序列化(deserialize)操作并且实现了添加报头(enLength)和去报头的操作(deLength)
#pragma once#include iostream
#include cstring
#include string
#include vector
#include sys/socket.h
#include sys/types.h
#include jsoncpp/json/json.h#define SEP
#define SEP_LEN strlen(SEP) // 不能使用sizeof()
#define LINE_SEP \r\n
#define LINE_SEP_LEN strlen(LINE_SEP)enum { OK 0, DIV_ZERO, MOD_ZERO, OP_ERROR }; // _exitcode _result - content_len\r\n_exitcode _result\r\n
// _x _op _y\r\n - content_len\r\n__x _op _y\r\n
std::string enLength(const std::string text)
{std::string send_string std::to_string(text.size());send_string LINE_SEP;send_string text; send_string LINE_SEP;return send_string;
}// content_len\r\n_x _op _y\r\n - _x _op _y
bool deLength(const std::string package, std::string* text)
{auto pos package.find(LINE_SEP);if(pos std::string::npos) return false;std::string text_len_string package.substr(0, pos);int text_len std::stoi(text_len_string);*text package.substr(posLINE_SEP_LEN, text_len);return true;
}// 没有人规定我们的网络通信的时候 只能有一种协议
// 我们如何让系统知道我们用的哪一种协议
// 协议编号\r\ncontent_len\r\n_exitcode _result\r\nclass 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){*out ;// 结构化 - _x _op _y\r\n 一个请求就一行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;}// 反序列化bool deserialize(const std::string in){// _x _op _y\r\n - 结构化数据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;std::string x_string in.substr(0, left); // 前闭后开区间std::string y_string in.substr(rightSEP_LEN, strlen(in.c_str())-LINE_SEP_LEN); // 前闭后开区间if( right-(leftSEP_LEN) ! 1) return false;_op in[leftSEP_LEN]; if(x_string.empty()) return false;if(y_string.empty()) return false;_x std::stoi(x_string);_y std::stoi(y_string);return true;}public:// _x _op _y 约定int _x;int _y;char _op;
};class Response
{
public:Response():_exitcode(0),_result(0){}// 序列化bool serialize(std::string* out){*out ;std::string ec_string std::to_string(_exitcode);std::string res_string std::to_string(_result);*out ec_string;*out SEP;*out res_string;return true;}// 反序列化bool deserialize(const std::string in){// _exitcode resultauto mid in.find(SEP);if(mid std::string::npos) return false;std::string ec_string in.substr(0, mid);std::string res_string in.substr(midSEP_LEN);if(ec_string.empty() || res_string.empty())return false;_exitcode std::stoi(ec_string);_result std::stoi(res_string);}public:int _exitcode; // 0计算成功 !0表示计算失败具体是多少定好标准int _result; // 计算结果
};bool recvPackge(int sock, std::string inbuffer, std::string *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 std::string::npos) continue;std::string text_len_string inbuffer.substr(0, pos);int text_len std::stoi(text_len_string); // 得到有效载荷的长度int total_len text_len_string.size() 2*LINE_SEP_LEN text_len;if(total_len inbuffer.size()) // 缓冲区还没有读到一个完整的报文{std::cout 你输入的消息没有严格按照我们的协议, 正在等待后续的内容, continue! std::endl;continue;}std::cout 处理前#inbuffer: \n inbuffer std::endl;// 至少有一个完整的报文*text inbuffer.substr(0, total_len); // content_len\r\n_exitcode _result\r\ninbuffer.erase(0, total_len);std::cout 处理后#inbuffer: \n inbuffer std::endl;break;}else return false;}return true;
}server.hpp
服务端在接收到来自客户端的请求后将其进行反序列化、去报头得到数据在对数据进行计算得到结果后加报头序列化组成一个响应发送给客户端。
#pragma once#include log.hpp
#include protocol.hpp#include iostream
#include string
#include cstring
#include functional
#include cstdlib
#include sys/wait.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.h
#include signal.hnamespace server
{using namespace std;enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport 8080;static const int gbacklog 5;typedef std::functionbool(const Request req, Response resp) func_t;// 解耦void handlerEntery(int sock, func_t func){std::string inbuffer;while (true){// 1.读取content_len\r\n_exitcode _result\r\n// 1.1 如何保证独到的消息是 一个完整的请求呢std::string req_text, req_str;// 1.2 读取成功req_text是一个完整的请求content_len\r\n_exitcode _result\r\nif (!recvPackge(sock, inbuffer, req_text))return; // 读取失败std::cout 带报头的请求 \n req_text std::endl;if (!deLength(req_text, req_str))return;std::cout 去报头的正文 \n req_str std::endl;// 2.对请求request反序列化// 2.1 得到一个结构化的请求对象Request req;if (!req.deserialize(req_str))return;// 3.计算处理业务 req._x, req._op, req._y --- 业务逻辑// 3.1 得到一个结构化的响应Response resp;func(req, resp); // 调用的为.cc中的cal// 4.对相应Response进行序列化// 4.1 得到了一个序列化的数据std::string resp_str;resp.serialize(resp_str);std::cout 计算完成序列化响应 req_str std::endl;// 5.然后发送响应给客户端// 5.1构建成为一个完整的报文std::string send_string enLength(resp_str);std::cout 构建完整的响应\n send_string std::endl;send(sock, send_string.c_str(), send_string.size(), 0); // 这里的发送也是有问题的}}class CalServer{public:CalServer(const uint16_t port gport): _port(port), _listenSockfd(-1){}void initServer(){// 1.创建socket文件套接字对象_listenSockfd socket(AF_INET, SOCK_STREAM, 0); // 第二个参数与UDP不同if (_listenSockfd 0){// 创建套接字失败logMessage(FATAL, created socket error!);exit(SOCKET_ERR);}logMessage(NORMAL, created socket success: %d!, _listenSockfd);// 2.bind绑定自己的网络信息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;if (bind(_listenSockfd, (struct sockaddr *)local, sizeof(local)) 0){logMessage(FATAL, bind socket error!);exit(BIND_ERR);}logMessage(NORMAL, bind socket success!);// 3.设置socket 为监听状态if (listen(_listenSockfd, gbacklog) 0) // 第二个参数backlog后面会讲 5的倍数{logMessage(FATAL, listen socket error!);exit(LISTEN_ERR);}logMessage(NORMAL, listen socket success!);}void start(func_t func){for (;;){// 4.server 获取新连接 不能直接接收数据/发送数据struct sockaddr_in peer;socklen_t len sizeof(peer);int sock accept(_listenSockfd, (struct sockaddr *)peer, len); // sock 和client进行通信if (sock 0){logMessage(ERROR, accept error, next!);continue;}logMessage(NORMAL, accept a new link success, get new sock: %d!, sock); // ?// version 2, 多进程版pid_t id fork();if (id 0){// 子进程 向外提供服务 不需要监听 关闭这个文件描述符close(_listenSockfd);// if(fork() 0) exit(0); // 让子进程的子进程执行下面代码 子进程退出// serviceIO(sock);handlerEntery(sock, func); // 读取请求close(sock); // 关闭父进程的exit(0); // 最后变成孤儿进程 交给OS回收这个进程}close(sock); // 关闭子进程的// 父进程pid_t ret waitpid(id, nullptr, 0); // 阻塞式等待if (ret 0){logMessage(NORMAL, wait child process seccess);}}}~CalServer(){}private:int _listenSockfd; // 套接字 -- 不是用来通信的 是用来监听链接到来获取新链接的uint16_t _port; // 端口号};
}
server.cc
# include calServer.hpp
# include protocol.hpp
#include memoryusing namespace server;
using namespace std;static void Usage(string proc)
{cout Usage:\n\t proc local_ip local_port\n\n;
}// req一定是我们的处理好的完整的请求对象
// resp根据req进行业务处理填充resp不用管任何读取和写入序列化和反序列化等任何细节
bool cal(const Request req, Response resp)
{// req已经有结构化完成的数据了 可以直接使用resp._exitcode 0;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;
}// tcp服务器在启动上与之前的udp server一模一样
// ./tcpServer localport
int main(int argc, char *argv[])
{if(argc ! 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port atoi(argv[1]);unique_ptrCalServer tsvr(new CalServer(port));tsvr-initServer();tsvr-start(cal);return 0;
}
client.hpp
客户端从键盘输入要计算的数据和运算符和将其加包头、序列化组成一个请求字符串发送给服务端然后阻塞等待直到接收到服务端的响应后将其反序列化去报头就是得到的计算结果打印到屏幕上。
#pragma once#include iostream
#include string
#include cstring
#include ctype.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.h#include protocol.hppusing namespace std;#define NUM 1024class CalClient
{
public:CalClient(const string serverIp, const uint16_t serverPort):_sock(-1),_serverIp(serverIp),_serverPort(serverPort){}void initClient(){// 1.创建socket_sock socket(AF_INET, SOCK_STREAM, 0);if(_sock 0){cerr socket creat error! endl;exit(2);}// 2.TCP的客户端要bind 但不需要显式的bindOS自动完成// 3.要不要listen 不要// 4.要不要accept 不要// 5.要发起链接}void start(){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());if(connect(_sock, (struct sockaddr*)server, sizeof(server)) ! 0){cerr socket connect error endl;}else{string line;std::string inbuffer;while(true){cout Mycal ;getline(cin, line);Request req ParseLine(line);std::string content;req.serialize(content); // 序列化std::string send_string enLength(content);send(_sock, send_string.c_str(), send_string.size(), 0); // BUG?std::string package, text;if(!recvPackge(_sock, inbuffer, package)) continue;if(!deLength(package, text)) continue;Response resp;resp.deserialize(text);std::cout exitCode: resp._exitcode std::endl;std::cout result: resp._result std::endl;}}}const Request ParseLine(const std::string line){// 11 123*123 21/0int status 0; // 0:开始 1碰到操作符 2操作符之后int i 0;int cnt line.size();std::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;}}cout std::stoi(left) op std::stoi(right) std::endl;return Request(std::stoi(left), std::stoi(right), op);}~CalClient(){if(_sock 0) close(_sock);}private:int _sock;string _serverIp;uint16_t _serverPort;
}; client.cc
# include calClient.hpp#include memoryusing namespace std;static void Usage(string proc)
{cout Usage:\n\t proc server_ip server_port\n\n;
}int main(int argc, char *argv[])
{if(argc ! 3){Usage(argv[0]);exit(1);}string serverIp argv[1];uint16_t serverPort atoi(argv[2]);unique_ptrCalClient tcli(new CalClient(serverIp, serverPort));tcli-initClient();tcli-start();return 0;
}
上面的代码我们实现了一个简单的网络计算器代码并且在protocol.hpp文件中实现了自定义协议自己实现了服务端客户端响应与请求的序列化反序列化的操作下面为一个测试用例的代码 客户端和服务端还分别打印出了报文的形状。
在tcp中客户端和服务端发送的本质都是将数据从自定义的缓冲区中拷贝到他们的发送缓冲区在由OS决定在合适的时间发送到网络中。接受的本质就是从网络中拷贝数据到接收缓冲区再拷贝到自定义的字符串或者变量中。发送缓冲区和接收缓冲区都是独立的。
TCP是如何保证收到一个完整的报文的 -- tcp是面向字节流的所以可以明确报文和报文的边界定长、特殊符号、子描述方式。在我们上面的代码中采取的是特殊符号来确定报文的边界。
在这里再介绍两个接口 # include sys/types.h # include sys/socket.h ssize_t send(int sockfd, const void* buff, size_t len, int flags); ssize_t recv(int sockfd, void* buff, size_t len, int flags); send 和 sendto send: 用于在已连接的TCP套接字上发送数据。在使用send时操作系统知道要发送数据的套接字并且已经建立了与远程主机的连接。因此send不需要指定目标地址因为操作系统已经知道数据将被发送到哪里。sendto: 用于无连接的UDP套接字也可以用于TCP套接字。在使用sendto时需要指定目标地址和端口号因为它没有依赖于之前的连接。在TCP中尽管可以使用sendto发送数据但通常更常见的是使用send因为TCP是面向连接的协议连接已经被建立操作系统已经知道目标地址。 recv 和 recvfrom recv: 用于从已连接的TCP套接字接收数据。类似于sendrecv操作系统知道从哪个套接字接收数据因为连接已经建立。recv不需要指定源地址因为操作系统已经知道要从哪里接收数据。recvfrom: 用于无连接的UDP套接字也可以用于TCP套接字。在使用recvfrom时需要指定一个缓冲区来存储接收到的数据以及一个指向结构体的指针该结构体用于存储发送方的地址和端口号。在TCP中虽然可以使用recvfrom接收数据但通常更常见的是使用recv因为TCP是面向连接的连接已经建立。 总结来说send和recv适用于TCP套接字而sendto和recvfrom主要用于UDP套接字但它们也可以在TCP套接字上使用。在TCP中通常使用send和recv因为连接已经建立操作系统已经知道目标地址和源地址。 对于序列化和反序列化有现成的方案可以使用1.json、2.protobud、3.xml