webbench网站测压工具源码分析
2019-10-12 08:10:35来源:博客园 阅读 ()
webbench网站测压工具源码分析
1 /* 2 * (C) Radim Kolar 1997-2004 3 * This is free software, see GNU Public License version 2 for 4 * details. 5 * 6 * Simple forking WWW Server benchmark: 7 * 8 * Usage: 9 * webbench --help 10 * 11 * Return codes: 12 * 0 - sucess 13 * 1 - benchmark failed (server is not on-line) 14 * 2 - bad param 15 * 3 - internal error, fork failed 16 * 17 */ 18 #include "socket.c" 19 #include <unistd.h> 20 #include <sys/param.h> 21 #include <rpc/types.h> 22 #include <getopt.h> 23 #include <strings.h> 24 #include <time.h> 25 #include <signal.h> 26 27 /* values */ 28 volatile int timerexpired=0;//判断测压市场是否已经达到设定的时间 29 int speed=0;//记录进程成功得到服务器相应的数量 30 int failed=0;//记录失败的数量(speed表示成功数,failed表示失败数) 31 int bytes=0;//记录进程成功读取的字节数 32 /* globals */ 33 int http10=1; /* 0 - http/0.9, 1 - http/1.0, 2 - http/1.1 *///HTTP版本 34 /* Allow: GET, HEAD, OPTIONS, TRACE */ 35 #define METHOD_GET 0 36 #define METHOD_HEAD 1 37 #define METHOD_OPTIONS 2 38 #define METHOD_TRACE 3 39 #define PROGRAM_VERSION "1.5" 40 int method=METHOD_GET;//定义HTTP请求方法:默认方式GET请求 41 int clients=1;//并发数目,默认只有一个进程发送请求,通过 -c 参数设置 42 int force=0;//是否需要等待读取从server返回的数据,0表示要等待读取 43 int force_reload=0;//是否使用缓存,1表示不缓存,0表示可以缓存页面 44 int proxyport=80;//代理服务器的端口 45 char *proxyhost=NULL;//代理服务器的端口 46 int benchtime=30;//测压时间,默认30秒,通过 -t 参数设置 47 /* internal */ 48 int mypipe[2];//使用管道进行父进程和子进程的通信 49 char host[MAXHOSTNAMELEN];//服务器端IP 50 #define REQUEST_SIZE 2048 51 char request[REQUEST_SIZE];//发送HTTP请求的内容 52 53 static const struct option long_options[]= 54 { 55 {"force",no_argument,&force,1}, 56 {"reload",no_argument,&force_reload,1}, 57 {"time",required_argument,NULL,'t'}, 58 {"help",no_argument,NULL,'?'}, 59 {"http09",no_argument,NULL,'9'}, 60 {"http10",no_argument,NULL,'1'}, 61 {"http11",no_argument,NULL,'2'}, 62 {"get",no_argument,&method,METHOD_GET}, 63 {"head",no_argument,&method,METHOD_HEAD}, 64 {"options",no_argument,&method,METHOD_OPTIONS}, 65 {"trace",no_argument,&method,METHOD_TRACE}, 66 {"version",no_argument,NULL,'V'}, 67 {"proxy",required_argument,NULL,'p'}, 68 {"clients",required_argument,NULL,'c'}, 69 {NULL,0,NULL,0} 70 }; 71 72 /* prototypes */ 73 static void benchcore(const char* host,const int port, const char *request); 74 static int bench(void); 75 static void build_request(const char *url); 76 77 /* 78 webbench在运行时可以设定压测的持续时间,以秒为单位。 79 例如我们希望测试30秒,也就意味着压测30秒后程序应该退出了。 80 webbench中使用信号(signal)来控制程序结束。 81 函数1是在到达结束时间时运行的信号处理函数。 82 它仅仅是将一个记录是否超时的变量timerexpired标记为true。 83 后面会看到,在程序的while循环中会不断检测此值, 84 只有timerexpired=1,程序才会跳出while循环并返回。 85 */ 86 static void alarm_handler(int signal) 87 { 88 timerexpired=1; 89 } 90 91 /* 92 教你如何使用webbench的函数, 93 在linux命令行调用webbench方法不对的时候运行,作为提示。 94 有一些比较常用的,比如-c来指定并发进程的多少; 95 -t指定压测的时间,以秒为单位; 96 支持HTTP0.9,HTTP1.0,HTTP1.1三个版本; 97 支持GET,HEAD,OPTIONS,TRACE四种请求方式。 98 不要忘了调用时,命令行最后还应该附上要测的服务端URL。 99 */ 100 static void usage(void) 101 { 102 fprintf(stderr, 103 "webbench [option]... URL\n" 104 " -f|--force Don't wait for reply from server.\n" 105 " -r|--reload Send reload request - Pragma: no-cache.\n" 106 " -t|--time <sec> Run benchmark for <sec> seconds. Default 30.\n" 107 " -p|--proxy <server:port> Use proxy server for request.\n" 108 " -c|--clients <n> Run <n> HTTP clients at once. Default one.\n" 109 " -9|--http09 Use HTTP/0.9 style requests.\n" 110 " -1|--http10 Use HTTP/1.0 protocol.\n" 111 " -2|--http11 Use HTTP/1.1 protocol.\n" 112 " --get Use GET request method.\n" 113 " --head Use HEAD request method.\n" 114 " --options Use OPTIONS request method.\n" 115 " --trace Use TRACE request method.\n" 116 " -?|-h|--help This information.\n" 117 " -V|--version Display program version.\n" 118 ); 119 }; 120 int main(int argc, char *argv[]) 121 { 122 int opt=0; 123 int options_index=0; 124 char *tmp=NULL; 125 126 if(argc==1) 127 { 128 usage(); 129 return 2; 130 } 131 132 while((opt=getopt_long(argc,argv,"912Vfrt:p:c:?h",long_options,&options_index))!=EOF ) 133 { 134 switch(opt) 135 { 136 case 0 : break; 137 case 'f': force=1;break; 138 case 'r': force_reload=1;break; 139 case '9': http10=0;break; 140 case '1': http10=1;break; 141 case '2': http10=2;break; 142 case 'V': printf(PROGRAM_VERSION"\n");exit(0); 143 /** 144 *C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型) 145 *int atoi(const char *str) 146 *str -- 要转换为整数的字符串。 147 *该函数返回转换后的长整数,如果没有执行有效的转换,则返回零。 148 */ 149 case 't': benchtime=atoi(optarg);break; 150 case 'p': 151 /* proxy server parsing server:port */ 152 /** 153 *strrchr() 函数用于查找某字符在字符串中最后一次出现的位置,其原型为: 154 *char * strrchr(const char *str, int c); 155 *【参数】str 为要查找的字符串,c 为要查找的字符。 156 *strrchr() 将会找出 str 字符串中最后一次出现的字符 c 的地址,然后将该地址返回。 157 *注意:字符串 str 的结束标志 NUL 也会被纳入检索范围,所以 str 的组后一个字符也可以被定位。 158 *【返回值】如果找到就返回该字符最后一次出现的位置,否则返回 NULL。 159 *返回的地址是字符串在内存中随机分配的地址再加上你所搜索的字符在字符串位置。设字符在字符串中首次出现的位置为 i,那么返回的地址可以理解为 str + i。 160 */ 161 /** 162 *optarg : char *optarg; //选项的参数指针 163 */ 164 tmp=strrchr(optarg,':'); 165 proxyhost=optarg; 166 if(tmp==NULL) 167 { 168 break; 169 } 170 if(tmp==optarg) 171 { 172 fprintf(stderr,"Error in option --proxy %s: Missing hostname.\n",optarg); 173 return 2; 174 } 175 /** 176 *C 库函数 size_t strlen(const char *str) 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。 177 *size_t strlen(const char *str) 178 *参数:str -- 要计算长度的字符串。 179 *该函数返回字符串的长度。 180 */ 181 if(tmp==optarg+strlen(optarg)-1) 182 { 183 fprintf(stderr,"Error in option --proxy %s Port number is missing.\n",optarg); 184 return 2; 185 } 186 *tmp='\0';//把:替换为\0 187 //从\0之后到\0之前 188 proxyport=atoi(tmp+1);break; 189 case ':': 190 case 'h': 191 case '?': usage();return 2;break; 192 case 'c': clients=atoi(optarg);break; 193 } 194 } 195 //int optind:argv的当前索引值。当getopt函数在while循环中使用时,剩下的字符串为操作数,下标从optind到argc-1 196 //argc,argv 参考:https://www.cnblogs.com/lanshanxiao/p/11568037.html 197 //getopt_long()中的函数,参考:https://www.cnblogs.com/xhg940420/p/7016574.html 198 //扫描完毕后,optind指向非长选项和非短选项和非参数的字段,这里应该指向URL 199 if(optind==argc) { 200 fprintf(stderr,"webbench: Missing URL!\n"); 201 usage(); 202 return 2; 203 } 204 205 if(clients==0) clients=1; 206 if(benchtime==0) benchtime=60; 207 /* Copyright */ 208 fprintf(stderr,"Webbench - Simple Web Benchmark "PROGRAM_VERSION"\n" 209 "Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.\n" 210 ); 211 /** 212 *命令读取完成后,argv[optind]中应该存放着URL, 213 *建立完整的http请求,http请求存放在变量char request[REQUEST_SIZE]中 214 */ 215 build_request(argv[optind]); 216 /* print bench info *///输出平台信息 217 printf("\nBenchmarking: "); 218 switch(method) 219 { 220 case METHOD_GET: 221 default: 222 printf("GET");break; 223 case METHOD_OPTIONS: 224 printf("OPTIONS");break; 225 case METHOD_HEAD: 226 printf("HEAD");break; 227 case METHOD_TRACE: 228 printf("TRACE");break; 229 } 230 printf(" %s",argv[optind]);//打印出URL 231 switch(http10) 232 { 233 case 0: printf(" (using HTTP/0.9)");break; 234 case 2: printf(" (using HTTP/1.1)");break; 235 } 236 printf("\n"); 237 if(clients==1) printf("1 client"); 238 else 239 printf("%d clients",clients); 240 241 printf(", running %d sec", benchtime); 242 if(force) printf(", early socket close"); 243 if(proxyhost!=NULL) printf(", via proxy server %s:%d",proxyhost,proxyport); 244 if(force_reload) printf(", forcing reload"); 245 printf(".\n"); 246 //压力测试最后一句话,所有的压力测试都在bench函数中实现 247 return bench(); 248 } 249 250 /* 251 函数主要操作全局变量char request[REQUEST_SIZE],根据url填充其内容。 252 典型的HTTP的GET请求: 253 GET /test.jpg HTTP/1.1 254 User-Agent: WebBench 1.5 255 Host:192.168.10.1 256 Pragma: no-cache 257 Connection: close 258 259 build_request函数的目的就是要把 260 类似于以上这一大坨信息全部存到全局变量request[REQUEST_SIZE]中, 261 其中换行操作使用的是”\r\n”。 262 而以上这一大坨信息的具体内容是要根据命令行输入的参数,以及url来确定的。 263 该函数使用了大量的字符串操作函数, 264 例如strcpy,strstr,strncasecmp,strlen,strchr,index,strncpy,strcat。 265 对这些基础函数不太熟悉的同学可以借这个函数复习一下。 266 */ 267 void build_request(const char *url) 268 { 269 char tmp[10]; 270 int i; 271 272 bzero(host,MAXHOSTNAMELEN);//bzero():置host(字节字符串)前MAXHOSTNAMELEN个字节为0,包括'\0') 273 bzero(request,REQUEST_SIZE); 274 275 if(force_reload && proxyhost!=NULL && http10<1) http10=1;//满足一定条件,更换HTTP协议 276 if(method==METHOD_HEAD && http10<1) http10=1; 277 if(method==METHOD_OPTIONS && http10<2) http10=2; 278 if(method==METHOD_TRACE && http10<2) http10=2; 279 280 switch(method) 281 { 282 default: 283 //strcpy() 函数用于对字符串进行复制(拷贝)。 284 //char* strcpy(char* strDestination, const char* strSource); 285 //strSource 指向的字符串复制到 strDestination 286 case METHOD_GET: strcpy(request,"GET");break; 287 case METHOD_HEAD: strcpy(request,"HEAD");break; 288 case METHOD_OPTIONS: strcpy(request,"OPTIONS");break; 289 case METHOD_TRACE: strcpy(request,"TRACE");break; 290 } 291 292 //char*strcat(char* strDestination, const char* strSource); 293 /* 294 strcat() 函数用来将两个字符串连接(拼接)起来。 295 strcat() 函数把 strSource 所指向的字符串追加到 strDestination 所指向的字符串的结尾, 296 所以必须要保证 strDestination 有足够的内存空间来容纳两个字符串,否则会导致溢出错误。 297 注意:strDestination 末尾的\0会被覆盖,strSource 末尾的\0会一起被复制过去,最终的字符串只有一个\0。 298 */ 299 strcat(request," "); 300 301 /* 302 char *strstr(const char *haystack, const char *needle) 303 在字符串 haystack 中查找第一次出现字符串 needle 的位置,不包含终止符 '\0'。 304 该函数返回在 haystack 中第一次出现 needle 的地址,如果未找到则返回 null。 305 */ 306 if(NULL==strstr(url,"://")) 307 { 308 fprintf(stderr, "\n%s: is not a valid URL.\n",url); 309 exit(2); 310 } 311 312 /* 313 strlen(char *); 314 检测字符串实际长度。 315 strlen(char *)检测的是'\0',strlen(char *)碰到'\0'就返回'\0'以前的字符数(不包括'\0')。 316 strlen(char*)函数求的是字符串的实际长度,它求得方法是从开始到遇到第一个'\0', 317 如果你只定义没有给它赋初值,这个结果是不定的,它会从aa首地址一直找下去,直到遇到'\0'停止。 318 */ 319 if(strlen(url)>1500) 320 { 321 fprintf(stderr,"URL is too long.\n"); 322 exit(2); 323 } 324 if(proxyhost==NULL) 325 /* 326 int strncasecmp(const char *s1, const char *s2, size_t n); 327 strncasecmp()用来比较参数s1 和s2 字符串前n个字符,比较时会自动忽略大小写的差异。 328 若参数s1 和s2 字符串相同则返回0。s1 若大于s2 则返回大于0 的值,s1 若小于s2 则返回小于0 的值。 329 */ 330 if (0!=strncasecmp("http://",url,7)) 331 { 332 fprintf(stderr,"\nOnly HTTP protocol is directly supported, set --proxy for others.\n"); 333 exit(2); 334 } 335 /* protocol/host delimiter */ 336 i=strstr(url,"://")-url+3; 337 /* printf("%d\n",i); */ 338 339 /* 340 char *strchr(const char *str, char c) 341 该函数返回在字符串 str 中第一次出现字符 c 的地址,如果未找到该字符则返回 NULL。 342 */ 343 if(strchr(url+i,'/')==NULL) { 344 fprintf(stderr,"\nInvalid URL syntax - hostname don't ends with '/'.\n"); 345 exit(2); 346 } 347 if(proxyhost==NULL) 348 { 349 /* get port from hostname */ 350 if(index(url+i,':')!=NULL && 351 index(url+i,':')<index(url+i,'/')) 352 { 353 /* 354 char * strncpy(char *s1,char *s2,size_t n); 355 将字符串s2中最多n个字符复制到字符数组s1中,返回指向s1的指针。 356 注意:如果源串长度大于n,则strncpy不复制最后的'\0'结束符, 357 所以是不安全的,复制完后需要手动添加字符串的结束符才行。 358 */ 359 strncpy(host,url+i,strchr(url+i,':')-url-i); 360 bzero(tmp,10); 361 strncpy(tmp,index(url+i,':')+1,strchr(url+i,'/')-index(url+i,':')-1); 362 /* printf("tmp=%s\n",tmp); */ 363 364 /* 365 C语言库函数名: atoi 366 功 能: 把字符串转换成整型数. 367 名字来源:array to integer 的缩写. 368 函数说明: atoi()会扫描参数nptr字符串,如果第一个字符不是数字也不是正负号返回零, 369 否则开始做类型转换,之后检测到非数字或结束符 \0 时停止转换,返回整型数。 370 原型: int atoi(const char *nptr); 371 */ 372 proxyport=atoi(tmp); 373 if(proxyport==0) proxyport=80; 374 } else 375 { 376 /* 377 size_t strcspn(const char *s, const char * reject); 378 函数说明:strcspn()从参数s 字符串的开头计算连续的字符, 379 而这些字符都完全不在参数reject 所指的字符串中。 380 简单地说, 若strcspn()返回的数值为n,则代表字符串s 开头连续有n 个字符都不含字符串reject 内的字符。 381 返回值:返回字符串s 开头连续不含字符串reject 内的字符数目。 382 */ 383 strncpy(host,url+i,strcspn(url+i,"/")); 384 } 385 // printf("Host=%s\n",host); 386 strcat(request+strlen(request),url+i+strcspn(url+i,"/")); 387 } else 388 { 389 // printf("ProxyHost=%s\nProxyPort=%d\n",proxyhost,proxyport); 390 strcat(request,url); 391 } 392 if(http10==1) 393 strcat(request," HTTP/1.0"); 394 else if (http10==2) 395 strcat(request," HTTP/1.1"); 396 strcat(request,"\r\n"); 397 if(http10>0) 398 strcat(request,"User-Agent: WebBench "PROGRAM_VERSION"\r\n"); 399 if(proxyhost==NULL && http10>0) 400 { 401 strcat(request,"Host: "); 402 strcat(request,host); 403 strcat(request,"\r\n"); 404 } 405 if(force_reload && proxyhost!=NULL) 406 { 407 strcat(request,"Pragma: no-cache\r\n"); 408 } 409 if(http10>1) 410 strcat(request,"Connection: close\r\n"); 411 /* add empty line at end */ 412 if(http10>0) strcat(request,"\r\n"); 413 // printf("Req=%s\n",request); 414 } 415 416 /** 417 *先进行了一次socket连接,确认能连通以后,才进行后续步骤。 418 *调用pipe函数初始化一个管道,用于子进行向父进程汇报测试数据。 419 *子进程根据clients数量fork出来。 420 *每个子进程都调用函数5进行测试,并将结果输出到管道,供父进程读取。 421 *父进程负责收集所有子进程的测试数据,并汇总输出。 422 */ 423 /* vraci system rc error kod */ 424 static int bench(void) 425 { 426 int i,j,k; 427 pid_t pid=0; 428 FILE *f; 429 430 /* check avaibility of target server */ 431 //检测目标服务器的可用性,调用socket.c文件中的函数 432 i=Socket(proxyhost==NULL?host:proxyhost,proxyport); 433 if(i<0) {//处理错误 434 fprintf(stderr,"\nConnect to server failed. Aborting benchmark.\n"); 435 return 1; 436 } 437 close(i); 438 /* create pipe */ 439 if(pipe(mypipe))//管道用于子进程向父进程回报数据 440 {//错误处理 441 perror("pipe failed."); 442 return 3; 443 } 444 445 /* not needed, since we have alarm() in childrens */ 446 /* wait 4 next system clock tick */ 447 /* 448 cas=time(NULL); 449 while(time(NULL)==cas) 450 sched_yield(); 451 */ 452 453 /* fork childs */ 454 for(i=0;i<clients;i++)//根据clients大小fork出来足够的子进程进行测试 455 { 456 pid=fork(); 457 if(pid <= (pid_t) 0) 458 { 459 /* child process or error*/ 460 sleep(1); /* make childs faster */ 461 break; 462 } 463 } 464 465 if( pid< (pid_t) 0) 466 {//错误处理 467 fprintf(stderr,"problems forking worker no. %d\n",i); 468 perror("fork failed."); 469 return 3; 470 } 471 472 if(pid== (pid_t) 0)//若是子进程,调用benchcore进行测试 473 { 474 /* I am a child */ 475 if(proxyhost==NULL) 476 benchcore(host,proxyport,request); 477 else 478 benchcore(proxyhost,proxyport,request); 479 480 /* write results to pipe */ 481 f=fdopen(mypipe[1],"w");//子进程将测试结果输出到管道 482 if(f==NULL) 483 {//错误处理 484 perror("open pipe for writing failed."); 485 return 3; 486 } 487 /* fprintf(stderr,"Child - %d %d\n",speed,failed); */ 488 fprintf(f,"%d %d %d\n",speed,failed,bytes); 489 fclose(f); 490 return 0; 491 } else 492 {//若是父进程,则从管道读取子进程输出,并做汇总 493 f=fdopen(mypipe[0],"r"); 494 if(f==NULL) 495 {//错误处理 496 perror("open pipe for reading failed."); 497 return 3; 498 } 499 setvbuf(f,NULL,_IONBF,0); 500 speed=0; 501 failed=0; 502 bytes=0; 503 504 while(1) 505 {//从管道读取数据,fscanf为阻塞式函数 506 pid=fscanf(f,"%d %d %d",&i,&j,&k); 507 if(pid<2) 508 {//错误处理 509 fprintf(stderr,"Some of our childrens died.\n"); 510 break; 511 } 512 speed+=i; 513 failed+=j; 514 bytes+=k; 515 /* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */ 516 if(--clients==0) break;//这句用于记录已经读了多少个子进程的数据,读完就退出 517 } 518 fclose(f); 519 //最后将结果打印到屏幕上 520 printf("\nSpeed=%d pages/min, %d bytes/sec.\nRequests: %d susceed, %d failed.\n", 521 (int)((speed+failed)/(benchtime/60.0f)), 522 (int)(bytes/(float)benchtime), 523 speed, 524 failed); 525 } 526 return i; 527 } 528 529 /** 530 *benchcore是子进程进行压力测试的函数,被每个子进程调用。 531 *这里使用了SIGALRM信号来控制时间, 532 *alarm函数设置了多少时间之后产生SIGALRM信号,一旦产生此信号,将运行alarm_handler(), 533 *使得timerexpired=1,这样可以通过判断timerexpired值来退出程序。 534 *另外,全局变量force表示我们是否在发出请求后需要等待服务器的响应结果 535 */ 536 void benchcore(const char *host,const int port,const char *req) 537 { 538 int rlen; 539 char buf[1500];//记录服务器响应请求所返回的数据 540 int s,i; 541 struct sigaction sa; 542 543 /* setup alarm signal handler */ 544 sa.sa_handler=alarm_handler;//将函数alarm_handler地址赋值给sa.alarm_handler,作为信号处理函数 545 sa.sa_flags=0; 546 if(sigaction(SIGALRM,&sa,NULL))//超时会产生信号SIGALRM,用sa中的指定函数处理 547 exit(3); 548 alarm(benchtime);//开始计时 549 550 rlen=strlen(req); 551 nexttry:while(1) 552 { 553 if(timerexpired)//一旦超时则返回 554 { 555 if(failed>0) 556 { 557 /* fprintf(stderr,"Correcting failed by signal\n"); */ 558 failed--; 559 } 560 return; 561 } 562 s=Socket(host,port);//调用socket建立TCP连接 563 if(s<0) { failed++;continue;} 564 if(rlen!=write(s,req,rlen)) {failed++;close(s);continue;}//发出请求 565 if(http10==0) //针对http0.9做的特殊处理 566 if(shutdown(s,1)) { failed++;close(s);continue;} 567 if(force==0) //全局变量force表示是否要等待服务器返回的数据 568 { 569 /* read all available data from socket */ 570 while(1) 571 { 572 if(timerexpired) break; 573 i=read(s,buf,1500);//从socket读取返回数据 574 /* fprintf(stderr,"%d\n",i); */ 575 if(i<0) 576 { 577 failed++; 578 close(s); 579 goto nexttry; 580 } 581 else 582 if(i==0) break; 583 else 584 bytes+=i; 585 } 586 } 587 if(close(s)) {failed++;continue;} 588 speed++; 589 } 590 }
原文链接:https://www.cnblogs.com/lanshanxiao/p/11651265.html
如有疑问请与原作者联系
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
- day17 2019-08-26
- 第一篇文章 2018-08-06
- CORS跨域请求C#版 2018-06-27
- 学习webbench需要掌握的基础知识(webbench源代码学习心得) 2018-06-18
- webbench1.5源码读后总结 2018-06-18
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash