Network programming can be cumbersome for even the most advanced developers. REALbasic, a rapid application development (RAD) environment and language, simplifies networking yet provides the power that developers expect from any modern object-oriented language. I'll show you how this powerful environment enables professional developers and parttime hobbyists alike to quickly create a full-featured webserver.
Because you may be new to REALbasic's unique features and language, it's a good idea to start with something simple. The goal of writing a webserver in REALbasic is not to replace Apache. Instead, this server will demonstrate how to use REALbasic to handle multiple connections using an object-oriented design.
Let's start with some preliminary information about how networking in REALbasic works, as well as a quick walk-through of the HTTP protocol.
REALbasic includes several networking classes that make development easier, including the TCPSocket, UDPSocket, IPCSocket, and ServerSocket, and they are all event-based. This means that you can implement events in your code that will run when certain things happen, such as when an error occurs or when data is received. This saves you the trouble of having to constantly check the states of sockets.
HTTP is a simple protocol that is used to request information (generally files). The HTTP protocol works by having the client, such as a browser, send a request to the server. The server will evaluate the request and reply with either an error or the information requested.
The syntax for a request is:
(GET | POST) + Space + RESOURCE_PATH + Space + HTTPVersion + CRLF
As with most text-based protocols, the ending of a line is a carriage-return and a line-feed concatenated together. This is called a CRLF. An example request that a browser would send for the root file on the server would be:
GET /index.html HTTP/1.1
The server then writes a response. Its syntax is:
HTTPVersion + Space + StatusCode + Space + ResponseStatus + CRLF + CRLF
The server can send more data after the end of the two CRLFs, if desired. That is where, for example, the page requested would be written. After all the data is sent, the connection is closed by the server.
REALbasic provides two classes which will be utilized for the majority of the work. The ServerSocket class automatically handles accepting connections without dropping any, even if they are done simultaneously. The TCPSocket provides an easy mechanism for communicating via TCP. Together, they allow for extremely easy creation of servers.
Let's get started by launching REALbasic 5.5. (Get a 10-day free trial here.) Once open, you will be presented with a few windows. To the left is a "Controls" palette, which contains commonly used GUI controls. To the right is the Properties window, which allows you to visually set properties on objects such as windows, controls, or classes. The window that contains the three items, Window1, MenuBar1, and App, is the project window.
To utilize the ServerSocket and TCPSocket, they need to be subclassed. When you subclass another class, you gain access to events that the superclass exposes. For example, the TCPSocket itself doesn't actually know what to do with the data once it has been received. So, you subclass the TCPSocket and implement the DataAvailable event so that you can do something when data is received.
First, create a new class named HTTPServerSocket whose superclass is ServerSocket. Do this by choosing "New Class" from the File menu. The project window will now have a new item named Class1. Click on it, and notice how the properties window updates to reflect what item is selected. Rename the class to HTTPServerSocket, and select "ServerSocket" from the Super popup menu.
While we're here, go ahead and add another class. Rename it to HTTPConnection, and set its superclass to TCPSocket. You can click on the superclass field to the left of the popup arrow, and REALbasic allows you to type in the class name. Start typing TCP, and notice how autocomplete kicks in. Press tab to let autocomplete finish the rest of the name for you.
Now, you have the two classes that are needed to make the HTTP server. The HTTPServerSocket class is responsible for creating HTTPConnections, and the HTTPConnection class is responsible for handling the HTTP communications.
This is a good time to save the project. Remember where you saved the project at because at the end of this article you will need to put another file beside it.
Double click on the HTTPServerSocket class. This will open a code editor window. On the left is the code browser, which helps navigate between methods, events, properties, and constants. All we need to do in the HTTPServerSocket is implement an event. Expand the "Events" section, and select "AddSocket."
The AddSocket event is fired when the ServerSocket needs more sockets in its pool. The way servers work is that there is generally a "pool" or collection of sockets that are sitting around. When a connection occurs, the server will hand off the connection to one of the sockets in the pool. The ServerSocket takes care of all the tricky TCP code required to create a server that doesn't drop connections for us. All you need to do is add this code to the AddSocket event:
return new HTTPConnection
That's all that is needed. Close that code editor, and double click on the HTTPConnection class we wrote. Here is a diagram of what will happen once the server receives a request:
Each of these tasks will be handled by different methods that we will create. The first task is to determine what resource is being requested. The server will have a string that contains the entire request, and this method will extract a string that represents the path to the resource. The next task will be to take that string and locate that file. The server will then either report an error or send the file. When done sending all the data, the server will close the connection.
To accomplish these tasks, three methods will be created. Add a method to the HTTPConnection class by choosing "Edit->New Method..." Create the method as shown:
The next method you'll need to create is one that takes the path returned from GetPathForRequest and returns the actual file to us. In REALbasic, files are dealt with through the FolderItem class rather than string paths. This is a huge benefit because not only is this same object used across all platforms, but it is so easy to use. Create a method named GetFolderItemForPath that takes "path as String" and returns a FolderItem. The method's scope can be private because the method only pertains to this class.
The last method you need to create is just a convenience method. No matter what type of response the server receives, the response will always have a similar header. To help eliminate repeated code, define one last method named WriteResponseHeader that takes the parameters "statusCode as Integer, response as String" with no return value. This method can also be private.
In the code browser, expand the Events section. You will see Connected, DataAvailable, Error, SendComplete, and SendProgress. With HTTP, there isn't anything the server needs to do in the connected event. The client will be sending a request, so the server needs to wait until we have data available. The DataAvailable event is very important because of that. That event fires whenever data has been received. That event is where most of the logic will reside. The Error event is fired when there is a socket-related error, but for this example, socket-related errors will be ignored. Finally, SendComplete and SendProgress can be used to keep the send buffer full without eating up too much memory.
Since this server is going to be memory-efficient, you need to keep one variable around. Variables that are on the class level and not created inside a method are called properties. The property that is needed is to store a reference to an already open file so that we can write more data from the file when needed. In REALbasic, there is a class called BinaryStream that is used to read and manipulate files. Although there are other classes for accessing files, the BinaryStream class will do what is needed. Create a property by choosing "Edit->New Property..." In the Declaration field, type "stream as BinaryStream" and change its scope to Private. Click OK when done.
|
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.
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.
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
Copyright © 2009 O'Reilly Media, Inc.