多人联机游戏中联网模块(Socket)的设计和各种问题解决

  • Post author:
  • Post category:其他



这是我自己做的一个多人联机游戏中网络部分的总结。全部为自己全新做的,没用开源软件(有一个网络游戏开源软件Raknet)。目的是写一个属于自己的可靠网络模块,修改、扩展后在很多地方都能用得着。也想

自己从上至下完全写一遍,对网络编程有更深的理解。代码因为某些限制不方便公布。这里把设计和遇到的问题跟大家分享。



1

设计、实现


1.1

设计总原则


先保证可靠性,保证各种特殊情况下所有主从机逻辑的一致性。这是必须保证的,否则各机逻辑任何不一致游戏就完全失效了。


可靠性得到完全保障后,再考虑速度、流畅问题,逐步优化到高速。


1.2

架构设计


分层架构:主循环层,

Game

层和

Net

层。

Net

层封装了游戏协议和

socket

,提供简单接口给应用层。

Game

层逻辑中,只把

Net

层当做一种普通的输入输出。


Game



Main Loop

仿照

HGE

游戏引擎的方式,主循环只做事件处理和时间间隔计算,具体的每帧逻辑由

Game



FrameProc

成员函数完成。

由于公司网络无法上传图片,这里不能给出架构图了。


1.3

发送操作还是状态


如果发送的是操作,会出现如下的问题:

A



B



C

三个机器,主机收到某个机器的操作后发送给所有机器,因为网络差别每个机器收到的时间早晚不一样,会造成逻辑不一致。例如某角色在惯性下向前走,然后玩家发送了一个停止操作,

A

机器在角色走了

10

米后收到,

B

机器在角色走了

8

米后收到,

C

机器在角色走了

12

米后收到,三个机器中,这个角色停止的位置就不一致了,逻辑失效。


所以不能发送纯操作数据,而应该发送状态或状态变化量这样的数据。发送状态,例如某机中操作角色向左走到了

(x,y)

处,并处于出拳状态,把这个坐标和状态发送到所有机器,各机逻辑才能保证一致。发送状态变化量,例如(角色向左移动

1

米,向上移动

2

米)这样的数据。


1.4

多线程


1.4.1


开多少线程



总原则:


由于接收操作是一个阻塞操作,所以每一个接收都要开一个线程。





Client

端:


比较简单,就是一个线程用来创建连接和发送,一个线程用来接收。





Server

端:


一个线程用来

侦听和接受

新客户端的连接,每接受一个

Client

端就创建一个线程接收来自该

Client

的数据。




Server



Client

发送消息,最好创建线程专门用于发送,而不能直接在主线程中调用

send

发送。

因为

send

函数会阻塞线程运行,因为如果协议正在发送缓冲区中的数据,

send

函数会等待协议将数据发送完毕才把数据加入缓冲区。


Server

端发送线程的三种做法:


1.

为每个客户端创建一个发送线程。

——目前采用的这个做法


好处:入参一次性在线程入口带入,方便。并且可以利用多核的处理性能。


坏处:需要为每个发送线程创建一个信号量。


2.

对所有客户端共用一个发送线程。


好处:不用为每个发送线程创建一个信号量


坏处:入参没法儿一次性带入了,因为要确定消息发送给哪个

Client

需要传递进去。另外由于单线程发送多请求,可能需要建立一个发送链表,并且可能需要有锁来保护该跨线程变量。


3.开辟线程池,所有客户端发送公用线程池


好处:充分发挥CPU效率并减少资源消耗。


坏处:实现的复杂度高,出现问题后不容易调试。





1.4.2


实现方式




POSIX



pthread

实现,以实现跨平台(

Windows



Linux



Tizen

)可移植。


1.4.3


多线程交互问题



多线程交互,复杂性极高且因为锁和信号量极大影响了处理效率。

例如:

ClientsList

的锁,导致了很多死锁问题,主要集中在

client



server

连接或断开连接时。


解决方法:只让一个线程操作

ClientsList

,就可以简单清晰地避免死锁问题。最好的解决方法就是用“生产者



消费者模式”把

CNetServer

做成一个事件处理线程,让

CNetServer

只接收事件来处理自己的数据,这样就安全了,不用再考虑多线程访问

CNetServer

数据的问题。


学习了“网络多线程编程”和“同步异步

IO

”的理论知识,从理论上解决该问题。根据“生产者



消费者”模式,做了“事件处理线程类”和“普通线程类”,让发送线程和

CNetServer

从“事件处理线程类”派生出来,侦听和接收线程从“普通线程类”派生。



1.4.4


关闭线程



1.4.5


线程共享数据的锁



总原则:


所有线程间共享数据都需要加锁。在有效率问题并且确认多线程不会同时操作此共享数据时,才可以不加锁访问。


经过“生产者



消费者模式”的改造,目前线程间共享数据已经很少了。




1.5


Socket


1.5.1


主动优雅地关闭

Socket

连接


我们的程序里,需要在

server



client

主动关闭时能有效去除相应资源,目的是为了在

player

进入或离开的操作后,不给系统留下残余。因为目前使用的

recv

是阻塞式的,所以在主动退出时,需要先调用

shutdown(socket, SD_BOTH)

关闭连接,

recv

才能退出,否则会一直阻塞在那里,接收线程无法结束。同样,

accept

也是阻塞的,主动退出时要

shutdown(serverSocket)



closeSocket(serverSocket)

之后

accpet

才能退出。




下面是主动优雅关闭

Socket

连接的方法:


如果要求待未发送完的数据发送出去后再关闭

socket

,一种方法是(

MSDN

上推荐):


关闭

Socket

时先

shutdown(socket, SD_BOTH)

,再

closesocket



close



但也有说

Linux

下这样还是会丢数据的,可能

Linux



Windows

不同?


所以也有另一种方法是进行如下

Socket

设置:


struct linger {


u_short l_onoff;


u_short l_linger;


}m_sLinger;


m_sLinger.l_onoff = 1;//

在调用

closesocket

()时还有数据未发送完,允许等待。若

m_sLinger.l_onoff=0

;则调用

closesocket

()后强制关闭


m_sLinger.l_linger = 5; //

设置等待时间为

5


setsockopt(s, SOL_SOCKET, SO_LINGER,     (const char*)&m_sLinger,sizeof(linger));


然后在需要关闭

socket

的时候

closesocket(Windows)



close(linux)




下面是原理解释:


从函数调用上来分析(

msdn)

:一旦完成了套接字的连接,应当将套接字关闭,并且释放其套接字句柄所占用的所有资源。真正释放一个已经打开的套接字句柄的资源直接调用

closesocket

即可,但要明白

closesocket

的调用可能会带来负面影响,具体的影响和如何调用有关,最明显的影响是数据丢失,因此一般都要在

closesocket

之前调用

shutdown

来关闭套接字。

shutdown:

为了保证通信双方都能够收到应用程序发出的所有数据,一个合格的应用程序的做法是通知接受双发都不在发送数据!这就是所谓的“正常关闭”套接字的方法,而这个方法就是由

shutdown

函数

,

传递给它的参数有

SD_RECEIVE,SD_SEND,SD_BOTH

三种,如果是

SD_RECEIVE

就表示不允许再对此套接字调用接受函数。这对于协议层没有影响,另外对于

tcp

套接字来说,无论数据是在等候接受还是即将抵达,都要重置连接(注意对于

udp

协议来说,仍然接受并排列传入的数据,因此

udp

套接字而言

shutdown

毫无意义)。如果选择

SE_SEND,

则表示不允许再调用发送函数。对于

tcp

套接字来说,这意味着会在所有数据发送出并得到接受端确认后产生一个

FIN

包。如果指定

SD_BOTH

,答案不言而喻。

closesocket:

对此函数的调用会释放套接字的描述,这个道理众所周知(凡是经常翻阅

msdn

的程序员),因此,调用此函数后,再是用此套接字就会发生调用失败,通常返回的错误是

WSAENOTSOCK

。此时与被

closesocket

的套接字描述符相关联的资源都会被释放,包括丢弃传输队列中的数据!!!!对于当前进程中的线程来讲,所有被关起的操作,或者是被挂起的重叠操作以及与其关联的任何事件,完成例程或完成端口的执行都将调用失败!另外

SO_LINGER

标志还影响着

closesocket

的行为,但对于传统的

socket

程序,这里不加解释


因此可以可以看出

shutdown

对切断连接有着合理的完整性。


下面从

tcp

协议上来分析

shutdown



closesocket

的行为(

behavior)



closesocket



shutdown(

使用

SD_SEND

当作参数时)

,

会向通信对方发出一个

fin

包,而此时套接字的状态会由

ESTABLISHED

变成

FIN_WAIT_1

,然后对方发送一个

ACK

包作为回应,套接字又变成

FIN_WAIT_2

,如果对方也关闭了连接则对方会发出

FIN

,我方会回应一个

ACK

并将套接字置为

TIME_WAIT

。因此可以看出

closesocket,shutdown

所进行的

TCP

行为是一样的,所不同的是函数部分,

shutdown

会确保

windows

建立的数据传输队列中的数据不被丢失,而

closesocket

会冒然的抛弃所有的数据,因此如果你愿意

closesocket

完全可以取代

shutdown,

然而在数据交互十分复杂的网络协议程序中,最好还是

shutdown

稳妥一些!?有关

TCP

协议的连接原理清访问




RFC793

号文件


对于



关闭连接




socket

连接的关闭分为

:”

优雅关闭







强制关闭

“;


MSDN

上有说明:

closesocket

的关闭动作依赖于

socket

选项

SO_LINGER



SO_DONTLINGER;(SO_DONTLINGER

为缺省值

),

其含义如下:


选项


阻塞时间


关闭方式


等待关闭与否


SO_DONTLINGER

不关心


优雅



SO_LINGER




强制



SO_LINGER

非零


优雅



MSDN

上,还有说明:为了确保数据能被对方接收,应用程序应当在调用

closesocket

之前调用

shutdown


closesocket

应最后被调用,以便系统释放

socket

句柄及相关资源。



windows

下,

socket

的客户端执行

closesocket(clientSocket)

时,就是关闭了

socket

连接,此时客户端会给服务端发送一个

recvLen



0

的包来通知服务端。

TCP

建立连接以后,双方就是对等的。不论是哪一方,只要正常

close(socket_handle)

,那么

TCP

底层软件都会向对端发送一个

FIN

包。

FIN

包到达对方机器之后,对方机器的

TCP

软件会向应用层程序传递一个

EOF

字符,同时自动进入断开连接流程(要来回协商几次,但这些都是自动的、不可控的)。什么是

EOF

字符?它其实什么也不是,只是一个标记,上层应用程序如果这时读

socket

句柄的话,就会读到

EOF

,也就是说,此时

socket

句柄看起来里面有数据,但是读不出来,因此

select

返回可读(非阻塞模式下)

read

不会阻塞(阻塞模式下)但是

read

的返回值却是

0


如果此时不是读操作而是写操作,并且此时

socket

已经断开连接,那么

write

函数会返回

-1

且置

errno



EPIPE

(如果忽略了

SIGPIPE

信号的话)或者引发

SIGPIPE

信号(如果没忽略的话)


1.5.2


TCP

心跳机制(断网检测)


所谓的心跳包就是客户端定时发送简单的信息给服务器端告诉它我还在而已。代码就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息如果服务端几分钟内没有收到客户端信息则视客户端断开。




TCP

的机制里面,本身是存在有心跳包的机制的,也就是

TCP

的选项:

SO_KEEPALIVE

。系统默认是设置的

2

小时的心跳频率。

TCP



KeepAlive

默认是不打开的。


IM

软件一般自己实现心跳机制,因为自己实现的心跳机制通用,可以无视底层的

UDP



TCP

协议。


参考文章:

http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO


http://blog.sina.com.cn/s/blog_7a3510120101775w.html


我们自己实现心跳很简单:只需要

send

或者

recv

一下,如果结果为零或

SOCKET_ERROR

,则为掉线。



1.5.3


网络延迟


注意网络延迟

!=

网速慢,而是指网元相隔太远,传输节点太多导致收报时间



发报时间的时间差。


魔兽对战的网络流量,是十几

K

每秒,发送的数据并不大。


如果发送状态数据的话,游戏中有上百个对象,按每秒

20

帧速发送,每对象

20byte

数据,则每秒需要发送

80K



还有职业玩家专门分析在延迟情况下怎么打。


对于关键动作,为了逻辑的一致性,都是采用服务器统一分发命令的方式来严格保证逻辑,例如击中处理。


只有在普通的移动时,才会用“航迹推算算法”这样的

p2p+

推算

+

纠正的方式



1.5.4


网络多线程设计模式


lf leader/followers

是分布式系统底层常用的快速分离网络请求的设计模式。


poco c++

是一套风格清爽,易读易学的开源基础库。目前比较遗憾的是网络


核心中没有实现

lf leader/followers

设计模式。本人对

poco c++



reactor

模式


加以适应多线程的改造,实现了

lf leader/followers

设计模式,希望对想要开发


高效多线程网络应用的同学有所帮助。代码下载地址分别是

:


google code: http://code.google.com/p/lfreactor/downloads/list


sourceforge: http://sourceforge.net/projects/lfreactor/?source=directory



1.5.5


Socket

选项(属性)设置




Socket

连接之前(

Server

端就是

accept

之前,

client

端就是

connect

之前),调用

setsockopt

设置

Socket

一些属性。




设置

send



recv

的超时


int nNetTimeout = 1000; //1


//

发送时限


setsockopt(socket,SOL_SOCKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));


//

接收时限


setsockopt(socket,SOL_SOCKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));




设置

send



recv

缓冲区大小


//

接收缓冲区


int nRecvBuf = 32 * 1024; //

设置为

32K


setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));


//

发送缓冲区


int nSendBuf = 32*1024; //

设置为

32K


setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));


//

在发送数据的时,不执行由系统缓冲区到

socket

缓冲区的拷贝,以提高程序的性能:


int nZero = 0;


setsockopt(socket,SOL_SOCKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));


//

在接收数据时,不执行将

socket

缓冲区的内容拷贝到系统缓冲区:


int nZero = 0;


setsockopt(s,SOL_SOCKET,SO_RCVBUF,(char *)&nZero,sizeof(int));


1.6

游戏通信协议


游戏通信协议主要是为了解决“

TCP

粘包问题”,也为了解决各种控制流程的问题。问题详见“极端、例外情况及处理”章节。


协议报文结构如下:


Packet Length


Controller


PlayerID


Action


Data


16bit


8bit


8bit


8bit


不定长


Packet Length

代表总报文长度,包括报文头和数据的长度。


Controller

代表该报文要做什么,目前有如下可取值:


typedef enum


{


LOG_IN = 1,


LOG_IN_ACK,


LOG_OUT,


PLAYER_JOIN,      //

新玩家加入


PLAYER_LEAVE,     //

玩家离开


SERVER_DISCONNECT,//

断开与服务器连接


MOVE,             //

角色动作


}GAME_PROTOCOL_CONTROLLER;


PlayerID

代表玩家的角色

ID


Action

代表子动作,目前尚未启用


Data

是报文携带的数据。



1.7

发送队列


发送队列是为了解决操作速率、发送速率、接收速率的不匹配问题。问题详见“极端、例外情况及处理”章节。目前发送线程已经通过“生产者



消费者模式”拥有了队列机制。



实现:


1.


发送的数据先插入队列,发送线程从队列取数据进行发送。——测试完成


2.


发送队列满时,让发送线程停止接收和发送。目前实现方式是用

::Send()

函数的返回值返回

false

来表示队列满,处理方式如下:


while ( false == CEventProcThread::Send(event) )


{


sleep_msec(50);


}



测试结果:


模拟

Server

操作比发送快的场景(在

Server

端发送线程中,每发送完一包进行延时

0.5

秒,人为造成发送速度比操作速度慢),看

Client

还能否保证逻辑一致

——测试通过,现在能保证逻辑一致了。


模拟

Client

操作比发送快的场景(在

Client

端发送线程中,每发送完一包延时

0.5

秒),看

client

能否阻塞主线程操作

——测试通过,能阻塞



模拟

Server

处理能力低下的场景(

Server

端接收线程每接收一包延时

100ms

,再把发送速度每发送一包延时更大,

500ms

,让发送速度低于接收速度)。看

client

端逻辑还能否和

server

端保持一致

——测试通过,现在能保证逻辑一致了。


1.8

极端、例外情况及处理


1.8.1


TCP

粘包问题


TCP

是流式协议,当发送较快时,会出现多个包并成一个包发送的情况。例如先发送“

luojiao

”,再发送“

lishanshan

”,有可能会合并成“

luojiaolishanshan

”一起发送。这时接收方直接取数据作为数据结构就不对了,例如接收方以

12byte

为单位

recv

数据,就会取出“

luojiaolisha

”和“

nshan

”两个错误的数据,而不是预期的先取出“

luojiao

”后取出“

lishanshan

”。


在服务端和客户端都在本地时,不容易粘包,因为实际上没走网络。而服务端和客户端不在一个机器时,就很容易出现这个问题,就普通的

30

帧率就会出现。



解决方法:


通过应用层协议解决。核心目的是要确定包长,好从

TCP

流中解析出各个应用层报文来。

——已解决


1.8.2


断网和离线处理:


目前用的

TCP

协议,本身是面向连接的会有心跳保活机制,通过

send



recv

的返回值即可判断是否断网。


断网或离线后,要做一系列相应处理:


1.


Game

删除此角色对象


2.


NetServer

去掉该

client

对象,回收

playerID

。清空对该

client

的发送队列。


3.


正常关闭对该

client

的发送和接收线程。



如果用

UDP

协议,那就要自己实现心跳机制和超时来判断断网了。



——正在进行中,遇到困难。







1.8.3


发送速率低于操作速率问题


当网络较慢或瞬时拥塞时,虽然发送缓存

buf

(这个

buf

指我们自己的主线程和发送线程传递数据的

buf

,不是

Socket

本身的发送缓存)做了互斥锁处理不会导致发送出错误的数据,但是会出现某些“包丢失”的现象,发送出

0

长度空包。其实不是包丢失了,而是由于主线程填写

buf

比发送线程发送

buf

要快(也就是发送速率低于帧率),第一次填写

buf

后,在发送线程还没来得及发送出这个

buf

,主线程又写了多次

buf

并给了多个信号量,此时发送线程获取多个信号量循环多次,就只发出了最后一个

buf



memset

之后的多个空

buf




经过分析有如下几种场景会出现问题:



场景

1




Server



10

个用户操作,

Server

本机处理了这

10

个操作。发给

Client 10

个包,有

5

个包因为发送线程太慢而被缓冲区清空没有发出去,就造成

Client

端只收到和处理了

5

个操作,而

Server

端处理了

10

个操作,两者逻辑不一致了。



模拟方法:



Server

端发送线程中,每发送完一包进行延时

1

秒,人为造成发送速度比操作速度慢,即可模拟出此场景。下图是模拟出的场景,可以看出

Client

端红圈的位置已经与

Server

端不一致了。



场景

2




Server

端收到

Client

端的操作,分发给所有

Client

端时,有的

Client

端发送的正常,有的

Client

端因网络慢发送有空包,导致各机逻辑不一致。其实该问题跟场景

