/* * weighttp - a lightweight and simple webserver benchmarking tool * * Author: * Copyright (c) 2009-2011 Thomas Porzelt * * License: * MIT, see COPYING file */ #include "weighttp.h" extern int optind, optopt; /* getopt */ static void show_help(void) { printf("weighttp \n"); printf(" -n num number of requests (mandatory)\n"); printf(" -t num threadcount (default: 1)\n"); printf(" -c num concurrent clients (default: 1)\n"); printf(" -k keep alive (default: no)\n"); printf(" -6 use ipv6 (default: no)\n"); printf(" -H str add header to request\n"); printf(" -h show help and exit\n"); printf(" -v show version and exit\n\n"); printf("example: weighttpd -n 10000 -c 10 -t 2 -k -H \"User-Agent: foo\" localhost/index.html\n\n"); } static struct addrinfo *resolve_host(char *hostname, uint16_t port, uint8_t use_ipv6) { int err; char port_str[6]; struct addrinfo hints, *res, *res_first, *res_last; memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; sprintf(port_str, "%d", port); err = getaddrinfo(hostname, port_str, &hints, &res_first); if (err) { W_ERROR("could not resolve hostname: %s", hostname); return NULL; } /* search for an ipv4 address, no ipv6 yet */ res_last = NULL; for (res = res_first; res != NULL; res = res->ai_next) { if (res->ai_family == AF_INET && !use_ipv6) break; else if (res->ai_family == AF_INET6 && use_ipv6) break; res_last = res; } if (!res) { freeaddrinfo(res_first); W_ERROR("could not resolve hostname: %s", hostname); return NULL; } if (res != res_first) { /* unlink from list and free rest */ res_last->ai_next = res->ai_next; freeaddrinfo(res_first); res->ai_next = NULL; } return res; } static char *forge_request(char *url, char keep_alive, char **host, uint16_t *port, char **headers, uint8_t headers_num) { char *c, *end; char *req; uint32_t len; uint8_t i; uint8_t have_user_agent; char *header_host; *host = NULL; *port = 0; header_host = NULL; if (strncmp(url, "http://", 7) == 0) url += 7; else if (strncmp(url, "https://", 8) == 0) { W_ERROR("%s", "no ssl support yet"); url += 8; return NULL; } len = strlen(url); if ((c = strchr(url, ':'))) { /* found ':' => host:port */ *host = W_MALLOC(char, c - url + 1); memcpy(*host, url, c - url); (*host)[c - url] = '\0'; if ((end = strchr(c+1, '/'))) { *end = '\0'; *port = atoi(c+1); *end = '/'; url = end; } else { *port = atoi(c+1); url += len; } } else { *port = 80; if ((c = strchr(url, '/'))) { *host = W_MALLOC(char, c - url + 1); memcpy(*host, url, c - url); (*host)[c - url] = '\0'; url = c; } else { *host = W_MALLOC(char, len + 1); memcpy(*host, url, len); (*host)[len] = '\0'; url += len; } } if (*port == 0) { W_ERROR("%s", "could not parse url"); free(*host); return NULL; } if (*url == '\0') url = "/"; // total request size len = strlen("GET HTTP/1.1\r\nHost: :65536\r\nConnection: keep-alive\r\n\r\n") + 1; len += strlen(*host); len += strlen(url); have_user_agent = 0; for (i = 0; i < headers_num; i++) { if (strncmp(headers[i], "Host:", sizeof("Host:")-1) == 0) { if (header_host) { W_ERROR("%s", "Duplicate Host header"); free(*host); return NULL; } header_host = headers[i] + 5; if (*header_host == ' ') header_host++; if (strlen(header_host) == 0) { W_ERROR("%s", "Invalid Host header"); free(*host); return NULL; } len += strlen(header_host); continue; } len += strlen(headers[i]) + strlen("\r\n"); if (strncmp(headers[i], "User-Agent:", sizeof("User-Agent:")-1) == 0) have_user_agent = 1; } if (!have_user_agent) len += strlen("User-Agent: weighttp/" VERSION "\r\n"); req = W_MALLOC(char, len); strcpy(req, "GET "); strcat(req, url); strcat(req, " HTTP/1.1\r\nHost: "); if (header_host) { strcat(req, header_host); } else { strcat(req, *host); if (*port != 80) sprintf(req + strlen(req), ":%"PRIu16, *port); } strcat(req, "\r\n"); if (!have_user_agent) sprintf(req + strlen(req), "User-Agent: weighttp/" VERSION "\r\n"); for (i = 0; i < headers_num; i++) { if (strncmp(headers[i], "Host:", sizeof("Host:")-1) == 0) continue; strcat(req, headers[i]); strcat(req, "\r\n"); } if (keep_alive) strcat(req, "Connection: keep-alive\r\n\r\n"); else strcat(req, "Connection: close\r\n\r\n"); return req; } uint64_t str_to_uint64(char *str) { uint64_t i; for (i = 0; *str; str++) { if (*str < '0' || *str > '9') return UINT64_MAX; i *= 10; i += *str - '0'; } return i; } int main(int argc, char *argv[]) { Worker **workers; pthread_t *threads; int i; char c; int err; struct ev_loop *loop; ev_tstamp ts_start, ts_end; Config config; Worker *worker; char *host; uint16_t port; uint8_t use_ipv6; uint16_t rest_concur, rest_req; Stats stats; ev_tstamp duration; int sec, millisec, microsec; uint64_t rps; uint64_t kbps; char **headers; uint8_t headers_num; printf("weighttp - a lightweight and simple webserver benchmarking tool\n\n"); headers = NULL; headers_num = 0; /* default settings */ use_ipv6 = 0; config.thread_count = 1; config.concur_count = 1; config.req_count = 0; config.keep_alive = 0; while ((c = getopt(argc, argv, ":hv6kn:t:c:H:")) != -1) { switch (c) { case 'h': show_help(); return 0; case 'v': printf("version: " VERSION "\n"); printf("build-date: " __DATE__ " " __TIME__ "\n\n"); return 0; case '6': use_ipv6 = 1; break; case 'k': config.keep_alive = 1; break; case 'n': config.req_count = str_to_uint64(optarg); break; case 't': config.thread_count = atoi(optarg); break; case 'c': config.concur_count = atoi(optarg); break; case 'H': headers = W_REALLOC(headers, char*, headers_num+1); headers[headers_num] = optarg; headers_num++; break; case '?': W_ERROR("unkown option: -%c", optopt); show_help(); return 1; } } if ((argc - optind) < 1) { W_ERROR("%s", "missing url argument\n"); show_help(); return 1; } else if ((argc - optind) > 1) { W_ERROR("%s", "too many arguments\n"); show_help(); return 1; } /* check for sane arguments */ if (!config.thread_count) { W_ERROR("%s", "thread count has to be > 0\n"); show_help(); return 1; } if (!config.concur_count) { W_ERROR("%s", "number of concurrent clients has to be > 0\n"); show_help(); return 1; } if (!config.req_count) { W_ERROR("%s", "number of requests has to be > 0\n"); show_help(); return 1; } if (config.req_count == UINT64_MAX || config.thread_count > config.req_count || config.thread_count > config.concur_count || config.concur_count > config.req_count) { W_ERROR("%s", "insane arguments\n"); show_help(); return 1; } loop = ev_default_loop(0); if (!loop) { W_ERROR("%s", "could not initialize libev\n"); return 2; } if (NULL == (config.request = forge_request(argv[optind], config.keep_alive, &host, &port, headers, headers_num))) { return 1; } config.request_size = strlen(config.request); //printf("Request (%d):\n==========\n%s==========\n", config.request_size, config.request); //printf("host: '%s', port: %d\n", host, port); /* resolve hostname */ if(!(config.saddr = resolve_host(host, port, use_ipv6))) { return 1; } /* spawn threads */ threads = W_MALLOC(pthread_t, config.thread_count); workers = W_MALLOC(Worker*, config.thread_count); rest_concur = config.concur_count % config.thread_count; rest_req = config.req_count % config.thread_count; printf("starting benchmark...\n"); memset(&stats, 0, sizeof(stats)); ts_start = ev_time(); for (i = 0; i < config.thread_count; i++) { uint64_t reqs = config.req_count / config.thread_count; uint16_t concur = config.concur_count / config.thread_count; uint64_t diff; if (rest_concur) { diff = (i == config.thread_count) ? rest_concur : (rest_concur / config.thread_count); diff = diff ? diff : 1; concur += diff; rest_concur -= diff; } if (rest_req) { diff = (i == config.thread_count) ? rest_req : (rest_req / config.thread_count); diff = diff ? diff : 1; reqs += diff; rest_req -= diff; } printf("spawning thread #%d: %"PRIu16" concurrent requests, %"PRIu64" total requests\n", i+1, concur, reqs); workers[i] = worker_new(i+1, &config, concur, reqs); if (!(workers[i])) { W_ERROR("%s", "failed to allocate worker or client"); return 1; } err = pthread_create(&threads[i], NULL, worker_thread, (void*)workers[i]); if (err != 0) { W_ERROR("failed spawning thread (%d)", err); return 2; } } for (i = 0; i < config.thread_count; i++) { err = pthread_join(threads[i], NULL); worker = workers[i]; if (err != 0) { W_ERROR("failed joining thread (%d)", err); return 3; } stats.req_started += worker->stats.req_started; stats.req_done += worker->stats.req_done; stats.req_success += worker->stats.req_success; stats.req_failed += worker->stats.req_failed; stats.bytes_total += worker->stats.bytes_total; stats.bytes_body += worker->stats.bytes_body; stats.req_2xx += worker->stats.req_2xx; stats.req_3xx += worker->stats.req_3xx; stats.req_4xx += worker->stats.req_4xx; stats.req_5xx += worker->stats.req_5xx; worker_free(worker); } ts_end = ev_time(); duration = ts_end - ts_start; sec = duration; duration -= sec; duration = duration * 1000; millisec = duration; duration -= millisec; microsec = duration * 1000; rps = stats.req_done / (ts_end - ts_start); kbps = stats.bytes_total / (ts_end - ts_start) / 1024; printf("\nfinished in %d sec, %d millisec and %d microsec, %"PRIu64" req/s, %"PRIu64" kbyte/s\n", sec, millisec, microsec, rps, kbps); printf("requests: %"PRIu64" total, %"PRIu64" started, %"PRIu64" done, %"PRIu64" succeeded, %"PRIu64" failed, %"PRIu64" errored\n", config.req_count, stats.req_started, stats.req_done, stats.req_success, stats.req_failed, stats.req_error ); printf("status codes: %"PRIu64" 2xx, %"PRIu64" 3xx, %"PRIu64" 4xx, %"PRIu64" 5xx\n", stats.req_2xx, stats.req_3xx, stats.req_4xx, stats.req_5xx ); printf("traffic: %"PRIu64" bytes total, %"PRIu64" bytes http, %"PRIu64" bytes data\n", stats.bytes_total, stats.bytes_total - stats.bytes_body, stats.bytes_body ); ev_default_destroy(); free(threads); free(workers); free(config.request); free(host); free(headers); freeaddrinfo(config.saddr); return 0; }