引言
<<独白>> 席慕蓉 节选一
把向你借来的笔还给你吧。
一切都发生在回首的刹那。
我的彻悟如果是缘自一种迷乱,那么,我的种种迷乱不也就只是因为一种彻悟?
在一回首间,才忽然发现,原来,我的一生的种种努力,不过只是为了周遭的人都对我满意而已。
为了要博得他人的称许与微笑,我战战兢兢地将自己套入所有的模式,所有的桎梏。
走到中途,才忽然发现,我只剩下一副模糊的面目,和一条不能回头的路。
把向你借来的笔还给你吧。
配乐 :
我在长大 : http://music.163.com/#/song?id=155977
不知疲倦的整夜码代码 像块石头一样滚来滚去
可是找不到家的方向
我在长大 / 我在长大 / 我在长大 / 我在长大
前言
我们先说一下,参照的东西和需要掌握的东西
具体参照
1. HTTP协议简单介绍 http://www.cnblogs.com/biyeymyhjob/archive/2012/07/28/2612910.html
这里需要掌握简单的Linux 操作,这里分享一个自己总结的socket入门基础文档.
2. socket 简单入门doc http://download.csdn.net/detail/wangzhione/9412538
到这里, 基本准备工作就完成了,那就搞起吧, 看看那些说起来很玄乎的工具是怎么简单实现的.顺带扯一点,
下面代码自己多看几遍写一遍,可以帮助你理解Linux中很多基础概念,例如 信号,多进程, 管道,socket,中断,
协议等.
正文
1.简单说一点格式
这里我们先看下面一段代码格式,来了解C接口设计中一些潜规则.
/* * 这是一个对外的接口函数声明, *相当于public */ extern int isbig(void); /* * 这个接口也具备对外能力,但是省略了extern *意图就是构建整个 *.h文件的时候需要用它,但是不希望用户用. *属于 low level api, 内部接口集用,外部不推荐用,有点像protected */ int heoo(void); /* * 这个是个内部声明或定义, 只能在当前接口集的内部使用,外部无法 *访问,相当于pirate */ static void* __run(void* arg);
定义部分简单说明一下.以前博文中说过,这里再简单说一次,编程和打游戏一样,要有好节奏,
要有好套路,这样打起来才顺.等同于实践可以总结出理论基础.前任要感谢.
/* * 定义加声明,放在一块 */ static int __run(void) { static union { unsigned short _s; unsigned char _cs[sizeof(unsigned short)]; } __ut = { 1 }; return __ut._cs[0] == 0; } /* * 声明和定义分开 */ extern void start(void); //下面是定义部分 void start(void) { puts("你好!"); }
套路很多,都是历代编程王者,开创的各大武功的起手式,各大定式都可以就看你想拜入那个门派,开始这个编程网游之旅.
2.说思路
经过上面简单格式介绍,这里就说一下思路. 关于 这篇博文,C 对网页的压力测试构建的思路.具体如下:
a. 拼接请求报文 , 解析
b. 发送请求报文 , 请求
c. 处理请求报文, 处理
d.总结请求报文, 输出统计
上面就是这次处理的主要流程, 多次请求采用的fork多个子进程
for(i=0; ii){ pid = fork(); //狂开进程了 if(pid <= 0){ sleep(1);//别让它太快完毕,父进程来不及收尸 break;//结束子进程继续 } ++rt; }
关于父子进程通信采用 管道
//创建管道 进行测试, 代码比较杂,需要一些Linux基础知识,多练习 if(pipe(pfds)<-1) CERR_EXIT("pipe create is error!");
还有一个是通过信号处理超时问题
//默认全局标识, 控制开关
volatile int v_timer = 0;
//闹钟信号,当它启动那一刻就表示可以退下了.
static void __alarm(int sig)
{
v_timer = 1;
}
后面我们就通过代码来说了,最好你写一遍,就明白容易了.扯一点,关于volatile 可以声明是为了方式编译器优化,导致代码死循环.
3.逐个实现
第一部分头文件,声明解析
首先观察下面前戏代码
#include#include #include <string.h> #include #include #include #include #include #include #include #include #include #include #include in.h> #include //控制台打印错误信息, fmt必须是双引号括起来的宏 #ifndef CERR #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) //检测并退出的宏 #define CERR_EXIT(fmt, ...) \ CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE) #endif/* !CERR */ // url长度限制 和 请求串长度限制 #define _INT_URL (1024) #define _INT_REQ (2048) //主机地址的最大长度 #define _INT_PORT (255) //等待时间,在60s后就不在处理了 #define _INT_END (23) //简单结构 struct iport{ short port; char ip[_INT_PORT]; }; //默认全局标识, 控制开关 volatile int v_timer = 0; //闹钟信号,当它启动那一刻就表示可以退下了. static void __alarm(int sig) { v_timer = 1; } /* * 执行的主要方法,结果会通过写管道 wfd写入 */ void bcore(struct iport* ips, const char* req, int wfd); /* * 只接受完整请求 * 例如 * brequest("http://192.168.1.162:80/", ...) 或 http://www.baidu,com * 通过url拼接请求串,并返回访问结构 */ struct iport* brequst(const char* url, char rqs[], int len); //请求主方法,客户端,返回开启的子进程数 int bench(int cut, const char* url, int wfd); /* * 请求链接主机,根据ip或url * * host : url地址 * port : 请求的端口号 */ int contweb(const char* host, int port);
CERR系列是一个帮助宏,简单打印错误信息. 技巧值得推荐使用.其中 _INT_END 表示每个子进程请求的时间长度(s).
感觉注释也挺详细的关于
//请求主方法,客户端,返回开启的子进程数 int bench(int cut, const char* url, int wfd);
主要开启多进程的,依次调用 breques解析url 和 调用 bcore 处理socket .
其中 contweb 是一个可以根据ip或主机地址url解析,链接主机.具体实现如下
/* * 请求链接主机,根据ip或url * * host : url地址 * port : 请求的端口号 */ int contweb(const char* host, int port) { int sd; in_addr_t ia; struct sockaddr_in sa = { AF_INET }; struct hostent *hp; ia = inet_addr(host); if(ia != INADDR_NONE) memcpy(&sa.sin_addr, &ia, sizeof(ia)); else { hp = gethostbyname(host); if(hp < 0){ CERR("gethostbyname %s error!", host); return -1; } memcpy(&sa.sin_addr, hp->h_addr, hp->h_length); } if((sd=socket(PF_INET, SOCK_STREAM, 0))<0){ CERR("socket SOCK_STREAM"); return sd; } //连接主机 sa.sin_port = htons(port); if(connect(sd, (struct sockaddr*)&sa, sizeof sa)<0){ CERR("connect sd:%d error!", sd); return -1; } return sd; }
没有什么花哨的东西就是同步链接HTTP服务器, 如果传入的的host是url那么就调用 gethostbyname 获取到 请求
主机的sin_addr地址量.
扯一点,socket编程大部分也等同于业务代码,固定的套路固定的模式,写写也都会写了.但也挺麻烦了,随便一个简单的
完整的socket demo都4-5百行代码,写的手疼.封装也很麻烦.但是你理解了它的思路或套路,还是很容易上马提枪的.
第二部分各个,定义解析
首先代码可以优化,我写好后没优化了,就直接分享了.先看http协议解析代码如下
//通过url拼接请求串 struct iport* brequst(const char* url, char rqs[], int len) { static struct iport __ips; //请求的主机, 中间用的url, 临时数据 const char *hurl, *tmp; char c; int pt = 0;//临时用的int变量 //简单检查参数检查,检查数组大小,url是否合法 if(len<_INT_REQ || !url || !*url || strlen(url)>_INT_URL) CERR_EXIT("params url:%s, len[>=%d];%d error.", url, _INT_REQ, len); //检测url是否是http请求的url,也比较简单,复杂检查要用正则表达式 hurl = strstr(url, "://"); if(!hurl || strncasecmp(url, "http", 4)) CERR_EXIT("url is err [%s]!", url); //简单初始化 memset(rqs, 0, len); memset(&__ips, 0, sizeof __ips); //这种代码可以优化,用指针替代数组索引 strcat(rqs, "GET "); hurl += 3; //跳过 "://"指向下一个字符 //解析url 上是否有端口信息 tmp = index(hurl, ':'); if(tmp && tmp < index(hurl, '/')){ strncpy(__ips.ip, hurl, tmp - hurl); //没有做安全检查 while((c=*++tmp)!='\0' && c!='/'){ if(c<='9' && c>='0'){ pt = 10*pt + c - '0'; continue; } CERR_EXIT("url is error [%s].", url); } } else{ pt = 80; //默认端口 //这句话意思 是 将 www.baidu.com/ 其中/之前的地方写入到 ip[]中 strncpy(__ips.ip, hurl, strcspn(hurl, "/")); } __ips.port = pt; //将这些内容链接到rqs中 strcat(rqs, hurl + strcspn(hurl, "/")); //采用HTTP1.1 协议 strcat(rqs, " HTTP/1.1\r\n"); //添加请求方 strcat(rqs, "User-Agent: Happy is good.\r\n"); //上面这样解析有点慢,最快的是整体for,但是更难维护 strcat(rqs, "Host: "); strcat(rqs, __ips.ip); strcat(rqs, "\r\n"); //拼接串最后,连接状态,完毕就关闭 strcat(rqs, "Connection: close\r\n"); //拼接空行, 上面可以优化 strcat(rqs, "\r\n"); //最终检测,是否字符串太长了 if(rqs[len-1]) CERR_EXIT("get http too len!"); return &__ips; }
主要思路是先检查 参数, 看 是否是 右边这样正则格式的串 "http.://.+/"
后面拼接结果截图如下:
更加详细关于HTTP协议解析的,可以参照我前面的连接,HTTP协议简单解析.
后面介绍的是有了请求串,怎么发送请求和处理请求,代码如下:
/* * 执行的主要方法,结果会通过写管道 wfd写入 */ void bcore(struct iport* ips, const char* req, int wfd) { int srv, len, rlen; int speed = 0, failed = 0, bytes = 0; FILE* fx; char buf[_INT_REQ]; //先注册信号 if(SIG_ERR == signal(SIGALRM, __alarm)) CERR_EXIT("signal SIGALRM error!"); alarm(_INT_END); len = strlen(req); for(;;){ if(v_timer){ //结束之前减一,认为成功过 if(failed>0) --failed; break; } srv = contweb(ips->ip, ips->port); if(srv < 0){ ++failed; continue; } //开始写入数据 if(len != write(srv, req, len)){ ++failed; close(srv); continue; } //下面读取数据,v_timer标志是否超时,超时直接走 for(;!v_timer;){ rlen = read(srv, buf, _INT_REQ); if(rlen < 0){ //不考虑软中断了,有问题直接退出 ++failed; close(srv); goto __bcore_exit; //退出结算 } if(rlen == 0)//服务端关闭,结束读取 break; bytes += rlen; } close(srv); ++speed; } __bcore_exit: //管道写入 fx = fdopen(wfd, "w"); if(NULL == fx) CERR_EXIT("fdopen wfd:%d error!", wfd); fprintf(fx,"%d %d %d\n", speed, failed, bytes); fclose(fx); }
中间发送了一个alarm延时发送SIGALRM 信号,还有一个信号容易混淆,是SIGCLD信号 子进程退出时候给父进程发送.
其中 fdopen 是将 Linux文件描述符和 C系统文件操作句柄转换.就是统计数据写入到管道中,管道 是 1进 0出.
最后还有个 开启多进程的函数代码
//请求主方法,客户端,返回开启子进程数 int bench(int cut, const char* url, int wfd) { int i, rt = 0;//rt 记录正常开启的进程 pid_t pid; char req[_INT_REQ]; struct iport* ips; //这里初始化一些环境 ips = brequst(url, req, sizeof req); puts("***********-----------------------------------------------------------***********"); puts(req); //打印数据 puts("***********-----------------------------------------------------------***********"); for(i=0; ii){ pid = fork(); //狂开进程了 if(pid <= 0){ sleep(1);//别让它太快完毕,父进程来不及收尸 break;//结束子进程继续 } ++rt; } if(pid < 0) CERR_EXIT("child %d process create error!", i); if(pid == 0){ //子进程处理 bcore(ips, req, wfd); exit(EXIT_SUCCESS);//子进程这里就结束了 } //下面是父进程处理 return rt; }
也比较好明白,先拼接请求串,再开启多进程挨个请求处理.
到这核心函数基本都完成了,总的业务构建请看下面
第三部分主函数逻辑构建
// 主函数业务,从这开始 int main(int argc, char* argv[]) { int cut, len; //开启进程数量 和 读取scanf返回值 int pfds[2]; FILE* fx; int speed = 0, failed = 0, bytes = 0; //简单检测 if((argc != 3) || (cut = atoi(argv[1]))<=0 || cut > USHRT_MAX){ CERR_EXIT( "\r\nuage: ./webtest.out [cut] [url]" "\r\n :=> ./webtest.out 19 http://www.baidu.com/" ); } //创建管道 进行测试, 代码比较杂,需要一些Linux基础知识,多练习 if(pipe(pfds)<-1) CERR_EXIT("pipe create is error!"); //结果处理,开启指定多个进程,重新获取进程开启成功数 cut = bench(cut, argv[2], pfds[1]); //这里读取管道信息 fx = fdopen(pfds[0], "r"); if(NULL == fx) CERR_EXIT("fdopen pfds[0]:%d error.", pfds[0]); //统计最后数据 setbuf(fx, NULL); while(cut > 0){ int s, f, b; len = fscanf(fx, "%d %d %d", &s, &f, &b); if(len < 3){ CERR("fscnaf read error! len:%d.", len); break; } speed += s; failed += f; bytes += b; --cut; } fclose(fx); close(pfds[0]); close(pfds[1]); //输出统计结果 puts("***********-----------------------------------------------------------***********"); printf("Collect %.2f pages/min, %.2f kb/sec. Request %d sucess, %d failed.\n", (speed+failed)*60.0f/_INT_END, bytes/1024.0f/_INT_END, speed, failed); puts("***********-----------------------------------------------------------***********"); return 0; }
创建管道,调用多进程处理函数,汇总最后结果.注意一点是开启的管道最后需要自己关闭.(可能这里有坑),回头看一下,代码其实
还是比较基础的,主要用的就是Linux编程相关的知识点.
最后输出 每分钟请求的页面数, 每秒请求的bit流,请求成功多少次,请求失败多少次.
4.看案例结果
首选看下面一个操作图,先请求百度试试
最后统计结果是 每分钟请求了6357个界面左右,开启了23个子进程, 成功2414个,23个错误. 总的而言百度的分流确实不错,负载均衡强.
再来请求一个普通网站的截图
上面打印很多connect error,意思是很多进程请求链接失败,短时间大量链接导致对方服务器拒绝链接, 负载均衡差,
流量差不多,可能和网速有关,请求界面数挺多的这个界面不错,可能是纯静态的. 对于详细查看为什么失败,可以在记录 failed++的时候打印日志看看,
这里就这样了. 麻雀虽小,能用就好. 完整测试 demo 如下
webtest.c
#include#include #include <string.h> #include #include #include #include #include #include #include #include #include #include #include in.h> #include //控制台打印错误信息, fmt必须是双引号括起来的宏 #ifndef CERR #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) //检测并退出的宏 #define CERR_EXIT(fmt, ...) \ CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE) #endif/* !CERR */ // url长度限制 和 请求串长度限制 #define _INT_URL (1024) #define _INT_REQ (2048) //主机地址的最大长度 #define _INT_PORT (255) //等待时间,在60s后就不在处理了 #define _INT_END (23) //简单结构 struct iport{ short port; char ip[_INT_PORT]; }; //默认全局标识, 控制开关 volatile int v_timer = 0; //闹钟信号,当它启动那一刻就表示可以退下了. static void __alarm(int sig) { v_timer = 1; } /* * 执行的主要方法,结果会通过写管道 wfd写入 */ void bcore(struct iport* ips, const char* req, int wfd); /* * 只接受完整请求 * 例如 * brequest("http://192.168.1.162:80/", ...) 或 http://www.baidu,com * 通过url拼接请求串,并返回访问结构 */ struct iport* brequst(const char* url, char rqs[], int len); //请求主方法,客户端,返回开启的子进程数 int bench(int cut, const char* url, int wfd); /* * 请求链接主机,根据ip或url * * host : url地址 * port : 请求的端口号 */ int contweb(const char* host, int port); // 主函数业务,从这开始 int main(int argc, char* argv[]) { int cut, len; //开启进程数量 和 读取scanf返回值 int pfds[2]; FILE* fx; int speed = 0, failed = 0, bytes = 0; //简单检测 if((argc != 3) || (cut = atoi(argv[1]))<=0 || cut > USHRT_MAX){ CERR_EXIT( "\r\nuage: ./webtest.out [cut] [url]" "\r\n :=> ./webtest.out 19 http://www.baidu.com/" ); } //创建管道 进行测试, 代码比较杂,需要一些Linux基础知识,多练习 if(pipe(pfds)<-1) CERR_EXIT("pipe create is error!"); //结果处理,开启指定多个进程,重新获取进程开启成功数 cut = bench(cut, argv[2], pfds[1]); //这里读取管道信息 fx = fdopen(pfds[0], "r"); if(NULL == fx) CERR_EXIT("fdopen pfds[0]:%d error.", pfds[0]); //统计最后数据 setbuf(fx, NULL); while(cut > 0){ int s, f, b; len = fscanf(fx, "%d %d %d", &s, &f, &b); if(len < 3){ CERR("fscnaf read error! len:%d.", len); break; } speed += s; failed += f; bytes += b; --cut; } fclose(fx); close(pfds[0]); close(pfds[1]); //输出统计结果 puts("***********-----------------------------------------------------------***********"); printf("Collect %.2f pages/min, %.2f kb/sec. Request %d sucess, %d failed.\n", (speed+failed)*60.0f/_INT_END, bytes/1024.0f/_INT_END, speed, failed); puts("***********-----------------------------------------------------------***********"); return 0; } /* * 请求链接主机,根据ip或url * * host : url地址 * port : 请求的端口号 */ int contweb(const char* host, int port) { int sd; in_addr_t ia; struct sockaddr_in sa = { AF_INET }; struct hostent *hp; ia = inet_addr(host); if(ia != INADDR_NONE) memcpy(&sa.sin_addr, &ia, sizeof(ia)); else { hp = gethostbyname(host); if(hp < 0){ CERR("gethostbyname %s error!", host); return -1; } memcpy(&sa.sin_addr, hp->h_addr, hp->h_length); } if((sd=socket(PF_INET, SOCK_STREAM, 0))<0){ CERR("socket SOCK_STREAM"); return sd; } //连接主机 sa.sin_port = htons(port); if(connect(sd, (struct sockaddr*)&sa, sizeof sa)<0){ CERR("connect sd:%d error!", sd); return -1; } return sd; } //通过url拼接请求串 struct iport* brequst(const char* url, char rqs[], int len) { static struct iport __ips; //请求的主机, 中间用的url, 临时数据 const char *hurl, *tmp; char c; int pt = 0;//临时用的int变量 //简单检查参数检查,检查数组大小,url是否合法 if(len<_INT_REQ || !url || !*url || strlen(url)>_INT_URL) CERR_EXIT("params url:%s, len[>=%d];%d error.", url, _INT_REQ, len); //检测url是否是http请求的url,也比较简单,复杂检查要用正则表达式 hurl = strstr(url, "://"); if(!hurl || strncasecmp(url, "http", 4)) CERR_EXIT("url is err [%s]!", url); //简单初始化 memset(rqs, 0, len); memset(&__ips, 0, sizeof __ips); //这种代码可以优化,用指针替代数组索引 strcat(rqs, "GET "); hurl += 3; //跳过 "://"指向下一个字符 //解析url 上是否有端口信息 tmp = index(hurl, ':'); if(tmp && tmp < index(hurl, '/')){ strncpy(__ips.ip, hurl, tmp - hurl); //没有做安全检查 while((c=*++tmp)!='\0' && c!='/'){ if(c<='9' && c>='0'){ pt = 10*pt + c - '0'; continue; } CERR_EXIT("url is error [%s].", url); } } else{ pt = 80; //默认端口 //这句话意思 是 将 www.baidu.com/ 其中/之前的地方写入到 ip[]中 strncpy(__ips.ip, hurl, strcspn(hurl, "/")); } __ips.port = pt; //将这些内容链接到rqs中 strcat(rqs, hurl + strcspn(hurl, "/")); //采用HTTP1.1 协议 strcat(rqs, " HTTP/1.1\r\n"); //添加请求方 strcat(rqs, "User-Agent: Happy is good.\r\n"); //上面这样解析有点慢,最快的是整体for,但是更难维护 strcat(rqs, "Host: "); strcat(rqs, __ips.ip); strcat(rqs, "\r\n"); //拼接串最后,连接状态,完毕就关闭 strcat(rqs, "Connection: close\r\n"); //拼接空行, 上面可以优化 strcat(rqs, "\r\n"); //最终检测,是否字符串太长了 if(rqs[len-1]) CERR_EXIT("get http too len!"); return &__ips; } /* * 执行的主要方法,结果会通过写管道 wfd写入 */ void bcore(struct iport* ips, const char* req, int wfd) { int srv, len, rlen; int speed = 0, failed = 0, bytes = 0; FILE* fx; char buf[_INT_REQ]; //先注册信号 if(SIG_ERR == signal(SIGALRM, __alarm)) CERR_EXIT("signal SIGALRM error!"); alarm(_INT_END); len = strlen(req); for(;;){ if(v_timer){ //结束之前减一,认为成功过 if(failed>0) --failed; break; } srv = contweb(ips->ip, ips->port); if(srv < 0){ ++failed; continue; } //开始写入数据 if(len != write(srv, req, len)){ ++failed; close(srv); continue; } //下面读取数据,v_timer标志是否超时,超时直接走 for(;!v_timer;){ rlen = read(srv, buf, _INT_REQ); if(rlen < 0){ //不考虑软中断了,有问题直接退出 ++failed; close(srv); goto __bcore_exit; //退出结算 } if(rlen == 0)//服务端关闭,结束读取 break; bytes += rlen; } close(srv); ++speed; } __bcore_exit: //管道写入 fx = fdopen(wfd, "w"); if(NULL == fx) CERR_EXIT("fdopen wfd:%d error!", wfd); fprintf(fx,"%d %d %d\n", speed, failed, bytes); fclose(fx); } //请求主方法,客户端,返回开启子进程数 int bench(int cut, const char* url, int wfd) { int i, rt = 0;//rt 记录正常开启的进程 pid_t pid; char req[_INT_REQ]; struct iport* ips; //这里初始化一些环境 ips = brequst(url, req, sizeof req); puts("***********-----------------------------------------------------------***********"); puts(req); //打印数据 puts("***********-----------------------------------------------------------***********"); for(i=0; i i){ pid = fork(); //狂开进程了 if(pid <= 0){ sleep(1);//别让它太快完毕,父进程来不及收尸 break;//结束子进程继续 } ++rt; } if(pid < 0) CERR_EXIT("child %d process create error!", i); if(pid == 0){ //子进程处理 bcore(ips, req, wfd); exit(EXIT_SUCCESS);//子进程这里就结束了 } //下面是父进程处理 return rt; }
编译代码如下
gcc -Wall -o webtest.out webtest.c
后面就需要大家尝试了,用的还凑合者.
光鲜的背后都是平凡.喜欢才是人这台机器的最好能源.
后记
错误是难免,欢迎指正交流,共同消磨有趣的时光. ( ^_^ )/~~拜拜