云计算百科
云计算领域专业知识百科平台

从零入门计算机网络系列(七):应用层协议设计实战——基于TCP与JsonCpp实现网络计算器

上两篇我们讲了TCP与UDP套接字的编程基础,也写了四套简单的Demo。从本文开始,我们正式动手设计一个属于自己的“应用层协议”,并在此基础上做一个“网络计算器”。好,废话不多说,直接开始。

目录

一、从需求出发:设计一个网络版计算器

1.1 自主设计应用层协议,需要解决哪些问题

1.2 再谈“协议”:网络通信为什么需要约定

1.3 网络版计算器的协议设计

1.3.1 方案一:设计通信数据的约定格式

1.3.2 方案二:进一步完善协议约定

1.3.3 序列化与反序列化:让结构化数据能够传输

1.3.4 网络版计算器的整体架构

二、开发准备:I/O机制、Socket封装与JsonCpp

2.1 重新理解read、write、recv与send

2.1.1 TCP为什么支持全双工通信

2.1.2 I/O操作的本质:用户态与内核态之间的数据拷贝

2.1.3 从生产者消费者模型理解数据收发过程

2.2 封装Socket:统一网络通信接口

2.2.1 设计模式:模板方法模式

2.2.2 Socket.hpp:封装实现与源码解析

2.3 认识JsonCpp:轻量级JSON处理工具

2.3.1 JsonCpp是什么

2.3.2 JsonCpp的主要特点

2.3.3 安装JsonCpp

CentOS:

2.3.4 JSON序列化:将结构化数据转换为文本

2.3.4.1 使用Json::Value的toStyledString()方法

2.3.4.2 使用Json::StreamWriterBuilder与writeString()(推荐)

2.3.4.3 FastWriter与StyledWriter等旧接口

2.3.5 JSON反序列化:从文本还原结构化数据

2.3.5.1 使用Json::Reader旧接口

2.3.5.2 使用Json::CharReaderBuilder创建解析器

2.3.6 Json::Value补充:常用操作与数据访问(选读)

2.3.6.1 构造与初始化

2.3.6.2 元素访问

2.3.6.3 类型检查

2.3.6.4 赋值与类型转换

2.3.6.5 JSON数组与对象操作

三、深入理解序列化与反序列化:应用层协议的核心

3.1 应用层协议与结构化数据

3.1.1 结构化数据与协议格式设计

3.1.2 为什么不能直接发送二进制结构体

3.2 序列化与反序列化的基本原理

3.2.1 序列化与反序列化的概念

3.2.2 序列化的作用与优势

3.3 应用层通信的标准化建议

四、完整源码与运行测试

4.1 Common.hpp(公共头文件)

4.2 Log.hpp(日志)

4.3 InetAddr.hpp(IP 地址封装)

4.4 Socket.hpp(Socket 封装)

4.5 Protocol.hpp(协议模块:序列化、反序列化、组帧)

4.6 TcpServer.hpp(服务端类)

4.7 TcpServer.cc(服务端主程序)

4.8 TcpClient.cc(客户端主程序)

4.9 Makefile

4.10 运行方式

4.11 协议设计说明


一、从需求出发:设计一个网络版计算器

1.1 自主设计应用层协议,需要解决哪些问题

我们即将设计的这套应用层协议,本质上就是一套结构化字段的编码约定。它得达成两个核心目标:

  • 提供完备的序列化与反序列化机制,确保结构化数据在字节流和内存对象之间无损转换。

  • 解决TCP字节流没有边界所导致的报文读取不完整问题。

1.2 再谈“协议”:网络通信为什么需要约定

协议,说到底就是一种“约定”。

Socket API在读写数据时,都是按“字符串”的方式来发送和接收的。可如果我们要传输一些“结构化的数据”呢?

答案还是那两个字:约定。协议,就是双方事先约定好的结构化数据。

1.3 网络版计算器的协议设计

举个具体的例子:我们要实现一个服务器版的加法器。客户端把要计算的两个加数发过去,服务器算完,再把结果返回给客户端。

1.3.1 方案一:设计通信数据的约定格式

客户端发送一个形如“1+2”的字符串。

这个字符串里有两个操作数,都是整型;两个数字之间有一个字符是运算符,运算符只能是+;数字和运算符之间没有空格。

1.3.2 方案二:进一步完善协议约定

定义结构体来表示我们需要交互的信息。

发送数据时,把这个结构体按照一个规则转换成字符串;接收到数据时,再按照相同的规则把字符串转换回结构体。

这个过程,就叫“序列化”和“反序列化”。

1.3.3 序列化与反序列化:让结构化数据能够传输

无论采用方案一、方案二,还是其他什么方案,只要保证一端发送时构造的数据,在另一端能够被正确解析,就是OK的。这种约定,就是应用层协议。

不过,为了让我们深刻理解协议,这次打算自定义实现一遍协议的过程。

我们采用方案二,同时也要体现协议定制的细节。

  • 引入序列化和反序列化。只不过这次直接采用现成的方案,jsoncpp库。

  • 对socket进行字节流的读取处理。

这里有个关键点:面向字节流的时候,发送报文的次数和读取报文的次数,并不是严格的1:1对应关系。

在TCP中,要读到完整的报文,得由接收方应用层自己确定边界。只读取半个报文,或者一次多读了报文,这类问题我们叫它:数据报黏报问题。

TCP是面向字节流的,所以“什么时候发、发多少、出错了怎么办”,完全由操作系统结合TCP协议自主决定。

1.3.4 网络版计算器的整体架构

二、开发准备:I/O机制、Socket封装与JsonCpp

2.1 重新理解read、write、recv与send
2.1.1 TCP为什么支持全双工通信

在任何一台主机上,一个TCP连接都配着两套缓冲区:一套发送缓冲区,一套接收缓冲区。

正因如此,内核才能在发消息的同时,也收消息。发你的,收我的,两条通道各跑各的,互不干扰。这就是全双工。

也正因为这个设计,一个TCP的sockfd,既能读,也能写,读写都靠它一个。不需要像UDP那样准备两个套接字,一个TCP套接字就把收发两件事全包了。

至于实际数据什么时候发、发多少、出错了怎么办,统统由TCP自己掌控。应用程序只管把数据塞进发送缓冲区,剩下的路由、重传、流量控制,TCP一手包办。

所以它才叫传输控制协议——“控制”两个字,不是白叫的。

2.1.2 I/O操作的本质:用户态与内核态之间的数据拷贝

写套接字通信代码时,write/send和read/recv天天见。但别被名字骗了,这些接口压根不直接跟网络、网卡打交道。它们的核心动作,就一件事:在应用层缓冲区和内核缓冲区之间,搬数据。

应用层缓冲区 (User Space)  <—>  系统调用 (write/read)  <—>  内核缓冲区 (OS Kernel)

write/send的本质:内存拷贝

上层调用write(sockfd, buffer, len),它干的活儿很单纯,把用户态应用层缓冲区(buffer)里的数据,拷贝到操作系统内核对应的TCP发送缓冲区里。

注意,数据并没有直接发到网络上。 write成功返回,只代表数据成功拷贝进了内核发送缓冲区,不代表对端已经收到。至于什么时候发、发多少、超时重传怎么处理,全由内核的 TCP 协议栈自己拿主意。

read/recv的本质:内存拷贝与同步

上层调用read(sockfd, buffer, len),它先瞅一眼TCP接收缓冲区里有没有数据。

有数据,就把内核接收缓冲区里的内容,拷贝到用户态的应用层缓冲区(buffer)里。没数据,调用通常就阻塞在那儿(阻塞模式下),直到网卡收到网络数据包、操作系统把它写进接收缓冲区,才把调用唤醒。

通信即拷贝——计算机世界的本质

两台主机之间的网络通信,说到底,就是把发送方发送缓冲区里的数据,跨过网络,拷贝到对端的接收缓冲区里。

整条数据流转链路是这样的:

应用层→ 内核发送缓冲区→网卡 →网络 →对端网卡 →对端内核接收缓冲区 →对端应用层

每一步的核心动作,都是数据拷贝。网络通信这件看起来无比复杂的事,拆到最底层,不过是一场接力式的数据搬运。

2.1.3 从生产者消费者模型理解数据收发过程

基于内核缓冲区与I/O系统调用的工作机制,用户进程和操作系统内核之间,其实构成了一个典型的生产者消费者模型。

[发送端应用层] —> (生产者: write) —> [发送缓冲区] —> (消费者: TCP协议栈)
|
(网络传输)
|
[接收端应用层] <— (消费者: read) <— [接收缓冲区] <— (生产者: 网卡/OS)

先看发送端。

应用层是生产者:write一调,数据就被放进发送缓冲区。写的人只管往里塞,塞完就走。

TCP协议栈是消费者:它盯着发送缓冲区,什么时候发、发多少,由它自己说了算。缓冲区里的数据被取走、打包、送上网卡,生产出来的“货”才算真正出库。

再看接收端,角色正好反过来。

网卡和操作系统是生产者:网络数据包一到,由网卡接收、操作系统接手,写进接收缓冲区。货到了,先入库。

应用层是消费者:read一调,就从接收缓冲区里把数据取走。取多少、什么时候取,应用层自己决定。

一进一出,一存一取。发送端是“应用层生产、TCP消费”,接收端是“内核生产、应用层消费”。两套生产者消费者模型,被网络这根线串在一起,各自转各自的圈,又彼此咬合。

理解了这层模型,再看write为什么返回了数据却没到对端、read为什么没数据就会阻塞,就都顺理成章了。缓冲区就是那个柜台,生产和消费的节奏对不上,柜台就得承担缓冲的职责,满了要等,空了也要等。

2.2 封装Socket:统一网络通信接口

把套接字的各种接口封装成一个类,接下来的代码写起来会顺手很多。

2.2.1 设计模式:模板方法模式

基类定好一套模板,同时提供一个完整的组合调用接口,具体功能交给子类去实现,这就是模板方法模式。

class Socket
{
public:
virtual void CreateSocket() = 0;
virtual void BindSocket() = 0;
virtual void ListenSocket() = 0;
virtual void ConnectSocket() = 0;
virtual void AcceptSocket() = 0;
virtual void RecvMessage() = 0;
virtual void SendMessage() = 0;
virtual void GetSockFd() = 0;
virtual void Close() = 0;

public:
void TcpClientSocketMethod()
{
CreateSocket();
}
void TcpServerSocketMethod()
{
CreateSocket();
BindSocket();
ListenSocket();
}

private:
int _listensockfd;
};

写模板方法模式的时候,有个小技巧:先把需要的函数统统设成void返回值、void参数的形式,把骨架搭出来,等实现完了,再回头改返回值和参数。

2.2.2 Socket.hpp:封装实现与源码解析

#ifndef __SOCKET__
#define __SOCKET__

#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
using namespace InetAddrModule;

namespace SocketModule
{
const int DEFAULT_BACKLOG = 258;

class Socket
{
public:
virtual void CreateSocket() = 0;
virtual bool BindSocket(InetAddr &Server) = 0;
virtual bool ListenSocket(int backlog = DEFAULT_BACKLOG) = 0;
virtual bool ConnectSocket(InetAddr &peer) = 0;
virtual std::shared_ptr<Socket> AcceptSocket(InetAddr &client) = 0;
virtual int RecvMessage(std::string *Message) = 0;
virtual bool SendMessage(const std::string &Message) = 0;
virtual int GetSockFd() = 0;
virtual void Close() = 0;

public:
void TcpClientSocketMethod()
{
CreateSocket();
}

void TcpServerSocketMethod(InetAddr &Server,
int backlog = DEFAULT_BACKLOG)
{
CreateSocket();
BindSocket(Server);
ListenSocket(backlog);
}

private:
int _listensockfd;
};

class TcpSocket : public Socket
{
public:
TcpSocket(int listensockfd = 0)
: _listensockfd(listensockfd)
{}

~TcpSocket()
{}

void CreateSocket()
{
_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LEVEL::FATAL) << "致命!套接字创建失败。" << CAGE;
exit(SOCKET_ERR);
}
}

bool BindSocket(InetAddr &Server)
{
int n = ::bind(_listensockfd, Server.GetSockaddrPtr(),
Server.GetSockLine());
if (n == -1)
{
LOG(LEVEL::FATAL) << "致命!绑定套接字失败。" << CAGE;
exit(BIND_ERR);
}
return true;
}

bool ListenSocket(int backlog = DEFAULT_BACKLOG)
{
int n = ::listen(_listensockfd, backlog);
if (n == -1)
{
LOG(LEVEL::FATAL) << "致命!监听套接字失败。" << CAGE;
exit(LISTEN_ERR);
}
return true;
}

bool ConnectSocket(InetAddr &peer)
{
int n = ::connect(_listensockfd, peer.GetSockaddrPtr(),
peer.GetSockLine());
if (n == -1)
{
LOG(LEVEL::FATAL) << "客户端连接服务器失败!" << CAGE;
exit(CONNECT__ERR);
}
return true;
}

std::shared_ptr<Socket> AcceptSocket(InetAddr &client)
{
sockaddr_in peer;
socklen_t len = sizeof(peer);

int _sockfd = ::accept(_listensockfd, (sockaddr *)&peer, &len);
if (_sockfd < 0)
{
LOG(LEVEL::FATAL) << "服务器获取连接失败!" << CAGE;
exit(ACCEPT_ERR);
}

std::shared_ptr<Socket> ptr =
std::make_shared<TcpSocket>(_sockfd);
client.SetAddr(peer);
return ptr;
}

int RecvMessage(std::string *Message)
{
char buffer[4096];
int n = ::recv(GetSockFd(), buffer, sizeof(buffer) – 1, 0);
if (n > 0)
{
buffer[n] = '\\0';
*Message = buffer;
}
else
{
Message->clear();
}
return n;
}

bool SendMessage(const std::string &Message)
{
LOG(LEVEL::DEBUG) << "如果能看到这句,说明进入了发送请求串" << CAGE;

int n = ::send(GetSockFd(),
Message.c_str(), Message.size(), 0);
if (n == -1)
{
LOG(LEVEL::DEBUG) << "发送请求串失败" << CAGE;
return false;
}

LOG(LEVEL::DEBUG) << "发送请求串成功" << CAGE;
return true;
}

int GetSockFd() { return _listensockfd; }

void Close() { ::close(_listensockfd); }

private:
int _listensockfd;
};
}

#endif

2.3 认识JsonCpp:轻量级JSON处理工具

序列化和反序列化的工具不少。protobuf是谷歌开源的一套方案,在很多公司里用得相当广泛,此方以后也会专门学习和出博文。XML是老牌方案,如今使用场景相对少了一些。

那为什么这次选jsoncpp?因为它超级轻量,学习成本低到几乎可以忽略。

2.3.1 JsonCpp是什么

Jsoncpp是一个处理JSON数据的C++库。它提供两大能力:把JSON数据序列化成字符串,以及把字符串反序列化成C++数据结构。开源,广泛用于各种需要处理JSON的C++项目。

2.3.2 JsonCpp的主要特点
  • 简单易用:API设计直观,处理JSON数据不绕弯子。

  • 高性能:经过优化,大量JSON数据也能高效处理。

  • 全面支持:JSON标准里的所有数据类型,对象、数组、字符串、数字、布尔值、null,统统支持。

  • 错误处理:解析JSON时,提供详细的错误信息和位置,调试起来有迹可循。

下面详细介绍Jsoncpp中序列化和反序列化的操作。

2.3.3 安装JsonCpp

Ubuntu:

sudo apt-get install libjsoncpp-dev

CentOS:

sudo yum install jsoncpp-devel

2.3.4 JSON序列化:将结构化数据转换为文本

序列化,就是把数据结构或对象转换成一种便于网络传输或文件存储的格式。Jsoncpp 提供了多种序列化方式。

2.3.4.1 使用Json::Value的toStyledString()方法

把Json::Value对象直接转成带格式的JSON字符串:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main() {
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";

std::string s = root.toStyledString();
std::cout << s << std::endl;
return 0;
}

2.3.4.2 使用Json::StreamWriterBuilder与writeString()(推荐)

这种方式提供了更多定制选项,比如缩进、换行符等:

#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";

Json::StreamWriterBuilder builder; // StreamWriter 的工厂
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());

std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
return 0;
}

输出:

{
"name" : "joe",
"sex" : "男"
}

2.3.4.3 FastWriter与StyledWriter等旧接口

FastWriter更快,不额外添加空格和换行;StyledWriter则输出格式化后的字符串。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";

// 使用 FastWriter
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;

// 使用 StyledWriter
Json::StyledWriter styled;
std::string s2 = styled.write(root);
std::cout << s2 << std::endl;

return 0;
}

FastWriter输出:

{"name":"joe","sex":"男"}

StyledWriter输出:

{
"name" : "joe",
"sex" : "男"
}

2.3.5 JSON反序列化:从文本还原结构化数据

反序列化,就是把序列化之后的字符串,重新还原成原来的数据结构或对象。Jsoncpp提供了下面这些方法来做这件事。

2.3.5.1 使用Json::Reader旧接口

这种方式能提供详细的错误信息和出错位置,调试的时候特别顺手。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main() {
// JSON 字符串
std::string json_string = "{\\"name\\":\\"张三\\", \\"age\\":30, \\"city\\":\\"北京\\"}";

// 解析 JSON 字符串
Json::Reader reader;
Json::Value root;

// 从字符串中读取 JSON 数据
bool parsingSuccessful = reader.parse(json_string, root);
if (!parsingSuccessful) {
// 解析失败,输出错误信息
std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessage() << std::endl;
return 1;
}

// 访问 JSON 数据
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();

// 输出结果
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "City: " << city << std::endl;

return 0;
}

输出结果:

Name: 张三
Age: 30
City: 北京

2.3.5.2 使用Json::CharReaderBuilder创建解析器

有时候,你可能需要对解析过程做更精细的控制,那就得直接使用Json::CharReader的派生类。但大多数情况下,用Json::parseFromStream或者Json::Reader的parse方法就足够了,没必要把事情搞复杂。

这里有个理解上的关键点:Json::Value是介于序列化和反序列化结果之间的那个“介质”。 序列化时,数据先装进Json::Value,再转成字符串;反序列化时,字符串先解析进Json::Value,再从里面取值。一进一出,它都是那个中转站。

2.3.6 Json::Value补充:常用操作与数据访问(选读)

Json::Value(Jsoncpp)API 整理

Json::Value是Jsoncpp库里的核心类,JSON数据的表示和操作,全靠它撑着。

2.3.6.1 构造与初始化
  • Json::Value():默认构造,造出一个空的Json::Value对象。
  • Json::Value(ValueType type, bool allocated = false):按给定的ValueType创建对象,比如nullValue、intValue、stringValue这些。
2.3.6.2 元素访问
  • Json::Value& operator[](const char* key):用字符串键访问对象里的元素。键不存在?那就当场新建一个。
  • Json::Value& operator[](const std::string& key):同上,只不过键的类型换成了std::string。
  • Json::Value& operator[](ArrayIndex index):用索引访问数组元素。索引越界?照样新建一个。
  • Json::Value& at(const char* key):用字符串键访问对象元素,键不存在就抛异常,绝不悄悄新建。
  • Json::Value& at(const std::string& key):同上,键的类型是std::string。
2.3.6.3 类型检查
  • bool isNull():是不是null。
  • bool isBool():是不是布尔类型。
  • bool isInt():是不是整数类型。
  • bool isInt64():是不是64位整数类型。
  • bool isUInt():是不是无符号整数类型。
  • bool isUInt64():是不是64位无符号整数类型。
  • bool isIntegral():是不是整数,或者能转成整数的浮点数。
  • bool isDouble():是不是双精度浮点数。
  • bool isNumeric():是不是数字,整数和浮点数都算。
  • bool isString():是不是字符串。
  • bool isArray():是不是数组。
  • bool isObject():是不是对象,也就是键值对的集合。
2.3.6.4 赋值与类型转换
  • Json::Value& operator=(bool value):把布尔值赋给Json::Value对象。
  • Json::Value& operator=(int value):赋整数。
  • Json::Value& operator=(unsigned int value):赋无符号整数。
  • Json::Value& operator=(Int64 value):赋64位整数。
  • Json::Value& operator=(UInt64 value):赋64位无符号整数。
  • Json::Value& operator=(double value):赋双精度浮点数。
  • Json::Value& operator=(const char* value):赋C字符串。
  • Json::Value& operator=(const std::string& value):赋std::string。
  • bool asBool():尽可能转成布尔类型。
  • int asInt():尽可能转成整数类型。
  • Int64 asInt64():尽可能转成64位整数类型。
  • unsigned int asUInt():尽可能转成无符号整数类型。
  • UInt64 asUInt64():尽可能转成64位无符号整数类型。
  • double asDouble():尽可能转成双精度浮点数类型。
  • std::string asString():尽可能转成字符串类型。
