Image of Navigational Map linked to Home / Contents / Search Socket to me

by Ross Mack - GUI Computing
Image of Line Break

Welcome to Sockets

Socket programming is perhaps one of those things that is all over the place and yet has something of a mystique about it, to those who have not done it. This was certainly how I felt, before I had done any socket programming. And I guess years ago when, to do socket programming, you would be forced to implement your own sockets layer, and then call bizarre APIs to get it to work and it wasn't commonly used or understood amongst desktop developers, it was hard. However, we now live in, what is perhaps, a golden age where TCP/IP is on most desktops, our network administrators (at least many of them) understand DNS, and where reliable socket tools are available to programmers to simply plug into their applications.

Java has a well formed suite of Sockets classes, Delphi exposes some Sockets objects and VB (as of version 5) ships with a control to easily do socket development. Third party vendors have additional socket tools on offer, Dart Communications, Mabry, and Distinct readily come to mind. Dolphin systems even offer a simple to use, shareware Sockets OCX and Dimac a quite usable freeware socket component. So, perhaps it's about time we learnt how to do socket programming so we can take advantage of these tools. It broadens our abilities and helps us understand other existing socket-based applications, like the web browser you are using now.

Another great thing about sockets is that they are platform independent. Sockets can be used by Windows PCs, Unix boxes, OS/2 boxes, Java applets running in a browser or whatever. Once a socket is open it doesn't matter what each end is, they have a way to communicate. But you already knew that, right ? After all when you use FTP, Internet email, or your web browser you are not concerned with what the FTP/Mail/Web server is running. And neither should you be. But all these applications (and countless more) are built upon socket based protocols.

What do you need to do sockets ?

Not much. Your PC needs to be running TCP/IP and have an IP address. That's about it. In the examples that follow I will use the Socket control from VB6 to implement some sample applications that do socket communication. The control in VB5 is almost identical and the sockets controls from the third party vendors are usually very similar but have a few more features.

So where do we start ? Let's build a simple application that listens on a socket and accepts incoming requests. In general this sort of application would act as a server. After all this is essentially what a web server does.

Building a Server

First of all open a new VB project (Standard EXE project is fine) and add the Microsoft Winsock control. Don't be confused by the term Winsock, it merely refers to an implementation of SOCKets under WINdows. You will notice the control in your toolbox has an icon like the Network Neighbourhood icon. Draw the control on the default form and we will set the following properties:

  • Name=sckListen
  • LocalPort=2080
  • Protocol=0 - sckTCPProtocol

    Essentially what we have done here is set up the control to use the TCP protocol and to use port number 2080. Port numbers, the TCP protocol and localhost will be explained toward the end.

    Now we need to add just a little code. First of all we need to have the control activated to listen for an incoming connection. So, put the following code in Form_Load


    Next, when there is an incoming connection request we need to accept the request to establish the socket connection. We do this by using the ConnectionRequest Event exposed by the control. Insert the following code:

    Private Sub sckListen_ConnectionRequest(ByVal requestID As Long)
       sckListen.Accept requestID
    End Sub

    All this code is doing is identifying that a request is incoming - hence the event firing, it stops the control from listening for incoming requests using the Close method and Accepts the request it received by passing the RequestID to the Accept method. The requestID is really a meaningless number, it merely identifies the individual incoming request. Once this ConnectionRequest event has fired and the control has accepted the incoming request you have an open socket between the application that made the request and this application you are writing. This socket acts as a conduit through which the applications can communicate.

    At the moment, however, your socket server application is going to do absolutely nothing with incoming data. So, let's write some code in the DataArrival event of the control.

    Private Sub sckListen_DataArrival(ByVal bytesTotal As Long)
       Dim sData As String
       sckListen.GetData sData, vbString
       Me.Print sData;
    End Sub

    This code simply takes whatever data has arrived, reads it into a string and uses a print method to just display it on the form itself. This is simply to demonstrate the arrival of data so you can see your applications communicating very simply. From here we can build.

    Just to make this application marginally more usable we also want to capture the Close event from the control.

    Private Sub sckListen_Close()
    End Sub

    A close event occurs when the application communicating with this application closes the connection. So, all we do to deal with that is close the connection at our end and start listening for a new connection.

    Run the application and we will see if it works.

    You may notice initially that nothing apparently happens. That is because your application is happily sitting on its port and listening for a connection. Let's now connect to it from another application. In this case the simplest application we can use is the Telnet application that ships with Windows 9x/NT. You may need to go to Start|Run to start telnet which should be located in your Windows directory. Once telnet is started we need to open a connection to your application so we can start communicating with it. Go to the Connect menu and select Remote System. You will then be prompted with a dialog box. Set Hostname to localhost, port to 2080 and leave the TermType as whatever it currently is. Then click the connect button.

    The title bar of telnet should now read 'telnet - (localhost)' this means you have established a connection to your application. Arrange your windows so that you can see both your application and the telnet window. Now, start typing in the telnet window. You should find that what you type appears in the window of your application. Of course backspaces and such will not be handled correctly, but that is not the point. You have just completed your first socket application.

    Note: All the code featured in this article is available for download.

    The Client

    Now that we have a workable (albeit extremely simple) server, let's look at the client side of the connection. The Telnet app that comes with Windows is a useful tool for opening a simple socket and sometimes for doing some testing, but let's build a real client using VB and the Winsock Control.

    First of all create a new project in VB, a Standard EXE will again be fine. Then we need to add the Winsock control to this project and rename is sckConnect.

    We also want to add three buttons and a textbox. Name the first button cmdConnect, the second one cmdDisconnect and the third cmdSend. Name the textbox txtData. You should also set their Caption and Text properties appropriately. I have also set the Send button to be the default. Set everything but the Connect button and the Socket Control to Enabled=False.

    Your form should look something like this:

    Now let's add some event code to the controls.

    Private Sub cmdConnect_Click()
       sckConnect.Connect "localhost", 2080
    End Sub

    In the click event of the Connect button we merely try to establish a connection to the server app we built earlier. We assume that it will also be running on the same computer (localhost) and that it will be listening on the port we set in the server app earlier.

    Private Sub sckConnect_Connect()
       ConnectChange True
    End Sub

    The Connect event fires when a successful connection has been established. When this happens we simply want to enable and disable the appropriate controls to allow the user to send data and disconnect, but not to try and open the connection again. To do this we call a procedure defined below that does the work.

    Private Sub cmdDisconnect_Click()
       ConnectChange False
    End Sub

    On the click event of the disconnect button we tell the socket control to close the connection and call the procedure that manages the controls.

    Private Sub sckConnect_Close()
       ConnectChange False
    End Sub

    On the Close event of the socket control we also call the procedure that manages enabling and disabling the controls. This may seem odd as we already called it in the Disconnect button click event. However, if the connection is closed in the Disconnect button click event the Close event does not fire. When the connection is closed by the other end (the server) then the Close event will fire, and in that case we still want to adjust the state of the controls.

    Private Sub cmdSend_Click()
       sckConnect.SendData txtData.Text & vbCrLf
       txtData.Text = ""
    End Sub

    On the click event of the send button all we want to do is use the SendData method of the socket control to pump whatever is in the textbox across the connection to the server and then clear the textbox, ready for more input.

    You may notice that this app seems to perform a bit quicker than when you were using telnet to do essentially the same thing. The difference is that telnet would send every keystroke across the socket to the server and they would all be processed separately. Whereas, this app sends whole strings at a time, which is much more efficient in network traffic, and processing at both ends.

    Sub ConnectChange(ByVal bConnected As Boolean)
       If bConnected Then
          cmdConnect.Enabled = False
          cmdDisconnect.Enabled = True
          txtData.Enabled = True
          cmdSend.Enabled = True
          cmdConnect.Enabled = True
          cmdDisconnect.Enabled = False
          txtData.Enabled = False
          cmdSend.Enabled = False
       End If
    End Sub

    this ConnectChange procedure is simply a central place to handle the enabling and disabling of the controls on the form based on whether we believe we are connected or disconnected.

    Now that we have the code in place, let's run the app and see what happens. Firstly, however you need to run the server app. I recommend opening up a second copy of VB with the server app loaded and running it that way. You could also compile it and just run it as a normal app.

    OK, so run your new client application, have it connect to the server then start sending data to it. Make sure your server application is visible so that you can see the data arrive and be displayed. Try connecting and disconnecting a few times. You can also try closing the server application while the two are connected. Notice that the client application deals with the different connection and disconnection states with no fuss.

    Trapping errors.

    We should really add some error trapping at this point. What sort of error trapping you implement is really determined by what sort of application you are building. For the moment we will put in place some simple code to just report an error if it occurs. The error event of the socket control is the correct place for this.

    Private Sub sckConnect_Error(ByVal Number As Integer, Description As String, ByVal Scode As Long, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, CancelDisplay As Boolean)
       Select Case Number
          Case Else
             MsgBox Description & vbCrLf & "(" & CStr(Number) & ")", vbInformation, "Socket Error"
             CancelDisplay = True
       End Select
    End Sub

    This is a very simple error handler in that it does the same thing no matter what the error is. All it really does is override the default MsgBox that the socket control generates. It also attempts to close any open connection. It's a good idea to isolate common problems that your app may experience and to customise how the error handler deals with those situations. You will usually want to do that based on the Error number (hence the select statement in the code above) and the State property of the socket control. For convenience I have included a table of the various State property values here.
    sckClosed0Default. Closed
    sckConnectionPending3Connection pending
    sckResolvingHost4Resolving host
    sckHostResolved5Host resolved
    sckClosing8Peer is closing the connection

    Multiple Connections

    You may notice that if you attempt to open up multiple connections to the server app we prepared before it does not cope. To demonstrate this simple compile two copies of the client applications and attempt to have them both connect at the same time. An error will result with the second application trying to connect to the server. You could always run two copies of the server, but only one application can be listening on a port at a time (on any given host/PC).

    So, what is the solution? You need to somehow make it possible to have the server application accept multiple requests. There are a few tricks in doing this. Firstly you will need multiple socket controls, the easiest way to handle that is to create a control array of socket controls and use different array elements. You also need to make sure that any code that deals with the socket controls can work independently of which socket control it is handling requests for.

    To enable multiple connections we will make a few changes to the server application. Firstly we will add another socket control to the form called sckServer and we will set its index property to 0, making it the first element of a control array. Then we copy and paste four copies of it, to create a control array of 5 elements. Ideally you would code this so that array elements were loaded and unloaded as required, but for simplicity we will just pre-create the elements.

    Next I will add a listbox to display the communications that occur for each socket control. This merely gives a visual guide to the activity on the sockets.

    Your server window should now look a little like this:

    Let's look at the differences in the code.

    Private Sub sckListen_ConnectionRequest(ByVal requestID As Long)
       Dim nNum As Integer
       Dim bConnected As Boolean
       bConnected = False
       For nNum = sckServer.LBound To sckServer.UBound
          If Not bConnected And sckServer(nNum).State = 0 Then
             sckServer(nNum).Accept requestID
             bConnected = True
             lstComms(nNum).AddItem "Connection established."
          End If
       Next nNum
    End Sub

    In the ConnectionRequest event is perhaps the major code difference. Instead of just using the one socket control to listen and to establish connections, we now have one socket control which listens for connections (sckListen) and a number of controls that it passes connections off to as they come in. As you can see in this code when a connection request arrives it loops through the sckServer socket controls looking for one with no connection (State=0). Once it finds that one it passes the requestID to the socket control's Accept method. This takes the incoming connection request and connects it to the chosen socket control. The code then sets a flag to stop searching for an available socket control and displays some information in the socket control's corresponding listbox control, just to show us what is going on.

    As I mentioned before, only one process can listen for requests on any given port at a time (otherwise, how could the system determine which process should get the connections?). This means that we can't simply have all these controls listening on the same port, instead we use one control to listen and others to service the connections.

    Private Sub sckServer_Close(Index As Integer)
       lstComms(Index).AddItem "Connection closed."
    End Sub

    The close event of the sckServer socket controls simply places an annotation in their corresponding listbox. Again, merely for visual confirmation of what is happening.

    Private Sub sckServer_DataArrival(Index As Integer, ByVal bytesTotal As Long)
       Dim sData As String
       Dim nPos As Integer
       ' copy the data from the socket buffer into the tag property of the socket control
       sckServer(Index).GetData sData, vbString
       sckServer(Index).Tag = sckServer(Index).Tag & sData
       ' check if we got a CRLF
       nPos = InStr(sckServer(Index).Tag, vbCrLf)
       If nPos > 0 Then
          ' if a CRLF is found in the string, extract that line from the tag
          sData = Left$(sckServer(Index).Tag, nPos + 2)
          ' remove the portion from the tag that we are now processing
          sckServer(Index).Tag = Right$(sckServer(Index).Tag, Len(sckServer(Index).Tag) - (nPos + 1))
          HandleIncomingData Index, sData
       End If
    End Sub

    The DataArrival event for the socket controls is also a little bit different to the way this was coded in the previous examples. Note that the DataArrival event for the sckListen control should now never fire as it never actually establishes a connection with itself. In this newer DataArrival code we add a little more intelligence to the way the data is handled.

    What we are trying to achieve here is to read whatever data may be arriving on the socket but only process it one line at a time. This means we need to read everything into a temporary space and Grab each line from that space as they are completed. For this purpose I use the tag property of the socket control involved. It's just a convenient storage space for string data that I know will be uniquely maintained for each socket control. It would be quicker to use an array of strings or some such thing, but the Tag property will suffice.

    At first glance it would appear that using the PeekData method of the socket control followed by selective GetData calls with length arguments would be optimal. However, when you fetch data from the socket's buffer using the GetData method it clears the whole buffer, even if you only request that it fetch a small amount of data. So, it proves unsuitable for this sort of processing. In effect the result is equivalent to a 'Line Input#' statement for the socket. Once we have the 'Line' from the buffer we pass it and the index indicating which socket it came from to a procedure that deals with it.

    Some third party controls have a specific method for reading data from the socket a line at a time, but the control that comes with VB does not, hence the strange workaround.

    As I have brought up the PeekData method I guess I should explain it a little further. It is simply a way to read data from the socket's incoming buffer without removing it from the buffer. GetData automatically clears the buffer when you use it, but PeekData does not. They are otherwise identical in usage.

    Sub HandleIncomingData(ByVal nIndex As Integer, ByVal sData As String)
       Dim sTemp As String
       Dim sReply As String
       ' Trim the crlf (if any) from the end of the string.
       If Right$(sData, 2) = vbCrLf Then
          sTemp = Left$(sData, Len(sData) - 2)
          sTemp = sData
       End If
       ' try to interpret the incoming data as a command of
       ' some sort that this server recognises - these are trivial examples.
       If StrComp(Left$(sTemp, 5), "USER:", vbTextCompare) = 0 Then
          sReply = "Welcome to the world of sockets, " & Trim$(Right$(sTemp, Len(sTemp) - 5)) & "."
       ElseIf StrComp(Left$(sTemp, 4), "CMD:", vbTextCompare) = 0 Then
          sReply = "As you command I will now, " & Trim$(Right$(sTemp, Len(sTemp) - 4)) & "."
       ElseIf StrComp(sTemp, "TIME", vbTextCompare) = 0 Then
          sReply = "TIME:" & Format$(Now, "d-mmm-yyyy hh:nn:ss")
       ElseIf StrComp(sTemp, "QUIET", vbTextCompare) = 0 Then
          sReply = ""
          sReply = "Unknown command."
       End If
       ' Display what is happening in the appropriate listbox
       lstComms(nIndex).AddItem "Received:" & sTemp
       lstComms(nIndex).AddItem "Reply:" & sReply
       ' If a reply was generated, send it back down the socket with a CRLF
       If sReply <> "" Then
          sckServer(nIndex).SendData sReply & vbCrLf
       End If
    End Sub

    The HandleIncomingData procedure is a simple concoction that demonstrates how you might deal with data that comes in from the client processes over the sockets. In this case we try and interpret the line of data that came in as some sort of recognised command. None of them really mean anything but it should become apparent from this example that by interpreting the data that was sent by the client you could easily launch tasks, do database lookups and return data or do any number of other things. The sockets really are just a conduit for the communication, what you do with it at either end is up to you.

    The first thing HandleIncomingData does is to trim any CRLF pair from the end of the string that is passed to it, as we are usually not interested in such things. then it tries to pick out some recognisable command from the string which may or may not generate a response. In either case some information about what is happening is added to the appropriate listbox and the reply (if any) is sent back on the socket to the client.

    Now that we have the server appropriately adjusted to handle multiple connections, deal more intelligently with the incoming data and send responses, try running up a number of copies of the client app and sending some data across the sockets to the server. You can also use Telnet to connect to it again at the same time and see if you can get some responses from the server.

    You will notice that if you use telnet to connect to the server that you will actually see the replies come back that the HandleIncomingData procedure generates. Let's have a look at displaying the responses in the client app we have been building.

    Client II

    Let's now add a little more functionality to the client, to keep it in step with the server and see how we handle the asynchronous nature of request sending followed later by the arrival of replies from the server.

    First of all we add two new buttons to the form named cmdTime and cmdCommand, then we add a listbox named lstReply. In this we will put some of the data that comes back to us from the server. Again, using a listbox is simply convenience. Data received from the server could be processed in any way. Your form should now look a little more like this (yes, I know it looks terrible).

    Now let's look at the code changes.

    Private Sub cmdConnect_Click()
       Dim sName As String
       sName = InputBox$("Please enter your name:", "Server Connect", "")
       If sName <> "" Then
          sckConnect.Connect "localhost", 2080
          msName = sName
       End If
    End Sub

    The Click event of the Connect button has been changed a little to prompt the user for a name before connecting to the server. You will notice that the connect method of the socket control is invoked but the name we prompted for is merely written to a module level variable (msName). This is because at this point we cannot be sure that the socket will connect or how long it will take. So, we keep the name information aside for when the connection has been established. Which brings us to...

    Private Sub sckConnect_Connect()
       DoSend "USER", msName
       ConnectChange True
    End Sub

    When the connect event fires on the socket control we call a procedure that I will detail below to pass the username we prompted for in the Connect button click event. We use this same procedure to pass all the data through the socket. It handles (in a simple fashion anyway) keeping track of how to send each type of data and records what data was sent last.

    Sub DoSend(ByVal sType As String, ByVal sData As String)
       Dim sSend As String
       Select Case UCase$(sType)
          Case "USER", "CMD"
              sSend = sType & ":" & sData
          Case "TIME", "QUIET"
             sSend = sType
          Case Else
             sSend = sData
       End Select
       sckConnect.SendData sSend & vbCrLf
       msLast = sType
    End Sub

    As I mentioned the DoSend routine handles the actual transmission of data down the line to the server. It uses the sType argument to figure out what exactly needs to be passed down the socket. Sometimes just the type string itself is passed. It also stores the type of the last request transmitted to the server in a module level variable. This is so that when we get a reply back from the server we can handle it within the context of what our last request was. With a more complex application you could keep a queue of such request identifiers and that are removed from the queue as replies come back from the server asynchronously. This would allow you to submit many requests in series without necessarily waiting for replies to come through in between.

    The DoSend routine also ensures that a CRLF is postfixed to the data being sent.

    Private Sub sckConnect_DataArrival(ByVal bytesTotal As Long)
       Dim sData As String
       Dim nPos As Integer
       ' copy the data from the socket buffer into the tag property of the socket control
       sckConnect.GetData sData, vbString
       sckConnect.Tag = sckConnect.Tag & sData
       ' check if we got a CRLF
       nPos = InStr(sckConnect.Tag, vbCrLf)
       If nPos > 0 Then
          ' if a CRLF is found in the string, extract that line from the tag
          sData = Left$(sckConnect.Tag, nPos + 2)
          ' remove the portion from the tag that we are now processing
          sckConnect.Tag = Right$(sckConnect.Tag, Len(sckConnect.Tag) - (nPos + 1))
          HandleReply sData
       End If
    End Sub

    The DataArrival routine is implemented virtually identically to the equivalent routine at the server (Hint: this could be abstracted to a simple function that returns the next line of data from the passed socket). Whatever data has come in is then passed to the HandleReply procedure which... um... handles the replies.

    Sub HandleReply(ByVal sReply As String)
       Dim sTemp As String
       If Right$(sReply, 2) = vbCrLf Then
          sTemp = Left$(sReply, Len(sReply) - 2)
          sTemp = sReply
       End If
       Select Case UCase$(msLast)
          Case "USER"
             MsgBox sTemp, vbInformation, "Connected"
          Case "CMD"
             lstReply.AddItem sTemp
          Case "TIME"
             lstReply.AddItem "The server time is:" & Right$(sTemp, Len(sTemp) - 5)
          Case "QUIET"
          Case Else
             lstReply.AddItem sTemp
       End Select
    End Sub

    Again, the HandleReply routine has some striking similarities to the HandleIncomingData routine from the server app. Anyway, in this routine we take whatever data has come in and based on what the last command sent to the server was we act upon it. In some cases we do nothing, in others we display message boxes or add a string to the faithful old listbox.

    Most of the other code that I modified in the client app is either trivial or quite straight forward. Here are the click events for the new buttons and the Send button sans explanation.

    Private Sub cmdCommand_Click()
       Dim sCmd As String
       sCmd = InputBox$("What command shall I send to the server ?", "Send Command", "")
       If sCmd <> "" Then
          DoSend "CMD", sCmd
       End If
    End Sub
    Private Sub cmdTime_Click()
       DoSend "TIME", ""
    End Sub
    Private Sub cmdSend_Click()
       DoSend "", txtData.Text
       txtData.Text = ""
    End Sub

    Of course the code for this version of the client is also available for download.

    The Promised Explanations

    Right at the start of this article I promised to explain a few terms I was bandying about. Here they are.

    Port Numbers

    A port number is merely an identification number. It has no meaning in and of itself. It merely provides a way to differentiate the different applications that are listening for socket connections on a given PC/Host. For example Web servers usually listen on Port 80, SMTP servers on port 25, FTP uses ports 20 and 21 and so on. You can run any of these servers/applications on different port numbers they would still work the same, but you would need to tell anyone who wanted to use it what port numbers they were so that they would know how to connect. Avoid using port numbers in your applications below 1000 or so as most of those numbers already have an accepted use.

    It's also a good idea to build your applications so that the port numbers it uses are configurable, so that you can avoid conflicts if they occur.

    You can find out what Port numbers are in use by your computer at any given time using the netstat command. Here is a sample of its output :

    Active Connections
      Proto  Local Address          Foreign Address        State
      TCP    brimstone:1025         localhost:1027         ESTABLISHED
      TCP    brimstone:1027         localhost:1025         ESTABLISHED
      TCP    brimstone:1176  ESTABLISHED
      TCP    brimstone:1194         MCCLURE:nbsession      ESTABLISHED

    You can also add the -a parameter to include in the list the sockets that your host is listening on as well (server processes).


    In the examples above I used the address localhost as an address to connect to. localhost is a special host address that is valid for every host (PC) running TCP/IP. It always refers to the host itself. In fact the address localhost always resolves to the IP address One of the neat things about working with sockets is that you can develop both client and server on the one machine and when you move one or the other to another machine all you should need to change is the address it is connecting to.

    The address, localhost, is convenient for the sort of examples above because it will work on any system. You could just as easily replace any reference to localhost in the code with the address, that would work identically (except for the minor lookup to convert the name into the address). You could also determine what your IP address is by running winipcfg under win9x or ipconfig under NT and substitute that address. You can also find out what your IP address and hostnames are by visiting this URL.

    TCP Protocol

    The TCP Protocol is a connection based protocol for transfer of data over IP networks. TCP requires a connection to be established then will manage validation of message receipt and order of packets and so on. UDP, on the other hand is not an authenticated protocol, meaning that if some UDP packets are missed or are not received the error is ignored. However, it is a more efficient protocol due to the reduced management overheads. IT is well suited to situations where any given datagram may not necessarily be required to be received. For example, where a server may be sending out packets to clients indicating what the current time is. If one of the clients misses one of those messages but gets the next one it will be back on track again. So the importance of any given message is relatively low.

    Final statement for the defence

    Now we have managed to create a couple of simple socket applications, both to act as a server and as a client. Of course you could quite easily have your socket applications work as peers, each listening for connections and able to establish connections to their brethren. There is also a lot more to explore, even the simple socket control from VB has a few more features. For example, the GetData method has the ability to try and read data into a number of different datatypes. You may have noticed that I consistently used the vbString constant when using it in the sample code above to have it interpret the data in the socket buffer as a string.

    Expanding on the code here you can build applications that pass any sort of data for any sort of reason to each other. Experiment wildly and see what you some up with.

    If you are interested in more info on sockets programming you will find some good information, sample code and details of common Internet protocols (HTTP, SMTP, POP3, NNTP, FTP, etc) in Carl Franklin's 'Visual Basic 4.0 Internet Programming'. Yes, I know it has Visual Basic 4.0 in the title but everything it says is still quite relevant.

    Written by: Ross Mack
    October '98

    Image of Arrow linked to Previous Article
    Image of Line Break