不知道记在哪里所以先写在这里:
TCP是全双工的,因为他的fd维护的结构体里有两个缓冲区,一个接受缓冲区,一个发送缓冲区,二者互不干扰,所以可以全双工。
为什么需要序列化反序列化
网络只能传输 "二进制数据流"(0 和 1 的组合),但我们程序中用的是 "结构化数据"(比如字符串、键值对、对象)------ 序列化就是把 "结构化数据" 翻译成 "二进制流"(能在网络上跑),反序列化就是把 "二进制流" 翻译回 "结构化数据"(程序能看懂)。
假如没有序列化与反序列化,我们如何传递结构化数据呢,例如
#include<bits/stdc++.h>
using namespace std;
struct student {
int age;
string name;
bool sex;
};
int main() {
student st{ 18,"Jon",0 };
return 0;
}
难道直接发送18"Jon"0,那边的主机接收后怎么把接受来的数据识别出各个属性分别对应哪一部分呢?所以我们要加标识如:
{
"age":18,
"name" : "Jon",
"sex" : "0"
}
再把它作为一个字符串传过去,那么对面主机就可以识别age属性的值是18,那么属性的值是"Jon",sex属性的值是0了。
使用nlohmann-json序列化反序列化
在ubuntu的安装指令
sudo apt install nlohmann-json3-dev
使用时包含头文件<nlohmann/json.hpp>
场景 1:结构体转 JSON
核心逻辑:直接将结构体对象赋值给 json 对象,宏会自动完成成员映射。
// 结构体转JSON
User user = {"张三", 25, "zhangsan@test.com", true};
json json_user = user; // 一键转换
// 打印JSON对象(原始格式,无缩进)
std::cout << "结构体转JSON对象:" << json_user << std::endl;
/* 输出:
{"age":25,"email":"zhangsan@test.com","is_vip":true,"name":"张三"}
*/
场景 2:JSON 转 string
核心方法:json::dump(int indent = -1),indent 为缩进空格数(-1 表示无缩进,0 表示换行,4 表示 4 空格缩进)。
// JSON转string(无缩进)
std::string json_str = json_user.dump();
std::cout << "\nJSON转string(无缩进):" << json_str << std::endl;
// JSON转string(带4空格缩进,可读性强)
std::string json_str_pretty = json_user.dump(4);
std::cout << "\nJSON转string(带缩进):\n" << json_str_pretty << std::endl;
/* 带缩进输出:
{
"age": 25,
"email": "zhangsan@test.com",
"is_vip": true,
"name": "张三"
}
*/
场景 3:string 转 JSON
核心方法:json::parse(const std::string& str),需处理解析失败的异常(比如字符串格式错误)。
// 模拟JSON字符串(注意转义符,实际代码中可直接写)
std::string raw_str = R"({"age":25,"email":"zhangsan@test.com","is_vip":true,"name":"张三"})";
try {
// string转JSON对象
json parsed_json = json::parse(raw_str);
std::cout << "\nstring转JSON对象:" << parsed_json << std::endl;
} catch (const json::parse_error& e) {
// 解析失败(比如JSON格式错误),捕获异常
std::cerr << "JSON解析失败:" << e.what() << std::endl;
}
⚠️ 新手避坑:用原始字符串字面量 R"(...)" 可以避免 JSON 中的双引号需要转义(比如不用写 \"),推荐使用。
场景 4:JSON 转结构体
核心逻辑:直接将 json 对象赋值给结构体对象,宏会自动完成映射。
// JSON转结构体
User parsed_user;
parsed_user = parsed_json; // 一键转换
// 打印结构体成员,验证转换结果
std::cout << "\nJSON转结构体:" << std::endl;
std::cout << "姓名:" << parsed_user.name << std::endl;
std::cout << "年龄:" << parsed_user.age << std::endl;
std::cout << "邮箱:" << parsed_user.email << std::endl;
std::cout << "是否VIP:" << (parsed_user.is_vip ? "是" : "否") << std::endl;
/* 输出:
JSON转结构体:
姓名:张三
年龄:25
邮箱:zhangsan@test.com
是否VIP:是
*/
场景 5:判断 JSON 空值 / 空对象(核心避坑点)
新手常混淆「空 JSON 对象」「键不存在」「值为 null」,下面分场景讲解:
5.1 判断 JSON 对象是否为空(无任何键值对)
json empty_json; // 空JSON对象
if (empty_json.empty()) {
std::cout << "\nempty_json 是空JSON对象" << std::endl;
} else {
std::cout << "\nempty_json 非空" << std::endl;
}
// 输出:empty_json 是空JSON对象
5.2 判断 JSON 中某个键是否存在
// 检查 "email" 键是否存在
if (json_user.contains("email")) {
std::cout << "\njson_user 包含 email 键" << std::endl;
} else {
std::cout << "\njson_user 不包含 email 键" << std::endl;
}
// 检查 "phone" 键是否存在(不存在)
if (!json_user.contains("phone")) {
std::cout << "json_user 不包含 phone 键" << std::endl;
}
/* 输出:
json_user 包含 email 键
json_user 不包含 phone 键
*/
5.3 判断 JSON 中某个键的值是否为 null(空值)
// 模拟一个值为 null 的 JSON
json user_with_null;
user_with_null["name"] = "李四";
user_with_null["age"] = 30;
user_with_null["email"] = nullptr; // 邮箱设为 null
user_with_null["is_vip"] = false;
// 方式1:用 is_null() 检查
if (user_with_null["email"].is_null()) {
std::cout << "\nuser_with_null 的 email 值为 null" << std::endl;
}
// 方式2:先判断键存在,再检查 null(避免键不存在时访问崩溃)
if (user_with_null.contains("phone") && user_with_null["phone"].is_null()) {
std::cout << "phone 值为 null" << std::endl;
} else {
std::cout << "phone 键不存在或值非 null" << std::endl;
}
/* 输出:
user_with_null 的 email 值为 null
phone 键不存在或值非 null
*/
场景6 判断是不是某种类型
|--------------|-----------------------|-----------------------------|
| 整数(int/long) | is_number_integer() | 判断是否为整型数字(如 25、-10、0) |
| 浮点数 | is_number_float() | 判断是否为浮点型数字(如 98.5、3.14) |
| 所有数字(整 + 浮) | is_number() | 判断是否为任意数字类型(包含整数和浮点数) |
| 字符串 | is_string() | 判断是否为字符串类型(如 "张三"、"123") |
| 布尔值 | is_boolean() | 判断是否为 true/false |
| null 值 | is_null() | 判断是否为 null |
| JSON 对象 | is_object() | 判断是否为 JSON 对象({...}) |
| JSON 数组 | is_array() | 判断是否为 JSON 数组([...]) |
场景7 JSON 值到目标类型的安全转换
get<> 不是 "多个命名不同的函数",而是一个通用模板函数 ,通过修改 <> 里的模板参数(目标类型) ,实现不同类型值的提取,库会自动完成 JSON 值到目标类型的安全转换(类型不匹配则抛 type_error)。
| 模板参数(目标类型) | 适用 JSON 值类型 | 配套判断函数 | 核心说明 |
|---|---|---|---|
get<std::string>() |
JSON 字符串类型 | is_string() |
提取字符串(如 "张三") |
get<int>() |
JSON 整数类型 | is_number_integer() |
提取 32 位整数(如 25、-10) |
get<long>() |
JSON 整数类型 | is_number_integer() |
提取长整数(避免数值溢出) |
get<long long>() |
JSON 整数类型 | is_number_integer() |
提取 64 位整数(如大数值) |
get<float>() |
JSON 浮点类型 | is_number_float() |
提取单精度浮点数(如 98.5) |
get<double>() |
JSON 浮点类型 | is_number_float() |
提取双精度浮点数(推荐) |
get<bool>() |
JSON 布尔类型 | is_boolean() |
提取 true/false |
json j = {
{"name", "张三"}, // 字符串
{"age", 25}, // 整数
{"id", 123456789012345LL}, // 长整数
{"score", 98.5}, // 浮点数
{"is_vip", true} // 布尔值
};
// 1. 提取字符串
if (j["name"].is_string()) {
std::string name = j["name"].get<std::string>();
std::cout << "string: " << name << std::endl;
}
协议头文件
#pragma once
#include <nlohmann/json.hpp>
using json = nlohmann::json;
#include <string>
using namespace std;
struct Request{
int a;
int b;
char op;//'+', '-', '*', '/'
string serialize()const{
json j;
j["a"]=a;
j["b"]=b;
j["op"]=op;
return j.dump();
}
static Request deserialize(const string& str){
json j=json::parse(str);
Request req;
req.a=j["a"];
req.b=j["b"];
req.op = static_cast<char>(j["op"].get<int>());
return req;
}
};
struct Response{
int result;
int status; // 0: 成功, 1: 除零错误, 2: 未知操作符
string msg;
string serialize()const{
json j;
j["result"]=result;
j["status"]=status;
j["msg"]=msg;
return j.dump();
}
static Response deserialize(const string& str){
json j=json::parse(str);
Response res;
res.result=j["result"];
res.status=j["status"];
res.msg=j["msg"];
return res;
}
};
注意
JSON 没有"字符"类型,库也未给
char写特化,因此get<char>()/j = 'a'都会编译失败或语义错乱;
为什么不能隐式转换
-
歧义:
'a'既是整数 97 也是字符"a",库无法决定。 -
无特化:nlohmann/json 只提供了
std::string/ 整型 / 浮点等特化,没有from_json(json&, char&)。
正确使用char类型
char op='a';
json j;
j["op"]=op;//这三行没bug
op=j["op"];//错误,不支持隐式转换
op=j["op"].get<char>()//错误,没有get<char>的特化
op = static_cast<char>(j["op"].get<int>());//正确
客户端
// Tcp_Socket客户端
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include<csignal>
#include"protocol.hpp"
#include <iostream>
using namespace std;
//服务端
int main() {
// ip 127.0.0.1
//
signal(SIGPIPE,SIG_IGN);
int fd = socket(AF_INET, SOCK_STREAM, 0); // ip4 ,tcp
if (fd < 0) {
cout << "socket faild" << endl;
}
//让操作系统自动bind端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8082);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
int i = 0;
int ret=connect(fd,(sockaddr*)&addr,sizeof addr);
if(ret<0){
cout<<"connect faild"<<endl;
}
while (true) {
//输入计算内容
struct Request req;
std::cout << "请输入计算内容:\n";
std::cout << "第一个数: ";
while (!(std::cin >> req.a)) { // 失败就清标志重读
std::cin.clear(); // 清除 fail 位
std::cin.ignore(10000, '\n'); // 丢掉错误行
std::cout << "请输入**数字**!\n";
}
std::cout << "第二个数: ";
while (!(std::cin >> req.b)) {
std::cin.clear();
std::cin.ignore(10000, '\n');
std::cout << "请输入**数字**!\n";
}
std::cout << "操作符: ";
while (!(std::cin >> req.op)) {
std::cin.clear();
std::cin.ignore(10000, '\n');
std::cout << "请输入合法操作符!\n";
}
string str=req.serialize();
ret=write(fd,str.c_str(),str.size());
if(ret<0){
cout<<"write faild"<<endl;
cout << strerror(errno) << endl;
if(errno==EINTR){
continue;
}
if(errno==EAGAIN){
continue;
}
close(fd);
return 0;
}
char buff[1024]={0};
size_t n = read(fd, buff, sizeof buff);
if(n==0){
cout<<"server close"<<endl;
break;
}
else if(n<0){
cout<<"read faild"<<endl;
cout << strerror(errno) << endl;
if(errno==EINTR){
continue;
}
if(errno==EAGAIN){
continue;
}
close(fd);
return 0;
}
buff[n] = '\0';
string s(buff);
struct Response res=Response::deserialize(s);
cout << "server say:" << res.result << endl;
if(res.status!=0){
cout << "server say:" << res.msg << endl;
}
}
return 0;
}
关于cin为什么写成循环
客户端用 cin >> req.a / req.b 读 整数,一旦用户手抖输入字母(如 a),cin 进入 fail 状态,后续所有 >> 都不执行,变量保持旧值(通常是 0),然后开始下次循环读取值,此时cin状态还是fail,还是读取失败,于是接着循环,即cin不会再阻塞,而会被跳过,用户就无法输入信息了。 → 服务器 不断接收到错误值, 不停打印信息。
服务端
// Tcp_Socket
// socket编程
//多线程
#include "protocol.hpp"
#include <arpa/inet.h>
#include <cerrno> // errno 也需要
#include <csignal> // 增加信号处理
#include <cstring> // C++ 推荐用 cstring
#include <iostream>
#include <sys/socket.h>
#include <thread>
#include <unistd.h>
using namespace std;
//服务端
int listen_fd;
void handle_client(int acfd) {
while (true) {
char buff[1024];
int ret = read(acfd, buff, (sizeof buff)-1);
if (ret == -1) {
cout << "read faild" << endl;
cout << strerror(errno) << endl;
close(acfd);
return;
}
else if(ret==0){
cout<<"client closed"<<endl;
close(acfd);
return;
}
buff[ret] = '\0';
string s(buff);
struct Request req = Request::deserialize(s);
cout << "client say:" << req.a << req.op << req.b << endl;
//计算
ret = 0;
int flag = 0;
switch (req.op) {
case '+':
ret = req.a + req.b;
break;
case '-':
ret = req.a - req.b;
break;
case '*':
ret = req.a * req.b;
break;
case '/':
if (req.b == 0) {
flag = 1;
break;
}
ret = req.a / req.b;
break;
default:
flag = 2;
break;
}
struct Response res;
res.status = flag;
res.result = ret;
if (flag == 1) {
res.msg = "div by zero";
} else if (flag == 2) {
res.msg = "unknown operator";
}
string msg = res.serialize();
ret = write(acfd, msg.c_str(), msg.size());
if (ret == -1) {
cout << "write faild" << endl;
cout << strerror(errno) << endl;
if (errno == EINTR) {
continue;
} else if (errno == EAGAIN) {
continue;
}
close(acfd);
return;
}
}
}
int main() {
// 忽略 SIGPIPE 信号,防止客户端关闭后 write 导致进程直接退出
signal(SIGPIPE, SIG_IGN);
// ip 127.0.0.1 端口8082 开启地址重用
listen_fd = socket(AF_INET, SOCK_STREAM, 0); // ip4 ,tcp
if (listen_fd < 0) {
cout << "socket faild" << endl;
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8082);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
int ret = bind(listen_fd, (sockaddr *)&addr, sizeof addr);
if (ret < 0)
cout << "bind faild" << endl;
int i = 0;
ret = listen(listen_fd, 10); //测试1会如何
if (ret < 0) {
cout << "listen faild" << endl;
}
struct sockaddr_in si;
socklen_t len = sizeof si;
while (true) {
int acfd = accept(listen_fd, (sockaddr *)&si, &len);
if (acfd < 0) {
cout << "accept faild" << endl;
}
cout << "accept over" << endl;
cout << "客户端端口:" << ntohs(si.sin_port) << endl;
char ip[16] = {0};
inet_ntop(AF_INET, &si.sin_addr, ip, sizeof ip);
cout << "客户端:" << ip << endl;
thread t(handle_client, acfd);
t.detach();
}
}