2.3.6.5 JSON数组与对象操作
  • size_t size():返回数组或对象里的元素数量。
  • bool empty():判断数组或对象是不是空的。
  • void resize(ArrayIndex newSize):调整数组大小。
  • void clear():清空数组或对象里的所有元素。
  • void append(const Json::Value& value):在数组末尾追加一个新元素。
  • Json::Value& operator[](const char* key, const Json::Value& defaultValue = Json::nullValue):在对象里插入或访问一个元素,键不存在就用默认值。
  • Json::Value& operator[](const std::string& key, const Json::Value& defaultValue = Json::nullValue):同上,键的类型是 std::string。

三、深入理解序列化与反序列化:应用层协议的核心

应用层,是网络五层模型的最顶层。网络通信里,上层应用处理的往往是结构化的业务数据,可底层的传输层(TCP/UDP)只负责搬运无结构的字节流。想让结构化数据在网络里高效、准确地跑起来,就得定一套应用层协议,再引入序列化与反序列化机制。

3.1 应用层协议与结构化数据

所谓协议定制,本质上就是通信双方约定好一套彼此都能识别的结构化数据格式。你按这个格式打包,我按这个格式拆包,中间就不会鸡同鸭讲。

3.1.1 结构化数据与协议格式设计

在C/C++程序里,业务数据通常以结构体(struct)或类(class)的形式存在。比如一个聊天消息结构体:

struct message {
std::string path; // 头像 → 图片路径
std::string nick_name; // 昵称
std::string msg; // 消息内容
};

这个struct message,就是通信双方共同认可的应用层协议。字段怎么排、每个字段什么类型,大家都按这个来,数据才能对上号。

3.1.2 为什么不能直接发送二进制结构体

在某些场景下,比如操作系统内部通信,确实可以通过二进制指针直接传递结构体对象。可一旦跨网络、走Client-Server通信,直接甩一个二进制结构体过去,就非常不推荐了。原因有三:

第一,内存对齐差异。 不同的机器、不同的编译器、不同的平台,结构体的内存对齐规则可能各不相同。同一个结构体,在这台机器上sizeof是24,到那台机器上可能就变成了32;内部字段的偏移量,也未必对得上。你按自己的对齐方式打包发出去,对面按自己的规则拆,拆出来的东西就全乱套了。

第二,跨语言兼容性。 网络通信的两端,未必都用C++。对面可能是Java,可能是Python。不同语言各有各的对象模型和内存布局,你发一个C++的二进制结构体过去,人家压根没法直接解析。语言不通,得靠一种“共同语言”来翻译。

第三,大小端字节序问题。 不同架构的CPU,在内存里保存数据的字节序可能不同。x86 是小端,部分ARM架构可能用大端。同一个整数,一个按小端存,一个按大端读,数值直接翻个底朝天。

跨网络传结构化数据,不能把内存里的二进制原样搬过去。得先把它“翻译”成一种双方都认得的格式,这就是序列化要干的事。

3.2 序列化与反序列化的基本原理

二进制直接传输这条路走不通,网络通信就在应用层补上了序列化与反序列化这套机制。

3.2.1 序列化与反序列化的概念

序列化(Serialization):把结构化数据(比如struct message)转换成一整条带分隔符或特殊编码格式的字节流/长字符串,“多变一”,方便在网络里传输。

反序列化(Deserialization):把接收到的字节流/长字符串解析还原成结构化数据,“一变多”,方便上层业务代码处理。

3.2.2 序列化的作用与优势

[结构化数据 struct] —> (序列化) —> [网络字节流/字符串] —> (网络传输) —> [网络字节流/字符串] —> (反序列化) —> [结构化数据 struct]

保证报文完整性。 序列化的时候,可以通过添加分隔符(空格、\\r\\n或者特定字符),或者打包一个长度头部,确保接收方能够精准拆分边界。TCP的粘包与半包问题,就在这一层被化解。

方便上层业务处理。 反序列化把流数据还原成对象结构,上层逻辑直接读结构体成员就行,底层的解析细节,一概不用操心。

应用层与传输层解耦。 序列化和反序列化,把应用层和传输层有效地隔开了。上层协议要改、要扩展,只要同步更新序列化/反序列化规则,底层的字节流传输机制完全不受影响。各改各的,互不牵连。

3.3 应用层通信的标准化建议

现代网络编程里,除非环境极度特殊且完全受控,比如硬件同构、语言统一,否则在应用层,就该老老实实守住下面这条原则:

  • 强推方案:应用层通信,强烈建议使用标准的序列化与反序列化方案,比如JSON、Protobuf、XML,或者自定义的分隔符协议。

  • 规避方案:除非特定场景,绝对不要直接往网络里写裸结构体的二进制内存块。

一句话:该翻译就翻译,别把内存里的二进制原样甩上网。

四、完整源码与运行测试

下面把基于自定义应用层协议的网络计算器完整代码整理出来,包括公共头文件、地址封装、Socket封装、协议模块(序列化/反序列化/组帧)、服务端、客户端和Makefile。

