目录
2.重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工
0.上篇文章
Linux--Socket 编程 UDP(简单的回显服务器和客户端代码)-优快云博客
Linux--Socket 编程 TCP(Echo Server)_linux socket tcp server程序-优快云博客
1.应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用
层。再谈一谈协议
协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接
收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?(发的是结构体变量,接收的也是结构体变量)其实, 协议就是双方约定好的结构化的数据(下面计算计算器的例子)
网络版计算器
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过
去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案一:
• 客户端发送一个形如"1+1"的字符串;
• 这个字符串中有两个操作数, 都是整形;
• 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
• 数字和运算符之间没有空格;
• ...
约定方案二:
• 定义结构体来表示我们需要交互的信息;
• 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按
照相同的规则把字符串转化回结构体;
• 这个过程叫做 "序列化" 和 "反序列化"
序列化 和 反序列化
发的消息其实就是一个结构体,不同的编程语言、编译器或硬件平台可能会以不同的方式在内存中组织结构体。例如,结构体中的字段可能会根据字段类型、对齐要求和编译器优化策略进行填充或重排。因此,直接传输结构体可能会导致接收方无法正确解释数据。所以我们需要序列化和反序列化。
因此我们在传消息的时候要按照规则序列化(一变多)后再进行网络传输,到另一端后再按照规则反序列化由多变一,线程结构化数据,方便上层处理。因此有了序列化和反序列化,这一层软件层,对于上层(应用层)来说方便数据的处理,对于下层(传输层)来说方便网络的传输
无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,
在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议
但是, 为了深刻理解协议, 我打算自定义实现一下协议的过程。
• 我们采用方案 2, 我们也要体现协议定制的细节
• 我们要引入序列化和反序列化, 只不过我直接采用现成的方案 -- jsoncpp库•我们要对 socket 进行字节流的读取处理
2.重新理解 read、 write、 recv、 send 和 tcp 为什么支持全双工
1.发送和接收缓冲区:在传输层创建tcp套接字,每个TCP套接字在内核中都有一个发送缓冲区和一个接收缓冲区。发送缓冲区用于暂存待发送的数据,接收缓冲区用于暂存已接收但尚未被应用程序读取的数据。tcp支持全双工通信的本质原因:这两个缓冲区是独立的,允许数据在发送和接收方向上同时流动。read、 write、 recv、 send,本质也是拷贝到缓冲区/从缓冲区拷贝数据;发送数据的本质:是从发送缓冲区把数据通过协议栈和网络拷贝给接收方
的接收缓冲区;2.滑动窗口机制:TCP通过滑动窗口机制来实现流量控制和拥塞控制,这也是TCP协议叫做传输控制协议的原因。发送方会维护一个接收窗口,该窗口的大小表示接收方当前能够接收的数据量。发送方根据接收窗口的大小来发送数据,确保不会因为发送过快而导致接收方缓冲区溢出。这种机制保证了数据的可靠传输,同时也支持了全双工通信。
3.非阻塞和异步I/O:虽然
read
、write
、recv
、send
等函数在默认情况下是阻塞的,但Linux提供了多种机制(如select、poll、epoll、非阻塞套接字等)来实现非阻塞和异步I/O。这些机制允许应用程序在等待I/O操作完成时继续执行其他任务,从而提高了程序的效率和响应性。在非阻塞或异步模式下,TCP连接仍然支持全双工通信,只是应用程序需要处理更多的I/O事件和状态变化。有人从缓冲区中拿数据,有人从缓冲区中填数据,这就是一个生产者消费者模型!IO函数要进行阻塞,就是为例维护消费端和生产端的同步关系。
所以:
• 在任何一台主机上, TCP 连接既有发送缓冲区, 又有接受缓冲区, 所以, 在内核
中, 可以在发消息的同时, 也可以收消息, 即全双工
• 这就是为什么一个 tcp sockfd 读写都是它的原因
• 实际数据什么时候发, 发多少, 出错了怎么办, 由 TCP 控制, 所以 TCP 叫做传
输控制协议(关于面向字节流:客户端发的,不一定全部是服务器收到)
3.网络计算器(代码实现)
3.1序列化&反序列化的接口
Jsoncpp
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。 它提供了将 JSON 数据序列化为字
符串以及从字符串反序列化为 C++ 数据结构的功能。 Jsoncpp 是开源的, 广泛用于各
种需要处理 JSON 数据的 C++ 项目中。
特性:
1. 简单易用: Jsoncpp 提供了直观的 API, 使得处理 JSON 数据变得简单。
2. 高性能: Jsoncpp 的性能经过优化, 能够高效地处理大量 JSON 数据。
3. 全面支持: 支持 JSON 标准中的所有数据类型, 包括对象、 数组、 字符串、 数
字、 布尔值和 null。
4. 错误处理: 在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置, 方便
开发者调试。
当使用 Jsoncpp 库进行 JSON 的序列化和反序列化时, 确实存在不同的做法和工具类
可供选择。 以下是对 Jsoncpp 中序列化和反序列化操作的详细介绍:
安装:
C++ ubuntu: sudo apt-get install libjsoncpp-dev Centos: sudo yum install jsoncpp-devel
序列化
序列化指的是将数据结构或对象转换为一种格式, 以便在网络上传输或存储到文件
中。 Jsoncpp 提供了多种方式进行序列化:
eg:1.使用 Json::Value 的 toStyledString 方法:
优点: 将 Json::Value 对象直接转换为格式化的 JSON 字符串
C++ #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; } $ . / test.exe { "name" : "joe", "sex" : "男" }
2. 使用 Json::StreamWriter:
优点: 提供了更多的定制选项, 如缩进、 换行符等。
#include <iostream> #include <string> #include <sstream> #include <memory> #include <jsoncpp/json/json.h> int main() { Json::Value root; root["name"] = "joe"; root["sex"] = "男"; Json::StreamWriterBuilder wbuilder; // StreamWriter 的 工厂 std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter()); std::stringstream ss; writer->write(root, &ss); std::cout << ss.str() << std::endl; return 0; } $ . / test.exe { "name" : "joe", "sex" : "男" }
3.使用 Json::FastWriter
优点: 比 StyledWriter 更快, 因为它不添加额外的空格和换行符。
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> #include <string> #include <sstream> #include <memory> #include <jsoncpp/json/json.h> int main() { Json::Value root; root["name"] = "joe"; root["sex"] = "男"; Json::FastWriter writer; std::string s = writer.write(root); std::cout << s << std::endl; return 0; } test.exe {"name":"joe", "sex" : "男"} #include <iostream> #include <string> #include <sstream> #include <memory> #include <jsoncpp/json/json.h> int main() { Json::Value root; root["name"] = "joe"; root["sex"] = "男"; // Json::FastWriter writer; Json::StyledWriter writer; std::string s = writer.write(root); std::cout << s << std::endl; return 0; } $ . / test.exe { "name" : "joe", "sex" : "男" }
反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。 Jsoncpp 提供
了以下方法进行反序列化:
使用 Json::Reader:优点: 提供详细的错误信息和位置, 方便调试。
C++ #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.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; } $ . / test.exe Name : 张三 Age : 30 City : 北京
类型检查
• 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(): 检查值是否为对象(即键值对的集合) 。
3.2 项目逻辑
tcp服务器(Tcpserver)处理请求,会做IO,那么就调用IO服务(Service),IO服务处理做序列化和反序列化,还要处理业务,这时就会调用业务处理服务(NetCal网络计算器)。网络->IO->业务,三层分别对应(会话层,表示层,应用层)
无论是服务端还是客户端,都要遵循协议(Protocol),根据协议所规定的结构化数据进行通信。
3.3 代码
3.3.1辅助库
用于封装和处理 IP 地址及其端口号:InetAddr.hpp
#inc