1

本质上是一样的,都是有的机器收到并处理正常,有的收到并处理的包少,导致各机逻辑不一致。



场景

3




Client

端把自己操作发送到

Server

的发送慢倒不会产生问题,因为

Client

端是收到

Server

端回发的操作才处理,不是直接本机处理操作。这样只要

Server

的发送不出问题,各

Client

端仍然收到一致的操作(一致的比实际操作少),逻辑仍然一致,只是会发送慢的该

Client

端会感觉不好因为自己的很多操作没有生效。



模拟方法:


client

端发送线程中,每发送完一包进行延时

1

秒,人为造成发送速度比操作速度慢,即可模拟出此场景。下图是模拟出的场景,经过长期操作,发送慢的

Client1

,和正常的

Server



client2

的逻辑不会出现不一致的情况,只是

client1

的很多操作没有生效。




解决方法:




1. Server

端发送的数据,对每个

client

建立一个发送队列,保证每个被处理的操作都发送到所有

Client

端,从而使各机逻辑一致。如果网络一直慢使某个发送队列满了,就通知主线程让其停止处理本机操作和发送,通知接收线程停止接收所有

client

操作和发送,此时所有用户操作无效。

等发送队列中所有包都发送出去后,再通知主线程和接收线程恢复正常。


这样解决了场景

1

中操作被

Server

处理却没被发送的问题,因为每个被

Server

处理的操作,都有队列机制保证被发送到

Client

端。解决了场景

2

中各

Client

收到操作不一致的问题,因为

Server

端为每个

Client

建一个发送队列,

Server

发给有任何

client

的发送队列满无法再发送,就通知

Server

不再处理所有

client

端的操作。


2.

为了让游戏体验更友好,发送队列满导致

Server

停止处理时,

Server

可以界面提示“等待谁谁中”,并发送一个“那个谁谁卡了,都别操作了等着吧”的消息给所有

client

端,

client

端界面提示“等待谁谁中”。这样会更友好,不然所有用户会发现自己的操作一直不生效也不知道为什么,体验不好。


该解决方法会引发另一个疑问:

Server

停止处理所有

Client

操作,但

Client

端仍然在发送,那

Server

接收各

Client

端的接收

Socket

缓存会不会溢出?该问题用

TCP



Socket

时不会出现,因为

TCP

本身的拥塞控制会在接收方

Socket

缓存满时自动控制发送方不再发送。此时发送方即

client



send()

会阻塞,使得发送线程阻塞,

client

主线程因为共享数据的锁也会阻塞,缓存不会溢出,

client

也不会再响应操作和发送。等

Server

恢复正常后,会再读取处理接收缓存,

client

端也恢复正常发送。通过

Server

接收线程一段时间不

recv()

,很容易模拟出该场景,经测试确实是上面所说的结果。所以不用担心这个问题。




3.client

端发送给

Server

最好也建立发送队列。

虽然经过场景

3

的分析,

Client

端把自己操作发送到

Server

的发送慢不会产生逻辑不一致问题,只是

client

会很多操作没有生效。但

client

端有些关键操作不能失效,例如加入游戏等。增加这个队列也让游戏更友好一些,在发送队列满时,提示用户网络阻塞,并发送一个包插到队列头尽早通知

Server

让其暂停游戏。其实就是异步可靠

IO

的概念,加入了队列后是异步可靠的,不会丢失的

IO

,外界用起来更方便、放心些。


1.8.4


接收速率高于处理速率问题


如果机器处理能力不足,出现接收速率高于处理速率问题,例如每秒接收了

50

个包,却只处理得了

20

个包,会出现什么问题?



场景

1




Client

端处理能力不足,

Server

端发送来的包,来不及处理。

Client



Socket

缓冲区满之前,表现出卡顿就是响应自己和别人的动作都不及时。直到

client

端的

Socket

缓存满时,

Server

端会

send()

函数被阻塞,并导致

Server

主线程因为锁也被阻塞。

Server

做了发送队列也一样,

Server

会因为发送队列满而停止处理。最终表现就是因为

Server

的阻塞或停止处理,其他

client

端也阻塞不响应用户操作,并不会出现逻辑不一致问题。


从本质上来说,这种场景的接收速率高于处理速率,与发送方发送速率低于操作速率是一样的。



模拟方法

:让某一个客户端接收处理包时进行延时,就可以模拟出此场景。下图是模拟出的结果,反应慢

client

缓冲区满之前,

Server



Client

处理和操作都正常,反应慢

client

上表现出自己和别人的操作都卡顿,滞后,但最终仍能保证一致。反应慢

client

缓冲区满后,

Server

和所有

Client

都会被阻塞(因为

Server

阻塞了,不再响应

client

的操作请求)。下面是模拟的结果,上图是

client

表现出卡顿滞后,下图是最终所有端仍然保证了逻辑一致。


反应慢

client

表现出卡顿、滞后:


最终仍能保证一致:



解决方法:


可以不用解决,因为不会导致逻辑不一致。


如果想做得更友好一些,可以将

Socket

的接收缓存设置得小一些,让处理速度慢的

client

能尽早缓冲区满从而阻塞

Server

。否则别人都很流畅操作

High

得很,自己却由于操作响应慢而被整惨。




场景

2




Server

端处理能力不足,各

client

端发来的包,来不及处理和发出。这样会使所有

client

端延迟收到操作,所有

client

会觉得所有角色都“行动缓慢”并且最终由于

server

的接收缓存满而被阻塞。如果

Server

发送速度高于接收速度和自身操作速度,那只是会卡,但不会造成逻辑不一致问题。而如果

Server

发送速度不够,就可能会丢失给

client

的包(因为覆写发送区),并且各对

client

丢的包不一致,从而导致各机逻辑不一致。如果有发送队列那么可以避免此逻辑不一致问题。


可以看出该场景下,接收速率高于处理速率本身不会造成逻辑不一致问题。只有牵扯到发送速率时,就会跟“

Server

端发送速率低于操作速率问题”一样的原因造成各机逻辑不一致。



模拟方法:


Server

端接收线程每帧延时(如

100ms

),就可以模拟出来。为了模拟

Server

发送速度低于接收速度,那就再把发送速度每帧延时更大(如

1s

),下图是模拟结果,可以明显看出

client

端逻辑已经和

server

端不一致了。



解决办法:


跟“

Server

端发送速率低于操作速率问题”一样,

Server

端发送的数据,对每个

client

建立一个发送队列,保证每个被接收的操作都发送到所有

Client

端,从而使各机逻辑一致。


1.9

速度优化


现在有

4

个及以上

player

时,连续操作时会有某些客户端延迟的现象。






可能原因

1



打印导致的处理速度慢。


因为每帧都打印,

printf

系统调用是比较耗时的。




可能原因

2



由于加太多锁导致线程间相互等待,

CPU

利用率不高。从理论上来说就是“计算操作和

IO

操作的并行化程度低”。尤其是

Server



Game

在收到

client

消息操作

m_role

时,对所有角色数据加锁可能是较大的等待消耗。


解决方法:使用双缓冲区技术。

M_role

对每个角色分别加锁,而不是每次全锁住。




可能原因

3



队列缓冲区的频繁堆内存申请和释放。


解决办法:用环形队列,使用固定的一块内存,避免频繁申请释放内存。




可能原因

4



开了太多的线程,线程调度开销太大。目前

Server



2+clientNum*2

个线程,当

client

数到

6

个时,

Server

就要开

14

个线程。


解决方法:用异步

IO

,这样

Server

端发送和接收可以各只用一个线程,

Server

就只用开

4

个线程,但这样又可能没有充分发挥

CPU

效率。另一种方法是用线程池,线程数量保持在“

CPU

最大核心数

+

某常量”。




版权声明:本文为luojiaoqq原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。