Qck is HTTP test request/response generator with ability to act as both HTTP client and server, useful when testing behaviour of proxy servers and load balancers. Qck is written in Node.js with aim to be used in a dev/testing environment. When contents of file are being sent as HTTP body, crude transmit rate controls are in place allowing one to simulate HTTP Layer 7 data rate speeds of a few Bytes per second to MBytes per second, or test behaviour of long lasting connections. Ability to be HTTP Client and Server in one app enables qck to match requests to responses and correlate validation.
Single configuration file is used to pass all settings so saving the test setup could be as simple as saving one config file.
qck.js <configuration file>
Example:
qck.js sample_qck.config
Qck will read configuration file specified and do several things.
- Look for
serverresponse
objects and start a HTTP server to respond to requests received matching configured method and pathname. Listener IP and port are configured under __globalconfig__ object. - Use
clientrequest
objects to setup tests available to be run. Tests are HTTP requests sent to server and serverPort under __globalconfig__ .
Running tests, that is generating HTTP requests, is independent of listener HTTP server side of the application. One could run tests towards external server, and at the same time have the listener respond to requests generated by external clients.
Using sample configuration documented further in this document, file output from '--list' interactive mode option is:
Tests available to run:
test1
test2
test3
test4
Listener setup:
Defined under test: test1 Method::Path = PUT::/
Defined under test: test3 Method::Path = PUT::/test3
Defined under test: test4 Method::Path = GET::/test4
To run test1 type test1 and press enter. Sample output:
test1
Test: test1 started Sun Feb 22 2015 17:55:07 GMT-0700 (MST)
Test: test1, Status: Client check Passed , Server check Passed Sun Feb 22 2015 17:55:37 GMT-0700 (MST)
To run multiple tests specify them as a comma separated list. Each request sent is tagged with unique id in form of custom HTTP header 'x-qck-id' whose value is random generated. This is used to match request sent to one received by listener and match serverresponse
to particular test run instance of clientrequest
.
test1,test2,test2
Test: test1 started Sun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2 started Sun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2 started Sun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2, Status: Client check PassedSun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test2, Status: Client check PassedSun Feb 22 2015 18:00:34 GMT-0700 (MST)
Test: test1, Status: Client check PassedSun Feb 22 2015 18:00:44 GMT-0700 (MST) , Server check Passed Sun Feb 22 2015 18:00:44 GMT-0700 (MST)
###Interactive mode If 'runtests' option is set in configuration file, qck will run tests listed, and exit upon completion. If no 'runtests' option is set in configuration file, qck enters interactive mode where you have these options available:
Options
--file=<filename> Set file to log output to
--nofile Disables file logging, if file is being logged to it is closed.
--log=[0-5] Set log level
--list Lists available tests to run, and Listener method::path
--exit Exits qck
<testname>[,<testname>] Comma separated list of tests to run. All tests are
ran simultaneously (in parallel), or if using
interactive mode at time of command execution.
Logging applies to messages that report test results, and data related to HTTP requests and responses. App errors are always outputted to console (stdout).
--file
or filename under __globalconfig__ in configuration file specify where test related messages are written to. If this file already exists, it is overwritten.
Log Levels
0 - All messages suppressed
1 - Outputs test result (one per line),
2 - Level 1 messages + servercheck result for requests that do not have qck-ID
header (not generated by qck or header was stripped in transit). These are
reported as 'Unknown Client'.
3 - Level 2 messages + Listener side bad requests received, requests for
pathname and method not defined in cfg file under "serverresponse" object.
4 - Level 2 messages + request and response HTTP objects with basic attributes
(url, method, statusCode, headers, body)
5 - Level 2 messages + full Node.js HTTP request and response objects
Configuration file is in JSON format. Currently there is no validation checking of this file, other then JSON.parse() exception being caught if present.
####Global App settings
__globalconfig__
object is used for global application settings. It can have these options:
listenerIp
- IP address to bind the HTTP server listener to, default is127.0.0.1
listenerPort
- TCP port to bind HTTP server listener to, default is6589
server
- Hostname, domain name, or IP address where to send HTTP requests, default is127.0.0.1
serverPort
- TCP port of server, default is6589
loglevel
- Log level to start with, default is1
outputfile
- File to log test data.runtests
- Comma separated list of tests to execute, disables qck interactive mode.
####Test settings
Configuring test options consists of clientrequest
, clientcheck
, serverresponse
, and servercheck
objects. If configuration file contains multiple tests with the same name, the last one read in will be used. Same goes for case where there are multiple tests, with different names, yet have the same serverresponse
pathname and method. Last serverresponse object read in will be used for Listener side setups.
#####clientrequest
Clientrequest object is used to configure parameters for HTTP request to be sent when test is ran. Options specified here are passed to Node.js http.new.request method. See Node.js HTTP ClientRequest for valid options. Notable ones are:
If no hostname and port option is given here, server and serverPort from __globalconfig__
will be applied.
In addition, following qck only options are supported:
-
filename
- file to read data from and send as part of HTTP body -
transrate
- transmission rate in bytes per second to send HTTP body data at -
transfreq
- frequency in milliseconds at which to send packets when transrate is used"test2": { "clientrequest": { "pathname": "/test2", "method": "POST", "filename": "file.sample", "transrate": "300000", "transfreq": "25" }
If you specify a file whose contents are to be sent, using filename
option, file is sent as multipart binary data in HTTP body, with boundary markers. boundaryKey is a random generated 16 digit number. Header added to HTTP request or response:
Content-Type :'multipart/form-data; boundary="+boundaryKey+"'
Written to HTTP body:
'--'+boundaryKey+'\r\n'
'Content-Type: application/octet-stream\r\n'
'Content-Disposition: form-data; name="filename"; filename="filename"\r\n'
'Content-Transfer-Encoding: binary\r\n\r\n'
<contents of file as raw binary data>
'\r\n--'+boundaryKey+'--'
By default (transrate
and transfreq
options not specified) contents of file are transmitted as fast as possible. Underlying Node.js fs.readStream is piped to HTTP request or response writeStream.
When using filename, transrate and transfreq are there to control transmission rate, and both options must be specified. Data throttling is done on Layer7 and does not account for any network stack layer 2-6 data present in each packet, such as Layer2, IP, TCP headers.
transrate
is used to specify transmission rate in Bytes per Second. Minimum value is 1, max is ... more then what (max_chunk[Bytes] * transfreq / 1000[ms]) can support.max_chunk
is 65536.transfreq
is used to specify at what interval should data be written to the network stack (for Node.js this is stream). This interval is in milliseconds, with minimum value of 1, max is not limited.
Using these two parameters amount of data (chunk) needed to be transmitted each transfreq period of time to achieve transrate is calculated and written to HTTP request or response Node.js writableStream. Note that max of this chunk size is 65536 bytes. Setting transfreq to 1ms does not produce good results due everything node.js stack has to do in such a small period of time, if configured server side (listener) processes all the data sent by requesting. Good rule of thumb, set transfreq to as high a value as possible to achieve desired transmission rate.
Setting transrate and transfreq examples:
Setting "transfreq" to 20, and "transrate" to 70000000 will result in qck writing 65536 Bytes (in Node.js to writableStream) of data every 20ms producing an approximate transmission rate of 3276800 Bytes per second, or approximately 26mbits per second. To achieve desired transrate at 20ms frequency, chunk size would have to be 1400000 Bytes in size. This is more then coded max of 65536 and is automatically throttled to this max setting.
Setting "transfreq" to 30, and "transrate" to 8000 will result in qck writing 240 Bytes of data every 30ms producing an approximate transmission rate of 8000 Bytes per second, or approximately 15kbits per second.
One could set transrate to 10 and transfreq to 100 resulting in one byte of payload sent every 100 milliseconds. Sending anything less then 64 Bytes of payload data probably means that more data is transmitted as overhead (other stack layers) then actual payload.
If size of payload (chunk) sent every transfreq is larger then what your network driver can put on the wire, you will see multiple packets on the wire per data chunk.
#####clientcheck
Clientcheck object is used to set parameters in HTTP response received to be crossreferenced. This is a simple one for one comparrison of Node.js HTTP response object properties. In following example status code (Node.js HTTP response object "statusCode" property) is checked to see if it equals 200.
"clientcheck":
{ "statusCode": "200"
},
#####serverresponse
Serverresponse object is used to set parameters for listener side. Supported options are any option supported by Node.js HTTP server response object. Internal to qck app, settings here are just copied over to that object.
method and pathname are used to match HTTP requests received.
In following example any request received with method GET wuth url path /test1 will be responded to with status Code 200, and include body "Test1 response here".
"serverresponse":
{ "pathname": "/test1",
"method": "GET",
"statusCode": "200",
"body": "Test 1 response here"
}
#####servercheck
HTTP requests received by listener that match serverresponse
will be checked in same manner clientcheck
works. Failing a check does not prevent the response from being sent. Test result will have count of mistmatches in check.
In following example requests received will be crossreferenced to see if they contain a header x-qck-id with value of 123456789.
"servercheck":
{ "headers": {"x-qck-id":"123456789"}
}
####Sample Configuration File
{
"__globalconfig__":
{ "listenerIp":"127.0.0.1",
"listenerPort":"6589",
"server":"127.0.0.1",
"serverPort":"6589",
"loglevel":"1"
},
"test1":
{ "clientrequest":
{ "pathname": "/test1",
"method": "GET",
},
"clientcheck":
{ "statusCode": "200"
},
"serverresponse":
{ "pathname": "/test1",
"method": "GET",
"statusCode": "200",
"body": "Test 1 response here"
}
},
"test2":
{ "clientrequest":
{ "pathname": "/testt2",
"method": "POST",
"filename": "file.sample",
"transrate": "300000",
"transfreq": "25"
},
"clientcheck":
{ "statusCode": "200"
},
"serverresponse":
{ "pathname": "/test2",
"method": "POST",
"statusCode": "200"
},
"servercheck":
{ "method": "GET"
}
},
"test3":
{ "clientrequest":
{ "pathname": "/test3",
"method": "GET",
},
"clientcheck":
{ "statusCode": "302"
},
"serverresponse":
{ "pathname": "/test3",
"method": "GET",
"statusCode": "302",
"headers": {"Contact":"/test4"},
"body": "Test 3 response here"
},
"servercheck":
{ "headers": {"x-qck-id":"123456789"}
}
},
"test4":
{ "serverresponse":
{ "pathname": "/test4",
"method": "GET",
"statusCode": "200",
"body": "Test 4 response here"
}
}
}
###Connection throttling internals
Throttling transmission rates inside Qck are implemented in such manner:
- Buffer (Array) containing 20 transmission chunks is populated at 1ms intervals, one chunk at a time. Once Buffer is full, this Node.js function will schedule it self to populate one chunk every "transfreq"*0.8 milliseconds. Node.js fs.readableStream used to get data from file is paused until available data could be entered into the Buffer.
- Simultaneously, function to send one chunk at a time from Buffer, every "transfreq" milliseconds is started using Node.js setInterval() function.
- Once all data is read from file, entered into the Buffer, and transmitted (Buffer is empty) closing boundaryKey is transmitted.
###Areas for improvement
- Function and logic that enters data into Buffer could use optimizing.
- Buffer size could be calculated based on transfreq and size of chunk to transmit, so instead of populating one chunk at a time, we populate multiple chunks at a time, and reschedule function to populate Buffer at X times larger interval then "timefreq", reducing load on event stack. This requires setting fs.readableStream internal buffer size to match X number of chunks. This is where 65536 chunk size comes from. On my test system (Mac OS X 64 bit), with default settings, Node.js fs.on('data', fn(chunk) { ...}) returns chunk size of 65536 bytes.
- fs.readableStream internal buffer size setting should be optimized, right now using defaults
- HTTP writableStream internal buffer could be optimized to match chunk size.