4.1 Common.hpp(公共头文件)

#pragma once

// 错误码定义
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define CONNECT_ERR 4
#define ACCEPT_ERR 5

4.2 Log.hpp(日志)

#pragma once

#include <iostream>
#include <string>

namespace LogModule
{
const std::string CAGE = "\\n";

enum class LEVEL
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};

inline void Log(LEVEL level, const std::string &msg)
{
std::cout << "[" << static_cast<int>(level) << "]" << msg << CAGE;
}
}

#define LOG(level) LogModule::Log(level, std::string(__FILE__) + ":" + std::to_string(__LINE__) + " ")

4.3 InetAddr.hpp(IP 地址封装)

#pragma once

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <string>
#include <cstring>

namespace InetAddrModule
{
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
_port = ntohs(_addr.sin_port);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}

InetAddr(const std::string &ip, uint16_t port)
: _ip(ip), _port(port)
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
}

void SetAddr(const struct sockaddr_in &addr)
{
_addr = addr;
_port = ntohs(_addr.sin_port);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}

std::string Ip() const { return _ip; }
uint16_t Port() const { return _port; }
struct sockaddr_in *GetSockaddrPtr() { return &_addr; }
socklen_t GetSockLine() const { return sizeof(_addr); }

private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
}

4.4 Socket.hpp(Socket 封装)

#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
using namespace InetAddrModule;

namespace SocketModule
{
const int DEFAULT_BACKLOG = 258;

class Socket
{
public:
virtual void CreateSocket() = 0;
virtual bool BindSocket(InetAddr &Server) = 0;
virtual bool ListenSocket(int backlog = DEFAULT_BACKLOG) = 0;
virtual bool ConnectSocket(InetAddr &peer) = 0;
virtual std::shared_ptr<Socket> AcceptSocket(InetAddr &client) = 0;
virtual int Recv(void *buf, size_t len) = 0;
virtual int Send(const void *buf, size_t len) = 0;
virtual int GetSockFd() = 0;
virtual void Close() = 0;

public:
void TcpClientSocketMethod()
{
CreateSocket();
}

void TcpServerSocketMethod(InetAddr &Server,
int backlog = DEFAULT_BACKLOG)
{
CreateSocket();
BindSocket(Server);
ListenSocket(backlog);
}
};

class TcpSocket : public Socket
{
public:
TcpSocket(int sockfd = -1)
: _sockfd(sockfd)
{}

~TcpSocket() {}

void CreateSocket() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LEVEL::FATAL) << "致命!套接字创建失败。";
exit(SOCKET_ERR);
}
}

bool BindSocket(InetAddr &Server) override
{
int n = ::bind(_sockfd, (struct sockaddr *)Server.GetSockaddrPtr(),
Server.GetSockLine());
if (n == -1)
{
LOG(LEVEL::FATAL) << "致命!绑定套接字失败。";
exit(BIND_ERR);
}
return true;
}

bool ListenSocket(int backlog = DEFAULT_BACKLOG) override
{
int n = ::listen(_sockfd, backlog);
if (n == -1)
{
LOG(LEVEL::FATAL) << "致命!监听套接字失败。";
exit(LISTEN_ERR);
}
return true;
}

bool ConnectSocket(InetAddr &peer) override
{
int n = ::connect(_sockfd, (struct sockaddr *)peer.GetSockaddrPtr(),
peer.GetSockLine());
if (n == -1)
{
LOG(LEVEL::FATAL) << "客户端连接服务器失败!";
exit(CONNECT_ERR);
}
return true;
}

std::shared_ptr<Socket> AcceptSocket(InetAddr &client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(LEVEL::FATAL) << "服务器获取连接失败!";
exit(ACCEPT_ERR);
}
client.SetAddr(peer);
return std::make_shared<TcpSocket>(sockfd);
}

int Recv(void *buf, size_t len) override
{
return ::recv(_sockfd, buf, len, 0);
}

int Send(const void *buf, size_t len) override
{
return ::send(_sockfd, buf, len, 0);
}

int GetSockFd() override { return _sockfd; }

void Close() override
{
if (_sockfd >= 0)
{
::close(_sockfd);
_sockfd = -1;
}
}

private:
int _sockfd;
};
}

4.5 Protocol.hpp(协议模块:序列化、反序列化、组帧)

#pragma once

#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>

namespace ProtocolModule
{
// ———- 请求结构体 ———-
struct Request
{
int x;
int y;
char op; // + – * / %
};

// ———- 响应结构体 ———-
struct Response
{
int result;
int code; // 0 成功,非 0 失败
std::string msg;
};

// ———- 序列化:Request → JSON 字符串 ———-
inline std::string SerializeRequest(const Request &req)
{
Json::Value root;
root["x"] = req.x;
root["y"] = req.y;
root["op"] = std::string(1, req.op);

Json::StreamWriterBuilder builder;
builder["indentation"] = "";
return Json::writeString(builder, root);
}

// ———- 反序列化:JSON 字符串 → Request ———-
inline bool DeserializeRequest(const std::string &json, Request &req)
{
Json::CharReaderBuilder builder;
Json::CharReaderBuilder::strictMode(&builder.settings_);
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());

Json::Value root;
std::string errs;
if (!reader->parse(json.c_str(), json.c_str() + json.size(), &root, &errs))
{
return false;
}

if (!root.isMember("x") || !root.isMember("y") || !root.isMember("op"))
{
return false;
}

req.x = root["x"].asInt();
req.y = root["y"].asInt();
std::string opStr = root["op"].asString();
if (opStr.size() != 1)
{
return false;
}
req.op = opStr[0];
return true;
}

