文章目录
- 协议的概念
- 序列化和反序列化
- 网络计算器
- 套接字接口的封装
- 服务端大致框架
- 协议的定制
- Request的序列化与反序列化
- Response的序列化与反序列化
- 报头的封装的解包
- 网络服务
- 服务端的封装
- 已提取报文的移除
- 客户端的封装
- 客户端的调用
- 服务端接收多个请求
- JSON 自动序列化反序列化
- 使用JSON再网络计算器中代替自定义的序列化与反序列化
- 网络计算器的守护进程化
- 重谈OSI七层模型
- 项目完整代码
协议的概念
协议实际上是指一组规则和约定,这些规则和约定定义在通信系统中的各方如何进行数据交换;
TCP是全双工的一种传输控制协议,在建立连接的双端中都存在着两个缓冲区,一个为输入缓冲区,一个为输出缓冲区;
当客户端试图向服务端发送一个请求时需要调用write()
函数,而这个函数并不能直接将数据从客户端发给服务端,实际上write()
函数只能将应用层的数据发送给TCP的发送缓冲区,对应的read()
函数只能将接收缓冲区的数据写入到应用层中,实际的网络传输功能都是通过TCP来进行的,在当前场景下只有TCP网络协议了解网络是否适合进行数据传输,包括数据如何发送,发送多少,发送出错的解决方案;
TCP是一种面向字节流的传输控制协议,而面向字节流意味着无论是什么数据TCP都会按照字节的方式进行传输,如存在一个客户端和一个服务端,客户端与服务端通过TCP协议建立连接,客户端向服务端发送若干个报文,所有的数据传输全权由TCP负责,客户端发给服务端的数据服务端无法直接知道自己收到的报文是否为一个完整的报文,是一个报文还是多个报文,所以需要定制协议,使得两端在进行数据传输时一端对报文进行封装时另一端能够根据协议来识别当前接收到的报文的个数及完整性,因为TCP协议只负责数据的传输,而数据的封装与解包是由应用层决定的(协议栈中的报头的封装与解包将有操作系统进行),TCP在进行数据传输中可能只传输了一个报文的一部分,或者是多个报文;
-
网络和系统之间的关系
TCP/IP
是属于操作系统的一部分,是存在操作系统源代码中的一部分,属于操作系统;数据链路层属于驱动部分;
应用层属于用户层;
所以实际上双端使用TCP建立连接后,一端调用write()
函数将数据发送给对端实际上是将用户层(应用层)中的数据写入(拷贝)至操作系统内,这与文件操作相同(在操作系统看来网络只是一种网络文件),在进行文件操作时若是用户需要将数据写入至磁盘内那么同样的要将数据调用write()
系统调用接口将数据写入至操作系统的缓冲区,文件系统再将数据从缓冲区写入至磁盘中;
通常情况下程序员编码的大多数都是应用层的代码,如服务端和客户端,实际上在进行编码时所用的socket()
,bind()
,listen()
,accept()
,connect()
等函数都是应用层的编码,这些函数本质上就是在为双端定制协议(约定),这些常见函数和操作其实是为了与底层网络协议及系统服务进行有效交互;
序列化和反序列化
-
序列化
序列化主要为将对象或数据结构转换为某种线性格式,一边保存到文件,数据库或者传输到网络中的一个过程;
-
反序列化
对应的反序列化是序列化的逆过程,即将预定义格式的字节流转换回原始的数据结构或对象;
如假设在进行数据的网络传输时,双端需要传递双端都认识的结构体变量,那么直接将结构体变量进行传输本质上是不可行的,将会存在以下问题:
-
字节序问题
不同的计算机架构可能使用不同的字节序(大端序或者小端序)来存储多字节的数据类型(如整形和浮点型);
如果将结构体直接进行传输,接收方计算机可能回因为字节序不同而误解数据;
-
对齐与填充问题
编译器处理结构体时,常加入填充字节来满足硬件的对其要求;
这些额外的字节因编译器和平台的不同而变化,如果直接传输结构体,在接收端读取时这些填充字节可能导致数据错位和不正确读取;
-
数据类型大小不一问题
不同平台和编译器对于同意数据类型可能存在不同的大小,如
int
类型可能在一些系统上为16
位,但在其他系统上可能为32
位,导致了直接传输结构体时通过字节解释数据不一致问题;
当一个结构体需要进行数据传输时为了避免上述问题可以将结构体的数据转化为一种特定的格式用于传输,这个格式必须是两端都认识的格式,并且能够以相同的手段将其封装或解包,其中这种双端都认识的结构体就叫做协议,从结构体转换为特定格式的过程被称为序列化,反之将特定格式的数据转化为结构体称之为反序列化;
网络计算器
假设需要设计一款网络计算器,服务端进行计算服务,客户端向服务端发送两个数据,服务端进行两个数据的计算并把最终结果响应给客户端;
那么在数据的传输前需要对双端进行约定,即客户端以什么方式将数据发送给服务端,服务端如何读取,服务端如何把计算后的结果发回给客户端,客户端如何读取,这里有两个方案:
-
将数据直接以字符串的形式传输并处理
客户端向服务端以
"1+1"
字符串的形式将数据发送,数据与数据之间只存在操作符作为间隔;这种方式有一些短板,虽然这种数据的传输方式可以解决将结构体变量进行网络传输所产生的大小端问题,但是这种数据并不能让对端很好的区别报文的详细信息,如该报文是否为一个完整的报文,所接收的数据是一个报文还是多个报文;
-
将数据以封装为一个结构体进行传输
同样的这样的方式也存在短板,虽然能够使数据在进行从传输过程中识别数据的详细信息,但是存在上文中提到的几个问题,包括大小端字节序问题,对齐与填充问题,数据类型大小不一等问题;
最优的解决方案是将上面的两个方案进行一种结合,即为双端定制协议,使双端都能识别到同一种结构,而在数据传输的时候采用较为简单的方式,如字符串,这里就涉及到了序列化与反向序列化的概念,将一个结构体变量序列化为一个字符串方便传输,对端接收后将字符串进行解析反向序列化回结构体并进行计算,计算结果作为一个响应同样的进行序列化,对端接收到响应时反向序列化进行读取;
这种方式实际上已经形成了某种网络分层,对于上层而言只需要进行双端的协议定制,而下层只需要对数据以一种特定的结构方式进行数据传输;
套接字接口的封装
为了在后期能够更加方便的使用网络套接字的接口,这里对套接字接口进行一次封装;
/* TcpSocket.hpp */
enum
{
SOCKET_ERR = 2,
BIND_ERR,
LISTEN_ERR
};
class NetSocket
{
public:
NetSocket() {}
~NetSocket()
{
if (sockfd_ >= 0)
{
close(sockfd_);
}
sockfd_ = -1;
}
public:
void Socket()
{
// 创建套接字
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
lg(FATAL, "socket fail, errno: %d, error message:%s", errno, strerror(errno));
exit(SOCKET_ERR);
}
lg(INFO, "socket sucess ...");
}
// 绑定
void Bind(const uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
// 填充
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
socklen_t len = sizeof(local);
if (bind(sockfd_, (struct sockaddr *)&local, len) < 0)
{
lg(FATAL, "bind fail, errno: %d, error message:%s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(INFO, "bind sucess ...");
}
// 设置监听状态
void Listen(int backlog = 7)
{
if (listen(sockfd_, backlog) < 0)
{
lg(FATAL, "listen fail, errno: %d, error message:%s", errno, strerror(errno));
exit(LISTEN_ERR);
}
lg(INFO, "listen sucess ...");
}
// 接收来自客户端的连接
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int retsockfd = accept(sockfd_, (struct sockaddr *)&peer, &len);
if (retsockfd < 0)
{
lg(WARNING, "accept fail, errno: %d, error message:%s", errno, strerror(errno));
return -1;
}
char ipstr[32];
inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
lg(INFO, "accept sucess ...");
return retsockfd;
}
// 向服务端发送连接
bool Connect(const std::string ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
socklen_t len = sizeof(peer);
if (connect(sockfd_, (sockaddr *)&peer, len) < 0)
{
lg(WARNING, "connect fail, errno: %d, error message:%s", errno, strerror(errno));
return false;
}
return true;
}
// 关闭不需要的套接字描述符
void Close()
{
if (sockfd_ >= 0)
{
close(sockfd_);
}
sockfd_ = -1;
}
private:
int sockfd_ = -1;
};
这段代码是网络套接字的封装,封装了一个NetSocket
类,定义了一个enum
枚举来定义可能发生错误的错误码;
这个类中构造函数和析构函数为空,因为创建套接字时不需要传递其他的参数,类中封装了一个成员变量int sockfd_ = -1
,-1
不是一个有效的文件描述符;
-
void Socket()
这个函数主要是用来创建套接字,调用
socket()
函数创建一个新的套接字,当套接字创建成功时将新的套接字描述符赋给私有成员sockfd_
中;如果
sockfd_
小于0
说明套接字创建失败,当套接字创建失败则不具备网络通信的条件,使用日志插件打印出对应的错误信息并退出进程即可; -
void Bind(const uint16_t port)
这个函数主要是用来绑定端口号,调用这个函数时函数的使用者需要传入一个端口号作为绑定的端口号;
函数内部创建了协议族结构体
struct sockaddr_in
并进行信息的填充,最终调用bind()
函数将端口号进行绑定;同样的如果
bind()
函数调用失败同样的这个套接字不具备网络通信的条件,使用日志插件打印出对应的错误信息并退出进程即可; -
void Listen(int backlog = 7)
这个函数是给服务端调用的,服务端需要设置为监听状态;
这个函数调用了
listen()
函数,这个函数需要传入两个参数;-
int sockfd
需要传入一个套接字描述符,这个类中已经封装了一个套接字描述符,所以不需要进行传入;
-
int backlog
传入一个
int
类型的数据作为服务端的连接队列最大长度;这里默认给值为
7
(缺省参数),用户可根据需求使用缺省参数也可直接传入参数;
当这个函数调用失败同样这个套接字不具备网络通信条件,使用日杂hi插件打印出对应错误日志信息并退出进程即可;
若是这个函数以及上述函数都调用成功时则表示具备网络通信条件;
-
-
int Accept(std::string *clientip, uint16_t *clientport)
这个函数是服务器用来获取对端连接的,内部封装了
accept()
函数,当服务端监听到了一个来自客户端的连接将会调用Accept()
函数,函数调用成功时将会返回一个新的套接字,这个套接字是用来进行TCP
通信的套接字;调用这个函数时需要传入两个输出型参数用来获取客户端发给服务端连接的客户端的基本信息,
clientip
表示客户端的IP
,clientport
表示客户端的端口号;这个函数调用失败并不是一个致命的错误,这个函数表示获取来自客户端的连接,若是获取失败可能需要进行其他处理而不是直接终止程序;
-
bool Connect(const std::string ip, const uint16_t &port)
这个函数是客户端向服务端发起连接的,内部封装了
connect()
函数,调用这个函数需要传递两个参数,分别为客户端自身的IP
与端口号;返回类型为
bool
类型来标定该函数的调用成功与否; -
void Close()
这个函数用来关闭套接字描述符;
这个函数主要是用来使多进程下关于多余的套接字描述符使用的;
服务端大致框架
/* ServerCalculator.hpp */
class TcpServer
{
public:
TcpServer() {}
bool Init() {
// 服务端初始化的步骤为创建套接字 绑定端口号 将套接字设置为监听状态
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
}
bool Start() {
while(true){
uint16_t clientport;
std::string clientip;
// 监听套接字中accept获取来自客户端的连接
int sockfd = listensock_.Accept(&clientip,&clientport);
if(sockfd<0) continue;
// 提供服务
}
}
~TcpServer() {}
private:
uint16_t port_;
NetSocket listensock_;
};
定义一个TcpServer
类对服务端进行封装,这个类封装了两个成员变量,一个是服务端用于绑定的端口号port_
以及封装的套接字相关网络接口的封装类listensock_
;
Init()
函数主要功能是为服务端进行基本的初始化,调用了自定义套接字类成员函数的Socket()
,Bind()
以及Listen()
函数进行套接字的创建,端口的绑定以及设置监听;
Start()
函数为将服务端运行起来,这里暂时提供的是一个长服务,循环调用Accept()
函数接收来自客户端的连接,当函数调用失败时并不是一个知名的错误,所以使用日志插件打印对应日志信息并continue
继续下一次循环重新接收来自客户端的连接;
协议的定制
协议是一种约定,需要定制一种服务端与客户端双端都认识的一种结构体类型;
/* protocol.hpp */
// 请求
class Request
{
public:
Request() {}
// 初始化需要三个参数 分别为两个操作数和一个操作符
Request(int num1, int num2, char op) : num1_(num1), num2_(num2), op_(op) {}
// 获取操作数
int GetNum1() const
{
return num1_;
}
// 获取操作数
int GetNum2() const
{
return num2_;
}
// 获取操作符
char GetOp() const
{
return op_;
}
private:
int num1_; // 操作数
int num2_;
char op_; // 操作符 + - * / %
};
// 响应
class Response
{
public:
Response(int res, int code) : result_(res), code_(code) {}
Response() {}
// 获取返回值
int GetResult() const
{
return result_;
}
// 获取错误码
int GetCode() const
{
return code_;
}
private:
int result_; // 结果
int code_; // 错误码
};
这个网络计算器提供了整型数据的+, -, *, /, %
操作;
定义了两个类,一个类为Request
表示请求操作,客户端需要传递对应的操作数与操作符作为请求发送给服务端进行处理,服务端接收到请求后对客户端发来的请求进行处理,处理完毕后以Response
类对象的方式将结果进行封装,包括结果以及错误码,使用错误码标识该结果是否可信;
同时数据在网络中的传输不建议使用直接传输类对象,所以在进行网络传输的时候需要将其转化为一种特定的类型,这个类型的数据读取不会因为双端的大小端等问题影响,所以需要将封装后的类对象进行序列化和反序列化的操作,其中序列化即将这个类对象转化为这个特定的类型,反序列化则是将这个特定类型的数据转换回对应的类对象,在这里可以使用std::string
作为一个特定的类型,以
空格或是\n
换行作为数据之间的分隔符方便数据的转换;
同时在Request
与Response
类中定义了几个Get
函数用来获取类中的被私有化的数据;
Request的序列化与反序列化
/* protocol.hpp */
const std::string spacesep = " ";
const std::string separator = "\n";
class Request
{
public:
// 序列化
bool Serialize(std::string *out)
{
std::string s = std::to_string(num1_);
s += spacesep; // 报文中数据与数据之间的分隔符
s += op_;
s += spacesep;
s += std::to_string(num2_);
// 将数据序列化为 ("num1 op num2") 的风格
*out = s;
return true;
}
// 反序列化
bool Deserialize(const std::string &str)
{
size_t left = str.find(spacesep);
if (left == std::string::npos)
return false;
std::string num1 = str.substr(0, left);
size_t right = str.rfind(spacesep);
if (right == std::string::npos)
return false;
std::string num2 = str.substr(right + 1);
if (left + 2 != right)
return false;
op_ = str[left + 1];
num1_ = std::stoi(num1);
num2_ = std::stoi(num2);
return true;
}
private:
int num1_; // 操作数
int num2_;
char op_; // 操作符
};
其中Serialize()
函数为序列化,Deserialize()
函数为反序列化,序列化操作把该类接收到的两个操作数和一个操作符通过字符串拼接的方式拼接为"num1 + num2"
的方式,而反序列化则是通过字符串string
自带的find()
函数和rfind()
函数再通过substr()
函数将字符串中的各个部分的数据进行裁剪,最终再以std::string::stoi()
将操作数转化为原本的类型,操作符op
则直接进行赋值;
通常情况下数据的序列化不会产生什么太大问题,但是数据的反序列化可能会因为数据读取的不全导致读取失败;
序列化时将传入一个输出型参数,通过输出型参数将这个类数据的序列化带出来,同样的反序列化需要传入一个输入型参数,这个输入型参数将用来对Request
类对象进行赋值操作;
可以直接通过构造Request
对象来进行测试;
-
测试
int main() { Request req(123456, 123456, '+'); std::string s; req.Serialize(&s); cout << s << endl; req.Deserialize(s); cout << req.GetNum1() << endl; cout << req.GetOp() << endl; cout << req.GetNum2() << endl; return 0; } /* 运行结果: $ ./servercal 123456 + 123456 123456 + 123456 */
在测试文件中构造了一个
Request
对象并且传入两个123456
为操作数,+
为操作符;创建一个空字符串
s
作为序列化数据后接收返回的参数,调用Serialize()
函数对数据进行序列化,并将序列化的字符串进行打印;调用
Deserialize()
函数对序列化的数据进行反序列化,并通过几个Get()
获取函数将各个部分的对象内容进行打印;最终运行结果与预期相同;
Response的序列化与反序列化
/* protocol.hpp */
const std::string spacesep = " ";
const std::string separator = "\n";
// 序列化
class Response
{
public:
bool Serialize(std::string *out)
{
std::string s = std::to_string(result_);
s += spacesep;
s += std::to_string(code_);
*out = s;
return true;
}
// 反序列化
bool Deserialize(const std::string &str) // "result code"
{
size_t pos = str.find(spacesep);
if (pos == std::string::npos)
{
return false;
}
std::string resultstr = str.substr(0, pos);
std::string codestr = str.substr(pos + 1);
result_ = std::stoi(resultstr);
code_ = std::stoi(codestr);
return true;
}
private:
int result_; // 结果
int code_; // 错误码
};
这里的序列化与反序列化思路与Request
中的序列化与反序列化的思路相同,即采用字符串相加的方式将类对象数据进行序列化,反序列化则是将序列化的字符串通过find()
找出分隔符并调用substr()
将字符串进行提取;
-
测试
int main() { Response res(200, 0); std::string r; res.Serialize(&r); cout << r << endl; res.Deserialize(r); cout << res.GetResult() << endl; cout << res.GetCode() << endl; return 0; } /* 运行结果: $ ./servercal 200 0 200 0 */
测试的运行结果与预期相同;
报头的封装的解包
数据在进行网络传输的时候光对数据进行序列化与反序列化可能并不够,可能需要为数据进行封装自定义的报头,对应的对端在接收到整个报文时需要对报头进行解包;
报头的作用可以使接收端通过接收报文的报头信息来检查所接收的报文是否残缺,以保证报文的完整性;
报头可以对数据的完整性进行检查,常见的自定义协议的报头有:
-
长度验证
报头中包含数据包的长度信息,使接收端能检查接收到的数据是否完整;
-
校验
可以在报头中加入校验和或其他完整性校验信息以检测报文在传输过程中是否被篡改;
-
标识符
可以在报头中添加标识符用于区分不同类型的报文或协议版本;
在这个网络计算机中为可以为序列化后的字符串数据及逆行简单的长度验证作为报头进行封装;
以Request
中序列化后的数据num1 op num2
进行报头的封装即为"len"\n"num1 op num2"\n
,对应的Response
序列化后的数据result code
进行报头的封装即为"len"\n"result code"\n
,其中len
长度为序列化后的数据的长度,当对端接收到整段报文后可以提取出对应的长度并对整段报文的长度进行判断,如果报文的长度与len
不同则表示报文接收的不完全;
-
封装
/* protocol.hpp */ const std::string spacesep = " "; const std::string separator = "\n"; std::string Encode(std::string &text) { // 把长度转化为 string 对象并作为报头封装在报文的最前位置 std::string package = std::to_string(text.size()); // 报头添加分隔符 package += separator; // 分隔符后再次添加报文的正文部分 package += text; // 再次添加分隔符 package += separator; return package; }
这个报头的封装函数需要传入序列化后的
string
对象,返回一个string
对象,函数中创建一个空串先将序列化的报文长度转化为字符串放置在首位,再这个报头后添加一个\n
作为分隔符,报头后连接数据正文部分,数据正文部分后再次跟上一个\n
; -
解包
/* protocol.hpp */ const std::string spacesep = " "; const std::string separator = "\n"; bool Decode(std::string &package, std::string *text) { // "len"\n"num1 op num2"\n // "len"\n"result code"\n // 找到分隔符的位置 并取出长度 size_t pos = package.find(separator); if (pos == std::string::npos) return false; std::string len_str = package.substr(0, pos); size_t len = std::stoi(len_str); // 将长度进行计算 并判断长度是否符合 不符合则直接返回 false size_t total_len = len_str.size() + len + 2; if (package.size() < total_len) { return false; } // 以输出型参数的方式把接报后的报文写入 text 中 *text = package.substr(pos + 1, len); // 移除提取出的报文 // 返回 true return true; }
这个解包的函数需要传入两个参数,
package
表示传入需要进行解包的数据,即接收到的数据,text
为一个输出型参数,解包后的数据将被写入到text
中;首先将报文中代表长度的部分(报头)取出,取出后判断解包前字段的长度是否小于应有的长度,如果小于则表示数据接收不全,若是解析将出现错误,返回
false
,如果不小于则表示接收的报文量>=1
,即接收的报文中存在至少一个完整的报文,可以对报文进行解析;随后将报文进行提取并解析,并擦除已经被提取了的报文;
-
测试
int main() { Request req(123456, 123456, '+'); std::string s; req.Serialize(&s); cout << s << endl; cout << "---------------------" << endl; s = Encode(s); cout << s << endl; cout << "---------------------" << endl; Decode(s, &s); cout << s << endl; cout << "---------------------" << endl; req.Deserialize(s); cout << req.GetNum1() << endl; cout << req.GetOp() << endl; cout << req.GetNum2() << endl; // ---------------- cout << "######################" << endl; cout << "######################" << endl; Response res(200, 0); std::string r; res.Serialize(&r); cout << r << endl; cout << "---------------------" << endl; r = Encode(r); cout << r << endl; cout << "---------------------" << endl; Decode(r, &r); cout << r << endl; cout << "---------------------" << endl; res.Deserialize(r); cout << res.GetResult() << endl; cout << res.GetCode() << endl; return 0; } /* 运行结果: $ ./servercal 123456 + 123456 --------------------- 15 123456 + 123456 --------------------- 123456 + 123456 --------------------- 123456 + 123456 ###################### ###################### 200 0 --------------------- 5 200 0 --------------------- 200 0 --------------------- 200 0 */
对
Request
类和Response
类都进行了测试,测试结果与预期相符;
解包的过程不仅要判断报文发送的是否完整,还需要将已经被提取了的报文进行擦除,由于这里的操作并还未直接与网络进行连接所以为对该功能进行编写,将在下面的部分对已提取的报文的擦除操作进行编写;
网络服务
当用户收到一个报文时表示收到了一个请求,这个请求必然是被序列化后的且调用了Encode()
函数被封装了报头了的报文,因此服务端当接收到一个请求时需要调用Decode()
对报文进行报头的解包;
调用Decpde()
函数将会返回一个bool
类型的值,这个值可以判断客户端发来的报文是否是一个完整的报文,所以当该函数调用失败时则返回,同时报文接收的不完整不是一个知名的错误,所以直接返回函数即可不需要终止进程,也不需要对报文进行解析;
当报文进行报头的解包成功后则表示这个报文起码是一个完整的报文,可以对报文进行下一步的反序列化工作;
同样的反序列化工作也可能会出现错误,当反序列化失败时也表示解析错误,直接返回即可;
当反序列成功后表示当前的请求已经是一个结构化数据,可以进行下一步的对客户端所发的请求进行处理,这里是整形计算器对应的网络服务是加减乘除取模等操作;
/* CalRequest.hpp */
enum
{
SUCESS = 0,
DIVERR = 2,
MODERR,
NONE
};
class ServerCal
{
ServerCal() {};
Response CalculatorHandler(const Request &req)
{
// 提前声明Response变量,这样可以确保每个case都是同一作用域中的变量
Response resp(-1, NONE);
int num1 = req.GetNum1();
int num2 = req.GetNum2();
switch (req.GetOp())
{
case '+':
resp = Response(num1 + num2, SUCESS);
break;
case '-':
resp = Response(num1 - num2, SUCESS);
break;
case '*':
resp = Response(num1 * num2, SUCESS);
break;
case '/':
if (num2 == 0)
{
resp = Response(-1, DIVERR);
}
else
{
resp = Response(num1 / num2, SUCESS);
}
break;
case '%':
if (num2 == 0)
{
resp = Response(-1, MODERR);
}
else
{
resp = Response(num1 % num2, SUCESS);
}
break;
default:
resp = Response(-1, NONE);
break;
}
return resp;
}
std::string Calculator(std::string &package)
{
std::string text;
// 将接收到的整个报文进行去报头
if (!Decode(package, &text))
return "";
// 将去报头的报文进行反序列化
Request req;
if (!req.Deserialize(text))
return "";
// 将 text 清空,复用于给响应返回
text = "";
// 计算结果
Response resp = CalculatorHandler(req);
// 将计算结果进行序列化
resp.Serialize(&text);
// 将序列化后的数据添加报头
text = Encode(text);
// 将添加报头后的报文进行返回
return text;
}
~ServerCal() {};
};
其中Calculator()
函数进行报文的去报头操作以及反序列化操作,并调用CalculatorHandler()
函数进行计算器的计算操作;
服务端的封装
上文完成了对服务端的大致封装,以Init()
函数为主,Start()
函数主要对服务端的执行操作,首先获取来自客户端的连接,当建立连接后需要对客户端提供给服务,可以使用父进程调用双重fork()
创建出孙子进程并使孙子进程进行服务的整体操作,父进程则是去监听下一个来自其他客户端的连接;
class TcpServer
{
public:
bool Start()
{
while (true)
{
signal(SIGCHLD, SIG_IGN);
uint16_t clientport;
std::string clientip;
int sockfd = listensock_.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
// 提供服务
if (fork() == 0)
{
// child
if (fork() == 0)
{
// grandchild
listensock_.Close(); // 关闭不需要的套接字描述符
// ...
}
exit(0);
}
// parent
close(sockfd);
}
return true;
}
~TcpServer() {}
private:
uint16_t port_;
NetSocket listensock_;
func_t callback_;
};
采用双重fork()
的形式是多进程的形式,需要关闭父子进程中不需要的多余的套接字描述符;
服务端在建立连接后需要调用read()
函数读取来自客户端的请求,客户端所发来的报文是一个string
对象,在上文中CalRequest.hpp
文件中的ServerCal
类已经提供了服务端的主要服务内容;
class ServerCal
{
std::string Calculator(std::string &package)
{
std::string text;
// 将接收到的整个报文进行去报头
if (!Decode(package, &text))
return "";
// 将去报头的报文进行反序列化
Request req;
if (!req.Deserialize(text))
return "";
// 将 text 清空,复用于给响应返回
text = "";
// 计算结果
Response resp = CalculatorHandler(req);
// 将计算结果进行序列化
resp.Serialize(&text);
// 将序列化后的数据添加报头
text = Encode(text);
// Debug 打印出计算,序列化,报头封装后的报文
lg(DEBUG, "Calculator() debug : %s", text.c_str());
// 将添加报头后的报文进行返回
return text;
}
};
其中Calculator
函数将负责接收完整报文,对完整报文进行去报头,序列化,并调用计算函数进行计算,将计算结果再次序列化封装报头并返回,那么整个服务端中代表核心功能的函数则是这个函数,可以使用function<>
包装器对函数进行包装,并且使用bind()
绑定接口将需要的参数预先绑定到函数中;
/* ServerCalculator.hpp */
using func_t = std::function<std::string(std::string &package)>;
class TcpServer
{
public:
TcpServer(uint16_t port, func_t callback):port_(port),callback_(callback) {}
bool Init()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
bool Start()
{
while (true)
{
signal(SIGCHLD, SIG_IGN);
uint16_t clientport;
std::string clientip;
int sockfd = listensock_.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
// 提供服务
if (fork() == 0)
{
// child
if (fork() == 0)
{
// grandchild
listensock_.Close(); // 关闭不需要的套接字描述符
std::string in_stream;
while (true)
{
char buffer[128];
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
in_stream += buffer;
// 调用回调函数
std::string info = callback_(in_stream);
// 返回为空串表示调用解析时失败
if (info.empty())
{
lg(WARNING, "Analysis fali");
continue;
}
// 调试
lg(DEBUG, "Start() debug : %s", info.c_str());
// 将报文写回给客户端
write(sockfd, info.c_str(), info.size());
}
}
return true;
}
~TcpServer() {
delete this; // 对自己进行释放操作
}
private:
uint16_t port_;
NetSocket listensock_;
func_t callback_; // function 函数包包装器封装的函数对象
};
这里使用function<>
包装器包装了一个函数类型,并在TcpServer
类中声明了一个该类型的函数成员变量callback_
作为回调函数,服务端需要传入ServerCal
类中的std::string Calculator(std::string &package)
成员函数作为回调函数,当该函数调用成功后将会返回计算后的序列化并且封装了报头的报文,在这段代码中处理好的报文会被info
接收,可以使用日志插件对报文进行打印Debug
;
同样的具体的回调函数调用可以在主函数中进行;
/* server.cc */
void Usage()
{
std::cout << "\n\t" << "Usage: ./servercal port[>1024]" << std::endl
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage();
exit(0);
}
ServerCal cal;
uint16_t port = std::stoi(argv[1]);
TcpServer *tsvp = new TcpServer(port, std::bind(&ServerCal::Calculator, &cal, std::placeholders::_1));
tsvp->Init();
tsvp->Start();
return 0;
}
这个函数是服务端的整体调用,首先封装了一个Usage()
函数作为手册函数,这个手册函数将告诉服务端的使用者该函数的使用方式,当调用时传入的命令行参数数量不正确时将调用这个函数;
在这个函数中实例化出一个ServerCal
具体请求对象用于服务端对客户端提供的服务cal
;
接收使用者传递的端口号,并且用new
实例化出一个TcpServer
对象tsvp
为服务端对象,其中传入两个参数,一个参数为使用者传入的端口号,一个为使用bind()
绑定的ServerCal::Calculator()
成员函数为回调函数;
在主函数中不需要调用delete
来释放堆空间中的tsvp
,在TcpServer
中的析构函数将自动把this
指针进行释放;
-
测试
由于客户端并没有编写,这里采用
telnet
工具进行测试,测试结果与预期相同;
这里的测试是使用telnet
工具直接向服务端传输一个处理好的请求报文,即"len"\n"num1 op num2"\n
格式的字符串;
已提取报文的移除
服务端接收来自客户端的报文主要是通过调用read()
函数进行的,但这并不是实际的传输,而是将报文由内核缓冲区写入到用户缓冲区中,实际上的数据传输是TCP传输控制协议进行的,用户区无法判断TCP协议传输了多少的数据,所以服务端可能会提取到一个报文或者多个报文或者是不完整的报文;
当提取到多个报文时对应的需要对报文进行解析与提取,当提取并解析结束后对应的这个完整的报文应该也需要从用户缓冲区中移除;
在上文中完成的服务端并未进行已提取报文的移除,这将导致服务端只会对同一个报文进行分析;
在这个例子中重新向服务端进行输入,但服务端只对一个报文进行解析;
对已提取报文的移除操作只需要在Decode()
报头的解包函数中调用string::erase()
函数即可;
bool Decode(std::string &package, std::string *text)
{
// "len"\n"num1 op num2"\n
// "len"\n"result code"\n
size_t pos = package.find(separator);
if (pos == std::string::npos)
return false;
std::string len_str = package.substr(0, pos);
size_t len = std::stoi(len_str);
size_t total_len = len_str.size() + len + 2;
if (package.size() < total_len)
{
return false;
}
*text = package.substr(pos + 1, len);
// 对已提取报文进行 erase 移除
package.erase(0, total_len);
return true;
}
-
测试
这里使用的是
telnet
工具进行测试,而telnet
工具默认的分隔符为\r\n
,将会出现解析错误的问题;所以这里可以直接打印或者使用自定义日志文件打印进行调试来进行验证;
bool Decode(std::string &package, std::string *text) { //... lg(DEBUG, "\n-------------------\nErase before package : %s \n", package.c_str()); // 对已提取报文进行 erase 移除 package.erase(0, total_len); lg(DEBUG, "\n-------------------\nErase after package : %s \n", package.c_str()); return true; }
客户端的封装
对客户端进行封装,同样的客户端主要需要调用两个函数,Init()
成员函数与Start()
成员函数;
Init()
成员函数主要对客户端进行初始化,主要的初始化为流式套接字的创建,在上文中已经对套接字进行了封装,相应的Init()
函数只需要调用NetSocket
类中的Socket()
成员函数,这也意味着客户端类中需要封装一个该类的成员变量;
Start()
函数主要是用来客户端向服务端发送的请求,在发送请求前客户端应先向服务端发送连接,当服务端监听到来自客户端的连接并成功与客户端建立连接时客户端才能向服务端发送请求Request()
;
/* ClientCalculator.hpp */
class TcpClient
{
public:
TcpClient(std::string ip, uint16_t port) : ip_(ip), port_(port) {}
~TcpClient() {}
public:
bool Init()
{
sock_.Socket();
}
bool Start()
{
bool r = sock_.Connect(ip_, port_);
if (!r)
return 1;
}
private:
std::string ip_;
uint16_t port_;
NetSocket sock_;
};
客户端不需要绑定端口号,当客户端向服务端发送连接的时候客户端将自动绑定随机端口号;
当服务端与客户端建立连接后客户端需要向服务端发送请求,协议是面向双方的,当服务端遵守协议后对应的客户端也应该遵守协议,在上文中已经对协议进行了编写,所以客户端只需要在发送请求的时候遵守协议,同时解析来自服务端的响应时也需要遵守协议;
/* ClientCalculator.hpp */
class TcpClient
{
public:
bool Start()
{
bool r = sock_.Connect(ip_, port_);
if (!r)
return false;
srand(time(nullptr) ^ getpid());
int cnt = 3;
const std::string opers = "+_*?%^=!";
while (cnt--)
{
int num1 = rand() % 100 + 1;
usleep(100);
int num2 = rand() % 100 + 1;
usleep(100);
char op = opers[rand() % opers.size()];
// 构造
Request req(num1, num2, op);
req.DebugPrint();
// 序列化
std::string package;
req.Serialize(&package);
// 封装报头
package = Encode(package);
std::cout << "The latest request was not sent : " << package << std::endl;
// 写入请求
write(sock_.GetFd(), package.c_str(), package.size());
// write 返回值代表写了多少长度
// 读取响应
char buff[128];
std::string getresp;
ssize_t n = read(sock_.GetFd(), buff, sizeof(buff));
std::string content;
if (n > 0)
{
buff[n] = 0;
getresp = buff;
// 解包报头
bool r = Decode(getresp, &content);
assert(r);
}
// 反序列化
Response resp;
r = resp.Deserialize(content);
assert(r);
resp.DebugPrint();
}
return true;
}
private:
std::string ip_;
uint16_t port_;
NetSocket sock_;
};
这里的请求并没有设计成交互式的,也可根据自己的需求更改为交互式的;
协议是双方都要遵守的,客户端按照协议实例化了一个Request
请求对象req
,并传入两个数以及一个操作符,操作符同样采用伪随机的方式在构造的opers
字符串中随机获取一个;
这里在请求Request
以及响应Response
类中都添加了一个成员函数DebugPrint()
用于打印构造后的信息从而进行调试;
/* protocol.hpp*/
class Request
{
public:
void DebugPrint()
{
std::cout << std::endl
<< "New Request: " << num1_ << op_ << num2_ << " =? " << std::endl;
}
private:
int num1_; // 操作数
int num2_;
char op_; // 操作符
};
// -------------------
class Response
{
public:
void DebugPrint()
{
std::cout << std::endl
<< "New Response: " << "result = " << result_ << " ,code = " << code_ << std::endl;
std::cout << "####################################" << std::endl;
}
private:
int result_; // 结果
int code_; // 错误码
};
当请求对象构造好后调用请求对象中的DebugPrint()
成员函数打印出对应的内容进行调试;
请求对象构造好后需要将对象按照协议进行序列化,调用其中的Serialize()
成员函数进行序列化,在进行序列化后调用Encode()
函数进行报头的封装;
当一个请求对象被构造,序列化,封装报头后就可以进行网络通信,调用write()
函数将该报文写入至内核缓冲区,由传输控制协议TCP进行报文的传输,当然这里可以判断write()
函数的返回值来查看写入了多少字节的数据;
客户端需要调用write()
函数,在调用该函数的时候需要传入一个套接字描述符,所以在NetSocket
类中补充了一个成员函数为GetFd()
来获取该TCP套接字的描述符;
/* TcpSocket.hpp*/
class NetSocket
{
public:
int GetFd()
{
return sockfd_;
}
private:
int sockfd_ = -1;
};
当调用write()
函数后对应的报文将从用户缓冲区写至内核缓冲区,并且将通过TCP传输控制协议发送给服务端,服务端将按照约定(协议)将报文进行解析与处理,发送一个序列化且封装了报头的响应报文发回给客户端;
客户端接收到报文之后同样需要按照约定将响应报文进行去报头与反序列化,反序列化后报文将反序列化为一个响应对象,只需要调用这个响应对象中的DebugPrint()
成员函数将内容打印即可;
客户端的调用
客户端的调用与服务端的调用相同,只需要先调用Init()
函数进行客户端对象的初始化,随后调用Start()
函数进行客户端的运行即可;
/* client.cc */
void Usage() { printf("\n\tUsage : ./client ip port[port>1024]\n\n"); }
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage();
exit(-1);
}
// 获取用户输入的IP与端口号
int16_t port = std::stoi(argv[2]);
std::string ip(argv[1]);
// 实例化客户端对象
TcpClient tc(ip, port);
tc.Init();
tc.Start();
return 0;
}
这里的调用方式与服务端的调用方式相同,不作赘述;
-
测试
结果与预期相符;
服务端接收多个请求
同样的服务端可能会在同一个时间中获取来自客户端的多个请求,那么当获取到多个请求时同样需要对多个请求进行处理;
可以通过while
循环控制服务端处理来自客户端的多个请求;
/* ServerCalculator.hpp */
class TcpServer
{
public:
bool Start()
{
while (true)
{
signal(SIGCHLD, SIG_IGN);
uint16_t clientport;
std::string clientip;
int sockfd = listensock_.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
// 提供服务
if (fork() == 0)
{
// child
if (fork() == 0)
{
// grandchild
listensock_.Close(); // 关闭不需要的套接字描述符
std::string in_stream;
while (true)
{
char buffer[128];
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
in_stream += buffer;
while (true) // 循环处理多个请求
{
std::string info = callback_(in_stream);
if (info.empty())
{
lg(WARNING, "Analysis fali");
break;
}
lg(DEBUG, "Start() debug : %s", info.c_str());
write(sockfd, info.c_str(), info.size());
}
}
}
return true;
}
exit(0);
}
// parent
close(sockfd);
}
return true;
}
private:
uint16_t port_;
NetSocket listensock_;
func_t callback_;
};
循环解析读取到的报文,当报文解析错误时将会返回一个空串,当返回为空串时则表示解析错误,接收到的报文或者处理完的剩下的报文不存在完整的;
-
测试
当客户端向服务端发送多个请求时服务端将会处理这些多个请求;
/* ClientCalculator.hpp */ class TcpClient { public: bool Start() { // ... std::cout << "The latest request was not sent : " << package << std::endl; // 写入请求 write(sock_.GetFd(), package.c_str(), package.size()); write(sock_.GetFd(), package.c_str(), package.size()); write(sock_.GetFd(), package.c_str(), package.size()); // ... } return true; } private: std::string ip_; uint16_t port_; NetSocket sock_; };
这里的客户端并没有进行太大的改动,只是多次调用
write()
函数将同一个报文发送多次;客户端并没有改动所以同样的只会进行一次打印;
当然客户端也可以同时处理多个响应,即多次对缓冲区内的报文进行解析,直至报文中不存在一个完整的
Response
响应即可;
JSON 自动序列化反序列化
JSON是一种轻量级的数据交换格式,通常情况下当一个结构数据使用JSON进行序列化后,该结构数据中的各个数据都是以Key-Value
的表示的;
在C++中JSON是一个第三方库,在使用第三方库时需要对其进行安装,同时在编译时需要进行链接;
sudo yum install -y jsoncpp-devel # CentOS 使用 yum 进行安装
使用上述命令对JSON第三方库进行安装;
在编译时同样需要链接该库;
g++ -o mytest test.cc -ljsoncpp
默认情况下JSON的头文件被安装在/usr/include/jsoncpp/json
目录中;
可以看到在该目录下存在多个头文件,实际上用的最多的头文件为json.h
头文件,而编译器只认得到/usr/include
目录,通常情况下只会在该目录下寻找头文件,所以对应的在使用json
对应的头文件时应该带路径;
假设需要使用json.h
头文件则需要像下面这样对头文件进行引入;
#include <jsoncpp/json/json.h>
-
序列化
JSON中存在一个
Value
,可以定义一个万能对象来接收任何类型的参数;对应的这个万能对象将以
Key-Value
的方式对这些数据进行存储;int main() { Json::Value root; root["num1"] = 20; root["num2"] = 10; root["op"] = '+'; root["text"] = "hello world"; Json::FastWriter fastw; std::string ret = fastw.write(root); std::cout << std::endl; std::cout << ret << std::endl; std::cout << "-----------------" << std::endl; Json::StyledWriter stylew; ret = stylew.write(root); std::cout << ret << std::endl; return 0; }
在这个例子中定义了一个万能对象
root
,其中存储了四个参数分别为num1
,num2
,op
,和text
;在进行序列化时需要实例化对应的序列化对象,JSON的序列化方式有两种,一种为
Json::FastWriter
,这种方式的序列化将更快,因为不需要格式的定义,还有一种序列化方式为Json::StyledWriter
,这种序列化方式将以一个更加清晰的分隔符(换行)来使序列化后的数据更加具有可读性;当进行序列化时需要通过实例化的序列化对象调用其中的成员函数
write()
,这个成员函数需要传入利用JSON创建的万能对象;最终结果如下:
上部分的为快速序列化所序列化的对象,下部分的为风格序列化过后的数据;
其中的区别为有无分隔符;
通常情况下序列化后的数据可以用
string
对象进行接收(内存缓冲区,文件流或者字符串流); -
反序列化
反序列化同样的需要存在一个反序列化对象用来调用反序列化
Json::Reader
的成员方法,以及一个用来接收反序列化后的Json::Value
万能对象;通常情况下反序列化需要调用反序列化对象
Json::Reader
中的成员函数parse()
,通常这个函数需要传入两个参数,分别为需要反序列化的字符串以及一个输出型参数,即接收反序列化后的万能对象;当反序列化完成后需要对万能对象中的各个参数进行提取,同样以
[]
以Key-Value
的方式进行提取,在提取过程中需要调用对应的asXXXX
成员方法来将其转化为对应的类型才能被对应类型进行接收;int main() { Json::Value root; root["num1"] = 20; root["num2"] = 10; root["op"] = '+'; root["text"] = "hello world"; Json::FastWriter fastw; // 序列化对象 std::string ret = fastw.write(root); // 进行序列化并用string接收 std::cout << std::endl << ret << std::endl; Json::Value v; // 用于接收反序列化后的万能对象 Json::Reader reader; // 反序列化对象 reader.parse(ret, v); // 调用反序列化成员函数 // 将反序列化后的成员采用内置类型一一进行提取 提取时需调用asXXX函数将其转化为对应的数据类型 int x = v["num1"].asInt(); int y = v["num2"].asInt(); char op = v["op"].asInt(); std::string str = v["text"].asString(); std::cout << "x : " << x << std::endl; std::cout << "y : " << y << std::endl; std::cout << "op : " << op << std::endl; std::cout << "str : " << str << std::endl; return 0; }
在这个例子中首先将上文的数据进行序列化,然后再演示反序列化;
首实例化一个
Json::Value v
万能对象来接收反序列化后的万能对象,并实例化一个用于反序列化的对象Json::Reader reader
并调用reader
的成员方法parse()
函数,将v
与需要反序列化的数据ret
作为参数传入;当反序列化完成后
v
将接收到这个反序列化后的对象,采用Key-Value
的方式,即[]
的方式将其取出并调用对应内置类型的asXXXX
成员方法将其转化为对应的类型即可;
对应的Json::Value
是一个万能对象,既然是万能对象那就表示这个万能对象也可以接收一个Json::Value
万能对象,即Json
的嵌套;
使用JSON再网络计算器中代替自定义的序列化与反序列化
序列化与反序列化部分是属于协议定制的一部分,只需要修改protocol.hpp
头文件即可;
这里为了方便后续的调试,使用条件编译来区别自定义序列化与反序列化和使用JSON进行的序列化与反序列化;
#ifndef PROTOCOL_HPP
#define PROTOCOL_HPP
#include <iostream>
#include "log.hpp"
#include <string>
#include <jsoncpp/json/json.h>
// #define Myself 1
const std::string spacesep = " ";
const std::string separator = "\n";
// 编码函数,将文本编码成特定格式的字符串
std::string Encode(std::string &text)
{
std::string package = std::to_string(text.size());
package += separator;
package += text;
package += separator;
return package;
}
// 解码函数,从编码后的字符串提取出原始文本
bool Decode(std::string &package, std::string *text)
{
// "len"\n"num1 op num2"\n 或 "len"\n"result code"\n
size_t pos = package.find(separator);
if (pos == std::string::npos)
return false;
std::string len_str = package.substr(0, pos);
size_t len = std::stoi(len_str);
size_t total_len = len_str.size() + len + 2;
if (package.size() < total_len)
{
return false;
}
*text = package.substr(pos + 1, len);
// 移除已提取的报文内容
package.erase(0, total_len);
return true;
}
class Request
{
public:
Request(int num1, int num2, char op) : num1_(num1), num2_(num2), op_(op) {}
Request() {}
public:
int GetNum1() const { return num1_; }
int GetNum2() const { return num2_; }
char GetOp() const { return op_; }
void DebugPrint()
{
std::cout << std::endl
<< "New Request: " << num1_ << op_ << num2_ << " =? " << std::endl;
}
public:
// 序列化请求对象,转为字符串
bool Serialize(std::string *out)
{
#ifdef Myself
std::string s = std::to_string(num1_);
s += spacesep; // 数据之间的分隔符
s += op_;
s += spacesep;
s += std::to_string(num2_);
*out = s;
return true;
#else
// JSON 格式序列化
Json::Value root;
root["x"] = num1_;
root["y"] = num2_;
root["op"] = op_;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
// 反序列化字符串,转为请求对象
bool Deserialize(const std::string &str)
{
#ifdef Myself
size_t left = str.find(spacesep);
if (left == std::string::npos)
return false;
std::string num1 = str.substr(0, left);
size_t right = str.rfind(spacesep);
if (right == std::string::npos)
return false;
std::string num2 = str.substr(right + 1);
if (left + 2 != right)
return false;
op_ = str[left + 1];
num1_ = std::stoi(num1);
num2_ = std::stoi(num2);
return true;
#else
// JSON 格式反序列化
Json::Value root;
Json::Reader r;
r.parse(str, root);
num1_ = root["x"].asInt();
num2_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
private:
int num1_; // 操作数1
int num2_; // 操作数2
char op_; // 运算符
};
class Response
{
public:
Response(int res, int code) : result_(res), code_(code) {}
Response() {}
public:
int GetResult() const { return result_; }
int GetCode() const { return code_; }
public:
// 序列化响应对象,转为字符串
bool Serialize(std::string *out)
{
#ifdef Myself
std::string s = std::to_string(result_);
s += spacesep;
s += std::to_string(code_);
*out = s;
return true;
#else
// JSON 格式序列化
Json::Value root;
root["result"] = result_;
root["code"] = code_;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
// 反序列化字符串,转为响应对象
bool Deserialize(const std::string &str)
{
#ifdef Myself
size_t pos = str.find(spacesep);
if (pos == std::string::npos)
{
return false;
}
std::string resultstr = str.substr(0, pos);
std::string codestr = str.substr(pos + 1);
result_ = std::stoi(resultstr);
code_ = std::stoi(codestr);
return true;
#else
// JSON 格式反序列化
Json::Value root;
Json::Reader r;
r.parse(str, root);
result_ = root["result"].asInt();
code_ = root["code"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << std::endl
<< "New Response: result = " << result_ << ", code = " << code_ << std::endl;
std::cout << "####################################" << std::endl;
}
private:
int result_; // 运算结果
int code_; // 错误码
};
#endif // PROTOCOL_HPP
这段代码中添加了条件编译,当定义了对应的宏#define Myself 1
时将使用自定义的序列化与反序列化,若是没有定义宏则默认使用JSON的序列化与反序列化;
同时由于这里的序列化与反序列化最终以字符串流的方式接收,与封装报头与解包报头Encode()
,Decode()
不冲突,无需再为这两个函数单独进行修改;
在原本的代码测试中使用了DebugPrint()
进行调试,同时这里使用的序列化方式采用的是Json::StyledWriter
的序列化方式,将以一种更加具有可读性的方式对数据进行格式化;
-
测试
同样的运行服务端与客户端;
运行结果与预期相符;
网络计算器的守护进程化
在『 Linux 』简单TCP英译汉程序中编写了一个守护进程的Demo;
/* daemon.hpp */
const std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{
// 4.标准输入 标准输出 标准错误重定向至/dev/null中
int fd = open(nullfile.c_str(), O_RDWR); // 以读写方式打开文件
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
将该Demo文件复制到该项目的根目录当中,在服务端中引入该头文件并在服务端的Start()
成员函数中调用该Deamon
方法即可;
class TcpServer
{
public:
bool Start()
{
Daemon();
// ...
}
private:
uint16_t port_;
NetSocket listensock_;
func_t callback_;
};
重谈OSI七层模型
-
会话层
会话层用于通信管理,负责建立和断开通信连接(数据流动的逻辑通路);
同时管理传输层一下的分层;
该层实际上对应的上文代码中服务端获取新连接的部分,当服务端监听到来自客户端的连接时将会获取该客户端的连接,为该客户端维护连接;
在上述代码中该操作主要是依靠子进程进行的,子进程维护服务端与客户端的连接,当提供完服务后子进程将会把与客户端的连接进行关闭;
该项目的实现方式是以多进程的方式实现,每当服务端监听到一个连接时会为该客户端创建一个子进程,实际上这个子进程可以理解为服务端监听到子进程后由子进程维护双端的连接,该子进程也可以看成是一个会话;
-
表示层
表示层主要为设备固有数据格式和网络标准数据格式的转换;
实际上上述代码中所定制的协议(序列化与反序列化,报头的封装与解包)即对应着该层;
其中协议即表示固有的数据格式;
同样的如果这个项目不是一个网络计算器,而是一种视频播放或者音乐播放,那么在进行网络传输的时候也需要将视频或者音乐进行固有格式的转化使其能够进行网络传输;
-
应用层
应用层是一个针对特定应用的协议;
而在该项目中对应的区域为服务端为客户端提供计算器服务的区域,该区域也是服务端主要功能的区域,包括每个字段的解释以及计算方式;
项目完整代码
-
代码(供参考)
[Gitee - 半介莽夫 / CSDN - Dio夹心小面包]