在开发过程中常常需要模拟各种带宽,以尽可能的模拟用户的状况,解决一些Corner的case。但是往往由于工具不力,我们费时费力还得不到很好的结果。关于带宽限制的软件还真不多,国外有一个Net Limiter不过要收费的。于是花了两天时间自己根据自己的需要简单的时间实现了一个,原理是使用LSP过滤,下面简单介绍一下。
1. 什么是LSP?
LSP – Layered Service Provider,官方的介绍很复杂,简单点说,就类似于Hook Socket的函数。比如你程序里面调用了WSASend在LSP里面就对应了一个WSPSend。
2. 从哪里开始?
Microsoft还是很强大的,提供了一个LSP的例子,大部分的程序也都是基于这个例子改造的了。例子的位置在Microsoft SDKs/Windows/v6.0/Samples/NetDs/winsock/LSP,是属于Windows SDK里面的一部分。
3. IFS vs NonIFS
说实话,我也没太明白,我是使用NONIFS的。引用一段:
“IFS LSP uses an actual OS I/O handle per socket, which means that handles passed from the LSPto the upper layer or application do not need translation.Non-IFS LSP allocates its own handle per socket, which means that handles passed from the LSPto the upper layer of application must be translated by the LSP when it passes all the commandsto its lower-layer LSP or base provider.It is important to understand that from an installer point of view, each LSP needs to be installeddifferently. Non-IFS LSP can be layered into any position in the stack. IFS LSP must be installedfirst after the base providers or after another IFS-based LSP. However, IFS LSP cannot beinstalled after a Non-IFS LSP. Another important point is that the installer needs to set a specialflag to denote that the IFS LSP is installed in the stack.”
4. 特别提醒:
在开始之前最好要备份一下注册表的以下项,虽然正常操作并无出现异常,但是万一出现意外可以恢复,呵呵:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/WinSock2
5. 安装LSP
MS的例子里面带的安装是INSTALL目录下面的工程,编译出来一个命令行程序,具体的使用帮助可以看里面的readme,但是在VISTA下面需要管理员权限的。我做的程序是一个MFC的程序,只要点一下INSTALL就可以安装了,UNINSTALL就卸载。我的安装是Overload了UDP和TCP,是根据名字查找的,我不能确定是否每个计算机都是一样的,如果发现不同可以稍微改下。
6. 进行带宽限制:
对UDP发送的带宽控制还是很简单的,我在WSPSend和WSPSendTo里面增加了带宽控制代码,这个一看就明白。但是接收带宽的控制就有些麻烦了,因为Socket的异步模式有一个完成端口,所以需要改WSPReceive, WSPReceiveFrom和IntermediateCompletionRoutine,其中IntermediateCompletionRoutine里面的代码我简单写了一下,没测试,因为我的项目不需要处理;)不过也应该是不难的。
BOOL CheckRecvBandwidth(DWORD dwRecvedBytes, SOCK_INFO* SocketContext) { if(m_wLimitUdpPort != 0 && m_wLimitUdpPort == SocketContext->wPort && SocketContext->protcol == IPPROTO_UDP) { if(m_udpReceiveFlow.wPort == 0) { m_udpReceiveFlow.wPort = m_wLimitUdpPort; m_udpReceiveFlow.dwFirstPacketTime = timeGetTime(); dbgprint("CheckRecvBandwidth Limit Initialized, limited bps = %d", m_dwLimitedRecvBps); } m_udpReceiveFlow.dwTotalBytes += dwRecvedBytes; double bps = 0; DWORD dwDiff = timeGetTime() - m_udpReceiveFlow.dwFirstPacketTime; if(dwDiff > 0) bps = m_udpReceiveFlow.dwTotalBytes * 8000.0 / dwDiff; // dbgprint("CheckRecvBandwidth: Send flow port=%d, total=%d dwDiff=%d, first=%d, bps=%d", m_udpReceiveFlow.wPort, m_udpReceiveFlow.dwTotalBytes, timeGetTime(), m_udpReceiveFlow.dwFirstPacketTime, long(bps)); if(bps > m_dwLimitedRecvBps) { dbgprint("CheckRecvBandwidth: Bandwidth exceed the limited, current bps = %d", int(bps)); m_udpReceiveFlow.dwTotalBytes -= dwRecvedBytes; return TRUE; } } return FALSE; }
调用的地方,要在调用底层的Provider的receive之后加上如下的代码:
SetBlockingProvider(SocketContext->Provider); ret = SocketContext->Provider->NextProcTable.lpWSPRecv( SocketContext->ProviderSocket, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno); SetBlockingProvider(NULL); if ( SOCKET_ERROR != ret ) { if(*lpNumberOfBytesRecvd > 0 && CheckRecvBandwidth(*lpNumberOfBytesRecvd, SocketContext)) { *lpNumberOfBytesRecvd = 0; *lpErrno = 10035; } SocketContext->BytesRecv += *lpNumberOfBytesRecvd; }
7. 进程通信
因为LSP是驻扎在目标进程的,因此我要控制带宽就需要进行多进程通信,我使用了共享内存的方法。
#pragma data_seg("SHARED") UDP_FLOW m_udpSendFlow = {0}; UDP_FLOW m_udpReceiveFlow = {0}; WORD m_wLimitUdpPort = 0; DWORD m_dwLimitedSendBps = 33 << 10; DWORD m_dwLimitedRecvBps = 56 << 10; #pragma data_seg() #pragma comment(linker, "/section:SHARED,RWS")
8. 关于调试:
因为这个没法直接调试,所以只能靠DBGVIEW进行查看了,另外,要卸载后更换需要重现启动系统,虽然有虚拟机帮忙,还是费时的事情。
9. 下载地址:
最后,我把我修改的程序和编译结果共享一下(微软的例子无工程,设置比较麻烦),使用VS2008的工程。
http://cppxml.googlecode.com/files/MyNetLimiter_src.rar
我的工作参考了此文:http://blog.donews.com/zwell/archive/2004/08/23/75748.aspx,以及作者barak@komodia.com的一些工作成果。