对于Linux:模版方法类的解析以及socket、TcpSocket的封装

开篇介绍:

hello 大家,那么在本篇博客中,我们就将对socket、TcpSocket进行封装(秉承着我们的一贯风格),但是在封装的同时,我们也将学习一个新的模式------模版方法类,我们之前已经学习过了策略模式,相信大家也能在本篇博客中收获多多。

引言:设计模式的魅力与网络编程的挑战

在软件开发的漫长历程中,设计模式作为一种经验的沉淀,为我们提供了一套解决特定问题的通用方案。它们就像是编程世界里的 "武功秘籍",让开发者能够站在巨人的肩膀上,快速构建出优雅、可维护且高性能的系统。其中,模板方法模式(Template Method Pattern) 以其简洁的结构和强大的复用能力,成为了行为型设计模式中的佼佼者。

当我们将目光聚焦于网络编程领域,尤其是 TCP/IP 协议的实现时,会发现一个有趣的现象:无论是服务端的 listen、accept,还是客户端的 connect、send,都遵循着一套相似的流程。然而,这些流程又存在着细微的差异,比如服务端需要绑定端口,而客户端不需要;服务端需要监听连接,而客户端需要主动发起连接。这种 "相似中的不同" 正是模板方法模式发挥作用的绝佳场景。

模板方法模式的核心概念与实现

1. 什么是模板方法模式?

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

想象一下,您需要制作一杯咖啡和一杯奶茶。虽然它们的制作流程有相似之处,但也有明显的不同:

  • 咖啡:烧开水 → 冲泡咖啡粉 → 倒入杯子 → 加糖
  • 奶茶:烧开水 → 冲泡奶茶粉 → 倒入杯子 → 加珍珠

在这个例子中,"烧开水" 和 "倒入杯子" 是所有饮品都必须遵循的固定步骤,而 "冲泡原料" 和 "添加调料" 则是不同饮品的特有步骤。模板方法模式正是利用了这种 "固定流程 + 可变步骤" 的结构,将不变的部分抽象到一个基类中,将可变的部分留给子类实现。

2. 模板方法模式的结构

一个典型的模板方法模式包含以下几个核心角色:

抽象类(Abstract Class)

抽象类定义了算法的骨架,它包含:

  • 模板方法(Template Method) :定义算法的整体流程,按固定顺序调用各个步骤。通常将模板方法声明为final,以防止子类修改算法结构。
  • 抽象步骤(Primitive Operations):算法中必须由子类实现的步骤,通常是纯虚函数。
  • 具体步骤(Concrete Steps):在抽象类中已经实现的步骤,所有子类共享。
  • 钩子方法(Hook Operations):算法中的可选步骤或扩展点,通常有默认实现,子类可以选择覆盖。
具体类(Concrete Class)

具体类继承自抽象类,实现抽象步骤和可选的钩子方法,提供具体的业务逻辑。

3. 模板方法模式的实现步骤

以制作饮品为例,我们来详细说明模板方法模式的实现步骤:

第一步:定义抽象类
cpp 复制代码
// 抽象类:饮品制作模板
class DrinkTemplate {
public:
    // 模板方法:固定制作流程(final防止子类修改流程)
    final void makeDrink() {
        boilWater();   // 固定步骤1:烧开水
        brew();        // 抽象步骤2:冲泡原料(子类实现)
        pourInCup();   // 固定步骤3:倒入杯子
        addCondiment();// 抽象步骤4:加调料(子类实现)
    }

    // 固定步骤:烧开水(所有饮品都一样)
    void boilWater() {
        cout << "1. 烧开水(100℃)" << endl;
    }

    // 固定步骤:倒入杯子(所有饮品都一样)
    void pourInCup() {
        cout << "3. 将饮品倒入杯子" << endl;
    }

    // 抽象步骤:冲泡原料(子类自己实现)
    virtual void brew() = 0;

    // 抽象步骤:加调料(子类自己实现)
    virtual void addCondiment() = 0;
};
第二步:实现具体子类
cpp 复制代码
// 具体实现类1:咖啡
class Coffee : public DrinkTemplate {
public:
    // 实现"冲泡原料":咖啡粉
    void brew() override {
        cout << "2. 冲泡咖啡粉" << endl;
    }

    // 实现"加调料":方糖
    void addCondiment() override {
        cout << "4. 加方糖" << endl;
    }
};

// 具体实现类2:奶茶
class MilkTea : public DrinkTemplate {
public:
    // 实现"冲泡原料":奶茶粉
    void brew() override {
        cout << "2. 冲泡奶茶粉" << endl;
    }

    // 实现"加调料":珍珠+椰果
    void addCondiment() override {
        cout << "4. 加珍珠和椰果" << endl;
    }
};
第三步:使用模板方法
cpp 复制代码
int main() {
    // 泡一杯咖啡
    DrinkTemplate* coffee = new Coffee();
    cout << "===== 制作咖啡 =====" << endl;
    coffee->makeDrink();

    // 泡一杯奶茶
    DrinkTemplate* milkTea = new MilkTea();
    cout << "\n===== 制作奶茶 =====" << endl;
    milkTea->makeDrink();

    delete coffee;
    delete milkTea;
    return 0;
}
运行结果:
cpp 复制代码
===== 制作咖啡 =====
1. 烧开水(100℃)
2. 冲泡咖啡粉
3. 将饮品倒入杯子
4. 加方糖

===== 制作奶茶 =====
1. 烧开水(100℃)
2. 冲泡奶茶粉
3. 将饮品倒入杯子
4. 加珍珠和椰果

通过这个简单的例子,我们可以看到模板方法模式的核心思想:定框架、填细节。基类定义了算法的固定流程,子类负责实现可变的细节。这种结构不仅减少了代码冗余,还保证了算法流程的一致性。

模板方法模式的优势与适用场景

1. 模板方法模式的优势

代码复用

在模板方法模式中,固定的步骤只需要在抽象类中实现一次,所有子类都可以共享这些代码,避免了重复编写相同逻辑的麻烦。

流程统一

模板方法模式确保了所有子类都遵循相同的算法流程,避免了因流程混乱导致的错误。例如,在饮品制作的例子中,我们可以确保所有饮品都先烧开水,再冲泡原料,最后倒入杯子并添加调料,不会出现先加调料再烧开水的情况。

扩展方便

当需要新增一种饮品时,只需要创建一个新的子类并实现抽象步骤,无需修改现有代码。这符合 "开闭原则",即对扩展开放,对修改关闭。

控制流程

模板方法中的模板方法通常用final修饰,防止子类修改算法的整体流程。这保证了核心逻辑的稳定性,同时允许子类灵活地调整细节。

2. 模板方法模式的适用场景

模板方法模式在实际开发中应用广泛,以下是一些常见的适用场景:

框架生命周期

许多框架都使用模板方法模式来定义组件的生命周期。例如,Spring 框架中的 Bean 生命周期:

复制代码
实例化 → 初始化 → 使用 → 销毁

其中,"实例化" 和 "销毁" 是固定步骤,而 "初始化" 和 "使用" 则是由具体 Bean 子类实现的。

报表生成

在报表系统中,通常有一个固定的流程:

复制代码
表头 → 数据填充 → 计算统计 → 生成文件

其中,"表头" 和 "计算统计" 是固定步骤,而 "数据填充" 则由不同的报表子类实现。

支付流程

在支付系统中,支付流程通常包括:

复制代码
参数验证 → 调用支付接口 → 处理结果 → 通知用户

其中,"参数验证" 和 "通知用户" 是固定步骤,而 "调用支付接口" 则由不同的支付方式(如微信支付、支付宝)实现。

游戏角色技能

在游戏开发中,角色技能的释放通常遵循固定流程:

复制代码
释放前摇 → 技能效果 → 释放后摇

其中,"释放前摇" 和 "释放后摇" 是固定步骤,而 "技能效果" 则由不同的技能子类实现。

模板方法模式在 Socket 封装中的应用

现在,让我们将目光转向网络编程领域,看看模板方法模式是如何在 Socket 封装中发挥作用的。

1. 问题分析:传统 Socket 编程的痛点

在传统的 Socket 编程中,我们通常需要编写大量重复的代码。例如,无论是服务端还是客户端,都需要创建 Socket、绑定地址、监听连接等操作。这些操作不仅重复,而且容易出错。

以服务端为例,传统的 Socket 编程流程如下:

cpp 复制代码
// 创建Socket
int server_socket = socket(AF_INET, SOCK_STREAM, 0);

// 设置端口复用
int opt = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

// 绑定地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 监听连接
listen(server_socket, 16);

// 接受连接
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);

// 发送数据
send(client_socket, "Hello, client!", 13, 0);

// 接收数据
char buf[1024];
recv(client_socket, buf, sizeof(buf)-1, 0);

// 关闭Socket
close(server_socket);
close(client_socket);

可以看到,这段代码包含了大量重复的操作。如果我们需要为不同的服务创建多个 Socket,就需要重复编写这些代码,不仅繁琐,而且容易出错。

2. 解决方案:使用模板方法模式封装 Socket

为了解决这个问题,我们可以使用模板方法模式来封装 Socket 操作。通过将不变的步骤抽象到一个基类中,将可变的步骤留给子类实现,我们可以大大简化 Socket 编程。

基类设计:Socket

我们首先定义一个抽象基类Socket,它包含了所有 Socket 操作的固定步骤和抽象步骤:

cpp 复制代码
namespace SocketModule
{

    class TcpSocket;

    const static int DEFAULT_BACKLOG = 16;    // 监听队列默认长度
    const static int RECV_BUF_SIZE = 1024;    // 接收缓冲区大小
    const static int SOCKET_INVALID = -1;     // 无效套接字标识

    //最基础的基类
    class Socket
    {
    public:

        //虚析构函数
        virtual ~Socket()
        {}

        //要声明一系列网络通信所用到的函数
        //注意都得是虚函数,这样子才能重写,实现多态

        //使用socket函数创建套接字的函数:
        //不用返回套接字,套接字直接就在子类中成为子类的成员变量
        virtual void CreateSocketOrNot()=0;

        //使用bind函数将socket和程序绑定的函数
        //那么就需要传入端口号,至于ip地址就不用了,因为客户端不用显式bind,在发消息的时候
        //OS就会自动给客户端bind绑定上随机端口
        //而服务端虽然要显式bind,但是ip地址也是设为INADDR_ANY去增大接收范围
        //但是服务端需要绑定指定端口号,这样子客户端才能找的到服务端
        virtual void BindSocketOrNot(const uint16_t port)=0;

        //关闭自身套接字的函数
        //那么我们知道,套接字是存放在子类中的成员变量的,而要是外界想关闭套接字的话
        //是没办法直接访问到子类的套接字成员变量的
        //所以我们就得提供一个接口用于外界调用去close套接字,避免内存泄露
        virtual void CloseSocket()=0;

        //使用listen函数去实现监听的函数,用于服务端
        //那么由于listen函数需要传入参数去指定监听队列要有多少
        //所以我们该函数也需要传入该参数,虽然我们也会设置缺省值!!!
        virtual void ListenSocketOrNot(const int backlog=DEFAULT_BACKLOG)=0;

        //使用accept函数去让服务端接收客户端发起的链接函数
        //那么我们知道,accept函数是不需要什么外界传入的参数的
        //它就是会返回accept所创建的新的socket套接字用于一对一的沟通交流
        //但是我们又想把这一些都都封装起来,不想让外界可以直接修改
        //所以,我们可以把accept函数封装为返回TcpSocket类对象指针
        //毕竟TcpSocket类里面是可以存储socketfd套接字文件描述符的
        //那么我们返回TcpSocket类对象指针,其实就是创建一个新的TcpSocket类对象指针
        //然后我们将accept函数的返回值丢给返回的新的TcpSocket类对象指针
        //如此一来就实现了解耦封装
        virtual std::shared_ptr<TcpSocket> AcceptSocketOrNot()=0;

        //使用connect函数去进行链接的封装函数
        //那么我们知道,connect函数是需要外界传入要进行链接的对方主机的ip地址和端口号
        //所以我们要设置参数哦
        //返回值依旧是不需要
        //同样的,connect函数是客户端使用的,
        //所以客户端也就只能调用TcpSocket类中的创建socket、connect等函数
        //是不能调用accept、listen函数的
        //我们本文件只负责把这些函数都封装起来
        //但是使用还是需要使用者自己规范
        //比如客户端要做的就是创建socket,然后connect
        //本质上就是调用该类的CreateSocketOrNot、ConnectSocketOrNot函数
        virtual void ConnectSocketOrNot(const std::string& ip,const uint16_t port)=0;

        //客户、服务端要用的向服务、客户端发送信息的函数的封装,即对send函数的封装
        //那么肯定需要传入要发送的字符串吧,所以要设置字符串形参
        //那么我们还得知道发送信息有木有成功,木有就得再发送
        //总不能终止通信了吧,所以要把返回值设置为bool
        //至于send函数需要的网络套接字,不就是在客户端所创建的TcpSocket类中吗
        //直接传调用该函数的类的成员变量即可
        //那么客户端在调用他所创建的TcpSocket类的CreateSocketOrNot、ConnectSocketOrNot函数之后,
        //自然就是调用他所创建的TcpSocket类中的发送信息的函数
        //因为客户端的socket套接字就存储在它所创建的TcpSocket类中!!!
        //至于服务端要发送信息给客户端,就肯定是要用accept之后的和每个客户端一对一的socket套接字文件描述符
        //所以服务端调用send函数的就是要用服务端调用了AcceptSocketOrNot锁返回的TcpSocket对象指针中的send函数!!!
        //因为accept函数返回的和每个客户端一对一的socket套接字文件描述符就是存储在它返回的TcpSocket对象指针中
        //依旧是那句话:我们本文件只负责把这些函数都封装起来,但是使用还是需要使用者自己规范
        virtual bool Send(const std::string& send_message)=0;

        //封装服务端、客户端接收信息的函数,其实也就是对recv函数的封装
        //那么服务端我们要把至于守护进程,所以是用不到recv函数的
        //而客户端是肯定要接收信息的,所以它就需要recv函数
        //那么同样的,你要接收信息,那么你就得传入你要接收信息的字符串吧
        //然后我把recv获取到字符串+到你所传入的字符串里
        //这样子你就能获取到recv函数所获取的信息了
        //那么我们还得知道接收信息有木有成功,木有就得再接收
        //总不能终止通信了吧,所以要把返回值设置为int
        //返回recv函数的返回值,由外界去判断是什么情况,我们这里没必要做的太具体!!!
        //可不要再问我客户端的socket套接字在哪里了,去看看我对Send函数的解析吧
        virtual int Recv(std::string& recv_message)=0;

        //提供给外界可以一次性调用的接口
        //服务端一键创建listen套接字的函数
        //那么就是先创建套接字,然后bind,然后listen
        void BulidListenSocket(const uint16_t port,const int backlog=DEFAULT_BACKLOG)
        {
            CreateSocketOrNot();
            BindSocketOrNot(port);
            ListenSocketOrNot(backlog);
        }

        //客户端一键connect的函数
        //那么就是先创建socket套接字,然后connect
        void BulidConnectSocket(const std::string& ip,const uint16_t port)
        {
            CreateSocketOrNot();
            ConnectSocketOrNot(ip,port);
        }

        //使用这些函数的前提都是服务/客户端创建基类对象/指针
        //然后将TcpSocket类对象/指针赋值给基类,然后再去调用这些函数,从而实现多态
        //也能直接调用TcpSocket里面的成员函数
        //因为在基类中都虚函数了,所以不用担心切片
    };

在 Socket 抽象基类中,我们不仅声明了所有核心操作的抽象接口,还设计了两个关键的 "模板方法"------BulidListenSocketBulidConnectSocket,这两个方法正是模板方法模式的核心体现。

BulidListenSocket是为服务端设计的一键式创建监听套接字的模板方法,它按固定顺序调用CreateSocketOrNot(创建套接字)、BindSocketOrNot(绑定端口)、ListenSocketOrNot(启动监听)三个抽象步骤,将服务端创建监听套接字的完整流程固化下来。外界使用者无需关心这三个步骤的调用顺序,只需调用这一个方法,传入端口号和监听队列长度即可完成监听套接字的创建,从根本上避免了因步骤顺序错误导致的编程错误。

BulidConnectSocket则是为客户端设计的一键式连接服务端的模板方法,它按固定顺序调用CreateSocketOrNot(创建套接字)和ConnectSocketOrNot(发起连接)两个抽象步骤,同样将客户端连接服务端的核心流程固化,使用者只需传入服务端的 IP 地址和端口号,就能完成连接的建立,极大降低了客户端编程的复杂度。

需要特别说明的是,Socket 基类将所有核心操作声明为纯虚函数(抽象步骤),这是因为 TCP 和 UDP 的套接字操作虽然流程框架一致,但具体实现细节存在差异(比如 UDP 无需 listen 和 accept,TCP 则必须)。将这些操作抽象为纯虚函数,既保证了流程的统一性,又为不同协议的套接字子类(如 TcpSocket、UdpSocket)预留了自定义实现的空间,完美契合模板方法模式 "定框架、填细节" 的核心思想。

此外,Socket 基类的析构函数被声明为虚析构函数,这是 C++ 多态编程的基本规范 ------ 当使用基类指针指向子类对象时,虚析构函数能确保子类的析构函数被正确调用,避免内存泄漏。这一设计细节看似微小,却是保证整个套接字封装体系健壮性的关键。

3. 子类设计:TcpSocket

TcpSocket 作为 Socket 基类的具体实现子类,负责完成 TCP 协议下所有抽象步骤的具体实现。它的核心设计思路是将 TCP 套接字的文件描述符(socketfd)封装为私有成员变量,对外隐藏底层实现细节,仅通过基类暴露的接口提供服务,既保证了数据的安全性,又符合 "封装" 的面向对象设计原则。

(1)构造与析构:资源的安全管理

TcpSocket 设计了两个构造函数:无参构造函数和带套接字文件描述符的构造函数。

无参构造函数将私有成员_socketfd初始化为SOCKET_INVALID(-1),表示初始状态下套接字无效,这是一种防御性编程策略 ------ 通过初始值明确套接字的状态,避免后续操作中使用未初始化的无效套接字。

带套接字文件描述符的构造函数则专门为AcceptSocketOrNot方法设计:当服务端调用 accept 函数接收到客户端连接时,会返回一个新的套接字文件描述符(用于与该客户端一对一通信),此时通过这个构造函数可以快速创建一个包含该新套接字的 TcpSocket 对象,让服务端能够通过这个对象与客户端进行数据交互。

TcpSocket 的析构函数会检查_socketfd的有效性:如果套接字有效(≥0),则调用系统的 close 函数关闭套接字,并将_socketfd置为无效。这一设计实现了套接字资源的自动释放,使用者无需手动调用 close 函数,大大降低了因忘记关闭套接字导致的资源泄漏风险。

cpp 复制代码
    public:
        //无参构造函数
        TcpSocket()
        :_socketfd(SOCKET_INVALID)
        {}

        //传入套接字的构造函数
        //用于accept函数返回一个新的TcpSocket对象指针
        //里面放accept函数的返回值,也就是新的一对一的socket套接字文件描述符
        TcpSocket(int socketfd)
        :_socketfd(socketfd)
        {}

        //析构函数
        //将本类的socket套接字文件描述符close
        ~TcpSocket()
        {
            if(_socketfd>=0)
            {
                ::close(_socketfd);
            }
        }
(2)CreateSocketOrNot:TCP 套接字的创建

CreateSocketOrNot方法是对系统 socket 函数的封装,其核心逻辑分为三步:

第一步,调用系统 socket 函数创建 TCP 套接字:指定地址族为 AF_INET(IPv4)、套接字类型为 SOCK_STREAM(流式套接字,对应 TCP 协议)、协议为 0(由系统自动匹配对应类型的默认协议)。如果创建失败,会输出错误信息并退出程序(实际项目中可改为返回错误码,增强容错性)。

第二步,设置端口复用:这是服务端开发中必不可少的配置。当服务端程序意外退出后,端口可能会处于 TIME_WAIT 状态,此时新启动的程序无法立即绑定该端口。通过 setsockopt 函数设置 SO_REUSEADDR 选项,可以让端口快速复用,避免服务端重启时的端口占用问题。

第三步,将创建成功的套接字文件描述符赋值给 TcpSocket 的私有成员_socketfd,完成套接字的创建。

需要注意的是,该方法中所有系统函数调用前都添加了作用域解析符(::),这是为了明确调用的是系统全局函数,而非类的成员函数,避免因函数名冲突导致的编译错误 ------ 这是 C++ 编程中容易被忽略但至关重要的细节。

cpp 复制代码
       //创建套接字
        //即调用socket函数
        //那么其实这个函数所得出来的socket套接字在服务端中也是拿来做listen套接字
        virtual void CreateSocketOrNot() override
        {
            int socketfd=::socket(AF_INET,SOCK_STREAM,0);
            if(socketfd<0)
            {
                std::cerr<<"socket failed!!!"<<strerror(errno)<<std::endl;
                exit(SOCKET_ERR);//在Common头文件中
            }

            //端口复用(服务端必须)
            int opt = 1;
            if (::setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) 
            {
                std::cerr << "setsockopt SO_REUSEADDR failed: " << strerror(errno) << std::endl;
            }

            //创建socket套接字成功,赋值给成员变量
            _socketfd=socketfd;

            //至此创建套接字成功,但是要注意是在当前这个类变量中,有着socket套接字
        }
(3)BindSocketOrNot:TCP 套接字的绑定

BindSocketOrNot方法是对系统 bind 函数的封装,仅服务端需要调用,核心逻辑如下:

首先,初始化 sockaddr_in 结构体:该结构体用于存储套接字的地址信息,包括地址族(AF_INET)、端口号(需要转换为网络字节序,通过 htons 函数)、IP 地址(设置为 INADDR_ANY,即监听所有网卡的 IP 地址,扩大服务端的接收范围)。

然后,调用系统 bind 函数将套接字与指定的端口号绑定:如果绑定失败,输出错误信息并退出程序。

这里需要解释两个关键的字节序转换函数:htons(主机字节序转网络字节序,短整型)和 htonl(主机字节序转网络字节序,长整型)。由于不同主机的字节序(大端 / 小端)不同,网络传输必须遵循统一的网络字节序(大端),因此端口号和 IP 地址在绑定前必须完成字节序转换,否则会出现地址解析错误。

此外,方法的参数仅设计为端口号,而未包含 IP 地址,这是因为客户端无需显式绑定 IP 和端口(系统会在发送数据时自动分配),而服务端绑定 INADDR_ANY 即可监听所有 IP,无需指定具体 IP------ 这一设计简化了接口,符合 "最小接口原则"。

cpp 复制代码
        //绑定bind函数,也就服务端需要
        virtual void BindSocketOrNot(const uint16_t port) override
        {
            //老样子,socketaddr_in结构体
            struct sockaddr_in server;
            ::memset(&server,0,sizeof(server));
            server.sin_family=AF_INET;
            server.sin_port=::htons(port);
            //不用担心我们把形参设置为const是不是不可以这样子,htons只是读取它的值,不会进行修改哦
            server.sin_addr.s_addr=::htonl(INADDR_ANY);

            socklen_t len=sizeof(server);

            int ret_bind=::bind(_socketfd,(struct sockaddr*)(&server),len);
            if(ret_bind<0)
            {
                std::cerr<<"bind failed!!!"<<strerror(errno)<<std::endl;
                exit(BIND_ERR);//在Common头文件中
            }
        }
(4)CloseSocket:套接字的安全关闭

CloseSocket方法是对系统 close 函数的封装,核心逻辑是:先检查_socketfd的有效性,若有效则调用 close 函数关闭套接字,并将_socketfd重置为SOCKET_INVALID

_socketfd重置为无效是关键细节:如果仅关闭套接字而不重置状态,后续可能会重复调用 close 函数(比如析构函数再次调用),而重复关闭无效的套接字会导致系统调用错误。通过重置状态,能有效避免这类问题,保证方法的幂等性(多次调用与一次调用的效果一致)。

cpp 复制代码
        //关闭自身套接字的函数
        //那么我们知道,套接字是存放在子类中的成员变量的,而要是外界想关闭套接字的话
        //是没办法直接访问到子类的套接字成员变量的
        //所以我们就得提供一个接口用于外界调用去close套接字,避免内存泄露
        virtual void CloseSocket() override
        {
            if(_socketfd>=0)
            {
                ::close(_socketfd);
                _socketfd = SOCKET_INVALID; //标记为无效,避免重复关闭
            }
            //就这么简单
        }
(5)ListenSocketOrNot:TCP 套接字的监听

ListenSocketOrNot方法是对系统 listen 函数的封装,仅服务端需要调用,核心逻辑是:

调用系统 listen 函数,将套接字设置为监听状态,参数包括套接字文件描述符和监听队列长度(默认值为 DEFAULT_BACKLOG=16)。监听队列用于存储已完成三次握手但尚未被 accept 的客户端连接,队列长度过小将导致客户端连接被拒绝,过大则会浪费系统资源,16 是兼顾性能和资源的默认值。

需要强调的是,调用该方法的 TcpSocket 对象对应的是服务端的 "监听套接字",而非与客户端通信的 "通信套接字"------ 监听套接字仅负责接收客户端的连接请求,不参与实际的数据交互,这是 TCP 服务端编程的核心概念。

cpp 复制代码
        //使用listen函数去实现监听的函数,用于服务端
        //那么由于listen函数需要传入参数去指定监听队列要有多少
        //所以我们该函数也需要传入该参数,虽然我们也会设置缺省值!!!
        virtual void ListenSocketOrNot(const int backlog=DEFAULT_BACKLOG) override
        {
            //传入本类变量的socket套接字成员变量即可
            //虽然我们本类也要实现accept函数的封装
            //但是listen函数和accept函数的调用是分开的,且accept函数是返回新的TcpSocket类对象指针
            //我们是不可能用accept函数返回的TcpSocket对象指针去执行listen函数的
            //所以调用了listen函数的TcpSocket的本质其实是listensocketfd哦!!!
            int ret_listen=::listen(_socketfd,backlog);
            if(ret_listen<0)
            {
                std::cerr<<"listen failed!!!"<<strerror(errno)<<std::endl;
                exit(LISTEN_ERR);//在Common头文件中
            }

        }
(6)AcceptSocketOrNot:客户端连接的接收

AcceptSocketOrNot方法是对系统 accept 函数的封装,仅服务端需要调用,核心逻辑如下:

首先,初始化 sockaddr_in 结构体:用于存储发起连接的客户端的地址信息(IP 和端口)。

然后,调用系统 accept 函数:该函数会阻塞等待客户端的连接请求,当接收到请求后,返回一个新的套接字文件描述符(通信套接字),用于与该客户端进行一对一的数据交互。

最后,创建一个包含该通信套接字的 TcpSocket 智能指针(std::shared_ptr)并返回:使用智能指针而非裸指针,是为了实现套接字资源的自动管理 ------ 当智能指针的引用计数为 0 时,会自动调用 TcpSocket 的析构函数关闭套接字,彻底避免内存泄漏。

这里需要解释为什么返回智能指针:如果返回裸指针,使用者需要手动管理内存,容易出现忘记释放的情况;而 std::shared_ptr 是共享智能指针,能自动跟踪对象的引用计数,保证资源在不再使用时被释放,是 C++ 现代编程中管理动态资源的首选方式。

cpp 复制代码
        //使用accept函数去让服务端接收客户端发起的链接函数
        //那么我们知道,accept函数是不需要什么外界传入的参数的
        //它就是会返回accept所创建的新的socket套接字用于一对一的沟通交流
        //但是我们又想把这一些都都封装起来,不想让外界可以直接修改
        //所以,我们可以把accept函数封装为返回TcpSocket类对象指针
        //毕竟TcpSocket类里面是可以存储socketfd套接字文件描述符的
        //那么我们返回TcpSocket类对象指针,其实就是创建一个新的TcpSocket类对象指针
        //然后我们将accept函数的返回值丢给返回的新的TcpSocket类对象指针
        //如此一来就实现了解耦封装
        virtual std::shared_ptr<TcpSocket> AcceptSocketOrNot() override
        {
            //老样子,创建socketaddr_in结构体
            //用于存储和服务端链接的客户端信息
            struct sockaddr_in client;
            ::memset(&client,0,sizeof(client));
            socklen_t len=sizeof(client);

            int accept_socketfd=::accept(_socketfd,(struct sockaddr*)(&client),&len);
            if(accept_socketfd<0)
            {
                std::cerr<<"accept failed!!!"<<strerror(errno)<<std::endl;
                exit(ACCEPT_ERR);//在Common头文件中
            }

            //接下来就返回存有accept函数返回值,也就是新的套接字文件描述符的TcpSocket指针
            //所以我们要把新的套接字文件描述符传入新的TcpSocket指针中
            //这也是为什么我们上面要实现单参数的TcpSocket构造函数的原因
            std::shared_ptr<TcpSocket> ret(std::make_shared<TcpSocket>(accept_socketfd));
            return ret;
        }
(7)ConnectSocketOrNot:客户端连接的发起

ConnectSocketOrNot方法是对系统 connect 函数的封装,仅客户端需要调用,核心逻辑分为四步:

第一步,前置检查:确保套接字有效(_socketfd≥0),若无效则输出错误信息并退出程序 ------ 这是防御性编程,避免调用 connect 函数时传入无效套接字。

第二步,初始化 sockaddr_in 结构体:存储服务端的地址信息,包括地址族(AF_INET)、端口号(转换为网络字节序)、IP 地址(通过 inet_pton 函数转换为网络字节序,该函数是线程安全的,优于传统的 inet_addr 函数)。

第三步,IP 地址转换:调用 inet_pton 函数将字符串形式的 IP 地址(如 "127.0.0.1")转换为网络字节序的二进制形式。该函数会检查 IP 地址的有效性,若转换失败(如 IP 格式错误),则输出错误信息并退出程序。

第四步,调用系统 connect 函数:向服务端发起 TCP 连接(三次握手),若连接失败则输出错误信息并退出程序。

需要注意的是,connect 函数的阻塞特性:默认情况下,connect 函数会阻塞直到连接建立或失败(超时时间由系统决定)。在实际项目中,可将套接字设置为非阻塞模式,结合 select/poll/epoll 实现异步连接,提升程序的并发性能 ------ 这是扩展优化的方向,基础封装中暂不涉及,避免增加复杂度。

cpp 复制代码
        //使用connect函数去进行链接的封装函数
        //那么我们知道,connect函数是需要外界传入要进行链接的对方主机的ip地址和端口号
        //所以我们要设置参数哦
        //返回值依旧是不需要
        //同样的,connect函数是客户端使用的,
        //所以客户端也就只能调用TcpSocket类中的创建socket、connect等函数
        //是不能调用accept、listen函数的
        //我们本文件只负责把这些函数都封装起来
        //但是使用还是需要使用者自己规范
        //比如客户端要做的就是创建socket,然后connect
        //本质上就是调用该类的CreateSocketOrNot、ConnectSocketOrNot函数
        virtual void ConnectSocketOrNot(const std::string& ip,const uint16_t port) override
        {
            // 前置检查:套接字必须有效
            if (_socketfd < 0) 
            {
                std::cerr << "connect failed: socket not created" << std::endl;
                exit(CONNECT_ERR);
            }

            //老样子,创建socketaddr_in结构体
            //用于存储客户端要链接的服务端的信息,然后才能connect
            struct sockaddr_in server;
            ::memset(&server,0,sizeof(server));
            server.sin_family=AF_INET; 
            server.sin_port=::htons(port);
            //使用线程安全函数去将ip地址转换为网络字节序
            int ret_inet_pton=::inet_pton(AF_INET,ip.c_str(),&(server.sin_addr));
            if (ret_inet_pton == 0) 
            {
                std::cerr << "inet_pton failed: invalid IP address (" << ip << ")" << std::endl;
                exit(CONNECT_ERR);
            } 
            else if (ret_inet_pton == -1) 
            {
                std::cerr << "inet_pton failed: " << strerror(errno) << std::endl;
                exit(CONNECT_ERR);
            }

            socklen_t len=sizeof(server);

            int ret_connect=::connect(_socketfd,(struct sockaddr*)(&server),len);
            if(ret_connect<0)
            {
                std::cerr<<"connect failed!!!"<<strerror(errno)<<std::endl;
                exit(CONNECT_ERR);//在Common头文件中
            }
 
        }
(8)Send:TCP 数据的发送

Send方法是对系统 send 函数的封装,服务端和客户端都需要调用,核心设计思路是解决 TCP "粘包" 和 "部分发送" 问题,核心逻辑如下:

第一步,空字符串检查:如果待发送的字符串为空,直接返回成功,避免无意义的系统调用。

第二步,循环发送:由于 TCP 是面向字节流的协议,send 函数可能无法一次性发送所有数据(即 "部分发送"),因此需要通过循环确保所有数据都被发送。具体来说,定义sentlen变量记录已发送的字节数,每次调用 send 函数发送 "未发送的剩余部分"(通过指针偏移实现),并将发送成功的字节数累加到sentlen,直到sentlen等于待发送字符串的长度。

第三步,错误处理:如果 send 函数调用失败(返回值 < 0),输出错误信息并返回 false;若所有数据发送完成,返回 true。

这里需要解释指针偏移的原理:字符串的底层是字符数组,data+sentlen表示从已发送字节的下一个位置开始发送,确保数据不重复、不遗漏。例如,若待发送字符串长度为 10,第一次发送了 4 个字节,第二次则从第 5 个字节开始发送剩余的 6 个字节。

此外,Send 方法的返回值设计为 bool 类型,目的是让使用者快速判断发送是否成功,而无需关心具体的发送字节数 ------ 这符合 "接口易用性" 原则,将复杂的底层细节封装起来,对外提供简单清晰的接口。

cpp 复制代码
        //客户、服务端要用的向服务、客户端发送信息的函数的封装,即对send函数的封装
        //那么肯定需要传入要发送的字符串吧,所以要设置字符串形参
        //那么我们还得知道发送信息有木有成功,木有就得再发送
        //总不能终止通信了吧,所以要把返回值设置为bool
        //至于send函数需要的网络套接字,不就是在客户端所创建的TcpSocket类中吗
        //直接传调用该函数的类的成员变量即可
        //那么客户端在调用他所创建的TcpSocket类的CreateSocketOrNot、ConnectSocketOrNot函数之后,
        //自然就是调用他所创建的TcpSocket类中的发送信息的函数
        //因为客户端的socket套接字就存储在它所创建的TcpSocket类中!!!
        //至于服务端要发送信息给客户端,就肯定是要用accept之后的和每个客户端一对一的socket套接字文件描述符
        //所以服务端调用send函数的就是要用服务端调用了AcceptSocketOrNot所返回的TcpSocket对象指针中的send函数!!!
        //因为accept函数返回的和每个客户端一对一的socket套接字文件描述符就是存储在它返回的TcpSocket对象指针中
        //依旧是那句话:我们本文件只负责把这些函数都封装起来,但是使用还是需要使用者自己规范
        virtual bool Send(const std::string& send_message) override
        {       
            // 空字符串直接返回成功,避免无意义循环
            if (send_message.empty()) 
            {
                return true;
            }
        
            int sentlen=0;//定义send函数成功发送的字节数
            //要是小于我们要它发送的字符串字节数的话,就一直while循环发送

            size_t send_message_len=send_message.size();//总待发送长度

            const char* data = send_message.c_str();//要发送的信息的字符串形式

            //至于send函数需要的网络套接字,不就是在客户端所创建的TcpSocket类中吗
            //直接传调用该函数的类的成员变量即可
            while(sentlen<send_message_len)
            {
                //发送 "未发送的剩余部分"(data+sentlen,长度send_message_len-sentlen)
                //因为要是只发送了一部分,那么我们下次发送肯定就得从上次发送结束的地方再去往后发送
                //不可能每次都发送全部内容吧!!!
                //那么我们怎么知道上次发送结束的地方呢???不就是用sentlen记录着吗
                //我们用data加上sentlen,不就是定位到了上次发送结束
                //(指针可以++哦,一个字符的大小是一个字节哦)
                //那么我们发送的字符长度就应该是send_message_len-sentlen才对
                //这一点要注意一下
                int ret_send=::send(_socketfd,data+sentlen,send_message_len-sentlen,0);
                if(ret_send<0)
                {
                    std::cerr<<"send failed!!!"<<strerror(errno)<<std::endl;
                    return false;//发送信息失败,自然返回fasle
                }
                
                //然后我们要给send函数成功发送的字节数的sentlen加上send函数的返回值
                //因为要是一次没有发送完,那么我们得加上上次发送成功的字节数!!!
                sentlen+=ret_send;
            }

            return true;//发送信息成功,自然返回true
        }
(9)Recv:TCP 数据的接收

Recv方法是对系统 recv 函数的封装,服务端(通过通信套接字)和客户端都需要调用,核心设计思路是解决 TCP "粘包" 和 "部分接收" 问题,核心逻辑如下:

第一步,初始化接收缓冲区:创建固定大小的字符数组(RECV_BUF_SIZE=1024),用于存储接收到的数据。

第二步,调用 recv 函数:从套接字中读取数据到缓冲区,返回值为实际读取的字节数。需要注意的是,recv 函数的第三个参数设置为sizeof(buf)-1,这是为了预留一个字节的空间存储字符串终止符('\0'),避免缓冲区溢出。

第三步,数据拼接:如果 recv 函数返回值 > 0(成功接收数据),则将缓冲区中的数据追加到传入的字符串参数中。这里使用 string 的 append 方法而非直接赋值,是为了处理 "部分接收" 问题 ------ 如果一次 recv 未能读取完整的数据包,后续的 recv 调用可以将新数据追加到字符串末尾,最终拼接出完整的数据包。

第四步,返回 recv 函数的返回值:让使用者根据返回值判断接收状态(>0 表示成功接收字节数,0 表示对方关闭连接,<0 表示接收失败),保留了底层的灵活性。

需要重点解释 TCP "粘包" 问题:由于 TCP 是面向字节流的协议,数据会被拆分成多个数据包发送,也可能将多个小数据包合并为一个发送,因此接收端无法保证一次 recv 就能读取到完整的数据包。通过将数据追加到字符串中,使用者可以在外部根据数据包的边界(如固定长度、分隔符)解析完整数据,这是解决粘包问题的基础思路。

cpp 复制代码
        //封装服务端、客户端接收信息的函数,其实也就是对recv函数的封装
        //那么服务端我们要把至于守护进程,所以是用不到recv函数的
        //而客户端是肯定要接收信息的,所以它就需要recv函数
        //那么同样的,你要接收信息,那么你就得传入你要接收信息的字符串吧
        //然后我把recv获取到字符串+到你所传入的字符串里
        //这样子你就能获取到recv函数所获取的信息了
        //那么我们还得知道接收信息有木有成功,木有就得再接收
        //总不能终止通信了吧,所以要把返回值设置为int
        //返回recv函数的返回值,由外界去判断是什么情况,我们这里没必要做的太具体!!!
        //可不要再问我客户端的socket套接字在哪里了,去看看我对Send函数的解析吧
        virtual int Recv(std::string& recv_message) override
        {
            char buf[RECV_BUF_SIZE]={0}; 
            ssize_t ret_recv=::recv(_socketfd,buf,sizeof(buf)-1,0);
            // if(ret_recv<0)
            // {
            //     std::cerr<<"recv failed!!!"<<strerror(errno)<<std::endl;
            //     return false;//接收信息失败,自然返回fasle
            // }
            // else if(ret_recv==0)//发送信息的对方关闭了
            // {
            //     std::cerr<<"a stream socket peer has performed an orderly shutdown(服务端关闭)"<<std::endl;
            //     return false;
            // }
            // else
            // {
            //     buf[ret_recv]='\0';//在字符串末尾添加字符串终止符,符合C语言字符串
            // }

            //接收信息成功
            //recv_message=recv_message + static_cast<std::string>(buf);

            //使用string里的append函数去将收到的字符串追加进去
            if(ret_recv>0)//成功了才去追加
            {
                recv_message.append(buf,ret_recv);
            }   

            //为什么要累加???
            //因为TCP是面向字节流,因为我们怕收到的数据少于一个完整报头!!!
            //所以,要是少了的话,我们就得去再recv序列化字符串!!!
            //可是之前recv的呢???
            //加上啊!!!!!!!!!!!!!!!!!!!!!

            //返回recv函数的返回值,由外界去判断是什么情况
            return ret_recv;
        }

4. 模板方法模式在 TCP Socket 封装中的核心价值

将模板方法模式应用于 TCP Socket 封装后,整个网络编程体系呈现出以下核心优势:

(1)流程标准化,降低出错概率

Socket 基类通过BulidListenSocketBulidConnectSocket两个模板方法,将服务端和客户端的核心流程固化:

  • 服务端:创建套接字 → 绑定端口 → 启动监听(一键完成)
  • 客户端:创建套接字 → 发起连接(一键完成)

使用者无需记忆复杂的步骤顺序,只需调用对应的模板方法即可,从根本上避免了因步骤颠倒(如先监听后绑定)导致的编程错误。

(2)代码复用,减少冗余

TCP 套接字的创建、关闭、数据发送 / 接收等核心操作,仅在 TcpSocket 子类中实现一次,所有使用者都可以通过基类接口调用,避免了传统编程中重复编写这些代码的问题。例如,服务端和客户端都需要发送数据,只需调用Send方法即可,无需各自实现发送逻辑。

(3)封装底层细节,提升易用性

所有系统调用(socket、bind、listen 等)都被封装在类的内部,使用者无需了解底层的系统函数参数、字节序转换、错误处理等细节,只需关注业务逻辑(如发送什么数据、接收后如何处理)。例如,使用者无需手动调用 htons 转换端口号,也无需手动关闭套接字,这些都由封装层自动完成。

(4)扩展灵活,符合开闭原则

如果需要扩展 UDP 套接字封装,只需创建 UdpSocket 子类,继承 Socket 基类并实现对应的抽象步骤即可,无需修改基类的任何代码。例如,UdpSocket 只需实现CreateSocketOrNot(创建数据报套接字 SOCK_DGRAM)、BindSocketOrNotSendRecv等方法,无需改变基类的模板方法 ------ 这完全符合 "开闭原则",对扩展开放,对修改关闭。

(5)多态特性,便于统一管理

通过基类指针(Socket*)指向子类对象(TcpSocket),可以实现对不同协议套接字的统一管理。例如,一个网络框架可以通过 Socket 基类指针管理所有的 TCP/UDP 套接字,调用统一的CloseSocket方法关闭所有套接字,无需区分具体的协议类型。

5. 封装后的使用方式

(1)服务端使用方式
  1. 创建 Socket 基类指针,指向 TcpSocket 子类对象;
  2. 调用BulidListenSocket方法,传入端口号(如 8080),一键创建监听套接字;
  3. 循环调用AcceptSocketOrNot方法,接收客户端连接,获取通信套接字的智能指针;
  4. 通过通信套接字的智能指针调用Recv方法接收客户端数据,调用Send方法向客户端发送数据;
  5. 通信完成后,调用CloseSocket方法关闭套接字(或依赖智能指针自动释放)。
(2)客户端使用方式
  1. 创建 Socket 基类指针,指向 TcpSocket 子类对象;
  2. 调用BulidConnectSocket方法,传入服务端 IP(如 "127.0.0.1")和端口号(如 8080),一键连接服务端;
  3. 调用Send方法向服务端发送数据,调用Recv方法接收服务端数据;
  4. 通信完成后,调用CloseSocket方法关闭套接字。

可以看到,封装后的使用方式极其简洁,使用者只需关注业务逻辑,无需处理复杂的底层细节,大大提升了开发效率。

6. UDP 套接字封装的扩展思路(补充)

虽然本文的核心是 TCP 套接字封装,但模板方法模式同样适用于 UDP 套接字封装,其扩展思路如下:

(1)UdpSocket 子类的核心差异

UDP 是无连接的数据包协议,与 TCP 的核心差异体现在:

  • 创建套接字时,类型为 SOCK_DGRAM(数据报套接字);
  • 无需 listen 和 accept 操作;
  • 发送数据时需要指定目标地址(sendto 函数),接收数据时需要获取发送方地址(recvfrom 函数)。
(2)扩展步骤
  1. 创建 UdpSocket 子类,继承 Socket 基类;
  2. 实现CreateSocketOrNot方法:创建 SOCK_DGRAM 类型的套接字;
  3. 实现BindSocketOrNot方法:与 TCP 一致,绑定端口号(客户端可选,服务端必须);
  4. 重写SendRecv方法:分别封装 sendto 和 recvfrom 函数;
  5. ListenSocketOrNotAcceptSocketOrNot方法实现为空(或抛出异常),因为 UDP 无需监听和接收连接。

通过这种方式,UDP 套接字可以复用 Socket 基类的模板方法(如BulidListenSocket,仅服务端绑定端口),同时自定义 UDP 特有的实现细节,充分体现了模板方法模式的灵活性。

模板方法模式与 Socket 封装的深度总结

1. 模板方法模式的核心本质

模板方法模式的本质是 "流程固化 + 细节定制":

  • 固化:将算法的核心流程(如套接字创建、连接、数据交互)抽象到基类中,保证所有子类遵循统一的流程;
  • 定制:将流程中的可变细节(如 TCP/UDP 的套接字创建、数据发送方式)延迟到子类中实现,满足不同场景的需求。

它不是 "限制",而是 "规范"------ 在保证核心流程正确的前提下,给予子类足够的灵活性,这也是设计模式的核心价值:在混乱中建立秩序,在约束中保留灵活。

2. Socket 封装的核心价值

基于模板方法模式的 Socket 封装,解决了传统网络编程的三大痛点:

  • 流程混乱:通过模板方法固化核心流程,避免步骤错误;
  • 代码冗余:将重复的系统调用封装为类方法,提升代码复用率;
  • 门槛过高:隐藏底层细节,降低网络编程的学习和使用门槛。

对于新手开发者来说,无需深入理解系统调用、字节序、粘包等底层概念,就能快速实现 TCP 客户端 / 服务端的开发;对于资深开发者来说,封装后的代码更易维护、更易扩展,符合大型项目的工程化要求。

3. 设计模式的落地思考

模板方法模式的应用,给我们带来了关于设计模式落地的重要启示:

  • 设计模式不是 "炫技",而是解决实际问题的工具:本文中模板方法模式的应用,核心是解决 Socket 编程的流程混乱和代码冗余问题,而非单纯为了使用设计模式而使用;
  • 面向抽象编程,而非面向具体实现:通过基类定义接口,子类实现细节,提升代码的灵活性和可维护性;
  • 封装细节,暴露接口:好的封装应该隐藏复杂的底层细节,对外提供简单、清晰、易用的接口,这是所有设计模式的共同追求。

完整示例代码:

cpp 复制代码
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <functional>
#include <cerrno>
#include <cstring>
#include <sys/wait.h>
//#include "Log.hpp"
#include "Common.hpp"
//#include "InetAddr.hpp" //可以利用该头文件做优化,但这里向突出最原始的,所以没有使用,在翻译这里,我们就进行使用简化


// OK,那么在本文件中,我们就秉承着我们之前的封装大法
// 对TCPsocket等等系列的函数都去进行一个封装,方便于外部的使用,大大提高效率
// 但是嘞,在这里我们换一种方式来进行封装
// 使用模版方法类方式,和策略模式有那么一丢丢类似
// 本质也是利用多态!!!
// 想想看,TCP和UDP都是用类似的一些函数,socket创建套接字,bind绑定,liseten监听,accept接受等等
// 虽然UDP没有后面那么些函数,但是我们是主用TCP的呀
// 那么对于UDP和TCP而言,它们都要进行socket创建套接字,bind绑定
// 那么难道要在TCP和UDP中把两个都各自实现???
// 这未免代码冗余
// 所以我们可以把这些相同的步骤,都放在基类中实现,而不用的步骤,再在各自的子类中去实现
// 大大简洁
// 其实在这里我们直接封装也可以,但使用模版方法类方式会更显得我们专业
// 具体要如何实现呢???

// 1. 模式结构
// 模板方法模式包含以下核心角色:
// 抽象类(Abstract Class):定义算法的骨架(模板方法),
// 声明抽象步骤(primitive operations),由子类实现。
// 可以提供默认实现的钩子方法(hook operations),子类可选覆盖。
// 通常将模板方法声明为 final,防止子类改变算法结构。

// 具体类(Concrete Class):实现抽象步骤,提供具体的业务逻辑,可覆盖钩子方法,调整算法的行为。

// 2. 关键概念
// 模板方法(Template Method):定义算法的流程,按固定顺序调用各个步骤。
// 抽象步骤(Primitive Operations):算法中必须由子类实现的步骤(函数),
// 即基类只负责申明这一些抽象步骤(函数),可用于多态时调用
// 钩子方法(Hook Operations):算法中可选的步骤或扩展点,通常有默认实现。
// 不变部分:在抽象类中实现,所有子类共享。
// 可变部分:在子类中实现,不同子类有不同实现。


/*************************************************************************
 * 模板方法模式核心理解:
 * 核心比喻:就像老师给的作文模板 ------ 开头、结尾、结构是固定的,中间的具体内容由你自己填。
 * 专业定义:模板方法模式是一种「行为型设计模式」,核心是:定义一个算法的骨架(固定流程),
 *          把其中可变的步骤延迟到子类中实现。这样子类可以在不改变整体流程的前提下,只修改细节步骤。
 *************************************************************************/

/*************************************************************************
 * 一、为什么需要模板方法模式?
 * 假设要做两款饮品:泡咖啡和泡奶茶,制作流程对比:
 * 泡咖啡                泡奶茶
 * 1. 烧开水(100℃)     1. 烧开水(100℃)
 * 2. 冲泡咖啡粉         2. 冲泡奶茶粉
 * 3. 倒入杯子           3. 倒入杯子
 * 4. 加方糖             4. 加珍珠 / 椰果
 * 
 * 关键发现:步骤 1、3 是完全一样的(固定流程),步骤 2、4 是不同的(可变细节)。
 * 无模板方法的问题:需要给咖啡和奶茶各写一套完整流程,重复写「烧开水」「倒杯子」的代码;
 * 有模板方法的优势:把固定流程抽出来,只让子类实现可变步骤 ------ 既省代码,又保证流程不会乱
 *                  (比如不会有人先加方糖再烧开水)。
 *************************************************************************/

/*************************************************************************
 * 二、模板方法模式的核心角色(通俗版)
 * 不用记专业术语,记住 3 个关键部分:
 * 角色                通俗解释
 * 抽象模板类          定义「固定流程」(比如饮品制作的 4 步),包含:
 *                     1. 模板方法(固定流程)
 *                     2. 固定步骤(比如烧开水)
 *                     3. 抽象步骤(留给子类填的细节)
 * 具体实现类          继承抽象模板类,只实现「抽象步骤」(比如咖啡实现 "冲泡咖啡粉",
 *                     奶茶实现 "冲泡奶茶粉")
 * 模板方法(核心)    抽象类中固定流程的方法,通常加 final 防止子类改流程(比如不让人乱改 
 *                     "先烧开水" 的顺序)
 *************************************************************************/

/*************************************************************************
 * 三、实战例子:用代码实现「泡饮品」
 * 极简C++版本,核心逻辑一目了然
 *************************************************************************/


/*************************************************************************
 * 第一步:定义抽象模板类(饮品制作模板)
 * 把固定流程(烧开水、倒杯子)写死,可变步骤(冲泡、加调料)留空让子类实现
 *************************************************************************/
// 抽象模板类:饮品制作模板
// class DrinkTemplate {
// public:
//     // 模板方法:固定制作流程(final防止子类修改流程)
//     final void makeDrink() {
//         boilWater();   // 固定步骤1:烧开水
//         brew();        // 抽象步骤2:冲泡原料(子类实现)
//         pourInCup();   // 固定步骤3:倒入杯子
//         addCondiment();// 抽象步骤4:加调料(子类实现)
//     }

//     // 固定步骤:烧开水(所有饮品都一样)
//     void boilWater() {
//         cout << "1. 烧开水(100℃)" << endl;
//     }

//     // 固定步骤:倒入杯子(所有饮品都一样)
//     void pourInCup() {
//         cout << "3. 将饮品倒入杯子" << endl;
//     }

//     // 抽象步骤:冲泡原料(子类自己实现)
//     virtual void brew() = 0;

//     // 抽象步骤:加调料(子类自己实现)
//     virtual void addCondiment() = 0;
// };

/*************************************************************************
 * 第二步:实现具体子类(咖啡 / 奶茶)
 * 只需要补全「抽象步骤」,不用管整体流程
 *************************************************************************/
// 具体实现类1:咖啡
// class Coffee : public DrinkTemplate {
// public:
//     // 实现"冲泡原料":咖啡粉
//     void brew() override {
//         cout << "2. 冲泡咖啡粉" << endl;
//     }

//     // 实现"加调料":方糖
//     void addCondiment() override {
//         cout << "4. 加方糖" << endl;
//     }
// };

// 具体实现类2:奶茶
// class MilkTea : public DrinkTemplate {
// public:
//     // 实现"冲泡原料":奶茶粉
//     void brew() override {
//         cout << "2. 冲泡奶茶粉" << endl;
//     }

//     // 实现"加调料":珍珠+椰果
//     void addCondiment() override {
//         cout << "4. 加珍珠和椰果" << endl;
//     }
// };

/*************************************************************************
 * 第三步:使用模板方法
 * 调用时只需要创建具体子类,执行模板方法即可
 *************************************************************************/
// int main() {
//     // 泡一杯咖啡
//     DrinkTemplate* coffee = new Coffee();
//     cout << "===== 制作咖啡 =====" << endl;
//     coffee->makeDrink();

//     // 泡一杯奶茶
//     DrinkTemplate* milkTea = new MilkTea();
//     cout << "\n===== 制作奶茶 =====" << endl;
//     milkTea->makeDrink();

//     delete coffee;
//     delete milkTea;
//     return 0;
// }

/*************************************************************************
 * 运行结果:
 * ===== 制作咖啡 =====
 * 1. 烧开水(100℃)
 * 2. 冲泡咖啡粉
 * 3. 将饮品倒入杯子
 * 4. 加方糖
 * 
 * ===== 制作奶茶 =====
 * 1. 烧开水(100℃)
 * 2. 冲泡奶茶粉
 * 3. 将饮品倒入杯子
 * 4. 加珍珠和椰果
 *************************************************************************/

/*************************************************************************
 * 四、模板方法模式的核心好处(为什么要用?)
 * 1. 代码复用:固定步骤(烧开水、倒杯子)只写一次,子类不用重复写,减少冗余;
 * 2. 流程统一:所有子类都遵循相同的算法流程,避免混乱(比如不会有人先加调料再烧开水);
 * 3. 扩展方便:新增饮品(比如泡红茶),只需要新建子类,实现brew()和addCondiment()即可,
 *              不用改模板类(符合「开闭原则」);
 * 4. 控制流程:模板方法用final修饰,子类只能改细节,不能改整体流程,保证核心逻辑不被破坏。
 *************************************************************************/

/*************************************************************************
 * 五、常见适用场景(哪里能用到?)
 * 模板方法模式在实际开发中非常常见,比如:
 * 1. 框架生命周期:Spring 的 Bean 生命周期(固定:初始化→使用→销毁,初始化细节由你写);
 * 2. 报表生成:固定流程(表头→填充数据→生成文件),填充数据的逻辑由不同报表子类实现;
 * 3. 支付流程:固定流程(验证参数→调用接口→处理结果),不同支付方式(微信 / 支付宝)只改「调用接口」步骤;
 * 4. 游戏角色技能:固定流程(释放前摇→技能效果→释放后摇),不同技能只改「技能效果」。
 *************************************************************************/

/*************************************************************************
 * 六、总结
 * 模板方法模式的本质就是:定框架、填细节。
 * 把不变的「流程骨架」抽成模板,保证一致性;
 * 把变化的「细节步骤」留给子类,保证灵活性。
 * 就像你做手工模型:说明书(模板)固定了拼接顺序,你只需要按顺序把不同零件(子类细节)
 * 拼上去就行,既不会拼错顺序,又能做出不同样式的模型。
 *************************************************************************/

// 我们可以创建一个最基础的基类,那么这个基类要抽象出一系列的虚函数,这些也就是抽象步骤
//那么这些虚函数就是要在子类中去具体的重写实现,父类只负责声明
//那么这个时候就有问题了,为什么要在父类中去声明了,然后再去子类中去重载呢??
//不能直接子类吗???
//其实这是因为我们要把基类的部分接口去开放给外界用户使用,而这些函数就可能要调用多个子类重写的虚函数,
//比如TCP子类要重写实现listen、socket、accept、connect等函数
//那么我们在基类中可以就开放一个buildlistensocket的函数,而这个函数就是连续执行socket、listen函数
//这样子就不需要外界自己去调用子类实现的socket、listen函数,可以用这一个函数接口直接实现
//极大的便利了用户的使用,也使得我们将底层隐藏起来
//除此之外,我们把socket形成的套接字放在子类中去作为它的成员变量
//由此我们也可以去通过子类去访问到socket套接字,同时外界无法修改,大大提高安全性
//那么我们知道,模版方法类方式的本质其实是多态
//外界就创建一个基类指针可以去接收子类的指针
//然后再去调用基类指针中build等等函数,然后build函数中调用的那些重写虚函数就会自动更替为
//该基类指针所接受的子类的中的那些重写虚函数,由此实现多态,
//同时也可以通过该基类指针去调用子类和基类都重写的函数

//下面我们就按照上面的思路来实现一下模版方法类方式去封装TCPsocket


namespace SocketModule
{

    class TcpSocket;

    const static int DEFAULT_BACKLOG = 16;    // 监听队列默认长度
    const static int RECV_BUF_SIZE = 1024;    // 接收缓冲区大小
    const static int SOCKET_INVALID = -1;     // 无效套接字标识

    //最基础的基类
    class Socket
    {
    public:

        //虚析构函数
        virtual ~Socket()
        {}

        //要声明一系列网络通信所用到的函数
        //注意都得是虚函数,这样子才能重写,实现多态

        //使用socket函数创建套接字的函数:
        //不用返回套接字,套接字直接就在子类中成为子类的成员变量
        virtual void CreateSocketOrNot()=0;

        //使用bind函数将socket和程序绑定的函数
        //那么就需要传入端口号,至于ip地址就不用了,因为客户端不用显式bind,在发消息的时候
        //OS就会自动给客户端bind绑定上随机端口
        //而服务端虽然要显式bind,但是ip地址也是设为INADDR_ANY去增大接收范围
        //但是服务端需要绑定指定端口号,这样子客户端才能找的到服务端
        virtual void BindSocketOrNot(const uint16_t port)=0;

        //关闭自身套接字的函数
        //那么我们知道,套接字是存放在子类中的成员变量的,而要是外界想关闭套接字的话
        //是没办法直接访问到子类的套接字成员变量的
        //所以我们就得提供一个接口用于外界调用去close套接字,避免内存泄露
        virtual void CloseSocket()=0;

        //使用listen函数去实现监听的函数,用于服务端
        //那么由于listen函数需要传入参数去指定监听队列要有多少
        //所以我们该函数也需要传入该参数,虽然我们也会设置缺省值!!!
        virtual void ListenSocketOrNot(const int backlog=DEFAULT_BACKLOG)=0;

        //使用accept函数去让服务端接收客户端发起的链接函数
        //那么我们知道,accept函数是不需要什么外界传入的参数的
        //它就是会返回accept所创建的新的socket套接字用于一对一的沟通交流
        //但是我们又想把这一些都都封装起来,不想让外界可以直接修改
        //所以,我们可以把accept函数封装为返回TcpSocket类对象指针
        //毕竟TcpSocket类里面是可以存储socketfd套接字文件描述符的
        //那么我们返回TcpSocket类对象指针,其实就是创建一个新的TcpSocket类对象指针
        //然后我们将accept函数的返回值丢给返回的新的TcpSocket类对象指针
        //如此一来就实现了解耦封装
        virtual std::shared_ptr<TcpSocket> AcceptSocketOrNot()=0;

        //使用connect函数去进行链接的封装函数
        //那么我们知道,connect函数是需要外界传入要进行链接的对方主机的ip地址和端口号
        //所以我们要设置参数哦
        //返回值依旧是不需要
        //同样的,connect函数是客户端使用的,
        //所以客户端也就只能调用TcpSocket类中的创建socket、connect等函数
        //是不能调用accept、listen函数的
        //我们本文件只负责把这些函数都封装起来
        //但是使用还是需要使用者自己规范
        //比如客户端要做的就是创建socket,然后connect
        //本质上就是调用该类的CreateSocketOrNot、ConnectSocketOrNot函数
        virtual void ConnectSocketOrNot(const std::string& ip,const uint16_t port)=0;

        //客户、服务端要用的向服务、客户端发送信息的函数的封装,即对send函数的封装
        //那么肯定需要传入要发送的字符串吧,所以要设置字符串形参
        //那么我们还得知道发送信息有木有成功,木有就得再发送
        //总不能终止通信了吧,所以要把返回值设置为bool
        //至于send函数需要的网络套接字,不就是在客户端所创建的TcpSocket类中吗
        //直接传调用该函数的类的成员变量即可
        //那么客户端在调用他所创建的TcpSocket类的CreateSocketOrNot、ConnectSocketOrNot函数之后,
        //自然就是调用他所创建的TcpSocket类中的发送信息的函数
        //因为客户端的socket套接字就存储在它所创建的TcpSocket类中!!!
        //至于服务端要发送信息给客户端,就肯定是要用accept之后的和每个客户端一对一的socket套接字文件描述符
        //所以服务端调用send函数的就是要用服务端调用了AcceptSocketOrNot锁返回的TcpSocket对象指针中的send函数!!!
        //因为accept函数返回的和每个客户端一对一的socket套接字文件描述符就是存储在它返回的TcpSocket对象指针中
        //依旧是那句话:我们本文件只负责把这些函数都封装起来,但是使用还是需要使用者自己规范
        virtual bool Send(const std::string& send_message)=0;

        //封装服务端、客户端接收信息的函数,其实也就是对recv函数的封装
        //那么服务端我们要把至于守护进程,所以是用不到recv函数的
        //而客户端是肯定要接收信息的,所以它就需要recv函数
        //那么同样的,你要接收信息,那么你就得传入你要接收信息的字符串吧
        //然后我把recv获取到字符串+到你所传入的字符串里
        //这样子你就能获取到recv函数所获取的信息了
        //那么我们还得知道接收信息有木有成功,木有就得再接收
        //总不能终止通信了吧,所以要把返回值设置为int
        //返回recv函数的返回值,由外界去判断是什么情况,我们这里没必要做的太具体!!!
        //可不要再问我客户端的socket套接字在哪里了,去看看我对Send函数的解析吧
        virtual int Recv(std::string& recv_message)=0;

        //提供给外界可以一次性调用的接口
        //服务端一键创建listen套接字的函数
        //那么就是先创建套接字,然后bind,然后listen
        void BulidListenSocket(const uint16_t port,const int backlog=DEFAULT_BACKLOG)
        {
            CreateSocketOrNot();
            BindSocketOrNot(port);
            ListenSocketOrNot(backlog);
        }

        //客户端一键connect的函数
        //那么就是先创建socket套接字,然后connect
        void BulidConnectSocket(const std::string& ip,const uint16_t port)
        {
            CreateSocketOrNot();
            ConnectSocketOrNot(ip,port);
        }

        //使用这些函数的前提都是服务/客户端创建基类对象/指针
        //然后将TcpSocket类对象/指针赋值给基类,然后再去调用这些函数,从而实现多态
        //也能直接调用TcpSocket里面的成员函数
        //因为在基类中都虚函数了,所以不用担心切片
    };

    //在这里统一说明一下,我们下面之所以在调用的函数前面加::,
    //是为了让编译器知道,我调用的函数是系统的函数,而不是我们自己可能实现的同名函数
    //避免编译器混淆报错!!!

    //TcpSocket子类,要重写listen、connect等函数
    class TcpSocket:public Socket
    {
    public:
        //无参构造函数
        TcpSocket()
        :_socketfd(SOCKET_INVALID)
        {}

        //传入套接字的构造函数
        //用于accept函数返回一个新的TcpSocket对象指针
        //里面放accept函数的返回值,也就是新的一对一的socket套接字文件描述符
        TcpSocket(int socketfd)
        :_socketfd(socketfd)
        {}

        //析构函数
        //将本类的socket套接字文件描述符close
        ~TcpSocket()
        {
            if(_socketfd>=0)
            {
                ::close(_socketfd);
            }
        }

        //创建套接字
        //即调用socket函数
        //那么其实这个函数所得出来的socket套接字在服务端中也是拿来做listen套接字
        virtual void CreateSocketOrNot() override
        {
            int socketfd=::socket(AF_INET,SOCK_STREAM,0);
            if(socketfd<0)
            {
                std::cerr<<"socket failed!!!"<<strerror(errno)<<std::endl;
                exit(SOCKET_ERR);//在Common头文件中
            }

            //端口复用(服务端必须)
            int opt = 1;
            if (::setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) 
            {
                std::cerr << "setsockopt SO_REUSEADDR failed: " << strerror(errno) << std::endl;
            }

            //创建socket套接字成功,赋值给成员变量
            _socketfd=socketfd;

            //至此创建套接字成功,但是要注意是在当前这个类变量中,有着socket套接字
        }

        //绑定bind函数,也就服务端需要
        virtual void BindSocketOrNot(const uint16_t port) override
        {
            //老样子,socketaddr_in结构体
            struct sockaddr_in server;
            ::memset(&server,0,sizeof(server));
            server.sin_family=AF_INET;
            server.sin_port=::htons(port);
            //不用担心我们把形参设置为const是不是不可以这样子,htons只是读取它的值,不会进行修改哦
            server.sin_addr.s_addr=::htonl(INADDR_ANY);

            socklen_t len=sizeof(server);

            int ret_bind=::bind(_socketfd,(struct sockaddr*)(&server),len);
            if(ret_bind<0)
            {
                std::cerr<<"bind failed!!!"<<strerror(errno)<<std::endl;
                exit(BIND_ERR);//在Common头文件中
            }
        }

        //关闭自身套接字的函数
        //那么我们知道,套接字是存放在子类中的成员变量的,而要是外界想关闭套接字的话
        //是没办法直接访问到子类的套接字成员变量的
        //所以我们就得提供一个接口用于外界调用去close套接字,避免内存泄露
        virtual void CloseSocket() override
        {
            if(_socketfd>=0)
            {
                ::close(_socketfd);
                _socketfd = SOCKET_INVALID; //标记为无效,避免重复关闭
            }
            //就这么简单
        }

        //使用listen函数去实现监听的函数,用于服务端
        //那么由于listen函数需要传入参数去指定监听队列要有多少
        //所以我们该函数也需要传入该参数,虽然我们也会设置缺省值!!!
        virtual void ListenSocketOrNot(const int backlog=DEFAULT_BACKLOG) override
        {
            //传入本类变量的socket套接字成员变量即可
            //虽然我们本类也要实现accept函数的封装
            //但是listen函数和accept函数的调用是分开的,且accept函数是返回新的TcpSocket类对象指针
            //我们是不可能用accept函数返回的TcpSocket对象指针去执行listen函数的
            //所以调用了listen函数的TcpSocket的本质其实是listensocketfd哦!!!
            int ret_listen=::listen(_socketfd,backlog);
            if(ret_listen<0)
            {
                std::cerr<<"listen failed!!!"<<strerror(errno)<<std::endl;
                exit(LISTEN_ERR);//在Common头文件中
            }

        }

        //使用accept函数去让服务端接收客户端发起的链接函数
        //那么我们知道,accept函数是不需要什么外界传入的参数的
        //它就是会返回accept所创建的新的socket套接字用于一对一的沟通交流
        //但是我们又想把这一些都都封装起来,不想让外界可以直接修改
        //所以,我们可以把accept函数封装为返回TcpSocket类对象指针
        //毕竟TcpSocket类里面是可以存储socketfd套接字文件描述符的
        //那么我们返回TcpSocket类对象指针,其实就是创建一个新的TcpSocket类对象指针
        //然后我们将accept函数的返回值丢给返回的新的TcpSocket类对象指针
        //如此一来就实现了解耦封装
        virtual std::shared_ptr<TcpSocket> AcceptSocketOrNot() override
        {
            //老样子,创建socketaddr_in结构体
            //用于存储和服务端链接的客户端信息
            struct sockaddr_in client;
            ::memset(&client,0,sizeof(client));
            socklen_t len=sizeof(client);

            int accept_socketfd=::accept(_socketfd,(struct sockaddr*)(&client),&len);
            if(accept_socketfd<0)
            {
                std::cerr<<"accept failed!!!"<<strerror(errno)<<std::endl;
                exit(ACCEPT_ERR);//在Common头文件中
            }

            //接下来就返回存有accept函数返回值,也就是新的套接字文件描述符的TcpSocket指针
            //所以我们要把新的套接字文件描述符传入新的TcpSocket指针中
            //这也是为什么我们上面要实现单参数的TcpSocket构造函数的原因
            std::shared_ptr<TcpSocket> ret(std::make_shared<TcpSocket>(accept_socketfd));
            return ret;
        }

        //使用connect函数去进行链接的封装函数
        //那么我们知道,connect函数是需要外界传入要进行链接的对方主机的ip地址和端口号
        //所以我们要设置参数哦
        //返回值依旧是不需要
        //同样的,connect函数是客户端使用的,
        //所以客户端也就只能调用TcpSocket类中的创建socket、connect等函数
        //是不能调用accept、listen函数的
        //我们本文件只负责把这些函数都封装起来
        //但是使用还是需要使用者自己规范
        //比如客户端要做的就是创建socket,然后connect
        //本质上就是调用该类的CreateSocketOrNot、ConnectSocketOrNot函数
        virtual void ConnectSocketOrNot(const std::string& ip,const uint16_t port) override
        {
            // 前置检查:套接字必须有效
            if (_socketfd < 0) 
            {
                std::cerr << "connect failed: socket not created" << std::endl;
                exit(CONNECT_ERR);
            }

            //老样子,创建socketaddr_in结构体
            //用于存储客户端要链接的服务端的信息,然后才能connect
            struct sockaddr_in server;
            ::memset(&server,0,sizeof(server));
            server.sin_family=AF_INET; 
            server.sin_port=::htons(port);
            //使用线程安全函数去将ip地址转换为网络字节序
            int ret_inet_pton=::inet_pton(AF_INET,ip.c_str(),&(server.sin_addr));
            if (ret_inet_pton == 0) 
            {
                std::cerr << "inet_pton failed: invalid IP address (" << ip << ")" << std::endl;
                exit(CONNECT_ERR);
            } 
            else if (ret_inet_pton == -1) 
            {
                std::cerr << "inet_pton failed: " << strerror(errno) << std::endl;
                exit(CONNECT_ERR);
            }

            socklen_t len=sizeof(server);

            int ret_connect=::connect(_socketfd,(struct sockaddr*)(&server),len);
            if(ret_connect<0)
            {
                std::cerr<<"connect failed!!!"<<strerror(errno)<<std::endl;
                exit(CONNECT_ERR);//在Common头文件中
            }
 
        }


        //客户、服务端要用的向服务、客户端发送信息的函数的封装,即对send函数的封装
        //那么肯定需要传入要发送的字符串吧,所以要设置字符串形参
        //那么我们还得知道发送信息有木有成功,木有就得再发送
        //总不能终止通信了吧,所以要把返回值设置为bool
        //至于send函数需要的网络套接字,不就是在客户端所创建的TcpSocket类中吗
        //直接传调用该函数的类的成员变量即可
        //那么客户端在调用他所创建的TcpSocket类的CreateSocketOrNot、ConnectSocketOrNot函数之后,
        //自然就是调用他所创建的TcpSocket类中的发送信息的函数
        //因为客户端的socket套接字就存储在它所创建的TcpSocket类中!!!
        //至于服务端要发送信息给客户端,就肯定是要用accept之后的和每个客户端一对一的socket套接字文件描述符
        //所以服务端调用send函数的就是要用服务端调用了AcceptSocketOrNot所返回的TcpSocket对象指针中的send函数!!!
        //因为accept函数返回的和每个客户端一对一的socket套接字文件描述符就是存储在它返回的TcpSocket对象指针中
        //依旧是那句话:我们本文件只负责把这些函数都封装起来,但是使用还是需要使用者自己规范
        virtual bool Send(const std::string& send_message) override
        {       
            // 空字符串直接返回成功,避免无意义循环
            if (send_message.empty()) 
            {
                return true;
            }
        
            int sentlen=0;//定义send函数成功发送的字节数
            //要是小于我们要它发送的字符串字节数的话,就一直while循环发送

            size_t send_message_len=send_message.size();//总待发送长度

            const char* data = send_message.c_str();//要发送的信息的字符串形式

            //至于send函数需要的网络套接字,不就是在客户端所创建的TcpSocket类中吗
            //直接传调用该函数的类的成员变量即可
            while(sentlen<send_message_len)
            {
                //发送 "未发送的剩余部分"(data+sentlen,长度send_message_len-sentlen)
                //因为要是只发送了一部分,那么我们下次发送肯定就得从上次发送结束的地方再去往后发送
                //不可能每次都发送全部内容吧!!!
                //那么我们怎么知道上次发送结束的地方呢???不就是用sentlen记录着吗
                //我们用data加上sentlen,不就是定位到了上次发送结束
                //(指针可以++哦,一个字符的大小是一个字节哦)
                //那么我们发送的字符长度就应该是send_message_len-sentlen才对
                //这一点要注意一下
                int ret_send=::send(_socketfd,data+sentlen,send_message_len-sentlen,0);
                if(ret_send<0)
                {
                    std::cerr<<"send failed!!!"<<strerror(errno)<<std::endl;
                    return false;//发送信息失败,自然返回fasle
                }
                
                //然后我们要给send函数成功发送的字节数的sentlen加上send函数的返回值
                //因为要是一次没有发送完,那么我们得加上上次发送成功的字节数!!!
                sentlen+=ret_send;
            }

            return true;//发送信息成功,自然返回true
        }

        //封装服务端、客户端接收信息的函数,其实也就是对recv函数的封装
        //那么服务端我们要把至于守护进程,所以是用不到recv函数的
        //而客户端是肯定要接收信息的,所以它就需要recv函数
        //那么同样的,你要接收信息,那么你就得传入你要接收信息的字符串吧
        //然后我把recv获取到字符串+到你所传入的字符串里
        //这样子你就能获取到recv函数所获取的信息了
        //那么我们还得知道接收信息有木有成功,木有就得再接收
        //总不能终止通信了吧,所以要把返回值设置为int
        //返回recv函数的返回值,由外界去判断是什么情况,我们这里没必要做的太具体!!!
        //可不要再问我客户端的socket套接字在哪里了,去看看我对Send函数的解析吧
        virtual int Recv(std::string& recv_message) override
        {
            char buf[RECV_BUF_SIZE]={0}; 
            ssize_t ret_recv=::recv(_socketfd,buf,sizeof(buf)-1,0);
            // if(ret_recv<0)
            // {
            //     std::cerr<<"recv failed!!!"<<strerror(errno)<<std::endl;
            //     return false;//接收信息失败,自然返回fasle
            // }
            // else if(ret_recv==0)//发送信息的对方关闭了
            // {
            //     std::cerr<<"a stream socket peer has performed an orderly shutdown(服务端关闭)"<<std::endl;
            //     return false;
            // }
            // else
            // {
            //     buf[ret_recv]='\0';//在字符串末尾添加字符串终止符,符合C语言字符串
            // }

            //接收信息成功
            //recv_message=recv_message + static_cast<std::string>(buf);

            //使用string里的append函数去将收到的字符串追加进去
            if(ret_recv>0)//成功了才去追加
            {
                recv_message.append(buf,ret_recv);
            }   

            //为什么要累加???
            //因为TCP是面向字节流,因为我们怕收到的数据少于一个完整报头!!!
            //所以,要是少了的话,我们就得去再recv序列化字符串!!!
            //可是之前recv的呢???
            //加上啊!!!!!!!!!!!!!!!!!!!!!

            //返回recv函数的返回值,由外界去判断是什么情况
            return ret_recv;
        }

    private:
        int _socketfd;
        //套接字,这个套接字即可以是socket的套接字,
        //也可以是accept的套接字,更可以是listen、send、recv的套接字
        //就是看哪个函数调用,然后哪个函数将该值赋值
        //因为不同的使用端,就是创建不同的TcpSocket类对象
        //所以对象里面存储的_socketfd肯定也是不一样的
        //比如客户端创建的TcpSocket类对象里面就是存储要用来send、recv、connect的
        //而服务端创建的TcpSocket类对象里面就是存储要用来listen、accept的
        //而accpet返回的新的TcpSocket类对象里面就是存储服务端要用来recv和send的
        //我们本文件只负责把这些函数都封装起来,但是使用还是需要使用者自己规范
        //由此才能实现完美的解耦封装!!!
    };
}

//至于UdpSocke的封装,由于我们其实很少使用UDP,所以在这里就不实现了,并且它会比TcpSocket封装要简单!!!

结语:模板方法模式与 Socket 封装的深度总结

亲爱的读者朋友们,当我们以模板方法模式为蓝图,亲手构建起这个基于 C++ 的 TCP Socket 封装体系时,我们不仅完成了一次对网络编程技术的深入探索,更完成了一次对设计模式魅力的深刻体悟。

回望这一路,我们从对模板方法模式的理论认知,到将其精准应用于 Socket 的封装实践,每一步都凝聚着对设计原则的尊重与对工程实践的思考。我们将 TCP Socket 编程中那些看似独立的系统调用 ------socketbindlistenacceptsendrecv等,通过抽象基类与具体子类的分层设计,构建出一个既统一又灵活的架构。

模板方法模式的核心本质

模板方法模式的本质是 "流程固化 + 细节定制"。在我们的 Socket 封装中,抽象基类Socket扮演了模板方法的角色,它定义了算法的整体流程:服务端的CreateSocketOrNotBindSocketOrNotListenSocketOrNot,以及客户端的CreateSocketOrNotConnectSocketOrNot。这些固定的步骤如同制作饮品时的 "烧开水" 和 "倒入杯子",确保了所有 Socket 操作都遵循统一的规范。

而子类TcpSocket则负责实现那些可变的细节:服务端的AcceptSocketOrNotListenSocketOrNot,以及客户端的ConnectSocketOrNot。这种设计让我们能够在不改变整体流程的前提下,灵活地为不同场景提供定制化的实现。就像我们可以制作咖啡和奶茶,它们都遵循相同的基本流程,但原料和调料各不相同。

Socket 封装的核心价值

基于模板方法模式的 Socket 封装,为我们解决了传统网络编程中的三大核心痛点:

流程标准化 :通过BuildListenSocketBuildConnectSocket两个模板方法,我们将服务端和客户端的核心流程固化下来。服务端只需调用BuildListenSocket传入端口号,就能一键完成套接字的创建、绑定和监听;客户端只需调用BuildConnectSocket传入服务端 IP 和端口号,就能建立连接。这种设计避免了因步骤顺序错误导致的编程错误,大大降低了开发难度。

代码复用 :TCP Socket 的创建、关闭、数据发送和接收等核心操作,在TcpSocket子类中只实现一次,所有使用者都可以通过基类接口调用,避免了传统编程中重复编写这些代码的问题。例如,服务端和客户端都需要发送数据,只需调用Send方法即可,无需各自实现发送逻辑。

封装底层细节:所有系统调用都被封装在类的内部,使用者无需了解底层的系统函数参数、字节序转换、错误处理等细节,只需关注业务逻辑。这种封装让我们能够:

  • 隐藏套接字文件描述符的管理细节,通过私有成员变量_socketfd实现数据安全
  • 自动处理字节序转换(htonshtonlinet_pton
  • 统一处理错误输出和退出机制
  • 提供智能指针std::shared_ptr<TcpSocket>实现资源的自动管理

设计模式的落地思考

在我们的 Socket 封装实践中,模板方法模式的应用给我们带来了关于设计模式落地的重要启示:

设计模式不是 "炫技",而是解决实际问题的工具。本文中模板方法模式的应用,核心是解决 Socket 编程的流程混乱和代码冗余问题,而非单纯为了使用设计模式而使用。当我们面临一个具体问题时,应该先分析问题的本质,再选择合适的设计模式,而不是为了使用模式而创造问题。

面向抽象编程,而非面向具体实现 。通过基类定义接口,子类实现细节,我们提升了代码的灵活性和可维护性。在我们的封装体系中,使用者通过Socket基类指针与具体的TcpSocket子类交互,这种多态特性让我们能够:

  • 统一管理不同类型的套接字
  • 轻松扩展 UDP 套接字的封装
  • 保持代码的一致性和可预测性

封装细节,暴露接口 。好的封装应该隐藏复杂的底层细节,对外提供简单、清晰、易用的接口。在我们的设计中,Socket基类暴露了SendRecvCloseSocket等简洁的接口,而将所有底层实现细节都封装在内部。这种设计让使用者能够专注于业务逻辑,而不是底层的网络编程细节。

希望大家持续努力,一步一步的向前进!!!诸君共勉!!!

相关推荐
An_s5 小时前
c++对接pdfium(一)win系统篇
开发语言·c++
众少成多积小致巨5 小时前
C++ 规范参考(上)
c++
ShirleyWang0126 小时前
让headlamp控制台能访问
linux·服务器·python·k8s·k3s
liuccn6 小时前
Linux 存储系统:LVM 与直接分区
linux·运维
Android研究员6 小时前
Android进阶之事件分发机制深度剖析
android·前端·面试
触底反弹6 小时前
🔥 AI 写代码总翻车?这套实战方法论救了我
人工智能·面试·程序员
风栖柳白杨6 小时前
【面试】AI算法工程师_空白自测版本
人工智能·算法·面试
xexpertS8 小时前
CI/CD 熔断机制:如何通过编排级熔断器提升开发者效率
java·linux·运维
带娃的IT创业者8 小时前
突破内存瓶颈:利用 Nvidia GPU 显存作为 Linux Swap 空间的深度实践
linux·运维·服务器·nvidia·swap·gpu显存
一拳一个呆瓜8 小时前
【STL】iostream 编程:使用提取运算符
c++·stl