Preface
Working in an operations department, running load tests is an indispensable part of day-to-day work. So by reading the source code of the WebBench project, I did a quick pass to understand the basic principles of stress-testing tools and recorded some notes here.
The cleaned-up source code can be downloaded here. It includes a correct CMake file and some appropriate changes, so it can be compiled with CLion on Windows; it should also work on Linux.
The actual runtime effect on Windows is as follows:

Source Code Walkthrough
socket.c
/* $Id: socket.c 1.1 1995/01/01 07:11:14 cthuang Exp $
*
* This module has been modified by Radim Kolar for OS/2 emx
*/
/***********************************************************************
module: socket.c
program: popclient
SCCS ID: @(#)socket.c 1.5 4/1/94
programmer: Virginia Tech Computing Center
compiler: DEC RISC C compiler (Ultrix 4.1)
environment: DEC Ultrix 4.3
description: UNIX sockets code.
***********************************************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
// In simple terms, this function connects based on the target address and port, and returns a socket.
int Socket(const char *host, int clientPort) {
int sock;
unsigned long inaddr;
struct sockaddr_in ad;
struct hostent *hp;
memset(&ad, 0, sizeof(ad));
ad.sin_family = AF_INET;
// First, convert a dotted-decimal IP address into an integer.
inaddr = inet_addr(host);
if (inaddr != INADDR_NONE)
memcpy(&ad.sin_addr, &inaddr, sizeof(inaddr));
else {
// If conversion fails, it indicates a domain name, so call gethostbyname() for DNS resolution.
hp = gethostbyname(host);
if (hp == NULL)
return -1;
memcpy(&ad.sin_addr, hp->h_addr, hp->h_length);
}
ad.sin_port = htons(clientPort);
// Create a socket and initiate the connection.
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
return sock;
if (connect(sock, (struct sockaddr *) &ad, sizeof(ad)) < 0)
return -1;
return sock;
}
webbench.c
/*
* (C) Radim Kolar 1997-2004
* This is free software, see GNU Public License version 2 for
* details.
*
* Simple forking WWW Server benchmark:
*
* Usage:
* webbench --help
*
* Return codes:
* 0 - sucess
* 1 - benchmark failed (server is not on-line)
* 2 - bad param
* 3 - internal error, fork failed
*
*/
#include "socket.c"
#include <unistd.h>
#include <sys/param.h>
#include <getopt.h>
#include <strings.h>
#include <time.h>
#include <signal.h>
/* values */
// The volatile keyword declares this variable as volatile; for consistency it should be stored in memory.
volatile int timerexpired = 0;
int speed = 0;
int failed = 0;
int bytes = 0;
// HTTP protocol version
/* globals */
int http10 = 1; /* 0 - http/0.9, 1 - http/1.0, 2 - http/1.1 */
// Define the HTTP methods allowed by the program here.
/* Allow: GET, HEAD, OPTIONS, TRACE */
#define METHOD_GET 0
#define METHOD_HEAD 1
#define METHOD_OPTIONS 2
#define METHOD_TRACE 3
#define PROGRAM_VERSION "1.5"
int method = METHOD_GET;
// Number of concurrent clients
int clients = 1;
int force = 0;
int force_reload = 0;
// Proxy server address and port
int proxyport = 80;
char *proxyhost = NULL;
int benchtime = 30;
/* internal */
int mypipe[2];
char host[MAXHOSTNAMELEN];
#define REQUEST_SIZE 2048
// This string stores the HTTP request content.
char request[REQUEST_SIZE];
/*
* To use getopt_long(), we first need to determine two structures:
1. A string including the required short option characters; if an option takes an argument, add a ":" after the character.
2. A struct array containing long option strings. Each struct contains 4 fields:
the first is the long option string; the second is a flag which can only be 0, 1, or 2, meaning respectively no option, has option, option is optional;
the third is always NULL; the fourth is the corresponding short option character.
The last element of the struct array is all NULL and 0, marking the end.
*/
static const struct option long_options[] =
{
{"force", no_argument, &force, 1},
{"reload", no_argument, &force_reload, 1},
{"time", required_argument, NULL, 't'},
{"help", no_argument, NULL, '?'},
{"http09", no_argument, NULL, '9'},
{"http10", no_argument, NULL, '1'},
{"http11", no_argument, NULL, '2'},
{"get", no_argument, &method, METHOD_GET},
{"head", no_argument, &method, METHOD_HEAD},
{"options", no_argument, &method, METHOD_OPTIONS},
{"trace", no_argument, &method, METHOD_TRACE},
{"version", no_argument, NULL, 'V'},
{"proxy", required_argument, NULL, 'p'},
{"clients", required_argument, NULL, 'c'},
{NULL, 0, NULL, 0}
};
/* prototypes */
static void benchcore(const char *host, const int port, const char *request);
static int bench(void);
static void build_request(const char *url);
// This is a signal handler.
// If an alarm signal occurs, set the timeout flag to 1.
// The request loop stops issuing new requests when it detects this flag.
static void alarm_handler(int signal) {
timerexpired = 1;
}
// Print help information to standard output.
static void usage(void) {
fprintf(stderr,
"webbench [option]... URL\n"
" -f|--force Don't wait for reply from server.\n"
" -r|--reload Send reload request - Pragma: no-cache.\n"
" -t|--time <sec> Run benchmark for <sec> seconds. Default 30.\n"
" -p|--proxy <server:port> Use proxy server for request.\n"
" -c|--clients <n> Run <n> HTTP clients at once. Default one.\n"
" -9|--http09 Use HTTP/0.9 style requests.\n"
" -1|--http10 Use HTTP/1.0 protocol.\n"
" -2|--http11 Use HTTP/1.1 protocol.\n"
" --get Use GET request method.\n"
" --head Use HEAD request method.\n"
" --options Use OPTIONS request method.\n"
" --trace Use TRACE request method.\n"
" -?|-h|--help This information.\n"
" -V|--version Display program version.\n"
);
};
int main(int argc, char *argv[]) {
int opt = 0;
int options_index = 0;
char *tmp = NULL;
// If called without extra arguments, print help and exit.
if (argc == 1) {
usage();
return 2;
}
// Use getopt_long() to parse command-line options.
// Each call parses one option; when parsing finishes, it returns EOF.
while ((opt = getopt_long(argc, argv, "912Vfrt:p:c:?h", long_options, &options_index)) != EOF) {
switch (opt) {
case 0 :
break;
case 'f':
force = 1;
break;
case 'r':
force_reload = 1;
break;
case '9':
http10 = 0;
break;
case '1':
http10 = 1;
break;
case '2':
http10 = 2;
break;
case 'V':
printf(PROGRAM_VERSION"\n");
exit(0);
case 't':
benchtime = atoi(optarg);
break;
case 'p':
/* proxy server parsing server:port */
// optarg is a string pointer, pointing to the parsed argument.
tmp = strrchr(optarg, ':');
proxyhost = optarg;
if (tmp == NULL) {
break;
}
// Validate the proxy server argument here.
if (tmp == optarg) {
fprintf(stderr, "Error in option --proxy %s: Missing hostname.\n", optarg);
return 2;
}
if (tmp == optarg + strlen(optarg) - 1) {
fprintf(stderr, "Error in option --proxy %s Port number is missing.\n", optarg);
return 2;
}
// If OK, set the proxy server port and address here.
*tmp = '\0';
proxyport = atoi(tmp + 1);
break;
case ':':
case 'h':
case '?':
usage();
return 2;
break;
case 'c':
// Set how many concurrent clients.
clients = atoi(optarg);
break;
}
}
// If there are no more option characters, getopt() returns -1.
// Then optind is the index in argv of the first argv-element that is not an option.
// This means optind points to the first element in argv that is not an option; normally it should point to the URL.
// If optind points outside argv, it means no URL was provided, so report a missing-argument error.
if (optind == argc) {
fprintf(stderr, "webbench: Missing URL!\n");
usage();
return 2;
}
// Handle invalid parameters and reset to correct values.
if (clients == 0) clients = 1;
if (benchtime == 0) benchtime = 60;
/* Copyright */
fprintf(stderr, "Webbench - Simple Web Benchmark "PROGRAM_VERSION"\n"
"Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.\n"
);
// In the normal case, optind should point to the position of the URL string in argv, i.e., the last element.
// Build the request content based on this URL.
build_request(argv[optind]);
/* print bench info */
// The following prints benchmark-related information,
// such as the request type, URL, protocol version, number of clients, etc.
printf("\nBenchmarking: ");
switch (method) {
case METHOD_GET:
default:
printf("GET");
break;
case METHOD_OPTIONS:
printf("OPTIONS");
break;
case METHOD_HEAD:
printf("HEAD");
break;
case METHOD_TRACE:
printf("TRACE");
break;
}
printf(" %s", argv[optind]);
switch (http10) {
case 0:
printf(" (using HTTP/0.9)");
break;
case 2:
printf(" (using HTTP/1.1)");
break;
}
printf("\n");
if (clients == 1) printf("1 client");
else
printf("%d clients", clients);
// Print more information based on various parameters.
printf(", running %d sec", benchtime);
if (force) printf(", early socket close");
if (proxyhost != NULL) printf(", via proxy server %s:%d", proxyhost, proxyport);
if (force_reload) printf(", forcing reload");
printf(".\n");
// Finally, start the test. The request has been built and stored in the request string.
return bench();
}
// This function builds the request content and stores it into the global request char array.
void build_request(const char *url) {
char tmp[10];
int i;
// Zero out the first n bytes of memory (string).
bzero(host, MAXHOSTNAMELEN);
bzero(request, REQUEST_SIZE);
// Validate parameters and reset them to a legal state.
if (force_reload && proxyhost != NULL && http10 < 1) http10 = 1;
if (method == METHOD_HEAD && http10 < 1) http10 = 1;
if (method == METHOD_OPTIONS && http10 < 2) http10 = 2;
if (method == METHOD_TRACE && http10 < 2) http10 = 2;
// Build the request based on the method; overall this is a bunch of string operations.
switch (method) {
default:
case METHOD_GET:
strcpy(request, "GET");
break;
case METHOD_HEAD:
strcpy(request, "HEAD");
break;
case METHOD_OPTIONS:
strcpy(request, "OPTIONS");
break;
case METHOD_TRACE:
strcpy(request, "TRACE");
break;
}
strcat(request, " ");
// Validate the URL.
// It must include the protocol name.
if (NULL == strstr(url, "://")) {
fprintf(stderr, "\n%s: is not a valid URL.\n", url);
exit(2);
}
// It cannot be too long.
if (strlen(url) > 1500) {
fprintf(stderr, "URL is too long.\n");
exit(2);
}
// Without a proxy server, the protocol must be HTTP.
if (proxyhost == NULL) if (0 != strncasecmp("http://", url, 7)) {
fprintf(stderr, "\nOnly HTTP protocol is directly supported, set --proxy for others.\n");
exit(2);
}
/* protocol/host delimiter */
// Find the start position of the actual URL content after stripping the *:// prefix.
i = strstr(url, "://") - url + 3;
/* printf("%d\n",i); */
// Check whether the URL ends with '/'.
if (strchr(url + i, '/') == NULL) {
fprintf(stderr, "\nInvalid URL syntax - hostname don't ends with '/'.\n");
exit(2);
}
if (proxyhost == NULL) {
// If not using a proxy, extract the hostname as the target address for sending the request.
// First, check whether the URL contains a port number.
if (index(url + i, ':') != NULL &&
index(url + i, ':') < index(url + i, '/')) {
// If a port is included, separate the hostname and port; extract the hostname first.
strncpy(host, url + i, strchr(url + i, ':') - url - i);
// Then manually parse out the port string, convert it to an integer, and store it.
bzero(tmp, 10);
strncpy(tmp, index(url + i, ':') + 1, strchr(url + i, '/') - index(url + i, ':') - 1);
/* printf("tmp=%s\n",tmp); */
proxyport = atoi(tmp);
if (proxyport == 0) proxyport = 80;
} else {
// Otherwise, just extract the hostname.
// strcspn() returns the length of the string before certain characters.
// Here, it is the number of characters before '/'.
strncpy(host, url + i, strcspn(url + i, "/"));
}
// Finally, attach the request path.
// printf("Host=%s\n",host);
strcat(request + strlen(request), url + i + strcspn(url + i, "/"));
} else {
// If using a proxy, there's no need to care about the hostname, because the destination will be the proxy.
// In that case, just paste the URL into the request; the proxy will send the request to that address.
// printf("ProxyHost=%s\nProxyPort=%d\n",proxyhost,proxyport);
strcat(request, url);
}
// Protocol version
if (http10 == 1)
strcat(request, " HTTP/1.0");
else if (http10 == 2)
strcat(request, " HTTP/1.1");
strcat(request, "\r\n");
// Add UA.
if (http10 > 0)
strcat(request, "User-Agent: WebBench "PROGRAM_VERSION"\r\n");
// If not using a proxy, also include the Host header.
if (proxyhost == NULL && http10 > 0) {
strcat(request, "Host: ");
strcat(request, host);
strcat(request, "\r\n");
}
// And a few other HTTP headers.
// From this header handling we can see that sometimes options are not always handled exactly the way you expect.
// For example, this program adjusts various options in code to comply with rules.
if (force_reload && proxyhost != NULL) {
strcat(request, "Pragma: no-cache\r\n");
}
if (http10 > 1)
strcat(request, "Connection: close\r\n");
/* Finally, add an empty line to end the request. */
if (http10 > 0) strcat(request, "\r\n");
// printf("Req=%s\n",request);
}
/* vraci system rc error kod */
// This function is responsible for a series of preparations before issuing requests.
static int bench(void) {
int i, j, k;
pid_t pid = 0;
FILE *f;
/* check avaibility of target server */
// Create a socket connection to the target server to check whether it can connect normally.
i = Socket(proxyhost == NULL ? host : proxyhost, proxyport);
if (i < 0) {
fprintf(stderr, "\nConnect to server failed. Aborting benchmark.\n");
return 1;
}
close(i);
/* create pipe */
// Then create a pipe for communicating with child processes.
if (pipe(mypipe)) {
perror("pipe failed.");
return 3;
}
/* not needed, since we have alarm() in childrens */
/* wait 4 next system clock tick */
/*
cas=time(NULL);
while(time(NULL)==cas)
sched_yield();
*/
/* fork childs */
// Create child processes according to the preset number of clients.
for (i = 0; i < clients; i++) {
pid = fork();
// If this is a child process or an error occurred, exit the loop.
if (pid <= (pid_t) 0) {
/* child process or error*/
sleep(1); /* make childs faster */
break;
}
}
// If fork() fails, report and exit.
if (pid < (pid_t) 0) {
fprintf(stderr, "problems forking worker no. %d\n", i);
perror("fork failed.");
return 3;
}
// For child processes, have them call the benchmark function to run the test.
if (pid == (pid_t) 0) {
/* I am a child */
// Depending on whether a proxy is used, send the request to different hostnames.
if (proxyhost == NULL)
benchcore(host, proxyport, request);
else
benchcore(proxyhost, proxyport, request);
// fdopen() converts a file descriptor into FILE *, allowing use of standard library functions.
f = fdopen(mypipe[1], "w");
// If opening fails, still report and exit.
// It would be better to define the error codes for readability.
if (f == NULL) {
perror("open pipe for writing failed.");
return 3;
}
/* fprintf(stderr,"Child - %d %d\n",speed,failed); */
// Write the test results into the pipe for the parent process to read.
fprintf(f, "%d %d %d\n", speed, failed, bytes);
fclose(f);
return 0;
} else {
// The parent process reads the results returned by the child processes.
// The pattern here is that multiple child processes write to the same pipe, then the parent reads them.
f = fdopen(mypipe[0], "r");
if (f == NULL) {
perror("open pipe for reading failed.");
return 3;
}
// Disable buffering for this stream
// to avoid waiting during reads below.
setvbuf(f, NULL, _IONBF, 0);
speed = 0;
failed = 0;
bytes = 0;
// Start looping to read results returned by child processes.
while (1) {
pid = fscanf(f, "%d %d %d", &i, &j, &k);
if (pid < 2) {
fprintf(stderr, "Some of our childrens died.\n");
break;
}
speed += i;
failed += j;
bytes += k;
/* fprintf(stderr,"*Knock* %d %d read=%d\n",speed,failed,pid); */
// If the number of reads matches the number of child processes, exit the loop.
if (--clients == 0) break;
}
fclose(f);
// Finally, print the aggregated results.
printf("\nSpeed=%d pages/min, %d bytes/sec.\nRequests: %d susceed, %d failed.\n",
(int) ((speed + failed) / (benchtime / 60.0f)),
(int) (bytes / (float) benchtime),
speed,
failed);
}
return i;
}
// This is the core function responsible for issuing requests.
void benchcore(const char *host, const int port, const char *req) {
int rlen;
char buf[1500];
int s, i;
struct sigaction sa;
/* setup alarm signal handler */
// Add a signal handler here so that we can stop the benchmark via a timer signal.
sa.sa_handler = alarm_handler;
sa.sa_flags = 0;
if (sigaction(SIGALRM, &sa, NULL))
exit(3);
// Then set a timer.
// When time is up, send an alarm signal.
// This stops the program.
alarm(benchtime);
rlen = strlen(req);
// Enter the main loop.
nexttry:
while (1) {
// If a stop signal is detected, exit the loop.
if (timerexpired) {
// In that case, we need to correct the statistics.
// Because a connection has been interrupted by this signal, but this fail should not be counted.
if (failed > 0) {
/* fprintf(stderr,"Correcting failed by signal\n"); */
failed--;
}
return;
}
// Initiate a connection.
s = Socket(host, port);
// If the connection fails, add to the stats.
if (s < 0) {
failed++;
continue;
}
// If the request cannot be sent successfully, also add to error stats.
if (rlen != write(s, req, rlen)) {
failed++;
close(s);
continue;
}
// In general, count possible errors under various situations.
if (http10 == 0) if (shutdown(s, 1)) {
failed++;
close(s);
continue;
}
// If we do not force-close the connection,
// then it is also necessary to read all returned data.
if (force == 0) {
/* read all available data from socket */
while (1) {
// Of course, this read process should be interruptible by the stop signal.
if (timerexpired) break;
i = read(s, buf, 1500);
/* fprintf(stderr,"%d\n",i); */
// If something goes wrong during reading, include it in the stats;
// if reading is finished, exit.
if (i < 0) {
failed++;
close(s);
goto nexttry;
}
else if (i == 0) break;
else
// If reading is normal, count the bytes.
bytes += i;
}
}
// The connection must also close normally.
if (close(s)) {
failed++;
continue;
}
speed++;
}
}