
目录
[2.1 Jsoncpp](#2.1 Jsoncpp)
[2.2 协议类](#2.2 协议类)
[2.3 服务端代码](#2.3 服务端代码)
[2.4 客户端代码](#2.4 客户端代码)
在传输层和网络层是由操作系统决定的,让我们来研究一下应用层。假设现在有这样一个问题。我们定义一个结构体,结构体内容包括头像,昵称,消息内容。我们想把这个结构体从发送到另一台机器上。如果直接发送的话很可能会出现匹配错误。为了保证报文的完整性,方便网络传送。我们就需要对传送的内容进行序列化和反序列化操作
序列化把消息由多变一,方便网络传送。反序列化把消息由一变多,方便上层处理。消息由几部分构成,分别是什么含义,由上层软件来解释。
所谓协议定制,本质就是在定义双方都认识的,符合通信和业务需要的结构化数据,结构化数据就是所谓的struct和class
重新理解与一下write和read 
其中由TCP自主决定什么时候发,发多少,出错了怎么办(传输控制协议)。主机间通信的本质,把发送方的发送缓冲区的数据拷贝到接收方的接收缓冲区。为什么TCP是全双工的:因为有两对缓冲区。
我们未来定制的协议:
-
结构化的字段,提供好序列化和反序列化方案
-
解决因为字节流导致的读取报文不完整的问题
自定义网络版计算器的实现
1.socket的封装:模版方法模式
写一个基类socket,基类的大多数方法,都设置为纯虚方法
bash
class Socket
{
public:
virtual ~Socket() {}
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void Close() = 0;
public:
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
};
再写一个子类TcpSocket继承Socket,实现这些方法
2.自定义协议
2.1 Jsoncpp
Jsoncpp是一个处理JSON数据的C++库,它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。
使用方法:
1.序列化操作
bash
std::string Serialize()
{
// _x = 10 _y = 20, _oper = '+'
// "10" "20" '+' : 用空格作为分隔符
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper; // ?
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
2.反序列化操作
bash
bool Deserialize(std::string &in)
{
// "10" "20" '+' -> 以空格作为分隔符 -> 10 20 '+'
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
if (ok)
{
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt(); //?
}
return ok;
}
将字符串in的内容转换成Json::Value格式,储存在root中
2.2 协议类
协议(基于TCP的)需要解决两个问题:
-
request和response必须得有序列化和反序列化功能
-
你必须保证,读取的时候,读到完整的请求(TCP, UDP不用考虑)
举个例子 
因为TCP是面向字节流的,当我们tcp中读取数据的时候,读取到的报文不完整,或者读多了,导致下一个报文不完整了,做"粘报"问题。read本身不负责读到报文的完整性,需要程序员自己解决
我们可以自己在报文前面添加报头
bash
std::string Encode(const std::string &jsonstr)
{
// 50\r\n{"x": 10, "y" : 20, "oper" : '+'}\r\n
std::string len = std::to_string(jsonstr.size());
return len + sep + jsonstr + sep; // 应用层封装报头
}
// 1. 判断报文完整性
// 2. 如果包含至少一个完整请求,提取他, 并从移除它,方便处理下一个
bool Decode(std::string &buffer, std::string *package)
{
ssize_t pos = buffer.find(sep);
if (pos == std::string::npos)
return false; // 让调用方继续从内核中读取数据
std::string package_len_str = buffer.substr(0, pos);
int package_len_int = std::stoi(package_len_str);
// buffer 一定有长度,但是一定有完整的报文吗?
int target_len = package_len_str.size() + package_len_int + 2 * sep.size();
if (buffer.size() < target_len)
return false;
// buffer一定至少有一个完整的报文!
*package = buffer.substr(pos + sep.size(), package_len_int);
buffer.erase(0, target_len);
return true;
}
2.3 服务端代码
bash
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
// 读取
std::string buffer_queue;
while (true)
{
int n = sock->Recv(&buffer_queue);
if(n > 0)
{
std::string json_package;
// 1. 解析报文,提取完整的json请求,如果不完整,就让服务器继续读取
bool ret = Decode(buffer_queue, &json_package);
if(!ret)
continue;
// 我敢100%保证,我一定拿到了一个完整的报文
// {"x": 10, "y" : 20, "oper" : '+'} -> 你能处理吗?
// 2. 请求json串,反序列化
Request req;
bool ok = req.Deserialize(json_package);
if(!ok)
continue;
// 3. 我一定得到了一个内部属性已经被设置了的req了.
// 通过req->resp, 不就是要完成计算功能嘛!!业务
Response resp = _func(req);
// 4. 序列化
std::string json_str = resp.Serialize();
// 5. 添加自定义长度
std::string send_str = Encode(json_str); // 携带长度的应答报文了"len\r\n{result:XXX, code:XX}\r\n"
// 6. 直接发送
sock->Send(send_str);
}
else if(n == 0)
{
LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "Quit!";
break;
}
else
{
LOG(LogLevel::WARNING) << "client:" << client.StringAddr() << ", recv error";
break;
}
}
}
让我们来理清一下服务端所做的工作

2.4 客户端代码
客户端也需要创建套接字,建立连接,连接服务器成功后获取数据,构建请求,发送请求,获取应答,显示结果
bash
// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
client->BuildTcpClientSocketMethod();
if (client->Connect(server_ip, server_port) != 0)
{
// 失败
std::cerr << "connect error" << std::endl;
exit(CONNECT_ERR);
}
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
std::string resp_buffer;
// 连接服务器成功
while (true)
{
// 1. 从标准输入当中获取数据
int x, y;
char oper;
GetDataFromStdin(&x, &y, &oper);
// 2. 构建一个请求-> 可以直接发送的字符串
std::string req_str = protocol->BuildRequestString(x, y, oper);
// std::cout << "-----------encode req string-------------" << std::endl;
// std::cout << req_str << std::endl;
// std::cout << "------------------------------------------" << std::endl;
// 3. 发送请求
client->Send(req_str);
// 4. 获取应答
Response resp;
bool res = protocol->GetResponse(client, resp_buffer, &resp);
if(res == false)
break;
// 5. 显示结果
resp.ShowResult();
}
client->Close();
return 0;
}
一个自定义网络版计算器就实现了
点个关注不迷路,持续更新好内容