I l @ ve RuBoard |
Over the preceding sections, you've seen quite a few examples of how the RPC library works and should now have a fairly clear understanding of the theory. This section puts it into practice with a few real-life demonstrations of how this technology can be used across the web. Retrieving Meteorological Data with XML-RPCI'll begin with something simple ”a basic RPC client-server implementation for information delivery over HTTP. I'll be using a MySQL database as the data source, XML-RPC as the expression language for the procedure calls and responses, and PHP for the actual mechanics. RequirementsLet's assume the existence of a fictional government department that is charged with the task of obtaining meteorological information and charting it for statistical purposes. In a rare fit of generosity, this department has decided to make some of its meteorological readings available to the general public, in the hope of encouraging amateur meteorologists (and interested science students) to take an interest in the subject. Consequently, this department needs some way to broadcast its raw data (at the moment, sets of temperature readings for different cities) over the web, to be available to any connecting client. Here's a snippet of the MySQL database table that contains the temperature data (you may assume that this table is automatically updated on a regular basis by an independent process). +------+-------+------+-------+ city t1 t2 t3 +------+-------+------+-------+ BOM 27.6 26.9 26.1 LON 10.5 11.9 10.8 NYC 17.69 15.8 14.99 +------+-------+------+-------+ All that is required is an interface to this database so that users can connect to the system and obtain readings for any city (keyed against each city's unique three-letter city code). RPC's client-server architecture provides a suitable framework for this kind of application. An RPC server can be used at one end of the connection to accept XML-encoded remote procedure calls (containing arguments such as the city code and the format in which data is required), and execute appropriate functions to retrieve the required data from the database. The results of invoking the procedure can then be sent back to the client for further processing or display. Obviously, there are a number of different ways to implement this application (Perl, Java, WDDX, and even a plain- vanilla PHP script with MySQL hooks), so my usage of XML-RPC here is purely illustrative . In the long run, however, using XML-RPC might well turn out to be a good decision ”because implementations of the protocol are available for different platforms, it would be possible for the fictional government organization to create other, non-web-based clients (for example, a Windows COM-based application) without any changes required to its RPC server. The ServerThe server is fairly easy to implement (and, in fact, hardly changes across subsequent examples), so I'll demonstrate that first. Listing 6.27 has the code. Listing 6.27 A Simple RPC Server<?php // get the request $request = $HTTP_RAW_POST_DATA; // create server $rpc_server = xmlrpc_server_create() or die("Could not create RPC server"); // register methods xmlrpc_server_register_method($rpc_server, "getWeatherData", "phpWeather") or die("Could not register method"); // call method $response = xmlrpc_server_call_method($rpc_server, $request, NULL, array("verbosity" => "no_white_space")); // print response echo $response; // clean up xmlrpc_server_destroy($rpc_server); // function to return weather data // $args include a city code and a parameter indicating whether // all three readings (raw) or a composite calculation (avg) // are required function phpWeather($method, $args, $add_args) { // open connection to database $connection = mysql_connect("localhost", "rpc_agent", "secret") or die ("Unable to connect!"); mysql_select_db("weather_data") or die ("Unable to select database!"); // get data $city = mysql_escape_string($args[0]["city"]); $query = "SELECT t1, t2, t3 FROM weather WHERE city = '$city'"; $result = mysql_query($query) or die ("Error in query: $query. " . mysql_error()); // if a result is returned if (mysql_num_rows($result) > 0) { // iterate through resultset list($t1, $t2, $t3) = mysql_fetch_row($result); // process data depending on requested output format if ($args[0]["format"] == "raw") { // return raw data return array("format" => "raw", "data" => array($t1, $t2, $t3)); } else if ($args[0]["format"] == "avg") { // total and average readings // return average $total = $t1 + $t2 + $t3; // do this to avoid division by zero errors if ($total != 0) { $avg = $total/3; } else { $avg = 0; } return array("format" => "avg", "data" => $avg); } } else { // return a fault return array("faultCode" => 33, "faultString" => "No dataavailable for that city"); } // close database connection mysql_close($connection); } ?> Most of this should be familiar to you from the previous sections of this chapter. The procedure getWeatherData() , which maps internally to the PHP function phpWeather() , is set up to accept two arguments: the three-letter city code and the data format (raw readings or arithmetic mean of raw readings) required by the client. This procedure is registered with the RPC server via the xmlrpc_server_register_method() function, and it is invoked when the server receives a POST request containing the XML-encoded RPC request.
Internally, the phpWeather() function is pretty simple. It uses the arguments provided to it to connect to a MySQL database, retrieve the data from it, and then return it to the caller as an array containing either the average or the raw data. This returned array is then encoded into XML and sent back to the client as a POST response. The ClientThe RPC client needs to do a few important things:
Listing 6.28 has the complete code. Listing 6.28 A Simple XML-RPC Client<html> <head> <basefont face="Arial"> </head> <body> <?php if(!$_POST['submit']) { ?> <form action="<? echo $_SERVER['PHP_SELF']; ?>" method="POST"> <b>City code:</b> <br> input type="text" name="city" size="4" maxlength="3"> <p> <b>Data format:</b> <br> <input type="Radio" name="format" value="avg" checked>Average only <br> <input type="radio" name="format" value="raw">Raw data <p> <input type="submit" name="submit" value="Go!"> </form> <?php } else { // where is the RPC server? $server = "weather.domain.com"; $url = "/rpc/server.php"; $port = 80; // RPC arguments $city = strtoupper($_POST['city']); $params = array("city" => $city, "format" => $_POST['format']); // encode XML-RPC request $request = xmlrpc_encode_request("getWeatherData", $params, array("verbosity" => "no_white_space")); // open socket $fp = fsockopen($server, $port); if(!$fp) { echo "Could not open socket"; } else { // create POST data string $post_data = "POST $url HTTP/1.0\r\nUser-Agent: PHP-RPC Client\r\nContent-Type: text/xml\r\nContent-Length: " .strlen($request) . "\r\n\r\n" . $request; // send POST data fwrite($fp, $post_data); // read the response $post_response = fread($fp, 14096); // close the socket fclose($fp); } // strip out HTTP headers in response // look for <?xml and get everything from there to the end of the response $response = substr($post_response, strpos($post_response, "<?xml")); // decode response $output = xmlrpc_decode($response); // more stringent error checks would be good here if(is_array($output)) { // check to see if a fault was returned if (!isset($output[0]["faultCode"])) { // no? this means valid data was returned // format and display if ($output["format"] == "avg") { echo "Average temperature reading for city $city is " . $output["data"] . " (based on three readings)"; } else if ($output["format"] == "raw") { echo "Last three temperature readings for city $city (8-hour intervals) are:<br><ul>"; echo "<li>" . $output["data"][0]; echo "<li>" . $output["data"][1]; echo "<li>" . $output["data"][2]; echo "</ul>"; } } else { // a fault occurred // format and display fault information echo "The following fault occurred:<br>"; echo $output[0]["faultString"] . " (fault code " . $output[0]["faultCode"] . ")"; } } else { // no array returned echo "Unrecognized response from server"; } } ?> </body> </html> This entire script is split into two main sections. The first section merely displays an HTML form for user input; this form contains a text field for the city code and a pair of radio buttons to choose the data format required. After the form is submitted, the script calls itself again; this time around, however, the $submit variable will exist, so the second half of the script will be executed. At this stage, the first order of business is to create an array to hold the remote procedure arguments, and create an XML-encoded RPC request with xmlrpc_encode_request() . Next , a socket is opened to the remote RPC server (actually a web server with PHP's RPC extension compiled in), and the XML data is sent to it as a POST request (note the various HTTP headers that must accompany the data packet). After the server has received the request, decoded it, and executed the remote procedure, the response is sent back (through the same socket connection) to the client as a POST response. This POST response contains a bunch of HTTP headers, in addition to the XMLRPC response string. These need to be stripped out so that the remaining XML can be decoded into a native PHP data type with xmlrpc_decode() . Finally, depending on the data format requested (this format is included in the RPC response), the temperature reading(s) are displayed with an appropriate message. Figure 6.3 and Figure 6.4 demonstrate what the client looks like, both before and after sending an RPC request. Figure 6.3. The XML-RPC client from Listing 6.28, prior to requesting weather data.
Figure 6.4. The XML-RPC client from Listing 6.28, with the weather data requested.
Now that was fairly complicated, especially the part involving sockets and HTTP headers ”which is why it makes sense to package those bits of code into functions and then separate them from the main body of your script. And, if you look at the official web site for PHP's RPC extension, you'll see that the library author, Dan Libby, has done just this: He's packaged a number of network transmission functions as separate utilities that can be included and used in your RPC client. Listing 6.29 demonstrates an alternative implementation of the script in Listing 6.28, using these packaged functions. As you will see, this listing is far simpler to read and understand. Listing 6.29 An Alternative XML-RPC Client<html> <head> <basefont face="Arial"> </head> <body> <?php if($_POST['submit']) { ?> <form action="<? echo $_SERVER['PHP_SELF']; ?>" method="POST"> <b>City code:</b> <br> <input type="text" name="city" size="4" maxlength="3"> <p> <b>Data format:</b> <br> <input type="Radio" name="format" value="avg" checked>Average only <br> <input type="radio" name="format" value="raw">Raw data <p> <input type="submit" name="submit" value="Go!"> </form> <?php } else { include("utils.php"); // where is the RPC server? $server = "weather.domain.com"; $url = "/rpc/server.php"; $port = 80; // RPC arguments $city = strtoupper($_POST['city']); $params = array("city" => $city, "format" => $_POST['format']); // encode, transmit request and receive, decode response $output = xu_rpc_http_concise(array("host" => $server, "uri" => $url,"port" => $port, "method" => "getWeatherData", "args" => $params)); if(is_array($output)) { // check for faults if (!xu_is_fault($output)) { // no fault! // data has been returned // format and display if ($output["format"] == "avg") { echo "Average temperature reading for city $city is " .$output["data"] . " (based on three readings)"; } else if ($output["format"] == "raw") { echo "Last three temperature readings for city $city (8-hour intervals) are:<br><ul>"; echo "<li>" . $output["data"][0]; echo "<li>" . $output["data"][1]; echo "<li>" . $output["data"][2]; echo "</ul>"; } } else { // fault! format and display echo "The following fault occurred:<br>"; echo $output[0]["faultString"] . " (fault code " . $output[0]["faultCode"] . ")"; } } else { // no array returned echo "Unrecognized response from server"; } } ?> </body> </html> In this case, everything related to HTTP header transmission and response translation has been extracted into the function xu_rpc_http_concise() , which accepts a number of arguments identifying the RPC server, remote procedure name and arguments, and response format. This function is defined in the file "utils.php" , which you can download from the official site (http://xmlrpc-epi. sourceforge .net/). If you examine its internals, you'll see that it performs almost all the tasks so lovingly detailed in Listing 6.28, albeit with more stringent error checks. The same holds true for the xu_is_fault() function, which merely checks the server response for a fault identifier, and returns true if it finds one. Building an RPC-Based Mail ServiceThe previous example examined remote database access using the RPC framework. However, this is just one application of many; RPC makes it possible to expose and execute some very useful services over a network. The following example demonstrates one such service: an RPC-accessible mail service that checks a POP3 server for email and returns the number of messages in the specified mailbox. RequirementsThe requirements here are fairly simple: a server capable of accepting RPC requests and making POP3 (or IMAP) connections, and a client capable of encoding and decoding XML-encoded RPC data. Both client and server use SOAP as the encoding mechanism. Note that it's not necessary that both client and server be implemented using PHP (although that's the way I've done it here); either client or server (or both) can be implemented in any SOAP-supported platform. The ServerListing 6.30 has the code for the server. Listing 6.30 A SOAP Server<?php include("utils.php"); $request = $HTTP_RAW_POST_DATA; // create server $rpc_server = xmlrpc_server_create() or die("Could not create RPC server"); // register methods xmlrpc_server_register_method($rpc_server, "getTotalPOP3Messages", "getTotalPOP3Messages") or die("Could not register method"); // call method $response = xmlrpc_server_call_method($rpc_server, $request, NULL,array("verbosity" => "no_white_space", "version" => "soap 1.1")); // print response echo $response; // clean up xmlrpc_server_destroy($rpc_server); // function to return number of messages function getTotalPOP3Messages($method, $args, $add_args) { // check to see if the server supports POP3 // you may need to recompile your PHP build // to enable this support if (function_exists('imap_open')) { // open connection to mail server $inbox = imap_open ("{". $args[0]["pop_host"] . "/pop3:110}", $args[0]["pop_user"], $args[0]["pop_pass"]); if ($inbox) { // get number of messages $total = imap_num_msg($inbox); imap_close($inbox); return $total; } else { return xu_fault_code(2, "Could not connect to POP3 server"); } } else { return xu_fault_code(1, "POP3 support not available on RPC server"); } } ?> Again, the server code here is not very different from that seen in Listing 6.27. The primary difference here lies in the remote procedure ”the getTotalPOP3Messages() function accepts a username, password, and mail host as procedure arguments, and attempts a connection to the specified mail host using PHP's POP3 functions. If the connection is successful, the function returns the number of messages found; if not, it generates an appropriate SOAP-Fault using one of the additional utility functions include()-d in "utils.php" . SOAP encoding is implemented by the simple expedient of specifying SOAP as the encoding mechanism to the xmlrpc_server_call_method() function. Because PHP's RPC extension supports both SOAP and XML-RPC (see the earlier sidebar entitled "Teething Trouble" for a few caveats related to this), switching between the two is a fairly simple matter, and one that is quickly accomplished. The ClientIn its internals, this client also resembles the previous ones. Take a look at Listing 6.31. Listing 6.31 A SOAP Client<html> <head> <basefont face="Arial"> </head> <body> <?php if(!$_POST['submit']) { // display form ?> <table border="0" cellspacing="5" cellpadding="5"> <form action="<? echo $_SERVER['PHP_SELF']; ?>" method="POST"> <tr> <td><b>Username:</b></td> <td><input type="text" name="pop_user"></td> </tr> <tr> <td><b>Password:</b></td> <td><input type="password" name="pop_pass"></td> </tr> <tr> <td><b>POP Server:</b></td> <td><input type="text" name="pop_host"></td> </tr> <tr> <td colspan="2" align="center"><input type="submit" name="submit" value="Get Total Messages!"></td> </tr> </form> </table> <?php } else { // include useful network connection functions include("utils.php"); // where is the RPC server $server = "mail.service"; $url = "/rpc/server.php"; $port = 80; // RPC arguments $params = array("pop_user" => $_POST['pop_user'], "pop_pass" => $_POST['pop_pass'], "pop_host" => $_POST['pop_host']); // encode, transmit request and receive, decode response $result = xu_rpc_http_concise(array("host" => $server, "uri" => $url, "port" => $port, "method" => "getTotalPOP3Messages", "args" => $params, "output" => array("version" => "soap 1.1"))); // check for faults if(is_array($result) && $result["faultcode"]) { echo "The following error occurred: " . $result["faultstring"] . " (error code " . $result["faultcode"] . ")"; } // if none present, display message count else { echo "$result messages in mailbox"; } } ?> </body> </html> Again, the first half of the script sets up an HTML form for the user to enter mail account details; these details are then encoded into a SOAP envelope and sent to the SOAP server using the provided xu_rpc_http_concise() function. The decoded return value is then analyzed , and an appropriate message is displayed, depending on whether a fault was recorded or not. Figure 6.5 and Figure 6.6 demonstrate what the client looks like, both before and after sending an RPC request. Figure 6.5. The SOAP client from Listing 6.31, prior to retrieving mailbox information.
Figure 6.6. The SOAP client from Listing 6.31, with the mailbox information requested.
Executing Shell Commands via RPCFinally, an example that demonstrates the power inherent in RPC, and illustrates what a double-edged sword that power can be. The next (and final) example creates an RPC server that can accept any system or shell command from a client and execute this command on the server. RequirementsAgain, fairly simple: a client who accepts a command through user input, encodes it, and transmits it to a server that executes this command and returns the resulting output to the client. Both client and server communicate using XML-RPC, with HTTP as the network transport layer. The ServerListing 6.32 has the complete code for the server. This is probably one of the simplest servers created in this section: It merely exposes one procedure ” execR() ”that in turn simply uses PHP's backtick operator command (equivalent to the exec () command you read about in Chapter 2, "PHP and the Simple API for XML (SAX)") to execute the command received and return the output to the calling script. Listing 6.32 An XML-RPC Server<?php $request = $HTTP_RAW_POST_DATA; // create server $rpc_server = xmlrpc_server_create() or die("Could not create RPC server"); // register methods xmlrpc_server_register_method($rpc_server, "execR", "execR") or die("Could not register method"); // call method $response = xmlrpc_server_call_method($rpc_server, $request, NULL); // print response echo $response; // clean up xmlrpc_server_destroy($rpc_server); // execute command function execR($method, $args, $add_args) { $output = `$args[0]`; return $output; } ?> The ClientAs you might guess, the client is equally simple ”a form consisting of one text box, into which the user can enter the command to be executed on the remote server. This data is then encoded and sent to the server as an XML-RPC request with the command output coming back as an XML-RPC response. Listing 6.33 has the complete code listing. Listing 6.33 An XML-RPC Client<html> <head> <basefont face="Arial"> </head> <body> <?php if(!$_POST['submit']) { ?> <form action="<? echo $_SERVER['PHP_SELF']; ?>" method="POST"> <b>Remote command:</b> <br> <input type="text" name="command"> <input type="submit" name="submit" value="Go!"> </form> <?php } else { include("utils.php"); // where is the RPC server? $server = "remote.server"; $url = "/rpc/server.php"; $port = 80; // encode, transmit request and receive, decode response output = xu_rpc_http_concise(array("host" => $server, "uri" => $url, "port" => $port, "method" => "execR", "args" => $_POST['command'])); echo $output; } ?> </body> </html> Figure 6.7 and Figure 6.8 demonstrate what the client looks like, both before and after sending an RPC request. Figure 6.7. The XML-RPC client from Listing 6.33, prior to executing a remote command.
Figure 6.8. The XML-RPC client from Listing 6.33, after receiving the output of the uname -a command from the XML-RPC server.
|
I l @ ve RuBoard |