文章目录
定制协议
我们知道,协议就是一种“约定”,在前面的TCP/UDP网络通信的代码中,读写数据的时候都是按照"字符串"的形式来发送和接收的,如果我们要传送一些结构化的数据怎么办呢?
拿我们经常使用的微信聊天来举例:
聊天窗口中的信息包括头像(url),时间,昵称,消息等等,暂且将这几个信息看成是多个字符串,将这多个字符串形成一个结构化的数据:
struct/class message
{
string url;
string time;
string nickname;
string msg;
};
在聊天的过程中,通过网络发送的数据就成了上面代码所示的结构化数据,而不再是一个字符串那么简单。
如上图所示,用户A发送的消息虽然只有msg,但是经过用户层(微信软件)处理后,又增加了头像,时间,昵称等信息,形成一个结构化的数据struct/class message。
这个结构化的数据再发送到网络中,但是在发送之前,必须将结构化的数据序列化,然后才能通过socket发送到网络中。
- 序列化:就是将任意类型的数据或者数据结构转换成一个字符串。
如上图中的message结构体,序列化后就将所有成员合并成了一个字符串。
网络再将序列化后的数据发送给用户B,用户B接收到的报文必然是一个字符串。
然后用户B的应用层(微信软件)将接收到的报文进行反序列化,还原到原理的结构化数据message的样子,再将结构化数据中不同信息的字符串显式出来。
- 反序列化:就是将一个字符串中不同信息类型的字串提取出来,并且还原到结构化类型的数据。
网络发送的本质
实际上sockfd会指向一个操作系统给分配好的socket file control block,而这个socket文件控制块内部会维护网络发送和网络接收的缓冲区,我们调用的所有网络发送函数,write send sendto等实际就是将数据从应用层缓冲区拷贝到TCP协议层,也就是操作系统内部的发送缓冲区,而网络接收函数,read recv recvfrom等实际就是将数据从TCP协议层的接收缓冲区拷贝到用户层的缓冲区中,而实际双方主机的TCP协议层之间的数据发送是完全由TCP自主决定的,什么时候发?发多少?发送时出错了怎么办?这些全部都是由TCP协议自己决定的,这是操作系统内部的事情,和我们用户层没有任何瓜葛,这也就是为什么TCP叫做传输控制协议的原因,因为传输的过程是由他自己所控制决定的。
c->s和s->c之间发送使用的是不同对儿的发送和接收缓冲区,所以c给s发是不影响s给c发送的,这也就能说明TCP是全双工的,一个在发送时,不影响另一个也再发送,所以网络发送的本质就是数据拷贝。
服务器在调用网络接收函数进行TCP协议层接收缓冲区的数据拷贝到应用层时,有一个问题,如果客户端发送的报文很多怎么办?接收缓冲区会堆积很多的报文,而这些报文都会黏到一起,服务器该怎么保证read的时候读取到的是一个完整的报文呢?为了解决这个问题,就需要我们在应用层定制协议明确报文和报文的边界。
常见的解决方式有定长,特殊符号,自描述方式等,而我们今天所写的代码会将后两个方式合起来一块使用,我们会进行报文与报文之间添加特殊符号进行分隔,同时还会在报文前增加报头来表示报文的长度,以及在报头和报文的正文间增加特殊符号进行分隔,那么在读取的时候就可以以报头和报文之间的特殊符号作为依据先将报头读取上来,然后报头里存储的不是正文长度吗?那我们就再向后读取正文长度个字节,这样就完成了一个完整报文的读取。
网络计算器完成协议自定和序列化的任务
1.封装socket
上面的socket基本接口很简单,我们就不用多赘述了
2.完成TCPServer.hpp服务器的封装
我们希望在服务器主函数中创建对象,然后直接调用Init
和Start
初始化服务器、让服务器启动
启动服务器之后我们就需要和客户端进行数据之间的传输,传输数据的过程中就需要我们自己定制传输协议,包括对数据进行序列化和反序列化
- 服务器类里面因为我们需要使用绑定、监听套接字等等,所以我们的成员就需要使用刚刚封装的Socket类
- Start()服务器之后,就需要CS之间双向通信,完成网络计算器功能,因此就需要使用一个回调函数完成此功能
- 我们这里使用多进程版本,创建了子进程之后,让父进程去连接其他客户端,让子进程完成后续的计算任务
TCP是传输控制协议,所以我们不知道当前这个报文是否是一次完成的数据,因此根据我们自定义的协议判断是否是完成报文,如果不是那就continue继续读取,如果是完整报文那么处理数据即可,所以我们这里使用 += 和 continue
3.自定义协议
通过微信聊天的例子,我们知道了,协议其实就是结构化数据,是客户端和服务端都知道的一种结构化数据,所以需要创建Request和result两个结构
- 上图所示代码是Request,包含两个操作数x和y,以及操作符op,运算表达式的形式如1+1 。
- 网络请求Request是由客户端发送的,但是网络端也必须要知道Request中的内容,如x就表示第一个操作数y就表示第二个操作数,op表示计算的类型。
- Request就是客户端和服务端之间的一个协议。
- 上图所示代码是result,包含成员code退出码和ret计算结果。
- 网络响应result是由服务端发送的,同样客户端也必须知道result中的内容,要知道code是退出码,result是运算结果。
- result也是服务端和客户端之间的一个协议。
用户传输过来的数据保存在Request这个结构中,我们希望将结构化的数据序列化成字符串,在有效载荷前面添加上报头的数据,我们指定报头的内容是”有效载荷的长度\n“
最终我们传输到网络的字符串是 “有效载荷的长度\nx op y\n”
//添加报头
std::string Encode(std::string &content)
{
std::string package = std::to_string(content.size());
package += protocol_sep;
package += content;
package += protocol_sep;
return package;
}
//去掉报头
bool Decode(std::string &package, std::string *content)
{
std::size_t pos = package.find(protocol_sep);
if (pos == std::string::npos)
return false;
std::string len_str = package.substr(0, pos);
std::size_t len = std::stoi(len_str);
// package = len_str + content_str + 2
std::size_t total_len = len_str.size() + len + 2;
if (package.size() < total_len)
return false;
*content = package.substr(pos + 1, len);
// earse 移除报文 package.erase(0, total_len);
package.erase(0, total_len);
return true;
}
4.序列和反序列化
Request的序列化和反序列化
bool serialize(std::string *out) // 序列化 结构x op y -> "x op y"
{
// 构建报文的有效载荷
// struct => string, "x op y"
std::string s = std::to_string(_x);
s += blank_space_sep;
s += _op;
s += blank_space_sep;
s += std::to_string(_y);
*out = s;
return true;
}
bool Deserialization(const std::string &in) // 反序列化 "x op y" -> 结构x op y
{
std::size_t left = in.find(blank_space_sep);
if (left == std::string::npos)
return false;
std::string part_x = in.substr(0, left);
std::size_t right = in.rfind(blank_space_sep);
if (right == std::string::npos)
return false;
std::string part_y = in.substr(right + 1);
if (left + 2 != right)
return false;
_op = in[left + 1];
_x = std::stoi(part_x);
_y = std::stoi(part_y);
return true;
}
result序列化和反序列化
bool serialize(std::string *out) // 序列化 结构 -> "_ret _code"
{
// "result code"
// 构建报文的有效载荷
std::string s = std::to_string(_ret);
s += blank_space_sep;
s += std::to_string(_code);
*out = s;
return true;
}
bool Deserialization(const std::string &in) // 反序列化 "_ret _code" -> 结构
{
std::size_t pos = in.find(blank_space_sep);
if (pos == std::string::npos)
return false;
std::string part_left = in.substr(0, pos);
std::string part_right = in.substr(pos + 1);
_ret = std::stoi(part_left);
_code = std::stoi(part_right);
return true;
}
5. 计算器任务类
我们收到的是一个报文,但是不知道是否完整,所以我们在每一步:去报头、反序列化、 计算结果反序列化、结果序列化、添加报头中判断是否是一个完整的报文,不过不是完整的报文,我们就返回空串
因为我们在TcpSever
中_callback函数绑定的是Calculator函数,所以我们在调用Calculator函数的过程中如果发现这个报文不是一个合法报文直接返回空串,在Start函数中我们对Calculator函数的返回值进行的判断,如果不完整或者不合法直接重新读取!!
6. 客户端代码
#include <iostream>
#include <string>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include "Socket.hpp"
#include "Protocol.hpp"
static void Usage(const std::string &proc)
{
std::cout << "\nUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./clientcal ip port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
Sock sockfd;
sockfd.Socket();
bool r = sockfd.Connect(serverip, serverport);
if(!r) return 1;
srand(time(nullptr) ^ getpid());
int cnt = 1;
const std::string opers = "+-*/%=-=&^";
std::string inbuffer_stream;
while(cnt <= 10)
{
std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
int x = rand() % 100 + 1;
usleep(1234);
int y = rand() % 100;
usleep(4321);
char oper = opers[rand()%opers.size()];
Request req(x, y, oper);
req.DebugPrint();
std::string package;
req.serialize(&package); //序列化
package = Encode(package); //添加报头
write(sockfd.Fd(), package.c_str(), package.size());
char buffer[128];
ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); // 我们也无法保证我们能读到一个完整的报文
if(n > 0)
{
buffer[n] = 0;
inbuffer_stream += buffer; // "len"\n"result code"\n
std::cout << inbuffer_stream << std::endl;
std::string content;
bool r = Decode(inbuffer_stream, &content); // "result code"
assert(r);
result resp;
r = resp.Deserialization(content);
assert(r);
resp.DebugPrint();
}
std::cout << "=================================================" << std::endl;
sleep(1);
cnt++;
}
sockfd.Close();
return 0;
}
Json序列化和反序列化
前面手写了一遍如何进行序列化以及反序列化,目的是为了能够更好的感受到序列化和反序列化也是协议的一部分,以及协议被制订的过程。
虽然序列化和反序列化可以自己实现,但是非常麻烦,有一些现成的工具可以直接进行序列化和反序列化,如:
json——使用简单。
protobuf——比较复杂,局域网或者本地网络通信使用较多。
xml——其他编程语言使用(如Java等)。
这里介绍json的使用,同时这也是使用最广泛的,有兴趣的小伙伴可以去了解下protobuf。
对于序列化和反序列化,有现成的解决方案,绝对不要自己去写。
序列化和反序列化不等于协议,协议仍然可以自己制定。
安装Json
json安装后,它的头文件json.h所在路径为/usr/include/jsoncpp/json/,由于编译器自动查找头文件只到usr/include,所以在使用json时,包含头文件的形式为jsoncpp/json/json.h。
json是一个动态库,它所在路径为/lib64/,完整的名字为libjsoncpp.sp,在使用的时候,编译器会自动到/lib64路径下查找所用的库,所以这里不用包含库路径,但是需要指定库名,也就是掐头去尾后的结果jonscpp。
//重新编写协议.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
// #define MyStyle 1
const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";
std::string Encode(std::string &content)
{
std::string package = std::to_string(content.size());
package += protocol_sep;
package += content;
package += protocol_sep;
return package;
}
// "len"\n"x op y"\nXXXXXX
// "protocolnumber"\n"len"\n"x op y"\nXXXXXX
bool Decode(std::string &package, std::string *content)
{
std::size_t pos = package.find(protocol_sep);
if (pos == std::string::npos)
return false;
std::string len_str = package.substr(0, pos);
std::size_t len = std::stoi(len_str);
// package = len_str + content_str + 2
std::size_t total_len = len_str.size() + len + 2;
if (package.size() < total_len)
return false;
*content = package.substr(pos + 1, len);
// earse 移除报文 package.erase(0, total_len);
package.erase(0, total_len);
return true;
}
class Request
{
public:
int _x;
int _y;
char _op;
public:
Request(int x, int y, char op) : _x(x), _y(y), _op(op)
{
}
Request()
{
}
~Request()
{
}
void DebugPrint()
{
std::cout << "新请求构建完成: " << _x << _op << _y << "=?" << std::endl;
}
bool serialize(std::string *out) // 序列化 结构x op y -> "x op y"
{
#ifdef MyStyle
// 构建报文的有效载荷
// struct => string, "x op y"
std::string s = std::to_string(_x);
s += blank_space_sep;
s += _op;
s += blank_space_sep;
s += std::to_string(_y);
*out = s;
return true;
#else
Json::Value val;
val["_x"] = _x;
val["_y"] = _y;
val["_op"] = _op;
Json::FastWriter w;
*out = w.write(val);
return true;
#endif
}
bool Deserialization(const std::string &in) // 反序列化 "x op y" -> 结构x op y
{
#ifdef MyStyle
std::size_t left = in.find(blank_space_sep);
if (left == std::string::npos)
return false;
std::string part_x = in.substr(0, left);
std::size_t right = in.rfind(blank_space_sep);
if (right == std::string::npos)
return false;
std::string part_y = in.substr(right + 1);
if (left + 2 != right)
return false;
_op = in[left + 1];
_x = std::stoi(part_x);
_y = std::stoi(part_y);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root); // 将传进来的数据放到root中
_x = root["_x"].asInt();
_y = root["_y"].asInt();
_op = root["_op"].asInt();
return true;
#endif
}
};
class result
{
public:
int _ret;
int _code; // 0 正确 !0错误
public:
result(int ret = 0, int code = 0) : _ret(ret), _code(code)
{
}
bool serialize(std::string *out) // 序列化 结构 -> "_ret _code"
{
#ifdef MyStyle
// "result code"
// 构建报文的有效载荷
std::string s = std::to_string(_ret);
s += blank_space_sep;
s += std::to_string(_code);
*out = s;
return true;
#else
Json::Value root;
root["_ret"] = _ret;
root["_code"] = _code;
Json::FastWriter w;
*out = w.write(root);
return true;
#endif
}
bool Deserialization(const std::string &in) // 反序列化 "_ret _code" -> 结构
{
#ifdef MyStyle
std::size_t pos = in.find(blank_space_sep);
if (pos == std::string::npos)
return false;
std::string part_left = in.substr(0, pos);
std::string part_right = in.substr(pos + 1);
_ret = std::stoi(part_left);
_code = std::stoi(part_right);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
_ret = root["_ret"].asInt();
_code = root["_code"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "结果响应完成, result: " << _ret << ", code: " << _code << std::endl;
}
};