Web Pressure Test

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
2
3
4
5
6
struct option {
const char *name;
int has_arg;
int *flag;
int val;
}

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
2
3
4
5
6
GET http://www.github.com:80/ HTTP/1.1 (CRLF)
User-Agent: WPT 1.5 (CRLF)
Host: www.github.com (CRLF)
Pragma: no-cache (CRLF)
Connection: close (CRLF)
(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
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}

信号处理函数可以采用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 启动程序,可以加参数
print 查看变量信息
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左右时,就提示出错了。这一结果是正常的,创建的进程加上系统自身的服务进程,差不多是这个值。


wpttest1

  • 对网站进行测试

对几个网站进行了一些简单的测试,没做极限测试,因为……


wpttest2


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!