在TCP协议中我们通过网络传输的数据都是字节流形式的,如果我们想要通过网络传输结构化数据又应该怎么做呢?
比如说我们想通过网络传输一个结构体,结构体里面有整型、浮点型、字符型的数据,我们就不建议通过网络直接传输结构体。举个例子,假如我们要实现一个网络版本的计算器,让客户端输入要计算的数据,然后发送给服务端进行计算,客户端就可以定义一个data结构体来保存数据。
如果我们直接在客户端将结构体通过网络发送给服务端,在有些情况下服务端可以收到并且不会出现任何错误,假设服务端接收到的数据放在buffer中,它可以将buffer强转成为data*类型,然后就可以访问结构体内的数据了。
但直接传输结构体很容易出现问题,举个例子,如果我们的服务器是在Linux操作系统下编译运行的,但客户端并不是在Linux下运行的,有可能是Windows下,有可能是Android下,也有可能是IOS下,这样的话我们就很难保证客户端发送过去的结构体大小和服务端接收到的结构体大小是否相同。如果结构体大小不相同,在接收数据的时候就会出现数据错误、解析错误等问题。所以通过网络直接传输结构化数据是不可取的。
要想通过网络传输结构化数据,我们需要对结构化数据进行序列化和反序列化。
我们继续看上述的网络计算器例子,既然客户端不能直接通过网络传输结构化数据,那我们可以在客户端先将结构化数据转化成字节流式数据,比如转化成字符串,将字符串发送给服务端。服务端收到字符串数据之后,再将字符串的内容提取出来转化成结构化数据,就可以对数据进行处理了。
由于网络传输是有协议的,协议本质就是一种约定,所以我们可以约定在传输过程中的字符串代表什么含义。比如我们可以约定将结构体内的数据按照冒号进行分割,每一部分分别代表的是运算数1:运算数2:运算符。有了这个约定,服务端也就能够看懂客户端传过来的字符串是什么含义。
在这个过程中,客户端将结构化数据转化成字节流数据就叫作序列化,服务端将字节流数据转化成结构化数据就叫作反序列化。
为了方便序列化和反序列化之后的数据读取,我们还需要在序列化好的内容前面加上一个字段,用来表示内容的长度,这样在读取的时候首先读取长度字段,拿到内容的长度之后再根据这个长度去读取内容。
加长度字段有两种方案:第一种是在内容的前面加上长度,长度大小是确定的比如说就是4个字节,所以在读取的时候首先读取上前4个字节就是内容的长度,这种方式的实现比较简单,但是可读性比较差;第二种是在内容的前面加上长度,长度也是以字符串的形式添加,长度字符串与内容字符串之间通过我们规定的字符来间隔,这样在读取的时候首先读取到第一个间隔字符就可以拿到长度字段,这种方式的实现比较复杂,但是可读性比较好。
我们写一个网络版本的计算器,该计算器由客户端接收用户输入的运算式子,客户端将式子发送给服务端,服务端进行运算之后返回给客户端。
我们需要自定义一套简单的协议来模拟实现一下序列化和反序列化的过程,定义两个类Request
类和Response
类,Request
类负责处理请求的序列化和反序列化操作,Response
类负责处理响应的序列化和反序列化操作。
成员函数:
Request类的成员函数分别是_x表示第一个运算数,_y表示第二个运算数,_op表示运算符。
private:
int _x;
int _y;
char _op;
serialize:
serialize函数是序列化函数,我们要将Request类中的成员变量转换成一条字符串,也就是将结构化数据转换成序列化数据。其实就是一个简单的将整数转换成字符串,然后将字符串进行拼接,拼接成一个新的字符串之后再返回。
// 序列化
// 100 + 200
// 必须添加上空格,因为deEncode是根据空格进行查找的
string serialize()
{
string package = to_string(_x);
package += SPACE;
package += _op;
package += SPACE;
package += to_string(_y);
return package;
}
deSerialize:
deSerialize函数是反序列化函数,与上面的序列化函数相反,我们要将形参传进来的字符串拆分成一个个整型数据,再将整型数据填充到Request的成员变量中,也就是将序列化数据转换成结构化数据。
形参in传递进来的是形如“100 + 200”格式的字符串,所以我们需要提取的有三部分,分别是第一个运算数、运算符以及第三个运算数。所以我们通过查找空格的方式来拆分字符串,首先正向查找第一个空格,第一个空格前面的字符串就是第一个运算数;然后逆向查找最后一个空格,最后一个空格后面的字符串就是第二个运算数。最后变量第一个空格和最后一个空格之间的字符,取出运算符即可。
// 反序列化
// 100 + 200
bool deSerialize(const string &in)
{
size_t left_pos = in.find(SPACE);
if (left_pos == string::npos)
{
return false;
}
string left = in.substr(0, left_pos);
size_t right_pos = in.rfind(SPACE);
if (right_pos == string::npos)
{
return false;
}
string right = in.substr(right_pos + SPACE_LEN);
for (int i = left_pos; i < right_pos; i++)
{
if (in[i] == '+' || in[i] == '-' || in[i] == '*' || in[i] == '/' || in[i] == '%')
{
_op = in[i];
break;
}
}
_x = atoi(left.c_str());
_y = atoi(right.c_str());
return true;
}
Request类代码:
class Request
{
public:
Request()
: _x(0), _y(0), _op('\0')
{
}
~Request()
{
}
public:
// 序列化
// 100 + 200
// 必须添加上空格,因为deEncode是根据空格进行查找的
string serialize()
{
string package = to_string(_x);
package += SPACE;
package += _op;
package += SPACE;
package += to_string(_y);
return package;
}
// 反序列化
// 100 + 200
bool deSerialize(const string &in)
{
size_t left_pos = in.find(SPACE);
if (left_pos == string::npos)
{
return false;
}
string left = in.substr(0, left_pos);
size_t right_pos = in.rfind(SPACE);
if (right_pos == string::npos)
{
return false;
}
string right = in.substr(right_pos + SPACE_LEN);
for (int i = left_pos; i < right_pos; i++)
{
if (in[i] == '+' || in[i] == '-' || in[i] == '*' || in[i] == '/' || in[i] == '%')
{
_op = in[i];
break;
}
}
_x = atoi(left.c_str());
_y = atoi(right.c_str());
return true;
}
public:
int getX() const
{
return _x;
}
int getY() const
{
return _y;
}
char getOp() const
{
return _op;
}
void setX(int x)
{
_x = x;
}
void setY(int y)
{
_y = y;
}
void setOp(char op)
{
_op = op;
}
// 打印反序列化结果
void printDeSerialize()
{
cout << "##############################" << endl;
cout << "x: " << _x << endl;
cout << _op << endl;
cout << "y: " << _y << endl;
cout << "##############################" << endl;
}
private:
int _x;
int _y;
char _op;
};
成员函数:
Response类的成员函数分别是_exit_code表示退出码,如果运算式子是合法式子,退出码将被设置为0,正常返回运算结果。如果运算式子是非法式子,比如出现除零操作,非法运算符等,退出码将被设置为负数,不做计算。_result表示计算结果。
private:
int _exit_code; // 退出码
int _result; // 计算结果
serialize:
serialize是序列化函数,Response类的序列化函数是将成员变量变成字符串,按照"exit_code result"的形式拼接成字符串后返回。
// 序列化
// 按照exit_code result格式进行序列化
string serialize()
{
string package = to_string(_exit_code);
package += SPACE;
package += to_string(_result);
return package;
}
deSerialize:
deSerialize函数是反序列化函数,形参in的格式是”exit_code result",所以我们在提取子串的时候,依旧是根据空格来查找,这里只有一个空格,只要找到这个空格的位置,空格前面的字符串就是exit_code,空格后面的字符串就是result。
// 反序列化
// exit_code result
bool deSerialize(const string &in)
{
size_t pos = in.find(SPACE);
if(pos == string::npos)
{
return false;
}
string exit_code = in.substr(0, pos);
string result = in.substr(pos + SPACE_LEN);
_exit_code = atoi(exit_code.c_str());
_result = atoi(result.c_str());
return true;
}
Response类代码:
class Response
{
public:
Response()
: _exit_code(0), _result(0)
{
}
~Response()
{
}
public:
// 序列化
// 按照exit_code result格式进行序列化
string serialize()
{
string package = to_string(_exit_code);
package += SPACE;
package += to_string(_result);
return package;
}
// 反序列化
// exit_code result
bool deSerialize(const string &in)
{
size_t pos = in.find(SPACE);
if(pos == string::npos)
{
return false;
}
string exit_code = in.substr(0, pos);
string result = in.substr(pos + SPACE_LEN);
_exit_code = atoi(exit_code.c_str());
_result = atoi(result.c_str());
return true;
}
public:
int getExitCode() const
{
return _exit_code;
}
int getResult() const
{
return _result;
}
void setExitCode(int exit_code)
{
_exit_code = exit_code;
}
void setResult(int result)
{
_result = result;
}
private:
int _exit_code; // 退出码
int _result; // 计算结果
};
encode函数是添加长度字段函数,在序列化完成之后,我们还需要为序列化好以后的字符串添加长度字段。encode函数有两个形参,字符串in代表的是序列化好后的字符串,len代表的是in字符串的长度。所以我们首先需要创建一个新的字符串,这个新的字符串先加上in字符串的长度,然后加上分隔符,再加上in字符串本身的内容,最后再加上分隔符后返回。
// 添加长度字段
// 3\r\n0 1\r\n
// 9\r\n100 + 200\r\n
string encode(const string &in, int len)
{
string package = to_string(len);
package += CRLF;
package += in;
package += CRLF;
return package;
}
deEncode函数是用来提取长度字段的函数。在我们获取到序列化数据之后,它一定是带有长度字段的字符串,我们要先通过deEncode函数将它的长度字段提取出来,然后再返回剩下的运算式子。
deEncode函数有两个参数,in字符串代表的是需要提取长度字段的字符串,len是一个输出型参数,将提取出来的长度转换成len输出出去。
in字符串传递进来的可能是9\r\n100 + 200\r\n
这种完整的字符串,也可能是9\r\n100 +
这种不完整的字符串,也可能是9\r\n100 + 200\r\n9\r\n100 +
这种比完整还要长的字符串。
所以在对in进行解析的时候,首先正向查找第一个分隔符,如果第一个分隔符都没找到,说明连长度字段都没有读取完整,那就返回空串,len设置为0。
如果读取到了完整的长度字段,那就接着读取in剩下的子串,需要判断一下剩下的子串长度是否大于或者等于长度字段的标记长度,比如长度字段标记的长度是9,说明完整式子的长度应该是9,如果剩下的子串长度小于9,说明还是没有读到一个完整的式子,依然是返回空串,len设置为0。
当读取到一个完整的式子之后,就可以拆分字符串提取出长度字段和运算式子了。需要注意的是,提取出来之后,我们需要将提取的内容从原来的in字符串中删除,否则下次拆分的时候就会出现重复读取。
// 提取长度字段
// 如果读取上来的是完整的一条式子,就提取长度字段,返回除去长度字段后的剩下内容
// 如果读取上来的不是完整的式子,就什么也不做
// 9\r\n100 + 200\r\n
string deEncode(string &in, int *len)
{
*len = 0;
size_t pos = in.find(CRLF);
// 没有读取到完整的长度字段
if (pos == string::npos)
{
return "";
}
// 走到这里一定是读取到了完整的长度字段,
// 提取长度字段并接着提取剩下的字段
string str_len = in.substr(0, pos);
int int_len = atoi(str_len.c_str());
// 截取剩下的字段判断长度
string leave_code = in.substr(pos + CRLF_LEN);
// 如果剩下字段的长度比int_len小,则证明没有读取到完整的一条式子,返回空串
if (leave_code.size() < int_len)
{
return "";
}
// 走到这里一定是读取到了完整的一条式子
string package = in.substr(pos + CRLF_LEN, int_len);
*len = int_len;
// 最后还要清空in中已经读取上来的内容
int remove_len = str_len.size() + int_len + 2 * CRLF_LEN;
in.erase(0, remove_len);
return package;
}
我们还是采用TCP套接字的方式来实现网络通信,服务端首先是编写通用的TCP服务器代码,这里直接介绍业务实现阶段的代码:
netCal:
netCal函数是提供计算服务的核心代码,实现的思路是首先从客户端中读取内容,读取到的内容一定是一个添加了长度字段的序列化内容,所以读取上来之后,需要调用deEncode函数提取长度字段。但是我们不能保证每次读取都一定能读取到完整的运算式子,所以根据deEncode函数的返回值,如果没有读取到完整的运算式子,len会被设置为0,所以如果len被设置为0说明还没有读取到完整的运算式子,那么继续读取。
当读取到一条完整的运算式子之后,我们需要创建Request对象对其进行反序列化,将序列化数据转换成为结构化数据。反序列化完成以后,我们就可以开始计算了,计算好之后再创建Response对象,填充成员变量并进行序列化,序列化好之后再调用encode函数添加长度字段,最后就可以将结果发送回给客户端了。
void netCal(int accept_socket)
{
string req_package;
while (true)
{
Request req;
char buffer[128];
// 从客户端读取数据
// 9\r\n100 + 200\r\n
ssize_t readRes = read(accept_socket, buffer, sizeof(buffer) - 1);
if (readRes < 0)
{
cerr << "read error" << endl;
break;
}
else if (readRes == 0)
{
cout << "client quit" << endl;
break;
}
// 走到这里一定是读取成功了
buffer[readRes] = '\0';
req_package += buffer;
int req_package_len = 0;
string req_message = deEncode(req_package, &req_package_len);
// 如果还没读取到一条完整的式子,就继续读取
if (req_package_len == 0)
{
cerr << "req_package_len == 0" << endl;
continue;
}
cout << "计算式子: " << req_message << endl;
// 走到这里一定是读取到了一条完整的式子了
// 反序列化
if (!req.deSerialize(req_message))
{
cerr << "req deSerialize error" << endl;
continue;
}
// 反序列化成功,打印查看反序列化结果
req.printDeSerialize();
// 获取到Response的结构化数据
Response res = calculator(req);
// 序列化
string res_package = res.serialize();
// 序列化成功,打印序列化结果
cout << "response序列化结果: " << res_package << endl;
// encode
res_package = encode(res_package, res_package.size());
// encode成功,打印encode结果
cout << "response encode结果: " << res_package << endl;
// 发送回给客户端
write(accept_socket, res_package.c_str(), res_package.size());
}
}
服务端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include "protocol.hpp"
using namespace std;
class TcpServer
{
public:
TcpServer(uint16_t port, const string &ip = "")
: _listen_socket(0), _port(port), _ip(ip)
{
}
~TcpServer()
{
}
public:
void init()
{
// 1.创建套接字
_listen_socket = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_socket < 0)
{
cerr << "socket error" << endl;
exit(1);
}
cout << "socket success" << endl;
// 2.bind
// 2.1填充网络信息
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
// 2.2bind网络信息
if (bind(_listen_socket, (const sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error" << endl;
exit(2);
}
cout << "bind success" << endl;
// 3.listen
if (listen(_listen_socket, 5) < 0)
{
cerr << "listen error" << endl;
exit(3);
}
cout << "listen success" << endl;
}
void start()
{
while (true)
{
// 1.accept
sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t peer_len = sizeof(peer);
int accept_socket = accept(_listen_socket, (sockaddr *)&peer, &peer_len);
if (accept_socket < 0)
{
continue;
}
// 获取连接成功,开始提供服务
netCal(accept_socket);
}
}
private:
void netCal(int accept_socket)
{
string req_package;
while (true)
{
Request req;
char buffer[128];
// 从客户端读取数据
// 9\r\n100 + 200\r\n
ssize_t readRes = read(accept_socket, buffer, sizeof(buffer) - 1);
if (readRes < 0)
{
cerr << "read error" << endl;
break;
}
else if (readRes == 0)
{
cout << "client quit" << endl;
break;
}
// 走到这里一定是读取成功了
buffer[readRes] = '\0';
req_package += buffer;
int req_package_len = 0;
string req_message = deEncode(req_package, &req_package_len);
// 如果还没读取到一条完整的式子,就继续读取
if (req_package_len == 0)
{
cerr << "req_package_len == 0" << endl;
continue;
}
cout << "计算式子: " << req_message << endl;
// 走到这里一定是读取到了一条完整的式子了
// 反序列化
if (!req.deSerialize(req_message))
{
cerr << "req deSerialize error" << endl;
continue;
}
// 反序列化成功,打印查看反序列化结果
req.printDeSerialize();
// 获取到Response的结构化数据
Response res = calculator(req);
// 序列化
string res_package = res.serialize();
// 序列化成功,打印序列化结果
cout << "response序列化结果: " << res_package << endl;
// encode
res_package = encode(res_package, res_package.size());
// encode成功,打印encode结果
cout << "response encode结果: " << res_package << endl;
// 发送回给客户端
write(accept_socket, res_package.c_str(), res_package.size());
}
}
Response calculator(const Request &req)
{
Response res;
switch (req.getOp())
{
case '+':
{
res.setResult(req.getX() + req.getY());
}
break;
case '-':
{
res.setResult(req.getX() - req.getY());
}
break;
case '*':
{
res.setResult(req.getX() * req.getY());
}
break;
case '/':
{
if (req.getY() == 0)
{
res.setExitCode(-1); // 除零错误
}
else
{
res.setResult(req.getX() / req.getY());
}
}
break;
case '%':
{
if (req.getY() == 0)
{
res.setExitCode(-2); // 模零错误
}
else
{
res.setResult(req.getX() % req.getY());
}
}
break;
default:
{
res.setExitCode(-3); // 运算符错误
}
break;
}
return res;
}
private:
int _listen_socket;
uint16_t _port;
string _ip;
};
// ./server port ip
int main(int argc, char *argv[])
{
// 命令行参数的格式输入错误
if (argc != 2 && argc != 3)
{
cerr << "argc error,usage:./server port ip";
exit(4);
}
uint16_t server_port = (uint16_t)atoi(argv[1]);
string server_ip = "";
if (argc == 3)
{
server_ip = argv[2];
}
TcpServer svr(server_port, server_ip);
svr.init();
svr.start();
return 0;
}
客户端的核心是让用户输入运算式子,然后创建一个Request对象,将用户输入的运算式子转换成为结构化数据填充到Request对象的成员变量中,然后调用Request对象的序列化函数将结构化数据转换成序列化数据,再调用encode函数为序列化数据添加长度字段。最后就可以发送给服务端了。
发送给服务端之后,客户端开始读取服务端发送回来的内容,服务端发送回来的是Response对象的序列化数据,并且一定是添加了长度字段的序列化数据,所以我们首先要调用deEncode函数提取长度字段,然后对其进行反序列化,最后打印出结果即可。
#include
#include
#include
#include
#include
#include
#include "protocol.hpp"
using namespace std;
// ./client ip port
int main(int argv, char *argc[])
{
if (argv != 3)
{
cerr << "argc error,usage:./client ip port";
exit(1);
}
string server_ip = argc[1];
uint16_t server_port = (uint16_t)atoi(argc[2]);
// 1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket error" << endl;
exit(2);
}
// 2.connect
sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons(server_port);
peer.sin_addr.s_addr = inet_addr(server_ip.c_str());
if (connect(sock, (const sockaddr *)&peer, sizeof(peer)) < 0)
{
cerr << "connect error" << endl;
exit(3);
}
while (true)
{
cout << "请输入表达式 #";
string message;
getline(cin, message);
Request req = makeRequest(message);
// req序列化
string req_package = req.serialize();
// encode
req_package = encode(req_package, req_package.size());
// 发送给服务端
ssize_t writeRes = write(sock, req_package.c_str(), req_package.size());
if (writeRes > 0)
{
// 从服务端读取结果
char buffer[128];
ssize_t readRes = read(sock, buffer, sizeof(buffer) - 1);
if (readRes > 0)
{
buffer[readRes] = '\0';
Response res;
string res_package = buffer;
int package_len = 0;
// 提取长度
res_package = deEncode(res_package, &package_len);
if (package_len > 0)
{
// 反序列化
res.deSerialize(res_package);
// 打印结果
int exit_code = res.getExitCode();
if (exit_code < 0)
{
if (exit_code == -1)
{
cout << "退出码: " << exit_code << endl;
cout << "除零错误" << endl;
cout << endl;
}
else if (exit_code == -2)
{
cout << "退出码: " << exit_code << endl;
cout << "模零错误" << endl;
cout << endl;
}
else
{
cout << "退出码: " << exit_code << endl;
cout << "运算符错误" << endl;
cout << endl;
}
}
else
{
cout << "退出码: " << exit_code << endl;
cout << "计算结果: " << res.getResult() << endl;
cout << endl;
}
}
}
else
{
cerr << "read error" << endl;
break;
}
}
else
{
cerr << "write error" << endl;
break;
}
}
return 0;
}