迷之16ms

最近在测试游戏内网络模块的传输效率时发现一个诡异的问题:

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!

迷之16ms_第1张图片
发包测试.png

利爷感觉可能和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个),收包也是非常均匀的,没有分批接收的现象,并且第一个包的延迟也很小。


迷之16ms_第2张图片
linux测试.png

那基本石锤是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的误差。。


迷之16ms_第3张图片
GetTickCount.png

也就是说之前写的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);

}
迷之16ms_第4张图片
GetTickCount测试.png

WDNMD这也太坑了。。

换成通过QueryPerformanceCounter QueryPerformanceFrequency这组函数来计算精确的毫秒数,重新测试windows的demo程序:

int GetCurMillisSecond()
{
    LARGE_INTEGER tm;
    LARGE_INTEGER freq;
    QueryPerformanceCounter(&tm);
    QueryPerformanceFrequency(&freq);
    return 1000.0 * tm.QuadPart / freq.QuadPart;
}
迷之16ms_第5张图片
Query测试.png

嚯嚯嚯,这下看上去正常了。

既然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);

}
迷之16ms_第6张图片
NOW_MS.png

又回到最初的起点,呆呆地站在镜子前

当我回到项目中再测了一次网络引擎时,新的怪事又出现了:

原来的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的谜团,终于解开了

你可能感兴趣的:(迷之16ms)