C++游戏服务器框架笔记(四)_封装Select

news2025/1/23 14:57:43

C++游戏服务器框架笔记(一)_封装数据包类

C++游戏服务器框架笔记(二)_封装Socket类

C++游戏服务器框架笔记(三)_封装ByteBuffer类

C++游戏服务器框架笔记(四)_封装Select

        因为设想的次系列服务器主要应用场景是linux系统下,支持Windows系统是为了更好的调试和开发,为了简单起见,我在windows下选择使用select()这一IO多路复用模型。

一、Select()函数信息

微软官网的函数介绍:

#include <WinSock2.h>
int WSAAPI select(
  [in]      int           nfds,
  [in, out] fd_set        *readfds,
  [in, out] fd_set        *writefds,
  [in, out] fd_set        *exceptfds,
  [in]      const timeval *timeout
);

参数

[in] nfds

已忽略。 仅包含 nfds 参数,以便与 Berkeley 套接字兼容。

[in, out] readfds

一个可选指针,指向一组要检查的套接字是否可读。

[in, out] writefds

一个可选指针,指向要检查的一组套接字是否可写。

[in, out] exceptfds

指向一组要检查错误的套接字的可选指针。

[in] timeout

选择等待的最大时间,以 TIMEVAL 结构的形式提供。 将 超时 参数设置为 null 以阻止操作。

typedef struct timeval {
  long tv_sec;
  long tv_usec;
} TIMEVAL, *PTIMEVAL, *LPTIMEVAL;

tv_sec

时间间隔,以秒为单位。

tv_usec

时间间隔(以微秒为单位)。 此值与 tv_sec 成员结合使用来表示非秒数的时间间隔值。

(我在代码中设置的时间是0)

返回值

select 函数返回已准备就绪且包含在fd_set结构中的套接字句柄总数、时间限制过期时为零;如果发生错误,则返回SOCKET_ERROR。 如果返回值SOCKET_ERROR,可以使用 WSAGetLastError 检索特定的错误代码。

错误代码含义

WSANOTINITIALISED

在使用此函数之前,必须执行成功的 WSAStartup 调用。

WSAEFAULT

Windows 套接字实现无法为其内部操作或 readfd、writefdexceptfd 或 timeval 参数不是用户地址空间的一部分。

WSAENETDOWN

网络子系统失败。

WSAEINVAL

超时值无效,或者所有三个描述符参数均为 null

WSAEINTR

阻止 Windows 套接字 1.1 调用通过 WSACancelBlockingCall 取消。

WSAEINPROGRESS

正在执行阻止的 Windows 套接字 1.1 调用,或者服务提供商仍在处理回调函数。

WSAENOTSOCK

描述符集之一包含不是套接字的条目。

select()相关宏

头文件 Winsock2.h 定义了FD_SETSIZE确定了fd_set限制的描述符的最大数目,windows下默认值是64,对于这个限制,我们可以在包含Winsock2.h前,重定义FD_SETSIZE来修改这个限制,不过不建议修改太大,太大的话效率会很低~

 头文件 Winsock2.h 中定义了四个宏,用于操作和检查描述符集(fd_set):

FD_ZERO(set)                初始化set设置为空集,在使用之前,应该使用此宏清空set。

FD_CLR(fd, set)              从set中删除fd套接字。

FD_ISSET(fd, set)           检查fd是否为set的成员,返回true 或者false。

FD_SET(fd, set)              添加fd到set集。

现在来详细看下Windows下这些宏的具体实现,其实linux下也是大差不差的,只复杂一点点,

我们先看下Winsock2.h 中fd_set的结构:

//UINT_PTR unsigned long long or  unsigned __int64
typedef UINT_PTR        SOCKET;

//u_int ===> unsigned int
typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

SOCKET就是Windows下套接字描述符的类型,UINT_PTR 类型,UINT_PTR有事unsigned long long类型,其实SOCKET就是一个长整数类型。

fd_set有两个成员:

        fd_count :记录了当前fd_set中有多少关注的SOCKET描述符

        fd_array :是一个SOCKET的数组,记录了当前fd_set中关注的SOCKET描述符值,也就是linux下的socket fd。这里FD_SETSIZE就是定义了这个数组的大小,所以前面说的最大限制,就是体现在这里。

FD_ZERO(set):

#define FD_ZERO(set) set->fd_count = 0

这个宏的实现很简单,就只是将fd_set结构中记录描述符数量的fd_count设置为0,select()内部通过判断fd_set的fd_count去操作fd_array,所以这里改成0后,就可以达到清空的效果,而没有再去把数组重新初始化掉。

FD_CLR(fd, set):

//头文件Winsock2.h中宏定义代码
#define FD_CLR(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
            while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
                ((fd_set FAR *)(set))->fd_array[__i] = \
                    ((fd_set FAR *)(set))->fd_array[__i+1]; \
                __i++; \
            } \
            ((fd_set FAR *)(set))->fd_count--; \
            break; \
        } \
    } \
} while(0, 0)

//上面代码乍一看感觉复复杂杂的,现在来给简化一个格式,去除干扰我们阅读的细枝末节
//FD_CLR(fd, set) 宏展开
#define FD_CLR(fd, set) do {
    u_int __i;
    for (__i = 0; __i < set->fd_count ; __i++) {
        if (set->fd_array[__i] == fd) { 
            while (__i < set->fd_count-1) { 
                set->fd_array[__i] = set->fd_array[__i+1]; 
                __i++; 
            } 
            set->fd_count--; 
            break; 
        } 
    } 
} while(0, 0)

我们看下面简化和宏展开后的代码,代码逻辑就比较清晰了,对于从fd_set中删除描述符的操作,使用的是数组元素的前移——for循环遍历fd_set的fd_array数组,找到当前需要删除的fd描述符,找到之后,用while循环依次将当前数组下标后面每一个元素向前移动一个元素位置,最后fd_count减1,完成删除操作。

FD_ISSET(fd, set) 

//__WSAFDIsSet是windows的一个api 检测fd是否包含在set数组中
#define FD_ISSET(fd, set) __WSAFDIsSet(fd, set)

 FD_SET(fd, set):

//头文件Winsock2.h中FD_SET宏定义代码
#define FD_SET(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
            break; \
        } \
    } \
    if (__i == ((fd_set FAR *)(set))->fd_count) { \
        if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
            ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
            ((fd_set FAR *)(set))->fd_count++; \
        } \
    } \
} while(0, 0)


//FD_SET(fd, set) 宏展开如下
do {
    u_int __i;
    for (__i = 0; __i < set->fd_count; __i++) {
        if (set->fd_array[__i] == fd) {
            break;
        }
    }
    if (__i == set->fd_count) {
        if (set->fd_count < FD_SETSIZE) {
            set->fd_array[__i] = fd;
            set->fd_count++;
        }
    }
} while(0, 0)

我们看下面简化和宏展开后的代码, for循环遍历fd_set的数组fd_array,如果发现本次设置的fd描述符存在则break不做任何操作,如果不存在,则判断当前fd_set的fd_count是否已经达到FD_SETSIZE的最大限制数量,如果未达到则可以设置成功,如果达到最大限制数量则不作操作,设置无效。

这几个宏的实现,应该都是挺好理解的,不过大家应该也发现了为什么上面的宏定义都使用了do{......}while(0,0)   的结构,注意这里的是没有;分号结尾的

思考一下,答案稍后揭晓~~~~

好了,关于宏定义中为什么使用do{...}while(0)结构

先说结论:为了宏的健壮性

只是这样说,太宽泛了,我们举例说明一下:

第一种情况 我们按照普通的方式定义一个宏

#define T(a) func1(a);func2(a);

//如果调用者在if中调用这样使用
if(a > 0)
    T(a);
else
    func1(a);
//当以上代码宏展开后如下:
if(a > 0)
    func1(a);func2(a);
else
    func1(a);
//虽然该例中,但是因为if...else 如果if后面不加{},那么if的作用域只对紧跟的一条语句(即只能有一句;结尾的语句)生效
//那么上面的代码实际上应该是这样子:
if(a > 0)
    func1(a);
func2(a);
else
    func1(a);
//这里就很明显,else会找不到与之对应的if,导致宏展开之后的代码报错

第二种情况 既然直接裸奔不行,那就加上{}给定一个局部作用域

//以上宏改成:
#define T(a) {func1(a);func2(a);}
//然后调用者还是如上调用
if(a > 0)
    T(a);

//宏展开:
if(a > 0)
   {func1(a);func2(a);};
else
    func1(a);
//展开后{}的后面多了一个; 我们知道if(){...}else...语法,如果if()后面加上{}的话,else前是不能有;符号的,
//然而在C/C++代码中在语句末尾加上;符号基本成习惯性了,所以如果调用者习惯性加上;上述代码就报错了,
//而使用do{}while(0)的形式 在后面加上;是不会报错的,所以会更加健壮。
if(a > 0)
   do{func1(a);func2(a);}while(0);
else
    func1(a);

        OK 对于select()函数的了解应该差不多了,如果需要更详细的信息,可以再去搜索引擎看看其他大佬的文章,这里主要是说下我的封装。

        为了将Windows下的select()和linux下的epoll()封装为统一接口,所以需要首先定义一个接口类,来约定统一接口和一些类型,做到不同平台,调用接口一致,内部实现根据平台不同的跨平台效果。

统一接口类(多路复用基类):

#ifndef __POLLBASE_H_
#define __POLLBASE_H_

#include <stdlib.h>
#include <map>
#include <vector>

//统一描述符的类型命名,都为SOCKET
#ifndef _WIN32
typedef int SOCKET;
#define INVALID_HANDLE_VALUE (-1)
#endif

//重定义Windows下FD_SETSIZE的大小
#ifdef _WIN32
#define FD_SETSIZE 1024
#include <WinSock2.h>
#else
#include <unistd.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <sys/select.h>
#endif

#define MAXEVENT 1024

//统一定义Windows下select()和linux下epoll()的事件的宏
#define NONE 0                        //无
#define POLLREAD 1                    //读事件
#define POLLWRITE 2                   //写事件
#define POLLET 3                      //边缘触发(用于EPOLL, 是否设置边缘触发,默认是水平触发)

//定义统一的事件结构
typedef struct PollEvent {
	int event;
	int fd;
}PollEvent;

//接口类(抽象类)
class PollBase
{
public:
	virtual ~PollBase() {};
    /*
    创建(初始化)接口
        size: 可关注的描述符数量
    返回值:
        0:     成功
        -1:     失败
    */
	virtual int Create(int size) = 0;

    /*
    为fd描述符添加关注的事件
        fd:     socket文件描述符
        mask:   上面统一定义的事件宏(读事件、写事件)
    返回值:
         0:     成功
        -1:     失败
    */
	virtual int AddEvent(SOCKET fd, int mask) = 0;

    /*
    删除fd描述符关注的事件
        fd:     socket文件描述符
        mask:   上面统一定义的事件宏(读事件、写事件)
    返回值:
         0:     成功
        -1:     失败
    */
	virtual int DelEvent(SOCKET fd, int mask) = 0;

    /*
    监听事件到来,IO多路复用的核心
        active_events: 输出参数,如果有事件到来,则会将事件设置到active_events输出
        timeout:        超时时间(秒)
    返回值:
        0:无事件
        >0:到来事件的数量
    */
	virtual int Poll(PollEvent* active_events, int timeout = 0) = 0;
};

#endif

统一定义了接口之后,现在来继承接口类封装Windows下的select(),类名:Select

Select.h

#ifndef _SELECT_H_
#define _SELECT_H_

#include "PollBase.h"

class Select :
    public PollBase
{
public:
    ~Select();
    int Create(int size);
    int AddEvent(SOCKET fd, int event);
    int DelEvent(SOCKET fd, int event);
    int Poll(PollEvent* active_events, int timeout = 0);

private:
    std::map<SOCKET, int> m_EventMap;

    //可读事件集,存储关注的读事件
    fd_set m_ReadSet;
    //可写事件集,存储关注的写事件
    fd_set m_WriteSet;
    
    //临时可读事件集,每次调用select()之前,将m_ReadSet中的值拷贝到该临时事件集中
    fd_set m_TmpReadSet;
    //临时可写事件集,每次调用select()之前,将m_WriteSet中的值拷贝到该临时事件集中
    fd_set m_TmpWriteSet;
};

#endif // !_SELECT_H_

Select.cpp 

#include "Select.h"
#ifdef _WIN32
Select::~Select() {

}

int Select::Create(int size) {
    //初始化m_ReadSet和m_WriteSet描述符集,这里的参数size,用不到,可以忽略
	FD_ZERO(&m_ReadSet);
	FD_ZERO(&m_WriteSet);

	return 0;
}

int Select::AddEvent(SOCKET fd, int event) {
    //判断是否是可读事件
	if (event & POLLREAD) {
        //为fd关注可读事件
		FD_SET(fd, &m_ReadSet);
	}

    //判断是否可写事件
	if (event & POLLWRITE) {
        //为fd关注可写事件
		FD_SET(fd, &m_WriteSet);
	}
	return 0;
}

int Select::DelEvent(SOCKET fd, int event) {
    //判断是否是可读事件
	if (event & POLLREAD) {
        //删除fd的可读事件
		FD_CLR(fd, &m_ReadSet);
	}

    //判断是否是可读事件
	if (event & POLLWRITE) {
        //删除fd的可写事件
		FD_CLR(fd, &m_WriteSet);
	}
	return 0;
}

int Select::Poll(PollEvent* active_events, int timeout) {
    //由于select()的fd_set参数都是传出参数,所以每次调用返回之后,内部的值都会变成到达的事件集,
    //所以不可以直接将&m_ReadSet或者&m_WriteSet作参数传递,会导致记录的事件丢失
    //每次都应该拷贝到临时描述符集中
	memcpy(&m_TmpReadSet, &m_ReadSet, sizeof(m_ReadSet));
	memcpy(&m_TmpWriteSet, &m_WriteSet, sizeof(m_WriteSet));
    
    //设置超时时间
	struct timeval time = {};
	time.tv_usec = timeout * 1000;

	int fd;
	u_int i = 0, event_index = 0;
	int num = select(0, &m_TmpReadSet, &m_TmpWriteSet, nullptr, &time);
	if (num > 0) {
		//如果存在可读事件,遍历临时可读事件集,并将事件添加到active_events中
		for (i = 0; i < m_TmpReadSet.fd_count; ++i, ++event_index) {
			fd = m_TmpReadSet.fd_array[i];
			active_events[event_index].event = POLLREAD;
			active_events[event_index].fd = fd;
			m_EventMap[fd] = event_index;
		}
		
		//如果存在可写事件,遍历临时可写事件集,并将事件添加到active_events中
		for (i = 0; i < m_TmpWriteSet.fd_count; ++i) {
			fd = m_TmpWriteSet.fd_array[i];
            //
			if (m_EventMap[fd]){
				//如果fd已存在读事件,则可以直接用|运算加上写事件
				active_events[m_EventMap[fd]].event |= POLLWRITE;
			}
			else {
				active_events[event_index].event = POLLWRITE;
				active_events[event_index].fd = fd;
				++event_index;
			}
		}
	}
	return num;
}

#endif

封装后的大概调用流程,后面linux下的epoll封装后,也是使用相同的调用流程,不需要再修改接口:

......

void EventLoop::Run() {
	while (!m_Stop) {
		memset(m_ActiveEvents, 0, m_Size);
        //监听事件到来
		int num = m_Poll->Poll(m_ActiveEvents);
		if (num > 0) {
            //处理事件
			this->ActiveEvents(num);
		}
	}
}

......

以上就是对Windows下的select封装, 由于linux下也有select函数所以这里我特别使用#ifdef _WIN32宏将Select类包装起来,避免报错。

下一章 封装Epoll  请不要期待~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/521790.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【sqlite】联查Join更新

系列文章 C#底层库–MySQLBuilder脚本构建类&#xff08;select、insert、update、in、带条件的SQL自动生成&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/129179216 C#底层库–MySQL数据库操作辅助类&#xff08;推荐阅读&#xff0…

虚拟化技术 — 虚拟机迁移

目录 文章目录 目录Libvirt 的 Live Migration(热迁移)网络数据传输层控制层Pre-Copy Live MigrationLibvirt 的 Live Migration(热迁移) Libvirt 的 Live Migration 主要分为 “数据“ 和 “控制“ 这两个层面的内容。 网络数据传输层 基于 Hypervisor 的传输:两个 Hyp…

MiniGPT-4 笔记

目录 简介 实现方法 效果及局限 参考资料 简介 MiniGPT-4 是前段时间由KAUST&#xff08;沙特阿卜杜拉国王科技大学&#xff09;开源的多模态大模型&#xff0c;去网站上体验了一下功能&#xff0c;把论文粗略的看了一遍&#xff0c;也做个记录。 论文摘要翻译&#xff1…

【图】DFS、BFS遍历

图有两种遍历方式&#xff1a;DFS深度优先&#xff0c;BFS广度优先。 把所有顶点访问一遍&#xff0c;且每个顶点只访问一次&#xff0c;把走过的顶点标记一下。 标记&#xff1a;为图设置一个访问标志数组visited[n&#xff3d;&#xff0c;用于标示图中每个顶点是否被访问过…

C++引用()笔记

C引用(&)笔记 1.寄存器一般只有4/8个字节&#xff0c;所以返回时候的中间变量(下图的临时变量)不一定是储存在寄存器当中 2.传引用返回可以减少拷贝&#xff0c;增加效率 但运行打印会出错的&#xff0c;因为当栈帧销毁的时候&#xff0c;清理栈帧就会得到随机值 正确表达…

西宾蜻蜓FM语音下载(qingtingdown)

一、介绍 西宾蜻蜓FM语音下载&#xff08;qingtingdown&#xff09;&#xff0c;能够帮助你下载蜻蜓FM音频节目。如果你是蜻蜓FM会员&#xff0c;它还能帮你下载会员节目。 二、下载地址 本站下载&#xff1a;西宾蜻蜓FM语音下载&#xff08;qingtingdown&#xff09; 百度…

PSP - AlphaFold2 适配不同来源搜索的 MSA 接口

欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://blog.csdn.net/caroline_wendy/article/details/130594303 MSA (Multiple Sequence Alignment) 在 AlphaFold2 中的工作方式如下: 使用搜索工具 (hhblits/hhsearch/jackhmmer),从大型数据库中,搜索与目标…

从零开始学习JVM(二)--类加载子系统

1. 类加载子系统介绍 JVM内存结构如下图所示&#xff1a; 程序计数器&#xff08;PC寄存器&#xff09;&#xff1a;程序计数器是⼀块⼩的内存空间&#xff0c;可以看作是当前线程所执⾏的字节码的⾏号指示器。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏…

PyTorch读取数据集全部进内存,使网络的训练速度提高10倍以上!!!

正常情况下&#xff0c;torch读取数据的时候是Batch Size小批量读取。首先找到所有数据集的路径保持到一个变量中&#xff0c;之后需要读取哪个数据的时候&#xff0c;就根据这个变量中的路径索引去读取。因为硬件的限制&#xff0c;从硬盘中读取数据到显存中所花的时间要远远大…

计算机体系结构实验一

计算机体系结构实验一 一.实验目的 ​理解RISC-V的指令执行的数据流和控制信号&#xff0c;熟悉指令流水线的工作过程。 二.实验过程 1.RISC-V的相关指令 实验的模拟器使用RISC-V指令集&#xff0c;为了便于后续分析&#xff0c;首先学习实验中使用的RISC-V指令。 基本RIS…

Cesium最新版使用天地图地形及注记服务

天地图三维地名服务和地形服务需要利用 cesium 开源三维地球API与天地图扩展插件共同使用&#xff0c;目前支持cesuim1.52、1.58、1.63.1。 天地图调用demo: http://lbs.tianditu.gov.cn/docs/#/sanwei/ 注意&#xff1a; demo里的地形服务地址不对&#xff0c;需要自己更换成…

MCU通用移植方案

MCU通用移植方案 目录 MCU通用移植方案前言1 硬件移植2 软件移植2.1 底层移植方法2.1.1 移植原理2.1.2 移植方法 2.2 中间层移植方法2.2.1 移植原理2.2.2 移植方法 2.3 两种移植方法比对 3 结束语 前言 因为项目的需求或者成本控制等因素&#xff0c;我们经常会遇到更换MCU的情…

华硕 PRIME H610M-A D4 i5-12490F 1060电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件型号驱动情况 主板华硕 PRIME H610M-A D4&#xff08;LPC Controller/eSPI Controller H610芯片组&#xff09; 处理器12th Gen Intel Core i5-12490F 六核已驱动 内…

Mysql的重要知识点以及问题

查看索引的命令 show index from mytable 索引的原理 索引用来快速地寻找那些具有特定值的记录。如果没有索引&#xff0c;⼀般来说执行查询时遍历整张表。 索引的原理&#xff1a;就是把无序的数据变成有序的查询 把创建了索引的列的内容进行排序 对排序结果生成倒排表…

货运物流小程序开发功能有哪些?

移动互联网的深入发展让网购等线上交易更加盛行&#xff0c;货运快递物流也随之增多&#xff0c;成为我们日常生活的重要组成部分。传统的货运物流管理主要依赖人工&#xff0c;不仅效率慢还容易出错。随着市场的发展以及人们对服务质量要求的提高&#xff0c;现在很多中大型货…

Oracle 12c安装

前言 版本&#xff1a;12c第二版 检查弹出窗口程序&#xff0c;需要安装xmanager,并执行以下命令&#xff1a; xhost 192.168.194.91 安装步骤如下 安装必须的安装包&#xff1a; rpm -q bc binutils compat-libcap1 compat-libstdc-33 glibc glibc-devel ksh libaio libaio…

c++ 多态与虚函数

c中多态分为静态多态和动态多态&#xff0c;静态多态是函数重载&#xff0c;在编译阶段就能确定调用哪个函数。动态多态是由继承产生的&#xff0c;指不同的对象根据所接收的消息(成员函数)做出不同的反应。例如&#xff0c;动物都能发出叫声&#xff0c;但不同的动物能发出不同…

esp32之解析json

文章目录 前言一、json的作用二、json结构三、esp32 json解析安装库解析StaticJsonDocumentDynamicJsonDocument 四、解析今天的北京天气总结 前言 在现代Web开发中&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;已成为常用的数据传输格式。ESP32是一款…

Netty 爱好者必看!一文详解 ChannelHandler 家族,助你快速掌握 Netty 开发技巧!

1 Channel 接口的生命周期 Channel 定义了一组和 ChannelInboundHandler API 密切相关的简单但功能强大的状态模型 1.1 Channel 的状态 状 态描 述ChannelUnregisteredChannel 已经被创建&#xff0c;但还未注册到 EventLoopChannelRegisteredChannel 已经被注册到了 EventL…

Wealth 开源的账本响应式网站系统免费部署

演示网站&#xff1a; https://wealth.willin.wang 前置准备 首先需要注册一个 Github 账号&#xff0c;Fork 这个开源项目&#xff1a; https://github.com/willin/wealth &#xff08;欢迎 Star&#xff09; 然后使用 Github 账号分别注册 Vercel 和 Planetscale&#xf…