当前位置:首页 > 多人在线游戏架构实战 pdf(构建多人在线实战游戏编程教学)
多人在线游戏架构实战 pdf(构建多人在线实战游戏编程教学)
作者:拳真游戏解说 发布时间:2022/11/20 16:11 阅读量:717

本书主要讲述大型多人在线游戏开发的框架与编程实践,以实际例子来介绍从无到有地制作网络游戏框架的完整过程,让读者了解网络游戏制作中的所有细节。全书共12章,从网络游戏的底层网络编程开始,逐步引导读者学习网络游戏开发的各个步骤。

本书通过近50个真实示例、近80个流程图,以直观的方式阐述和还原游戏制作的全过程,涵盖了网络游戏设计的核心概念和实现,包括游戏主循环、线程、Actor模式、定时器、对象池、组件编码、架构层的解耦等。

本书既可以作为网络游戏行业从业人员的编程指南,也可以作为大学计算机相关专业网络游戏开发课程的参考书。

关键词:网络游戏,游戏程序,程序设计

1.1节介绍了单机游戏与网络游戏的区别。

1.2和1.3小节带你理解IP地址和TCP/IP。

1.4小节介绍了阻塞网络编程。

课程完整版可前往UWA学堂观看《多人在线游戏架构实战:基于C++的分布式游戏编程》

1.5 非阻塞网络编程
本节提供一个非阻塞功能的例子,要达到的目的如下:

(1)客户端与服务端建立网络通信。

(2)建立通信之后,客户端发起3个线程,分别向服务端发送不同数据。这些数据是ping_0、ping_1和ping_2。

(3)服务端收到数据,将相同数据转发给客户端。

(4)客户端收到数据,验证并打印结果。

本例在基础收发数据的功能上增加了非阻塞的设置,虽然看似只有细微改变,但其逻辑会变得非常不一样。

1.5.1 工程源代码
工程的源代码在本书源代码库的01_02_network_nonblock目录下,使用make-all脚本进行编译之后,在
01_02_network_nonblock/bin目录下会生成clientd和serverd两个文件,先启动serverd,再启动clientd。先来看一下这个非阻塞工程的执行结果,见表1-7。


表1-7 非阻塞式网络通信运行结果

在Linux下,按Ctrl+C组合键可退出服务端进程。执行本例时,每一次产生的结果都不一样。因为是线程操作,客户端发送数据的前后关系不确定,所以服务端收到数据的顺序也不确定。为了便于区分,将数据按到达服务端的时间分成了1组到3组,每一组数据在同一个Socket通道上。虽然这里只有几行简单的数据打印,但有几个关键点需要理解。

关键点1:Socket值的重用

从表1-7中可以看到,客户端首先发送了两条数据,在Socket值为3和4的通道中分别发送了ping_2和ping_1。

服务端首先收到了ping_2的数据。在打印信息时显示了一个Socket值,在Linux上,Socket的值是线性增加的,因为程序在启动时会用到前面的几个描述符,所以当前可用描述符是从4开始的。这意味着服务端建立第一个连接时,它的Socket值为4,数据是ping_2,服务端收到了该数据并发送了相同的数据给客户端。接着服务器收到了ping_0的数据,Socket值依然是4。

这里读者一定有疑问,不是说不同的通道值不一样吗?为什么两个Socket却用了一样的值?这是因为Socket是可以重用的,如果关闭了描述符为4的Socket,当有新的连接到服务端时会将描述符4重新分配给它。

从上面的数据中可以看出,当服务端收到ping_2并将它发送出去之后,会马上关闭Socket 4,这时又收到ping_0,Socket 4被再次分配。

关键点2:Socket值是进程级数据

认真观察会发现,客户端发送ping_2的时候,在客户端用的Socket值为3,为什么服务端接收到的Socket值却是4?客户端在发送ping_1的时候也使用了Socket 4,服务端和客户端使用了相同的Socket 4值,为什么没有出错?

这里需要区分一个概念,Socket值是在进程中独立的,不是通用的。它不像端口,就某个端口而言,一台物理机就只有一个,同一时间不可重用。但同一时间不同的进程可能存在相同的Socket值的通道。在客户端的Socket 3和在服务端的Socket 3不是同一个通道,只是值相同而已。

关键点3:网络数据的无序与有序

在客户端,从打印结果可以看出,先发送了ping_1,再发送了ping_0。但是在服务端,收到数据的时候却正好相反,先收到了ping_0,后收到了ping_1,这和发送数据的顺序不一致。网络数据的收发是无序的,两个连接即使同时发送数据,也有可能这次你先到,下次我先到。但同一个Socket连接,如果先发了Msg0,再发送Msg1,在TCP下,服务端收到的数据一定是有序的,必定是先收到Msg0,再收到Msg1。

在网络不稳定的时候,在TCP机制下,包丢失会重发。如果Msg0发送失败引起了重发,Msg1很有可能比Msg0先到达目标机器,这种情况是有可能存在的,但完全没必要担心,因为在TCP底层,有一套可靠的机制对收到的消息进行重新排序。如果出现错误,Socket连接就会抛出异常,也就是常说的玩家断线。

1.5.2 服务端代码分析
下面分析这个非阻塞的源代码,看看它是如何实现的,与阻塞的代码又有什么不同。还是先从服务端代码开始。

关键点1:Socket初始化

与前一个例子相比,服务器创建Socket、绑定IP地址、监听Socket的流程并没有发生变化,只看重点代码:

_sock_init();
SOCKET socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
...
_sock_nonblock(socket);
在服务端,Socket创建成功之后调用了一个新宏:_sock_nonblock。该宏设置Socket的属性,把阻塞模式变为非阻塞模式,在Windows下和Linux下的调用函数各不相同。宏对比如下(该宏定义在network.h文件中):

#ifndef WIN32
#define _sock_nonblock( sockfd ) { int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); }
#else
#define _sock_nonblock( sockfd )
{ unsigned long param = 1; ioctlsocket(sockfd, FIONBIO, (unsigned long *)¶m); }
#endif
在Windows下,函数ioctlsocket的目的是对Socket执行命令操作。在本例中,将Socket设置为非阻塞。函数ioctlsocket的说明可以在MSDN网上查到。原型如下:

int ioctlsocket(SOCKET sock, long cmd, u_long *argp);
在Linux下,调用fcntl,其原型如下:

int fcntl(int sock, int cmd, ...);
在Linux下,Socket被认为是一个文件描述符,可以用read、write、open等IO操作来操作它。而fcntl是对文件描述符进行属性操作的函数。在上面的宏定义中,首先传入参数F_GETFL,取得文件描述符的属性并保存到flags变量中,再在取得的属性之上增加一个O_NONBLOCK属性,即非阻塞模式。

关键点2:如何接收连接请求与收发数据

在上一个阻塞例子中,如果使用断点调试,就会发现在调用::accept、::recv和::send函数时会一直卡住,等待数据。现在,在非阻塞模式下,不论有没有数据,这些函数都会马上返回。在主线程中,不知道什么时候能收到::accept、::recv数据,因此做了个死循环。先浏览一下代码:

while (true) {
SOCKET newSocket = ::accept(socket, &socketClient, &socketLength);
if (newSocket != INVALID_SOCKET) {
_sock_nonblock(newSocket); // 接收到一个新的连接请求,设为非阻塞
sockets.push_back(newSocket);
}
// 遍历所有连接,调用::recv函数,查看是否有数据到来
// 如果有,就调用::send函数将接收到的数据发送回去
auto iter = sockets.begin();
while (iter != sockets.end()) {
SOCKET one = *iter;
auto size = ::recv(one, buf, 1024, 0);
if (size > 0) {
::send(one, buf, size, 0);
iter = sockets.erase(iter);
...
}
...
}
}
在循环中,收到新的连接便把新的Socket值保存起来。每一帧不停地主动询问在这个Socket上是否有数据,如果接收到数据,就发送一段相同的数据到客户端。

修改一下代码,把while循环去掉,再执行一下,会发现程序开始执行就退出了。因为所有的底层函数都不会再等待数据,没有数据就直接返回了。

如果在while循环中加一个计数并打印出来,就会发现一秒可以执行无数次的检查。非阻塞模式保证了每个Socket的收发都在同时进行,互不影响。

1.5.3 客户端代码分析
在客户端,为了保证多组Socket互不影响地发送数据,用到了线程。将每个连接包装成一个ClientSocket类,其定义在client_socket.h文件中。

每一个ClientSocket类的功能类似于1.4节中阻塞的例子,发送数据,阻塞等待数据到来,接收到数据,然后退出。图1-9展示了客户端与服务端通信的整体流程。


图1-9 非阻塞式网络通信流程

关键点1:ClientSocket类

为了让客户端互不影响,在client.cpp文件的main函数里创建了多个ClientSocket实例,每个实例都是在线程中执行的,即使在本线程中是阻塞的,也不会影响其他线程。ClientSocket类一共有4个函数,其中1个是构造函数。定义如下:

class ClientSocket {
public:
ClientSocket(int index); // 开启了一个线程
void MsgHandler(); // 阻塞式的Socket收发数据流程
bool IsRun() const; // 收发数据是否已完成
void Stop(); // 结束线程
private:
bool _isRun{ true };
...
};
ClientSocket类的构造函数实现如下:

ClientSocket::ClientSocket(int index) : _curIndex(index) {
_thread = std::thread([index, this]() {
_isRun = true;
this->MsgHandler();
_isRun = false;
});
}
在创建ClientSocket类时创建了一个线程,在线程中调用了MsgHandler函数。

函数MsgHandler的思路和前一个例子中的一样,即创建Socket,发送一条数据,再等待接收一条数据,使用的还是阻塞模式,这里不再重复。处理完数据之后,线程退出。

总之,ClientSocket类使用了线程的方式发送数据。

关键点2:主线程逻辑

在主线程中创建了3个ClientSocket实例,使用while不断循环查看每个ClientSocket实例是否还处于IsRun运行模式下,如果已经完成,就调用ClientSocket的Stop函数,直到全部ClientSocket类都退出,程序结束。

int main(int argc, char *argv[]) {
std::list<std::shared_ptr<ClientSocket>> clients;
// 启动3个线程
for (int index = 0; index < 3; index++) {
clients.push_back(std::make_shared<ClientSocket>(index));
}
// 遍历当前所有ClientSocket类,如果已完成数据收发,就关闭线程,剔除数组
while (!clients.empty()) {
auto iter = clients.begin();
while (iter != clients.end()) {
auto pClient = (*iter);
if ((*pClient).IsRun()) {
++iter;
continue;
}
pClient->Stop();
iter = clients.erase(iter);
}
}
return 0;
}
主线程并不关心ClientSocket类中做了些什么,只是关心它的生命周期,每个线程结束了,整个程序就认为结束了。

特别说明:std::thread是C++标准线程库,在创建线程时使用了一个Lambda匿名函数,std::thread执行匿名函数。如果读者没有接触过Lambda匿名函数,就要学习一下Lambda匿名函数与常规代码的对比。下面的代码中,TestThread1使用了匿名函数,TestThread2则使用的是常规代码。

void TestThread1() {
bool isrun = true;
auto thread = std::thread([index, &isrun]() {
printf("call test1.\n");
isrun = false;
});
while (isrun) {
while (thread.joinable()) {
thread.join();
}
}
}
void TestThread2() {
bool isrun = true;
auto thread = std::thread(&CallFunc, &isrun);
while (isrun) {
while (thread.joinable()) {
thread.join();
}
}
}
void CallFunc(bool* pIsRun) {
printf("call test2.\n");
*pIsRun = false;
}
两个函数中代码的目的完全一样,一个使用Lambda匿名函数给线程注册了一个调用函数,而另一个则编写了一个常规函数注册到线程中。Lambda匿名函数使我们的代码显得更为简洁。

1.5.4 小结
在本例中,服务端使用了非阻塞模式接收数据。简单来说,就是你接收你的数据,我发送我的数据,互不影响。有数据到来就处理,至于对方是何时发过来的,接收数据方并不关心。

本例采用了主动询问数据的方式在服务端判定有没有数据直接调用::recv函数,每一个循环中都要对每一个Socket调用::recv函数,当返回值大于0时,认为有数据到来。

课程完整版可前往UWA学堂观看《多人在线游戏架构实战:基于C++的分布式游戏编程》

https://edu.uwa4d.com/course-intro/0/382

本书特色
1、从网络游戏的底层编码开始,深入讲解游戏开发的详细步骤、游戏主循环、线程的使用、Actor模式的应用等。

2、以直观的方式阐述和还原游戏制作的全过程,全面介绍游戏编码过程中众多的核心概念和具体实现,如定时器、对象池、组件编码、架构层的解耦等。

3、使用C++来实现游戏的架构,读者也可以举一反三,使用其他的编程语言轻松实现游戏开发目标。

你将获得
1、充分了解业务逻辑和底层框架的设计意图

2、立足实践的服务端学习思路,深入浅出

3、用实际案例贯穿各知识点,在实践中学习

4、了解商业游戏的设计思路和实现方法

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 3385344009@qq.com 举报,一经查实,本站将立刻删除。
下载天玑谷App
抢先玩0.1折好游戏
领取游戏好礼
扫码下载安卓版天玑谷手游APP
天玑谷游戏 - 抢先玩好游戏
一款高颜值都在用的游戏聚合APP
立即下载
多人在线游戏架构实战 pdf(构建多人在线实战游戏编程教学)

多人在线游戏架构实战 pdf(构建多人在线实战游戏编程教学)

作者:拳真游戏解说 发表时间:2022/11/20 16:11 阅读量:717

本书主要讲述大型多人在线游戏开发的框架与编程实践,以实际例子来介绍从无到有地制作网络游戏框架的完整过程,让读者了解网络游戏制作中的所有细节。全书共12章,从网络游戏的底层网络编程开始,逐步引导读者学习网络游戏开发的各个步骤。

本书通过近50个真实示例、近80个流程图,以直观的方式阐述和还原游戏制作的全过程,涵盖了网络游戏设计的核心概念和实现,包括游戏主循环、线程、Actor模式、定时器、对象池、组件编码、架构层的解耦等。

本书既可以作为网络游戏行业从业人员的编程指南,也可以作为大学计算机相关专业网络游戏开发课程的参考书。

关键词:网络游戏,游戏程序,程序设计

1.1节介绍了单机游戏与网络游戏的区别。

1.2和1.3小节带你理解IP地址和TCP/IP。

1.4小节介绍了阻塞网络编程。

课程完整版可前往UWA学堂观看《多人在线游戏架构实战:基于C++的分布式游戏编程》

1.5 非阻塞网络编程
本节提供一个非阻塞功能的例子,要达到的目的如下:

(1)客户端与服务端建立网络通信。

(2)建立通信之后,客户端发起3个线程,分别向服务端发送不同数据。这些数据是ping_0、ping_1和ping_2。

(3)服务端收到数据,将相同数据转发给客户端。

(4)客户端收到数据,验证并打印结果。

本例在基础收发数据的功能上增加了非阻塞的设置,虽然看似只有细微改变,但其逻辑会变得非常不一样。

1.5.1 工程源代码
工程的源代码在本书源代码库的01_02_network_nonblock目录下,使用make-all脚本进行编译之后,在
01_02_network_nonblock/bin目录下会生成clientd和serverd两个文件,先启动serverd,再启动clientd。先来看一下这个非阻塞工程的执行结果,见表1-7。


表1-7 非阻塞式网络通信运行结果

在Linux下,按Ctrl+C组合键可退出服务端进程。执行本例时,每一次产生的结果都不一样。因为是线程操作,客户端发送数据的前后关系不确定,所以服务端收到数据的顺序也不确定。为了便于区分,将数据按到达服务端的时间分成了1组到3组,每一组数据在同一个Socket通道上。虽然这里只有几行简单的数据打印,但有几个关键点需要理解。

关键点1:Socket值的重用

从表1-7中可以看到,客户端首先发送了两条数据,在Socket值为3和4的通道中分别发送了ping_2和ping_1。

服务端首先收到了ping_2的数据。在打印信息时显示了一个Socket值,在Linux上,Socket的值是线性增加的,因为程序在启动时会用到前面的几个描述符,所以当前可用描述符是从4开始的。这意味着服务端建立第一个连接时,它的Socket值为4,数据是ping_2,服务端收到了该数据并发送了相同的数据给客户端。接着服务器收到了ping_0的数据,Socket值依然是4。

这里读者一定有疑问,不是说不同的通道值不一样吗?为什么两个Socket却用了一样的值?这是因为Socket是可以重用的,如果关闭了描述符为4的Socket,当有新的连接到服务端时会将描述符4重新分配给它。

从上面的数据中可以看出,当服务端收到ping_2并将它发送出去之后,会马上关闭Socket 4,这时又收到ping_0,Socket 4被再次分配。

关键点2:Socket值是进程级数据

认真观察会发现,客户端发送ping_2的时候,在客户端用的Socket值为3,为什么服务端接收到的Socket值却是4?客户端在发送ping_1的时候也使用了Socket 4,服务端和客户端使用了相同的Socket 4值,为什么没有出错?

这里需要区分一个概念,Socket值是在进程中独立的,不是通用的。它不像端口,就某个端口而言,一台物理机就只有一个,同一时间不可重用。但同一时间不同的进程可能存在相同的Socket值的通道。在客户端的Socket 3和在服务端的Socket 3不是同一个通道,只是值相同而已。

关键点3:网络数据的无序与有序

在客户端,从打印结果可以看出,先发送了ping_1,再发送了ping_0。但是在服务端,收到数据的时候却正好相反,先收到了ping_0,后收到了ping_1,这和发送数据的顺序不一致。网络数据的收发是无序的,两个连接即使同时发送数据,也有可能这次你先到,下次我先到。但同一个Socket连接,如果先发了Msg0,再发送Msg1,在TCP下,服务端收到的数据一定是有序的,必定是先收到Msg0,再收到Msg1。

在网络不稳定的时候,在TCP机制下,包丢失会重发。如果Msg0发送失败引起了重发,Msg1很有可能比Msg0先到达目标机器,这种情况是有可能存在的,但完全没必要担心,因为在TCP底层,有一套可靠的机制对收到的消息进行重新排序。如果出现错误,Socket连接就会抛出异常,也就是常说的玩家断线。

1.5.2 服务端代码分析
下面分析这个非阻塞的源代码,看看它是如何实现的,与阻塞的代码又有什么不同。还是先从服务端代码开始。

关键点1:Socket初始化

与前一个例子相比,服务器创建Socket、绑定IP地址、监听Socket的流程并没有发生变化,只看重点代码:

_sock_init();
SOCKET socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
...
_sock_nonblock(socket);
在服务端,Socket创建成功之后调用了一个新宏:_sock_nonblock。该宏设置Socket的属性,把阻塞模式变为非阻塞模式,在Windows下和Linux下的调用函数各不相同。宏对比如下(该宏定义在network.h文件中):

#ifndef WIN32
#define _sock_nonblock( sockfd ) { int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); }
#else
#define _sock_nonblock( sockfd )
{ unsigned long param = 1; ioctlsocket(sockfd, FIONBIO, (unsigned long *)¶m); }
#endif
在Windows下,函数ioctlsocket的目的是对Socket执行命令操作。在本例中,将Socket设置为非阻塞。函数ioctlsocket的说明可以在MSDN网上查到。原型如下:

int ioctlsocket(SOCKET sock, long cmd, u_long *argp);
在Linux下,调用fcntl,其原型如下:

int fcntl(int sock, int cmd, ...);
在Linux下,Socket被认为是一个文件描述符,可以用read、write、open等IO操作来操作它。而fcntl是对文件描述符进行属性操作的函数。在上面的宏定义中,首先传入参数F_GETFL,取得文件描述符的属性并保存到flags变量中,再在取得的属性之上增加一个O_NONBLOCK属性,即非阻塞模式。

关键点2:如何接收连接请求与收发数据

在上一个阻塞例子中,如果使用断点调试,就会发现在调用::accept、::recv和::send函数时会一直卡住,等待数据。现在,在非阻塞模式下,不论有没有数据,这些函数都会马上返回。在主线程中,不知道什么时候能收到::accept、::recv数据,因此做了个死循环。先浏览一下代码:

while (true) {
SOCKET newSocket = ::accept(socket, &socketClient, &socketLength);
if (newSocket != INVALID_SOCKET) {
_sock_nonblock(newSocket); // 接收到一个新的连接请求,设为非阻塞
sockets.push_back(newSocket);
}
// 遍历所有连接,调用::recv函数,查看是否有数据到来
// 如果有,就调用::send函数将接收到的数据发送回去
auto iter = sockets.begin();
while (iter != sockets.end()) {
SOCKET one = *iter;
auto size = ::recv(one, buf, 1024, 0);
if (size > 0) {
::send(one, buf, size, 0);
iter = sockets.erase(iter);
...
}
...
}
}
在循环中,收到新的连接便把新的Socket值保存起来。每一帧不停地主动询问在这个Socket上是否有数据,如果接收到数据,就发送一段相同的数据到客户端。

修改一下代码,把while循环去掉,再执行一下,会发现程序开始执行就退出了。因为所有的底层函数都不会再等待数据,没有数据就直接返回了。

如果在while循环中加一个计数并打印出来,就会发现一秒可以执行无数次的检查。非阻塞模式保证了每个Socket的收发都在同时进行,互不影响。

1.5.3 客户端代码分析
在客户端,为了保证多组Socket互不影响地发送数据,用到了线程。将每个连接包装成一个ClientSocket类,其定义在client_socket.h文件中。

每一个ClientSocket类的功能类似于1.4节中阻塞的例子,发送数据,阻塞等待数据到来,接收到数据,然后退出。图1-9展示了客户端与服务端通信的整体流程。


图1-9 非阻塞式网络通信流程

关键点1:ClientSocket类

