Embedding Web Servers
by Robert Spier
|
Pages: 1, 2, 3
Graph Walker
Above we mentioned using GraphViz to create an embedded graph viewer. To do that we'll use a Graph class that has some methods that will make our life easier. (There isn't actually a class that does all this, but you can do it with a combination of Graph and GraphViz available on CPAN.)
It is outside the scope of this article to cover graph operations, but I've named the methods so that they should be easy to figure out. I am also going to gloss over some of the GraphViz details. They can be picked up from a tutorial.
Three Easy Steps
-
This is the easy part.
To develop a graph browser that allows the user to click on a node to recenter the graph.
-
How is the Web browser going to communicate back to the Web server? The only way it can is by requesting pages (via URLs.) For a graph browser we need two different kinds.
First, an image representing the graph. Second, a HTML page containing the IMG tag for the graph and the imagemap.
Since every node has a unique name we can use that to represent which node, and then use an extension to determine whether it is the HTML page or the graphic.
node1.html - HTML page for graph centered on node1 node1.gif - GIF image for graph centered on node1 -
Now that you know what you're building, you can put it all together and implement it.
my $graph = do_something_and_build_a_graph();
while ($client = $server->accept()) {
$client->autoflush(1);
my $request = <$client>;
if ($request =~ m|^GET /(.+)\.(html|gif) HTTP/1.[01]|) {
if ($graph->has_node($1)) {
if ($2 eq "gif") {
send_gif( $client, $1 );
} else { # $2 must be 'html'
send_html( $client, $1 );
}
} else {
print $client "HTTP/1.0 404 NODE NOT FOUND\n";
print $client "Content-Type: text/plain\n\n";
print $client "node $1 not found\n";
}
} else {
print $client "HTTP/1.0 400 BAD REQUEST\n";
print $client "Content-Type: text/plain\n\n
print $client "BAD REQUEST\n";
}
close $client;
}
sub send_html {
my ($client, $node) = @_;
my $subgraph = $graph->subgraph( $node, 2, 2 );
my $csimap = $subgraph->as_csimap( "graphmap" );
my $time = scalar localtime;
print $client "HTTP/1.0 200 OK\nContent-Type: text/html\n\n";
print $client<<"EOF";
<HTML>
<HEAD>
<TITLE>Graph centered on $node</TITLE>
$csimap
</HEAD>
<BODY>
<H1>Graph centered on $node</H1>
<IMG SRC="/$node.gif" USEMAP="graphmap" BORDER=0>
<HR>
<SMALL>Page generated at $time</SMALL>
</BODY>
</HTML>
EOF
;
}
sub send_gif {
my ($client, $node) = @_;
my $subgraph = $graph->subgraph( $node, 2, 2 );
my $gif = $subgraph->as_gif();
print $client "HTTP/1.0 200 OK\nContent-Type: text/gif\n\n";
print $client $gif;
}
And that's it! We have created a dynamic graph browser.
I will admit that we glossed over some of the HTML and Client Side Imagemap details -- because they're tangential to the issue of embedding a Web server into a tool. An embedded Web server is like merging the Web server, cgi script and source of the data into one program -- sometimes the best way to build one is to start with a standard CGI script and use that.
More Details
Query Strings
Because our embedded Web server isn't serving actual files off of your hard drive you have lots of flexibility as to how to parse the requested URL. In normal Web servers the common way to pass extra arguments to requested pages/scripts is by using a query string.
A URL with a query string looks like this:
http://foo.bar/thepage.html?querystring
There is a convention for passing key/value data in query strings (from HTML FORM's for example):
http://foo.bar/page.html?keyone=dataone&keytwo=datatwo&keythree=3"
It's easy to modify our embedded webserver template to accept query strings. Just add it to the regular expression that parses the request:
if ($request =~ m|^GET /(.+)(?:\?(.*))? HTTP/1.[01]|) {
$2 will then contain the query string. You can parse it by hand or pass it to CGI.pm or another CGI Module to parse it.
URI Escaping
Some characters have special meaning in URIs. (We've already seen ? and &. Others are " (space), %, and #. See the RFC for the full list.) In order to allow them to be passed in requests they need to be escaped. Escaping a URI changes the special characters into their hex representation with a prepended %. For example, " becomes %20.
The easiest way to perform this encoding is to use the URI::Escape module.
use URI::Escape;
$safe = uri_escape("10% is enough\n");
# $safe is "10%25%20is%20enough%0D";
$str = uri_unescape($safe);
# $str is 10% is enough\n
We will want to unescape any data received from the client:
if ($request =~ m|^GET /(.+)(?:\?(.*))? HTTP/1.[01]|) {
my $page = uri_unescape($1);
my $querystring = uri_unescape($2);
More Ideas
You might want to embed a Web server into a tool to display the status of a task in a complicated way. Sure, you could just write the file to disk, but that's less fun!
If a task has multiple possible outputs, then you could use the Web server to allow the user to choose between them visually, or pop up a browser at various states of a project to let the user confirm that things are going according to plan.
Speaking of popping up, you don't need to make the user do anything to see the results of the page. You can force the browser to do it for you.
On UNIX systems you can use Mozilla/Netscape's X-Remote protocol:
system(q[netscape -remote 'openURL("http://localhost:9000/")' &]);
Our example code has a port number hardcoded into it. If that port is already being used on your system, then the IO::Socket::INET::new() call will fail. An easy improvement is to loop over a range of ports, or random port numbers, until an available port is found.
Reusable Code
In some cases, I've avoided the use of modules in this article. There are many things that could be done with modules including argument handling (CGI.pm), URI/URL parsing (the URI family of modules), and even the HTTP server itself. (HTTP::Daemon)
The code we've presented here tries to go through the behind the scenes process so you know what's going on.
For quick and dirty servers, HTTP::Daemon is probably easier to use. Here's an example:
use HTTP::Daemon;
use HTTP::Status;
use Pod::Simple::HTML;
my $file = shift;
die "File $file not found" unless -e $file;
my $d = HTTP::Daemon->new || die;
print "Please contact me at: <URL:", $d->url, ">\n";
while (my $c = $d->accept) {
while (my $r = $c->get_request) {
if ($r->method eq 'GET') {
my $rs = new HTTP::Response(RC_OK);
$rs->content( Pod::Simple::HTML->filter($file) );
$c->send_response($rs);
} else {
$c->send_error(RC_FORBIDDEN)
}
}
$c->close;
undef($c);
}
The above HTTP::Daemon based server is a simple, single purpose, POD->HTML converter. I provide it with the name of a pod file to parse, and every time I reload the page, it will pass it through Pod::Simple and display the HTML to the browser.
You'll note it has the same structure as our hand-made examples, but it handles some of the nitty-gritty work for you by encapsulating it in classes.
(If you think that HTTP::Daemon is much simpler than the original, then you should see the first version of this article which used the low-level socket calls.)
In Conclusion
Embedded hardware is all the rage these days - but embedded software can be quite useful, too. By embedding a Web server into your software you gain lots of possible output options that were difficult to have before. Tables, graphics and color! (Not to mention, output can be viewed by multiple computers.) Embedded Web servers open a world of opportunities to you.

