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

【Linux网络】封装Socket

1. 模版方法模式

模板方法模式是一种行为型设计模式,它定义了一个算法的骨架,将某些步骤延迟到子类中实现,从而在不改变算法结构的情况下允许子类重新定义特定步骤。

核心结构抽象类(Abstract Class)‍:定义算法的框架(模板方法),并声明若干抽象方法或虚方法供子类实现。模板方法通常被声明为final以防止子类重写算法结构。具体子类(Concrete Class)‍:实现抽象类中定义的抽象方法,提供算法步骤的具体实现。

C++ 实现示例以下是一个典型的模板方法模式示例,以制作饮料为例:

代码语言:javascript

AI代码解释

#include <iostream> using namespace std; // 抽象类:定义饮料制作的模板方法 class DrinkTemplate { public: // 模板方法(算法骨架) void makeDrink() { boilWater(); brew(); pourInCup(); addCondiments(); } virtual ~DrinkTemplate() {} protected: void boilWater() { cout << "煮水" << endl; } void pourInCup() { cout << "倒入杯子" << endl; } virtual void brew() = 0; // 子类实现冲泡步骤 virtual void addCondiments() = 0; // 子类实现添加调料 }; // 具体子类:茶 class Tea : public DrinkTemplate { protected: void brew() override { cout << "泡茶" << endl; } void addCondiments() override { cout << "加柠檬" << endl; } }; // 具体子类:咖啡 class Coffee : public DrinkTemplate { protected: void brew() override { cout << "冲泡咖啡" << endl; } void addCondiments() override { cout << "加糖和牛奶" << endl; } }; int main() { DrinkTemplate* tea = new Tea(); tea->makeDrink(); // 输出:煮水、泡茶、倒入杯子、加柠檬 DrinkTemplate* coffee = new Coffee(); coffee->makeDrink(); // 输出:煮水、冲泡咖啡、倒入杯子、加糖和牛奶 delete tea; delete coffee; return 0; }

应用场景

  • 固定流程可变实现:如文档处理(PDF/Word 解析)、编译流程、游戏循环等。
  • 框架设计:父类控制整体逻辑,子类扩展细节(如 GUI 库、网络库)。

优点与缺点

  • 优点
    • 提高代码复用性,将公共行为集中在父类。
    • 允许子类扩展特定步骤,符合开闭原则。
  • 缺点
    • 子类数量可能过多,导致系统复杂。
    • 父类修改可能影响所有子类。

注意事项

  • 模板方法应声明为非虚函数(如使用 final 或非虚函数),以保持算法结构稳定。
  • 抽象方法(如 brew())应声明为 protected,限制外部直接调用。

此模式通过继承和多态实现算法的可变性与稳定性的平衡,是 C++ 中常用的设计模式之一。


2. 封装Socket

  1. 那我们就可以抽象一个Socket的基类,将创建套接字等需要的系统调用在基类中设为纯虚函数,然后我们可以定义两个模板方法,一个UDP的模板方法,一个TCP的模板方法,需要使用哪个传输层协议的网络服务就在主程序中调用哪个模板方法
  2. 具体子类UDP或TCP服务端可以实现抽象类中虚函数的具体实现

不过UDP相对TCP简单一点,所以我们具体子类主要实现TCP服务器

2.1 Socket基类

基类主要就是所需要的系统调用设为纯虚函数,然后定义一个TCP服务端所需要的系统调用的模板方法

代码如下:

代码语言:javascript

AI代码解释

#pragma once #include "Common.hpp" #include "Log.hpp" #include "InetAddr.hpp" namespace SocketModule { using namespace LogModule; const static int gbacklog = 16; // 模板方法模式 class Socket { protected: virtual ~Socket() {} virtual void SocketOrDie() = 0; 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; 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: void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog) { SocketOrDie(); BindOrDie(port); ListenOrDie(backlog); } void BuildTcpClientSocketMethod() { SocketOrDie(); } }; }

我们基类将TCP服务端需要的系统调用都设为虚函数,在前面的文章中,我们已经写过TCP网络编程,对于需要的系统调用我们已经熟悉了。两个模板方法分别为服务端和客户端调用,服务端通过子类TcpSocket多态调用基类中的模板方法来完成创建套接字,绑定,监听等连接操作


2.2 TcpSocket子类

这里我们设置两个构造函数,一个无参构造用于初始化listen套接字,一个用于将connect返回的文件描述符构造为套接字类型,而不是直接返回一个int类型的文件描述符,这样做的好处是,在后续使用该文件描述符时可以直接通过套接字来调用封装的函数,而如果是int类型的话,只能使用原始的系统调用,但我们已经封装了就尽量使用封装的系统调用,这样虽然也行但是有点挫

代码语言:javascript

AI代码解释