为了让客户端互不影响,在client.cpp文件的main函数里创建了多个ClientSocket实例,每个实例都是在线程中执行的,即使在本线程中是阻塞的,也不会影响其他线程。ClientSocket类一共有4个函数,其中1个是构造函数。定义如下:

class ClientSocket {
public:
ClientSocket(int index); // 开启了一个线程
void MsgHandler(); // 阻塞式的Socket收发数据流程
bool IsRun() const; // 收发数据是否已完成
void Stop(); // 结束线程
private:
bool _isRun{ true };
...
};
ClientSocket类的构造函数实现如下:

ClientSocket::ClientSocket(int index) : _curIndex(index) {
_thread = std::thread([index, this]() {
_isRun = true;
this->MsgHandler();
_isRun = false;
});
}
在创建ClientSocket类时创建了一个线程,在线程中调用了MsgHandler函数。

函数MsgHandler的思路和前一个例子中的一样,即创建Socket,发送一条数据,再等待接收一条数据,使用的还是阻塞模式,这里不再重复。处理完数据之后,线程退出。

总之,ClientSocket类使用了线程的方式发送数据。

关键点2:主线程逻辑

在主线程中创建了3个ClientSocket实例,使用while不断循环查看每个ClientSocket实例是否还处于IsRun运行模式下,如果已经完成,就调用ClientSocket的Stop函数,直到全部ClientSocket类都退出,程序结束。

int main(int argc, char *argv[]) {
std::list<std::shared_ptr<ClientSocket>> clients;
// 启动3个线程
for (int index = 0; index < 3; index++) {
clients.push_back(std::make_shared<ClientSocket>(index));
}
// 遍历当前所有ClientSocket类,如果已完成数据收发,就关闭线程,剔除数组
while (!clients.empty()) {
auto iter = clients.begin();
while (iter != clients.end()) {
auto pClient = (*iter);
if ((*pClient).IsRun()) {
++iter;
continue;
}
pClient->Stop();
iter = clients.erase(iter);
}
}
return 0;
}
主线程并不关心ClientSocket类中做了些什么,只是关心它的生命周期,每个线程结束了,整个程序就认为结束了。

特别说明:std::thread是C++标准线程库,在创建线程时使用了一个Lambda匿名函数,std::thread执行匿名函数。如果读者没有接触过Lambda匿名函数,就要学习一下Lambda匿名函数与常规代码的对比。下面的代码中,TestThread1使用了匿名函数,TestThread2则使用的是常规代码。

void TestThread1() {
bool isrun = true;
auto thread = std::thread([index, &isrun]() {
printf("call test1.\n");
isrun = false;
});
while (isrun) {
while (thread.joinable()) {
thread.join();
}
}
}
void TestThread2() {
bool isrun = true;
auto thread = std::thread(&CallFunc, &isrun);
while (isrun) {
while (thread.joinable()) {
thread.join();
}
}
}
void CallFunc(bool* pIsRun) {
printf("call test2.\n");
*pIsRun = false;
}
两个函数中代码的目的完全一样,一个使用Lambda匿名函数给线程注册了一个调用函数,而另一个则编写了一个常规函数注册到线程中。Lambda匿名函数使我们的代码显得更为简洁。

1.5.4 小结
在本例中,服务端使用了非阻塞模式接收数据。简单来说,就是你接收你的数据,我发送我的数据,互不影响。有数据到来就处理,至于对方是何时发过来的,接收数据方并不关心。

本例采用了主动询问数据的方式在服务端判定有没有数据直接调用::recv函数,每一个循环中都要对每一个Socket调用::recv函数,当返回值大于0时,认为有数据到来。

课程完整版可前往UWA学堂观看《多人在线游戏架构实战:基于C++的分布式游戏编程》

https://edu.uwa4d.com/course-intro/0/382

本书特色
1、从网络游戏的底层编码开始,深入讲解游戏开发的详细步骤、游戏主循环、线程的使用、Actor模式的应用等。

2、以直观的方式阐述和还原游戏制作的全过程,全面介绍游戏编码过程中众多的核心概念和具体实现,如定时器、对象池、组件编码、架构层的解耦等。

3、使用C++来实现游戏的架构,读者也可以举一反三,使用其他的编程语言轻松实现游戏开发目标。

你将获得
1、充分了解业务逻辑和底层框架的设计意图

2、立足实践的服务端学习思路,深入浅出

3、用实际案例贯穿各知识点,在实践中学习

4、了解商业游戏的设计思路和实现方法

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 3385344009@qq.com 举报,一经查实,本站将立刻删除。
相关攻略
更多