// ———- 序列化:Response → JSON 字符串 ———-
inline std::string SerializeResponse(const Response &resp)
{
Json::Value root;
root["result"] = resp.result;
root["code"] = resp.code;
root["msg"] = resp.msg;

Json::StreamWriterBuilder builder;
builder["indentation"] = "";
return Json::writeString(builder, root);
}

// ———- 反序列化:JSON 字符串 → Response ———-
inline bool DeserializeResponse(const std::string &json, Response &resp)
{
Json::CharReaderBuilder builder;
Json::CharReaderBuilder::strictMode(&builder.settings_);
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());

Json::Value root;
std::string errs;
if (!reader->parse(json.c_str(), json.c_str() + json.size(), &root, &errs))
{
return false;
}

if (!root.isMember("result") || !root.isMember("code") || !root.isMember("msg"))
{
return false;
}

resp.result = root["result"].asInt();
resp.code = root["code"].asInt();
resp.msg = root["msg"].asString();
return true;
}

// ———- 组帧:内容 → "len\\r\\ncontent" ———-
inline std::string Encode(const std::string &content)
{
return std::to_string(content.size()) + "\\r\\n" + content;
}

// ———- 解帧:从缓冲区提取一条完整报文 ———-
// 返回值:> 0 成功解码一条报文;0 需要更多数据;-1 协议错误
inline int Decode(std::string &buffer, std::string &content)
{
// 1. 找长度头部的结束符 \\r\\n
size_t pos = buffer.find("\\r\\n");
if (pos == std::string::npos)
{
// 长度头部还没收全,但也不能无限长
if (buffer.size() > 20)
{
return -1;
}
return 0;
}

// 2. 解析长度
std::string lenStr = buffer.substr(0, pos);
if (lenStr.empty() || lenStr.size() > 10)
{
return -1;
}

int len = 0;
for (char c : lenStr)
{
if (c < '0' || c > '9')
{
return -1;
}
len = len * 10 + (c – '0');
}

// 3. 判断内容是否收全
size_t totalLen = pos + 2 + len;
if (buffer.size() < totalLen)
{
return 0;
}

// 4. 提取内容,并从缓冲区中移除
content = buffer.substr(pos + 2, len);
buffer.erase(0, totalLen);
return 1;
}
}

4.6 TcpServer.hpp(服务端类)

#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

#include "Socket.hpp"
#include "Protocol.hpp"

using namespace SocketModule;
using namespace ProtocolModule;

namespace ServerModule
{
class TcpServer
{
public:
TcpServer(uint16_t port = 8080)
: _port(port)
{
}

void Init()
{
InetAddr server("0.0.0.0", _port);
_listensock = std::make_shared<TcpSocket>();
_listensock->TcpServerSocketMethod(server);
}

void Start()
{
// 忽略 SIGCHLD,子进程退出后由系统自动回收
signal(SIGCHLD, SIG_IGN);

while (true)
{
InetAddr client;
auto conn = _listensock->AcceptSocket(client);
LOG(LEVEL::INFO) << "客户端连接:" << client.Ip() << ":" << client.Port();

// 创建子进程处理该客户端
pid_t id = fork();
if (id == 0)
{
_listensock->Close();
Handler(conn);
conn->Close();
exit(0);
}

// 父进程关闭服务套接字,继续 accept
conn->Close();
}
}

private:
void Handler(std::shared_ptr<Socket> conn)
{
std::string buffer;
char tmp[4096];

while (true)
{
int n = conn->Recv(tmp, sizeof(tmp));
if (n <= 0)
{
break;
}

buffer.append(tmp, n);

// 循环解帧,处理每一条完整报文
std::string content;
while (true)
{
int ret = Decode(buffer, content);
if (ret == 0)
{
break; // 数据还不够,继续收
}
if (ret < 0)
{
LOG(LEVEL::ERROR) << "协议解析错误,关闭连接";
return;
}

// 反序列化请求
Request req;
if (!DeserializeRequest(content, req))
{
LOG(LEVEL::ERROR) << "请求反序列化失败";
Response badResp{0, 3, "bad request"};
std::string frame = Encode(SerializeResponse(badResp));
conn->Send(frame.c_str(), frame.size());
continue;
}

// 计算
Response resp = Calculate(req);
LOG(LEVEL::INFO) << "计算:" << req.x << " " << req.op << " "
<< req.y << " = " << resp.result;

// 序列化并组帧发送
std::string frame = Encode(SerializeResponse(resp));
conn->Send(frame.c_str(), frame.size());
}
}
}

Response Calculate(const Request &req)
{
Response resp;
resp.code = 0;
resp.msg = "success";
resp.result = 0;

switch (req.op)
{
case '+':
resp.result = req.x + req.y;
break;
case '-':
resp.result = req.x – req.y;
break;
case '*':
resp.result = req.x * req.y;
break;
case '/':
if (req.y == 0)
{
resp.code = 1;
resp.msg = "divide by zero";
}
else
{
resp.result = req.x / req.y;
}
break;
case '%':
if (req.y == 0)
{
resp.code = 1;
resp.msg = "modulo by zero";
}
else
{
resp.result = req.x % req.y;
}
break;
default:
resp.code = 2;
resp.msg = "unknown operator";
break;
}

return resp;
}

private:
std::shared_ptr<Socket> _listensock;
uint16_t _port;
};
}

