RemoteCtrl-初步的网络编程框架搭建

利用单例设计模式搭建服务器架构

开发项目时,先开发比较难的模块。好处是:①使得项目的进度具有可控性;②方便对接;③能够进行可行性的评估,提早暴露风险。

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();
}

环境初始化、网络初始化等操作都封装到单例对象中去了。到这里初步的网络编程框架搭建完成,具体代码可参考初步的网络编程框架搭建