RemoteCtrl-初步的网络编程框架搭建
利用单例设计模式搭建服务器架构
开发项目时,先开发比较难的模块。好处是:①使得项目的进度具有可控性;②方便对接;③能够进行可行性的评估,提早暴露风险。
1.window网络编程中的套接字初始化
在window网络编程中的套接字初始化,如下代码是固定的,并且该代码只会被执行一次,在程序初始化以及程序被销毁时执行。其中,在windows系统中使用WSAStartup()函数初始化Winsock函数,接着就是网络编程的代码,如创建套接字socket、绑定bind、监听listen等完成之后,使用WSACleanup()函数释放Winsock资源。
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
#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
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
//静态成员变量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()函数。代码如下所示:
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;
CServerSocket::CHelper CServerSocket::m_helper;
那么定义CHelper类的作用是什么?我们声明一个CHelper对象实例m_helper,并在ServerSocket.cpp文件中实现该实例。我可以确保实例m_helper是全局唯一的。此时,就调用了析构函数。完整代码如下:
ServerSocket.h
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
#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函数中就无法使用了。
//在构造函数中初始化socket环境
CServerSocket()
{m_client = INVALID_SOCKET;////初始化为无效的套接字 -1if (InitSockEnv() == FALSE){MessageBox(NULL, _T("无法初始化套接字环境,请检查网络设置!"), _T("初始化错误!"), MB_OK | MB_ICONERROR);exit(0);}//windows环境初始化完成后,就可以创建套接字了。m_sock = socket(PF_INET, SOCK_STREAM, 0);
}
//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;
}
//接收来自于客户端的连接请求
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;
}
~CServerSocket()
{closesocket(m_sock);//关闭服务器套接字WSACleanup();
}
环境初始化、网络初始化等操作都封装到单例对象中去了。到这里初步的网络编程框架搭建完成,具体代码可参考初步的网络编程框架搭建。