1. 协议
在之前我们谈到,协议就是一种"约定",socket api接口,在读写数据时,都是按照"字符串"的方式来发送接收的,那么我们要传输一些"结构化"数据时怎么办呢?,比如说一个结构体 eg:
struct message{string url;string time;string id;string msg;
};
我们可以将数据,变为一个字符串(有效载荷),并为其添加报头(包含数据的一些属性),最后形成一个报文,这个过程就是序列化的过程 再将这个报文发送到网络中;另一台主机从网络中接收到该数据,将其取报头,并且将字符串转换为我们上面的结构化数据,这个过程就是反序列化。
业务结构化数据在发送到玩过中时先序列化再发送,收到的数据一定是序列字节流,要先进行反序列化,然后才能使用。
2. 自定义协议
下面我们实现一个网络版本的服务端和客户端,你并且自定义一个协议,实现序列化反序列化的过程。
makefile文件:
cc=g++
LD=-DMYSELF
.PHONY:all
all:calServer calClientcalServer:calServer.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}
calClient:calClient.cc$(cc) -o $@ $^ -std=c++11 -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.h>using 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(pos+LINE_SEP_LEN, text_len);return true;
}// 没有人规定我们的网络通信的时候 只能有一种协议
// 我们如何让系统知道我们用的哪一种协议?
// "协议编号"\r\n"content_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(right+SEP_LEN, strlen(in.c_str())-LINE_SEP_LEN); // 前闭后开区间if( right-(left+SEP_LEN) != 1) return false;_op = in[left+SEP_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 result"auto 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(mid+SEP_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.h>namespace 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::function<bool(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 <memory>using 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_ptr<CalServer> 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.hpp"using 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 但不需要显式的bind,OS自动完成// 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){// "1+1" "123*123" "21/0"int 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 <memory>using 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_ptr<CalClient> 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套接字接收数据。类似于send,recv操作系统知道从哪个套接字接收数据,因为连接已经建立。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