Skip site navigation (1)Skip section navigation (2)
Date:      Sun, 12 Nov 95 0:46:13 DNT
From:      kim@dde.dk (Kim Andersen)
To:        jkh@time.cdrom.com (Jordan K. Hubbard)
Cc:        hackers@FreeBSD.org
Subject:   Re: Anyone else think it's about time to beat a WEB server to death?
Message-ID:  <9511120046.AA26742@nessie.dde.dk>
In-Reply-To: <8352.816054219@time.cdrom.com>; from "Jordan K. Hubbard" at Nov 10, 95 5:43 pm

next in thread | previous in thread | raw e-mail | index | archive | help
> 
> > All people interested in the setup of the machines, etc, can join
> > w3test@ibenet.it by sending e-mail to me <piero@ibenet.it>
> > (no automatic daemon, sorry).
> 
> I have also set up a webtest@freebsd.org mailing alias for those folks
> actually participating in setting up this test.  So far, it's myself,
> Piero, Jaye and Brad.  Any suggestions concerning how to run the tests
> or features that people are particularly interested in having tested
> should be sent to this alias.  Thanks!

For local testing of a webservers "response" the following might be of interest:

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <netdb.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <string.h>

/* Webmonkey.c --- when you want to know how your server responds to a
 * hundred monkeys all pounding on it.  Transactions are replayed out of
 * a common-log-format log file.  (We replay GETs only; sufficient data
 * is not logged to properly replay PUTs.  Also, HTTP authentication isn't
 * done right, so sites which rely on it heavily will mainly see how fast
 * their server rejects unauthorized access).
 */

/* Defaults for whose back the monkey climbs ... and how hard it works
 */ 

char *default_host = "www";
int default_port = 80;

int max_trans = 25;		/* Maximum number of transactions at once */
int usec_per = 200000;		/* Five per second for starters */

/* Mime headers sent on Monkey requests, starting with the newline which
 * follows the request itself
 */

char mime_headers[] =
"\r\n"
"Accept: text/plain\r\n"
"Accept: application/x-html\r\n"
"Accept: application/html\r\n"
"Accept: text/x-html\r\n"
"Accept: text/html\r\n"
"Accept: application/x-hdf\r\n"
"Accept: application/x-netcdf\r\n"
"Accept: application/hdf\r\n"
"Accept: application/netcdf\r\n"
"Accept: audio/basic\r\n"
"Accept: audio/x-aiff\r\n"
"Accept: image/gif\r\n"
"Accept: image/jpeg\r\n"
"Accept: image/tiff\r\n"
"Accept: image/x-portable-anymap\r\n"
"Accept: image/x-portable-bitmap\r\n"
"Accept: image/x-portable-graymap\r\n"
"Accept: image/x-portable-pixmap\r\n"
"Accept: image/x-rgb\r\n"
"Accept: image/rgb\r\n"
"Accept: image/x-xbitmap\r\n"
"Accept: image/x-xpixmap\r\n"
"Accept: image/xwd\r\n"
"Accept: image/x-xwd\r\n"
"Accept: image/x-xwindowdump\r\n"
"Accept: video/mpeg\r\n"
"Accept: application/postscript\r\n"
"Accept: application/x-dvi\r\n"
"Accept: message/rfc822\r\n"
"Accept: application/x-latex\r\n"
"Accept: application/x-tex\r\n"
"Accept: application/x-texinfo\r\n"
"Accept: application/x-troff\r\n"
"Accept: application/x-troff-man\r\n"
"Accept: application/x-troff-me\r\n"
"Accept: application/x-troff-ms\r\n"
"Accept: text/richtext\r\n"
"Accept: text/tab-separated-values\r\n"
"Accept: text/x-setext\r\n"
"Accept: */*\r\n"
"User-Agent:  WebMonkey 0.02\r\n"
"\r\n"
;

#define MIME_HDR_SIZE (sizeof(mime_headers) - 1)

/* Globals holding status of all current connections */

fd_set read_fds;		/* FDs currently being read from */
fd_set write_fds;		/* FDs currently being sent to, or
				 * with a connection in progress...
				 */
fd_set except_fds;		/* FDs currently in use */

int transactions_in_progress = 0;
int fdmax = 0;

int transactions_done = 0;	/* Statistics... */
int total_msecs = 0;

/* State of sockets, for each possible socket...
 */

struct sock_status
{
  char *request;		/* Kept for as long as request is active */
  struct timeval start_time;	/* For statistics gathering */
  
  char *data;			/* Data *currently* being written */
  int len;			/* Amount to write */
  int len_written;		/* Amount of that done so far */
  int state;			/* State of the socket (see below) */
} sock_status[FD_SETSIZE];

/* Possible socket states:
 * (note that the appropriate bits must *also* be set in the fdsets
 * for anything to work...).
 */

#define CONNECT 0		/* Connecting */
#define WRITE_REQ 1		/* Writing request */
#define WRITE_HDR 2		/* Writing MIME header */
#define READ_RESPONSE 3		/* Reading response */

#define NSTATES 4

char *state_names[NSTATES] =
{
  "connecting",
  "writing request",
  "writing MIME header",
  "reading response",
};

/****************************************************************
 * Time fiddling
 */

void add_usecs (struct timeval *dest, struct timeval *src, int usecs)
{
  dest->tv_sec = src->tv_sec;
  dest->tv_usec = src->tv_usec + usec_per;
  
  if (dest->tv_usec > 1000000)
  {
    dest->tv_usec -= 1000000;
    dest->tv_sec += 1;
  }
}

void sub_timevals (struct timeval *dest,
		   struct timeval *later, struct timeval *earlier)
{
  dest->tv_sec = later->tv_sec - earlier->tv_sec;
  dest->tv_usec = later->tv_usec - earlier->tv_usec;

  if (dest->tv_usec < 0)
  {
    dest->tv_usec += 1000000;
    dest->tv_sec -= 1;
  }

  if (dest->tv_usec < 0 || dest->tv_sec < 0)
  {
    dest->tv_usec = dest->tv_sec = 0;
  }
}

int get_msecs (struct timeval *tm)
{
  return tm->tv_sec * 1000 + tm->tv_usec / 1000;
}

/****************************************************************
 * Routines to manipulate sock_status structures --- set 'em up,
 * tear 'em down.  Note that the request is passed as malloc'ed memory
 * which we "own" once we get it, so it's on us to free it afterward.
 *
 * These routines also take care of a lot of the globals, to have all
 * that stuff in one place.
 */

void
init_sock_status (int s, int state, char *req)
{
  sock_status[s].state = state;
  sock_status[s].request = req;
  sock_status[s].data = req;
  sock_status[s].len = strlen(req);
  sock_status[s].len_written = 0;
  gettimeofday (&sock_status[s].start_time, NULL);
  
  FD_SET (s, &except_fds);	/* Protocol code determines when
				 * writes and reads are appropriate,
				 * but we *always* want to know about
				 * exceptional conditions.
				 */
  
  ++transactions_in_progress;
  if (s > fdmax) fdmax = s;
} 

void
finalize_sock_status (int s)
{
  struct timeval now, interval;
  
  /* Common code in tearing down transactions:
   * No more I/O on this descriptor.
   */
  
  FD_CLR (s, &read_fds);
  FD_CLR (s, &write_fds);
  FD_CLR (s, &except_fds);
  close (s);
  
  /* No more need for request text */
  
  free (sock_status[s].request);

  /* Fiddle other globals and keep statistics */
  
  gettimeofday (&now, 0);
  sub_timevals (&interval, &now, &sock_status[s].start_time);
  
  total_msecs += get_msecs (&interval);
  transactions_done += 1;
  
  --transactions_in_progress;
}

/****************************************************************
 * Set up the globals, and find the address of the host we're
 * going to flog.
 */

void
monkey_init (struct sockaddr_in *sin, char *host, int port)
{
  struct hostent *hostp;
  
  if (!(hostp = gethostbyname(host)))
  {
    fprintf (stderr, "Can't find host %s!\n", host);
    exit (1);
  }

  sin->sin_family = hostp->h_addrtype;
  sin->sin_port = port;
  memcpy ((char*)&sin->sin_addr, hostp->h_addr, hostp->h_length);

  FD_ZERO(&read_fds);
  FD_ZERO(&write_fds);
  FD_ZERO(&except_fds);
}
     

/****************************************************************
 * Make a connection on a socket; set up to continue transaction 
 */

void
monkeystart (char *req, struct sockaddr_in *sin)
{
  int s, rv;
  struct sock_status *t;

  /* Get a socket, and set up to keep statistics on this connection */
  
  if ((s = socket(sin->sin_family, SOCK_STREAM, 0)) < 0)
  {
    perror("webmonkey: socket");
    exit(1);
  }

  init_sock_status (s, CONNECT, req);

  /* Set up to write the request, once the connection is accepted
   * (and thus, the socket shows ready for writing in select()).
   */

  fcntl (s, F_SETFL, O_NDELAY); 
  FD_SET (s, &write_fds);
  
  while ((rv = connect(s, (struct sockaddr *)sin, sizeof(*sin))) < 0
	 && errno == EINTR)
    continue;
  
  if (rv < 0 && errno != EINPROGRESS)
  {
    extern void monkey_except(int);
    monkey_except (s);	/* Handle as other exceptional conditions */
  }
}

/****************************************************************
 * Continue sending an incomplete request or MIME header
 * on a socket clear for writing
 */

void
monkey_write (int s)
{
  struct sock_status *t = &sock_status[s];
  int rv = write (s, t->data + t->len_written, t->len - t->len_written);
  
  if (rv <= 0) return;

  t->len_written += rv;

  if (t->len_written == t->len)
    switch (t->state)
    {
    case CONNECT:
      t->state = WRITE_REQ;
      /* and fall through... */
      
    case WRITE_REQ:
      t->state = WRITE_HDR;	/* Start writing MIME headers */
      t->data = mime_headers;	/* (they're boilerplate) */
      t->len = MIME_HDR_SIZE;
      t->len_written = 0;
      break;
      
    case WRITE_HDR:		
      t->state = READ_RESPONSE;
      FD_CLR (s, &write_fds);	/* Done with (boilerplate) headers */
      FD_SET (s, &read_fds);	/* Read HTTP response */
      break;
      
    default:
      fprintf (stderr,
	       "Weird, weird --- write ready on socket with state %d\n",
	       t->state);
      FD_CLR (s, &write_fds);
      break;
    }
}

/* Continue reading the response to a request to which the server
 * is now replying.
 */

void
monkey_read (int s)
{
  char dummybuf[8192];
  int rv = read (s, dummybuf, 8192);

  if (rv == 0) finalize_sock_status(s);
}

/* Handle exceptions (if they occur) --- just close the damn
 * socket and forget about it.
 */

void
monkey_except(int s)
{
  fprintf (stderr, "Error on \"%s\", while %s\n",
	   sock_status[s].request, state_names[sock_status[s].state]);
  finalize_sock_status(s);
}

/* Do *all* I/O for transactions currently in progress */

void
do_transactions(struct timeval *timeout)
{
  fd_set my_read_fds = read_fds;
  fd_set my_write_fds = write_fds;
  fd_set my_except_fds = except_fds;

  int nfds = select (fdmax + 1, &my_read_fds, &my_write_fds, &my_except_fds,
		     timeout);
  int i;
      
  for (i = 0; i <= fdmax; ++i)
  {
    if (FD_ISSET (i, &my_except_fds)) monkey_except(i);
    else if (FD_ISSET (i, &my_read_fds)) monkey_read(i);
    else if (FD_ISSET (i, &my_write_fds)) monkey_write(i);
  }
}

/* Get a request out of the log file we're replaying
 */

char *
get_request (FILE *log)
{
  static char reqbuffer[1024];
  char *reqp;
  
  while (fgets (reqbuffer, sizeof(reqbuffer), log))
  {
    char *cp = reqbuffer;

    /* Find start of request... */
    
    while (*cp && *cp != '"') ++cp;
    if (!*cp) continue;
    reqp = ++cp;

    /* Find and flag end of request */
    
    while (*cp && *cp != '"') ++cp;
    if (!*cp) continue;
    *cp = '\0';

    /* Sanity check the request... */

    if (!strncmp (reqp, "GET ", 4)) return strdup(reqp);
  }

  return NULL;
}

/* Executive */

int
main (int argc, char **argv)
{
  int port = default_port;
  char *host = default_host;
  FILE *log = stdin;
  int skip = 0;
  
  int last_stats_printout;
  int lost_transactions = 0;

  struct sockaddr_in sin;
  
  struct timeval now;
  struct timeval next_start;
  struct timeval interval;
  
  while (*++argv)
  {
    if (!strcmp (*argv, "-host")) host = *++argv;
    else if (!strcmp (*argv, "-port")) port = atoi(*++argv);
    else if (!strcmp (*argv, "-max")) max_trans = atoi(*++argv);
    else if (!strcmp (*argv, "-usec")) usec_per = atoi(*++argv);
    else if (!strcmp (*argv, "-skip")) skip = atoi (*++argv);
    else if (!strcmp (*argv, "-per")) {
      double atof (char*);
      double per = atof (*++argv);
      usec_per = (int)(1e6 / per);
    }
    else if (!strcmp (*argv, "-log")) {
      log = fopen (*++argv, "r");
      if (log == NULL) { perror(log); exit(1); }
    }
    else {
      fprintf (stderr,
	       "Usage: webmonkey [-host h] [-port p] [-max max_transactions]\n"
	       "       [-per connects_per_second] [-log logname]\n");
      exit (1);
    }
  }
  
  printf ("Webmonkey: Flogging %s at port %d\n", host, port);
  printf ("New transaction every %d microseconds, up to %d\n",
	  usec_per, max_trans);

  while (skip > 0)
  {
    int c = getc(log);
    
    if (c == EOF)
    {
      fprintf (stderr, "Skipped past end of log!\n");
      exit(1);
    }

    if (c == '\n') --skip;
  }

  monkey_init (&sin, host, port);
  signal (SIGPIPE, SIG_IGN);

  last_stats_printout = 1;
  gettimeofday (&now, 0);
  add_usecs (&next_start, &now, usec_per);

  while (1)
  {
    if ((transactions_done + lost_transactions) > last_stats_printout
	&& (transactions_done + lost_transactions) % 100 == 0)
    {
      if (lost_transactions == 0)
	printf ("Done %d transactions; average response %d msecs.\n",
		transactions_done,
		total_msecs / transactions_done);
      else
	printf ("Done %d transactions (missed %d); average response %d msecs.\n",
		transactions_done, lost_transactions,
		total_msecs / transactions_done);

      last_stats_printout = transactions_done + lost_transactions;
    }
    
    gettimeofday (&now, 0);

    if (now.tv_sec > next_start.tv_sec
	|| (now.tv_sec == next_start.tv_sec
	    && now.tv_usec >= next_start.tv_usec))
    {
      /* Time to fire off a new request */
      
      char *request = get_request(log);
      
      if (!request) exit(0);
      
      add_usecs (&next_start, &next_start, usec_per);

      if (transactions_in_progress > max_trans)
	++lost_transactions;
      else
	monkeystart (request, &sin);
    }

    /* Do I/O, or pause, until it's time to fire off the next one. */
    
    sub_timevals (&interval, &next_start, &now);
    do_transactions (&interval);
  }
}



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?9511120046.AA26742>