目录
2.重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工
前言
其实我们前面都有提及过协议和序列化、反序列化相关的基础内容,但都没有用实际代码进行讲述,本篇内容就是通过编写一个小项目——网络版本计算器的方式帮助大家加深对自定义协议和序列化、反序列化的理解以及搞懂相关代码的书写(理论与实践相结合!!)
右边这段代码是在套接字tcp编程代码中读取数据的操作,有bug的原因在于tcp面向字节流,这种直接读取的方式是可能读不完整的或者多读取了的
如今我们带着这两块内容重谈一下应用层协议
1.应用层
1.再谈协议
协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?
其实, 协议就是双方约定好的结构化的数据
网络版计算器
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端
约定方案一: :
客户端发送一个形如"1+1"的字符串
这个字符串中有两个操作数, 都是整形
两个数字之间会有一个字符是运算符, 运算符只能是 +
数字和运算符之间没有空格
....
约定方案二:
定义结构体来表示我们需要交互的信息
发送数据时将这个结构体按照一个规则转换成字符串,
接收到数据的时候再按照相同的规则把字符串转化回结构体
这个过程叫做 "序列化" 和 "反序列化
2.序列化 和 反序列化
(其实就是结构化信息和字节流信息的相互转换)
[^] 以上是C/S模式(客户端/服务端),我们前面学的内容、写的代码都是在下面这层玩
无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议
这里我们可能会想能不能直接以二进制发送结构体对象而不用序列化反序列化?答案是可以,但是不推荐,因为我们知道c和s可能会处于不同系统上,那么不同系统对于结构体对象的内存对齐的规则可能不一致,就会导致相同结构体的内存布局不一致;或者c和s用的是不同的语言编写(比如c++/java等),那么不同语言class的大小会对不上的;所以这种方式只能用于同系统同语言之间传递,比如os内部协议全都是传递结构体对象的(因为操作系统都是c语言写的)
那么我们的序列化和反序列化的本质就是对软件进行解耦,都是把这些语言序列化成字符串
结论:从今天开始,如果我们要进行网络协议式的通信,在应用层强烈建议使用序列化和反序列化方案,至于直接传递结构体的方案,除非场景特殊,否则不建议
2.重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工
重新理解write(send)和read(recv)
无论是write还是read,其本质都是拷贝一份数据,write是把这个数据拷贝到缓冲区中,read则是把数据从缓冲区中拷贝一份到上面用户自主接收设置的缓冲区;而在缓冲区中os把数据发送到网络中,还是反过来,本质也是拷贝,所以——计算机世界:通信即拷贝
结论:
-
write和read并不直接和网络联系,而主要进行的是数据拷贝,recv和send也是一样的
-
主机间通信的本质:把发送方的发送缓冲区内部的数据拷贝到对端的接受缓冲区中!
-
为啥TCP通信时是全双工的?答:因为有两对发送和接受缓冲区,对一台主机来说接受和发送是分离的,所以当然可以实现全双工
-
这些缓冲区就相当于内核和用户之间的生产者消费者模型(当缓冲区无数据时,read会阻塞,这是用户边在同步内核边;缓冲区被write写满时,write也会阻塞,这也是同步)
如果是UDP,不存在发送错误的问题,因为是直接os整合数据一起发送的,面向数据报;而我们的TCP面向的是字节流,就会导致os可能只把序列化好的字符串其中一部分发送过去(毕竟发送是由操作系统自主决定的,不一定会完整发送),这个时候,在接受方的应用层不会进行反序列化,而是要等发送方将剩下的字符串发过来之后做完整合再进行反序列化——这就是tcp的数据包粘包问题
我们未来定制的协议:
-
得是结构化的字段,提供好序列化和反序列化的方案
-
得能解决因为字节流问题,导致读取报文不完整的问题(只处理读取)
我们借着以上知识点实现一个网络版的计算器
-
socket的封装——模板方法模式
(想要进一步了解模板方法模式可以看我这篇文章:深入理解模板方法模式:框架设计的“骨架”艺术-CSDN博客)
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
const static int gbacklog = 16;
// 模板方法模式!
// 基类socket,大部分方法都是纯虚函数
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;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string &message) = 0;
virtual int Connect(std::string &server_ip, uint16_t server_port) = 0;
public:
// 基类中的固定化的模板化方法
// 完成某种套接字的初始化方法
void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
void BuildClientSocketMethod()
{
SocketOrDie();
}
};
const static int defaultfd = -1;
class TcpSocket : public Socket
{
public:
TcpSocket()
: _sockfd(defaultfd)
{
}
// 重载一个能设置fd的构造(此时这个fd应被accept设置为accept的套接字)
TcpSocket(int sockfd)
: _sockfd(sockfd)
{
}
~TcpSocket() {}
void SocketOrDie() override
{
// 用的是系统的socket
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success";
}
void BindOrDie(uint16_t port) override
{
InetAddr localaddr(port);
int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success";
}
// 返回的是一个指向tcp套接字可调用子类方法的基类指针
// 这里使用shared_ptr是因为我们返回的socket是需要多个模块共享访问
// 且其生命周期需由所有使用者共同管理
// 而unique_ptr是唯一对象的,一旦一个使用模块关闭了它,socket就销毁了
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sockfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept warning ...";
return nullptr;
}
// 将客户端信息带出去
client->SetAddr(peer);
// 这样的返回可以让我们未来在调用accept获取了accept套接字后
// 可以用其来调用其他的接口
return std::make_shared<TcpSocket>(sockfd);
}
void Close() override
{
if (_sockfd >= 0)
{
::close(_sockfd);
}
}
// 读取方法
// n==read的返回值
// 和read函数一样,我们封装的Recv函数返回的也是读取到的实际长度大小
int Recv(std::string *out) override
{
// 流式读取,不关心读到的是什么
char buffer[1024];
// 这里的recv和read是一样的,不过是多了一个设置读取的方式的参数,不必管先设为0
ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
// 把信息直接加入到out这个缓冲区中
*out += buffer;
}
return n;
}
// 写入方法
// 和write函数一样,我们封装的Send函数返回的也是实际写入的长度大小
int Send(const std::string &message) override
{
// 发送数据
// 这里可以使用send函数,和write差不多,和上面的recv是一样的
// 多了一个设置写入方式的参数
return send(_sockfd, message.c_str(), message.size(), 0);
}
int Connect(std::string &server_ip, uint16_t server_port) override
{
InetAddr server(server_ip, server_port);
return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
}
private:
int _sockfd; //_sockfd , listensockfd/sockfd
};
}
3.自定义协议
关于序列化和反序列化,我们可以使用jsoncpp来做!!
我们这里给出一份Json的使用测试代码:
#include <iostream>
#include <string>
// 因为需要的是jsoncpp下的json下的json.h头文件,所以需要把前导路径带上
#include <jsoncpp/json/json.h>
#include <sstream>
void x()
{
// 1. 序列化
// 万能对象
Json::Value root;
// 不用关注类型
root["name"] = "张三";
root["sex"] = "男";
root["age"] = "18";
// Json::FastWriter writer; //去掉了换行符,网络传送的数据量就小了
// 风格式的写入,用\n给我们按行进行设置了,可读性好
// Json::StyledWriter writer;
// std::string s = writer.write(root);
// std::cout << s << std::endl;
// 关于中文被转换为了unicode编码的解决方式
Json::StreamWriterBuilder jswBuilder; // StreamWriter的工厂
jswBuilder["emitUTF8"] = true;
std::unique_ptr<Json::StreamWriter> writer(jswBuilder.newStreamWriter());
std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
// 还可以定义子json
// Json::Value sub;
// sub["籍贯"] = "xxx";
// sub["tel"] = "12345";
// root["info"] = sub;
// // 调用json对象内部方法转换成字符串
// std::string s = root.toStyledString();
// std::cout << s << std::endl;
}
int main()
{
// 反序列化
std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";
// 反序列化,起手式Json::Value;
Json::Value root;
Json::Reader reader; // 序列化时是Writer,那么反序列化时用的就是Reader,类似写读
// 把json_String反序列化到root中去
bool ok = reader.parse(json_string, root);
if (!ok) // 解析失败,反序列化失败
{
std::cout << reader.getFormattedErrorMessages() << std::endl;
return 1;
}
// 反序列化成功
// (把序列化之后的字符串反序列化到了Json::Value中,和序列化相反)
// 通过键值k来提取v,也就是对应的信息
// 然后把v转化成对应信息类型就能拿我们的对应信息类型变量接收啦
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();
std::cout << name << std::endl;
std::cout << age << std::endl;
std::cout << city << std::endl;
}
我们在网络版本计算器当中规定好计算数据x,y以及计算符合oper,这些我们通过json进行序列化整合之后就是我们所需要的请求信息(需要从客户端发往服务器)为请求操作,在服务器端又需要将这个json串反序列化得到x,y和oper再进行计算之后得到结果result以及能够确定这个结果有没有出现问题(比如说除零错误啥的)的一个标志code,再把这些信息json序列化之后发往客户端为应答操作,客户端这边再做反序列化拿到结果和标志结果是否出现问题的code
那么请求方法和应答方法的具体代码如下:
class Request
{
public:
Request() {}
Request(int x, int y, int oper)
: _x(x),
_y(y),
_oper(oper)
{
}
// 提供序列化接口
std::string Serialize()
{
// _x=10 _y=20 _oper='+'
//"10" "20" '+' :用空格作为分隔符
// 使用jsoncpp
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper; //_oper是char类型,也是整数,看到的是对应的ascii值
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
// 提供反序列化接口
bool Deserialize(std::string &in)
{
//"10" "20" '+' -> 以空格作为分隔符来反序列 -> 10 20 '+'
// 使用jsoncpp
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;
}
~Request() {}
int X() { return _x; }
int Y() { return _y; }
char Oper() { return _oper; }
private:
int _x;
int _y;
char _oper; // + - * % -> _x _oper _y -> 10 + 20
};
// server -> client
// 应答
class Response
{
public:
Response() {}
Response(int result, int code)
: _result(result),
_code(code)
{
}
// 提供序列化接口
std::string Serialize()
{
// 使用jsoncpp
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
// 提供反序列化接口
bool Deserialize(std::string &in)
{
// 使用jsoncpp
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
_result = root["result"].asInt();
_code = root["code"].asInt();
return true;
}
// 设置结果方法
void SetResult(int res)
{
_result = res;
}
// 设置运算的状态方法
void SetCode(int code)
{
_code = code;
}
// 显示结果的方法
void ShowResult()
{
std::cout << "计算结果是:" << _result << "[" << _code << "]" << std::endl;
}
~Response() {}
private:
int _result; // 运算结果,无法区分清楚应答是计算结果还是异常值
int _code; // 0:success,1,2,3,4->不同的运算异常的情况——约定!!就是协议!!
};
但是这个时候会出现一个问题:
也就是说我们在做自定义协议的时候,得保证我们读到的至少是一个完整的报文,也就是说,我们在json序列化之后以及反序列化之前还得需要一步;这一步我们得确保能够使得报文完整,这里我们运用到的是通过给序列化之后新增一步操作编码以及反序列化之前需要新增一步解码操作来拿到完整报文,那么我们这个编码和解码操作要怎么进行呢?
欸,我们这里可以规定这个编码操作就是在要发送的json串中增添一些能够保证报文完整性的信息,就比如说这个完整报文的长度,没错,我们在这个json字符串的最前面得加上这个完整请求/应答报文的长度,然后我们通过\r\n这种分离字符来隔离长度和完整的报文;那么解码就是完成这个编码之后的字符串的长度与完整报文分离的操作了,当然前提时我们得先读到一个完整长度(那就是得查找到\r\n,不然就一直重复读),然后有了长度之后,就可以通过长度字符串的长度+这个长度+两个\r\n的长度就可以得到这个完整字符串的长度了,我们只需要让我们读取的字符串小于这个长度时重复去读就可以至少在这个重复循环结束时读到一个完整字符串了,那么我们这个完整报文的分离操作不就是substr从长度+一个\r\n长度的位置开始读这个报文的长度大小的字符串吗
所以这个编码和解码操作的具体实现代码如下:
// 编码
std::string sep="\r\n"
std::string Encode(const std::string jsonstr)
{
// 需要对json串进行编码成:50\r\n{"x":10,"y":20,"oper":'+'}\r\n这种形式
std::string len = std::to_string(jsonstr.size());
// 做应用层封装报头,编码
std::string package = len + sep + jsonstr + sep;
return package;
}
// 解码
// 我们未来会受到类似于50\r\n{"x":10,"y":20,"oper":'+'}这样的字符串
// 得解码成{"x":10,"y":20,"oper":'+'}这样的完整报文
// 但是这种是不一定的,我们的tcp面向字节流,可能是残缺的,也可能是有多出来的
// 比如:残缺:50,多出来:50\r\n{"x":10,"y":20,"oper":'+'}50\r\n
// 所以我们的Decode方法得完成:
// 1.判断报文完整性
// 2.如果包含至少一个完整请求,就得提取它到package中,并从缓冲区中移除它,方便处理下一个
bool Decode(std::string &buffer, std::string *package)
{
// 先从左到右找到第一个分隔符"\r\n"
ssize_t pos = buffer.find(sep);
if (pos == std::string::npos) // 说明本次读到的报文连有效长度都不完整
{
// 让调用方继续从内核中读取数据
return false;
}
// 走到这就说明当前报文的长度是完整知道的
// 那就从开头处提取pos个的字符(pos是'\'对应下标,正好可以代表前面有几个字符)
std::string pack_len_str = buffer.substr(0, pos);
// 把有效长度转成整数
int pack_len_int = std::atoi(pack_len_str.c_str());
// 但是在这里不一定有完整的报文
// 一个完整字符串的总长度
int target_len = pack_len_str.size() + pack_len_int + 2 * sep.size();
// 我们的buffer的长度必须要大于/等于这个target_len才有完整的报文
if (buffer.size() < target_len)
{
// 所以这里的buffer是残缺的,不完整,直接返回false
return false;
}
// 走到这里就说明buffer中至少有一个完整的报文
// 那我们就开始提取
// 让buffer从下标为pos+sep.size()的地方分割pack_len_int长度的字符串
*package = buffer.substr(pos + sep.size(), pack_len_int);
// 我们的package就是我们提取出来的json串(输出型参数)
// 然后我们将这一个完整的报文从buffer中移除避免影响后面的读取操作(方便下次解码操作)
buffer.erase(0, target_len);
return true;
// 我们未来调用这个解析解码操作方法时,可以循环调用来达到一直解码操作
}
那么我们设计的实现网络版本计算器需要的自定义协议的代码如下:
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include "Socket.hpp"
#include <jsoncpp/json/json.h>
#include <functional>
using namespace SocketModule;
// 实现一个自定义的网络版本的计算器
// 约定好各个字段的含义,本质就是约定好协议!
// client -> server
// 请求
// 如何要做到序列化和反序列化
// 1.我们直接自己写(怎么做的知道就好,因为不具备很好的扩展性)
// 2.使用现成的方案(我们要写这种)--- json -》 jsoncpp
// content_len(整个json串的长度也要发过去)\r\n jsonstring \r\n
// 50\r\n{"x":10,"y":20,"oper":'+'}\r\n
// 一开始一直读到\r\n,这样我们读到的内容就一定是完整的json串的长度
// 接下来我们只需要读到该长度大小的报文即为完整的json串
// 也就是说我们的\r\n是用来分离报头(content_len)和有效载荷(jsonstring)的
class Request
{
public:
Request() {}
Request(int x, int y, int oper)
: _x(x),
_y(y),
_oper(oper)
{
}
// 提供序列化接口
std::string Serialize()
{
// _x=10 _y=20 _oper='+'
//"10" "20" '+' :用空格作为分隔符
// 使用jsoncpp
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper; //_oper是char类型,也是整数,看到的是对应的ascii值
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
// 提供反序列化接口
bool Deserialize(std::string &in)
{
//"10" "20" '+' -> 以空格作为分隔符来反序列 -> 10 20 '+'
// 使用jsoncpp
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;
}
~Request() {}
int X() { return _x; }
int Y() { return _y; }
char Oper() { return _oper; }
private:
int _x;
int _y;
char _oper; // + - * % -> _x _oper _y -> 10 + 20
};
// server -> client
// 应答
class Response
{
public:
Response() {}
Response(int result, int code)
: _result(result),
_code(code)
{
}
// 提供序列化接口
std::string Serialize()
{
// 使用jsoncpp
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
std::string s = writer.write(root);
return s;
}
// 提供反序列化接口
bool Deserialize(std::string &in)
{
// 使用jsoncpp
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(in, root);
_result = root["result"].asInt();
_code = root["code"].asInt();
return true;
}
// 设置结果方法
void SetResult(int res)
{
_result = res;
}
// 设置运算的状态方法
void SetCode(int code)
{
_code = code;
}
// 显示结果的方法
void ShowResult()
{
std::cout << "计算结果是:" << _result << "[" << _code << "]" << std::endl;
}
~Response() {}
private:
int _result; // 运算结果,无法区分清楚应答是计算结果还是异常值
int _code; // 0:success,1,2,3,4->不同的运算异常的情况——约定!!就是协议!!
};
const std::string sep = "\r\n";
using func_t = std::function<Response(Request &req)>;
// 协议(基于Tcp)需要解决两个问题
// 1. request和response必须得有序列化和反序列化的功能
// 2. 我们必须得确保读取的时候,读到的是完整的报文(Tcp的数据不一致问题,Udp则没有)
class Protocol
{
public:
Protocol()
{
}
// 我们把处理计算的业务交给上层来实现
Protocol(func_t func)
: _func(func)
{
}
// 编码
std::string Encode(const std::string jsonstr)
{
// 需要对json串进行编码成:50\r\n{"x":10,"y":20,"oper":'+'}\r\n这种形式
std::string len = std::to_string(jsonstr.size());
// 做应用层封装报头,编码
std::string package = len + sep + jsonstr + sep;
return package;
}
// 解码
// 我们未来会受到类似于50\r\n{"x":10,"y":20,"oper":'+'}这样的字符串
// 得解码成{"x":10,"y":20,"oper":'+'}这样的完整报文
// 但是这种是不一定的,我们的tcp面向字节流,可能是残缺的,也可能是有多出来的
// 比如:残缺:50,多出来:50\r\n{"x":10,"y":20,"oper":'+'}50\r\n
// 所以我们的Decode方法得完成:
// 1.判断报文完整性
// 2.如果包含至少一个完整请求,就得提取它到package中,并从缓冲区中移除它,方便处理下一个
bool Decode(std::string &buffer, std::string *package)
{
// 先从左到右找到第一个分隔符"\r\n"
ssize_t pos = buffer.find(sep);
if (pos == std::string::npos) // 说明本次读到的报文连有效长度都不完整
{
// 让调用方继续从内核中读取数据
return false;
}
// 走到这就说明当前报文的长度是完整知道的
// 那就从开头处提取pos个的字符(pos是'\'对应下标,正好可以代表前面有几个字符)
std::string pack_len_str = buffer.substr(0, pos);
// 把有效长度转成整数
int pack_len_int = std::atoi(pack_len_str.c_str());
// 但是在这里不一定有完整的报文
// 一个完整字符串的总长度
int target_len = pack_len_str.size() + pack_len_int + 2 * sep.size();
// 我们的buffer的长度必须要大于/等于这个target_len才有完整的报文
if (buffer.size() < target_len)
{
// 所以这里的buffer是残缺的,不完整,直接返回false
return false;
}
// 走到这里就说明buffer中至少有一个完整的报文
// 那我们就开始提取
// 让buffer从下标为pos+sep.size()的地方分割pack_len_int长度的字符串
*package = buffer.substr(pos + sep.size(), pack_len_int);
// 我们的package就是我们提取出来的json串(输出型参数)
// 然后我们将这一个完整的报文从buffer中移除避免影响后面的读取操作(方便下次解码操作)
buffer.erase(0, target_len);
return true;
// 我们未来调用这个解析解码操作方法时,可以循环调用来达到一直解码操作
}
// 获取请求的方法
void GetRequest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
// 读取
// Recv在不断给这个解析队列+=报文
// 解析出一个完整报文Recv就将其移除,就相当于一个解析队列啦
std::string buffer_queue;
while (true)
{
int n = sock->Recv(&buffer_queue); // 把数据读到inbuffer中
if (n > 0)
{
std::string json_package;
// 1.解析报文,提取完整的json请求,如果不完整,就让服务器继续提取
// 在这里使用Decode返回进行的循环是为了持续处理挤压的json请求数据
while (Decode(buffer_queue, &json_package))
{
// 走到这里就能确保拿到了一个完整的报文
// 是这样的报文:{"x":10,"y":20,"oper":'+'}
// 2.发过来的是请求json串,得做一下反序列化
LOG(LogLevel::DEBUG) << client.StringAddr() << " 请求:" << json_package;
Request req;
bool ok = req.Deserialize(json_package);
if (!ok)
{
// 反序列化失败,继续返回循环再走流程
continue;
}
// 3. 走到这说明反序列化成功了
// 一定得到了一个内部属性已经被设置了的req了
// 此时我们需要req->resp,要得到_result和_code,也就是要完成计算功能——业务
// 这个业务交给上一层完成,我们这里用Response类型变量来接收结果就好
Response resp = _func(req);
// 4.这个resp应答对象是结构化字符串,我们应该先给它序列化
std::string json_str = resp.Serialize();
// 5.还要给json_str进行编码(也就是添加自定义长度)形成send_str
std::string send_str = Encode(json_str);
// 此时这个send_str就是携带长度的应答报文了,比如:50\r\n{}50\r\n
// 6.此时就可以将这个send_str写回给客户端了
sock->Send(send_str);
}
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client:" << client.StringAddr() << "退出了";
// 我们也退出
break;
}
else
{
LOG(LogLevel::WARNING) << "client" << client.StringAddr() << ",secv error";
// 读取失败了,退出
break;
}
}
}
// 获取应答
// 把客户端对象、读取存放的缓冲区、应答对象传进来
bool GetResponse(std::shared_ptr<Socket> &client, std::string &resp_buff, Response *resp)
{
// 面向字节流
// 得先确保client读到得网络字符串是一个完整的应答
while (true)
{
int n = client->Recv(&resp_buff);
if (n > 0)
{
std::cout << "----------resp_buffer-----------" << std::endl;
std::cout << resp_buff << std::endl;
std::cout << "---------------------------------" << std::endl;
// 成功
std::string json_package;
// 1.解析报文,提取完整的json应答,如果不完整,就让服务器继续提取
bool ok = Decode(resp_buff, &json_package);
if (!ok)
{
continue;
}
std::cout << "----------response json-----------" << std::endl;
std::cout << json_package << std::endl;
std::cout << "----------------------------------" << std::endl;
// 走到这里就能确保拿到了一个完整的应答json报文
// 2.对这个应答做反序列化
resp->Deserialize(json_package);
return true;
}
else if (n == 0)
{
// server端退出了
std::cout << "server quit" << std::endl;
return false;
}
else
{
// 读取失败了
std::cout << "secv error" << std::endl;
return false;
}
}
}
// 客户端构建请求字符串的方法
std::string BuildRequestString(int x, int y, char oper)
{
// 1.构建一个完整的请求
Request req(x, y, oper);
// 2.序列化
std::string json_req = req.Serialize();
// 2.1 debug
std::cout << "----------json_req string-----------" << std::endl;
std::cout << json_req << std::endl;
std::cout << "--------------------------------------" << std::endl;
// 3.添加属性报头——编码
return Encode(json_req);
}
~Protocol() {}
private:
// 因为我们使用的是多进程
// Request _req;
// Response _resp;
func_t _func;
};
紧接着我们可以依次实现我们的服务器代码、客户端代码以及处理计算业务的代码:
(我们的服务器和客户端代码都是基于上面我们用模板方法模式封装的Socket套接字来调用接口实现的)
TcpServer.hpp
#include "Socket.hpp"
#include <sys/wait.h>
#include <functional>
using namespace SocketModule;
using namespace LogModule;
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
// 主要解决:连接的问题,IO通信的问题
// 细节:TcpServer需不需要关心自己未来传递的信息是什么?答:不需要!!由上层关心
// 网络版本的计算器,长服务
class TcpServer
{
public:
TcpServer(uint16_t port, ioservice_t service)
: _port(port),
// 基类指针指向子类,子类重写了基类需要重写的方法
_listensocketptr(std::make_unique<TcpSocket>()),
_isrunning(false),
_service(service)
{
// 直接完成了tcp服务器的初始化工作
_listensocketptr->BuildTcpSocketMethod(_port);
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 1. 和client通信的sockfd 2.client网络地址
InetAddr client;
auto sock = _listensocketptr->Accept(&client);
if (sock == nullptr)
{
// accept失败了
continue;
}
LOG(LogLevel::DEBUG) << "accept success...";
// 到这就说明有了客户端套接字以及客户端的地址信息
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FOCK_ERR);
}
else if (id == 0)
{
// 子进程,关闭监听套接字
_listensocketptr->Close();
if (fork() > 0)
{
exit(OK);
}
// 孙子进程在执行任务,已经是孤儿了
_service(sock, client);
// 回调方法执行结束,回到这说明获取请求结束了,我们可以套接字关闭了
sock->Close();
exit(OK);
}
else
{
// 父进程,关闭accept的套接字
sock->Close();
pid_t rid = ::waitpid(id, nullptr, 0);
(void)rid;
}
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port; // 端口号
std::unique_ptr<Socket> _listensocketptr;
bool _isrunning;
ioservice_t _service;
};
TcpClient.cc
#include "Socket.hpp"
#include "Common.hpp"
#include "Protocol.hpp"
#include <iostream>
#include <string>
#include <memory>
using namespace SocketModule;
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}
void GetFromStdin(int *x, int *y, char *oper)
{
std::cout << "Please Enter x: ";
std::cin >> *x;
std::cout << "Please Enter y: ";
std::cin >> *y;
std::cout << "Please Enter oper: ";
std::cin >> *oper;
}
// ./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->BuildClientSocketMethod();
// 客户端连接服务器
int n = client->Connect(server_ip, server_port);
if (n != 0) // 连接失败
{
// 那么我们连接不上服务端,客户端直接退出吧
std::cerr << "connect error" << std::endl;
exit(CONNECT_ERR);
}
std::string resp_buffer;
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>();
// 连接服务器成功
while (true)
{
// 1.我们这里直接从标准输入(键盘)中获取x,y,oper数据
int x, y;
char oper;
GetFromStdin(&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 n = protocol->GetResponse(client, resp_buffer, &resp);
if (n == false)
{
// 读取失败
break;
}
// 显示一下结果
resp.ShowResult();
}
client->Close();
return 0;
}
NetCal.hpp
// 网络计算器的处理计算的业务模块
#include <iostream>
#include "Protocol.hpp"
class Cal
{
public:
// 计算方法
Response Execute(Request &req)
{
Response resp(0, 0); // 先把结果和情况初始化为0,表示默认情况是运算成功
switch (req.Oper())
{
case '+':
resp.SetResult(req.X() + req.Y());
break;
case '-':
resp.SetResult(req.X() - req.Y());
break;
case '*':
resp.SetResult(req.X() * req.Y());
break;
case '/':
{
// 判断是否存在除零错误
if (req.Y() == 0)
{
// 设置计算异常情况
resp.SetCode(1); // 规定1为除零错误
}
else
{
resp.SetResult(req.X() / req.Y());
}
}
break;
case '%':
// 判断是否存在模零错误
if (req.Y() == 0)
{
// 设置计算异常情况
resp.SetCode(2); // 规定1为模零错误
}
else
{
resp.SetResult(req.X() % req.Y());
}
break;
default:
// 发过来的运算操作符不对,非法操作
resp.SetCode(3); // 规定3为非法操作
break;
}
// 到这里说明计算完成了,直接返回resp
return resp;
}
};
关于我们的网络主函数的编写一般写的时候会不自觉的形成以下三层设计:
上面这三层是没办法设计到内核里的,因为这三层主要受用户的需求影响,无法合并到内核中,一旦合并到内核中,所有的操作就都一样了,无法使用其他的实现方法
(我们的网络代码都需要这样分三层来进行编写)
主函数代码如下:
// 从上往下是分了三层
#include "NetCal.hpp" //负责业务处理
#include "Protocol.hpp" //负责协议处理:分析协议、分析报文、读到完整请求,完成序列化(反)
#include "TcpServer.hpp" //负责进行网络连接、获取
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << "port" << std::endl;
}
// ./tcpserver port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// 日志选择是往显示器上输出的刷新策略
Enable_Console_Log_Strategy();
// 1.顶层,业务逻辑层
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// 2.协议层
// 构建一个协议对象
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(
[&cal](Request &req) -> Response
{
// 调用Execute方法进行业务处理,也就是计算逻辑
return cal->Execute(req);
});
// 3.服务器层
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
std::stoi(argv[1]),
[&protocol](shared_ptr<Socket> &sock, InetAddr &client)
{
// 这样直接从网络过渡到协议该如何读取的问题了
protocol->GetRequest(sock, client);
});
tsvr->Start();
return 0;
}
我们的代码总体流程如下:
好的,我们实现的TCP网络版本计算器的代码:网络版本计算器(注意:这里的完整代码是已经守护进程化的版本,但其实整体就多了一两行代码,不影响阅读,当然我们下篇要讲的就是守护进程相关内容)
总结
本篇重点是将自定义协议和序列化、反序列化理论和实践相结合从而写出我们的小项目——网络版本计算器,其内容量还是蛮大的,大家下来也一定要跟着写一写才能更好的理解相关知识