Socket的速率控制
(一)、目标
做一个以精确速率向外输出数据的数据源,要完成这个目标,最基础的是:
1、找到一种精确的计时器,在精确的时间范围内控制数据源以指定的速度向外发送数据。
2、通过对套接字选项和线程优先级的设置减少网络因素对发送速度造成的影响,从而提高发送精度,保证数据的实际发送量尽可能的达到指定的理论发送量。
针对第一个要求,通过寻找到一种时间精度达到微秒级的精确计数器来保证,在硬件支持的情况下可以通过WindowsAPI获取时钟频率以及震荡次数,通过在事件两端分别调用函数得到震荡次数的差值并结合时钟频率可以计算出精确的时间间隔,通过指定的传输速度和精确的延时可以计算出需要发送的数据量。对于第二个要求通过设置数据源所在线程的优先级,以及对套接字选项的设置来减小协议本身对数据传输的过多控制而造成的时延,从而使实际的数据发送量尽可能的接近理论值。
(一)、设置线程优先级
首先在主函数中创建线程函数DWORDWINAPI ThreadProc(LPVOIDlpParam),在线程函数中实现数据源的功能,线程创建成功后对线程优先级进行设置,windows下对线程优先级进行设置的API函数为BOOLWINAPI SetThreadPriority( __in HANDLE hThread, __in intnPriority),其中nPriority定义了线程的优先级。
|
线程优先级 |
标志 |
优先级值 |
1 |
IDLE(最低) |
THREAD_PRIORITY_IDLE |
如果进程优先级为realtime则调整为16,其他情况为1 |
2 |
LOWEST(低) |
THREAD_PRIORITY_LOWEST |
-2(在原有基础上-2) |
3 |
BELOW(低于标准) |
THREAD_PRIORITY_BELOW_NORAL |
-1(在原有基础上-1) |
4 |
NORMAL(标准) |
THREAD_PRIORITY_NORMAL |
不变(取进程优先级) |
5 |
ABOVE(高于标准) |
THREAD_PRIORITY_ABOVE_NORMAL |
+1(在原有基础上+1) |
6 |
HIGHEST(高) |
THREAD_PRIORITY_HIGHEST |
+2(在原有基础上+2) |
7 |
CRITICAL(最高) |
THREAD_PRIORITY_TIME_CRITICAL |
如果进程优先级为realtime则调整为31,其他情况为15 |
将线程优先级设置为最高级,即THREAD_PRIORITY_TIME_CRITICAL,排除本地其他进程的干扰,使得操作系统能够优先调度。
(二)、使用高精度计数器要实现精确的发送速度,需要有精确的时间控制,在一般情况下使用以下两种方式就满足要求了。一是用SetTimer函数建立一个定时器后,在程序中通过处理有定时器发送到线程的消息队列中的WM_TIMER消息,而得到定时的效果。二是利用GetTickCount函数可以返回计算机启动后的时间,通过两次调用GetTickCount函数,然后控制他们的差值来取得定时的效果。但是以上两种方法都是毫秒级的,在Windows内部有一个高精度运行计时器(high-resolution
performance counter),利用它可以获得高精度的定时间隔,其精度可以达到微秒级,其精度与CPU的时钟频率有关。利用API函数QueryPerformanceFrequency能够得到这个定时器的频率。利用API函数QueryPerformanceCounter能够得到定时器的当前值,根据要延时的时间和定时器的频率,能够算出定时器要经过的周期数,循环指定的周期数,就达到了高精度定时的目的。以下是这个API函数的原型:
- BOOLQueryPerformanceFrequency(LARGE_INTEGER *lpFrequency);
作用:返回硬件支持的高精度计数器的频率。
返回值:如果硬件支持高精度计数器则返回频率值,若硬件不支持返回零。
- BOOLQueryPerformanceCounter(LARGE_INTEGER*lpPerformanceCount);
作用:得到高精度计时器的值。
返回值:如果安装的硬件支持高精度计时器则函数返回计数器的值,如果硬件不支持参数返回零。
采用这种方法的步骤如下:
-
首先调用QueryPerformanceFrequency函数取得高精度运行计数器的频率f,单位是每秒多少次(n/s),此数一般很大,在实验主机上此频率值为2241054。
-
在循环之外先调用一次QueryPerformanceCounter,得到计数器的值n1,在send之前调用循环调用QueryPerformanceCounter,得到计数器的值n2,两次数值的差值通过计数器的频率f换算成时间间隔,t=(n2-n1)/f。当t达到指定的大小后跳出循环,并将此时的n2赋给n1,重新进入循环计时,send发送的数据大小根据指定的速度与定时时间确定。
这种方法存在一个致命的问题,就是忙等,循环过程会占满CPU资源。因此需要调整一下算法,循环过程引入休眠机制,降低CPU占用,但是休眠机制由于操作系统调度的不确定性,会造成循环结束后的实际时间和预设时间会有差别。没有关系,我们在循环结束后调用QueryPerformanceCounter,重新计算本次和上次循环的精确时间差,运用这个时间来计算send发送的数据大小,这样保证输出的数据始终恒定。
(三)设置套接字选项
SO_SNDBUF:通过设置该选项可以改变发送缓冲区的大小,根据应用需求,设置合理的缓冲区大小能够有效提高数据发送的效率。
TCP_NODELAY:在默认情况下,Windows协议栈发送数据采用Nagle算法,这样做虽然提高了网络的吞吐量,但是实时性却降低了,如果设置了TCP_NODELAY选项,就会禁用Nagle算法,应用程序调用send发送的数据包会被立即投送到网络,而没有延迟。考虑到程序中需要做到精确发送的要求,所以将TCP_NODELAY选项设置为TRUE。
SO_DONTROUTE:通过设置该选项,可以绕过路由表中的网关所在的表项,设置了该套接字选项的socket可以将数据包不经网关发送,而是发往直接相连的主机。该套接字选项的合法的值是整数形式的布尔标志值,这里设为TRUE。
在Windows下,对套接字选项进行设置的API函数为
intsetsockopt(
__in SOCKET s,
__in int level,
__in int optname,
__in const char* optval,
__in int optlen
);
获取当先套接字选项值的API函数为
intgetsockopt(
__in SOCKET s,
__in int level,
__in int optname,
__out char* optval,
__inout int* optlen
);
通过以上两个函数可以查看并重新设置套接字选项的值。
当指定传输速度为64kb/s时,速度的波动控制在了0.002kb/s-0.02kb/s。
当指定传输速度为100kb/s时,速度的波动在0.001kb/s-0.02kb/s以内。
当指定传输速度为1000kb/s时,速度的波动控制在了0.015kb/s-0.2kb/s之内。
思考与改进:
Socket的通信,数据从发送方到接收方,影响速率的因素很多,主要包括以下几个方面:
1、操作系统调度。
2、其他网络进程对网络带宽资源的争抢。
3、协议本身,如果是TCP协议,则速率控制受到TCP协议流量控制和拥塞控制机制,链路层流量控制机制的影响。
4、协议包在发送和接收途中受到其他网络数据的挤占及丢失等。
我们只能在发送端的应用层以尽可能精确的速率向传输层送速率,然而数据经过传输层、IP层、链路层到物理层最终到网络上,不可控的因素太多。如果能够在网卡驱动层检测实际的进程数据流量,然后根据网卡流量来反馈控制数据投送量,将更为精确地实现速率的控制,具体实现需要进一步研究。