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 run
This 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 1234
On 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-essential
Then 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-dev
Alternatively, 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.tcl
If 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.txt
Test 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.tcl
Single 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.tcl
Tests 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 run
Result:
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/s
By 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/s
And 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/s
Of 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.