fluidmind
Star God: Fu Star God: Lu Star God: Shou

Web Services Made Simple

I wrote this little tutorial back in 2004 as part of a series of training sessions for the developers working under me. The use of web services was just becoming popular, but most developers still hadn't grasped the concept—much less begun to incorporate it into their work—so I wanted to reduce it to its simplest terms. These examples return the data in XML, but these days we typically use the JSON format.

If you're first encounter with Web Services was with XML-RPC or SOAP, you probably had a rather unpleasant experience. Standards committees seem to have one prime directive: take a simple concept and make it complicated. The idea behind Web Services is really quite simple. If you have two completely separate applications that need to exchange information, you can write a script in one that simply makes an HTTP request to a script in the other to ask it for that information. That's all there is to it. It's no more complex than what you do with your Web browser every time you ask a server for a Web page. In fact, the scripts you write to create Web Services are exactly like the scripts you write to create Web pages.

A Simple Example

The scenario: Two servers, A and B, are at separate locations (say one in Chicago and the other in Denver). Server B has a directory of files that server A needs to have access to. The problem: Only TCP ports 80 and 443 are open—that is, the Web server. So server A can't use telnet or SSH to directly login to server B and get a directory listing. The only way it can connect to server B is to send HTTP requests to server B's Web server. The solution is to write a script on server B that get the directory listing and sends it back to the requestor—in this case, a script on server A.

The files we want to give access to are in a folder called "files" in the document root of our Web server. First we'll create a folder in the document root called "webservices". Inside that folder, we'll place a script called "file-list.php", which will look something like this:

<?php
// Path to the directory we want to list
$path = '/data/files';

// Use the XML mime type so that parsers will treat it correctly
header("Content-type: application/xml\r\n");

print "<?xml version='1.0' encoding='UTF-8'?>\n";
?>
<DirectoryListing>
<?php
// Loop through the contents of the directory
foreach(new DirectoryIterator($path) as $file) {
    // Output just the files
    if ($file->isFile()) {
?>
    <File>
        <Name><?= htmlentities($file->getFilename()) ?></Name>
        <Size><?= $file->getSize() ?></Size>
        <LastModified><?= date('Y-m-d H:i:s', $file->getMTime()) ?></LastModified>
    </File>
<?php
    }
}
?>
</DirectoryListing>

This Web Service can be accessed very easily by making an HTTP request to http://www.example.com/webservices/file-list.php. We'll see in moment how to do that with a PHP script. The script will output the list of files into an XML format like the following:

<?xml version='1.0' encoding='UTF-8'?>
<DirectoryListing>
    <File>
        <Name>file1.txt</Name>
        <Size>20</Size>
        <LastModified>2006-05-08 11:12:38</LastModified>
    </File>
    <File>
        <Name>file2.txt</Name>
        <Size>15</Size>
        <LastModified>2006-05-08 09:38:47</LastModified>
    </File>
    <File>
        <Name>file3.txt</Name>
        <Size>15</Size>
        <LastModified>2006-05-08 09:38:57</LastModified>
    </File>
</DirectoryListing>

Next we need a second Web Service script to retreive one of the files. For the sake of simplicity, we'll assume that all of these files will be plain text files so that we don't have to deal with different MIME types. Our script then, can look something like this:

<?php
// Check for the parameters
if (isset($_GET['name'])) {
    $file = $_SERVER['DOCUMENT_ROOT'] . '/files/' . $_GET['name'];
    if (!file_exists($file)) {
        $error = 'Specified file does not exist.';
    }
} else {
    $error = 'No filename was specified.';
}

// If there were no errors, send the file
if (empty($error)) {
    // Change the HTTP headers to be appropriate to the file we're sending
    header("Content-type: text/plain");
    header('Content-Length: ' . filesize($file));
    header("Content-Disposition: inline; filename=" . $_GET['name']);
    include($file);
} else {
    header("HTTP/1.1 404 Not Found");
    header("Content-type: application/xml");
    print "<?xml version='1.0' encoding='UTF-8'?>\n";
?>
<Response>
    <Error><?= $error ?></Error>
</Response>
<?php
}
?>

This gives us two Web Services that we can call from any application in order to find out what files are available and to get any of those files.

In order to use, or "consume", those Web Services we need to be able to make an HTTP request from our script. This is easy in PHP using the "curl" functions. But for the sake of brevety, we'll just use an fopen directly to the URL.

<?php
$remoteServer = 'localhost';

// This example uses fopen to get the resource. But you should really use curl functions
// because your installation of PHP shouldn't allow fopen to open URLs directly
$resource = fopen('http://' . $remoteServer . '/webservices/file-list.php', 'r');
while (!feof($resource)) {
    $response .= fgets($resource);
}
fclose($resource);

// In case you didn't know, PHP's SimpleXML kicks ass!
$xml = simplexml_load_string($response);

// This is where you'd actually do something interesting with the information
// But for this stupid example, we'll just output it to the browser.
foreach ($xml->File as $file) {
    print '<p>File ' . $file->Name . ' is ' .
          $file->Size . ' bytes in size and was last modified on ' . 
          date('j F Y', strtotime($file->LastModified)) . ' at ' .
          date('g:i:s A', strtotime($file->LastModified)) . ".</p>\n";
?>

Security

Obviously this simplistic example has some security issues. In order to limit access to your Web Services you can employ a number of techniques. Obviously using SSL over port 443 is a good idea to keep things encrypted. You'll also want to use some form of authentication, either in your script or just HTTP basic authentication. If you know that you have a short list of servers that should have access to certain Web Services, you could place them in a separate directory and configure your web server to only allow access to that directory from the specified list of server IP addresses. Regardless, if you're Web Services aren't meant to be accessable to the entire public, you'll need to do some sort of authentication.

Further Reading

For a good exercise in writing scripts to consume Web Services, and to see how different REST techniques are from SOAP, take a look at the following two tutorials that walk you, step by step, through writing PHP scripts that access Amazon's Web Services.

The following articles give good overviews of REST Web Services: