/*
* thrulay.c -- network throughput tester (the client part).
*
* Written by Stanislav Shalunov, http://www.internet2.edu/~shalunov/
* Huadong Liu, http://www.cs.utk.edu/~hliu/
*
* Copyright 2003, Internet2.
* Legal conditions are in file LICENSE
* (MD5 = ecfa50d1b0bfbb81b658c810d0476a52).
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include "thrulay.h"
#include "util.h"
#include "rcs.h"
RCS_ID("@(#) $Id: thrulay.c,v 1.17 2005/08/25 15:52:34 hliu Exp $")
/*
* Thrulay server use one day as infinite delay. If a delay value equals to
* DELAY_INF * 1000.0 ms, the client should consider it as infinity.
*/
#define DELAY_INF 24 * 60 * 60
/*
* Print out usage of thrulay client.
*/
static void
usage(void)
{
fprintf(stderr, "Usage: thrulay [-t#] [-i#] [-w#] [-l#] [-p#] [-u#]"
"[-by/n] [-S#] [-D#] host\n");
fprintf(stderr, "-t#\t\ttest duration, in seconds (default: 60s)\n");
fprintf(stderr, "-i#\t\treporting interval, in seconds (default: 1s)\n");
fprintf(stderr, "-w#\t\twindow, in bytes (default: 4194304B)\n");
fprintf(stderr, "-l#\t\tblock size (default: 8192B)\n");
fprintf(stderr, "-p#\t\tserver port (default: 5003)\n");
fprintf(stderr, "-n#\t\tnumber of parallel streams (default: 1)\n");
fprintf(stderr, "-u#[kMGT]\tUDP mode with given rate (default: off)\n");
fprintf(stderr, "\t\tIn UDP mode, rate is in bits per second and can be\n"
"\t\tfollowed by a SI suffix (K/k for 1000, M/m for 100000);\n"
"\t\tdefault packet size, 1500, can be changed with -l;\n"
"\t\twindow size becomes the UDP send buffer size;\n"
"\t\treporting interval is ignored.\n");
fprintf(stderr, "-b[y/n]\t\tbusy wait or not when sending UDP packets"
"(default: y)\n");
fprintf(stderr, "-S\t\tTOS(type-of-service) for outgoing packets. You may\n"
"\t\tspecify the value in hex with a '0x' prefix, in octal with\n"
"\t\ta '0' prefix, or in decimal. Valid values are:\n"
"\t\t0x10(IPTOS_LOWDELAY) for minimize delay;\n"
"\t\t0x08(IPTOS_THROUGHPUT) for maximize throughput;\n"
"\t\t0x04(IPTOS_RELIABILITY) for maximize reliability;\n"
"\t\t0x02(IPTOS_LOWCOST) for minimize cost.\n");
fprintf(stderr, "-D\t\tDSCP values for TOS bytes,"
"mutual exclusive with \"-S\"\n");
fprintf(stderr, "host\t\tserver to send test data to (no default)\n");
exit(1);
}
/*
* Convert a string like "12k" to a number like 12000.
* Return zero on error.
*/
u_int64_t
rate2i(char *s)
{
u_int64_t r; /* Result. */
char *p;
int l;
int suffix = 0;
/* First, set the multiple. */
l = strlen(s);
switch (s[l-1]) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
r = 1;
break;
case 'k':
case 'K':
r = 1000;
suffix = 1;
break;
case 'm':
case 'M':
r = 1000000;
suffix = 1;
break;
case 'g':
case 'G':
r = 1000000000ULL;
suffix = 1;
break;
case 't':
case 'T':
r = 1000000000000ULL;
suffix = 1;
break;
default:
r = 0;
}
if (suffix)
s[l-1] = '\0';
r *= strtoll(s, &p, 10);
if (!*s || *p) /* Invalid character found. */
r = 0;
return r;
}
/*
* Parse thrulay client settings. The caller should cast the return value
* to either (thrulay_setting_tcp_t *) or (thrulay_setting_udp_t *) after
* determining the value in traffic.
*/
static void *
get_settings(int argc, char *argv[], thrulay_traffic_type_t *traffic)
{
char *server_name;
int server_port = 5003;
/* How long to run, default is 60 seconds. */
int test_duration = 60;
/* How often to report, default is 1 second. */
int report_interval = 1;
/* Window size for TCP, send buffer size for UDP, in bytes. */
int buffer_size = 4194304;
/* Packet size for both TCP and UDP, in bytes. */
int block_size = 0;
/* UDP packet send rate in b/s, zero in TCP test. */
u_int64_t rate = 0;
/* busywait flag in UDP test, default is busywait. */
thrulay_wait_type_t wt = THRULAY_BUSYWAIT;
/* Type-of-service for outgoing packets for both TCP and UDP tests. */
int tos = 0;
/* Number of parallel test streams, default is 1. */
int num_stream = 1;
thrulay_setting_tcp_t *opt_tcp;
thrulay_setting_udp_t *opt_udp;
void *options;
int ch;
while ((ch = getopt(argc, argv, "t:i:w:l:p:u:b:n:S:D:")) != -1)
switch (ch) {
case 't':
test_duration = atoi(optarg);
if (test_duration <= 0) {
fprintf(stderr, "test duration must be "
"a positive integer (in seconds)\n");
usage();
}
break;
case 'i':
report_interval = atoi(optarg);
if (report_interval <= 0) {
fprintf(stderr, "reporting interval must be "
"a positive integer (in seconds)\n");
usage();
}
break;
case 'w':
buffer_size = atoi(optarg);
if (buffer_size <= 0) {
fprintf(stderr, "window must be "
"a positive integer (in bytes)\n");
usage();
}
break;
case 'l':
block_size = atoi(optarg);
if (block_size <= 0) {
fprintf(stderr, "block size must be "
"a positive integer (in bytes)\n");
usage();
}
break;
case 'p':
server_port = atoi(optarg);
if (server_port <= 0) {
fprintf(stderr, "port must be "
"a positive integer\n");
usage();
}
break;
case 'u':
rate = rate2i(optarg);
if (rate == 0) {
fprintf(stderr, "rate must be positive\n");
usage();
}
break;
case 'b':
if (optarg[0] == 'n')
wt = THRULAY_NONBUSYWAIT;
break;
case 'n':
num_stream = atoi(optarg);
if (num_stream < 1) {
fprintf(stderr, "number of streams must be positive\n");
usage();
}
break;
case 'S':
if (tos) {
fprintf(stderr, "Invalid option '-S'. "
"Can only set one '-D' or '-S'\n");
usage();
}
tos = strtol(optarg, NULL, 0);
if ((tos != 0x10)&&(tos != 0x08)&&(tos != 0x04)&&(tos != 0x02)) {
fprintf(stderr, "Invalid IP_TOS value\n");
}
break;
case 'D':
if (tos) {
fprintf(stderr, "Invalid option '-D'. "
"Can only set one '-D' or '-S'.\n");
}
tos = strtol(optarg, NULL, 0);
if (tos & ~0x3F) {
fprintf(stderr, "Invalid value for option \'-D\'. "
"DSCP value expected");
}
tos &= 0x3F;
tos <<= 2; /* shift for setting TOS */
break;
default:
usage();
}
argc -= optind;
argv += optind;
if (argc != 1)
usage();
server_name = argv[0];
/* default packet size is 8192 for TCP and 1500 for UDP */
block_size = (!block_size && rate) ? 1500 : 8192;
if (rate) {
*traffic = THRULAY_UDP;
opt_udp = calloc(1, sizeof(thrulay_setting_udp_t));
if (!opt_udp) {
error(ERR_FATAL, "could not allocate memory");
}
opt_udp->server_name = server_name;
opt_udp->server_port = server_port;
opt_udp->test_duration = test_duration;
opt_udp->udp_buffer_size = buffer_size;
opt_udp->packet_size = block_size;
opt_udp->nstream = num_stream;
opt_udp->tos = tos;
opt_udp->rate = rate;
opt_udp->wt = wt;
options = (void *)opt_udp;
}
else {
/* make sure report_interval <= test_duration */
if (report_interval > test_duration) {
fprintf(stderr, "Report interval cannot be greater than"
"test duration\n");
usage();
}
*traffic = THRULAY_TCP;
opt_tcp = calloc(1, sizeof(thrulay_setting_tcp_t));
if (!opt_tcp) {
error(ERR_FATAL, "could not allocate memory");
}
opt_tcp->server_name = server_name;
opt_tcp->server_port = server_port;
opt_tcp->test_duration = test_duration;
opt_tcp->report_interval = report_interval;
opt_tcp->window_size = buffer_size;
opt_tcp->block_size = block_size;
opt_tcp->nstream = num_stream;
opt_tcp->tos = tos;
options = (void *)opt_tcp;
}
return options;
}
/*
* We assume mss is between optimistic position and moderate position, i.e.
* MSS = MTU - 20(TCPHDR) - 20(IPHDR) = MTU - 40, and
* MSS = MTU - 20(TCPHDR) - 60(IPHDR) = MTU - 80
* as defined in RFC 879.
* This also works for IPv6 since there is no option filed.
*/
#define check_mss_mtu(mss, mtu) (mtu-40) >= mss && mss >= (mtu-80)
static void
print_mss_mtu(int mss, int mtu)
{
if (mtu && mss) {
printf("# Path MTU = %dB, MSS = %dB\n", mtu, mss);
}
else if (mtu && !mss) {
printf("# Path MTU = %dB, Unknown MSS\n", mtu);
}
else if (!mtu && mss) {
if (check_mss_mtu(mss, 576)) {
printf("# Path MTU = 576B, MSS = %dB, matchs Minimum\n", mss);
}
else if (check_mss_mtu(mss, 1500)) {
printf("# Path MTU = 1500B, MSS = %dB, matchs Ethernet\n", mss);
}
else if (check_mss_mtu(mss, 4352)) {
printf("# Path MTU = 4352B, MSS = %dB, matchs FDDI\n", mss);
}
else if (check_mss_mtu(mss, 9180)) {
printf("# Path MTU = 9180B, MSS = %dB, matchs ATM\n", mss);
}
else {
printf("# Path MTU = %dB, MSS = %dB, Unknown interface\n",
mss+40, mss);
}
}
else {
printf("# Unknown path MTU and MSS\n");
}
return;
}
/*
* Print header of TCP report.
*/
static void
print_tcp_report_header(int tid, thrulay_setting_tcp_t *settings)
{
thrulay_setup_tcp_t setup;
/*
* Get setup, mss and mtu of stream 0. Here we assume every stream
* have the same setup except fds.
*/
if (0 != thrulay_get_setup(tid, 0, &setup)) {
error(ERR_FATAL, "could not get thrulay setup");
}
printf("# local window = %dB; remote window = %dB\n",
setup.window_size_local, setup.window_size_remote);
if (settings->block_size == setup.block_size) {
printf("# block size = %dB\n", setup.block_size);
}
else {
printf("# requested block size = %dB; actual block size = %dB\n",
settings->block_size, setup.block_size);
}
print_mss_mtu(setup.mss, setup.mtu);
printf("# test duration = %ds; reporting interval = %ds\n",
settings->test_duration, settings->report_interval);
printf("SID begin,s end,s Mb/s RTT,ms: min avg max\n");
fflush(stdout);
return;
}
/*
* Print header of UDP report.
*/
static void
print_udp_report_header(int tid, thrulay_setting_udp_t *option)
{
int i;
thrulay_setup_udp_t setup;
/*
* Although UDP buffer size is the same, each stream has a different
* server UDP port number.
*/
for (i=0; i<option->nstream; i++) {
if (0 != thrulay_get_setup(tid, i, &setup)) {
error(ERR_FATAL, "could not get thrulay setup");
}
if (i == 0) {
printf("# client buffer size = %dB\n", setup.udp_buffer_size);
if (option->packet_size == setup.packet_size) {
printf("# packet size = %dB\n", setup.packet_size);
}
else {
printf("# requested packet size = %dB; "
"actual packet size = %dB\n",
option->packet_size, setup.packet_size);
}
}
printf("# (%d) remote UDP port = %d\n", i, setup.port_udp_server);
}
return;
}
static void
print_report_udp(thrulay_report_udp_t *rpt)
{
int i;
double loss;
u_int64_t npacket_loss;
if (rpt->udp_buffer_size > 0) {
printf("Server UDP buffer size: %dB\n", rpt->udp_buffer_size);
}
else {
printf("Server UDP buffer size unknown\n");
}
printf("Packets client proposed to send: %llu\n", rpt->npackets);
printf("Packets client sent: %llu\n", rpt->packets_sent);
if (rpt->udp_buffer_size > 0) {
printf("Packets server received: %llu\n", rpt->packets_received);
printf("Packets duplicated: %llu\n", rpt->packets_duplicate);
/* an delay of DELAY_INF*1000.0ms indicates that the packet is lost */
if (rpt->min_delay != DELAY_INF * 1000.0) {
printf("0th quantile of delay (ignoring clock offset): %.3lfms\n",
rpt->min_delay);
}
else {
printf("0th quantile of delay (ignoring clock offset): infms\n");
}
if (rpt->median_delay != DELAY_INF * 1000.0) {
printf("50th quantile of delay (ignoring clock offset): %.3lfms\n",
rpt->median_delay);
}
else {
printf("50th quantile of delay (ignoring clock offset): infms\n");
}
if (rpt->quantile95_delay != DELAY_INF * 1000.0) {
printf("95th quantile of delay (ignoring clock offset): %.3lfms\n",
rpt->quantile95_delay);
}
else {
printf("95th quantile of delay (ignoring clock offset): infms\n");
}
npacket_loss = rpt->packets_sent - rpt->packets_received +
rpt->packets_duplicate;
if (rpt->packets_sent > rpt->packets_received-rpt->packets_duplicate) {
loss = 100.0*(double)(npacket_loss)/(double)(rpt->packets_sent);
}
else {
loss = 0;
}
printf("Total packet loss: %llu\n", npacket_loss);
printf("Total loss period: %llu\n", rpt->nloss_periods);
printf("Packet loss rate: %lf%%\n", loss);
for (i = 0; i < MAX_N && rpt->nreorder[i]; i++) {
printf("%d-reordering = %f%%\n", i+1,
100.0*rpt->nreorder[i]/(rpt->packets_received-i-1));
}
if (i == 0) printf("no reordering\n");
else if (i < MAX_N) printf("no %d-reordering\n", i+1);
else printf("only up to %d-reordering is handled\n", MAX_N);
}
else {
printf("Packets server received unknown\n");
printf("Packets duplicated unknown\n");
printf("0th quantile of delay (ignoring clock offset) unknown\n");
printf("50th quantile of delay (ignoring clock offset) unknown\n");
printf("95th quantile of delay unknown\n");
printf("Packet loss rate unknown\n");
printf("n-reording metric unknown\n");
}
printf("\n");
return;
}
/*
* Print reports starting from from_idx to to_idx.
*/
static void
print_reports(thrulay_traffic_type_t traffic, int tid, int nstream,
int from_idx, int to_idx)
{
char ch;
int i, j;
thrulay_report_tcp_t rpt_tcp;
thrulay_report_udp_t rpt_udp;
for (i=from_idx; i<=to_idx; i++) {
for (j=0; j<nstream; j++) {
if (traffic == THRULAY_TCP) {
thrulay_get_report(tid, j, i, &rpt_tcp);
/* Print a # for final reports. */
ch = (rpt_tcp.type == THRULAY_FINAL) ? '#' : ' ';
printf("(%d)%c%9.3f %8.3f %8.3f %8.3f %8.3f %8.3f\n", j,
ch, rpt_tcp.begin, rpt_tcp.end, rpt_tcp.bandwidth,
rpt_tcp.min_rtt, rpt_tcp.avg_rtt, rpt_tcp.max_rtt);
}
else {
thrulay_get_report(tid, j, i, &rpt_udp);
print_report_udp(&rpt_udp);
}
}
}
fflush(stdout);
return;
}
/*
* Progressively print TCP reports.
*/
static int
progressive_report(int tid, int nstream, int interval)
{
int rc;
int status;
int printed = 0; /* Number of reports that have been printed. */
sleep(interval); /* first interval. */
do {
/* check for available test reports */
rc = thrulay_wait(tid, &status, THRULAY_POLL);
if (rc < 0) {
return rc;
}
else if (rc == 0) {
/* if we get here a bit earlier, wait for 1000 useconds. */
if (status == 0) {
usleep(1000);
continue;
}
/* when rc==0, status cannot be less than 0. */
else {
print_reports(THRULAY_TCP, tid, nstream, printed+1, status);
printed = status;
}
sleep(interval);
}
else {
/* test process exits successfully. */
if (status > 0) {
print_reports(THRULAY_TCP, tid, nstream, printed+1, status);
}
/* test process exits with error. */
else if (status < 0) {
return status;
}
else {
/* do nothing here. */
}
}
} while (rc != tid);
return 0;
}
int
main(int argc, char *argv[])
{
int tid; /* unique test id */
thrulay_traffic_type_t traffic; /* TCP or UDP test */
thrulay_setting_tcp_t *setting_tcp=0; /* settings for TCP test */
thrulay_setting_udp_t *setting_udp=0; /* settings for UDP test */
void *settings; /* user settings for thrulay test */
int nstream; /* number of parallel tests */
int status;
int rc;
/* Ignore SIGPIPE. Always do in any socket code. */
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
perror("signal(SIGPIPE, SIG_IGN)");
error(ERR_FATAL, "could not ignore SIGPIPE");
}
rc = thrulay_init();
if (rc < 0) {
thrulay_err_msg(rc);
error(ERR_FATAL, "could not initialize thrulay lib");
}
settings = get_settings(argc, argv, &traffic);
tid = thrulay_open(traffic, settings);
if (tid < 1) { /* tid starts from 1, not 0. */
thrulay_err_msg(tid);
error(ERR_FATAL, "open a thrulay test failed");
}
/* Print out test report header before any statistics. */
if (traffic == THRULAY_TCP) {
setting_tcp = (thrulay_setting_tcp_t *)settings;
nstream = setting_tcp->nstream;
print_tcp_report_header(tid, setting_tcp);
}
else {
setting_udp = (thrulay_setting_udp_t *)settings;
nstream = setting_udp->nstream;
print_udp_report_header(tid, setting_udp);
}
if (tid != thrulay_start(tid)) {
thrulay_err_msg(tid);
error(ERR_FATAL, "start a thrulay test failed");
}
/* for TCP tests, report statistics at a given interval. */
if (traffic == THRULAY_TCP) {
rc = progressive_report(tid, nstream, setting_tcp->report_interval);
if (rc != 0) thrulay_err_msg(rc);
}
/* for UDP tests, report statistics when tests are done. */
else {
if (tid != thrulay_wait(tid, &status, THRULAY_WAIT)) {
error(ERR_FATAL, "wait for a thrulay test failed");
}
if (status > 0) {
print_reports(THRULAY_UDP, tid, nstream, 1, status);
}
else {
thrulay_err_msg(status);
error(ERR_FATAL, "no report produced");
}
}
/*
* We do not have to call thrulay_close before exiting because
* thrulay_exit will do this for us if the test is not closed.
*/
if (0 != thrulay_close(tid)) {
error(ERR_FATAL, "cannot close a thrulay test");
}
thrulay_exit();
/* Since we are exiting, we do not bother to free up memory allocated. */
return 0;
}