4.7 TcpServer.cc(服务端主程序)

#include "TcpServer.hpp"
#include <memory>

int main(int argc, char *argv[])
{
uint16_t port = 8080;
if (argc == 2)
{
port = static_cast<uint16_t>(std::stoi(argv[1]));
}

std::unique_ptr<ServerModule::TcpServer> server =
std::make_unique<ServerModule::TcpServer>(port);

server->Init();
server->Start();

return 0;
}

4.8 TcpClient.cc(客户端主程序)

#include <iostream>
#include <string>
#include <sstream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>

#include "Socket.hpp"
#include "Protocol.hpp"

using namespace SocketModule;
using namespace ProtocolModule;

int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}

std::string server_ip = argv[1];
uint16_t server_port = static_cast<uint16_t>(std::stoi(argv[2]));

// 1. 创建套接字并连接服务器
std::shared_ptr<Socket> client = std::make_shared<TcpSocket>();
client->TcpClientSocketMethod();

InetAddr server(server_ip, server_port);
client->ConnectSocket(server);
std::cout << "已连接到服务器 " << server_ip << ":" << server_port << std::endl;

// 2. 循环发送请求 + 接收响应
std::string line;
while (true)
{
std::cout << "请输入表达式(格式:x op y,例如 10 + 20,输入 quit 退出):";
std::getline(std::cin, line);

if (line == "quit" || line == "exit")
{
break;
}

// 解析输入
std::istringstream iss(line);
Request req;
if (!(iss >> req.x >> req.op >> req.y))
{
std::cout << "格式错误,请按 \\"x op y\\" 输入" << std::endl;
continue;
}

// 序列化 + 组帧 + 发送
std::string json = SerializeRequest(req);
std::string frame = Encode(json);
client->Send(frame.c_str(), frame.size());

// 接收响应
std::string buffer;
char tmp[4096];
std::string content;
bool gotResponse = false;

while (!gotResponse)
{
int n = client->Recv(tmp, sizeof(tmp));
if (n <= 0)
{
std::cout << "服务器已断开连接" << std::endl;
client->Close();
return 0;
}

buffer.append(tmp, n);

int ret = Decode(buffer, content);
if (ret < 0)
{
std::cout << "协议解析错误" << std::endl;
client->Close();
return 1;
}
if (ret > 0)
{
gotResponse = true;
}
}

// 反序列化响应
Response resp;
if (!DeserializeResponse(content, resp))
{
std::cout << "响应反序列化失败" << std::endl;
continue;
}

if (resp.code == 0)
{
std::cout << "计算结果:" << req.x << " " << req.op << " "
<< req.y << " = " << resp.result << std::endl;
}
else
{
std::cout << "计算失败:" << resp.msg << std::endl;
}
}

client->Close();
return 0;
}

4.9 Makefile

.PHONY: all
all: tcp_server tcp_client

tcp_server: TcpServer.cc TcpServer.hpp Socket.hpp InetAddr.hpp Protocol.hpp Common.hpp Log.hpp
g++ -o $@ $< -std=c++17 -ljsoncpp

tcp_client: TcpClient.cc Socket.hpp InetAddr.hpp Protocol.hpp Common.hpp Log.hpp
g++ -o $@ $< -std=c++17 -ljsoncpp

.PHONY: clean
clean:
rm -f tcp_server tcp_client

4.10 运行方式

编译:

make

先启动服务端(默认端口8080):

./tcp_server

再启动客户端:

./tcp_client 127.0.0.1 8080

4.11 协议设计说明

报文格式:

len\\r\\ncontent

  • len:内容的字节长度(十进制数字字符串)
  • content:JSON字符串

请求JSON:

{"x": 10, "y": 20, "op": "+"}

响应JSON:

{"result": 30, "code": 0, "msg": "success"}

为什么用长度头?

TCP是字节流,没有天然的消息边界。长度头明确告诉接收方“后面还有多少字节”,接收方就能精确地切出每一条完整报文,粘包和半包问题一并解决。

解帧逻辑:

Decode函数先从缓冲区里找\\r\\n,拿到长度;再判断缓冲区里剩余的数据够不够这个长度。够了,就切出一条完整报文;不够,就返回0,等下次recv继续收。一条一条处理,边界清晰。

序列化/反序列化的位置:

Protocol.hpp里的SerializeRequest/DeserializeRequest和SerializeResponse/DeserializeResponse,负责JSON和结构体之间的互转。Encode/Decode负责组帧和解帧。两层职责分开,改序列化方案不影响组帧逻辑,改组帧格式也不影响序列化。


如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连支持。你的每一次反馈,都是我继续硬核输出的最大动力。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 从零入门计算机网络系列(七):应用层协议设计实战——基于TCP与JsonCpp实现网络计算器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!