Embedding Web Servers
by Robert Spier
|
Pages: 1, 2, 3
Writing A Simple Web Server
The Basics
With the above information, it isn't hard to write your own simple Web server. There are several ways to do this and a few already written on CPAN. We're going to start from first principles though, and pretend, for the moment, we don't know about CPAN.
A good place to start looking for client/server information is in the perlipc document. About 2/3 of the way through is a section on "Internet TCP Clients and Servers". This section shows how to use simple socket commands to setup a simple server. A little further down is the section we're interested in - it demonstrates using the IO::Socket module to write a simple TCP server. I'll replicate that here.
#!/usr/bin/perl -w
use IO::Socket;
use Net::hostent; # for OO version of gethostbyaddr
$PORT = 9000; # pick something not in use
$server = IO::Socket::INET->new( Proto => 'tcp',
LocalPort => $PORT,
Listen => SOMAXCONN,
Reuse => 1);
die "can't setup server" unless $server;
print "[Server $0 accepting clients at http://localhost:$PORT/]\n";
while ($client = $server->accept()) {
$client->autoflush(1);
print $client "Welcome to $0; type help for command list.\n";
$hostinfo = gethostbyaddr($client->peeraddr);
printf "[Connect from %s]\n", $hostinfo->name || $client->peerhost;
print $client "Command? ";
while ( <$client>) {
next unless /\S/; # blank line
if (/quit|exit/i) { last; }
elsif (/date|time/i) { printf $client "%s\n", scalar localtime; }
elsif (/who/i ) { print $client `who 2>&1`; }
elsif (/cookie/i ) { print $client `/usr/games/fortune 2>&1`; }
elsif (/motd/i ) { print $client `cat /etc/motd 2>&1`; }
else {
print $client "Commands: quit date who cookie motd\n";
}
} continue {
print $client "Command? ";
}
close $client;
}
HTTPify It
That's not a HTTP server by any stretch of the imagination, but with a different inner loop it could easily become one:
while ($client = $server->accept()) {
$client->autoflush(1);
my $request = <$client>;
if ($request =~ m|^GET /(.+) HTTP/1.[01]|) {
if (-e $1) {
print $client "HTTP/1.0 200 OK\nContent-Type: text/html\n\n";
open(my $f,"<$1");
while(<$f>) { print $client $_ };
} else {
print $client "HTTP/1.0 404 FILE NOT FOUND\n";
print $client "Content-Type: text/plain\n\n";
print $client "file $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;
}
Let's look at the changes piece by piece:
my $request = <$client>;
Retrieve one line from the socket connected to the client. For this to be a valid HTTP request, it must match the following:
if ($request =~ m|^GET /(.+) HTTP/1.[01]|) {
That checks that it's a HTTP GET request, and is of a protocol version we know about.
if (-e $1) {
print "HTTP/1.0 200 OK\nContent-Type: text/html\n\n";
open(my $f,"<$1");
while(<$f>) { print $client $_ };
If the requested file exists, then send back a HTTP header that says that, along with a content type, and then the data. (We are assuming the content type is HTML here. Most http servers figure out the content type from the extension of the file.)
} else {
print $client "HTTP/1.0 404 FILE NOT FOUND\n";
print $client "Content-Type: text/plain\n\n"
print $client "file $1 not found\n";
}
If the file doesn't exist, then send back a 404 error. The content of the error is a description of what went wrong.
} else {
print $client "HTTP/1.0 400 BAD REQUEST\n";
print $client "Content-Type: text/plain\n\n
print $client "BAD REQUEST\n";
}
A similar error handler, in case we can't parse the request.
Almost 50 percent of the code is for error handling, and that doesn't even take into account the error handling we didn't do for I/O issues. But that's the core of a Web server, all in about 15 lines of Perl.
If you use the above code without modification, it will allow every
file on your system to be read. Generally, this is a bad thing. An
explanation of proper security is outside the scope of this article,
but generally you want to limit access to a subset of files, located
under some directory prefix. File::Spec::canonpath and
Cwd::realpath are useful functions for testing this.
Single Threaded, Nonforking, Blocking
The Web server presented above is very simple. It only deals with one request at a time. If a second request is received while the first is being processed, then it will be blocked until the first completes.
There are two schemes used to take advantage of modern computers' ability to multiprocess (run more than one thing at once.) The simplest way is to fork off a Web server process for each incoming request. Because of forking overhead, many servers pre-fork. The second method is to create multiple threads. (Threads are lighter weight than processes.)
For a simple embedded server, it isn't much more difficult to build a forking server, but the extra work is unnecessary if it's only going to be used by one person or with a low hit-rate. The only advantage to the forking method is that it can serve multiple pages at once. (Taking advantage of modern operating systems ability to multiprocess.)
More information on forking servers, can be found in the perlipc documentation.
With a simple modification to our loop, we can turn our Web server into a forking client:
while ($client = $server->accept()) {
my $pid = fork();
die "Cannot fork" unless defined $pid;
do { close $client; next; } if $pid; # parent
# fall through in child
$client->autoflush(1);
Structure
The example server above is useful for simple reporting of generated data. Because the accept loop is closed, all processing by the main part of the program needs to be complete before the Web server is run. (Of course, actions from the Web server can trigger other pieces of the program to run.)
There are other ways to integrate a simple Web server depending on the structure of your program, but for this article, we'll stick with the design above.

