An HTTP Server

An HTTP Server

In PHP, a PHP file represents an HTML page. A web server, such as Apache, accepts requests and if a PHP page is requested, the web server runs the PHP. But in Node.js, the main Node.js file represents the entire web server. It does not run inside a web server like Apache; it replaces Apache. So, some bootstrap Node.js code is needed to make the web server work.

The httpsvr.njs file was presented as an example in the previous chapter. Here’s the Node.js code for the httpsvr.njs file:

var http = require ( 'http' ); var static = require ( 'node-static' ); var file = new static . Server ();

http . createServer ( function ( req , res ) {

file . serve ( req , res ); }). listen ( 1337 , '127.0.0.1' ); console . log ( 'Server running at http://127.0.0.1:1337/' );

How does this work? As described in the previous chapter, the Node.js require() API function makes a

module available for use. The first two lines show a built-in module and an external module:

var http = require ( 'http' ); // built-in module var static = require ( 'node-static' ); // external module

If you installed Node.js and followed the examples in the previous chapter, the node- static npm package, which contains the node-static external module, will already be installed. If not, install it now using the npm executable:

npm install node-static

The third line is a little tricky:

var file = new static . Server ();

The node-static module wants to provide the Node.js server with as many file serving objects as needed rather than limit the client to a single file serving object. So, instead of using the module itself as the file serving object, the file serving object is created by calling a constructor function. A constructor function is a function that is designed to

be used with the new keyword. Using the new keyword and invoking the constructor function creates a new object.

In this case, the module object is named static. Inside the module object, there is a key named Server, which has a value that is a constructor function. The dot (.) operator indicates this relationship such that the new operator is properly applied. A newly con‐ structed file serving object is created and stored in the file variable.

The file serving object constructor can take zero or one arguments. If no arguments are provided, the file serving object uses the current directory (folder) as the HTTP server’s top-level documents directory. For example, if the httpsvr.njs is run in the ferris directory and a web browser such as Google Chrome goes to http://127.0.0.1:1337/hello.html, the file serving object will look for the hello.html file in the ferris directory. However, if a web browser goes to http://127.0.0.1:1337/exit/goodbye.html, the file serving object will look for the goodbye.html file in the exit directory inside the ferris directory.

However, when one argument is passed to the file serving object constructor, the file serving object will look for files in the directory specified in the argument. For example, if the .. string is passed as the argument, the newly created file serving object will look for files in the parent directory of the current directory.

The require() function takes only one argument, the name of the module to load. There is no flexibility to pass additional arguments to the module while loading. Since it is desirable to specify a directory to load files from as an argument, it is best that loading the module be completely separate from specifying where to load files from.

After creating a file serving object, the HTTP server object is created to accept HTTP requests and return the file to the client, probably a web browser:

http . createServer ( function ( req , res ) {

file . serve ( req , res ); }). listen ( 1337 , '127.0.0.1' );

22 | Chapter 2: A Simple Node.js Framework

This code can be rewritten as three statements to make it easier to understand:

var handleReq = function ( req , res ) {

file . serve ( req , res ); }; var svr = http . createServer ( handleReq ); svr . listen ( 1337 , '127.0.0.1' );

The first statement takes up three lines. It defines a variable named handleReq that, instead of containing a “normal” value like a string or a number, contains a function. In Node.js, functions can be assigned to variables, just like strings and numbers. When a function is assigned to a variable, the function itself is called a callback, and for our convenience, the variable that it is assigned to is called a callback variable. A callback variable is defined nearly the same as a regular function is defined, except that a callback can be unnamed and is preceded by a variable assignment.

In this case, the callback expects two parameters. The first parameter, req, contains all the data related to the HTTP request. The second parameter, res, contains all the data related to the HTTP response. In this implementation, the file serving object, file, decodes the HTTP request, finds the appropriate file on the hard disk, and writes the appropriate data to the HTTP response such that the file will be returned to the browser. The node-static module was specifically written with this in mind, so that the file serving object could return hard disk files with only one line of code.

The fourth line creates an HTTP server and an HTTP request handling loop that will continuously wait for new HTTP requests and use the handleReq callback variable to fulfill them:

var svr = http . createServer ( handleReq );

Inside the createServer() function, the handleReq callback variable will be invoked:

function createServer ( handleReq ) {

while ( true ) { // wait for HTTP request var req = decodeHttpRequest (); // somehow decode the request var res = createHttpRequest (); // somehow create a response object

handleReq ( req , res ); // invoke handleReq()

// send HTTP response back to client based on "res" object } }

Like functions (but unlike other types of variables), a callback variable can invoke the function that it contains. As you can see, invoking the handleReq callback argument is identical to calling a standard function; it just so happens that handleReq is not the name of a function but is the name of a callback variable or argument. Callback variables can

be passed to functions as arguments just like other kinds of variables. Why not just hardcode the file.serve() call into the createServer() function? Isn’t

serving files what a web server does?

An HTTP Server | 23 An HTTP Server | 23

while ( true ) { // wait for HTTP request var req = decodeHttpRequest (); // somehow decode the request var res = createHttpRequest (); // somehow create a response object

file . serve ( req , res ); // hardcode a reference to the "node static" module // send HTTP response back to the client based on "res" object } }

Yes, but passing a callback to the createServer() function is more flexible. Remember: the http module is built into Node.js and the node static module is an npm package that was installed separately. If the file.serve() call was baked into the create Server() function, using a different module instead of the node static module or adding additional custom HTTP request handling code would require copying and pasting the entire createServer() function just so a line in the middle could be tweaked. Instead, a callback is used. So, if you think about it, a callback is a way for some calling code to insert some of its own code into a function that it is calling. It is a way to modify the function that it is calling without having to modify the code of the function itself. The function being called, createServer() in this case, has to expect and support the callback, but if it is written with the callback in mind, a caller can create a callback that matches what is expected and the called function can use it without knowing anything about the calling code. Callbacks enable two pieces of code to successfully work together, even though they are written by different people at different times.

In this case, a callback function is passed to allow the caller to handle an HTTP request in whatever way that it sees fit. But, in many other cases, a callback function is passed as an argument so that the callback function can be invoked when an asynchronous operation has been completed. In the next chapter, this use of callback functions will be covered in detail.

The fifth line uses the svr object to listen on port 1337 on the '127.0.0.1' computer, a.k.a. the “localhost” computer (meaning the computer that the Node.js server is run‐ ning on):

svr . listen ( 1337 , '127.0.0.1' );

It should be pointed out that it is much more likely that the HTTP request handling loop is in the listen() function instead of the createServer() function. But for the purposes of explaining callbacks, it really does not matter.

Since the svr variable and the handleReq variable are used only once and can be replaced with more succinct code, the three statements are combined into one:

http . createServer ( function ( req , res ) {

file . serve ( req , res ); }). listen ( 1337 , '127.0.0.1' );

24 | Chapter 2: A Simple Node.js Framework

The last statement of the httpsvr.njs file writes a message to the console so that the person who started the HTTP server will know how to access it:

console . log ( 'Server running at http://127.0.0.1:1337/' );

The httpsvr.njs file makes a basic Node.js HTTP server. Now we move all the files that constitute the web application from the current web server that supports PHP to the same directory that the httpsvr.njs file resides in. When the httpsvr.njs file is started, these files—which include all the HTML, CSS, client-side JavaScript, image files (e.g., PNG files), and other assorted files—will be delivered to the client, probably a web browser, and work just as they always have. The client needs to be directed only to the correct port (e.g., 1337) to load these files from Node.js instead of the original web server. The only reason that the web application will break is that it is still written in PHP, but since HTML, CSS, client-side JavaScript, and image files are all handled and executed exclusively on the client, they will work as much as they can until a PHP file is needed. The .php files can be moved to the Node.js server, too, but they will not work because Node.js cannot interpret .php files.

The key difference between the .php files and all the other files is that the .php files are interpreted and the result of the interpretation is passed back to the client as an HTTP response. For all other files, the contents of the file are read and then written directly into the HTTP response with no interpretation. If the .php file is not interpreted, the client receives the PHP source code, which it does not know how to use. The client needs the output of the source code, not the source code itself, in order to work. A PHP to Node.js conversion boils down to writing Node.js code that will produce the exact same HTTP response in all cases that the PHP source code would have produced. It seems simple, but it is still a ton of work.

To start with, a Node.js local module will be created for each .php file. For the purposes of this book, a local module is a local .njs file that can be loaded by the main .njs file (i.e., httpsvr.njs) using the Node.js require() API function. To create a Node.js local module for a .php file, we will create an empty .njs file in the same directory as the .php file using the same filename but a different extension. For example, for admin/ index.php, create an empty admin/index.njs file.

For your own conversion effort, you will have to use your own judgment. In some cases, there may be so many .php files that creating the corresponding .njs files will be a lot of busywork, so it may be better just to do a few at first. In other cases, there may be only

a few .php files, so it may make sense to create all the empty files at once. Once there is a corresponding .njs file for some .php files, choose one of the .php files

and edit its corresponding .njs file:

exports . serve = function ( req , res ) { res . writeHead ( 200 , { 'Content-Type' : 'text/plain' });

res . end ( 'admin/index.njs' ); };

An HTTP Server | 25

Type this Node.js code into the .njs file and change the res.end() function call imple‐ mentation to indicate the correct path (in this case, admin/index.njs) to the particu‐ lar .njs file that is being edited.

With this simple code, the .njs file is now a Node.js module with an exports.serve() stub function implementation. A stub implementation is a placeholder with some very simple code that will be replaced with a serious implementation later. The exports .serve() stub function takes two parameters that correspond to the parameters to the callback function that is passed to the http.createServer() function in httpsvr.njs. The req parameter is the HTTP request object and the res parameter is the HTTP response object.

When the exports.serve() function is invoked, the stub implementation will return an HTTP response with a Content-Type HTTP header set to “text/plain” with the con‐ tent containing the path name of the file that is being invoked. By customizing the stub implementation, it will be a little easier to debug later, in case a coding error leads to an HTTP request for one file accidentally ending up in another file.

Once you have a .njs file with a exports.serve() stub function, you should modify the httpsvr.njs file to invoke the exports.serve() function at the appropriate time. But first, the module with the exports.serve() function must be made accessible to the httpsvr.njs file. The Node.js require() API function can load local modules as well as built-in module and npm packages:

var admin_index = require ( './admin/index.njs' );

To load a local module, the argument must contain a path name with the ./ string at the beginning. The ./ indicates that this is a path name to a local module and not a name of a built-in module or npm package.

In this example, the local module is assigned to the admin_index variable. Depending on the number of .php files that are in a web application (and, thus, the number of corresponding .njs files), it may be convenient and less confusing to use a variation of the path name as the name of the variable that holds the module that contains the Node.js code that implements the functionality of a particular .php file.

To route an HTTP request for a particular PHP page to the correct Node.js local module, the callback passed to the http.createServer() function needs to be modified to identify and pass the HTTP request to the appropriate Node.js local module. Using a simple comparison in an if statement, the HTTP request is examined to see if it is requesting a particular .php file and if it is, it is routed to the appropriate Node.js local module instead of passing it to the node static npm package (i.e., the file variable):

http . createServer ( function ( req , res ) {

if ( url . parse ( req . url ). pathname == '/admin/index.php' ) {

admin_index . serve ( req , res );

26 | Chapter 2: A Simple Node.js Framework

} else {

file . serve ( req , res );

} }). listen ( 1337 , '127.0.0.1' );

This source code uses the http and url Node.js built-in modules to implement a simple but functional Node.js server. If desired, more sophisticated optional packages, such as the express Node.js npm package, could be installed and used instead.

For this implementation, the url built-in module is needed to parse the URL in the HTTP request. The following Node.js code uses the require() API function to access the url built-in module:

var url = require ( 'url' );

Here we continue with our previous example. If the HTTP request is requesting the /admin/index.php URL resource, the admin_index local module interprets the re‐ quest and provides the HTTP response instead of allowing the file variable to read the file and return it as a static page.

You may notice that the URL in the HTTP request is tested against /admin/index.php instead of /admin/index.njs. Since the browser handles requests for .php files with custom code, it can handle .php file requests in any way that it likes, including ignoring the .php file on the hard disk and running Node.js code instead. An HTTP request is a request, not a demand, and the Node.js server can force .php file requests to be handled without using PHP at all. In Node.js, the .php file reference becomes a unique identifier to perform a particular action; the .php extension no longer means PHP. The if-else statements in the callback now just match a unique path with a specific piece of Node.js code. The semantic meaning of the .php extension is ignored.

Most likely, the client is requesting .php files either because the user clicked on a link in an HTML page that had the .php file as a hyperlink reference or because it is part of a JavaScript AJAX call. To change this, the HTML and client-side JavaScript files would need to be modified to refer to .njs files instead of .php files. Whether or not you make these changes depends on your situation. On the plus side, removing the .php file ref‐ erences eliminates confusion by other developers accessing your code, who may wonder why there are PHP references in a Node.js codebase. On the minus side, removing the .php file references creates technically unnecessary differences between the PHP and Node.js codebases.

Having walked through a step-by-step explanation with a single .php file, let’s put it all together and do the same thing with multiple .php files:

var http = require ( 'http' );

var static = require ( 'node-static' ); var file = new static . Server (); var url = require ( 'url' ); var index = require ( './index.njs' ); var login = require ( './login.njs' );

An HTTP Server | 27 An HTTP Server | 27

http . createServer ( function ( req , res ) { if ( url . parse ( req . url ). pathname == '/index.php' ) { index . serve ( req , res );

} else if ( url . parse ( req . url ). pathname == '/login.php' ) {

login . serve ( req , res );

} else if ( url . parse ( req . url ). pathname == '/admin/index.php' ) {

admin_index . serve ( req , res );

} else if ( url . parse ( req . url ). pathname == '/admin/login.php' ) {

admin_login . serve ( req , res );

} else {

file . serve ( req , res );

} }). listen ( 1337 , '127.0.0.1' ); console . log ( 'Server running at http://127.0.0.1:1337/' );

There are essentially two modifications: (1) a set of require() function calls have been added with one require() function call per .php file; and (2) a set of else-if statements have been added with one else-if statement per .php file. In this case, this modified version of httpsvr.njs is handling four .php files: /index.php, /login.php, /admin/ index.php, and /admin/login.php. For any web application that is converted from PHP to Node.js, each .php file in the application will cause both a new require() function call and an else-if statement to be added so that the httpsvr.njs file can properly route an HTTP request for a .php file to a specific and separate .njs file that implements the PHP code in Node.js instead.

As described earlier, a corresponding .njs file will be created for each .php file. For the example with four .php files, there will be four .njs files: index.njs, login.njs, admin/ index.njs, and admin/login.njs. The files will have the same code except for a slight modification to indicate the path name of the file: