当前位置: 首页 > news >正文

Minetest游戏引擎源代码解析

先来为没有听说过Minetest(现已更名为Luanti)的读者简单介绍一下,Minetest是一款类似于我的世界类型的沙盒类游戏的游戏引擎,100%开源,是初学者学习游戏引擎设计与实现的最佳实践,读者如果想要下载源代码的话可以在Linux(Ubuntu)的终端输入如下命令:

git clone https://github.com/minetest/minetest.git

更详细的介绍可以去问AI或是查看官方文档,这里不多BB,直接开始上干货。

1.大框架搭建

Minetest整个项目有1400多个文件,约40多万行代码,主要使用了C,C++和Lua脚本语言,如果读者只了解C/C++,不清楚Lua也不用慌,如果你的C/C++掌握的牢固,Lua是可以直接看懂并很快上手的,整个项目大体上分为三层,分别是: 游戏层,接口层和引擎核心层,我们要看的重点就位于引擎核心层,该层主要使用C++实现,可以分为五个模块,分别是:渲染模块, 地图生成模块, 网络同步模块, 脚本绑定模块与物理逻辑模块。对于这种大型项目,肯定是不能从头看到尾的,必须要透过代码看出其中的逻辑,要说明的是笔者也是第一次分析这种大型项目,如果有哪些地方分析的有问题的话,还请指出。下面进入正题。

1.1网络同步模块

在分析前,首先要明确要按什么顺序进行分析,依据笔者不多的分析源代码的经历来看,大体上可以分为按执行流程分析和按作用模块分析,按流程分析就是从main函数开始,按照程序执行的流程一点点分析,不过笔者目前最熟悉的知识体系就只有网络同步模块,其它4个模块是没什么理解的,因此选择按照模块分析,从自己已有的知识开始,一点点往后推理,所以分析的第一个模块就是网络同步模块,先不看源代码,我们自己猜想一下网络同步模块要干些什么,根据笔者的编程经验,首先肯定要有一个客户端与一个服务端,两端使用网络协议进行通信,一般来说在游戏这种实时性要求高的情况下,会采用UDP协议进行通信,因为UDP协议虽然不保证可靠性,但是它快速,轻量,非常适合实时在线的游戏通信,并且通过上层的封装,也是可以让UDP协议变得可靠的,那么最简单的网络同步模块的模型就是这样子的:

把所有游戏模块都视为黑盒,上图就是一款游戏的本质了,任何游戏都是,我们现在要做的就是根据Minetest源代码中的网络同步模块部分,一点点的完善上图,现在来补充细节,首先在两端都需要有报文接收与发送功能的模块,并且还需要管理所有的链接,把收发报文与管理链接两个功能组合起来,称为链接管理层,在获取到了UDP报文后,肯定需要对报文进行解析,并且在发送报文时也需要对报文进行封装,也就是序列化与反序列化,我们将负责该功能的层称为协议层,协议层在解析了报文后,必然要交付给应用层处理,我们称该层为业务逻辑层,依据我们的猜想,把网络同步模块再次划分成了:链接管理层,协议层与业务逻辑层,下面正式开始分析源代码,先来看看链接管理层在Minetest中是如何实现的。

1.1.1链接管理层

首先链接管理层的主要文件是:

在这部分文件中又可以划分为实现辅助功能的小组件和实现主功能的类,我们先从实现主功能的类入手,理清楚一条主脉络,先看到connextion.h文件中的一个类:

很显然,该类是一个抽象基类,规定了接口的形式,并且从名称也可以看出来,这个类与链接的关系非常大,下面再看到impl.h文件中的另一个类:

显然Connection就是继承了IConnection的具体实现了,final关键字指明Connection不可被继承,之后就以IConnection与Connection类为核心,来分析链接管理层的第一条脉络,先看到IConnect中的函数名称,大概的猜测一下,Serve应该是让服务端开启链接并绑定IP地址和端口号的,Connect应该是让客户端链接上服务端的,Connected应该是让客户端检查链接是否建立成功的,Disconnect与DisconnectPeer应该与断开连接有关,ReceiveTimeoutMs应该是用于接收UDP报文的,并且还设置了超时时间,而TryReceive显然就是超时时间为0的完全非阻塞式的报文获取接口了,Send显然与UDP报文的发送有关,get系列的接口应该是用于确定连接信息的。下面让我们去Connection类中确认一下,首先成员变量有这些:

成员函数的大部分操作一定都是对成员变量进行的,因此我们先来大概猜测一下成员变量的作用,从名称与类型来看,m_event_queue应该是一条阻塞队列,在队列中存放的应该是连接事件的指针,m_peer_id应该是连接的唯一标识,在完整的连接中,一个服务端要对接多个客户端,每一个服务端与客户端都会有自己的Connection对象,因此可以使用m_peer_id来唯一标识一次完整连接中的单个服务端或客户端,m_protocol_id应该是与协议有关的id,估计是用于版本匹配的,比如每次客户端向服务端发送报文时,就先匹配一次服务端的m_protocol_id与客户端的m_protocol_id,如果客户端的版本不匹配的话,那么服务端就断开连接或是让客户端更新协议版本,m_peers显然是配合m_peer_id使用的,其结构是一个红黑树,结构特征是能够快速的提供key定位到对应的value,在此处应该就是通过m_peer_id唯一的定位到一个Peer对象的指针,尽管我们还没有查看Peer具体的什么,但是可以大胆的猜测一下,首先m_peer_id是用于唯一标识一个链接的,但是能够唯一标识链接显然是不够的,应该还得要通过m_peer_id快速的定位到一个具体的链接,比如同时有三个客户端向服务端发送了报文,那么服务端在处理完成后就应该要能够快速准确的进行返回,首先就得找到具体的客户端,因此就可以通过m_peer_id在自身的m_peers中查找出一个Peer指针,显然Peer类型中存储的应该是一个客户端的信息,比如IP地址和端口号信息,m_peer_ids中显然就是存储所有m_peer_id的了,在服务端向所有客户端广播某条信息时就可以遍历m_peer_ids找到所有的客户端并发送信息,游戏中的全服广播就是类似的原理,m_peers_mutex显然就是一个锁了,用于保证线程安全的从阻塞队列中拿取或是放入报文,m_sendThread应该是用于发送报文报文的线程,而m_recviveThread应该是用于接收报文的线程,m_info_mutex是另一把锁,目前还无法判断出具体的作用,下面的m_bc_peerhandler是一个指向PeerHandler类型的指针,PeerHandler应该是一个用于存放回调函数的类,最后的m_shutting_down从名称来看应该和关闭有关,估计是用来标识链接状态的,比如链接还未断开就标记为false,链接断开了就标记shutt_down为true,在下面的m_udpSocket是一个自定义UDP套接字类型,该类具体的定义位于socket.cpp文件中,很显然m_udpSocket的作用就是绑定IP地址与端口号了并建立套接字对象了,实际的发送与接收调用的底层接口也多半是在该类型中的,最后一个m_command_queue也是一条阻塞队列,不过在队列中存储的内容不同,从名称来看存储从应该是发送指令的指针,单从发送与接收对称的角度来看,这两条阻塞队列应该一条用于存放接收信息,另一条用来存放发送信息的,那么具体的工作流程就是客户端把存放在发送阻塞队列中的UDP报文发送给服务端,服务端在收到报文后就把报文存入接收阻塞队列,然后业务层再把接收阻塞队列中的报文取出,在处理完毕后再放入服务端的发送阻塞队列,然后再发送出去,客户端在接收到应答报文后就把报文存入接收阻塞队列。

根据上方成员变量字段的分析,可以大概推测出Minetest的链接管理层是使用了:多线程+阻塞队列+生产者消费者模型进行信息收发的,简易建模就是这样子:

在上方的分析中也可以看出一个好的名称有多么重要了,笔者能够分析出来这些信息主要是因为笔者之前手写过类似的多线程+阻塞队列+生产者消费者模型的服务器,只不过用的是Epoll,但是原理是类似的,并且笔者在项目中的命名也是类似的风格,因此单单只是从成员变量的名称就可以推理出大量的信息和主体结构了,当然肯定还存在不少细节问题,毕竟上方仅仅只是根据成员变量得出来的信息,下面就来看看Connection具体函数的实现,以此来验证我们的猜测,先来看看构造函数对于成员变量是如何初始化的:

首先是m_udpSocket,传入了一个bool类型,可以判断Minetest应该是同时支持IPv4与IPv6并且可以切换的,m_protocol_id传入了一个宏,其值就是当前服务端与客户端的版本,然后是给m_sendThread和m_receivedThread分别申请了不同的线程对象,其中在初始化m_sendThread时传入了两个参数,从名称来看,应该是指定了一次发送报文的最大发送数和发送等待的超时时间,然后是m_bc_peerhandler,按照上方对于成员变量的分析,我们猜测应该是注册了一个回调函数类,在函数体中,显然就是对线程和套接字的一些初始值设置操作并启动了发送线程与接收线程。目前分析到了岔路口,我们有两种往下分析的路径,其一是直接去看Connection中的函数,摸清楚各个函数模块的作用,其二是顺着这个构造函数中使用到的函数按照执行流顺序往后推理,看过笔者其它文章的读者应该知道,笔者喜欢把知识链串起来,因此在此处选择第二个分析思路,以该构造函数为起点,继续往后分析,首先就是m_udeSocket,是时候去看看socker.h了:

从该类的声明中可以发现有三个成员变量,其中m_handle无法确定作用,但是m_timeout_ms肯定和时间设置有关,m_addr_family肯定与套接字的模式有关,在成员函数中Send显然就是用来发送数据的,而Receive显然就是接收数据的,不过此时我们的目的并不是分析透UDPSocket类,而是为了分析透Connection才来看UDPSocket类的,因此只分析我们需要的,因此看到UDPSocket的构造函数:

从参数类型可以确定在Connection中初始化m_udpSocket使用的就是该构造函数了,让我们继续往下看看init函数:

参数是两个bool类型,第一个显然是指定是否使用IPv6,第二个应该与是否抛异常有关,在函数的第一个if中,看到判断值与g_sockets_initialized有关,该值不是函数的成员变量,那么就应该是一个全局的变量,该操作是一个防御性编程,简单来说就是在操作系统层面准备网络套接字也是需要时间的(主要是Windows平台需要),因此在调用系统调用前必须先保证操作系统准备好了,g_sockets_initialized就是在操作系统准备好套接字创建环境后就被置为true,否则就置为false,避免在下面调用socket时调用失败导致程序崩溃,然后是下方的if判断,判断的值是m_handle,现在就可以看出,m_handle的效果就是接收socket的返回值了,在Linux中就是接收套接字创建后操作系统分配出来的文件描述符,后续的所有通信都要使用该文件描述符与操作系统交互,那么在此处的if条件判断显然就是避免一个套接字被重复创建的情况了,上方两条防御性if结束,然后正式开始创建套接字,可以看到在m_addr_family出印证了我们上方的猜想,Minetest确实是同时支持IPv6与IPv4并且可以互相切换的,在下面就是创建实际的套接字了,让m_hendle获取文件描述符,之后的if语句显然就是通过返回值判断套接字是否创建成功的(这里我们先不管异常模块和日志模块),最后又有一个函数,看名称与超时时间的设置有关,来具体看看:

嗯,非常简单,没什么好说的,然后回到Connection的初始化函数中,我们已经明确了m_udpSocket的初始化流程,然后是m_protocol_id,这个没什么好说的,就是使用一个值继续初始化,版本的匹配的逻辑要在后面才会体现,下面来看看初始化的重点,也就是发送线程:m_sendThread与接收线程m_receiveThread的初始化,先来看m_sendThread,也就是类型ConnectionSendThread的构造函数与成员变量:

成员变量:

构造函数:

先来看成员变量,m_connection明显是一个回指向Connection的指针,其效果应该是让线程能够调用自身所属Connection对象的函数,m_max_packet_size应该是一次最大发送报文的上限报文大小,m_timeout显然是超时时间,比如在等待阻塞队列中的报文时,如果等待时间超过了m_timeout那么就先停止等待,让线程去处理其它工作(猜测),然后_m_outgoing_queue,是一条队列,可能是把从阻塞队列中的一批报文取出来后再放入该队列,然后一个个发送给指定的客户端或服务端,m_send_sleep_semaphore是一个自定义类型对象,从变量名称来看应该与发送休眠有关,m_iteration_packets_avaialble看名称估计与迭代器有关,可能是迭代有效可发送报文的,然后是m_max_data_packets_per_iteration,好像又是一个迭代器相关的成员变量,迭代的好像与报文的内容有关,最后的m_max_packets_requeued估计是一次可以从阻塞队列中获取的最大报文的数量。读者可能发现了,上文的大部分内容似乎都是靠猜的,完全没有笔者之前文章的那种严谨性,在这里要告诉读者的是,分析语法,操作系统,计算机组成原理与分析实际项目完全是两码事,在实际分析项目时,如果你还要严谨,那么你分析1年都不一定能分析完10万行代码,进了公司后,撑死了给你2周查看项目的时间,然后就要上手修bug和接需求了,那么就不可能严谨了,在初步分析时大部分内容都是从自己熟悉的模块开始,然后靠"猜+经验结合"的,笔者上方的流程就是按照这样子来的,可能不够严谨,但是其思想与方法论一定能够让你在进入公司后以最快的速度上手项目,翻越实习生入职后最难翻过第一座大山(分析源代码),下面回到正题,看到ConnectionSendThread的构造函数:

首先是Thread的构造,显然是显示调用了基类的构造函数,可以推测出ConnectionSendThread应该是继承了Thread,来看看是不是这样的:

确实如此,因此我们得先去看看Thread的成员变量与构造函数了:

成员变量:

构造函数:

首先是成员变量,显然m_name与一个线程的名称有关,然后是m_retval,是一个void*类型的通用指针,目前还无法确定实际用途,然后是m_joinable,这个显然与线程的回收发生有关,应该是指定让线程被主线程等待回收还是让操作系统自动回收的,然后是m_request_stop与m_running,估计是与线程的启动与暂停有关的,下面还有两把锁,m_mutex应该是用于原子的访问资源的,m_start_finished_mutex应该是与启动与结束有关的,最后一个m_thread_obj显然就是线程真正的主体了,采用了C++标准库中的线程,然后来看看构造函数,非常简单,只初始化指定了线程的名称,然后把启动标志和暂停标识都初始化成了false。

下面回到ConnectionSendThread的构造函数,基类对象Thread的初始化名称是 "ConnectionSend",那么我们有理由推断,或许是使用m_name来区分发送线程与接收线程,接收线程多半也会继承Thread,并且其名称大概率会是ConnectionRecive,之后初始化的m_max_packet_size和m_timeout显然就是指定指定发送报文的最大大小与超时时间了,下一个是m_max_data_packets_per_iteration,这个成员变量的初始化比较奇怪,冒出来了一个get_settings,首先要搞明白这东西在哪里,既然其不是成员变量,又可以直接在类中被使用,那么多半就是全局的了,笔者最终在setting.h中找到了该全局变量:

这东西的类型是Settings,估计与游戏的初始设置有关,这个类比较复杂,我们就只挑出其中的getU16函数查看:

可以看到该函数接收一个字符串,返回一个uint16_t类型,return处的stoi显然是一个函数,笔者在util中找到了该函数:

其中s32是对int32_t从重命名,显然该函数先将字符串转为s32,然后限制了其大小在0到65535之间,但是目前我们还无法推测出这么做的意义,先回到ConnectionSendThread中,在调用getU16时传入了一个宏,该宏值是:

是一个字符串..嗯..,更奇怪了,显然该字符串要通过get(name)来转换成存放整数的字符串,下面来看看get函数:

显然还得继续往下看getEntry函数,并且还涉及到了SettingsEntry类,让我们继续深入:

在getEntry函数中,进来就直接加了锁,使用的是RAII风格的,可以判断该函数肯定是要访问临界资源的,然后是定义了一个n,看类型名称似乎是一个迭代器,在之后又使用了m_settings,这东西绝对是Settings类的一个成员变量,去看看吧:

显然推测正确,不过m_settings本身又是SettingEntries类型的,可以确定的是该类型中一定有一个find成员函数和一个end成员函数,并且还有一个迭代器,去看看吧:

好家伙,原来是个重命名,那么显然m_settings.find(name)就是一次KV操作了,通过字符串name找到一个SettingsEntry类型的对象,显然得去看看SettingsEntry了:

一个非常简单的结构体,重点要关注其中的value,这个value绝对会被初始化成一个存放整数的字符串,不过还无法确定具体的内容,目前还无法找到往SettingEntries中插入元素的操作在哪里,或许在其它模块,暂且先放一放,那么函数开始往回调,首先在getEntry中返回的n->second就是一个SettingsEntry对象,然后在get中使用entry接收了返回值,在get函数中又返回了entry.value,然后返回值就被stoi接收并传入了mystoi函数中,最后在mystoi中返回了一个s32类型的整数到getU16中,在getU16中又将该整数返回给了ConnectionSendThread的构造函数,真是一条复杂的调用链,我们还没有分析出这么做的意义,不过肯定是存在某些作用的,下面去看看ConnectionSendThread函数体中的内容:

首先mppi引用了成员变量,然后就对mppi进行了修改,调用的显然是一个宏函数,下面去看看:

一条非常简单的宏函数,其效果就是让mppi的值至少大于或等于1,然后是mppi_default,可以发现Settings是一个非常重要的类,看调用函数的名字getLayer,应该是与线有关的设置,这显然不属于链接管理层的内容了,我们线放一放,至此ConnectionSendThread初始化完成,把视角拉回Connection中:

目前我们已经分析了前三个成员变量的初始化,下面是m_receiveThread,简单推测一下,应该和m_sendThread的初始化是类似的,让我们去看看:

非常简单,并且也与我们上方的猜测相同,接收线程确实是叫"ConnectionReceive"的,发送比接收复杂是必然的,因为在接收时,复杂的工作都被对方的发送线程处理好了。回到Connection中,初始化列表中最后是m_bc_peerhandler的初始化,根据之前的猜测,其多半和回调函数有关,去看看吧:

是一个简单的抽象基类,从函数名称来看,肯定和peer的添加与删除有关,先忽略具体的细节,继续分析Connection函数体中的内容,首先是setTimeoutMs,明显是设置超时时间的,函数细节如下:

然后是两个setParent,有点奇怪了,我们现在已知发送线程和接收线程都是继承了Thread的,但是在setParent中却传入了this,去看看具体的函数实现:

在上文我们猜测m_connection是让发送线程与接收线程回调Connection中的函数的,目前来看,猜测应该是正确的,在Connection中的最后两个start显然就是启动线程了,去看看吧:

发送线程与接收线程的start调用的都是继承自Thread中的start,在该函数中,进来就加了个锁,说明整个函数都属于临界区域,然后是一个if判断,根据m_running来采取不同的行为,m_running在上文分析过了,其默认值为false,然后将m_request_stop设置成了false,含义应该就是不需要暂停,下面又有一把锁,注释信息的含义是:“如果线程正在被重启,该互斥锁(mutex)可能已经被锁定了。待办/疑问:如果操作失败了怎么办?或者如果它已经被同一个线程锁定了又该怎么办?这把锁应该就是解决这个问题的,然后在try中,真正的申请了线程,使用了C++标准库中的线程,传入了两个参数,其中threadProc显然就是线程要执行的函数,而this就是传入的参数,也就是说这个threadProc必然是重点,来看看吧:

首先this就是传递给thr的,在函数体中将current_thread设置成了thr,current_thread不是成员变量,那么多半是全局的,即:

是一个Thread对象,不过应该是加了锁,目前还无法确定实际用途,暂时忽略,然后显然就是设置线程的名称并注册到日志中了,先暂时忽略日志模块,注意到最关键的run,线程执行任务的循环一定就位于其中,要注意的是,Thread是一个抽象基类,有非常多在子类都继承了它,每一个继承Thread的类中必然都实现了run,我们在此处重点要关注的是发送线程的run和接收线程的run,先来看看发送线程的:

意料之中的,是一个非常复杂的函数,我们先忽略上方那些前置的设置,看到最重要的主循环:

好吧,主循环也非常复杂,一点点分析吧,首先是循环条件的判断,使用了两个函数,看名称应该是检测否需要暂停和检查阻塞队列中是否有报文的,去看看吧:

stopRequested是Thread中的函数,而packetsQueued显然是ConnectionSendThread特有的,首先是stopRequested函数,直接返回了m_request_stop,这印证了上文的猜想,m_request_stop确实是用于停止线程的,然后是packetsQueued函数,首先是调用了getPeerIDs,应该是获取所有链接的id的,函数如下:

返回了m_peer_ids,该字段在上文就分析过了,是Connection类的成员变量,类型就是vector<session_t>的,并且还发现是使用m_connection调用的,那么猜测正确,m_peer_ids确实是存储所有链接id的,m_connection也确实是给线程调用链接对象中的函数的,也就是每一个链接都对应一批线程,每一个线程都有一个回指向链接的指针,并通过该指针调用链接中的函数,然后是两个判空,含义应该就是如果发送阻塞队列中有报文,并且此时还有链接,那么就返回true,下面是一个循环,遍历peerIds,在服务端的角度,对应的应该就是遍历此时与服务端链接的所有客户端,在遍历中,又使用了m_connection调用线程对应链接中的函数,并且又出现了一个新的PeerHelper类型,去看看吧:

PeerHelper是对Peer的封装,Peer是一个非常复杂的类,我们暂且先不关注,看到getPeerNoEx函数,显然是在m_peers中查找对应的Peer并交给迭代器node,然后返回了使用查找到的Peer指针构造出来的一个PeerHelper,回到packetsQueued,后面是两个判断,显然语义就是如果peer为空,那么说明对应的peerId是无效的,就继续遍历下一个,如果peer无法为转换为UDPpeer,那么也继续循环,注意到&被PeerHelper重载了,因此&不是取出了一个PeerHelper对象的地址,而是返回了底层的Peer对象的指针,又由于使用的是dynamic_cast,是专门用于将基类类型转换为子类类型的,因此可以推测UDPPeer必然是继承了Peer的,去看看吧:

结果如我们所料,并且还指定了final,表明final是不可被继承的,因此上方判断的含义就是如果Peer无法被转换成UDPPeer,那么就继续遍历下一个Peer,下面又是一个遍历,并且又出现了新的类型Channel,看名称应该与管道有关系。从语法上来看应该是遍历每一条管道,然后查看管道的阻塞队列中是否存在信息,如果存在,那么就返回true,Channel是一个非常复杂的类,我们先只看我们需要的,也就是Channel中的queued_commands:

是一条队列,队列中存放的元素是ConnectionCommandPtr类型,该类型我们在上文大概猜测过,下面去大概看看其中的主要成员变量:

可以看到最终的类型是ConnectionCommand,该类型一定与链接指令有关,目前先不深挖其细节,回到packetsQueued中,可以确定该函数只有两种情况会返回true,其一是发送队列不为空并且存在对应的链接,其二是链接命令队列不为空,再回到发送线程的主循环中:

可以判断,进入循环的条件确实就是线程没有暂停与阻塞队列中存在报文了,下面进入循环体,要说明的是,这里的调用链非常复杂,涉及到了大量的类,我们必须抓住一个核心,那就是无论封装的有多么复杂,最终要把信息send出去,就必须得调用系统调用sendto函数,这才是最终的目的,其它所有的工作都是为了辅助该工作而进行的,因此先不管上层的封装,让我们找到调用sendto的地方在哪里,注意到循环体末尾的sendPackets函数,显然该函数与send有关,函数实现如下:

该函数非常长,笔者只截取了函数中的主循环,同样的,先找与send相关的函数,显然就是rawSendAsPacket了,去看看吧:

该函数同样非常长,笔者只截取了与send有关的部分,可以发现还是没有sendto,不过有两个名字带send的函数,先来看看上方那个sendAsPacketReliable:

可以发现sendAsPacketReliable最终也是调用了rawSend,只不过封装了一下p,我们有理由推断,在rawSend函数就是真正调用sendto函数的地方,去看看吧:

注意到线程使用了回执行Connection的指针访问到了其中的成员变量m_udpSocket,该成员变量是保护的,而线程类又没有继承链接类,那么线程类应该就是链接类的友元:

确实如此,而m_udpSocket在上文分析过了,是UDPsocket类型的,其中的Send函数如下:

好的,找到sendto了,报文最终就是从这里发送出去的,发送报文前的预备工作必然是非常复杂的,先不管那些细节,回到Connection类中:

下方对于接收线程的启动必然也是类似的逻辑,可以推断在UDPsocket类中应该存在一个Recv函数:

确实如此,目前我们以及分析了大量的类,是时候进行一次总结了,笔者将把已经分析的部分绘制成一张类图:

类图代码如下:

@startuml title 链接管理层类图 class IConnection{ {abstract} Serve(bind_addr : Address) void {abstract} Connect(address : Address) void {abstract} Send(peer_id : session_t, channelnum : u8, pkt : NetworkPacket, reliable : bool) void } class Connection{ + Connection(max_packet_size : u32, timeout : float, ipv6 : bool, peerhandler : PeerHandler*) # m_udpSocket : UDPSocket # m_command_queue : MutexedQueue<ConnectionCommandPtr> - m_event_queue : MutexedQueue<ConnectionEventPtr> - m_sendThread : std::unique_ptr<ConnectionSendThread> - m_receiveThread : std::unique_ptr<ConnectionReceiveThread> - m_bc_peerhandler : PeerHandler* - m_peers : map<session_t, Peer *> } note top of Connection : final class note top of Connection : session_t->u16 class UDPSocket{ + UDPSocket(ipv6 : bool) + init(ipv6 : bool, noExceptions : bool) bool + Send(dsetination : const Address&, data : const void*, size : int) void + Receive(sender : Address&, data : void*, size : int) int + setTimeoutMs(timeout_ms : int) void } class Thread{ # m_name : string - m_joinalbe : bool - m_request_stop : atomic<bool> - m_running : atomic<bool> - m_mutex : mutex + Thread(name : const string&) + start() bool + stop() bool + wait() bool # {abstract} run() void* } class ConnectionSendThread{ - m_connection : Connection - m_max_packet_size : unsigned int - m_timeout - m_outgoint_queue : queue<OutgoingPacket> + ConnectionSendThread(max_packet_size : unsigned int, timeout : float) + setParent(parent : Connection*) void + sendPackets(dtime : float, peer_packet_quota : u32) void + sendAsPacketReliable(p : BufferedPacketPtr&, channel : Channel*) void + rawSend(p : const BufferedPacket*) void + run() void* } class ConnectionReceiveThread{ + setParent(parent : Connection*) void + run() void* } class PeerHandler{ + {abstract} peerAdded(peer : IPeer*) void + {abstract} deletingPeer(peer : IPeer*, timeout : bool) void } class Peer{ } class PeerHelper{ - m_peer : Peer } class UDPPeer{ # channels[] : Channel } note top of UDPPeer : final class class Channel{ + queued_commands : deque<ConnectionCommandPtr> } ' 类关系 Connection --|> IConnection : public ConnectionSendThread --|> Thread : public ConnectionReceiveThread --|> Thread : public UDPPeer --|> Peer : public Connection *-- UDPSocket Connection *-- ConnectionSendThread Connection *-- ConnectionReceiveThread Connection *-- PeerHandler Connection *-- Peer PeerHelper *-- Peer UDPPeer *-- Channel @enduml
http://www.cnnetsun.cn/news/2899553.html

相关文章:

  • 基于PLC的电镀生产线控制系统设计31(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_可以扫码或者私信
  • 智慧树刷课插件终极指南:3分钟实现学习自动化,提升300%学习效率
  • 【机器学习】(1)—— 线性回归
  • 新手避坑指南:用Arduino UNO和TB6600驱动42步进电机,从接线到调试的全流程记录
  • STM32H750裸机跑LVGL 8.2驱动480×480 RGB屏,三线SPI接GT9147触控
  • DataGrip 2024.1新版本上手:5个隐藏功能让SQL调试和数据分析快人一步
  • 假设检验实战指南:从p值误解到业务决策落地
  • Spring Boot 3.4落地:原生AI成企业标配?
  • Spring Cloud 熔断器与降级策略:从雪崩效应到弹性自愈,微服务的防护体系
  • Claude推理卸载层:零感知成本的动态计算分流技术
  • 魔兽争霸III终极兼容方案:WarcraftHelper一键解决现代系统六大兼容性问题
  • 基于BERTopic的跨文化心理量表简化方法与实践
  • 告别手动测试:如何用CANoe的Interactive Generator和Trace窗口高效模拟与排查总线故障
  • OnmyojiAutoScript终极指南:阴阳师全自动托管解决方案
  • 徐子崴新歌《故乡的四季》全网发布,一缕乡愁一生羁绊!
  • How LLMs Actually Work:一篇值得精读的 LLM 内部机制长文
  • 如何为欧洲卡车模拟2添加自动驾驶功能:ETS2LA车道保持辅助完整指南
  • 超越Demo:用TI IWR6843和Industrial Visualizer GUI,打造你自己的室内人员计数与轨迹追踪应用
  • 大模型应用开发工程师入门指南:小白也能学会的AI岗位,收藏这份学习攻略!
  • RK3568底板屏幕接口设计避坑:为什么你的MIPI屏引脚定义总对不上?
  • 九大网盘直链下载助手LinkSwift:告别限速困扰的终极指南
  • Houdini Vellum Solver SOP保姆级配置指南:从布料解算到流体模拟的完整参数解析
  • 别再只会用示波器了!用LabVIEW自制调制信号发生器,深入理解AM/FM/PM原理
  • 企业品牌展厅设计策略与落地 | 让展厅成为品牌最有说服力的“自我介绍“
  • 从Kafka到Iceberg:一个Flink 1.16实时数据入湖的完整配置与避坑指南
  • 别再让Cesium点位图标糊成马赛克了!手把手教你高清图标与自定义弹窗的完整配置
  • 手把手教你给戴尔R740服务器配置RAID1和RAID5(保姆级图文)
  • 从“电通量”到“高斯定理”:用Python模拟电场分布,直观理解大学物理电磁学核心
  • 给汽车ECU上把锁:手把手带你玩转UDS 0x27安全访问服务(附报文分析)
  • Genshin FPS Unlocker深度解析:打破60帧限制的完整实践指南