IPerf——网络测试工具介绍与源码解析(3)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介:
【线程的生成】
 
生成线程时需要传入一个thread_Settings类型的变量,thread_Settings包含所有线程运行时需要的信息,命令行选项参数解析后所有得到的属性都存储到该类型的变量中,作为线程生成的传入值能够决定当前线程扮演的角色。
thread_Settings结构中有两个thread_Settings*类型的变量runNow和runNext,runNow不为NULL时表示生成当前Setings所决定的线程之前要先生成包含该指针指向的Settings特征信息的线程,换句话说就要并发运行线程;runNext则表明在当前Settings所决定的线程结束后随即要生成包含该指针指向的Settings特征信息的线程,这中情况分别表现在客户端多并发连接测试和交易测试模式执行时。thread_Settings结构中ThreadMode枚举类型的变量mThreadMode指明线程扮演的角色。
 
  生成线程的入口函数

 

【报告者线程 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 }
复制代码
复制代码

 

程序在开始时会对ReportRoot进行初始化,具体表现在InitReport函数和ReportSettings中,前者创建TRANSFER_REPORT | CONNECTION_REPORT类型的报告者首部并插入到ReportRoot链表中(与下文提到的报告者首部链表指代同一个意思),没错,一个报告者首部可以表示为多种报告类型,而不能说只能是一种报告者类型;对于ReportSetting,它仅生成SETTINGS_REPORT类型的报告者首部并插入到报告者首部链表中,该首部仅在开始时打印了设置信息后就从报告者首部链表中销毁,光荣的结束了它短暂的生命周期。
 
报告者线程会在reporter_spawn函数中循环检测在报告者首部链表的根节点是否为空,非空的情况下调用reporter_process_report函数,该函数递归执行,遍历一次报告者首部链表并在有打印内容的情况下进行打印,其次根据其返回值决定是否需要销毁当前的报告者首部节点。在多并发连接进行的情况下,传输类型的报告者首部节点有多个。
 

报告者线程的绝大部分时间都花在打印传输类型的报告内容。

 

报告者线程的职能还未阐述完,需要结合下面的客户端线程才能更好地解释其是如何将绝大部分时间花费在打印传输类型的报告者首部的。
 
【客户端线程 kMode_Client】
在命令行选项中输入 -c 选项后表明该程序作为客户端运行,作为客户端运行时,首先会走一次client_init函数,在双向测试模式或者交易模式下会添加生成监听者线程的逻辑,如果选项-P在用户输入的命令行选项参数中有体现的话,那么就意味着客户端要进行多并发连接到服务端,那么根据-P选项带进来的线程数添加生成相应数目的客户端线程的逻辑。
 

 

 

在初始化传输报告首部这一步,程序在初始化传输类型的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函数返回时才加上将要打印的包的大小,因为循环体条件中判断两个时间时使用的是小于符号,注定后面的包大小不宜在该时间段中打印出来。

 如果还不太明白,可以结合下图来理解的:)

 

未完待续...

目录
相关文章
|
16天前
|
安全 虚拟化
在数字化时代,网络项目的重要性日益凸显。本文从前期准备、方案内容和注意事项三个方面,详细解析了如何撰写一个优质高效的网络项目实施方案,帮助企业和用户实现更好的体验和竞争力
在数字化时代,网络项目的重要性日益凸显。本文从前期准备、方案内容和注意事项三个方面,详细解析了如何撰写一个优质高效的网络项目实施方案,帮助企业和用户实现更好的体验和竞争力。通过具体案例,展示了方案的制定和实施过程,强调了目标明确、技术先进、计划周密、风险可控和预算合理的重要性。
40 5
|
18天前
|
SQL 安全 网络安全
网络安全的护城河:漏洞防御与加密技术的深度解析
【10月更文挑战第37天】在数字时代的浪潮中,网络安全成为守护个人隐私与企业资产的坚固堡垒。本文将深入探讨网络安全的两大核心要素——安全漏洞和加密技术,以及如何通过提升安全意识来强化这道防线。文章旨在揭示网络攻防战的复杂性,并引导读者构建更为稳固的安全体系。
31 1
|
26天前
|
SQL 安全 测试技术
网络安全的盾牌与剑——漏洞防御与加密技术解析
【10月更文挑战第28天】 在数字时代的浪潮中,网络空间安全成为我们不可忽视的战场。本文将深入探讨网络安全的核心问题,包括常见的网络安全漏洞、先进的加密技术以及提升个人和组织的安全意识。通过实际案例分析和代码示例,我们将揭示黑客如何利用漏洞进行攻击,展示如何使用加密技术保护数据,并强调培养网络安全意识的重要性。让我们一同揭开网络安全的神秘面纱,为打造更加坚固的数字防线做好准备。
39 3
|
16天前
RS-485网络中的标准端接与交流电端接应用解析
RS-485,作为一种广泛应用的差分信号传输标准,因其传输距离远、抗干扰能力强、支持多点通讯等优点,在工业自动化、智能建筑、交通运输等领域得到了广泛应用。在构建RS-485网络时,端接技术扮演着至关重要的角色,它直接影响到网络的信号完整性、稳定性和通信质量。
|
1月前
|
边缘计算 自动驾驶 5G
|
23天前
|
SQL 安全 算法
网络安全的屏障与钥匙:漏洞防护与加密技术解析
【10月更文挑战第31天】在数字世界的海洋中,网络安全是航船的坚固屏障,而信息安全则是守护宝藏的金钥匙。本文将深入探讨网络安全的薄弱环节——漏洞,以及如何通过加密技术加固这道屏障。从常见网络漏洞的类型到最新的加密算法,我们不仅提供理论知识,还将分享实用的安全实践技巧,帮助读者构建起一道更加坚不可摧的防线。
28 1
|
20天前
|
机器学习/深度学习 人工智能 自动驾驶
深入解析深度学习中的卷积神经网络(CNN)
深入解析深度学习中的卷积神经网络(CNN)
35 0
|
7月前
|
Web App开发 测试技术 API
自动化测试工具Selenium的深度解析
【5月更文挑战第27天】本文旨在深入剖析自动化测试工具Selenium,探讨其架构、原理及应用。通过对其核心组件、运行机制及在实际项目中的应用案例进行详细解读,以期为软件测试人员提供全面、深入的理解与实践指导。
|
7月前
|
JSON 数据管理 测试技术
自动化测试工具Selenium Grid的深度应用分析深入理解操作系统的内存管理
【5月更文挑战第28天】随着互联网技术的飞速发展,软件测试工作日益复杂化,传统的手工测试已无法满足快速迭代的需求。自动化测试工具Selenium Grid因其分布式执行特性而受到广泛关注。本文旨在深入剖析Selenium Grid的工作原理、配置方法及其在复杂测试场景中的应用优势,为测试工程师提供高效测试解决方案的参考。
|
7月前
|
敏捷开发 监控 IDE
深入理解自动化测试工具Selenium的工作原理与实践应用
【5月更文挑战第26天】 随着敏捷开发和持续集成理念的普及,自动化测试在软件开发生命周期中扮演了至关重要的角色。Selenium作为最流行的自动化测试工具之一,以其开源、跨平台和支持多种编程语言的特性被广泛使用。本文将详细解析Selenium的核心组件,探讨其工作原理,并通过案例分析展示如何高效地实施Selenium进行Web应用的自动化测试。我们将从测试准备到结果分析的全过程,提供一系列实用的策略和最佳实践,帮助读者构建和维护一个健壮的自动化测试环境。

推荐镜像

更多