macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Write a Webserver in 100 Lines of Code or Less
Pages: 1, 2

Coding the Actual Server

Now you're ready to begin coding the guts of the server. Select the DataAvailable event in the code browser. When this event fires, the first thing the server needs to do is see is if it has a full request. This means that there needs to be a CRLF. The function InStrB is used to search for those two characters in the internal buffer, which can be accessed via TCPSocket.LookAhead().

if InStrB( LookAhead(), chr(13) + chr(10) ) = 0 then return

TCPSocket.LookAhead() returns the internal buffer without removing the data it gives you from the internal buffer. If you perform a "Read" or "ReadAll," the data is removed from the internal buffer. Since the server only needs to peek at the data, it will use LookAhead here. This means that if the request doesn't have a CRLF, the event will exit.

As you are writing code, you may notice the code editor helping you out in several ways. When the name of a function is typed, a tips window will appear showing you information, such as the parameters and return type. Also, when in the middle of typing a term, REALbasic will provide guesses as best as it can. I've used several IDEs, and in my opinion, REALbasic's autocomplete is the only one that is done right. When there is an ellipses, press the tab key to bring up a list of completions. If it is guessing correctly as-is, just press tab, and REALbasic will finish typing for you.

If the server has a CRLF, you will want to extract the path from the request. To do this, the server will be calling a method that you created (but haven't yet implemented). NthField is a function that splits a string by a delimiter and returns a specified field. In this case, you are extracting all the data before the CRLF.

dim path as String
path = GetPathForRequest( NthField( ReadAll(), chr(13) + chr(10), 1) )

The next step is to get the FolderItem that represents the file on disk. Again, the server will be calling one of your support functions that you will implement later. The GetFolderItemForPath method returns a FolderItem.

dim file as FolderItem
file = GetFolderItemForPath( path )

If the file isn't found, GetFolderItemForPath will return nil or a FolderItem that doesn't exist. If the resource is found, the function will return a valid FolderItem object. One last consideration is that if the user requests a directory, the server needs to try to locate a "default file" such as index.html. This will be done first so that the File Not Found error handling can be in one location.

// find a default file such as index.html
if file <> nil and file.Directory then
if file.Child( "index.html" ).exists then
file = file.Child( "index.html" )
else
file = nil
end if
end if

// Check to see if the file exists
if file = nil or not file.Exists then
WriteResponseHeader 404, "File not found."
Write "<p>The requested file could not be found.</p>"
return
end if

Now, after the server has made it this far, the server has a file that it will send. You need to open the file and begin writing it.

stream = file.OpenAsBinaryFile()
WriteResponseHeader 200, ""
Write stream.Read( &hFFFF )

Now, if the file is larger than 64K (&hFFFF), you will have the chance to fill the buffer with more data in the SendProgress/SendComplete events. Select the SendProgress event in the code browser and add this code:

if stream <> nil and not stream.EOF and BytesLeft < &hFFFF then
Write stream.Read( &hFFFF )
end if

This code first checks to see if the stream is nil. This would happen if the server was writing an error instead of a file. The next condition is stream.EOF. EOF stands for End Of File. If the stream has no more data, there isn't a point in trying to read from it anymore. The final condition in the if statement checks to see how many bytes are left to send. If it's less than the threshold of 64K, the server will write out more data.

The final event you will be implementing is the SendComplete event. It fires when all of the internal buffer has been written. Since it is possible to get this event before the stream has reached its end, this code will be similar to the code in SendProgress that checks to see if there is more data to write. If there isn't any more data, the server will simply close the connection by calling Disconnect.

if stream <> nil and not stream.EOF then
Write stream.Read( &hFFFF )
else
Disconnect
end if

It's time to implement the support methods. Take a look at GetPathForRequest. It will need to split the string into parts using the space character as the delimiter.

dim parts() as String
parts = Split( request, " " )

Once you have the parts, you need to get the second part because it's what contains the path. However, you first will want to make sure that there are at least two parts.

if ubound( parts ) < 2 then
return ""
end if

Since HTTP uses the space character to delimit the fields, problems arise when a request is asking for a file that contains a space. In this case, the program making the request encodes characters that may cause problems in the syntax of %HH, where HH represents two hexadecimal characters. For example, the space character has an ASCII value of 32 decimal (or 20 hex), so the encoded entity would be %20. This method needs to decode the URL before returning it.

Since strings in REALbasic are immutable and cannot be changed without creating new strings, this method will use a MemoryBlock to decode the URL. This allows you to modify the URL quickly without allocating a new string each time you need to change a character. Since it doesn't make sense to have a MemoryBlock of size 0, insert one more check before proceeding:

if parts(1) = "" then return ""

Now, create the MemoryBlock. and begin looping over it.

dim out as MemoryBlock
out = parts(1)

dim i as integer
dim position as Integer
dim character as Integer

while i < out.Size

You need to get the current character and check to see if it's a percent symbol (ASCII &h25). If it is, turn the next two characters into a single byte by interpreting them as hex. If it wasn't a percent symbol, don't do anything.

  character = out.Byte(i)
if character = &h25 then
out.Byte(position) = val( "&h" + out.StringValue(position + 1, 2) )
i = i + 2
end if
i = i + 1
position = position + 1
wend

The last thing is that you need to return the result. In the process, you want to trim off any unused space that may have been obtained when decoding the URL. This will be done by using the StringValue method and passing in the position variable.

return out.StringValue( 0, position )

You're finished with the first method. Select GetFolderItemForPath. This method will take the path that GetPathForRequest just returned and turn it into a FolderItem. This code will use Split like before, but this time, it will use the character "/" as the delimiter. The method will loop over each path part and try to traverse down the server's root. For the time being, the server's root will be located in a folder called "Documents" that will live next to the application. Dealing with files in REALbasic is very easy. The FolderItem object provides methods for accessing directory contents by name or index. This code will be accessing children by name. Finally, you can check whether or not the file exists by using the FolderItem.Exists property and return nil if the file doesn't exist.

if path = "" then return nil

dim pathParts() as String
pathParts = Split( path, "/" )

dim i as integer
dim file as FolderItem

file = new FolderItem( "Documents" )

// Skip the first part because it will always be empty
// since all request paths need to start with a "/"
for i = 1 to ubound( pathParts )
if file.Child( pathParts(i) ).Exists then
file = file.Child( pathParts(i) )
else
return nil
end if
next

return file

That's all there is to it.

The last method is WriteResponseHeader. Click on that method in the code browser. This method is responsible for writing a properly formatted response header. The format is HTTP_VERSION + Space + STATUS + Space + MESSAGE + CRLF + CRLF. This is a fairly simple method:

Write "HTTP/1.1 " + format( statusCode, "000" ) + " " + _
response + chr(13) + chr(10) + chr(13) + chr(10)

You're finished writing the TCPSocket subclass. The last step that is required is creating an instance of our ServerSocket and telling it to listen.

Making the Server Listen

Close the code editor for our HTTPConnection class, and double click on the App class. You need to add a property that will store our HTTPServerSocket in. Add a property by choosing "Edit->New Property..." and name it "server as HTTPServerSocket."

Expand the Events section, and select the Open event. This event is fired as soon as the application is launched. Here, the application needs to instantiate our ServerSocket, set the port number it should listen on, and tell it to listen. The act of "listening" basically means that the server will begin waiting for a connection on the port it's told to listen on.

server = new HTTPServerSocket

server.Port = 8080

server.Listen

That's it! Go to where you saved your project, and put a folder named "Documents" next to it. Inside that folder, put a file named "index.html". Finally, Choose "Debug->Run" and you will see a blank window pop up once the server is launched. Switch to your browser of choice, and type in the url "http://127.0.0.1:8080/index.html". You should see your web page being loaded. The ":8080" portion simply tells the browser to connect on port 8080, which is what port specified above.

What Now?

Although there is a lot of information to cover when writing an HTTP Server, I've outlined the steps needed to get a simple HTTP server up and running in a GUI application. What's more, you have created a fully cross-platform project that can be compiled and run on Mac OS X, Mac OS 9, Windows, and even Linux.

There are many areas in which this server can be improved, but the design should be flexible enough to expand it as needed.

Although the current project is a GUI application, console applications are easily supported with REALbasic Professional Edition. Console applications are more appropriate for this type of application, as they can be run without a user being logged in. The only modification you will need to make this run as a console application is to use the Run event in a console application project instead of the Open event in a desktop application project.

Jonathan Johnson is a programmer and tester at REAL Software, Inc., provider of REALbasic, an easy-to-use cross-platform software design environment.


Return to the Mac DevCenter