Web Pressure Test(WPT)是一个在Linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端访问设定的URL,测试网站在压力下工作的性能,最多可以模拟3万个并发连接去测试网站的负载能力。
1. 项目简介
一个简单的开源项目,在Linux下用于网站压力测试,内容虽然不多,但涉及方面很全,适合深入学习。
项目结构
- socket.c: 定义了Socket函数,用于对socket函数进行包裹。
- wpt.c: 用于命令行解析和创建并发进程,进行压力测试,并显示相应结果。
- makefile: 描述了整个工程的编译、连接等规则。
- README.md: 描述了项目的安装及使用方法。
- centOS: 包含了项目的更改日志、控制信息、版权及相关规则和目录。
扩展介绍
包裹函数: 完成实际的函数调用,检查返回值,并在发生错误时终止进程,可以通过定义包裹函数来缩短程序。包裹函数名一般是实际函数名的首字母大写形式。
2. 核心文件: wpt.c
核心函数
- main: 主要调用getopt_long函数处理命令行参数,紧接着调用以下函数。
- build_request: 主要用来创建http连接请求。
- wpt: 主要用于复制进程,通过子进程来测试http连接,并把测试结果写入管道,再由父进程读取管道信息,计算并输出相关测试信息。
- wptcore: 主要用于子进程对http请求进行测试。
3. 细节实现
3.1 getopt_long函数
- 头文件:
#include <getopt.h>
- 相关函数: getopt
- 函数声明:
int getopt_long(int argc, char* const argv[], const char* opstring, const struct option* longopts, int* longindex)
- 函数功能: 用于解析命令行选项参数,支持长选项。
getopt与getopt_long相似,而前者只有前三个参数,下面具体讲解getopt_long函数。
函数中有两个默认的参数:
optarg: 如果一个选项有参数,则该参数存放在optarg中,这是一个char*类型。
optind: 下一个将被处理到的参数在argv中的下标值。
前两个参数分别由main函数传入,分别代表参数的个数和参数详细值列表。opstring是选项参数组成的字符串,字符串opstring可以是下列元素: 单个字符,表示选项;单个字符后接一个冒号,表示该选项后必须跟一个参数,参数紧跟在选项后或以空格隔开,该参数的指针赋给optarg;单个字符后跟两个冒号,表示该选项后可以有参数也可以没有参数,如果有参数,参数必须紧跟在选项后不能以空格隔开,该参数的指针赋给optarg。
参数longopts,其实是一个结构的实例:
1 | struct option { |
name表示的是长参数名;has_arg有3个值,no_argument(或者是0),表示该参数后面不跟参数值,required_argument(或者是1),表示该参数后面一定要跟个参数值,optional_argument(或者是2),表示该参数后面可以跟,也可以不跟参数值;flag用来决定,getopt_long()的返回值到底是什么,如果flag是NULL(通常情况),则函数会返回与该项option匹配的val值;如果flag不是NULL,则将val值赋予flag所指向的内存,并且返回值设置为0;val用于和flag联合决定返回值。
参数longindex,表示当前长参数在longopts中的索引值。
设计方法
通过while循环+switch…case…语句配合处理命令行选项,并结合optarg和optind检查参数的合法性。
3.2 build_request函数
主要用一些基本的C字符函数处理有代理或无代理情况下的主机名和段口号,并生成相应的请求消息。如果有代理,则直接根据请求方法和url生成request消息;如果没有代理,则采用url中的域名作为主机名,如果url中含有端口,则采用相应的端口,否则采用默认端口80。
C字符函数
- strchr(string, char): 查找字符串中首次出现指定字符的位置,如果成功,返回该字符以及其后面的字符,如果失败,则返回NULL。
- strrchr(string, char): 查找字符在指定字符串中从正面开始的最后一次出现的位置,如果成功,返回该字符以及其后面的字符,如果失败,则返回NULL。
- strstr(str1, str2): 判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
- strncasecmp(s1, s2): 比较参数s1和s2字符串前n个字符,比较时会自动忽略大小写的差异。如果相同,返回0;s1>s2,返回大于0的值;s1<s2,返回小于0的值。
- strcspn(s, reject): 参数s字符串的开头计算连续的字符, 而这些字符都完全不在参数reject 所指的字符串中.简单地说,若strcspn()返回的数值为n,则代表字符串s开头连续有n个字符都不含字符串reject 内的字符。
本项目中HTTP的请求格式如下:
1 | GET http://www.github.com:80/ HTTP/1.1 (CRLF) |
HTTP请求和响应的具体报文格式及更详细的内容可以参照我的博文HTTP协议。
3.3 wpt函数
通过主机名和端口号建立套结字,并测试管道的可用性,使用fork函数创建子进程,在子进程中fork返回0,在父进程中返回大于0的值,出错则返回小于0的值。利用pid,让子进程执行wptcore函数对网站进行测试,并将得到的数据写入管道,包括速度,失败数和大小;父进程则继续运行,循环从管道中读取这些值,计算后输出。这里只需使用一个简单的单向数据流管道即可,如下图所示,仅描述管道一般原理,方向其实相反。
3.4 wptcore函数
进行压力测试的核心函数,通过建立套结字发送请求消息,并读取响应消息来计算成功、失败、字节数。使用alarm函数计时并发送SIGALRM信号。遇到套结字建立不成功、读写套结字不成功、协议不支持、关闭套结字不成功等都算失败。
- 头文件:
#include <signal.h>
- 函数声明:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
- 函数功能: sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
参数结构sigaction定义如下:
1 | struct sigaction { |
信号处理函数可以采用void (sa_handler)(int)或void (sa_sigaction)(int, siginfo_t, void)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (sa_sigaction)(int, siginfo_t, void),此时可以向处理函数发送附加信息;默认情况下采用void (sa_handler)(int),此时只能向处理函数发送信号的数值。
设计方法
使用变量timerexpired来判断连接是否超时,声明为volatile,初始化为0,默认连接成功。当alarm超时后,会设置该变量为1。
4. 调试
调试也是项目不可缺少的一部分,采用GDB进行调试。完成代码编写后,进行调试,发现了几个问题,学习了GDB的常用操作,常用操作如下。
首先,在生成程序时,需要加入-g选项,该选项可以利用操作系统“原生格式”生成调试信息。GDB可以直接利用这个信息,其他调试器也可以使用这个调试信息。该选项和-ggdb的区别是,该选项使gcc为GDB生成专用的更为丰富的调试信息,但是,此时就不能用其他的调试器来进行调试了。
命令 | 功能 |
---|---|
gdb | 开启GDB程序 |
file | 指定调试的二进制文件 |
list | 查看源代码,约十行左右 |
break | 设置断点 |
info breakpoint | 查看断点信息 |
clear | 清除断点 |
run | 启动程序,可以加参数 |
查看变量信息 | |
watch | 观察变量 |
next | 单步执行,不进入函数 |
step | 单步执行,进入函数 |
shell | 调用shell命令行 |
quit | 退出调试 |
调试问题1
第一个问题,真没有想到自己会犯。程序可以正常启动,但无论输入什么参数,都只显示help信息。刚开始以为是自己输入的参数格式有误,因为有URL和代理等等比较复杂。最后发现getopt_long函数根本没有执行,原来在刚开始判断参数个数时候就已经写错了,argc == 1 时,缺少参数,显示帮助信息,写成了 argc = 1,这样总是成立,因为argc参数至少为1,所以总是显示帮助信息。还是应该老老实实写成1 == argc,要养成良好习惯。
调试问题2
第二个问题,发现连接总是失败,设置断点后,发现socket函数中的host值总是http:://www.而不并是所希望的www.baidu.com,推测应该在获取主机名时错误,而获取主机名的主要部分在build_request中,进一步检查,发现在URL截取的时候,漏加了i,漏加了1,共有两处。
测试结果
- 对最大连接数测试
理论上来说,最多可以支持3万多个并发连接,因为最多可以有32768个进程,这个值是根据/proc/kernel/pid_max得到的。但考虑到系统对内存、CPU等方面的限制,一般会有一个上限值,这个值可以通过ulimit -u进行修改,CentOS下该值默认是4096。我们测试了3万了连接,根据设置的出错提示,发现进程号到4000左右时,就提示出错了。这一结果是正常的,创建的进程加上系统自身的服务进程,差不多是这个值。
- 对网站进行测试
对几个网站进行了一些简单的测试,没做极限测试,因为……
5. makefile
makefile也是项目中重要的一环,也需要认真学习,找了相关的文档,内容很是多,看了一遍。这个项目较小,但也按照相应的形式进行编写,以后如果有机会做大的项目,维护起来,这是必不可少的!
首先,设置相关的标志信息,如CFLAGS等。紧接着是all,最终生成的文件;还有一些常见的伪目标,如install,安装文件到相应的目录,为文件添加man帮助到相应目录,为文件添加版权信息和修改日志到相应目录;tar,打包命令;最后是clean命令清除相关的中间文件等。
6. 完善与维护
大功告成后,还需部分完善:例如,详细的man信息,版权信息,修改日志,控制信息等。同时将项目上传至Github,分享和维护。Github的相关使用,不在这里详述,因为我们之前已经hello world过啦!
项目地址:Web Pressure Test
7. 总结
这是自己第一个学习的开源项目,虽然很小,但是内容很丰富,学习到了很多东西,学到的东西我都列在本文的关键词中了,比如,fork、UNIX网络编程、IPC、GDB调试、makefile等等。其实动手做了就知道,远比这些学到的要多,不过还需要深入看书。这比在学校老师安排的项目有价值的多。学习了一整套开发流程。从项目需求编写(学校项目做过),到实施,到调试,到上传维护,到测试(学校项目做过)以及最后使用手册编写(学校项目做过)都做了一遍。
不过这次不是为的老师,为的是自己的兴趣!希望以后自己能越做越好,好了,就写到这里了。See you in next program!