IPerf——网络测试工具介绍与源码解析(3)
1 DWORD WINAPI thread_run_wrapper( void* paramPtr ) 2 { 3 struct thread_Settings* thread = (struct thread_Settings*) paramPtr; 4 switch ( thread->mThreadMode ) 5 { 6 case kMode_Server: 7 { 8 server_spawn( thread ); 9 } break; 10 case kMode_Client: 11 { 12 client_spawn( thread ); 13 } break; 14 case kMode_Reporter: 15 { 16 reporter_spawn( thread ); 17 } break; 18 case kMode_Listener: 19 { 20 } break; 21 default: 22 { 23 FAIL(1, "Unknown Thread Type!\n", thread); 24 } break; 25 } 26 27 if ( thread->runNext != NULL ) 28 { 29 thread_start( thread->runNext ); 30 } 31 Settings_Destroy( thread ); 32 33 return 0; 34 } // end run_wrapper
【报告者线程 kMode_Reporter】
IPerf不管是在客户端还是在服务端,都会创建一个报告者线程,该线程是用来输出各种信息到控制台界面,根据其报告的内容可将信息分为五种类型,这些类型都在代码中做了定义标识
1 /* 2 * The type field of ReporterData is a bitmask 3 * with one or more of the following 4 */ 5 #define TRANSFER_REPORT 0x00000001 6 #define SERVER_RELAY_REPORT 0x00000002 7 #define SETTINGS_REPORT 0x00000004 8 #define CONNECTION_REPORT 0x00000008 9 #define MULTIPLE_REPORT 0x00000010
传输类型:数据传输过程中的数据体现,例如:
[ ID] Interval Transfer Bandwidth
[244] 0.0- 1.0 sec 131 MBytes 1.10 Gbits/sec
[244] 1.0- 2.0 sec 281 MBytes 2.36 Gbits/sec
[244] 2.0- 3.0 sec 310 MBytes 2.60 Gbits/sec
服务端返回类型:UDP模式下打印服务端返回的内容,主要为延迟抖动、丢包率的统计信息,例如:
[244] Server Report:
[244] 0.0-10.0 sec 1.25 MBytes 1.05 Mbits/sec 0.000 ms 0/ 893 (0%)
设置类型:对于客户端,打印连接的对端地址和连接的端口,对于服务端,打印监听连接的端口等,例如:
------------------------------------------------------------
Client connecting to 127.0.0.1, UDP port 5001
Sending 1470 byte datagrams, IPG target: 11215.21 us (kalman adjust)
UDP buffer size: 64.0 KByte (default)
------------------------------------------------------------
------------------------------------------------------------
Server listening on TCP port 5001
TCP window size: 64.0 KByte (default)
------------------------------------------------------------
连接类型:打印连接的信息,例如:
[244] local 127.0.0.1 port 24003 connected with 127.0.0.1 port 5001
多播类型:在同一客户端发生多个连接到服务端时,对于服务端,在一定的打印时间段里,比如上面的1.0- 2.0 sec,程序将识别出为同一客户端的数据量进行累加,做一个总的输出打印,例如:
其中[SUM]开头所打印的信息类型为多播类型
[288] 6.0- 7.0 sec 163 MBytes 1.37 Gbits/sec
[ 40] 6.0- 7.0 sec 164 MBytes 1.37 Gbits/sec
[SUM] 6.0- 7.0 sec 327 MBytes 2.74 Gbits/sec
[288] 7.0- 8.0 sec 164 MBytes 1.38 Gbits/sec
[ 40] 7.0- 8.0 sec 164 MBytes 1.37 Gbits/sec
[SUM] 7.0- 8.0 sec 328 MBytes 2.75 Gbits/sec
那么,对于报告者线程,这是如何进行实现的呢?
IPerf维护了一个节点类型为ReportHeader的全局变量ReportRoot,作为维护报告者首部的根节点,该结构的组成情况是这样的:(这里只列出相关的结构,全面具体的结构体内容请进一步查看源码)
ReportHeader中的ReportData中的有个整型类型的type成员变量,它的值表明了该报告者首部属于那种类型的报告,Reporter.cpp会根据此变量的值进行相应的处理。
1 int reporter_process_report ( ReportHeader *reporthdr ) 2 { 3 if ( (reporthdr->report.type & SETTINGS_REPORT) != 0 ) 4 { 5 //...please read the sourc code for getting more information... 6 } 7 else if ( (reporthdr->report.type & CONNECTION_REPORT) != 0 ) 8 { 9 //...please read the sourc code for getting more information... 10 } 11 else if ( (reporthdr->report.type & SERVER_RELAY_REPORT) != 0 ) 12 { 13 //...please read the sourc code for getting more information... 14 } 15 16 if ( (reporthdr->report.type & TRANSFER_REPORT) != 0 ) 17 { 18 //...please read the sourc code for getting more information... 19 } 20 return need_free; 21 }
报告者线程的绝大部分时间都花在打印传输类型的报告内容。
在初始化传输报告首部这一步,程序在初始化传输类型的ReportHeader时会申请如下结构的空间大小:
其中ReportStruct类型共有NUM_REPORT_STRUCTS(#define NUM_REPORT_STRUCTS 700)个,后面它是循环使用的。
//src/Reporter.c/InitReport
1 reporthdr = (ReportHeader *) malloc( sizeof(ReportHeader) + 2 NUM_REPORT_STRUCTS * sizeof(ReportStruct) ); 3 if ( reporthdr != NULL ) 4 { 5 // Only need to make sure the headers are clean 6 memset( reporthdr, 0, sizeof(ReportHeader)); 7 reporthdr->data = (ReportStruct*)(reporthdr+1); 8 reporthdr->multireport = agent->multihdr; 9 data = &reporthdr->report; 10 //Set reporterindex with the last one 11 reporthdr->reporterindex = NUM_REPORT_STRUCTS - 1; 12 ... 13 ...
ReportHeader的data指向第一个ReportStruct结构的地址,agentindex和reporterindex为整型类型,作为data的下标与其结合,data[agentindex]表示当前最新发送包所在的填充位置,data[reporterindex]为报告者线程已报告到控制台的数据包的位置。
客户端线程每次发送数据到服务端后,都会填充一次ReportStrut结构,重要的信息有三项,记录当前发送的数据量大小、包发送出去的时间戳以及包的标识ID,所以可以把ReportStruct看作是Packet,毕竟ReportStructural的成员变量的命名说明其作为一个packet看待会更好,然后会将其填充到data[agentindex]中,并且将angentindex进行加一处理。当填充到ReportStrut数组的尾部时则会回到数组的第一项重新填充,以此方式循环利用,reporterindex永远不能超过agentindex,因为我数据都没填充,残留的是无效的数据,怎么可以进行提前打印呢。
来,再说得具体点。
首先,在InitReport函数中,如果选项参数中有使用到有-i选项的话(该选项参数的值存储在thread_settings类型的mInterval变量中),则将该值赋予ReportHeader中ReportData的intervalTime变量,然后将当前时间赋予ReportData的starttime变量(通过gettimeofday),再将startime + intervalTime初始化ReportData中的nexttime,这个值说明下一次将要打印报告的时间戳,具体看代码:
1 if ( agent->mInterval != 0.0 ) 2 { 3 struct timeval *interval = &data->intervalTime; 4 interval->tv_sec = (long) agent->mInterval; 5 //Equal to Zero Josephus 6 interval->tv_usec = (long) ((agent->mInterval - interval->tv_sec) 7 * rMillion); 8 }
//starttime和nexttime的初始化
1 else 2 { 3 4 // set start time 5 gettimeofday( &(reporthdr->report.startTime), NULL ); 6 } 7 reporthdr->report.nextTime = reporthdr->report.startTime; 8 TimeAdd( reporthdr->report.nextTime, reporthdr->report.intervalTime );
然后,在每次客户端线程发完数据后,判断-i选项是否有效,有效的情况下,给当前的包,也就是ReportStruct结构类型的变量填充值,包括发送的数据量大小currLen,获取当前的时间戳,PackID在TCP模式下起的作用只有一个——在发送完毕时添加一个数据量为0,PacketID为-1的包标识发送数据完毕,其余的时候PaketID的值均为0,然后调用ReportPacket函数。
ReportPacket函数的作用是维护agentindex和reportindex的先后关系,将数据包的内容添加到ReportHeader->data[agentindex]中,并将agentindex做加一处理。
此时,报告者线程在reporter_spawn中做循环操作,这点在开始的时候也有提到过,循环操作中有调用reporter_process_report函数,所以也可以说reporter_process_report函数一直被报告者线程调用,在该函数中,当处理到运输类型的报告首部时,首先对reporterindex和agentindex在某些特殊情况下进行了处理,确保reporterindex没有“超越”agentindex,然后调用reporter_handle_packet函数,来重点看一下这个函数:
reporter_handle_packet函数一开始就判断当前将要打印(或报告)的包(data[reporthdr->reporterindex])是否是最后一个包(通过PacketID值是否小于0),如果是,则将finished置为1,后面将这个值返回,上层可以通过函数的返回值销毁该运输类型结构体变量,如果不是,则调用reporter_condprintstats函数,但在调用该函数时,将当前可能要打印的包的时间赋予ReportData中的packetTime,注意此时并没有把该包的大小也加到ReportData的TotalLen中,而是等到reporter_condprintstats函数返回时才加上,原因等下说明,来深究一下reporter_condprintstats这个函数:
reporter_condprintstats函数中,如果传进来的参数force不等于0,在TCP模式下说明数据发送完了,将要打印的是统计的信息,如果force等于0,则会执行循环,循环的条件为:
1 else while ((stats->intervalTime.tv_sec != 0 || stats->intervalTime.tv_usec != 0) && 2 TimeDifference( stats->nextTime, stats->packetTime ) < 0 )
选项参数-i有使用,体现在隔段时间需要将当前发送信息以打印的方式报告一次,stats是ReportHeader中的ReporterData,其实“罪魁祸首”,起到最大作用的就是ReportData类型的成员变量report,也就是现在的stats,如果nexttime 小于 当前可能要打印的包的时间戳(注意在上层已经将包的时间戳赋予了packetTime),想象一下,本来要nexttime这个时间戳打印报告的,但是现在还没打印的第一个包的时间戳都超过了这个时间,那还不赶紧打印,所以符合条件,开始执行循环体的内容,对ReportData中Transfer_Info类型的变量info进行赋值,并注意保存本次的状态信息并在下次打印时做一系列的相减操作,接着调用reporter_print函数并传入Transfer_info类型的参数值进行控制台输出打印。一般来说,该while循环只执行一次,除非打印的时间间隔太小,也就是-i选项值设的过小,如果想要实现while循环执行多次的效果,可以试试在客户端线程发送数据完毕后紧接着在后面阻塞一段时间。
刚才提到的为什么在reporter_condprintstats函数返回时才加上将要打印的包的大小,因为循环体条件中判断两个时间时使用的是小于符号,注定后面的包大小不宜在该时间段中打印出来。
如果还不太明白,可以结合下图来理解的:)
未完待续...
更多内容请关注个人微信公众号 物役记 (微信号:materialchains)