目录
[1.1 重新理解read、write、send](#1.1 重新理解read、write、send)
[1.2 TCP为什么支持全双工](#1.2 TCP为什么支持全双工)
[1.3 存在的问题](#1.3 存在的问题)
[2.1 了解应用层](#2.1 了解应用层)
[2.2 了解协议](#2.2 了解协议)
[2.3 序列化和反序列化](#2.3 序列化和反序列化)
[3.1 Socket封装](#3.1 Socket封装)
[3.2 定制协议](#3.2 定制协议)
[3.3 流式数据的处理](#3.3 流式数据的处理)
[3.3.1 Jsoncpp](#3.3.1 Jsoncpp)
[3.3.1.3 Json::Value](#3.3.1.3 Json::Value)
[3.3.2 Jsoncpp的使用](#3.3.2 Jsoncpp的使用)
[3.3.2.1 序列化](#3.3.2.1 序列化)
[1.Json::Value的toStyledString 方法](#1.Json::Value的toStyledString 方法)
[3.3.2.2 反序列化](#3.3.2.2 反序列化)
[3.4 规定报文格式](#3.4 规定报文格式)
[3.5 计算业务](#3.5 计算业务)
[3.6 完整代码](#3.6 完整代码)
一、TCP协议数据接收
在前面的TCP和UDP socket编程中,我们使用read来接收数据。但是UDP中使用read不会出现问题,而TCP中接收数据使用到的read都是有问题的,原因是:
TCP是流式协议,内核只维护一份连续字节缓冲区,协议本身不存在天然数据包边界,直接调用一次 read 读取数据会出现两类典型问题:
1.粘包:多条业务数据在内核缓冲区被合并,单次 read 直接读到多条完整业务消息;
2.拆包:单条业务数据长度超过本次读取容量,一次 read 仅读到消息片段,剩余字节仍留存在内核缓冲区。
我们当前客户端、服务端代码直接固定缓冲区 1024 字节调用 read,完全没有做分包边界处理,运行时一定会出现命令错乱、输出截断等异常。 但以上都只是浮于表面的现象,没有触及问题根源。想要彻底看透这类边界问题的本质、并给出可靠解决方案,我们需要先重新吃透read、write、send这三个IO 接口,同时搞懂 TCP 天然支持全双工通信的底层逻辑。
1.1 重新理解read、write、send

前面我们所写的echoserver、dictserver、Command这些都属于应用层的业务逻辑。以TCP通信为例,对每一个文件描述符(一组链接),TCP在底层都会为我们提供一个数据接收缓冲区和发送缓冲区,建立链接的双方(主机A和主机B)都会有这两个缓冲区。
以主机A的视角来看,当要发送数据时,write并不是直接把数据发送到网络空间中进行传输,而是先将用户空间中的数据先拷贝到 发送缓冲区中。发送缓冲区的数据什么时候发、能存多少、出错了怎么办,这些问题都是由传输层TCP自己通过传输控制协议来维护;而这两个缓冲区属于操作系统内核进行维护。
进行数据发送时,操作系统使用TCP逻辑,将主机A的数据从发送缓冲区中发送到主机B的接收缓冲区中。当主机B的接收缓存区中有数据,就将缓冲区中的数据拷贝到用户空间中;如果缓冲区中没有数据,则read阻塞。当写入数据时写得太快,发送缓冲区被写满了,那么write就会阻塞住。
所以read、write本质上就是拷贝函数。而网络间的通信本质上是通信双方(主机A、B)的操作系统在进行网络通信,用户只是通过主机将要发送的数据交给操作系统,然后由操作系统来进行完整的网络通信的服务。
1.2 TCP为什么支持全双工
由前面的分析我们可以得到,通信双方各有一对缓冲区。所以一端在进行信息的发送时既可以发送,同时也可以接收来自另一端发送的信息,这一对缓冲区的工作互相不影响。所以TCP就能支持全双工通信了。而用户往缓冲区里写数据,操作系统从缓冲区中拿数据再发送到网络中去,本质上可以看作一个局部的生产者消费者模型。
所以前面提到的read和write会产生阻塞实际上就是用户和内核这两个角色的直接的同步过程。
那前面所提到的缓冲区在哪里呢?
在上述的操作系统与用户进行数据交换的过程中,就有可能会出现操作系统内部有很多的报文,而有些报文还没来得及放入到接收缓冲区中。于是操作系统就需要对收到的报文进行管理 。要管理就必须先描述,再组织!操作系统中存在着保存和管理这些报文的数据结构------struct sk_buff。
当我们从网络中收到报文时,操作系统会分配sk_buff结构体存储报文数据,记录协议、序列号等元信息,再挂载 到对应TCP连接的内核接收队列。用户进程调用read读取数据时,内核再把sk_buff里的字节拷贝到用户缓冲区,读完后释放对应的sk_buff。
进程PCB(task_struct)中维护文件描述符表,每个文件描述符对应一个struct file;socket 类型的struct file关联struct socket,struct socket再指向内核协议栈的struct sock。

而在sock这个结构体中包含了下面的这两个队列:

当我们收到**报文(sk_buff)**时,操作系统会将报文插入到receive_queue中,发送报文时会将报文插入write_queue中。而这就是接收和发送缓冲区!
1.3 存在的问题
当用户调用write输入数据之后,这些数据有没有发送到网络中,我们并不能确定。前面提到过,发送缓冲区的数据什么时候发、能存多少、出错了怎么办,这些问题都是由TCP传输控制协议来维护。
以远程命令执行服务器为例,假设主机A向服务器发送命令ls -a -l,当用户调用write把数据交给操作系统后,操作系统通过TCP逻辑将信息发送到服务器。但是由于TCP协议是面向字节流的,所以就有可能操作系统先发送了ls,并没有发送完一整条指令。然后服务器接收到这条指令,并执行ls。后续主机A再发送后续的指令-a -l,服务器认为这条指令是错误的指令,无法执行。
换句话来说,你所接收到的字符串数据有可能只是别人发送过来的一部分!站在TCP角度是没有错的。这种问题就是数据的拆包问题!
同理可得,由于TCP协议是面向字节流的,你接收到的字符串数据,也有可能不只是一段,而是别人好几次发送的数据拼在一起堆过来的!这种问题就是数据的粘包问题!
那在前面我们进行TCP socket编程时,为什么没有出现这种情况呢?
其实只是巧合,要么是我们收发的数据量太小,要么受操作系统缓冲区调度影响,这类问题属于偶发问题,不是每次运行都会复现。
二、应用层自定义协议
粘包、拆包这类问题没法靠 TCP 协议本身去解决,站在 TCP 的设计层面,字节流无边界本身就是正常机制,不存在任何错误。于是就需要上层用户自己来保证数据的完整性 !而这种解决方法就是应用层自定义协议!
2.1 了解应用层
在 TCP/IP 四层模型中,应用层位于最顶层,是直接面向用户程序的层级,下层传输层、网络层、网络接口层仅负责数据传输,不会区分业务消息边界,粘包、拆包问题都需要应用层自行处理。我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。
2.2 了解协议
协议是一种 "约定"。socket api 的接口,在读写数据时,都是按 "字符串" 的方式来发送接收的。如果我们要传输一些 "结构化的数据" 怎么办呢?
其实,协议就是双方约定好的结构化的数据。
我们以网络版计算器举例。 我们要实现一个服务器加法器,客户端把参与计算的数据发给服务端,服务端完成运算之后再把结果回传给客户端。
Socket 只能收发字节流,不能直接在网络上传输程序内存中的结构体对象。
方案一:可以直接约定文本格式,客户端发送形如1+2这样的字符串,服务端解析字符串提取出两个整数完成计算。
方案二:我们可以定义结构体,用来存放操作数、运算符这类交互信息。然后再在发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体。
2.3 序列化和反序列化
上述方案二中的过程就是序列化和反序列化。
发送方需要把结构体,按照双方提前约定好的规则转换成字节字符串,这个过程就是序列化 ; 接收方拿到字节数据之后,再依照完全相同的规则,把字节还原回程序中的结构体,这个过程就是反序列化。
序列化与反序列化,就是应用层自定义协议当中,处理结构化数据收发的核心手段 。同时,我们还可以在这套协议里增加消息长度字段,一并解决 TCP 带来的粘包、拆包问题。
下面是一个收发一条聊天信息时序列化和反序列化的例子:

序列化和反序列化的好处,就在于它可以让我们编写出来的代码具有很好的拓展性!通信双方不需要使用完全相同的编程语言,也不需要运行在同一套硬件平台上,只要遵循同一套协议格式,就可以完成网络数据交互。
为了让我们深刻理解协议,我们使用自定义实现一下协议的过程。
1.我们采用方案二,要体现协议定制的细节;
2.引入序列化和反序列化,我们采用现成的方案--jsoncpp库;
3.我们要对socket进行字节流的读取处理;

三、网络版计数器
3.1 Socket封装
无论是TCP还是UDP我们都需要先创建套接字、绑定套接字、监听套接字。所以第一步先要封装创建套接字的函数,这些函数放在Socket类中。但由于TCP和UDP的接口不一样,所以我们把这些函数设计成虚函数,然后再设计TcpSocket类和UdpSocket类继承父类Socket类,分开来实现各自的函数。
由于UDP不需要监听套接字,所以在UDP类中具体实现的时候使用空方法就行了。定义模板方法BuildTcpSocketMethod()和BuildUdpSocketMethod(),固定步骤顺序,然后把步骤的具体实现延迟交给子类来完成。这种模式我们称之为模板方法模式!这种模式比较适合解决完成某项业务的执行流程相对固定、步骤套路化,但流程中每一步的底层实现逻辑存在较大差异。
cpp
namespace NS_SOCKET_MODULE
{
class Socket
{
public:
~Socket()
{
}
public:
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie() = 0;
public:
void BuildTcpSocketMethod()//模板方法
{
CreateSocketOrDie();
BindSocketOrDie();
ListenSocketOrDie();
}
void BuildUdpSocketMethod()
{
CreateSocketOrDie();
BindSocketOrDie();
}
};
};
class TcpSocket:public Socket
{
};
class TcpSocket:public Socket
{
};
}
接下来分别实现子类TCP和UDP中的实现方法(以实现子类TCP为例进行讲解实现过程)。CreateSocketOrDie()、BindSocketOrDie()、ListenSocketOrDie()三个函数的完整实现:
cpp
void CreateSocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
}
void BindSocketOrDie(uint16_t port) override
{
InetAddr addr(port);
if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
}
void ListenSocketOrDie() override
{
if (listen(_sockfd, gbacklog) != 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(LISTEN_ERR);
}
}
逐步完善后的完整代码:
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <memory>
#include "Logger.hpp"
#include "InetAddr.hpp"
#define CONV(addr) ((struct sockaddr*)addr)
namespace NS_SOCKET_MODULE
{
using namespace NS_LOG_MODULE;
static const int gbacklog = 16;
enum
{
OK = 0,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 模版方法模式!
class Socket
{
public:
virtual ~Socket() = default;
protected:
// 模板方法拆分的基础步骤接口,仅子类内部调用,不对外暴露
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie() = 0;
public:
virtual std::shared_ptr<Socket> Accepter(InetAddr &addr) = 0;
virtual int Sockfd() = 0;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string &in) = 0;
virtual void Close() = 0;
virtual bool Connect(InetAddr &addr) = 0;
public:
void BuildTcpSocketMethod(uint16_t port) // 模版方法
{
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie();
}
void BuildTcpClientSockMethod()
{
CreateSocketOrDie();
}
void BuildUdpSocketMethod(uint16_t port)
{
CreateSocketOrDie();
BindSocketOrDie(port);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(-1)
{}
TcpSocket(int sockfd): _sockfd(sockfd)
{}
void CreateSocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
}
void BindSocketOrDie(uint16_t port) override
{
InetAddr addr(port);
if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
}
void ListenSocketOrDie() override
{
if (listen(_sockfd, gbacklog) != 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(LISTEN_ERR);
}
}
std::shared_ptr<Socket> Accepter(InetAddr &clientaddr) override
{
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int sockfd = accept(_sockfd, CONV(&addr), &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
return nullptr;
}
clientaddr = addr;
return std::make_shared<TcpSocket>(sockfd);//基类指针返回子类对象
}
int Sockfd() override
{
return _sockfd;
}
int Recv(std::string *out) override
{
char inbuffer[1024];
ssize_t n = recv(_sockfd, inbuffer, sizeof(inbuffer)-1, 0);
if(n > 0)
{
inbuffer[n] = 0;
*out += inbuffer; // 追加写入的
}
return static_cast<int>(n);
}
int Send(const std::string &in) override
{
return static_cast<int>(send(_sockfd, in.c_str(), in.size(), 0));
}
void Close() override
{
if(_sockfd >= 0)
{
close(_sockfd);
_sockfd = -1;
}
}
bool Connect(InetAddr &addr) override
{
int n = connect(_sockfd, addr.NetAddress(), addr.Len());
return n >= 0;
}
~TcpSocket()
{
Close();
}
private:
int _sockfd;
};
// 完整实现UdpSocket
class UdpSocket: public Socket
{
public:
UdpSocket() : _sockfd(-1)
{}
UdpSocket(int sockfd) : _sockfd(sockfd)
{}
protected:
void CreateSocketOrDie() override
{
// UDP使用SOCK_DGRAM
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create udp socket error";
exit(SOCKET_ERR);
}
}
void BindSocketOrDie(uint16_t port) override
{
InetAddr addr(port);
if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
{
LOG(LogLevel::FATAL) << "bind udp socket error";
exit(BIND_ERR);
}
}
// UDP无listen,空实现,仅满足纯虚函数要求
void ListenSocketOrDie() override
{
LOG(LogLevel::WARNING) << "UDP socket does not support listen, skip";
}
public:
// UDP无accept,返回空指针
std::shared_ptr<Socket> Accepter(InetAddr &addr) override
{
LOG(LogLevel::WARNING) << "UDP socket does not support accept";
return nullptr;
}
int Sockfd() override
{
return _sockfd;
}
// UDP专用recvfrom,读出对端地址到addr
int Recv(std::string *out) override
{
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buf, sizeof(buf)-1, 0, CONV(&peer), &len);
if (n > 0)
{
buf[n] = '\0';
*out += buf;
}
return static_cast<int>(n);
}
// 重载:可指定接收对端地址
int Recv(std::string *out, InetAddr &peerAddr)
{
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buf, sizeof(buf)-1, 0, CONV(&peer), &len);
if (n > 0)
{
buf[n] = '\0';
*out += buf;
peerAddr = peer;
}
return static_cast<int>(n);
}
// Send默认发送给已connect的对端
int Send(const std::string &in) override
{
return static_cast<int>(send(_sockfd, in.c_str(), in.size(), 0));
}
// 重载:指定地址发送数据
int SendTo(const std::string &in, InetAddr &peerAddr)
{
return static_cast<int>(sendto(
_sockfd,
in.c_str(),
in.size(),
0,
peerAddr.NetAddress(),
peerAddr.Len()
));
}
void Close() override
{
if (_sockfd >= 0)
{
close(_sockfd);
_sockfd = -1;
}
}
// UDP connect仅绑定默认通信对端,不建立连接
bool Connect(InetAddr &addr) override
{
int ret = connect(_sockfd, addr.NetAddress(), addr.Len());
if (ret < 0)
{
LOG(LogLevel::WARNING) << "udp connect failed";
return false;
}
return true;
}
~UdpSocket()
{
Close();
}
private:
int _sockfd;
};
} // namespace NS_SOCKET_MODULE
无论要实现哪种服务,都需要有一个部分,专门用于实现网络通信的逻辑:
TcpServer.hpp
cpp
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include <memory>
#include <unistd.h>
#include <signal.h>
#include <functional>
static uint16_t gport = 8080;
using namespace NS_SOCKET_MODULE;
using Handler_t = std::function<std::string(std::string&)>;
class TcpServer
{
public:
TcpServer(Handler_t handler, uint16_t port = gport)
: _port(port),
_listensock(std::make_unique<TcpSocket>()),
_handler(handler)
{
_listensock->BuildTcpSocketMethod(_port); // 使用创建listensockfd的模版方法!
LOG(LogLevel::INFO) << "create listen socket success: " << _listensock->Sockfd();
}
void Loop()
{
signal(SIGCHLD, SIG_IGN);
while (true)
{
InetAddr clientaddr;
auto sockfd = _listensock->Accepter(clientaddr);
if (!sockfd)
continue;
LOG(LogLevel::DEBUG) << "get a new link, socket address: " << clientaddr.ToString() << " sockfd: " << sockfd->Sockfd();
if (fork() == 0)
{
// child
service(sockfd, clientaddr);
sockfd->Close();
exit(0);
}
}
}
~TcpServer()
{
}
private:
void service(std::shared_ptr<Socket> sockfd, InetAddr &clientaddr)
{
std::string inbuffer, outbuffer;
while (true)
{
outbuffer.clear();
int n = sockfd->Recv(&inbuffer);
if(n <= 0)
{
LOG(LogLevel::WARNING) << "recv: client quit, " << clientaddr.ToString();
break;
}
LOG(LogLevel::DEBUG) << "inbuffer:\n" << inbuffer;
if(_handler)
outbuffer += _handler(inbuffer);
if(outbuffer.empty())
continue;
LOG(LogLevel::DEBUG) << "outbuffer:\n" << outbuffer;
n = sockfd->Send(outbuffer);
if(n < 0)
{
LOG(LogLevel::WARNING) << "send: client quit, " << clientaddr.ToString();
break;
}
}
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
Handler_t _handler;
};
3.2 定制协议
在序列化和反序列化时,我们有两种方法,一种是自己手动编写序列化与反序列化的代码,另一种是使用别人封装好的,现成的代码(json、protobuf、xml)。json是可视化的、可读性非常好的序列化方法;而protobuf的可视化效果不好,但效率最高。由于手动编写过程比较繁杂,这里我们使用可读性较好的json来实现序列化与反序列化。
3.3 流式数据的处理
3.3.1 Jsoncpp
3.3.1.1了解Jsoncpp
在使用json进行序列化和反序列化之前,我们先要了解C++中用于处理json数据的库--Jsoncpp。
Jsoncpp提供了将JSON数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。它是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。
Jsoncpp有几个特性:
1.简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
2.高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
3.全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
4.错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。
3.3.1.2Jsoncpp的安装
bash
1 ubuntu: sudo apt-get install -y libjsoncpp-dev
2 Centos: sudo yum install -y jsoncpp-devel
安装Jsoncpp其实是在安装一个库,所以安装成功后其头文件一般会放在include目录下(以ubantu为例):

而json中包含了json的头文件:

而库文件会被放在:

3.3.1.3 Json::Value
无论是在使用jsoncpp进行序列化还是反序列化的过程中,都离不开一种非常重要的一种数据类型--Json::Value,这个数据类型贯穿序列化和反序列化的始终。Json::Value就类似于一种万能对象,在其内部都对任意类型的中括号操作都进行了重载。
3.3.2 Jsoncpp的使用
3.3.2.1 序列化
1.Json::Value的toStyledString 方法
优点:将Json::Value对象直接转换为格式化的JSON字符串。
使用示例:
cpp
#include <iostream>
#include <jsoncpp/json/json.h>
int main()
{
// 构造Json::Value对象
Json::Value root;
root["name"] = "zhangsan";
root["age"] = 20;
root["major"] = "computer";
// toStyledString:格式化JSON字符串(带缩进换行)
std::string str = root.toStyledString();
std::cout << str << std::endl;
return 0;
}
输出结果:
{
"name" : "zhangsan",
"age" : 20,
"major" : "computer"
}
2.Json::StreamWriter
优点:提供了更多的定制选项,如缩进、换行符等。
使用示例:
1.直接返回字符串:
cpp
#include <iostream>
#include <sstream>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "lisi";
root["age"] = 21;
root["hobby"] = "coding";
// 构造输出配置:自定义缩进为4个空格
Json::StreamWriterBuilder builder;
builder["indentation"] = " "; // 设置缩进4空格
// 将Json::Value序列化为字符串
std::string jsonStr = Json::writeString(builder, root);
std::cout << jsonStr << std::endl;
return 0;
}
2.流式写法(输出到cout流、文件中):
cpp
#include <iostream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "lisi";
root["age"] = 21;
root["hobby"] = "coding";
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
// builder.newStreamWriter() 返回 Json::StreamWriter* 裸指针
// unique_ptr 智能指针接管这个指针,出作用域自动 delete
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
//swriter->write(待序列化JsonValue, 输出流指针)
//把内存中的"root"JSON树,序列化文本写入"ss"这个stringstream缓冲区。
std::stringstream ss;
swriter->write(root, &ss);
std::cout << ss.str() << std::endl;
// 离开大括号,writer销毁,自动调用delete,不需要手动释放
return 0;
}
输出结果:
cpp
{
"name" : "lisi",
"age" : 21,
"hobby" : "coding"
}
3.Json::FastWriter
优点:比StyledWriter 更快,因为它不添加额外的空格和换行符。
使用示例:
cpp
#include <iostream>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "wangwu";
root["age"] = 24;
root["score"] = 95;
Json::FastWriter writer;
std::string jsonStr = writer.write(root);
std::cout << jsonStr << std::endl;
return 0;
}
输出结果:
{"name":"wangwu","age":24,"score":95}
JSON 的 value 可以不是简单数字字符串,也可以是另外一个完整 JSON 对象:
cpp
int main()
{
//子对象1:人的基本信息
Json::Value sub_root1;
sub_root1["name"] = "zhangsan";
sub_root1["sex"] = "man";
sub_root1["age"] = 18;
//子对象2:考试分数
Json::Value sub_root2;
sub_root2["math"] = 88;
sub_root2["chinese"] = 98;
//最外层根对象
Json::Value root;
root["who"] = sub_root1; // 把整个子对象赋值给key
root["score"] = sub_root2;
}
得到的json文本:
bash
{
"who" :
{
"name" : "zhangsan",
"sex" : "man",
"age" : 18
},
"score" :
{
"math" : 88,
"chinese" : 98
}
}
3.3.2.2 反序列化
1.Json::Reader
优点:解析出错时,能输出详细错误信息、出错行号、位置,方便调试找 JSON 语法错误。
使用示例:
cpp
#include <jsoncpp/json/json.h>
#include <iostream>
#include <string>
int main()
{
// JSON字符串
std::string json_str = R"({"name":"zhangsan","age":18})";
Json::Reader reader;
Json::Value root;
// 解析字符串到root对象
bool ok = reader.parse(json_str, root);
if(!ok)
{
// 获取详细错误:出错位置、错误描述
std::string err_msg = reader.getFormattedErrorMessages();
std::cout << "解析错误:\n" << err_msg << std::endl;
return -1;
}
std::string name = root["name"].asString();
int age = root["age"].asInt();
// 读取数据
std::cout << name << std::endl;
std::cout << age << std::endl;
return 0;
}
输出结果:
zhangsan
18
2.Json::CharReader
优点:可灵活配置解析策略,解析出错能够提供详细错误信息及出错位置便于调试。
使用示例:
cpp
#include <iostream>
#include <memory>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
std::string json_str = R"({"name":"zhangsan","age":18})";
Json::Value root;
std::string err; //保存错误信息
Json::CharReaderBuilder rbuilder;
std::unique_ptr<Json::CharReader> reader(rbuilder.newCharReader());
// parse参数:字符串起始、字符串末尾指针,输出root,输出错误信息
bool ok = reader->parse(json_str.c_str(),
json_str.c_str() + json_str.size(),
&root, &err);
if(!ok)
{
//新版同样拿到完整错误信息
std::cout << "解析失败:" << err << std::endl;
return -1;
}
std::cout << root["name"].asString() << std::endl;
std::cout << root["age"].asInt() << std::endl;
return 0;
}
3.4 规定报文格式
虽然我们已经实现了序列化和反序列化,但是没法判断收发的报文是不是完整数据,所以我们还要提前约定好统一的报文传输格式。

首先规定发送报文的有效载荷的长度,如果长度对应上再从缓冲区中读取对应长度的内容。为了区分有效载荷的长度和内容,我们设置了特殊分隔符\r\n。
cpp
class Protocol
{
public:
Protocol(HandlerRequest_t handler) : _version("1.0"), _handler_request(handler)
{
}
Protocol(HandlerResponse_t handler_response):_version("1.0"), _handler_response(handler_response)
{
}
std::string Packet(const std::string &json_string)
{
return std::to_string(json_string.size()) + gsep + json_string + gsep;
}
int Unpack(std::string &packet, std::string *json_string)
{
if (packet.empty())
return 0;
if (json_string == nullptr)
return -1;
// 分析报文
auto pos = packet.find(gsep);
if (pos == std::string::npos)
return 0;
std::string lenstr = packet.substr(0, pos);
// lenstr 合法性判断,lenstr -> 123 345
int len = std::stoi(lenstr);
int total = lenstr.size() + len + 2 * gsep.size();
if (packet.size() < total)
return 0;
// 提取报文
*json_string = packet.substr(pos + gsep.size(), len);
packet.erase(0, total);
return 1;
}
// 如果读到半个报文,什么都不做
// 如果读到一个报文+,循环处理,把所有合法的报文都进行统一处理
std::string ParseRequest(std::string &inbuffer)
{
std::string result;
while (true)
{
std::string json_string;
// 1. 解包
int n = Unpack(inbuffer, &json_string);
if (n < 0)
{
LOG(LogLevel::DEBUG) << "no way !!";
return std::string();
}
if (n == 0)
{
LOG(LogLevel::INFO) << inbuffer << " parse done";
return result;
}
LOG(LogLevel::DEBUG) << "json_string:\n" << json_string;
LOG(LogLevel::DEBUG) << "unpack done, inbuffer:\n" << inbuffer;
// 2. 反序列化
// 得到一个完整的报文jsonstring
Request req;
if (!req.Deserialize(json_string))
return std::string();
// 3. 业务计算
Response resp;
if (_handler_request)
resp = _handler_request(req);
// 4. 应答序列化
std::string resp_json_string;
resp.Serialize(&resp_json_string);
// 5. 添加报头
result += Packet(resp_json_string);
}
}
3.5 计算业务
上面完成的Protocal部分只关心对数据的序列化和反序列化,以及报文格式的约定,本身并不关心需要完成什么业务逻辑。所以我们要完成网络版计算器的话还需要添加计算业务的逻辑:
Calculator.hpp
cpp
#pragma once
#include "Protocol.hpp"
#include <iostream>
#include <string>
class Calculator
{
public:
Response Execute(const Request &req)
{
Response resp;
switch (req._oper)
{
case '+':
resp._result = req._data_x + req._data_y;
break;
case '-':
resp._result = req._data_x - req._data_y;
break;
case '*':
resp._result = req._data_x * req._data_y;
break;
case '/':
{
if (req._data_y == 0)
resp._code = 1; // div error
else
resp._result = req._data_x / req._data_y;
}
break;
case '%':
{
if (req._data_y == 0)
resp._code = 2; // mod error
else
resp._result = req._data_x % req._data_y;
}
break;
default:
resp._code = 3; // 非法操作
break;
}
return resp;
}
};
3.6 完整代码
自定义协议部分代码:
Protocol.hpp
cpp
#pragma once
// 自定义协议部分
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <functional>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
// 请求报文
class Request
{
public:
Request() : _data_x(0), _data_y(0), _oper(0)
{
}
Request(int x, int y, char oper) : _data_x(x), _data_y(y), _oper(oper)
{
}
bool Serialize(std::string *out)
{
// 结构化 -> "_data_x _oper _data_y"
Json::Value root;
root["left"] = _data_x;
root["right"] = _data_y;
root["oper"] = _oper;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(std::string &in) // "_data_x _oper _data_y"
{
// "_data_x _oper _data_y" -> 结构化
Json::Value root;
Json::Reader reader;
bool parsesuccess = reader.parse(in, root);
if (!parsesuccess)
return false;
_data_x = root["left"].asInt();
_data_y = root["right"].asInt();
_oper = root["oper"].asInt();//JSON标准不存在独立char类型
//若JSON中oper字段存储数字 43('+'对应的 ASCII 码),root["oper"].asInt()会读取该整数43;
//C++ 中int类型数值可隐式转换为 char类型,因此赋值给char成员_oper后,变量存储字符'+'。
return true;
}
~Request()
{
}
// get set
public:
// 10 20 '-' -> 10 - 20 = ?
// _data_x _oper _data_y
int _data_x;
int _data_y;
char _oper; // '+' '-' '/' '*' '%'
};
// 应答报文
class Response
{
public:
Response() : _result(0), _code(0)
{
}
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::Value root;
Json::Reader reader;
bool parsesuccess = reader.parse(in, root);
if (!parsesuccess)
return false;
_result = root["result"].asInt();
_code = root["code"].asInt();
return true;
}
~Response()
{
}
public:
int _result; // 结果
int _code; // 状态码,用于区分是收到的数据是返回的错误码还是计算后的结果!
};
const std::string gsep = "\r\n";
using HandlerRequest_t = std::function<Response(Request &)>;
using HandlerResponse_t = std::function<void (Response &)>;
class Protocol
{
public:
Protocol(HandlerRequest_t handler) : _version("1.0"), _handler_request(handler)
{
}
Protocol(HandlerResponse_t handler_response):_version("1.0"), _handler_response(handler_response)
{
}
//封包
std::string Packet(const std::string &json_string)
{
return std::to_string(json_string.size()) + gsep + json_string + gsep;
}
//解包
int Unpack(std::string &packet, std::string *json_string)
{
if (packet.empty())
return 0;
if (json_string == nullptr)
return -1;
// 分析报文
auto pos = packet.find(gsep);
if (pos == std::string::npos)
return 0;
std::string lenstr = packet.substr(0, pos);
// lenstr 合法性判断,lenstr -> 123 345
int len = std::stoi(lenstr);
int total = lenstr.size() + len + 2 * gsep.size();
if (packet.size() < total)
return 0;
// 提取报文
*json_string = packet.substr(pos + gsep.size(), len);
packet.erase(0, total);
return 1;
}
// 如果读到半个报文,什么都不做
// 如果读到一个报文+,循环处理,把所有合法的报文都进行统一处理
std::string ParseRequest(std::string &inbuffer)
{
std::string result;
while (true)
{
std::string json_string;
// 1. 解包
int n = Unpack(inbuffer, &json_string);
if (n < 0)
{
LOG(LogLevel::DEBUG) << "no way !!";
return std::string();
}
if (n == 0)
{
LOG(LogLevel::INFO) << inbuffer << " parse done";
return result;
}
LOG(LogLevel::DEBUG) << "json_string:\n" << json_string;
LOG(LogLevel::DEBUG) << "unpack done, inbuffer:\n" << inbuffer;
// 2. 反序列化
// 得到一个完整的报文jsonstring
Request req;
if (!req.Deserialize(json_string))
return std::string();
// 3. 业务计算
Response resp;
if (_handler_request)
resp = _handler_request(req);
// 4. 应答序列化
std::string resp_json_string;
resp.Serialize(&resp_json_string);
// 5. 添加报头
result += Packet(resp_json_string);
}
}
std::string ParseResponse(std::string &inbuffer)
{
while (true)
{
std::string json_string;
// 1. 解包
int n = Unpack(inbuffer, &json_string);
if (n < 0)
{
LOG(LogLevel::DEBUG) << "no way !!";
return std::string();
}
if (n == 0)
{
LOG(LogLevel::INFO) << inbuffer << " parse done";
return std::string();
}
// 2. 反序列化
// 得到一个完整的报文jsonstring
Response resp;
if (!resp.Deserialize(json_string))
return std::string();
// 3. 回调处理
if (_handler_response)
_handler_response(resp);
}
}
~Protocol()
{
}
private:
std::string _version;
HandlerRequest_t _handler_request;
HandlerResponse_t _handler_response;
};
网络版计算器客户端主程序:
cpp
#include <iostream>
#include <string>
#include <memory>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"
using namespace NS_SOCKET_MODULE;
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t" << proc << " server_ip server_port" << std::endl;
}
static void HanlderResponse(Response &resp)
{
std::cout << "result: " << resp._result << "[" << resp._code << "]" << std::endl;
}
// ./netcal_client 目标IP 目标主机端口
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
std::unique_ptr<Socket> socket = std::make_unique<TcpSocket>();
socket->BuildTcpClientSockMethod();
InetAddr serveraddress(server_port, server_ip);
bool n = socket->Connect(serveraddress);
if (!n)
{
std::cerr << "connect error: " << serveraddress.ToString() << std::endl;
exit(2);
}
Protocol procotol(HanlderResponse);
std::string inbuffer;
while (true)
{
int cnt = 3;
std::string outbuffer;
while (cnt--)
{
// 0. 获取数据
int x, y;
char oper;
std::cout << "Enter Your x: ";
std::cin >> x;
std::cout << "Enter Your y: ";
std::cin >> y;
std::cout << "Enter Your oper: ";
std::cin >> oper;
// 1. 定义请求对象
Request req(x, y, oper);
// 2. 序列化
std::string req_json;
req.Serialize(&req_json);
// 3. 封装报头
std::string send_req_string = procotol.Packet(req_json);
outbuffer += send_req_string;
}
std::cout << "\n" << outbuffer << std::endl;
// 4. 发送
socket->Send(outbuffer);
// 5. 接收
socket->Recv(&inbuffer);
// 6. 解析
procotol.ParseResponse(inbuffer);
}
socket->Close();
// sock
return 0;
}
网络版计算器服务器端主程序:
cpp
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Calculator.hpp"
#include <memory>
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t" << proc << " port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = std::stoi(argv[1]);
// 1. 定义计算机
std::unique_ptr<Calculator> cal = std::make_unique<Calculator>();
// 2. 定义协议对象
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>(
[&cal](Request &req)->Response{
return cal->Execute(req);
}
);
// 3. 定义网络对象
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
[&protocol](std::string &inbuffer)->std::string{
return protocol->ParseRequest(inbuffer);
}, port
);
// 4. 启动
tsvr->Loop();
return 0;
}
四、与OSI七层模型的对应关系
OSI七层协议和TCP/IP 五层模型相比,多了会话层和表示层,把应用层的部分功能拆了出来。
| OSI 参考模型 | 各层的解释 |
|---|---|
| 应用层 | 为应用程序提供服务 |
| 表示层 | 数据格式转化、数据加密 |
| 会话层 | 建立、管理和维护会话 |
| 传输层 | 建立、管理和维护端到端的连接 |
| 网络层 | IP 选址及路由选择 |
| 数据链路层 | 提供介质访问和链路管理 |
| 物理层 | 比特流物理传输 |
而上面实现网络版计数器中的Calculator.hpp、Protocol.hpp和TcpServer.hpp同属于TCP/IP五层协议中的应用层。其中,Calculator对应的是OSI七层模型中的应用层,Protocol对应表示层,TcpServer对应会话层。
