一、应用层
我们程序员写的一个一个解决实际问题,满足我们日常需求的网络程序,都是在应用层。在应用层中的协议也是最多的。
1.1 再次认识协议
协议是一种约定,是通信双方约定的一种数据结构。在之前写的UDP服务器和TCP服务器中,在读取数据的时候,都是按照“字符串”的方式来发送接收;如果我们想要发送的数据是结构化数据,怎么办?首先,要让通信双方约定好一种协议,创建一种数据结构使得通信双方都认识。
协议就是双方约定好的结构化的数据!!!!!!!
1.2 序列化和反序列化(重要)
在网络通信中,通信双方会根据一种协议进行通信,这种协议一般都是一种数据结构,数据结构在网络中传输是比较困难的,进行传送是不方便的。因此在网络通信中,我们需要将数据进行过抽象成为一条数据,即字符串,进行发送;当接受方接受到这条字符串后,我们可以根据其抽象的过程将其还原为原本的数据结构的信息。
通过上述的图片,第一层中表示的是应用层,应用层中负责的是创建消息的数据结构,消息由几部分构成,分别表示什么含义;在传输层中,通过序列化将消息由多变为一,方便网络发送;在网络通信中,网络只需要把其看成字节流即可。在通过网络传输到接收方时,再次通过接收方的协议栈,将字节流从下层依次传输到上层,在传输层中,通过反序列化将消息由一变多,方便上层处理。
在这个过程中,其实也体现了网络分层的好处,进行解耦合,让网络各层完成各自的任务,不会打扰到其他层次。
jsoncpp序列化和反序列化
序列化函数:
思路:
我们需要先实例化一个工厂类对象,通过一个工厂类对象来生产派生类对象,然后将json类型的数据写入字符串类型中,完成序列化。
代码:
// 实现数据的序列化 bool serialize(Json::Value &val, std::string &body) { std::stringstream ss; // 实例化一个工厂类对象 Json::StreamWriterBuilder swb; // 通过一个工厂类对象来生产派生类对象 std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); // 将json类型写入字符串类型,完成序列化 int ret = sw->write(val, &ss); if(ret != 0) { std::cout << "json error" << std::endl; return false; } body = ss.str(); return true; }
反序列化函数:
思路:与序列化思路相同,也是要通过工厂来产生派生类对象,然后将字符串类型转换为json类型的数据,完成反序列化。
代码:
// 实现json字符串的反序列化 bool unserialize(const std::string &body, Json::Value &val) { // 实例化工厂类 Json::CharReaderBuilder crb; // 生产CharReader对象 std::string errs; std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), &val, &errs); if(ret == false) { std::cout << "Json unserialize error:" << errs << std::endl; return false; } return true; }
1.3 重新理解read,write,recv,send和tcp为什么支持全双工??
在理解完序列化和反序列后,将其与网络层结合在一起。在上述的IO系统调用中,会在传输层中创建出两个缓冲区:接受缓冲区和发送缓冲区。将序列化的消息从应用层传输给传输层,本质就是将序列化的消息拷贝到缓冲区中,由缓冲区重新向下层传输,通过网络传输给接收方的协议栈,重新从数据链路层传输到上层。
tcp是传输控制协议,tcp决定了数据什么时候发送,数据怎么发送,数据发送出错了怎么办?由于在tcp中每一个对象都有一个接受缓冲区和发送缓冲区,因此一个套接字既可以读,也可以写,所以tcp支持全双工,本质就是因为发送方和接收方都有一对发送缓冲区和接受缓冲区。
因为tcp层和ip层都在操作系统中,所以双方的通信是由操作系统完成的。tcp层发送数据的本质就是:将自己的发送缓冲区拷贝到接收方的接受缓冲区。通信的本质就是拷贝!!!
在接受数据的时候,由于tcp协议接受的是字节流,所以,我们还需要解决粘包问题,保证我们接受的是一个完整的请求。
二、封装Socket
在之前的博客中,由于创建套接字的流程是固定的,并且为了使得其具有开闭原则,所以我们将socket单独拿出来进行封装成一个类。
在封装这个类时,我们所使用的是模版方法模式。在这个模式下,我们采用多态的方式将TcpSocket和UdpSocket进行分别的封装。
2.1 基类——Socket
在Socket类中,创建套接字,绑定套接字,监听套接字,接收数据,连接函数是通用的,在TcpSocket和UdpSocket中已经全部包含了,我们只需选择套接字中所需要的函数即可。
class Socket
{
public:
virtual void CreateSocketOrDie() = 0; // 创建套接字
virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字
virtual void ListenSocketOrDie() = 0; // 监听套接字
virtual socket_sptr Accepter(InetAddr *addr) = 0; // 接受数据
virtual bool Connector(InetAddr &addr) = 0; // 连接
public:
// 创建套接字的过程是比较固定的
// 创建监听套接字
void BuildListenSocket(InetAddr &addr)
{
CreateSocketOrDie();
BindSocketOrDie(addr);
ListenSocketOrDie();
}
// 创建客户端套接字
bool BuildClientSocket(InetAddr &addr)
{
CreateSocketOrDie();
return Connector(addr);
}
};
2.2 子类——TcpSocket
class TcpSocket : public Socket // 公有继承
{
public:
TcpSocket(int sockfd) : _sockfd(sockfd) {}
void CreateSocketOrDie() override // 创建套接字
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(FATAL, "sockfd create failed!\n");
exit(SOCKET_ERROR);
}
LOG(DEBUG, "sockfd create success!\n");
}
void BindSocketOrDie(InetAddr &addr) override // 绑定套接字
// 传入的是服务端自己的详细地址
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());
local.sin_port = htons(addr.Port());
int n = ::bind(_sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (n < 0)
{
LOG(FATAL, "bind sockfd failed!\n");
exit(BIND_ERROR);
}
LOG(DEBUG, "Bind sockfd success!\n");
}
void ListenSocketOrDie() override // 监听套接字
{
int n = ::listen(_sockfd, backlog);
if (n < 0)
{
LOG(FATAL, "listen sockfd failed!\n");
exit(LISTEN_ERROR);
}
LOG(DEBUG, "listen sockfd success!\n");
}
socket_sptr Accepter(InetAddr *addr) override // 接受数据 // 为什么将
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(WARNING, "accept error...\n");
return nullptr;
}
*addr = peer;
socket_sptr sock = std::make_shared<TcpSocket>(sockfd);
return sock;
}
bool Connector(InetAddr &addr) override // 连接
// 传入的是服务器的详细地址
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(addr.Port());
server.sin_addr.s_addr = inet_addr(addr.Ip().c_str());
int n = ::connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect error..." << std::endl;
return false;
}
return true;
}
2.3 公共变量
class Socket;
const static int backlog = 16;
using socket_sptr = std::shared_ptr<Socket>;
2.4 陌生函数
2.4.1 listen函数
函数的原型:
函数的功能:
listen
函数的主要功能是将套接字设置为被动模式,以便它可以接收传入的连接请求。调用listen
后,套接字会进入“监听”状态,等待客户端发起连接请求。函数的参数:
sockfd
: 套接字描述符。这个描述符是之前通过socket
函数创建的,并且在调用bind
函数后用来标识套接字。
backlog
: 指定在套接字上允许的最大未完成连接队列的长度。这个队列用于存放尚未被accept
函数接受的连接请求。当连接请求的数量超过这个值时,新连接请求可能会被拒绝或忽略。函数的返回值:
- 成功: 如果成功,返回
0
。- 失败: 如果调用失败,返回
-1
,并设置errno
以指示错误原因。常见的错误代码包括EBADF
(无效的套接字描述符)、ENOTSOCK
(描述符不是一个套接字)、EADDRINUSE
(地址已被使用)等。
2.4.2 accept函数
函数的原型:
函数的功能:
accept
函数的主要功能是从已监听的套接字中接受一个传入的连接请求,创建一个新的套接字用于与客户端进行通信。调用accept
函数后,服务器可以通过返回的新套接字与客户端交换数据。函数的参数:
sockfd
: 已监听的套接字描述符,即之前通过socket
函数创建并通过bind
和listen
函数设置好的套接字。
addr
: 指向sockaddr
结构体的指针,用于存储客户端的地址信息。如果不需要地址信息,可以传入NULL
。
addrlen
: 指向socklen_t
类型的变量的指针,该变量表示addr
参数所指向的地址结构体的大小。accept
函数会更新这个变量以反映实际的地址长度。如果addr
为NULL
,这个参数可以被忽略。函数的返回值:
成功: 返回一个新的套接字描述符,这个描述符用于与客户端进行通信。新套接字是与连接相关的独立套接字,且继承了原套接字的属性。
失败: 返回
-1
,并设置errno
以指示错误原因。常见的错误代码包括EBADF
(无效的套接字描述符)、EINTR
(调用被中断)、ENOTSOCK
(描述符不是一个套接字)、EOPNOTSUPP
(套接字类型不支持)等。
2.4.3 connect函数
函数的原型:
函数的功能:
connect
函数用于在客户端程序中与远程服务器建立连接。当客户端想要与服务器通信时,它使用connect
函数来请求连接。成功调用connect
后,客户端与服务器之间就可以进行数据传输了。函数的参数:
sockfd
: 这是一个套接字文件描述符,它是通过socket
函数创建的。这个套接字用于建立连接。
addr
: 这是一个指向struct sockaddr
结构体的指针,它包含了要连接的远程主机的地址信息。这个结构体的具体类型通常取决于协议族,例如struct sockaddr_in
用于 IPv4。
addrlen
: 这是addr
指向的地址结构的大小,以字节为单位。通常,可以使用sizeof(struct sockaddr_in)
来获得这个大小。函数的返回值:
- 成功: 返回 0 表示连接成功。
- 失败: 返回 -1,并且设置
errno
以指示错误原因。例如,errno
可能会被设置为ECONNREFUSED
表示目标主机拒绝连接,或ETIMEDOUT
表示连接超时等。
三、自定义网络协议
在应用层中,我们解释了序列化和反序列化的操作,接下来,我们需要将请求和响应进行封装。根据我们自定义协议来进行序列化。
下面,我们就以一个有关计算器的请求和响应的数据结构,为了解决粘包问题,我们需要先设计一下传递的报文格式:报头 + 有效载荷。报头中存放的是有效载荷的长度,有效载荷存放的是正文内容。
3.1 简单介绍一下Json
3.1.1 Jsoncpp
在之后,我们有可能在肝出protobuf的博客,所以在这里简单介绍一下Json,并且可以使用Json实现序列化和反序列化的操作函数。
Jsoncpp是一个用于处理JSON数据的C++库。他提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能,Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。
3.1.2 Jsoncpp特性
简单易用:Jsoncpp提供了直观的API,使得处理JSON数据变得简单
高性能:jsoncpp的性能经过优化,能够高效地处理大量的JSON数据
全面支持:支持JSON标准中的所有数据类型,包括对象、数组。字符串。数字、布尔值和null
错误处理:在解析JSON数据时,Jsoncpp提供了详细的错误信息和位置,方便开发者调试。
3.1.3 安装Jsoncpp
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
3.1.4 序列化
序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "小名";
root["age"] = "14";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
3.1.5 反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。
3.2 协议类
在这个类中,我们定义了请求类和响应类,我们需要将请求类和响应类进行序列化和反序列化。在完成了上面Jsoncpp简单的了解,我们现在可以轻松地写出来序列化和反序列化的函数。
3.2.1 请求类
class Request
{
public:
// 我们自定义协议
// 报文 = 包头 + 有效载荷
// LV格式 固定字段长——后续字符串的长度 正文内容\n\t
// 对报文进行分析
// "len\r\n"_x_op_y\r\n"
Request()
{
}
Request(int x, int y, char oper)
: _x(x), _y(y), _oper(oper)
{
}
bool Serialize(std::string *out) // 序列化
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) // 反序列化
{
Json::Value root;
Json::Reader reader;
bool ret = reader.parse(in, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return ret;
}
~Request()
{
}
private:
int _x;
int _y;
char _oper; // "+-*/%"
};
3.2.2 响应类
class Response
{
public:
Response()
{
}
Response(int result, int code)
: _result(result), _code(code)
{
}
bool Serialize(std::string *out) // 序列化
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) // 反序列化
{
Json::Reader reader;
Json::Value root;
bool ret = reader.parse(in, root);
_result = root["result"].asInt();
_code = root["code"].asInt();
return ret;
}
~Response()
{
}
private:
int _result;
int _code;
};
3.2.3 解决粘包问题
为了能够读出一份完整的报文,我们可以精心设计发送报文的格式——LV格式。报文等于报头加有效载荷,报头中存在的是有效载荷的长度,我们可以使用一些字符串作为分割报头和有效载荷。怎么读取出一份完整的报文呢??我们可以通过分割符来获取报头。我们首先在字符串中查找第一个出现的分隔符,这个分割符的前面一定是报头,然后再进行分割出报头,读取出有效载荷的长度,然后计算出一个完整的报文的长度,最后将整个报文截取出来即可。
// 处理粘包问题
std::string Decode(const std::string &inbuffer)
{
// 先找出SEP,然后截取出来len的长度,最后将完整报文截取出来
auto pos = inbuffer.find(SEP);
if (pos == std::string::npos)
return "";
std::string len_str = inbuffer.substr(0, pos);
if (len_str.empty())
return "";
int len = std::stoi(len_str);
int total = len + len_str.size() + SEP.size() * 2;
if (inbuffer.size() < total)
return "";
std::string package = inbuffer.substr(pos + SEP.size(), len);
inbuffer.erase(0, total); // yichu
return package;
}
3.2.4 拼装报文
// 进行拼装报文
std::string Eecode(const std::string &json_str)
{
int json_str_len = json_str.size();
std::string proto_str = std::to_string(json_str_len);
proto_str += SEP;
proto_str += json_str;
proto_str += SEP;
return proto_str;
}