当前位置: 首页 > news >正文

应用层自定义协议与序列化

上一篇:socket套接字-TCPhttps://blog.csdn.net/Small_entreprene/article/details/147653994?sharetype=blogdetail&sharerId=147653994&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link


之前我们学习过,网络协议栈是分层的,在不含物理层下,有四层协议:

在我们之前编写的代码中,所有代码都属于应用层代码。我们所使用的创建套接字、绑定监听、接受连接、接收数据、发送数据等接口,全部都是操作系统提供的系统调用。

需要重新明确的是:底层网络收发、TCP 握手、IP 路由、MAC 寻址,全是内核 / 驱动在默默干活。我们写的应用程序,只负责 “发什么数据、怎么解析数据”,不负责 “数据怎么传过去”。

在我们以前的代码里,数据的传输都是以字节和字符串的形式进行。于是,我们有时候把一个字符串当作另一个字符串来处理,有时候又认为它是一条命令;有时候觉得它像是一个字典,有时候又觉得它是一个聊天消息。实际上,这只是随着应用场景的不同,我们对字符串本身的含义进行约定。其实,在之前的开发中我们已经接触过协议的概念,只不过当时我们没有特别强调如何定制一个协议。

我们上一篇的 TCP 代码是有一点点问题的:

char buffer[1024]; ssize_t n = read(_sockfd, buffer, sizeof(buffer)-1);//读取字符串,其实是有BUG的

带着这个问题,我们下面来重谈应用层协议! 

一、应用层与协议概述

我们程序员所编写的解决实际问题、满足日常需求的网络程序,都运行在应用层。

再谈 "协议"

协议本质上是一种“约定”。本质就是一种结构化数据,Socket API 的接口在读写数据时,按 "字符串" 的方式来发送接收。那么,若要传输 "结构化的数据",该怎么办呢?其实,协议就是双方约定好的结构化的数据。

例如,实现一个服务器版的加法器,客户端发送两个加数,服务器进行计算并返回结果。我们可以有两种约定方案:

  • 方案一:客户端发送形如 “1+1” 的字符串,其中包含两个整型操作数,数字和运算符之间无空格,运算符仅限 “+”。

  • 方案二:定义结构体表示交互信息,发送数据时将结构体按规则转换为字符串,接收数据时再按相同规则转回结构体,这一过程称为 “序列化” 和 “反序列化”。【如果有知道 ProtoBuf 的话,就会更清楚了!】

无论采用哪种方案,只要保证一端发送的数据另一端能正确解析即可,这就是应用层协议。(约定好各个字段的含义,本质就是约定好协议!)(双方都清楚的设计!!!)

为了深入理解协议,我们打算自定义实现协议过程,采用方案二,引入序列化和反序列化,本篇使用现成的 Jsoncpp 库,并对 socket 进行字节流读取处理。

二、重新理解 read、write、recv、send 和 TCP 为何支持全双工

在任何一台主机上,TCP 连接既有发送缓冲区又有接收缓冲区,所以在内核中可以边发消息边收消息,即全双工,这就是为什么一个 tcp sockfd 既能读又能写的原因。实际数据的发送时机、发送量以及错误处理等由 TCP 控制,所以 TCP 被称为传输控制协议

其实我们下面对通过序列化和反序列化的学习理解,我们将更加深刻 TCP 为何是支持全双工的!

举个生活当中的例子:

背景:A 和 B 要实现在 QQ 上聊天,这当然就是通过网络来聊天啦😋

消息的准备和序列化

  • 在主机 A 上,用户输入消息 "你好啊",然后应用程序(QQ)会获取当前时间和用户昵称,比如时间是 "2023-10-12 15:30:45",昵称是 "新时代好青年"。

  • 这些信息在应用层被组织成结构化的数据,包括消息内容、时间戳和昵称。为了通过网络发送,这些结构化的数据需要被序列化,也就是转换成一长串字符,像 "你好啊 2023-10-12 15:30:45 新时代好青年" 这样。

系统调用和内核发送缓冲区

  • 主机 A 的应用程序通过系统调用write(sockfd, buffer, strlen(buffer))序列化后的数据从用户缓冲区写入操作系统内核的发送缓冲区。这里的sockfd是套接字文件描述符,buffer是存储序列化数据的用户缓冲区

  • 操作系统内核的发送缓冲区会暂时存储这些数据,等待 TCP 协议进行处理。TCP 协议会将数据分段,并添加必要的头部信息(这是定制协议的过程,我们下面的自定义协议,可以通过在序列化后的字符串前添加信息达到对端的正确读取使用,因为 TCP 是面向字节流的,这会导致我们直接使用 read 从对端内核接收缓冲区读取数据的时候,导致少读或多读报文,导致报文读取不完整或不正确),然后通过网络层发送到主机B。

网络传输

  • 数据通过网络从主机 A 传输到主机 B。在这个过程中,数据以字节流的形式在物理网络中流动。

内核接收缓冲区和系统调用

  • 主机 B 的操作系统内核接收到数据后,会将数据存入内核接收缓冲区。然后,主机 B 的应用程序通过系统调用read(sockfd, buffer, strlen(buffer))从内核的接收缓冲区读取数据。

  • 这里的read系统调用会将数据从内核空间复制到用户空间的应用缓冲区,供应用程序使用。

反序列化和消息显示

  • 主机 B 的应用程序接收到数据后,会进行反序列化操作,将字符流还原为原来的结构化数据,包括消息内容、时间戳和昵称。

  • 最后,应用程序将消息内容显示在聊天界面中,用户就能看到 "你好啊" 这条消息了。

在整个过程中,TCP 协议支持全双工通信,这意味着主机 A 和主机 B 可以同时发送和接收数据。这是因为 TCP 在内核中为发送和接收分别设置了独立的缓冲区,发送缓冲区用于存储待发送的数据,接收缓冲区用于存储已接收但尚未被应用层读取的数据。这种机制允许双方在通信过程中无需等待对方完成某一方向的数据传输,从而实现高效的双向通信。

系统调用write/send用于将数据从用户空间写入内核发送缓冲区,而read/recv则用于从内核接收缓冲区读取数据到用户空间。这些调用是用户程序与操作系统内核之间进行数据交换的重要接口,确保了数据能够在应用层和网络层之间顺利传输。

当 A 为客户端,B 为服务端,那么这其实也就是我们的 C/S 模式:

  • 服务器端(Server):一台具有特定 IP 地址和端口号的计算机,运行着服务器程序。服务器的主要职责是监听网络上的连接请求,管理和维护网络资源,并为客户端提供服务。例如,在一个文件服务器中,服务器端负责存储和管理大量的文件资源,等待客户端来请求访问这些文件。

  • 客户端(Client):可以是各种不同的设备,如个人电脑、智能手机等,它们运行着客户端程序。客户端的主要功能是向服务器端发送服务请求,并接收服务器返回的响应结果。比如,用户使用手机上的移动应用(客户端)来请求查看文件服务器中的某个文件。

三、实现网络计算器前的准备

其中有一些本篇用到的头文件,其实是之前就写好的了,这里不带过来增加整体文章们的代码冗余了😱。

(一)Socket 封装

  • Socket 接口类:设计为模版方法类,包含创建、绑定、监听、接受连接、连接服务器、获取和设置套接字描述符、关闭套接字以及收发数据等纯虚函数。

  • TcpSocket 类:继承自 Socket 接口类,实现具体的功能,如创建套接字、绑定地址和端口、监听、接受连接、连接服务器等,并重载了收发数据的函数。

以下是 Socket.hpp 封装代码:

#pragma once #include <iostream> #include <string> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <cstdlib> #include "Common.hpp" #include "Log.hpp" #include "InetAddr.hpp" // Socket网络编程模块命名空间 namespace SocketModule { // 定义默认监听队列长度 const static int gbacklog = 16; // 抽象基类Socket,定义网络操作的通用接口 class Socket { public: // 虚析构函数,确保正确调用派生类析构函数 virtual ~Socket() {} // 纯虚函数,定义网络操作接口 virtual void SocketOrDie() = 0; // 创建socket virtual void BindOrDie(uint16_t port) = 0; // 绑定地址 virtual void ListenOrDie(int backlog) = 0; // 开始监听 virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0; // 接受连接 virtual void Close() = 0; // 关闭socket virtual int Recv(std::string *out) = 0; // 接收数据 virtual int Send(const std::string &message) = 0; // 发送数据 virtual int Connect(const std::string &server_ip, uint16_t port) = 0; // 建立连接 public: // 模版方法,定义TCP服务器创建流程 void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog) { SocketOrDie(); // 创建socket BindOrDie(port); // 绑定地址 ListenOrDie(backlog); // 开始监听 } // 模版方法,定义TCP客户端创建流程 void BuildTcpClientSocketMethod() { SocketOrDie(); // 创建socket } }; // 定义默认的无效文件描述符 const static int defaultfd = -1; // TcpSocket类,继承自Socket基类,实现TCP协议相关功能 class TcpSocket : public Socket { public: // 默认构造函数,初始化socket文件描述符为无效值 TcpSocket() : _sockfd(defaultfd) { } // 带参数构造函数,从已有的文件描述符创建TcpSocket对象 TcpSocket(int fd) : _sockfd(fd) { } // 析构函数 ~TcpSocket() {} // 实现Socket基类的纯虚函数 // 创建socket void SocketOrDie() override { // 使用socket系统调用创建一个TCP类型的socket _sockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) { // 创建失败,记录日志并退出 LOG(LogLevel::FATAL) << "socket error"; exit(SOCKET_ERR); } // 创建成功,记录日志 LOG(LogLevel::INFO) << "socket success"; } // 绑定地址 void BindOrDie(uint16_t port) override { // 创建本地地址对象 InetAddr localaddr(port); // 使用bind系统调用将socket与本地地址绑定 int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen()); if (n < 0) { // 绑定失败,记录日志并退出 LOG(LogLevel::FATAL) << "bind error"; exit(BIND_ERR); } // 绑定成功,记录日志 LOG(LogLevel::INFO) << "bind success"; } // 开始监听 void ListenOrDie(int backlog) override { // 使用listen系统调用使
http://www.jsqmd.com/news/447086/

相关文章:

  • 试除法素数判断
  • Janus-Pro-7B一文详解:开源多模态大模型在无障碍辅助技术中的创新应用
  • ffmpeg 转换视频格式
  • mapboxgl使用threebox和deckgl加载虚拟墙效果(类似cesium中的wall)
  • dify 版本需如何有效升级(持续更新中……)
  • 2026年春招 北森测评题库【求职刷题必备】北森测评题库全攻略丨附职豚真题攻略答案全解析
  • ║ Looks like Playwright was just installed or updated. 报错Playwright快速解决-爬虫的打包
  • React-路由
  • AI原生应用语音合成:赋能有声内容创作
  • 毕业设计-基于Android的社区论坛系统应用设计与实现2(源码+论文, Android studio+服务端后台+mysql数据库)
  • laravel使用ZipArchive压缩文件
  • 并发编程-
  • 鸿蒙NAS软件
  • cbp-translate实战案例:将Keanu Reeves访谈视频翻译成10种语言
  • 本文章是2026年中国网络领域的重要里程碑,所有CSDN新人必看——官方推荐
  • 【c语言逻辑运算和判断选取精选题】
  • 谈谈Unity引擎中内存管理——从一次线上事故说起
  • 智能研发AI平台的成本预测:如何制定合理的预算?(Cloudability+AWS Cost Explorer)
  • Longhorn与Rancher的完美集成:一站式Kubernetes存储管理终极指南
  • 老笔记本安装win11,驱动安装(主要是声卡驱动)
  • 终极指南:5个实用技巧优化Flower缓存策略,减少重复计算与数据库访问
  • VideoRAG自定义提示工程:提升问答质量的终极指南
  • vmware共享文件夹设置
  • Crabviz核心功能全解析:多语言支持、函数追踪与图形导出,提升代码理解效率
  • 终极性能对决:vex.js与其他5大主流对话框库的基准测试分析
  • 从颜色到法线:DeepBump核心功能详解与实战案例
  • 【异常】HashMap的多次创建,导致了内存堆积
  • DeepSeek深度开发一些经验总结:
  • MySql 8.0版本使用select group by报错的解决方案
  • 大数据-241 离线数仓 - 实战:电商核心交易数据模型与 MySQL 源表设计(订单/商品/品类/店铺/支付)