最近在测试游戏内网络模块的传输效率时发现一个诡异的问题:
A进程通过网络引擎封装的tcpsocket发送一个6字节的包(2Byte的标识符+4Byte的时间戳)给B进程,从发包到收包总会有15-16ms的延迟
收包延迟的计算方式:
a.发送方调用EvDataTime::NOW_MS()函数获取当前系统时间(毫秒),将这个值写入包中,通过tcpsocket发送出去
b.接收方从数据包中读取时间值,并调用EvDataTime::NOW_MS()得到收包时间,差值为收包延迟
网络引擎在windows系统下是调用winsock的send|recv接口实现数据传输。引擎跑在游戏主循环的tick中,本身不对帧数做限制(实际测试时我设置每秒1000帧,即理论上send和recv会在1ms以内被执行)
所以这16ms的延迟远远超出了预期。
排查的第一步:
脱离网络引擎,用winsock裸写一个demo程序,测试一下send|recv的实际性能
代码如下:
客户端发送部分
DWORD curTime = 0, endTime = 0;
int sendCnt = 0;
char str[100];
while(scanf("%d",&sendCnt) != EOF)
{
curTime = GetTickCount() % 100000;
_itoa(curTime, str, 10);
int len = strlen(str);
str[len] = ' ';
str[len+1] = '\0';
for(int i = 0; i < sendCnt; i++)
{
send(conSock, str, sizeof(str), 0);
}
endTime = GetTickCount() % 100000;
printf("Send Cost Time %d ms\n", endTime - curTime);
}
服务器接收部分
int recvCnt = 0;
char buf[102400];
while(1)
{
memset(buf,0,sizeof(buf));
recv(newConnection, buf, sizeof(buf), 0);
recvCnt++;
DWORD sendTime = GetMsgTime(buf);
DWORD recvTime = GetTickCount() % 100000;
printf("%d Recv Cost %d ms\n", recvCnt, recvTime - sendTime);
}
测试的结果却更加的诡异:
每次发少量的包时,延迟是0ms,并没有像项目中延迟16ms
但每次批量发送多个包(比如50个),会发现收发包的时间出现了“批次性”,并且每一批次之间的间隔刚好是15-16ms!
利爷感觉可能和tcp的socket设置项中的TCP_NODELAY有关,简单来说就是TCP为了传输效率,使用了一种优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块后再发送
于是我直接在注册表中禁用了这个优化策略后再次测试,发现对我的demo程序没有任何的影响。
暂时排除了tcp发包优化的问题后,开始怀疑是否是windows系统的io策略的问题,于是写了一份linux版的测试程序
代码如下:
客户端发送部分
struct timeval tv;
char str[100];
int cnt;
while(1)
{
scanf("%d",&cnt);
gettimeofday(&tv,NULL);
int ms = (tv.tv_sec*1000 + tv.tv_usec/1000)%100000;
snprintf(str,sizeof(str),"%d",ms);
int ll = strlen(str);
str[ll] = ' ';
str[ll+1] = '\0';
for(int i = 0; i < cnt; i++)
{
send(sockfd, str, strlen(str),0);
}
}
服务器接收部分
struct timeval tv;
int cnt = 0;
while(1)
{
if(recv(client_sockfd,buf,sizeof(buf),0)<=0)
break;
gettimeofday(&tv,NULL);
int ms = (tv.tv_sec*1000 + tv.tv_usec/1000)%100000;
int sendTime = atoi(buf);
cnt++;
printf("%d recv cost %d ms\n",cnt, ms-sendTime);
}
测试之后发现linux下的收包有一些差异:
1.不管有没有设置TCP_NODELAY,批量发送多个小包(1000个)都会在一个recv中被接收,而在windows下同样数量的包需要调用多得多次数的recv才能被处理完
2.当批量发包的数量非常大时(5000个),收包也是非常均匀的,没有分批接收的现象,并且第一个包的延迟也很小。
那基本石锤是windows系统的问题了。
虽然缩小了问题的范围,但依旧没有具体的排查方向。
开始在google上xjb搜。。。
看上去最奇怪的是16这个值,因为1000/60=16.7,似乎是和将1s钟切片为60份有关。
然后我就搜到了这个东西
The default timer resolution on Windows is 15.6 ms – a timer interrupt 64 times a second. When programs
increase the timer frequency they increase power consumption and harm battery life. They also waste
more compute power than I would ever have expected – they make your computer run slower! Because
of these problems Microsoft has been telling developers to not increase the timer frequency for years.
并且有贴吧大佬直接支持GetTickCount这个函数本身就有15-16ms的误差。。
也就是说之前写的windows版的测试程序是不准确的
测试了一下GetTickCount():
for(int i = 0; i < 20; i++)
{
int beginTime = GetTickCount();
Sleep(5);/// 挂起5ms
int endTime = GetTickCount();
printf("Cost Time: %d\n", endTime - beginTime);
}
WDNMD这也太坑了。。
换成通过QueryPerformanceCounter QueryPerformanceFrequency这组函数来计算精确的毫秒数,重新测试windows的demo程序:
int GetCurMillisSecond()
{
LARGE_INTEGER tm;
LARGE_INTEGER freq;
QueryPerformanceCounter(&tm);
QueryPerformanceFrequency(&freq);
return 1000.0 * tm.QuadPart / freq.QuadPart;
}
嚯嚯嚯,这下看上去正常了。
既然winsock没问题,那是不是项目中的16ms也是由于EvDataTime::NOW_MS()底层调用了GetTickCount()导致了时间精度的误差呢?
由于EvDataTime::NOW_MS()是封装在lib库中的,看不到代码实现,只能用之前的Sleep方法测试,然而结果却是EvDataTime::NOW_MS()的精度看上去是没问题的。
for(int i = 0; i < 20; i++){
UINT64 beginTime = EvDataTime::NOW_MS();
Sleep(5);/// 挂起5ms
UINT64 endTime = EvDataTime::NOW_MS();
printf("Cost Time: %d\n", 5);
}
又回到最初的起点,呆呆地站在镜子前
当我回到项目中再测了一次网络引擎时,新的怪事又出现了:
原来的15-16ms减少到了4-5ms
WTF?冷静回忆一下,这个时候我应该是把注册表中的TCP_NODELAY打开了。
关闭TCP_NODELAY再次测试,果然又回到了15-16ms。嗯。。。说好的并不会影响winsock呢?
另一方面,winsock的send和recv本身应该是没有问题的,那范围就进一步缩小到网络引擎封装在windows下实现的部分
考虑到这一点,向大佬发起求救。。金海看了一下引擎的basetick,然后发现虽然tick对帧数没有做限制,但是会检查前后两次tick之间的时间差,若为0则直接return
而tick中计算时间差也是用的EvDataTime::NOW_MS(),这个函数底层调用的是timeGetTime()这个windowsAPI
然后从官方说明里看到了这样一句话:
The default precision of the timeGetTime function can be five milliseconds or more, depending on the machine.
也就是说EvDataTime::NOW_MS()还是有一个5ms的误差。。。而之前我的测试程序sleep太久了导致没有发现这个问题
这就导致网络引擎在windows下的basetick实际上达不到我设置的1000帧(最小间隔为5ms)
收包延迟=发包进程tick+传输时间+收包进程tick
这里的tick时间影响了收包延迟的,再加上实际计算结果时又会存在5ms的精度误差,最后导致了15-16ms的延迟
而TCP_NODELAY确实会让发包速度变快,虽然可能没有5ms这么夸张,却会让一个包在更早的tick中发出去,而接收方也能在更早的tick中收到,里外里就产生了较大的差距
而裸写的winsock由于没有在tick中做收发包,所以不会有这样的差异。
16ms的谜团,终于解开了