在前面几篇文章中,我们实现了Socket编程,也就是基于UDP和TCP进行了网络编程,通过这几次编程我们已经熟悉了Socket编程相关的系统调用,那么这篇文章我们就来使用模版方法模式封装一个Socket
模板方法模式是一种行为型设计模式,它定义了一个算法的骨架,将某些步骤延迟到子类中实现,从而在不改变算法结构的情况下允许子类重新定义特定步骤。
核心结构 抽象类(Abstract Class):定义算法的框架(模板方法),并声明若干抽象方法或虚方法供子类实现。模板方法通常被声明为final以防止子类重写算法结构。 具体子类(Concrete Class):实现抽象类中定义的抽象方法,提供算法步骤的具体实现。
C++ 实现示例 以下是一个典型的模板方法模式示例,以制作饮料为例:
#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;
}应用场景
优点与缺点
注意事项
此模式通过继承和多态实现算法的可变性与稳定性的平衡,是 C++ 中常用的设计模式之一。
不过UDP相对TCP简单一点,所以我们具体子类主要实现TCP服务器
基类主要就是所需要的系统调用设为纯虚函数,然后定义一个TCP服务端所需要的系统调用的模板方法
代码如下:
#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多态调用基类中的模板方法来完成创建套接字,绑定,监听等连接操作
这里我们设置两个构造函数,一个无参构造用于初始化listen套接字,一个用于将connect返回的文件描述符构造为套接字类型,而不是直接返回一个int类型的文件描述符,这样做的好处是,在后续使用该文件描述符时可以直接通过套接字来调用封装的函数,而如果是int类型的话,只能使用原始的系统调用,但我们已经封装了就尽量使用封装的系统调用,这样虽然也行但是有点挫
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都可能
};
}对于创建,绑定,监听这三个必要的基本操作,我们已经熟悉了,不多说,代码如下
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
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
我们在退出的时候最好还是需要将文件描述符关闭,我们之前没有说这个,这里提一下
void Close() override
{
if (_sockfd >= 0)
::close(_sockfd);
}然后就是读写数据,tcp是面向字节流的,所以我们上篇文章中选择使用read/write来读写数据,这次我们介绍另一种tcp读写数据的系统调用
recv 系统调用用于从一个已连接的面向连接的套接字(如 SOCK_STREAM,即 TCP 套接字)或已绑定的无连接套接字(如 SOCK_DGRAM,即 UDP 套接字)接收数据。
它类似于 read 系统调用,但提供了额外的 flags 参数来控制接收行为。
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);参数详解
int sockfdvoid *bufsize_t lenint flags返回值 recv 的返回值是理解其行为的关键:
代码如下:
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;
}注意:
这里我们同样也是需要从外部调用,如果外部要得到读取的缓冲区内容就需要通过输出型参数,而且输出型参数需要+=buffer,因为外部(上层)可能还没有将之前的数据拿完,那这个时候就不能直接覆盖掉上次的数据,所以特意+=buffer
send 系统调用用于向一个已连接的套接字(如 TCP 套接字)发送数据。它类似于 write 系统调用,但提供了额外的 flags 参数来控制发送行为。
注意:对于无连接的套接字(如 UDP),通常使用 sendto 或 sendmsg,因为它们允许指定目标地址。
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);参数详解
nt sockfd**const void *bufsize_t lenint flags返回值 send 的返回值是理解其行为的关键:
代码如下:
int Send(const std::string &message) override
{
return send(_sockfd, message.c_str(), message.size(), 0);
}这里我们不做过多介绍,多路转接时会详细介绍
接下来就是客户端发起连接Connect
int Connect(const std::string &server_ip, uint16_t port) override
{
InetAddr server(server_ip, port);
return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
}封装好之后就是使用封装的Socket来实现服务端,我们已经实现过了,这里就不再介绍了,只需要将原先的原生系统调用换成封装的Socket即可
#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;
};我们这里使用多进程分别接收连接和执行任务,这里任务我们需要在上层去实现,后面文章会详细介绍。
后面文章我们会再谈协议,然后自己来定义协议,然后顶层封装一个任务,通过我们自己定义的协议来完成序列化和反序列化,让对端拿到我们的任务去处理,所以客户端也放在后面实现