一、基础知识
1.再谈"协议"
应用层协议就是应用程序之间通信的规则和格式约定,让不同的程序能够理解彼此发送的数据含义。
在之前的TCP网络通信中,发送数据和接收数据其实是有一点问题的
**问题:**在TCP通信中,由于TCP是流式协议,没有消息边界,因此当我们通过TCP发送多个报文时,接收方可能一次读取到多个报文粘在一起,或者一个报文被拆分成多次接收。
简单理解: TCP协议是一个字节一个字节的发送,**无法知道发送或读取的是否是一条完整的信息,**可能信息只发送了一半就被读取了
解决:通过使用自定义应用层协议(一种"约定")来确认是否是一条完整的信息
而UDP通信是面向数据报的,所以不存在这样的问题
2.结构化数据
举例说明:
在聊天软件中,用户发送的消息虽然大部分是字符串,但是经过用户层处理后,还需要增加头像,时间,昵称等信息。
此时网络中发送的就不再只是一个字符串,而是像类变量这样的复杂结构,这就是结构化数据。
cpp
struct message
{
string url;
string time;
string nickname;
string msg;
};
3.序列化和反序列化
在前面的TCP和UDP网络通信中,读写数据的时候都是按照**"字符串"** 的形式发送和接收的,那我们如果不发送字符串,而是要传送一些结构化的数据怎么办呢?
在使用 socket API 进行网络通信时,底层只能收发 字节流(或字符串) ,因此如果要在客户端‑服务器之间传递 结构化的数据 ,必须先把数据 序列化 为字节流 ,再在接收端 反序列化 回原来的结构。
序列化和反序列化的简单理解:(不太准确,便于理解)
- 序列化:结构化数据打包成"字符串"
- 反序列化:"字符串"转为结构化数据
常见序列化方案对比
| 方案 | 数据形态 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| JSON | 文本(UTF‑8) | 人类可读、调试方便、跨语言支持广泛 | 数据体积相对大、解析速度慢于二进制方案 | 配置、REST API、调试阶段 |
| XML | 文本(带标签) | 可自定义结构、支持命名空间 | 冗长、解析开销大 | 老旧系统、需要严格模式校验的场景 |
| Protocol Buffers (protobuf) | 二进制 | 体积小、序列化/反序列化速度快、跨语言、向后兼容 | 不可直接阅读,需要 .proto 定义文件 | 高性能 RPC、移动端‑服务器、带宽受限环境 |
| MessagePack | 二进制 | 类似 JSON 的结构但更紧凑 | 生态相对 protobuf 较少 | 需要兼顾可读性和效率的轻量服务 |
序列化和反序列化的常见的做法:
| 步骤 | 说明 | 常用实现方式 |
|---|---|---|
| 1️⃣ 定义数据结构 | 在双方约定好要交换的字段、类型、层次结构。可以用 结构体、类或 .proto、JSON Schema 等形式描述。 | C/C++ struct、Java class、.proto 文件等 |
| 2️⃣ 序列化 | 把结构体/对象转换为 字节序列(或可读的文本),并在发送前 加上长度前缀(防止粘包/半包)。 | JSON、XML、Protocol Buffers(protobuf)、MessagePack、Thrift、CBOR 等 |
| 3️⃣ 通过 socket 发送 | 直接 write()/send() 发送字节流;接收端使用 read()/recv() 读取指定长度的数据。 | 参考 Protobuf C++ Socket 示例 中的 write(client_sock, &msg, sizeof(msg)) 方式 |
| 4️⃣ 反序列化 | 接收端把字节流按照相同的规则恢复为原始结构体/对象。 | 对应的 JSON 解析库、protobuf 生成的 ParseFromArray 等 |

实际实现要点:
1.字节序(Endian)
- 网络字节序统一为 大端。在序列化前,整数等基本类型应使用 htonl/htons(或 protobuf 自动处理)保证跨平台兼容。
2.长度前缀
- 为防止 粘包/半包,常在发送的数据前加上 4 字节的长度字段(网络字节序),接收端先读取长度,再读取完整报文。
3.错误处理
- 读取不完整或解析失败时,需要关闭连接或请求重传,防止协议状态错位。
4.序列化方案Jsoncpp
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。
安装
bash
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
头文件
使用时需要包含头文件:
cpp
#include <jsoncpp/json/json.h>
编译
编译时需要链接 JsonCpp 库:
g++ -o testjson testjson.cc -ljsoncpp
序列化
1.使用 Json::Value 的 toStyledString 方法:
优点:将 Json::Value 对象直接转换为格式化的JSON字符串。
示例:
cpp
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "张三";
root["sex"] = "男";
std::string s = root.toStyledString();
std::cout << s << std::endl;
return 0;
}
解析:
- 创建 Json::Value 对象(根节点)
- 添加两个键值对:
- "name" → "张三"
- "sex" → "男"
- 使用 toStyledString() 方法,将JSON对象转换为格式化的字符串
- 输出结果到控制台
2.使用 Json::FastWriter :
cpp
int main()
{
Json::Value root;
root["name"] = "Bob";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。
使⽤ Json::Reader :
**优点:**提供详细的错误信息和位置,方便调试。
示例:
cpp
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.getFormattedErrorMessages() << 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;
}
解析:
- 定义一个JSON字符串。
- 创建Json::Reader对象和Json::Value对象。
- 使用Reader的parse方法解析字符串,如果解析失败,打印错误信息并返回。
- 从Json::Value对象中获取数据,注意这里使用了asString和asInt方法来转换类型。
- 打印提取的数据。
5.理解tcp为什么支持全双工

在任何一台主机上,TCP连接既有发送缓冲区,又有接收缓冲区,所以,在内核中,可以在发消息的同时,也可以收消息,即全双工
write和read是否将数据发送到网络中?
write和read操作不直接将数据发送到网络中,而是在主机内部完成数据拷贝:
- write操作:应用层将数据从应用缓冲区拷贝到内核缓冲区(发送缓冲区),再由TCP协议将内核缓冲区的数据通过网络层(如IP)发送到网络中。
- read操作:网络层将接收到的数据存入内核缓冲区(接收缓冲区),read再将内核缓冲区的数据拷贝到应用缓冲区。
因此,write和read的本质是主机内部的数据拷贝,而非直接与网络交互。
主机间通信的本质是什么?
主机间通信的本质是 "数据拷贝":
- 发送方:应用层数据 → 内核缓冲区(write拷贝) → 网络层发送。
- 接收方:网络层接收 → 内核缓冲区 → 应用缓冲区(read拷贝)。
整个过程的核心是数据在"应用缓冲区-内核缓冲区-网络"之间的拷贝,而非"数据直接通过网络传输"(网络传输由TCP/IP协议栈底层完成)。
TCP通信为何是全双工的?
TCP通信是全双工的,原因是 双方各自拥有独立的"发送缓冲区"和"接收缓冲区":
每个TCP连接的两端(如主机A和主机B)都维护一对缓冲区:
- 发送缓冲区:用于暂存待发送的数据(由write拷贝而来)。
- 接收缓冲区:用于暂存接收到的数据(等待read拷贝到应用层)。
由于双方都有独立的发送和接收缓冲区,因此可以同时进行双向数据传输(如主机A发送数据时,主机B可以同时发送数据),从而实现全双工通信。
总结
- write/read是主机内部数据拷贝,不直接涉及网络传输;
- 主机间通信的本质是"数据拷贝";
- TCP全双工的原因是双方拥有独立的发送/接收缓冲区,支持双向同时传输。
二、网络计算器
1.实现功能
实现服务器版的计算器:用户发送要计算式,服务器进行计算,再将结果返回给客户端
2.实现方法总述
我们使用之前写过的tcp通信客户端和服务端的多进程版本实现这个网络计算器。
数据的处理与流转过程大致如下:
- 用户输入计算式(比如:1+1)->构造一个Request变量->序列化为字符串"a op b"->添加报头转化为报文"content_len"\r\n"a op b"\r\n->将报文发给服务端
- 服务端接收报文->去掉报头得到正文"a op b"->反序列化为Request变量->服务端使用计算函数处理Request变量得到Response变量->Response变量序列化为字符串"exitcode result"->添加报头转化为报文"content_len"\r\n"exitcode result"\r\n->将报文发给客户端
- 客户端接收报文->去掉报头得到正文"exitcode result"->反序列化为Response变量->打印Response变量的内容。
举例说明:计算1+1

服务端
cpp
void serviceClient(int client_sockfd, func_t Calculate)
{
std::string inbuffer; // 缓冲区
while (true)
{
// 第一步,读取一个完整的字符串请求,并去掉报头,得到正文
//"content_len"\r\n"a op b"\r\n-->"a op b"
std::string req_text; // 原数据
std::string req_str; // 正文
// 使用recvPackage接收数据并将一个完整的请求放入req_text
if (!recvPackage(client_sockfd, inbuffer, &req_text))
return;
std::cout << "接收到的完整请求字符串(含报头):\n"
<< req_text << std::endl;
if (!deCode(req_text, &req_str)) // 将字符串转化为格式化数据保存在req_text内
return;
std::cout << "去掉报头后的请求正文:\n"
<< req_str << std::endl;
// 第二步,对Request进行反序列化,得到请求结构体
// 字符串"a op b" --> Request结构体
Request req;
if (!req.deserialize(req_str)) // 将正文信息填入req内
return;
// 第三步,利用func函数处理请求结构体,得到响应结构体(进行计算)
// Request结构体 --> Response结构体
Response resp;
Calculate(req, resp); // 将req的处理结果放入resp
// 第四步,对Response进行序列化,得到响应字符串
// Response结构体 --> "exitcode result"
std::string resp_str;
resp.serialize(&resp_str); // 将序列化后的结构题放入resp_str
std::cout << "序列化后的响应字符串:\n"
<< resp_str << std::endl;
// 第五步,将序列化的Response(响应字符串)加上报头并发回客户端
//"exitcode result" --> "content_len"\r\n"exitcode result"\r\n
std::string send_string = enCode(resp_str); // 加报头
std::cout << "加上报头后的响应字符串:\n"
<< send_string << std::endl; // 打印加上报头后的信息
send(client_sockfd, send_string.c_str(), send_string.size(), 0); // 发回
}
close(client_sockfd);
}
客户端
cpp
string message;
std::string inbuffer; // 接收缓冲区
while (true)
{
// 3. 主线程发送数据给服务器
cout << "Please Enter# ";
getline(cin, message);
Request req = ParseLine(message); // 解析用户输入的字符串,构造Request结构体
std::string content;// 存放序列化后的请求字符串
req.serialize(&content);// 将Request结构体序列化为字符串
std::string send_string = enCode(content);// 给请求字符串加上报头
std::cout << "sendstring:\n"
<< send_string << std::endl;
send(_sockfd, send_string.c_str(), send_string.size(), 0); // 发送数据
cout << "Send To Server Success!" << endl;
// 4.接收服务器回发的数据
std::string package, text;
// "content_len"\r\n"exitcode result"\r\n
if (!recvPackage(_sockfd, inbuffer, &package))
continue;
if (!deCode(package, &text))
continue;
// "exitcode result"
Response resp;// 构造响应结构体
resp.deserialize(text);// 反序列化得到响应结构体
std::cout << "exitCode: " << resp._exitcode << std::endl;
std::cout << "result: " << resp._result << std::endl;
}
3.具体实现
3.1使用的结构化数据
我们运算结果可能是运算错误的结果,比如除0错误,那么该结果是错误的,所以一个_result不够,还需要再引入一个变量_exitcode,表示运算结果是正常结果还是异常值
cpp
//客户端请求结构体 client -> server
class Request
{
public:
Request()
: _a(0), _b(0), _op(0)
{
}
Request(int a, int b, char op)
: _a(a), _b(b), _op(op)
{
}
public:
int _a;
int _b;
char _op;// + - * /
};
//服务器响应结构体 server -> client
class Response
{
public:
Response()
: _exitcode(0)
, _result(0)
{
}
Response(int exitcode, int result)
: _exitcode(exitcode)
, _result(result)
{
}
public:
int _exitcode; // 0:计算成功,!0表示计算失败,1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
int _result; // 计算结果
};
3.2序列化和反序列化
简单理解:
- 序列化:结构化数据打包成"字符串"
- 反序列化:"字符串"转为结构化数据
下面,我们分别通过 自定义方案 和 JSON方案 来实现序列化和反序列化,但是在实际应用中不会使用自定义方案 ,这里只是为了深入理解序列化和反序列化的原理
自定义方案
cpp
#define SEP " "
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)
//客户端请求结构体
class Request
{
public:
Request()
:_a(0)
,_b(0)
,_op(0)
{}
Request(int a, int b, char op)
:_a(a)
,_b(b)
,_op(op)
{}
//Request序列化
//Request结构体转化为字符串"a op b"
bool serialize(std::string *out)
{
out->clear();//清空
//将变量转为字符串
std::string a_string = std::to_string(_a);
std::string b_string = std::to_string(_b);
*out = a_string;
*out += SEP;
*out += _op;
*out += SEP;
*out += b_string;
return true;
}
//Request反序列化
//字符串"a op b"转化为Request结构体
bool deserialize(const std::string &in)
{
auto left = in.find(SEP);//查找左侧的SEP
auto right = in.rfind(SEP);//查找右侧的SEP
if (left == std::string::npos || right == std::string::npos)
return false;//找不到,数据有问题
if (left == right)
return false;//只有一个SEP,数据有问题
if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
return false;//指向的不是一个位置,数据有问题
//按左闭右开的方式构造两个数字
std::string a_string = in.substr(0, left);
std::string b_string = in.substr(right + SEP_LEN);
//读取到的数字不能为空
if (a_string.empty() || b_string.empty())
return false;
//填入数据
_a = std::stoi(a_string);
_b = std::stoi(b_string);
_op = in[left + SEP_LEN];
return true;
}
public:
int _a;
int _b;
char _op;
};
//服务器响应结构体
class Response
{
public:
Response()
:_exitcode(0)
,_result(0)
{}
Response(int exitcode, int result)
:_exitcode(exitcode)
,_result(result)
{}
//Response序列化
//Response结构体转化为字符串"exitcode result"
bool serialize(std::string *out)
{
out->clear();//清空
//将变量转为字符串
std::string ec_string = std::to_string(_exitcode);
std::string res_string = std::to_string(_result);
//拼接字符串
*out = ec_string;
*out += SEP;
*out += res_string;
return true;
}
//Response反序列化
//字符串"exitcode result"转化为Response结构体
bool deserialize(const std::string &in)
{
auto mid = in.find(SEP);//查找中间的SEP
if (mid == std::string::npos)
return false;//找不到,出错
//按左闭右开的方式构造两个数字
std::string ec_string = in.substr(0, mid);
std::string res_string = in.substr(mid + SEP_LEN);
//读取到的退出码和计算结果不能为空
if (ec_string.empty() || res_string.empty())
return false;
//填入数据
_exitcode = std::stoi(ec_string);
_result = std::stoi(res_string);
return true;
}
public:
int _exitcode; // 0:计算成功,!0表示计算失败,1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
int _result; // 计算结果
};
JSON方案
cpp
// 客户端请求结构体
class Request
{
public:
Request()
: _a(0), _b(0), _op(0)
{
}
Request(int a, int b, char op)
: _a(a), _b(b), _op(op)
{
}
// 使用jsoncpp进行序列化和反序列化
// Request序列化
bool serialize(std::string *out)
{
out->clear(); // 清空
Json::Value root;
root["a"] = _a;
root["b"] = _b;
root["op"] = _op;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
// Request反序列化
bool deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
reader.parse(in, root);// 解析字符串到root
_a = root["a"].asInt();
_b = root["b"].asInt();
_op = root["op"].asInt();// char类型用asInt()读取
return true;
}
public:
int _a;
int _b;
char _op;
};
// 服务器响应结构体
class Response
{
public:
Response()
: _exitcode(0), _result(0)
{
}
Response(int exitcode, int result)
: _exitcode(exitcode), _result(result)
{
}
// 使用jsoncpp进行序列化和反序列化
// Response序列化
bool serialize(std::string *out)
{
out->clear(); // 清空
Json::Value root;
root["exitcode"] = _exitcode;
root["result"] = _result;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
// Response反序列化
bool deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
reader.parse(in, root);// 解析字符串到root
_exitcode = root["exitcode"].asInt();
_result = root["result"].asInt();
return true;
}
public:
int _exitcode; // 0:计算成功,!0表示计算失败,1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
int _result; // 计算结果
};
3.3自定义协议
现在我们结构化的数据已经具备了序列化和反序列化的能力,但是对于自定义协议来说还是不够,为什么这么说呢?
在TCP通信中,由于TCP是流式协议,没有消息边界,因此当我们通过TCP发送多个JSON报文时,接收方可能一次读取到多个报文粘在一起,或者一个报文被拆分成多次接收。
也就是:
- 粘包:多个 JSON 报文被合并到一个接收缓冲区中
- 拆包:一个 JSON 报文被拆分成多次接收
那有什么解决办法吗?
方法一:使用长度前缀
发送方先计算JSON字符串的长度,然后将长度转换为固定格式(例如4字节的整数)放在JSON字符串的前面,接收方先读取4字节得到长度,再读取指定长度的JSON字符串。
eg."content"->"content_len"\r\n"content"\r\n
方法二:使用分隔符
在每个JSON字符串的末尾加上一个特殊的分隔符,例如换行符'\n'。这样,接收方可以一直读取直到遇到换行符,然后解析一个JSON对象。
eg."content"->\r\n"content"\r\n
如果使用长度前缀的话,你读取报文都不能确定是否完整,那你怎么知道你读取的长度前缀就是完整的呢?
所以我们可以结合长度前缀和分隔符一起使用,只要读到分隔符,我就能确定读到了一个完整的长度前缀,同时为了保证可读性,我们在报文结尾也加上一个分隔符。
即:"content"->"content_len"\r\n"content"\r\n
(content_len代表content的长度)
获取请求报文和获取应答报文
服务端需要获取客户端的请求报文,客户端同样需要获取服务端发送的应答报文,但本质上来说都是获取同种协议传输的"字符串",所以可以共用一个接口
要点:
通过分割符,首先保证读到完整的前缀(正文的长度),再利用前缀和分隔符,保证读到一条完整的信息
从网络中接收到的数据存放到buffer中,每次再将buffer中的数据添加到inbuffer尾部,当inbuffer中有一条完整的报文时,再将数据取出
cpp
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)
// 接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
char buffer[1024]; // 临时缓冲区
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据
if (n > 0)
{
buffer[n] = 0;
inbuffer += buffer; // 将接收到的数据放入inbuffer缓冲区
auto pos = inbuffer.find(LINE_SEP); // 查找第一个\r\n(分割符)
if (pos == std::string::npos)
// 如果没有找到LINE_SEP,就说明报文"content_len"的部分都没有读全
continue; // 接着到最开始读数据直到读全
// 执行至此时,报文"content_len"\r\n这部分一定读全了
// 可以将"content_len"的部分取出来并转化为整形
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
// 我们现在知道了报文中正文部分的长度(content_len),所以就能确定我们读到的是否是一个完整的报文
// 一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;// 计算完整报文的长度
std::cout << "当前#inbuffer:\n"
<< inbuffer << std::endl;
if (inbuffer.size() < total_len)// 如果缓冲区中的数据长度小于完整报文的长度
{
std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
continue; // 报文没读全继续到上面读取
}
// 此时缓冲区中就至少有一个完整的报文
*text = inbuffer.substr(0, total_len); // 取出一个完整的报文放入text
inbuffer.erase(0, total_len); // 删除缓冲区中的完整报文
std::cout << "处理后#inbuffer:\n"
<< inbuffer << std::endl;
break; // 准备处理报文,跳出循环
}
else // 没读到数据
return false;
}
return true;
}
封装报文和解析报文
封装报文
对序列化之后的JSON字符串进行封装,报头为JSON字符串的长度加上分隔符,报尾则是分隔符,这样就能封装一个完整的报文
cpp
#define LINE_SEP "\r\n"
//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enCode(const std::string &text)
{
//按"content_len"\r\n"content"\r\n拼接字符串
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
解析报文
通过分割符找到前缀content_len,再取出正文
cpp
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)
//"content_len"\r\n"content"\r\n 转化为 "content"
// 去掉报头
bool deCode(const std::string &package, std::string *text)
{
auto pos = package.find(LINE_SEP);// 查找第一个\r\n
if (pos == std::string::npos)// 找不到,数据有问题
return false;
std::string text_len_string = package.substr(0, pos);// 取出content_len部分
int text_len = std::stoi(text_len_string);// 转化为整形
*text = package.substr(pos + LINE_SEP_LEN, text_len);// 取出正文部分放入输出参数
return true;
}
3.4处理用户输入的数据
通过操作符来区分左操作数和右操作数
cpp
Request ParseLine(const std::string &line)
{
int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
int i = 0;
int cnt = line.size();
std::string left, right;
char op;
while (i < cnt)
{
switch (status)
{
case 0:
{
if (!isdigit(line[i]))
{
op = line[i];
status = 1;
}
else
left.push_back(line[i++]);
}
break;
case 1:
i++;
status = 2;
break;
case 2:
right.push_back(line[i++]);
break;
}
}
std::cout << "left: " << std::stoi(left) << " right: " << std::stoi(right) << " op: " << op << std::endl;
return Request(std::stoi(left), std::stoi(right), op);
}
3.5应用层封装计算器
因为实现的计算器比较简单,就不多说了
cpp
void calculate(const Request &req, Response &resp)
{
//初始化响应
resp._exitcode = 0;
resp._result = 0;
//根据请求进行计算
switch (req._op)
{
case '+':
resp._result = req._a + req._b;
break;
case '-':
resp._result = req._a - req._b;
break;
case '*':
resp._result = req._a * req._b;
break;
case '/':
if (req._b == 0)
{
resp._exitcode = 4; // 除数为0错误
}
else
{
resp._result = req._a / req._b;
}
break;
default:
resp._exitcode = 1; // 不支持的操作符错误
break;
}
}
4.总体代码
log.hpp
cpp
#pragma once
#include <iostream>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
void logMessage(int level, const std::string &msg)
{
const char *levelStr = nullptr;
switch (level)
{
case DEBUG:
levelStr = "DEBUG";
break;
case NORMAL:
levelStr = "NORMAL";
break;
case WARNING:
levelStr = "WARNING";
break;
case ERROR:
levelStr = "ERROR";
break;
case FATAL:
levelStr = "FATAL";
break;
default:
levelStr = "UNKNOWN";
break;
}
std::cout << "[" << levelStr << "] " << msg << std::endl;
}
自定义方案:protocol.hpp
cpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
#define SEP " "
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)
//"content" 转化为 "content_len"\r\n"content"\r\n
// 加上报头
std::string enCode(const std::string &text)
{
// 按"content_len"\r\n"content"\r\n拼接字符串
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
//"content_len"\r\n"content"\r\n 转化为 "content"
// 去掉报头
bool deCode(const std::string &package, std::string *text)
{
auto pos = package.find(LINE_SEP);// 查找第一个\r\n
if (pos == std::string::npos)// 找不到,数据有问题
return false;
std::string text_len_string = package.substr(0, pos);// 取出content_len部分
int text_len = std::stoi(text_len_string);// 转化为整形
*text = package.substr(pos + LINE_SEP_LEN, text_len);// 取出正文部分放入输出参数
return true;
}
// 客户端请求结构体
class Request
{
public:
Request()
: _a(0), _b(0), _op(0)
{
}
Request(int a, int b, char op)
: _a(a), _b(b), _op(op)
{
}
// Request序列化
// Request结构体转化为字符串"a op b"
bool serialize(std::string *out)
{
out->clear(); // 清空
// 将变量转为字符串
std::string a_string = std::to_string(_a);
std::string b_string = std::to_string(_b);
*out = a_string;
*out += SEP;
*out += _op;
*out += SEP;
*out += b_string;
return true;
}
// Request反序列化
// 字符串"a op b"转化为Request结构体
bool deserialize(const std::string &in)
{
auto left = in.find(SEP); // 查找左侧的SEP
auto right = in.rfind(SEP); // 查找右侧的SEP
if (left == std::string::npos || right == std::string::npos)
return false; // 找不到,数据有问题
if (left == right)
return false; // 只有一个SEP,数据有问题
if ((right - 1) != (left + SEP_LEN)) // 在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
return false; // 指向的不是一个位置,数据有问题
// 按左闭右开的方式构造两个数字
std::string a_string = in.substr(0, left);
std::string b_string = in.substr(right + SEP_LEN);
// 读取到的数字不能为空
if (a_string.empty() || b_string.empty())
return false;
// 填入数据
_a = std::stoi(a_string);
_b = std::stoi(b_string);
_op = in[left + SEP_LEN];
return true;
}
public:
int _a;
int _b;
char _op;
};
// 服务器响应结构体
class Response
{
public:
Response()
: _exitcode(0), _result(0)
{
}
Response(int exitcode, int result)
: _exitcode(exitcode), _result(result)
{
}
// Response序列化
// Response结构体转化为字符串"exitcode result"
bool serialize(std::string *out)
{
out->clear(); // 清空
// 将变量转为字符串
std::string ec_string = std::to_string(_exitcode);
std::string res_string = std::to_string(_result);
// 拼接字符串
*out = ec_string;
*out += SEP;
*out += res_string;
return true;
}
// Response反序列化
// 字符串"exitcode result"转化为Response结构体
bool deserialize(const std::string &in)
{
auto mid = in.find(SEP); // 查找中间的SEP
if (mid == std::string::npos)
return false; // 找不到,出错
// 按左闭右开的方式构造两个数字
std::string ec_string = in.substr(0, mid);
std::string res_string = in.substr(mid + SEP_LEN);
// 读取到的退出码和计算结果不能为空
if (ec_string.empty() || res_string.empty())
return false;
// 填入数据
_exitcode = std::stoi(ec_string);
_result = std::stoi(res_string);
return true;
}
public:
int _exitcode; // 0:计算成功,!0表示计算失败,1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
int _result; // 计算结果
};
// 接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
char buffer[1024]; // 临时缓冲区
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据
if (n > 0)
{
buffer[n] = 0;
inbuffer += buffer; // 将接收到的数据放入inbuffer缓冲区
auto pos = inbuffer.find(LINE_SEP); // 查找第一个\r\n(分割符)
if (pos == std::string::npos)
// 如果没有找到LINE_SEP,就说明报文"content_len"的部分都没有读全
continue; // 接着到最开始读数据直到读全
// 执行至此时,报文"content_len"\r\n这部分一定读全了
// 可以将"content_len"的部分取出来并转化为整形
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
// 我们现在知道了报文中正文部分的长度(content_len),所以就能确定我们读到的是否是一个完整的报文
// 一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;// 计算完整报文的长度
std::cout << "当前#inbuffer:\n"
<< inbuffer << std::endl;
if (inbuffer.size() < total_len)// 如果缓冲区中的数据长度小于完整报文的长度
{
std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
continue; // 报文没读全继续到上面读取
}
// 此时缓冲区中就至少有一个完整的报文
*text = inbuffer.substr(0, total_len); // 取出一个完整的报文放入text
inbuffer.erase(0, total_len); // 删除缓冲区中的完整报文
std::cout << "处理后#inbuffer:\n"
<< inbuffer << std::endl;
break; // 准备处理报文,跳出循环
}
else // 没读到数据
return false;
}
return true;
}
JSON方案:protocol.hpp
cpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
#define SEP " "
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)
//"content" 转化为 "content_len"\r\n"content"\r\n
// 加上报头
std::string enCode(const std::string &text)
{
// 按"content_len"\r\n"content"\r\n拼接字符串
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
//"content_len"\r\n"content"\r\n 转化为 "content"
// 去掉报头
bool deCode(const std::string &package, std::string *text)
{
auto pos = package.find(LINE_SEP);// 查找第一个\r\n
if (pos == std::string::npos)// 找不到,数据有问题
return false;
std::string text_len_string = package.substr(0, pos);// 取出content_len部分
int text_len = std::stoi(text_len_string);// 转化为整形
*text = package.substr(pos + LINE_SEP_LEN, text_len);// 取出正文部分放入输出参数
return true;
}
// 客户端请求结构体
class Request
{
public:
Request()
: _a(0), _b(0), _op(0)
{
}
Request(int a, int b, char op)
: _a(a), _b(b), _op(op)
{
}
// 使用jsoncpp进行序列化和反序列化
// Request序列化
bool serialize(std::string *out)
{
out->clear(); // 清空
Json::Value root;
root["a"] = _a;
root["b"] = _b;
root["op"] = _op;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
// Request反序列化
bool deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
reader.parse(in, root);// 解析字符串到root
_a = root["a"].asInt();
_b = root["b"].asInt();
_op = root["op"].asInt();// char类型用asInt()读取
return true;
}
public:
int _a;
int _b;
char _op;
};
// 服务器响应结构体
class Response
{
public:
Response()
: _exitcode(0), _result(0)
{
}
Response(int exitcode, int result)
: _exitcode(exitcode), _result(result)
{
}
// 使用jsoncpp进行序列化和反序列化
// Response序列化
bool serialize(std::string *out)
{
out->clear(); // 清空
Json::Value root;
root["exitcode"] = _exitcode;
root["result"] = _result;
Json::FastWriter writer;
*out = writer.write(root);
return true;
}
// Response反序列化
bool deserialize(const std::string &in)
{
Json::Value root;
Json::Reader reader;
reader.parse(in, root);// 解析字符串到root
_exitcode = root["exitcode"].asInt();
_result = root["result"].asInt();
return true;
}
public:
int _exitcode; // 0:计算成功,!0表示计算失败,1,2,3,4->不同的运算异常的情况, 这就是一种约定!!!
int _result; // 计算结果
};
// 接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvPackage(int sock, std::string &inbuffer, std::string *text)
{
char buffer[1024]; // 临时缓冲区
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据
if (n > 0)
{
buffer[n] = 0;
inbuffer += buffer; // 将接收到的数据放入inbuffer缓冲区
auto pos = inbuffer.find(LINE_SEP); // 查找第一个\r\n(分割符)
if (pos == std::string::npos)
// 如果没有找到LINE_SEP,就说明报文"content_len"的部分都没有读全
continue; // 接着到最开始读数据直到读全
// 执行至此时,报文"content_len"\r\n这部分一定读全了
// 可以将"content_len"的部分取出来并转化为整形
std::string text_len_string = inbuffer.substr(0, pos);
int text_len = std::stoi(text_len_string);
// 我们现在知道了报文中正文部分的长度(content_len),所以就能确定我们读到的是否是一个完整的报文
// 一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;// 计算完整报文的长度
std::cout << "当前#inbuffer:\n"
<< inbuffer << std::endl;
if (inbuffer.size() < total_len)// 如果缓冲区中的数据长度小于完整报文的长度
{
std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
continue; // 报文没读全继续到上面读取
}
// 此时缓冲区中就至少有一个完整的报文
*text = inbuffer.substr(0, total_len); // 取出一个完整的报文放入text
inbuffer.erase(0, total_len); // 删除缓冲区中的完整报文
std::cout << "处理后#inbuffer:\n"
<< inbuffer << std::endl;
break; // 准备处理报文,跳出循环
}
else // 没读到数据
return false;
}
return true;
}
tcpServer.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <thread>
#include "log.hpp"
#include "protocol.hpp"
using namespace std;
typedef std::function<void(const Request &, Response &)> func_t;
void serviceClient(int client_sockfd, func_t Calculate)
{
std::string inbuffer; // 缓冲区
while (true)
{
// 第一步,读取一个完整的字符串请求,并去掉报头,得到正文
//"content_len"\r\n"a op b"\r\n-->"a op b"
std::string req_text; // 原数据
std::string req_str; // 正文
// 使用recvPackage接收数据并将一个完整的请求放入req_text
if (!recvPackage(client_sockfd, inbuffer, &req_text))
return;
std::cout << "接收到的完整请求字符串(含报头):\n"
<< req_text << std::endl;
if (!deCode(req_text, &req_str)) // 将字符串转化为格式化数据保存在req_text内
return;
std::cout << "去掉报头后的请求正文:\n"
<< req_str << std::endl;
// 第二步,对Request进行反序列化,得到请求结构体
// 字符串"a op b" --> Request结构体
Request req;
if (!req.deserialize(req_str)) // 将正文信息填入req内
return;
// 第三步,利用func函数处理请求结构体,得到响应结构体(进行计算)
// Request结构体 --> Response结构体
Response resp;
Calculate(req, resp); // 将req的处理结果放入resp
// 第四步,对Response进行序列化,得到响应字符串
// Response结构体 --> "exitcode result"
std::string resp_str;
resp.serialize(&resp_str); // 将序列化后的结构题放入resp_str
std::cout << "序列化后的响应字符串:\n"
<< resp_str << std::endl;
// 第五步,将序列化的Response(响应字符串)加上报头并发回客户端
//"exitcode result" --> "content_len"\r\n"exitcode result"\r\n
std::string send_string = enCode(resp_str); // 加报头
std::cout << "加上报头后的响应字符串:\n"
<< send_string << std::endl; // 打印加上报头后的信息
send(client_sockfd, send_string.c_str(), send_string.size(), 0); // 发回
}
close(client_sockfd);
}
enum
{
USAGE_ERROR = 1,
SOCKET_ERROR = 2,
BIND_ERROR = 3,
LISTEN_ERROR = 4,
ACCEPT_ERROR = 5,
READ_ERROR = 6,
WRITE_ERROR = 7
};
class TcpServer
{
private:
uint16_t _port; // 端口号
int _listen_sockfd; // socket描述符(文件描述符)
public:
TcpServer(const uint16_t &port)
: _port(port),
_listen_sockfd(-1)
{
}
void InitServer()
{
// 1. 创建socket
_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4协议 TCP模式 默认值
if (_listen_sockfd == -1)
{
logMessage(FATAL, "socket error");
exit(SOCKET_ERROR);
}
logMessage(NORMAL, "Socket Create Success!");
// 2. 绑定bind
struct sockaddr_in local; // IPv4 网络地址结构体
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET; // 表示使用 IPv4 协议
local.sin_port = htons(_port); // 端口号 htons 主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)
int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)); // 绑定socket与地址
if (n < 0)
{
logMessage(FATAL, "bind error");
exit(BIND_ERROR);
}
logMessage(NORMAL, "Bind Success!");
// 3. 监听listen
n = listen(_listen_sockfd, 5); // 最大连接数5
if (n < 0)
{
logMessage(FATAL, "listen error");
exit(LISTEN_ERROR);
}
logMessage(NORMAL, "Listening On Port " + to_string(_port) + " ...");
}
void Start(func_t Calculate)
{
while (true)
{
// 4. 与客户端连接accept
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int client_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &len); //
if (client_sockfd < 0)
{
logMessage(ERROR, "accept error" + string(strerror(errno)));
continue;
}
logMessage(NORMAL, "Get A New Client!, client_sockfd:" + to_string(client_sockfd));
// 多线程
std::thread t(serviceClient, client_sockfd, Calculate);
t.detach(); // 分离线程
}
}
~TcpServer()
{
if (_listen_sockfd != -1)
{
close(_listen_sockfd);
}
}
};
tcpServer.cpp
cpp
#include <iostream>
#include <memory>
#include "tcpServer.hpp"
#include "protocol.hpp"
using namespace std;
void calculate(const Request &req, Response &resp)
{
//初始化响应
resp._exitcode = 0;
resp._result = 0;
//根据请求进行计算
switch (req._op)
{
case '+':
resp._result = req._a + req._b;
break;
case '-':
resp._result = req._a - req._b;
break;
case '*':
resp._result = req._a * req._b;
break;
case '/':
if (req._b == 0)
{
resp._exitcode = 4; // 除数为0错误
}
else
{
resp._result = req._a / req._b;
}
break;
default:
resp._exitcode = 1; // 不支持的操作符错误
break;
}
}
// 输入 ./tcpServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
cerr << "Usages:" << argv[0] << " port" << endl;
exit(USAGE_ERROR);
}
uint16_t _port = atoi(argv[1]);
unique_ptr<TcpServer> tcpserver(new TcpServer(_port));
tcpserver->InitServer();
tcpserver->Start(calculate);
return 0;
}
tcpClient.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "protocol.hpp"
using namespace std;
class TcpClient
{
private:
int _sockfd; // socket描述符(文件描述符)
string _server_ip;
uint16_t _server_port;
public:
TcpClient(const string &server_ip, const uint16_t &server_port)
: _server_ip(server_ip),
_server_port(server_port),
_sockfd(-1)
{
}
void InitClient()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4协议 TCP模式 默认值
if (_sockfd == -1)
{
cerr << "socket error" << strerror(errno) << endl;
exit(2);
}
cout << "Socket Create Success!" << endl;
}
void Start()
{
// 2. 连接connect
struct sockaddr_in server; // IPv4 网络地址结构体
bzero(&server, sizeof(server)); // 清空结构体
server.sin_family = AF_INET; // 表示使用 IPv4 协议
server.sin_port = htons(_server_port); // 端口号 htons 主机字节序转网络字节序
server.sin_addr.s_addr = inet_addr(_server_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式
int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
cerr << "connect error" << strerror(errno) << endl;
exit(3);
}
cout << "Connect To Server Success!" << endl;
string message;
std::string inbuffer; // 接收缓冲区
while (true)
{
// 3. 主线程发送数据给服务器
cout << "Please Enter# ";
getline(cin, message);
Request req = ParseLine(message); // 解析用户输入的字符串,构造Request结构体
std::string content;// 存放序列化后的请求字符串
req.serialize(&content);// 将Request结构体序列化为字符串
std::string send_string = enCode(content);// 给请求字符串加上报头
std::cout << "sendstring:\n"
<< send_string << std::endl;
send(_sockfd, send_string.c_str(), send_string.size(), 0); // 发送数据
cout << "Send To Server Success!" << endl;
// 4.接收服务器回发的数据
std::string package, text;
// "content_len"\r\n"exitcode result"\r\n
if (!recvPackage(_sockfd, inbuffer, &package))
continue;
if (!deCode(package, &text))
continue;
// "exitcode result"
Response resp;// 构造响应结构体
resp.deserialize(text);// 反序列化得到响应结构体
std::cout << "exitCode: " << resp._exitcode << std::endl;
std::cout << "result: " << resp._result << std::endl;
}
}
Request ParseLine(const std::string &line)
{
// 建议版本的状态机!
//"1+1" "123*456" "12/0"
int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
int i = 0;
int cnt = line.size();
std::string left, right;
char op;
while (i < cnt)
{
switch (status)
{
case 0:
{
if (!isdigit(line[i]))
{
op = line[i];
status = 1;
}
else
left.push_back(line[i++]);
}
break;
case 1:
i++;
status = 2;
break;
case 2:
right.push_back(line[i++]);
break;
}
}
std::cout << "left: " << std::stoi(left) << " right: " << std::stoi(right) << " op: " << op << std::endl;
return Request(std::stoi(left), std::stoi(right), op);
}
~TcpClient()
{
if (_sockfd != -1)
{
close(_sockfd);
}
}
};
tcpClient.cpp
cpp
#include <iostream>
#include <memory>
#include "tcpClient.hpp"
using namespace std;
// 输入 ./tcpClient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
cerr << "Usages:" << argv[0] << " server_ip server_port" << endl;
return 1;
}
string server_ip = argv[1];
uint16_t server_port = stoi(argv[2]);
unique_ptr<TcpClient> tcpclient (new TcpClient(server_ip, server_port));
tcpclient->InitClient();
tcpclient->Start();
return 0;
}
5.运行结果
自定义方案

JSON方案

6.总结
我们的代码分为三层:

所以这三层没有内置在OS中,就是因为这三层由用户决定,用户需要实现什么样的功能------应用层,用户要选择哪种数据格式转换------表示层,用户需要建立通信连接等------会话层。这三层我们已经自己定义实现了。
OSI参考模型中的应用层、表示层、会话层又是TCP/IP分层模型的应用层,所以我们的自定义协议就是应用层协议
后面我们学习的http/https协议也是别人定义好的应用层协议