namespace SocketModule { using namespace LogModule; const static int gbacklog = 16; // 模板方法模式 class Socket { protected: virtual ~Socket() {} virtual void SocketOrDie() = 0; 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; 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: void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog) { SocketOrDie(); BindOrDie(port); ListenOrDie(backlog); } void BuildTcpClientSocketMethod() { SocketOrDie(); } }; const static int defaultfd = -1; class TcpSocket : public Socket { public: TcpSocket() // 无参构造listensockfd :_sockfd(defaultfd) {} // 将connect返回的文件描述符构造为套接字类型 TcpSocket(int fd) :_sockfd(fd) {} ~TcpSocket() {} private: int _sockfd; // listensockfd, sockfd都可能 }; }

对于创建,绑定,监听这三个必要的基本操作,我们已经熟悉了,不多说,代码如下

代码语言:javascript

AI代码解释

void SocketOrDie() override { _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 local(port); int n = ::bind(_sockfd, local.NetAddrPtr(), local.NetAddrLen()); if(n < 0) { LOG(LogLevel::FATAL) << "bind error"; exit(BIND_ERR); } LOG(LogLevel::INFO) << "bind success"; } void ListenOrDie(int backlog) override { int n = ::listen(_sockfd, backlog); if (n < 0) { LOG(LogLevel::FATAL) << "listen error"; exit(LISTEN_ERR); } LOG(LogLevel::INFO) << "listen success"; }

基本操作做完,接下来就是服务端接受连接了,下面我们就来实现Accept

代码语言:javascript

AI代码解释

std::shared_ptr<Socket> Accept(InetAddr *client) override { struct sockaddr_in peer; socklen_t len = sizeof(peer); int fd = ::accept(_sockfd, (struct sockaddr*)&peer, &len); if (fd < 0) { LOG(LogLevel::WARNING) << "accept warning ..."; return nullptr; } client->SetAddr(peer); return std::make_shared<TcpSocket>(fd); }

注意:我们这里只是实现虚函数,将来是要在外部来调用,但是我们需要知道是哪个客户端发送的信息,可我们在定义时又不需要用到客户端的地址信息,那我们就可以通过输出型参数将地址信息让外部可以拿到。

不过我们是从网络中拿到的客户端地址信息,所以就需要从网络字节序转为主机字节序,那这步我们就可以在定义的时候来做。但是我们封装的InetAddr类只有构造的时候是将网络字节序转为主机字节序,我们这里是输出型参数,所以我们可以在InetAddr类中新增一个网络转主机的函数SetAddr,通过参数来调用SetAddr

我们在退出的时候最好还是需要将文件描述符关闭,我们之前没有说这个,这里提一下

代码语言:javascript

AI代码解释

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

然后就是读写数据,tcp是面向字节流的,所以我们上篇文章中选择使用read/write来读写数据,这次我们介绍另一种tcp读写数据的系统调用

recv系统调用

recv 系统调用用于从一个已连接的面向连接的套接字(如 SOCK_STREAM,即 TCP 套接字)或已绑定的无连接套接字(如 SOCK_DGRAM,即 UDP 套接字)接收数据。

它类似于 read 系统调用,但提供了额外的 flags 参数来控制接收行为。

代码语言:javascript

AI代码解释

#include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数详解

  1. int sockfd
  • 描述: 这是一个由 socket() 创建,并经过 connect()(对于客户端)或 accept()(对于服务器端)处理后的套接字文件描述符。
  • 要求: 套接字必须是已连接的(对于 TCP)或已绑定的(对于 UDP)。
  1. void *buf
  • 描述: 这是一个指向内存缓冲区的指针,用于存放接收到的数据。
  • 要求: 应用程序必须确保这个缓冲区有足够的空间(至少 len 字节)来存放数据,否则会导致内存越界,引发未定义行为(如程序崩溃)。
  1. size_t len
  • 描述: 指定缓冲区 buf 的最大容量,即你希望一次最多接收多少字节的数据。
  • 注意: recv 最多只会向你返回 len 字节的数据,即使对端发送了更多的数据。多余的数据会留在内核的套接字接收缓冲区中,等待下一次 recv 调用。
  1. int flags
  • 描述: 这是一个控制接收行为的标志位。它可以是一个或多个标志的按位或(OR)组合,最常用的标志是 0(表示默认行为,阻塞等待)。
  • 常用标志:
    • 0: 标准模式。调用将阻塞,直到有数据可用或连接关闭。
    • MSG_DONTWAIT: 以非阻塞方式操作。如果没有数据立即可用,recv 会立即返回失败,并设置错误码 EAGAIN 或 EWOULDBLOCK。这是实现高并发网络编程(如使用 epoll)的关键。
    • MSG_PEEK: "窥探"数据。将数据从内核缓冲区复制到应用缓冲区 buf,但不会将这些数据从内核缓冲区中移除。下一次调用 recv(不带 PEEK)还会看到这些相同的数据。
    • MSG_WAITALL: 阻塞等待,直到请求的完整数据(len 字节)全部到达、发生错误或连接关闭为止。但在某些情况下(如收到信号或连接被对端部分关闭),它返回的字节数可能仍少于请求的字节数。

返回值recv 的返回值是理解其行为的关键:

  • 成功时:
    • > 0: 返回实际接收到的字节数。这个值可以小于你请求的 len。对于面向流的协议(如 TCP),这是非常正常的。
  • 失败时:
    • -1: 发生错误,并设置全局变量 errno 以指示具体的错误类型。
  • 连接关闭时:
    • 0: 这表示对端已经优雅地关闭了连接(对于 TCP 来说,就是收到了 FIN 包)。这是一个正常的关闭信号,不应被视为错误。返回 0 是判断对端是否已关闭连接的标准方法。

代码如下:

代码语言:javascript

AI代码解释

int Recv(std::string *out) override { char buffer[1024]; ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0); if (n > 0) { buffer[n] = 0; *out += buffer; // 特意+= } return n; }

注意:

  • recv(sockfd, buf, len, 0) 基本等价于 read(sockfd, buf, len)。recv 只是多了一个 flags 参数。
  • recvfrom(): 是 recv 的增强版,主要用于无连接套接字(如 UDP)。它比 recv 多两个参数,可以获取发送方的地址信息。

这里我们同样也是需要从外部调用,如果外部要得到读取的缓冲区内容就需要通过输出型参数,而且输出型参数需要+=buffer,因为外部(上层)可能还没有将之前的数据拿完,那这个时候就不能直接覆盖掉上次的数据,所以特意+=buffer

send系统调用

send 系统调用用于向一个已连接的套接字(如 TCP 套接字)发送数据。它类似于 write 系统调用,但提供了额外的 flags 参数来控制发送行为。

注意:对于无连接的套接字(如 UDP),通常使用 sendto 或 sendmsg,因为它们允许指定目标地址。

代码语言:javascript

AI代码解释

#include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数详解

  1. i**nt sockfd**
  • 描述: 这是一个由 socket() 创建,并经过 connect()(对于客户端)或 accept()(对于服务器端)处理后的套接字文件描述符。
  • 要求: 套接字必须是已连接的(对于 TCP)或已绑定的(对于 UDP,但通常用 sendto)。
  1. const void *buf
  • 描述: 这是一个指向内存缓冲区的指针,该缓冲区包含要发送的数据。
  • 要求: 应用程序必须确保这个缓冲区包含有效的数据,并且至少有 len 字节。
  1. size_t len
  • 描述: 指定要发送数据的字节数。
  1. int flags
  • 描述: 这是一个控制发送行为的标志位。它可以是一个或多个标志的按位或(OR)组合,最常用的标志是 0(表示默认行为)。
  • 常用标志:
    • 0: 标准模式。阻塞发送,直到所有数据被内核接管(但不一定被对端接收)。
    • MSG_DONTWAIT: 以非阻塞方式操作。如果数据不能立即被发送(比如套接字发送缓冲区已满),send 会立即返回失败,并设置错误码 EAGAIN 或 EWOULDBLOCK。这是实现高并发网络编程的关键。
    • MSG_OOB: 发送带外数据(Out-of-band data)。这用于发送紧急数据,但通常不推荐使用,因为不同的实现可能不一致。
    • MSG_MORE: 提示内核还有更多数据要发送。对于 TCP,这个标志会导致内核将数据缓存起来,等待后续没有 MSG_MORE 标志的 send 调用时再一起发送。这有助于减少小数据包的传输(类似于 TCP_CORK 或 TCP_NODELAY 的调整)。

返回值send 的返回值是理解其行为的关键:

  • 成功时:
    • > 0: 返回实际发送的字节数。这个值可以小于你请求的 len,特别是在非阻塞模式下。
  • 失败时:
    • -1: 发生错误,并设置全局变量 errno 以指示具体的错误类型。

代码如下:

代码语言:javascript

AI代码解释

int Send(const std::string &message) override { return send(_sockfd, message.c_str(), message.size(), 0); }

这里我们不做过多介绍,多路转接时会详细介绍

接下来就是客户端发起连接Connect

代码语言:javascript

AI代码解释

int Connect(const std::string &server_ip, uint16_t port) override { InetAddr server(server_ip, port); return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen()); }

3. 服务端

封装好之后就是使用封装的Socket来实现服务端,我们已经实现过了,这里就不再介绍了,只需要将原先的原生系统调用换成封装的Socket即可

代码语言:javascript

AI代码解释

#pragma once #include "Socket.hpp" #include <memory> #include <sys/wait.h> using namespace SocketModule; using namespace LogModule; using ioservice_t = std::function<void(std::shared_ptr<Socket>&, InetAddr&)>; class TcpServer { public: TcpServer(uint16_t port, ioservice_t service) :_port(port), _listensockptr(std::make_unique<TcpSocket>()), _isrunning(false), _service(service) { _listensockptr->BuildTcpSocketMethod(_port); } void Start() { _isrunning = true; while (_isrunning) { InetAddr client; auto sock = _listensockptr->Accept(&client); // 1. 和client通信sockfd 2. client 网络地址 if (sock == nullptr) { continue; } LOG(LogLevel::DEBUG) << "accept success ..." << client.StringAddr(); pid_t id = fork(); if (id < 0) { LOG(LogLevel::FATAL) << "fork error ..."; exit(FORK_ERR); } else if (id == 0) { // 子进程 -> listensock _listensockptr->Close(); if (fork() > 0) exit(OK); // 孙子进程在执行任务,已经是孤儿了 _service(sock, client); sock->Close(); exit(OK); } else { // 父进程 -> sock sock->Close(); pid_t rid = ::waitpid(id, nullptr, 0); (void)rid; } } _isrunning = false; } ~TcpServer() {} private: uint16_t _port; std::unique_ptr<TcpSocket> _listensockptr; bool _isrunning; ioservice_t _service; };

我们这里使用多进程分别接收连接和执行任务,这里任务我们需要在上层去实现,后面文章会详细介绍。

后面文章我们会再谈协议,然后自己来定义协议,然后顶层封装一个任务,通过我们自己定义的协议来完成序列化和反序列化,让对端拿到我们的任务去处理,所以客户端也放在后面实现

http://www.jsqmd.com/news/729030/

相关文章:

  • R 4.5正式版时空模块深度解析(含未公开的spatialscale 2.0底层重构细节)
  • 避坑指南:STM32H7驱动ST7789屏幕,SPI时钟到底能跑多快?
  • 不止于测试:用Playwright的expect_download()给你的Python爬虫加上稳定下载模块
  • SMU源测量单元:精密电子测试的核心技术与应用
  • 深入了解电源纹波和噪声原理和测试方案
  • 我的世界 Java 版服务器联机搭建|零基础一键部署
  • Tidyverse 2.0报告崩溃频发,你还在用`knitr::kable()`硬扛?——解析`tidyselect 1.2.0`语义解析器重构引发的3类静默失败场景
  • python的逻辑与循环详解
  • 保姆级教程:用ECharts for Weixin在小程序里画个家庭旅行足迹地图
  • HI3861 I2C驱动NT3H1201 NFC标签的避坑指南:从地址0x55到NDEF封包的那些事儿
  • 2026年商场川味餐饮加盟TOP5推荐 聚焦场景适配性 - 优质品牌商家
  • 试了一下CSDN多平台同步发布功能:从单点发布到全网分发,还挺好用的
  • 第三周详细练习手册:网络排错实战
  • 基于LLM与Whisper的智能面试分析系统:从架构到实践
  • 包装设计选哪家,报价背后要看打样周期和修改次数
  • YOLO26涨点改进| CVPR 2026 |独家创新首发、特征融合改进篇| 引入SCMF空间-通道调制融合模块,兼顾通道特征表达和多尺度融合质量,助力小目标检测、小目标图像分割、图像融合有效涨点
  • Cursor-Flow:AI编程工作流引擎的设计原理与工程实践
  • 如何永久备份微信聊天记录:WeChatMsg完整数据导出终极指南
  • 新榜智汇拆解 靠谱GEO优化工具的必备功能解析
  • 为AI智能体注入元认知能力:基于开源模板的架构设计与工程实践
  • OpenClaw-Agents:操作型智能体框架的深度解析与实践指南
  • 中国半导体展会哪家好:优选中国本土半导体展会 深耕国内产业资源对接 - 品牌2026
  • 四博 AI-S3 双目交互终端方案:ESP32-S3 + VB6824 + 双屏动画 + 四路触控 + 姿态感应实现
  • 在Nodejs后端服务中集成Taotoken实现多模型智能问答接口
  • 4D动态重建正面交锋,流式建图凭什么完成破局?
  • PMSM无感FOC实战:滑模观测器(SMO)的‘坑’我都替你踩过了——增益调节与滤波器设计避坑指南
  • 量子模拟技术解析:从费米极化子到BEC-BCS转变
  • Laravel 12正式版AI扩展报错全解:从Composer冲突到OpenAI v1.0 SDK适配的7步标准化修复流程
  • COMTool:跨平台通信调试工具的模块化架构深度解析
  • 【研报410】AI大模型车载软件平台白皮书:分层解耦架构,推动智能汽车全域AI化