本章我们将重谈网络协议,了解主流的网络协议,学习序列化与反序列化,并通过自己手写一个简易版的网络协议,来了解网络传输中数据的变化过程。目标已经确定,办好小板凳,准备开讲啦……
在学习本章内容之前,先复习之前的 TcpServer
本章代码详情: Gitee
网络协议:
协议是一批数据和逻辑的结合。网络协议的分层,无外乎是数据结构和逻辑的分层。协议一封层方便我们去维护,因为能很好的做到了逻辑解耦。
在我们前几章的内容学习中,我们在干什么呢?之前在做的事情:造轮子,学习网路通信,定制化服务。我们做的所有的工作都是在原生的编写应用层代码。
我们之前写的应用层没有定协议,也可以理解为发字符串是定协议,但是这个协议定制的不明显。
TCP是面向字节流的,也就是其能够发送任意数据,就能够发送结构体的二进制数据:
因为网络协议考虑到了各种问题:
这些情况的出现,可能会导致传输的数据的丢失,这是我们不愿意看到的。
所以直接传结构体这种方案是绝对不行的!!!
直接发送结构体对象是不可取的,虽然在某些情况下它确实行,但是我们是不会采取这种办法传输数据的。
我们需要进行序列化和反序列化。
序列化(Serialization)是指将数据结构或对象转换为字节流的过程:
反序列化(Deserialization)是指将字节流还原为原始数据结构或对象的过程:
TCP,UDP在通信是底层用的就是结构体定好了的,但是通常学习工作中,我们要用一些其他的东西,组件或者框架,或者其他方式,完成序列化和反序列化,在应用层。如果愿意的话,也可以用结构体定自己的协议,但是太麻烦了。
借助编程语言提供的序列化库来实现,使用第三方库如Jackson、Gson等来实现对象的序列化和反序列化。
举个栗子:
struct Date
{
int Length;
int Width;
int Height;
};
序列化:要想将其序列化,可以用一个简单的方式将其拼接成一个字符串。
Length/:Width/:Height
反序列化:客户端在收到这个字符串之后,通过查找分隔符/:
的方式,将三个变量提取出来,在本地转回所需要的结构体。
我们自定义了一个序列化和反序列化的方式,也就是制定了一个简单的的协议!
TCP是面向字节流的,因为有可能同时读到多个信息,所以要单独的区分一个子串。
字符串整体的长度对方怎么知道?
序列化之后的字符串多长呢,要在最前面携带长度有两种方案:
\r\n
作区分,然后再和后面的字符串耦合起来,"strlen\r\n "XXXXXXXXXX\r\n
。我们采用第二种方式,因为第一种方式的可读性不好。
序列化的字符串和序列化的字符串之间用\r\n
就好了,为什么还要读取前面的长度呢?
\r\n
。定的就叫做应用层协议:
客户端接收到序列化的数据后,先根据报头取出前strlen
个字节,读取到有效载荷长度后,再往后读取出完整的有效载荷。
对方在读的时候叫做: decode 解码。
我们只是简单的实现一个网络计算器,不涉及到负载运算,目的在于学习了解制定协议的过程。
我们实现的是简单的,两个操作数之间的运算:
1+1
2*3
10-7
5/9
// ...
我们规定,序列化之后的字符串格式是,操作数和操作符之间带有空格:
x + y
规定,编码之后的格式是,报头(长度)和字符之间用\r\n
分隔,有效载荷最后用\r\n
结尾:
5\r\n1 + 1\r\n
7\r\n10 + 20\r\n
对于处理结果,我们需要有两个变量(退出状态,运算结果):
exitCode_ result_
同时要想将处理结果发送出去,也要进行序列化和编码:
数据长度\r\nexitCode_ result_\r\n
此时我们就定制完成了自己的一套协议。
把协议中的分隔符给定义出来,方便以后统一使用或更改:
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define OPS "+-*/%"
验证:
在解码之前,我们要做一些明确:
// 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
std::string decode(std::string &in, uint32_t *len)
{
assert(len);
// 1. 确认是否是一个包含len的有效字符串
*len = 0;
std::size_t pos = in.find(CRLF);
if (pos == std::string::npos)
return ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)
// 2. 提取长度
std::string inLen = in.substr(0, pos);
int intLen = atoi(inLen.c_str());
// 3. 确认有效载荷也是符合要求的
int surplus = in.size() - 2 * CRLF_LEN - pos;
if (surplus < intLen)
return "";
// 4. 确认有完整的报文结构
std::string package = in.substr(pos + CRLF_LEN, intLen);
*len = intLen;
// 5. 将当前报文完整的从in中全部移除掉
int removeLen = inLen.size() + package.size() + 2 * CRLF_LEN;
in.erase(0, removeLen);
// 6. 正常返回
return package;
}
这些操作都是之前语言的功底,就不再过多的赘述。
// encode, 整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{
// "exitCode_ result_"
// "len\r\n""exitCode_ result_\r\n"
std::string encodein = std::to_string(len);
encodein += CRLF;
encodein += in;
encodein += CRLF;
return encodein;
}
我们不仅可以自己制定协议,还可以使用第三方库,这是我们以后最常用的办法,而自己制定协议只是熟悉过程,加强理解。
接下来我们来安装json
库,并在我们的代码中将我们自己定制的协议和第三方库提供的协议,两种方案来进行序列化和反序列化。
// 定制的请求 x_ op y_
class Request
{
public:
// ...
public:
// 需要计算的数据
int x_;
int y_;
// 需要进行的计算种类
char op_; // + - * / %
};
// 序列化 -- 结构化的数据 -> 字符串
// 认为结构化字段中的内容已经被填充
void serialize(std::string *out)
{
#ifdef MY_SELF
std::string xstr = std::to_string(x_);
std::string ystr = std::to_string(y_);
// std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->
*out = xstr;
*out += SPACE;
*out += op_;
*out += SPACE;
*out += ystr;
#else
//json
// 1. Value对象,万能对象
// 2. json是基于KV
// 3. json有两套操作方法
// 4. 序列化的时候,会将所有的数据内容,转换成为字符串
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter fw;
// Json::StyledWriter fw;
*out = fw.write(root);
#endif
}
我们将自己定制的协议和json一并使用起来,有两种方案,首先我们先定义一个宏:
#define MY_SELF 1
如果我们定义了MY_SELF
那么就用自己的定制的序列化方案,否则就用json
提供的。
json本身就是一个KV的方案:
除了上述通过宏定义来选择调用序列化的方式,还可以通过编译指令来进行选择,下面我们修改一下makefile
:
// 反序列化 -- 字符串 -> 结构化的数据
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// 100 + 200
std::size_t spaceOne = in.find(SPACE);
if (std::string::npos == spaceOne)
return false;
std::size_t spaceTwo = in.rfind(SPACE);
if (std::string::npos == spaceTwo)
return false;
std::string dataOne = in.substr(0, spaceOne);
std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
if (oper.size() != 1)
return false;
// 转成内部成员
x_ = atoi(dataOne.c_str());
y_ = atoi(dataTwo.c_str());
op_ = oper[0];
return true;
#else
//json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt(); // char本身就是整数
return true;
#endif
}
把控好字符的下标,就能控制的很OK了,很考验基本功。
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "x_: " << x_ << std::endl;
std::cout << "op_: " << op_ << std::endl;
std::cout << "y_: " << y_ << std::endl;
std::cout << "#################################" << std::endl;
}
// 定制的响应
class Response
{
public:
// ...
public:
// 退出状态,0标识运算结果合法,非0标识运行结果是非法的,非0是几就表示是什么原因错了!
int exitCode_;
// 运算结果
int result_;
};
// 序列化 -- 不仅仅是在网络中应用,本地也是可以直接使用的!
void serialize(std::string *out)
{
#ifdef MY_SELF
// "exitCode_ result_"
std::string ec = std::to_string(exitCode_);
std::string res = std::to_string(result_);
*out = ec;
*out += SPACE;
*out += res;
#else
//json
Json::Value root;
root["exitcode"] = exitCode_;
root["result"] = result_;
Json::FastWriter fw;
// Json::StyledWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// "0 100"
std::size_t pos = in.find(SPACE);
if (std::string::npos == pos)
return false;
std::string codestr = in.substr(0, pos);
std::string reststr = in.substr(pos + SPACE_LEN);
// 将反序列化的结果写入到内部成员中,形成结构化数据
exitCode_ = atoi(codestr.c_str());
result_ = atoi(reststr.c_str());
return true;
#else
//json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
exitCode_ = root["exitcode"].asInt();
result_ = root["result"].asInt();
return true;
#endif
}
void debug()
{
std::cout << "#################################" << std::endl;
std::cout << "exitCode_: " << exitCode_ << std::endl;
std::cout << "result_: " << result_ << std::endl;
std::cout << "#################################" << std::endl;
}
我们继续用之前服务端代码,依旧是将任务派发给线程池,只需要修改Task
队列中处理的任务,这就体现了之前做好封装的好处。
// 5.4 v3.3
Task t(serviceSock, peerIp, peerPort, netCal);
tp_->push(t);
服务端对收到的客户端发来的序列化数据,进行解码和反序列化,再交由处理calculator
处理,计算出结果返回的是一个Response
的对象,之后序列化并编码,将序列化数据再返回给用户端:
// 1. 全部手写 -- done
// 2. 部分采用别人的方案--序列化和反序列化的问题 -- xml,json,protobuf
void netCal(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
// 9\r\n100 + 200\r\n 9\r\n112 / 200\r\n
std::string inbuffer;
while (true)
{
Request req;
char buff[128];
ssize_t s = read(sock, buff, sizeof(buff) - 1);
if (s == 0)
{
logMessage(NOTICE, "client[%s:%d] close sock, service done", clientIp.c_str(), clientPort);
break;
}
else if (s < 0)
{
logMessage(WARINING, "read client[%s:%d] error, errorcode: %d, errormessage: %s",
clientIp.c_str(), clientPort, errno, strerror(errno));
break;
}
// read success
buff[s] = 0;
inbuffer += buff;
std::cout << "inbuffer: " << inbuffer << std::endl;
// 1. 检查inbuffer是不是已经具有了一个strPackage
uint32_t packageLen = 0;
std::string package = decode(inbuffer, &packageLen);
if (packageLen == 0) continue; // 无法提取一个完整的报文,继续努力读取吧
std::cout << "package: " << package << std::endl;
// 2. 已经获得一个完整的package
if (req.deserialize(package))
{
req.debug();
// 3. 处理逻辑, 输入的是一个req,得到一个resp
Response resp = calculator(req); //resp是一个结构化的数据
// 4. 对resp进行序列化
std::string respPackage;
resp.serialize(&respPackage);
// 5. 对报文进行encode
respPackage = encode(respPackage, respPackage.size());
// 6. 将计算的结果序列化的返回给用户 -- 先简单的处理了以后再细说
write(sock, respPackage.c_str(), respPackage.size());
}
}
}
计算函数:
static Response calculator(const Request &req)
{
Response resp;
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 '/':
{ // x_ / y_
if (req.y_ == 0) resp.exitCode_ = -1; // -1. 除0
else resp.result_ = req.x_ / req.y_;
}
break;
case '%':
{ // x_ / y_
if (req.y_ == 0) resp.exitCode_ = -2; // -2. 模0
else resp.result_ = req.x_ % req.y_;
}
break;
default:
resp.exitCode_ = -3; // -3: 非法操作符
break;
}
return resp;
}
客户端发将输入的内容进行判定是否符合输入规范,并将调用了makeReuquest
对Request
的对象进行初始化。对得到Request
的对象进行序列化并编码,将序列化数据发送给服务端。待服务端处理完之后,再读回来Response
对象的序列化数据,之后解码和反序列化,得到Response
对象,最后将处理结果打印出来。
std::string message;
while (!quit)
{
message.clear();
std::cout << "请输入表达式>>> "; // 1 + 1
std::getline(std::cin, message); // 结尾不会有\n
if (strcasecmp(message.c_str(), "quit") == 0)
{
quit = true;
continue;
}
// 对用户输入的内容进行清洗
// message = trimStr(message); // 1+1 1 +1 1+ 1 1+ 1 1 +1 => 1+1 -- 就不处理了
// 网络计算器计算请求:
Request req;
if(!makeReuquest(message, &req)) continue;
req.debug();
std::string package;
req.serialize(&package); // done
// std::cout << "debug->serialize-> " << package << std::endl;
package = encode(package, package.size()); // done
// std::cout << "debug->encode-> \n" << package << std::endl;
ssize_t s = write(sock, package.c_str(), package.size());
if (s > 0)
{
char buff[1024];
size_t s = read(sock, buff, sizeof(buff)-1);
if(s > 0) buff[s] = 0;
std::string echoPackage = buff;
Response resp;
uint32_t len = 0;
// std::cout << "debug->get response->\n" << echoPackage << std::endl;
std::string tmp = decode(echoPackage, &len); // done
if(len > 0)
{
echoPackage = tmp;
// std::cout << "debug->decode-> " << echoPackage << std::endl;
resp.deserialize(echoPackage);
printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);
}
}
else if (s <= 0)
{
break;
}
}
makeReuquest
函数:
bool makeReuquest(const std::string &str, Request *req)
{
// 123+1 1*1 1/1
char strtmp[BUFFER_SIZE];
snprintf(strtmp, sizeof strtmp, "%s", str.c_str());
char *left = strtok(strtmp, OPS);
if (left == nullptr)
return false;
char *right = strtok(nullptr, OPS);
if (right == nullptr)
return false;
char mid = str[strlen(left)];
req->x_ = atoi(left);
req->y_ = atoi(right);
req->op_ = mid;
std::cout << "req->x_: " << req->x_ << std::endl;
return true;
}
我们将客户端输入之后被序列话之后的数据打印出来看看: