利用单例设计模式搭建服务器架构
开发项目时,先开发比较难的模块。好处是:①使得项目的进度具有可控性;②方便对接;③能够进行可行性的评估,提早暴露风险。
1.window网络编程中的套接字初始化
在window网络编程中的套接字初始化,如下代码是固定的,并且该代码只会被执行一次,在程序初始化以及程序被销毁时执行。其中,在windows系统中使用WSAStartup()函数初始化Winsock函数,接着就是网络编程的代码,如创建套接字socket、绑定bind、监听listen等完成之后,使用WSACleanup()函数释放Winsock资源。
cpp
WSADATA data;
WSAStartup(MAKEWORD(1, 1), &data)
网络编程代码...
WASCleanup();
在套接字初始化的代码中,以上代码是固定的,此时我们就需要考虑在面向对象编程中,是否有手段将这种只会被执行一次的代码(在程序初始化和程序销毁时执行)进一步优化。在C/C++语言中有静态变量,静态变量在首次调用时被初始化,在程序运行结束时被销毁。如果静态变量是全局的,会在main函数调用之前被初始化,在main函数返回之后被析构。
由前文内容可知,首先使用WSADATA data; WSAStartup(MAKEWORD(1, 1), &data);初始化套接字环境。由于该代码只执行一次,在程序初始化时执行WSAStartup,以及在程序销毁时执行WSACleanup()。此时,我们定义一个类CServerSocket,并在类中定义一个成员函数InitSocketEnv,用来初始化套接字环境。并将InitSocketEnv函数放如构造函数中,在构造函数中初始化socket环境,在析构函数中调用WSACleanup()函数释放资源。具体实现如下:
ServerSocket.h
cpp
#pragma once
#include "pch.h"
#include "framework.h"
class CServerSocket
{
public:
//在构造函数中初始化socket环境
CServerSocket()
{
if (InitSockEnv() == FALSE)
{
MessageBox(NULL, _T("无法初始化套接字环境,请检查网络设置!"), _T("初始化错误!"), MB_OK | MB_ICONERROR);
exit(0);
}
}
//在析构函数中确保WSACleanup()函数被调用。
///WSACleanup()是Windows网络编程中用于清理Winsock库资源的函数,它与WSAStartup()配对使用,通常在程序退出前调用。
~CServerSocket()
{
WSACleanup();
}
//初始化socket环境
BOOL InitSockEnv()
{
//定义一个WSADATA结构体变量data,用于接收WSAStartup()返回的Winsock库的详细信息(如版本、实现细节等)。
WSADATA data;
//WSAStartup()是Windows提供的初始化Winsock的函数,必须在任何socket操作之前调用。
//MAKEWORD(low, high)是一个宏,用于组合主版本号和次版本号(例如 MAKEWORD(1,1)表示 Winsock1.1)。
if (WSAStartup(MAKEWORD(1, 1), &data) != 0)
{
return FALSE;
}
return TRUE;
}
};
//声明一个外部定义的全局变量server,让多个文件共享同一个实例。
extern CServerSocket server;
WSAStartup()函数以及WSACleanup()函数详细解释如下图所示:
2.借助单例设计模式解决套接字可能被多次初始化的问题
假如我在main函数中再声明一个CServerSocket变量,那么就会调用两次构造函数和析构函数,就初始化了两次环境,并释放了两次资源,这就有问题了。如何避免随意拿extern类去声明多个对象?答案:使用单例模式。通过语法来保证这类情况的出现。
①首先将构造函数、拷贝构造函数、赋值运算符(=)的重载函数均设置为私有的,防止外部通过该三类函数去构造、拷贝构造、赋值构造多个对象;
②那么既然这些函数被设置成了私有,那么怎么来访问这些函数呢?使用静态函数的形式来访问。直接使用 类名::静态函数名 的形式访问该静态函数。**注意:**静态函数是没有this指针的,所以无法在类中直接访问类中的成员变量。那怎么办呢?将成员变量也设置为静态变量就OK了。
2.1 单线程环境下的单例设计模式介绍
本节的标题是单线程环境下的单例设计模式介绍,那么肯定还有多线程环境下的单例设计模式,后面再介绍。
单例模式的实现要点:
①构造函数和析构函数是私有的,不允许外部生成和释放该对象;
②静态成员变量和静态返回单例的成员函数;(静态成员变量就是需要创建的那个单例对象,静态返回单例的成员函数就是我们要返回单例对象的函数。)
③禁用拷贝构造函数和赋值构造函数。(这里还包括移动拷贝构造和移动拷贝赋值构造,即要禁用4个函数。)
单例模式要解决的问题:一个类只允许有一个实例,并且该实例需要在多处使用,比如日志对象、数据库连接池、线程池对象。只需要一个实例,能不能使用全局变量?不能。全局变量不能保证唯一性,并i企鹅全局变量的初始化不好控制,所以就用到了单例模式。
单例模式传统有两种解决办法,①懒汉模式;②饿汉模式。
懒汉模式:内存在被用到时再申请,不提前申请;
饿汉模式:内存要提前申请。
1)饿汉模式


2)懒汉模式


2.2 项目代码
使用单例设计模式设计windows环境初始化。


具体代码如下:
ServerSocket.h
cpp
class CServerSocket
{
public:
static CServerSocket*getInstance()
{
if (m_instance == NULL)//静态函数没有this指针,所以无法直接访问成员变量
{
m_instance = new CServerSocket();
}
return m_instance;
}
private:
CServerSocket& operator=(const CServerSocket& ss) {}
CServerSocket(const CServerSocket&ss) {}
//在构造函数中初始化socket环境
CServerSocket()
{
if (InitSockEnv() == FALSE)
{
MessageBox(NULL, _T("无法初始化套接字环境,请检查网络设置!"), _T("初始化错误!"), MB_OK | MB_ICONERROR);
exit(0);
}
}
~CServerSocket()
{
WSACleanup();
}
//1、初始化套接字环境
BOOL InitSockEnv()
{
//定义一个WSADATA结构体变量data,
//用于接收WSAStartup()返回的Winsock库的详细信息(如版本、实现细节等)。
WSADATA data;
if (WSAStartup(MAKEWORD(1, 1), &data) != 0)
{
return FALSE;
}
return TRUE;
}
static CServerSocket* m_instance;
};
ServerSocket.cpp
cpp
//静态成员变量m_instance类内声明、类外定义
CServerSocket* CServerSocket::m_instance = NULL;
//初始化pserver指针,指针pserver是全局变量
CServerSocket* pserver = CServerSocket::getInstance();
我们运行以上代码,发现首先调用getInstance()函数,但是析构函数没有被调用,由单例设计模式可知该情况是不允许发生的。那么析构函数为什么没有被调用呢?是因为没有人去调用析构函数。由上面的代码可知,我们通过静态函数getInstance()通过new操作拿到了一个CServerSocket类的实例m_instance。但是并没有delete,所以没有调用析构函数。
这里通过releaseInstance()函数来释放m_instance实例的资源,并通过CServerSocket类下的一个私有类CHelper调用getInstance()以及releaseInstance()函数。代码如下所示:
cpp
static void releaseInstance()
{
if (m_instance != NULL)
{
CServerSocket* tmp = m_instance;
m_instance = NULL;
delete tmp;
}
}
class CHelper
{
public:
CHelper()
{
CServerSocket::getInstance();
}
~CHelper()
{
CServerSocket::releaseInstance();
}
};
static CHelper m_helper;
cpp
CServerSocket::CHelper CServerSocket::m_helper;
那么定义CHelper类的作用是什么?我们声明一个CHelper对象实例m_helper,并在ServerSocket.cpp文件中实现该实例。我可以确保实例m_helper是全局唯一的。此时,就调用了析构函数。完整代码如下:
ServerSocket.h
cpp
class CServerSocket
{
public:
static CServerSocket*getInstance()
{
if (m_instance == NULL)//静态函数没有this指针,所以无法直接访问成员变量
{
m_instance = new CServerSocket();
}
return m_instance;
}
private:
CServerSocket& operator=(const CServerSocket& ss) {}
CServerSocket(const CServerSocket&ss) {}
//在构造函数中初始化socket环境
CServerSocket()
{
if (InitSockEnv() == FALSE)
{
MessageBox(NULL, _T("无法初始化套接字环境,请检查网络设置!"), _T("初始化错误!"), MB_OK | MB_ICONERROR);
exit(0);
}
}
~CServerSocket()
{
WSACleanup();
}
//1、初始化套接字环境
BOOL InitSockEnv()
{
//定义一个WSADATA结构体变量data,
//用于接收WSAStartup()返回的Winsock库的详细信息(如版本、实现细节等)。
WSADATA data;
if (WSAStartup(MAKEWORD(1, 1), &data) != 0)
{
return FALSE;
}
return TRUE;
}
static void releaseInstance()
{
if (m_instance != NULL)
{
CServerSocket* tmp = m_instance;
m_instance = NULL;
delete tmp;
}
}
static CServerSocket* m_instance;
class CHelper
{
public:
CHelper()
{
CServerSocket::getInstance();
}
~CHelper()
{
CServerSocket::releaseInstance();
}
};
static CHelper m_helper;
};
ServerSocket.cpp
cpp
#include "pch.h"
#include "ServerSocket.h"
//CServerSocket server;
//静态成员变量m_instance类内声明、类外定义
CServerSocket* CServerSocket::m_instance = NULL;
//初始化pserver指针,指针pserver是全局变量
CServerSocket* pserver = CServerSocket::getInstance();
//定义并初始化静态成员m_helper
CServerSocket::CHelper CServerSocket::m_helper;
2.3 网络编程
在构造函数中初始化socket环境之后,就可以创建套接字了,但是这里为什么将创建的socket文件描述符设置为类CServerSocket的成员变量呢?因为在InitSocket()函数、AcceptClient()函数中都需要用到m_client变量。最后在析构函数中调用closesocket(m_sock);关闭服务器套接字。同理,由于m_client变量在后面的DealCommand函数中还要使用,故m_client变量也要设置为类CServerSocket的成员变量,而不是在AcceptClient()函数中定义,如果在AcceptClient()函数中定义,那么m_client变量就是AcceptClient()函数中的局部变量,在DealCommand函数中就无法使用了。
cpp
//在构造函数中初始化socket环境
CServerSocket()
{
m_client = INVALID_SOCKET;////初始化为无效的套接字 -1
if (InitSockEnv() == FALSE)
{
MessageBox(NULL, _T("无法初始化套接字环境,请检查网络设置!"), _T("初始化错误!"), MB_OK | MB_ICONERROR);
exit(0);
}
//windows环境初始化完成后,就可以创建套接字了。
m_sock = socket(PF_INET, SOCK_STREAM, 0);
}
cpp
//1、初始化
bool InitSocket()
{
if (m_sock == -1) return false;
sockaddr_in serv_adr;//是一个结构体,用于存储IPv4地址和端口信息
//用于将serv_adr的所有字节初始化为0(清零),
//该操作是为了防止结构体内存中残留的随机数据影响后续操作(如未初始化的变量可能导致bind()失败)。
memset(&serv_adr, 0, sizeof(serv_adr));
//指定地址族(Address Family)为AF_INET(表示IPv4)
serv_adr.sin_family = AF_INET;
//sin_addr.s_addr存储IP地址,INADDR_ANY是一个特殊值(0.0.0.0),
//监听本机所有可用的网络接口(如WiFi、以太网、本地回环127.0.0.1)。
serv_adr.sin_addr.s_addr = INADDR_ANY;
serv_adr.sin_port = htons(9527);
//绑定
if (bind(m_sock, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
return false;
}
if (listen(m_sock, 1) == -1)//控制端一般是1对1控制,所以这里为1
{
return false;
}
return true;
}
cpp
//接收来自于客户端的连接请求
bool AcceptClient()
{
sockaddr_in client_adr;
int cli_sz = sizeof(client_adr);
//
m_client=accept(m_sock, (sockaddr*)&client_adr, &cli_sz);
if (m_client == -1) return false;
return true;
}
cpp
~CServerSocket()
{
closesocket(m_sock);//关闭服务器套接字
WSACleanup();
}
环境初始化、网络初始化等操作都封装到单例对象中去了。到这里初步的网络编程框架搭建完成,具体代码可参考初步的网络编程框架搭建。