/* * nghttp2 - HTTP/2 C Library * * Copyright (c) 2014 Tatsuhiro Tsujikawa * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "h2load.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_SPDYLAY #include #endif // HAVE_SPDYLAY #include #include #include "http-parser/http_parser.h" #include "h2load_http2_session.h" #ifdef HAVE_SPDYLAY #include "h2load_spdy_session.h" #endif // HAVE_SPDYLAY #include "ssl.h" #include "http2.h" #include "util.h" #include "template.h" using namespace nghttp2; namespace h2load { Config::Config() : addrs(nullptr), nreqs(1), nclients(1), nthreads(1), max_concurrent_streams(-1), window_bits(16), connection_window_bits(16), no_tls_proto(PROTO_HTTP2), port(0), default_port(0), verbose(false) {} Config::~Config() { freeaddrinfo(addrs); } Config config; namespace { void debug(const char *format, ...) { if (config.verbose) { fprintf(stderr, "[DEBUG] "); va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); } } } // namespace namespace { void debug_nextproto_error() { #ifdef HAVE_SPDYLAY debug("no supported protocol was negotiated, expected: %s, " "spdy/2, spdy/3, spdy/3.1\n", NGHTTP2_PROTO_VERSION_ID); #else // !HAVE_SPDYLAY debug("no supported protocol was negotiated, expected: %s\n", NGHTTP2_PROTO_VERSION_ID); #endif // !HAVE_SPDYLAY } } // namespace RequestStat::RequestStat() : completed(false) {} Stats::Stats(size_t req_todo) : req_todo(0), req_started(0), req_done(0), req_success(0), req_status_success(0), req_failed(0), req_error(0), bytes_total(0), bytes_head(0), bytes_body(0), status(), req_stats(req_todo) {} Stream::Stream() : status_success(-1) {} namespace { void writecb(struct ev_loop *loop, ev_io *w, int revents) { auto client = static_cast(w->data); auto rv = client->do_write(); if (rv == Client::ERR_CONNECT_FAIL) { client->disconnect(); rv = client->connect(); if (rv != 0) { client->fail(); return; } return; } if (rv != 0) { client->fail(); } } } // namespace namespace { void readcb(struct ev_loop *loop, ev_io *w, int revents) { auto client = static_cast(w->data); if (client->do_read() != 0) { client->fail(); return; } if (ev_is_active(&client->wev)) { writecb(loop, &client->wev, revents); // client->disconnect() and client->fail() may be called } } } // namespace Client::Client(Worker *worker, size_t req_todo) : worker(worker), ssl(nullptr), next_addr(config.addrs), reqidx(0), state(CLIENT_IDLE), req_todo(req_todo), req_started(0), req_done(0), fd(-1) { ev_io_init(&wev, writecb, 0, EV_WRITE); ev_io_init(&rev, readcb, 0, EV_READ); wev.data = this; rev.data = this; } Client::~Client() { disconnect(); } int Client::do_read() { return readfn(*this); } int Client::do_write() { return writefn(*this); } int Client::connect() { while (next_addr) { auto addr = next_addr; next_addr = next_addr->ai_next; fd = util::create_nonblock_socket(addr->ai_family); if (fd == -1) { continue; } if (config.scheme == "https") { ssl = SSL_new(worker->ssl_ctx); auto config = worker->config; if (!util::numeric_host(config->host.c_str())) { SSL_set_tlsext_host_name(ssl, config->host.c_str()); } SSL_set_fd(ssl, fd); SSL_set_connect_state(ssl); } auto rv = ::connect(fd, addr->ai_addr, addr->ai_addrlen); if (rv != 0 && errno != EINPROGRESS) { if (ssl) { SSL_free(ssl); ssl = nullptr; } close(fd); fd = -1; continue; } break; } if (fd == -1) { return -1; } writefn = &Client::connected; on_readfn = &Client::on_read; on_writefn = &Client::on_write; ev_io_set(&rev, fd, EV_READ); ev_io_set(&wev, fd, EV_WRITE); ev_io_start(worker->loop, &wev); return 0; } void Client::fail() { process_abandoned_streams(); disconnect(); } void Client::disconnect() { streams.clear(); session.reset(); state = CLIENT_IDLE; ev_io_stop(worker->loop, &wev); ev_io_stop(worker->loop, &rev); if (ssl) { SSL_set_shutdown(ssl, SSL_RECEIVED_SHUTDOWN); ERR_clear_error(); SSL_shutdown(ssl); SSL_free(ssl); ssl = nullptr; } if (fd != -1) { shutdown(fd, SHUT_WR); close(fd); fd = -1; } } void Client::submit_request() { auto req_stat = &worker->stats.req_stats[worker->stats.req_started++]; session->submit_request(req_stat); ++req_started; } void Client::process_abandoned_streams() { auto req_abandoned = req_todo - req_done; worker->stats.req_failed += req_abandoned; worker->stats.req_error += req_abandoned; worker->stats.req_done += req_abandoned; req_done = req_todo; } void Client::report_progress() { if (worker->id == 0 && worker->stats.req_done % worker->progress_interval == 0) { std::cout << "progress: " << worker->stats.req_done * 100 / worker->stats.req_todo << "% done" << std::endl; } } namespace { const char *get_tls_protocol(SSL *ssl) { auto session = SSL_get_session(ssl); switch (session->ssl_version) { case SSL2_VERSION: return "SSLv2"; case SSL3_VERSION: return "SSLv3"; case TLS1_2_VERSION: return "TLSv1.2"; case TLS1_1_VERSION: return "TLSv1.1"; case TLS1_VERSION: return "TLSv1"; default: return "unknown"; } } } // namespace namespace { void print_server_tmp_key(SSL *ssl) { #if OPENSSL_VERSION_NUMBER >= 0x10002000L EVP_PKEY *key; if (!SSL_get_server_tmp_key(ssl, &key)) { return; } auto key_del = defer(EVP_PKEY_free, key); std::cout << "Server Temp Key: "; switch (EVP_PKEY_id(key)) { case EVP_PKEY_RSA: std::cout << "RSA " << EVP_PKEY_bits(key) << " bits" << std::endl; break; case EVP_PKEY_DH: std::cout << "DH " << EVP_PKEY_bits(key) << " bits" << std::endl; break; case EVP_PKEY_EC: { auto ec = EVP_PKEY_get1_EC_KEY(key); auto ec_del = defer(EC_KEY_free, ec); auto nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); auto cname = EC_curve_nid2nist(nid); if (!cname) { cname = OBJ_nid2sn(nid); } std::cout << "ECDH " << cname << " " << EVP_PKEY_bits(key) << " bits" << std::endl; break; } } #endif // OPENSSL_VERSION_NUMBER >= 0x10002000L } } // namespace void Client::report_tls_info() { if (worker->id == 0 && !worker->tls_info_report_done) { worker->tls_info_report_done = true; auto cipher = SSL_get_current_cipher(ssl); std::cout << "Protocol: " << get_tls_protocol(ssl) << "\n" << "Cipher: " << SSL_CIPHER_get_name(cipher) << std::endl; print_server_tmp_key(ssl); } } void Client::terminate_session() { session->terminate(); } void Client::on_request(int32_t stream_id) { streams[stream_id] = Stream(); } void Client::on_header(int32_t stream_id, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen) { auto itr = streams.find(stream_id); if (itr == std::end(streams)) { return; } auto &stream = (*itr).second; if (stream.status_success == -1 && namelen == 7 && util::streq(":status", 7, name, namelen)) { int status = 0; for (size_t i = 0; i < valuelen; ++i) { if ('0' <= value[i] && value[i] <= '9') { status *= 10; status += value[i] - '0'; if (status > 999) { stream.status_success = 0; return; } } else { break; } } if (status >= 200 && status < 300) { ++worker->stats.status[2]; stream.status_success = 1; } else if (status < 400) { ++worker->stats.status[3]; stream.status_success = 1; } else if (status < 600) { ++worker->stats.status[status / 100]; stream.status_success = 0; } else { stream.status_success = 0; } } } void Client::on_stream_close(int32_t stream_id, bool success, RequestStat *req_stat) { req_stat->stream_close_time = std::chrono::steady_clock::now(); if (success) { req_stat->completed = true; ++worker->stats.req_success; } ++worker->stats.req_done; ++req_done; if (success && streams[stream_id].status_success == 1) { ++worker->stats.req_status_success; } else { ++worker->stats.req_failed; } report_progress(); streams.erase(stream_id); if (req_done == req_todo) { terminate_session(); return; } if (req_started < req_todo) { submit_request(); return; } } int Client::noop() { return 0; } int Client::on_connect() { if (ssl) { report_tls_info(); const unsigned char *next_proto = nullptr; unsigned int next_proto_len; SSL_get0_next_proto_negotiated(ssl, &next_proto, &next_proto_len); for (int i = 0; i < 2; ++i) { if (next_proto) { if (util::check_h2_is_selected(next_proto, next_proto_len)) { session = make_unique(this); } else { #ifdef HAVE_SPDYLAY auto spdy_version = spdylay_npn_get_version(next_proto, next_proto_len); if (spdy_version) { session = make_unique(this, spdy_version); } else { debug_nextproto_error(); fail(); return -1; } #else // !HAVE_SPDYLAY debug_nextproto_error(); fail(); return -1; #endif // !HAVE_SPDYLAY } } #if OPENSSL_VERSION_NUMBER >= 0x10002000L SSL_get0_alpn_selected(ssl, &next_proto, &next_proto_len); #else // OPENSSL_VERSION_NUMBER < 0x10002000L break; #endif // OPENSSL_VERSION_NUMBER < 0x10002000L } if (!next_proto) { debug_nextproto_error(); fail(); return -1; } } else { switch (config.no_tls_proto) { case Config::PROTO_HTTP2: session = make_unique(this); break; #ifdef HAVE_SPDYLAY case Config::PROTO_SPDY2: session = make_unique(this, SPDYLAY_PROTO_SPDY2); break; case Config::PROTO_SPDY3: session = make_unique(this, SPDYLAY_PROTO_SPDY3); break; case Config::PROTO_SPDY3_1: session = make_unique(this, SPDYLAY_PROTO_SPDY3_1); break; #endif // HAVE_SPDYLAY default: // unreachable assert(0); } } state = CLIENT_CONNECTED; session->on_connect(); auto nreq = std::min(req_todo - req_started, (size_t)config.max_concurrent_streams); for (; nreq > 0; --nreq) { submit_request(); } signal_write(); return 0; } int Client::on_read(const uint8_t *data, size_t len) { auto rv = session->on_read(data, len); if (rv != 0) { return -1; } worker->stats.bytes_total += len; signal_write(); return 0; } int Client::on_write() { if (session->on_write() != 0) { return -1; } return 0; } int Client::read_clear() { uint8_t buf[8192]; for (;;) { ssize_t nread; while ((nread = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR) ; if (nread == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return 0; } return -1; } if (nread == 0) { return -1; } if (on_read(buf, nread) != 0) { return -1; } } return 0; } int Client::write_clear() { for (;;) { if (wb.rleft() > 0) { ssize_t nwrite; while ((nwrite = write(fd, wb.pos, wb.rleft())) == -1 && errno == EINTR) ; if (nwrite == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { ev_io_start(worker->loop, &wev); return 0; } return -1; } wb.drain(nwrite); continue; } if (on_write() != 0) { return -1; } if (wb.rleft() == 0) { wb.reset(); break; } } ev_io_stop(worker->loop, &wev); return 0; } int Client::connected() { if (!util::check_socket_connected(fd)) { return ERR_CONNECT_FAIL; } ev_io_start(worker->loop, &rev); ev_io_stop(worker->loop, &wev); if (ssl) { readfn = &Client::tls_handshake; writefn = &Client::tls_handshake; return do_write(); } readfn = &Client::read_clear; writefn = &Client::write_clear; if (on_connect() != 0) { return -1; } return 0; } int Client::tls_handshake() { ERR_clear_error(); auto rv = SSL_do_handshake(ssl); if (rv == 0) { return -1; } if (rv < 0) { auto err = SSL_get_error(ssl, rv); switch (err) { case SSL_ERROR_WANT_READ: ev_io_stop(worker->loop, &wev); return 0; case SSL_ERROR_WANT_WRITE: ev_io_start(worker->loop, &wev); return 0; default: return -1; } } ev_io_stop(worker->loop, &wev); readfn = &Client::read_tls; writefn = &Client::write_tls; if (on_connect() != 0) { return -1; } return 0; } int Client::read_tls() { uint8_t buf[8192]; ERR_clear_error(); for (;;) { auto rv = SSL_read(ssl, buf, sizeof(buf)); if (rv == 0) { return -1; } if (rv < 0) { auto err = SSL_get_error(ssl, rv); switch (err) { case SSL_ERROR_WANT_READ: return 0; case SSL_ERROR_WANT_WRITE: // renegotiation started return -1; default: return -1; } } if (on_read(buf, rv) != 0) { return -1; } } } int Client::write_tls() { ERR_clear_error(); for (;;) { if (wb.rleft() > 0) { auto rv = SSL_write(ssl, wb.pos, wb.rleft()); if (rv == 0) { return -1; } if (rv < 0) { auto err = SSL_get_error(ssl, rv); switch (err) { case SSL_ERROR_WANT_READ: // renegotiation started return -1; case SSL_ERROR_WANT_WRITE: ev_io_start(worker->loop, &wev); return 0; default: return -1; } } wb.drain(rv); continue; } if (on_write() != 0) { return -1; } if (wb.rleft() == 0) { break; } } ev_io_stop(worker->loop, &wev); return 0; } void Client::record_request_time(RequestStat *req_stat) { req_stat->request_time = std::chrono::steady_clock::now(); } void Client::signal_write() { ev_io_start(worker->loop, &wev); } Worker::Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t req_todo, size_t nclients, Config *config) : stats(req_todo), loop(ev_loop_new(0)), ssl_ctx(ssl_ctx), config(config), id(id), tls_info_report_done(false) { stats.req_todo = req_todo; progress_interval = std::max((size_t)1, req_todo / 10); auto nreqs_per_client = req_todo / nclients; auto nreqs_rem = req_todo % nclients; for (size_t i = 0; i < nclients; ++i) { auto req_todo = nreqs_per_client; if (nreqs_rem > 0) { ++req_todo; --nreqs_rem; } clients.push_back(make_unique(this, req_todo)); } } Worker::~Worker() { // first clear clients so that io watchers are stopped before // destructing ev_loop. clients.clear(); ev_loop_destroy(loop); } void Worker::run() { for (auto &client : clients) { if (client->connect() != 0) { std::cerr << "client could not connect to host" << std::endl; client->fail(); } } ev_run(loop, 0); } namespace { double within_sd(const std::vector> &workers, const std::chrono::microseconds &mean, const std::chrono::microseconds &sd, size_t n) { auto upper = mean.count() + sd.count(); auto lower = mean.count() - sd.count(); size_t m = 0; for (const auto &w : workers) { for (const auto &req_stat : w->stats.req_stats) { if (!req_stat.completed) { continue; } auto t = std::chrono::duration_cast( req_stat.stream_close_time - req_stat.request_time); if (lower <= t.count() && t.count() <= upper) { ++m; } } } return (m / static_cast(n)) * 100; } } // namespace namespace { TimeStats process_time_stats(const std::vector> &workers) { auto ts = TimeStats(); int64_t sum = 0; size_t n = 0; ts.time_min = std::chrono::microseconds::max(); ts.time_max = std::chrono::microseconds::min(); ts.within_sd = 0.; // standard deviation calculated using Rapid calculation method: // http://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods double a = 0, q = 0; for (const auto &w : workers) { for (const auto &req_stat : w->stats.req_stats) { if (!req_stat.completed) { continue; } ++n; auto t = std::chrono::duration_cast( req_stat.stream_close_time - req_stat.request_time); ts.time_min = std::min(ts.time_min, t); ts.time_max = std::max(ts.time_max, t); sum += t.count(); auto na = a + (t.count() - a) / n; q = q + (t.count() - a) * (t.count() - na); a = na; } } if (n == 0) { ts.time_max = ts.time_min = std::chrono::microseconds::zero(); return ts; } ts.time_mean = std::chrono::microseconds(sum / n); ts.time_sd = std::chrono::microseconds( static_cast(sqrt(q / n))); ts.within_sd = within_sd(workers, ts.time_mean, ts.time_sd, n); return ts; } } // namespace namespace { void resolve_host() { int rv; addrinfo hints, *res; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; hints.ai_flags = AI_ADDRCONFIG; rv = getaddrinfo(config.host.c_str(), util::utos(config.port).c_str(), &hints, &res); if (rv != 0) { std::cerr << "getaddrinfo() failed: " << gai_strerror(rv) << std::endl; exit(EXIT_FAILURE); } if (res == nullptr) { std::cerr << "No address returned" << std::endl; exit(EXIT_FAILURE); } config.addrs = res; } } // namespace namespace { std::string get_reqline(const char *uri, const http_parser_url &u) { std::string reqline; if (util::has_uri_field(u, UF_PATH)) { reqline = util::get_uri_field(uri, u, UF_PATH); } else { reqline = "/"; } if (util::has_uri_field(u, UF_QUERY)) { reqline += "?"; reqline += util::get_uri_field(uri, u, UF_QUERY); } return reqline; } } // namespace namespace { int client_select_next_proto_cb(SSL *ssl, unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg) { if (util::select_h2(const_cast(out), outlen, in, inlen)) { return SSL_TLSEXT_ERR_OK; } #ifdef HAVE_SPDYLAY if (spdylay_select_next_protocol(out, outlen, in, inlen) > 0) { return SSL_TLSEXT_ERR_OK; } #endif return SSL_TLSEXT_ERR_NOACK; } } // namespace namespace { template std::vector parse_uris(Iterator first, Iterator last) { std::vector reqlines; // First URI is treated specially. We use scheme, host and port of // this URI and ignore those in the remaining URIs if present. http_parser_url u; memset(&u, 0, sizeof(u)); if (first == last) { std::cerr << "no URI available" << std::endl; exit(EXIT_FAILURE); } auto uri = (*first).c_str(); ++first; if (http_parser_parse_url(uri, strlen(uri), 0, &u) != 0 || !util::has_uri_field(u, UF_SCHEMA) || !util::has_uri_field(u, UF_HOST)) { std::cerr << "invalid URI: " << uri << std::endl; exit(EXIT_FAILURE); } config.scheme = util::get_uri_field(uri, u, UF_SCHEMA); config.host = util::get_uri_field(uri, u, UF_HOST); config.default_port = util::get_default_port(uri, u); if (util::has_uri_field(u, UF_PORT)) { config.port = u.port; } else { config.port = config.default_port; } reqlines.push_back(get_reqline(uri, u)); for (; first != last; ++first) { http_parser_url u; memset(&u, 0, sizeof(u)); auto uri = (*first).c_str(); if (http_parser_parse_url(uri, strlen(uri), 0, &u) != 0) { std::cerr << "invalid URI: " << uri << std::endl; exit(EXIT_FAILURE); } reqlines.push_back(get_reqline(uri, u)); } return reqlines; } } // namespace namespace { std::vector read_uri_from_file(std::istream &infile) { std::vector uris; std::string line_uri; while (std::getline(infile, line_uri)) { uris.push_back(line_uri); } return uris; } } // namespace namespace { void print_version(std::ostream &out) { out << "h2load nghttp2/" NGHTTP2_VERSION << std::endl; } } // namespace namespace { void print_usage(std::ostream &out) { out << R"(Usage: h2load [OPTIONS]... [URI]... benchmarking tool for HTTP/2 and SPDY server)" << std::endl; } } // namespace namespace { void print_help(std::ostream &out) { print_usage(out); out << R"( Specify URI to access. Multiple URIs can be specified. URIs are used in this order for each client. All URIs are used, then first URI is used and then 2nd URI, and so on. The scheme, host and port in the subsequent URIs, if present, are ignored. Those in the first URI are used solely. Options: -n, --requests= Number of requests. Default: )" << config.nreqs << R"( -c, --clients= Number of concurrent clients. Default: )" << config.nclients << R"( -t, --threads= Number of native threads. Default: )" << config.nthreads << R"( -i, --input-file= Path of a file with multiple URIs are seperated by EOLs. This option will disable URIs getting from command-line. If '-' is given as , URIs will be read from stdin. URIs are used in this order for each client. All URIs are used, then first URI is used and then 2nd URI, and so on. The scheme, host and port in the subsequent URIs, if present, are ignored. Those in the first URI are used solely. -m, --max-concurrent-streams=(auto|) Max concurrent streams to issue per session. If "auto" is given, the number of given URIs is used. Default: auto -w, --window-bits= Sets the stream level initial window size to (2**)-1. For SPDY, 2** is used instead. -W, --connection-window-bits= Sets the connection level initial window size to (2**)-1. For SPDY, if is strictly less than 16, this option is ignored. Otherwise 2** is used for SPDY. -H, --header=
Add/Override a header to the requests. -p, --no-tls-proto= Specify ALPN identifier of the protocol to be used when accessing http URI without SSL/TLS.)"; #ifdef HAVE_SPDYLAY out << R"( Available protocols: spdy/2, spdy/3, spdy/3.1 and )"; #else // !HAVE_SPDYLAY out << R"( Available protocol: )"; #endif // !HAVE_SPDYLAY out << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( Default: )" << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( -v, --verbose Output debug information. --version Display version information and exit. -h, --help Display this help and exit.)" << std::endl; } } // namespace int main(int argc, char **argv) { while (1) { static int flag = 0; static option long_options[] = { {"requests", required_argument, nullptr, 'n'}, {"clients", required_argument, nullptr, 'c'}, {"threads", required_argument, nullptr, 't'}, {"max-concurrent-streams", required_argument, nullptr, 'm'}, {"window-bits", required_argument, nullptr, 'w'}, {"connection-window-bits", required_argument, nullptr, 'W'}, {"input-file", required_argument, nullptr, 'i'}, {"header", required_argument, nullptr, 'H'}, {"no-tls-proto", required_argument, nullptr, 'p'}, {"verbose", no_argument, nullptr, 'v'}, {"help", no_argument, nullptr, 'h'}, {"version", no_argument, &flag, 1}, {nullptr, 0, nullptr, 0}}; int option_index = 0; auto c = getopt_long(argc, argv, "hvW:c:m:n:p:t:w:H:i:", long_options, &option_index); if (c == -1) { break; } switch (c) { case 'n': config.nreqs = strtoul(optarg, nullptr, 10); break; case 'c': config.nclients = strtoul(optarg, nullptr, 10); break; case 't': #ifdef NOTHREADS std::cerr << "-t: WARNING: Threading disabled at build time, " << "no threads created." << std::endl; #else config.nthreads = strtoul(optarg, nullptr, 10); #endif // NOTHREADS break; case 'm': if (util::strieq("auto", optarg)) { config.max_concurrent_streams = -1; } else { config.max_concurrent_streams = strtoul(optarg, nullptr, 10); } break; case 'w': case 'W': { errno = 0; char *endptr = nullptr; auto n = strtoul(optarg, &endptr, 10); if (errno == 0 && *endptr == '\0' && n < 31) { if (c == 'w') { config.window_bits = n; } else { config.connection_window_bits = n; } } else { std::cerr << "-" << static_cast(c) << ": specify the integer in the range [0, 30], inclusive" << std::endl; exit(EXIT_FAILURE); } break; } case 'H': { char *header = optarg; // Skip first possible ':' in the header name char *value = strchr(optarg + 1, ':'); if (!value || (header[0] == ':' && header + 1 == value)) { std::cerr << "-H: invalid header: " << optarg << std::endl; exit(EXIT_FAILURE); } *value = 0; value++; while (isspace(*value)) { value++; } if (*value == 0) { // This could also be a valid case for suppressing a header // similar to curl std::cerr << "-H: invalid header - value missing: " << optarg << std::endl; exit(EXIT_FAILURE); } // Note that there is no processing currently to handle multiple // message-header fields with the same field name config.custom_headers.emplace_back(header, value); util::inp_strlower(config.custom_headers.back().name); break; } case 'i': { config.ifile = std::string(optarg); break; } case 'p': if (util::strieq(NGHTTP2_CLEARTEXT_PROTO_VERSION_ID, optarg)) { config.no_tls_proto = Config::PROTO_HTTP2; #ifdef HAVE_SPDYLAY } else if (util::strieq("spdy/2", optarg)) { config.no_tls_proto = Config::PROTO_SPDY2; } else if (util::strieq("spdy/3", optarg)) { config.no_tls_proto = Config::PROTO_SPDY3; } else if (util::strieq("spdy/3.1", optarg)) { config.no_tls_proto = Config::PROTO_SPDY3_1; #endif // HAVE_SPDYLAY } else { std::cerr << "-p: unsupported protocol " << optarg << std::endl; exit(EXIT_FAILURE); } break; case 'v': config.verbose = true; break; case 'h': print_help(std::cout); exit(EXIT_SUCCESS); case '?': util::show_candidates(argv[optind - 1], long_options); exit(EXIT_FAILURE); case 0: switch (flag) { case 1: // version option print_version(std::cout); exit(EXIT_SUCCESS); } break; default: break; } } if (argc == optind) { if (config.ifile.empty()) { std::cerr << "no URI or input file given" << std::endl; exit(EXIT_FAILURE); } } if (config.nreqs == 0) { std::cerr << "-n: the number of requests must be strictly greater than 0." << std::endl; exit(EXIT_FAILURE); } if (config.max_concurrent_streams == 0) { std::cerr << "-m: the max concurrent streams must be strictly greater " << "than 0." << std::endl; exit(EXIT_FAILURE); } if (config.nthreads == 0) { std::cerr << "-t: the number of threads must be strictly greater than 0." << std::endl; exit(EXIT_FAILURE); } if (config.nreqs < config.nclients) { std::cerr << "-n, -c: the number of requests must be greater than or " << "equal to the concurrent clients." << std::endl; exit(EXIT_FAILURE); } if (config.nthreads > std::thread::hardware_concurrency()) { std::cerr << "-t: warning: the number of threads is greater than hardware " << "cores." << std::endl; } struct sigaction act; memset(&act, 0, sizeof(struct sigaction)); act.sa_handler = SIG_IGN; sigaction(SIGPIPE, &act, nullptr); OPENSSL_config(nullptr); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); SSL_library_init(); #ifndef NOTHREADS ssl::LibsslGlobalLock lock; #endif // NOTHREADS auto ssl_ctx = SSL_CTX_new(SSLv23_client_method()); if (!ssl_ctx) { std::cerr << "Failed to create SSL_CTX: " << ERR_error_string(ERR_get_error(), nullptr) << std::endl; exit(EXIT_FAILURE); } SSL_CTX_set_options(ssl_ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); if (SSL_CTX_set_cipher_list(ssl_ctx, ssl::DEFAULT_CIPHER_LIST) == 0) { std::cerr << "SSL_CTX_set_cipher_list failed: " << ERR_error_string(ERR_get_error(), nullptr) << std::endl; exit(EXIT_FAILURE); } SSL_CTX_set_next_proto_select_cb(ssl_ctx, client_select_next_proto_cb, nullptr); #if OPENSSL_VERSION_NUMBER >= 0x10002000L auto proto_list = util::get_default_alpn(); #ifdef HAVE_SPDYLAY static const char spdy_proto_list[] = "\x8spdy/3.1\x6spdy/3\x6spdy/2"; std::copy_n(spdy_proto_list, sizeof(spdy_proto_list) - 1, std::back_inserter(proto_list)); #endif // HAVE_SPDYLAY SSL_CTX_set_alpn_protos(ssl_ctx, proto_list.data(), proto_list.size()); #endif // OPENSSL_VERSION_NUMBER >= 0x10002000L std::vector reqlines; if (config.ifile.empty()) { std::vector uris; std::copy(&argv[optind], &argv[argc], std::back_inserter(uris)); reqlines = parse_uris(std::begin(uris), std::end(uris)); } else { std::vector uris; if (config.ifile == "-") { uris = read_uri_from_file(std::cin); } else { std::ifstream infile(config.ifile); if (!infile) { std::cerr << "cannot read input file: " << config.ifile << std::endl; exit(EXIT_FAILURE); } uris = read_uri_from_file(infile); } reqlines = parse_uris(std::begin(uris), std::end(uris)); } if (config.max_concurrent_streams == -1) { config.max_concurrent_streams = reqlines.size(); } Headers shared_nva; shared_nva.emplace_back(":scheme", config.scheme); if (config.port != config.default_port) { shared_nva.emplace_back(":authority", config.host + ":" + util::utos(config.port)); } else { shared_nva.emplace_back(":authority", config.host); } shared_nva.emplace_back(":method", "GET"); // list overridalbe headers auto override_hdrs = make_array(":authority", ":host", ":method", ":scheme"); for (auto &kv : config.custom_headers) { if (std::find(std::begin(override_hdrs), std::end(override_hdrs), kv.name) != std::end(override_hdrs)) { // override header for (auto &nv : shared_nva) { if ((nv.name == ":authority" && kv.name == ":host") || (nv.name == kv.name)) { nv.value = kv.value; } } } else { // add additional headers shared_nva.push_back(kv); } } for (auto &req : reqlines) { // For nghttp2 std::vector nva; nva.push_back(http2::make_nv_ls(":path", req)); for (auto &nv : shared_nva) { nva.push_back(http2::make_nv(nv.name, nv.value, false)); } config.nva.push_back(std::move(nva)); // For spdylay std::vector cva; cva.push_back(":path"); cva.push_back(req.c_str()); for (auto &nv : shared_nva) { if (nv.name == ":authority") { cva.push_back(":host"); } else { cva.push_back(nv.name.c_str()); } cva.push_back(nv.value.c_str()); } cva.push_back(":version"); cva.push_back("HTTP/1.1"); cva.push_back(nullptr); config.nv.push_back(std::move(cva)); } resolve_host(); if (config.nclients == 1) { config.nthreads = 1; } size_t nreqs_per_thread = config.nreqs / config.nthreads; ssize_t nreqs_rem = config.nreqs % config.nthreads; size_t nclients_per_thread = config.nclients / config.nthreads; ssize_t nclients_rem = config.nclients % config.nthreads; std::cout << "starting benchmark..." << std::endl; auto start = std::chrono::steady_clock::now(); std::vector> workers; workers.reserve(config.nthreads - 1); #ifndef NOTHREADS std::vector> futures; for (size_t i = 0; i < config.nthreads - 1; ++i) { auto nreqs = nreqs_per_thread + (nreqs_rem-- > 0); auto nclients = nclients_per_thread + (nclients_rem-- > 0); std::cout << "spawning thread #" << i << ": " << nclients << " concurrent clients, " << nreqs << " total requests" << std::endl; workers.push_back( make_unique(i, ssl_ctx, nreqs, nclients, &config)); auto &worker = workers.back(); futures.push_back( std::async(std::launch::async, [&worker]() { worker->run(); })); } #endif // NOTHREADS auto nreqs_last = nreqs_per_thread + (nreqs_rem-- > 0); auto nclients_last = nclients_per_thread + (nclients_rem-- > 0); std::cout << "spawning thread #" << (config.nthreads - 1) << ": " << nclients_last << " concurrent clients, " << nreqs_last << " total requests" << std::endl; workers.push_back(make_unique(config.nthreads - 1, ssl_ctx, nreqs_last, nclients_last, &config)); workers.back()->run(); #ifndef NOTHREADS for (auto &fut : futures) { fut.get(); } #endif // NOTHREADS auto end = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(end - start); Stats stats(0); for (const auto &w : workers) { const auto &s = w->stats; stats.req_todo += s.req_todo; stats.req_started += s.req_started; stats.req_done += s.req_done; stats.req_success += s.req_success; stats.req_status_success += s.req_status_success; stats.req_failed += s.req_failed; stats.req_error += s.req_error; stats.bytes_total += s.bytes_total; stats.bytes_head += s.bytes_head; stats.bytes_body += s.bytes_body; for (size_t i = 0; i < stats.status.size(); ++i) { stats.status[i] += s.status[i]; } } auto time_stats = process_time_stats(workers); // Requests which have not been issued due to connection errors, are // counted towards req_failed and req_error. auto req_not_issued = stats.req_todo - stats.req_status_success - stats.req_failed; stats.req_failed += req_not_issued; stats.req_error += req_not_issued; // UI is heavily inspired by weighttp[1] and wrk[2] // // [1] https://github.com/lighttpd/weighttp // [2] https://github.com/wg/wrk size_t rps = 0; int64_t bps = 0; if (duration.count() > 0) { auto secd = static_cast(duration.count()) / (1000 * 1000); rps = stats.req_success / secd; bps = stats.bytes_total / secd; } std::cout << R"( finished in )" << util::format_duration(duration) << ", " << rps << " req/s, " << util::utos_with_funit(bps) << R"(B/s requests: )" << stats.req_todo << " total, " << stats.req_started << " started, " << stats.req_done << " done, " << stats.req_status_success << " succeeded, " << stats.req_failed << " failed, " << stats.req_error << R"( errored status codes: )" << stats.status[2] << " 2xx, " << stats.status[3] << " 3xx, " << stats.status[4] << " 4xx, " << stats.status[5] << R"( 5xx traffic: )" << stats.bytes_total << " bytes total, " << stats.bytes_head << " bytes headers, " << stats.bytes_body << R"( bytes data min max mean sd +/- sd time for request: )" << std::setw(10) << util::format_duration(time_stats.time_min) << " " << std::setw(10) << util::format_duration(time_stats.time_max) << " " << std::setw(10) << util::format_duration(time_stats.time_mean) << " " << std::setw(10) << util::format_duration(time_stats.time_sd) << std::setw(9) << util::dtos(time_stats.within_sd) << "%" << std::endl; SSL_CTX_free(ssl_ctx); return 0; } } // namespace h2load int main(int argc, char **argv) { return h2load::main(argc, argv); }