【Linux网络】封装Socket
1. 模版方法模式模板方法模式是一种行为型设计模式它定义了一个算法的骨架将某些步骤延迟到子类中实现从而在不改变算法结构的情况下允许子类重新定义特定步骤。核心结构抽象类Abstract Class定义算法的框架模板方法并声明若干抽象方法或虚方法供子类实现。模板方法通常被声明为final以防止子类重写算法结构。具体子类Concrete Class实现抽象类中定义的抽象方法提供算法步骤的具体实现。C 实现示例以下是一个典型的模板方法模式示例以制作饮料为例代码语言javascriptAI代码解释#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那我们就可以抽象一个Socket的基类将创建套接字等需要的系统调用在基类中设为纯虚函数然后我们可以定义两个模板方法一个UDP的模板方法一个TCP的模板方法需要使用哪个传输层协议的网络服务就在主程序中调用哪个模板方法具体子类UDP或TCP服务端可以实现抽象类中虚函数的具体实现不过UDP相对TCP简单一点所以我们具体子类主要实现TCP服务器2.1 Socket基类基类主要就是所需要的系统调用设为纯虚函数然后定义一个TCP服务端所需要的系统调用的模板方法代码如下代码语言javascriptAI代码解释#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_ptrSocket 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类型的话只能使用原始的系统调用但我们已经封装了就尽量使用封装的系统调用这样虽然也行但是有点挫代码语言javascriptAI代码解释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_ptrSocket 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都可能 }; }对于创建绑定监听这三个必要的基本操作我们已经熟悉了不多说代码如下代码语言javascriptAI代码解释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代码语言javascriptAI代码解释std::shared_ptrSocket 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_sharedTcpSocket(fd); }注意我们这里只是实现虚函数将来是要在外部来调用但是我们需要知道是哪个客户端发送的信息可我们在定义时又不需要用到客户端的地址信息那我们就可以通过输出型参数将地址信息让外部可以拿到。不过我们是从网络中拿到的客户端地址信息所以就需要从网络字节序转为主机字节序那这步我们就可以在定义的时候来做。但是我们封装的InetAddr类只有构造的时候是将网络字节序转为主机字节序我们这里是输出型参数所以我们可以在InetAddr类中新增一个网络转主机的函数SetAddr通过参数来调用SetAddr我们在退出的时候最好还是需要将文件描述符关闭我们之前没有说这个这里提一下代码语言javascriptAI代码解释void Close() override { if (_sockfd 0) ::close(_sockfd); }然后就是读写数据tcp是面向字节流的所以我们上篇文章中选择使用read/write来读写数据这次我们介绍另一种tcp读写数据的系统调用recv系统调用recv 系统调用用于从一个已连接的面向连接的套接字如 SOCK_STREAM即 TCP 套接字或已绑定的无连接套接字如 SOCK_DGRAM即 UDP 套接字接收数据。它类似于 read 系统调用但提供了额外的 flags 参数来控制接收行为。代码语言javascriptAI代码解释#include sys/socket.h ssize_t recv(int sockfd, void *buf, size_t len, int flags);参数详解int sockfd描述 这是一个由 socket() 创建并经过 connect()对于客户端或 accept()对于服务器端处理后的套接字文件描述符。要求 套接字必须是已连接的对于 TCP或已绑定的对于 UDP。void *buf描述 这是一个指向内存缓冲区的指针用于存放接收到的数据。要求 应用程序必须确保这个缓冲区有足够的空间至少 len 字节来存放数据否则会导致内存越界引发未定义行为如程序崩溃。size_t len描述 指定缓冲区 buf 的最大容量即你希望一次最多接收多少字节的数据。注意 recv 最多只会向你返回 len 字节的数据即使对端发送了更多的数据。多余的数据会留在内核的套接字接收缓冲区中等待下一次 recv 调用。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 是判断对端是否已关闭连接的标准方法。代码如下代码语言javascriptAI代码解释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因为外部上层可能还没有将之前的数据拿完那这个时候就不能直接覆盖掉上次的数据所以特意buffersend系统调用send 系统调用用于向一个已连接的套接字如 TCP 套接字发送数据。它类似于 write 系统调用但提供了额外的 flags 参数来控制发送行为。注意对于无连接的套接字如 UDP通常使用 sendto 或 sendmsg因为它们允许指定目标地址。代码语言javascriptAI代码解释#include sys/socket.h ssize_t send(int sockfd, const void *buf, size_t len, int flags);参数详解i**nt sockfd**描述 这是一个由 socket() 创建并经过 connect()对于客户端或 accept()对于服务器端处理后的套接字文件描述符。要求 套接字必须是已连接的对于 TCP或已绑定的对于 UDP但通常用 sendto。const void *buf描述 这是一个指向内存缓冲区的指针该缓冲区包含要发送的数据。要求 应用程序必须确保这个缓冲区包含有效的数据并且至少有 len 字节。size_t len描述 指定要发送数据的字节数。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 以指示具体的错误类型。代码如下代码语言javascriptAI代码解释int Send(const std::string message) override { return send(_sockfd, message.c_str(), message.size(), 0); }这里我们不做过多介绍多路转接时会详细介绍接下来就是客户端发起连接Connect代码语言javascriptAI代码解释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即可代码语言javascriptAI代码解释#pragma once #include Socket.hpp #include memory #include sys/wait.h using namespace SocketModule; using namespace LogModule; using ioservice_t std::functionvoid(std::shared_ptrSocket, InetAddr); class TcpServer { public: TcpServer(uint16_t port, ioservice_t service) :_port(port), _listensockptr(std::make_uniqueTcpSocket()), _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_ptrTcpSocket _listensockptr; bool _isrunning; ioservice_t _service; };我们这里使用多进程分别接收连接和执行任务这里任务我们需要在上层去实现后面文章会详细介绍。后面文章我们会再谈协议然后自己来定义协议然后顶层封装一个任务通过我们自己定义的协议来完成序列化和反序列化让对端拿到我们的任务去处理所以客户端也放在后面实现
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2570708.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!