A simple, fast HTTP server with Tcl handlers.
- Register a Tcl command for each HTTP method you want to accept.
- Set status code, headers and content-type on each response.
- Fast C++ server implementation based on Boost::Beast.
- Also includes simple synchronous HTTP client.
Bonus:
- A configuration script,
act, to manage C++ dependencies, Tcl package installation, Tcl module creation and installation, etc. - Some tips for (advanced) beginners.
% package require act::http
% act::http configure -host 127.0.0.1 -port 1234 -get {list 200 "hello, world" "text/plain"}
% act::http runThis is a pure C shared library extension, with no Tcl code.
act is a single-file build tool available at https://github.com/anticrisis/tcl-act.
$ ./act clean
$ ./act build examples/manifests/hello_world.txt
...
$ sh build/hello-world-0.1.tm -host 127.0.0.1 -port 1234On Windows, cd into the build directory and .\hello-world-0.1.tm.bat -host 127.0.0.1 -port 1234
Use the http configure command. With no arguments, it returns the current
configuration. These are the options:
- Host configuration
-host-port-maxconnections: default is 250
- HTTP handlers
-head-get-post-put-delete-options
- Variables set on each callback
-reqtargetvariable: the target part of the request, e.g. "/home"-reqbodyvariable: the body of the request-reqheadersvariable: the headers of the request
Use http run to start the server. Because this implementation uses a
blocking read on socket I/O, you must use control-C to stop the server.
See the examples directory for examples.
Use your system's package manager to install cmake and a C++ compiler. For
example,
$ sudo apt install cmake build-essentialThen install vcpkg in a nearby parent directory. You may need to use
the --vcpkg option to act.
Your Tcl distribution must include tcl.h and the tclstub library
appropriate to your system. For example, the ActiveTcl distribution for
Windows includes these components.
On linux:
$ sudo apt install tcl8.6-devAlternatively, you can build Tcl from source yourself to generate the stubs library.
NOTE: These tips are not required to build the examples.
I place extensions and packages into my ~/.tcl directory, and use an init file
to configure the paths. That init file is sourced by the act script so that
the act system install commands find user paths in which to install the
artifacts.
For example, since I prefer modules, my .tcl directory looks like this:
/home/anticrisis/.tcl
├── init.tcl
├── modules
│ └── act
│ └── http-0.1.tm
└── packages
~/.tcl/init.tcl contains:
::tcl::tm::path add [file normalize ~/.tcl/modules]
lappend ::auto_path [file normalize ~/.tcl/packages]and ~/.tclshrc (or tclshrc.tcl on Windows):
source ~/.tcl/init.tclIf I was dealing with Tcl-version-specific modules and packages, I would adjust
init.tcl accordingly, to select the appropriate paths based on the running tclsh
version.
This extension requires C++17.
On Windows or Mac, install your vendor's C++ build tools. On Windows, I recommend Microsoft Visual Studio Community Edition, which is free of charge for open source projects. I believe it's also available on Mac. Or if you don't want a complete IDE, you can search for and install the Build Tools for Visual Studio, which are also free of charge.
On Unix/Linux, you probably already have build tools installed. If not, please refer to other instructions.
Use vcpkg for painless cross-platform C++ dependency management.
To aid in the initial setup of your development environment, the act script
and act.bat batch file are provided. act provides commands to download and
setup vcpkg on your machine, and to download and compile the C++
dependencies needed for this project.
act and vcpkg are compatible with Windows, Mac and Linux, provided you
have installed C++ build tools and they are available on your path.
For example, on Windows, start a Developer Command Prompt or use one of the
Visual Studio-provided batch files to initialize your environment for
development. You should ensure the Microsoft C++ compiler is present by trying
cl -help.
Ensure the parent directory of this repository is writable by you.
Then, in the root of this repository, run the act vcpkg setup or act.bat vcpkg setup command. This will clone the vcpkg directory and perform the
initial bootstrap, which can take several minutes. Unfortunately, there is no
output during the compilation. If this makes you nervous, you should perform
the commands manually. A -dry-run option is provided by act to display the
commands without executing them.
After completing the vcpkg setup with act vcpkg setup, you should run act vcpkg install. This will download and compile the necessary dependencies,
which will take at least five minutes of compilation. (This project uses the
Boost Beast library, which is quite nice but not, shall we say, lightweight.
The resulting DLL is approximately 300KB.)
Again, you could perform this setup step manually if you prefer to see progress output.
All dependencies are contained within the vcpkg directory. No files are
installed anywhere else on your system.
Once vcpkg is set up, building and installing is as easy as:
$ ./act build manifest.txt
$ ./act install manifest.txtTest if you've successfully installed the package on your TCLLIBPATH:
% package require act::http
0.1
% act::http configure
-host {} -port {} -head {} -get {} -post {} -put {} -delete {} -options {} -reqtargetvariable {} -reqbodyvariable {} -reqheadersvariable {} -exittarget {} -maxconnections {}Basic tests are provided, which launch subprocesses to run the http server, and use this extension's included http client to check for specific responses.
To run the tests, build the shared library, then:
$ cd test
$ tclsh all.tclSingle test or files matching another glob regexp can be invoked like this, for example to run the tests in url.tcl only:
$ tclsh all.tcl -file url.tclTests run successfully on Microsoft Windows 10 and Ubuntu 20.04 (in WSL2). If you are a Mac user and run into problems, please let me know.
This is an "on my machine" test of a simple "hello, world" app, which is of course not representative of actual workloads. But it does serve to show the minimal overhead added to a realistic workload by the http server itself.
% package require act::http
% act::http configure -host 127.0.0.1 -port 8080 -get {list 200 "hello, world" "text/plain"}
% act::http runResult:
PS E:\> bombardier-windows-amd64.exe http://127.0.0.1:8080
Bombarding http://127.0.0.1:8080 for 10s using 125 connection(s)
Done!
Statistics Avg Stdev Max
Reqs/sec 164035.38 9240.09 190945.90
Latency 686.81us 380.20us 176.00ms
HTTP codes:
1xx - 0, 2xx - 1638899, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 25.61MB/sBy contrast, a similar test using nodejs express:
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('hello, world')
})
app.listen(port, () => {
console.log(`listening at http:://localhost:${port}`)
})PS E:\> bombardier-windows-amd64.exe http://127.0.0.1:3000 -l
Bombarding http://127.0.0.1:3000 for 10s using 125 connection(s)
Done!
Statistics Avg Stdev Max
Reqs/sec 5553.01 773.61 6156.96
Latency 22.49ms 2.64ms 140.00ms
Latency Distribution
50% 21.00ms
75% 23.00ms
90% 25.56ms
95% 30.00ms
99% 38.00ms
HTTP codes:
1xx - 0, 2xx - 55583, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 1.59MB/sAnd this example is using nodejs' built-in http module:
const http = require('http')
const hostname = '127.0.0.1'
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('hello, world')
})
server.listen(port, hostname, () => {
console.log(`listening at http://${hostname}:${port}`)
})PS E:\> bombardier-windows-amd64.exe http://127.0.0.1:3000 -l
Bombarding http://127.0.0.1:3000 for 10s using 125 connection(s)
Done!
Statistics Avg Stdev Max
Reqs/sec 17463.05 2311.83 19928.00
Latency 7.16ms 0.88ms 85.98ms
Latency Distribution
50% 7.00ms
75% 7.01ms
90% 8.00ms
95% 8.02ms
99% 14.98ms
HTTP codes:
1xx - 0, 2xx - 174529, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 3.71MB/sOf course, nodejs users will typically run multiple workers, each in its own process, to increase utilisation of available CPU cores. This does add additional complexity for some applications, for example when using a shared database.
BSD-3-Clause, just like (almost) Tcl.