HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

It seems that there is an increasing demand for Internet based applications. Every time I turn on my television, I see Internet-related advertisements or commercials ending with "See us on the Web at: http:// www.somebigcompany.com'." These URLs actually refer to other computers out there in cyberspace. Each of these computers has one or more socket-based Web Servers administering connections, file transfers and other various duties. As the Internet begins to play a greater role in commerce, the demand for Web Servers or customized Web servlets will grow.

In this article, we will build a basic HTTP Web Server! This socket-based Web Server will showcase Java's Socket, ServerSocket and Thread classes. The server will be multi-threaded so that multiple file transfers can occur simultaneously. This application will also provide a great way to learn about file access, as we will be retrieving files on the Server, reading their contents and writing their bytes to the client.

In a previous article (JDJ, Vol. 2, Issue 4), we developed a Socket-based client program. The program was an HTML source code viewer. It extracted the user's data from text fields which were embedded in a Frame Window style standalone application. The user would enter both a Server name and a filename to retrieve. The application would then open a Socket connection over the Internet to the Server specified and download the file. Upon downloading the file, we displayed its contents in a TextArea, also embedded in the frame window.

Since understanding sockets is crucial to building a Web Server, let's review a working definition of a socket: Sockets are the end-points of a connection between two computers. Recall the following analogy: A more concrete example of sockets can be learned from your average 7 year old.... Remember that game that you played where you took two Dixie cups and tied them together with a length of string to form a telephone? Your friend would take one of the cups and walk to the other side of the room and talk into the cup. You would put your ear up to the other cup and be able to hear your friend. The Dixie cups in this example represent sockets. You communicate with your friend by talking into the cup (getting an output stream from the Socket and sending bytes) and by putting your ear up to the cup to hear your friend talk (getting an input stream from the socket and reading data from it).

Let's examine a typical interaction between a Browser and a Server, which will help us to create our Web Server. A Web Browser requests a file from the server like this:

  • Our Web Server runs idle and listens for a connection request on port 80.
  • The Browser (a Socket Client) requests a connection with our Web Server on port 80.
  • Our Server hears the request and completes the Socket connection.
  • Both the Browser and Server establish input and output streams.
  • The browser's input stream becomes the server's output stream and vice-versa.
  • Next, the browser sends a request to the server via its output stream, using the GET command.
GET /filename HTTP/1.0 \r\n\r\n
  • The Server reads the request via its input stream and processes it by sending the contents of the file requested, via its output stream, back to the client.
  • Now the browser reads the requested data from the server via its input stream.
Our Web Server will run and remain idle. It will remain in a loop, forever listening for connection requests. In order to do this, we need to create a ServerSocket object. This ServerSocket object can be used like an ear to listen for a connection request on a specific port number. The default port for HTTP is port 80. We need to tell the ServerSocket object to start listening for a connection request. We do this by invoking the ServerSocket.accept() method. Then, the program will stop and wait for a connection request.

Upon a connection request, the ServerSocket object will provide a socket object to complete the server side of the connection. With the connection established, we need to read the request from the client and handle it. The most effective way to do this is to create an object that will handle these responsibilities. Since an HTTP Server handles file requests, I will name this new object FileRequest, which will also implement the Runnable interface.

An interface is a group of abstract methods (and sometimes variable constants). Think of an interface as a business contract in Java. The Runnable interface provides one abstract method: run(). Our FileRequest class will implement the Runnable interface, meaning that we promise (or are "under contract") to provide a run() method in our class. If we do not provide a run method, we will get a compilation error ("sued for breach of contract" by the Java compiler).

A Runnable interface declaration is a clue that we are going to be using threads. Threads and Runnable Objects walk hand-in-hand. Since we want to allow for several simultaneous file transfers to run in the background, each FileTransfer object will run in its own thread.

Let's take a look at the code in Serve.java (see Listing 1). The first three lines are importing the java.net.*, java.io.* and java.awt.* packages. These packages have the Java class definitions that we'll need for network socket connections (java.net.*), input/output communication (java.io.*) and for a GUI frame window and UI components (java.awt.*). Since this server will be a standalone application in a frame window, we are defining the Serve class to extend Frame and implement Runnable. We are using the Runnable interface because we want to process connection requests in a separate thread.

The Serve() constructor will initialize the server upon its creation. Here, we are setting the title in the frame window's title bar to "Server Program". Then, we create a Panel to contain a Label object, TextField object and two Button Objects. The TextField allows the user to change the port number that the server will listen for connections on. The buttons will allow the user to start and stop the server. All of these components are added to the Panel container and then the whole Panel is added to the top of the frame window.

A TextArea object will be placed in the "Center" of the frame window, allowing users to see information about connections and file requests. We finish the constructor by resizing the frame window and finally showing it to the user.

Now, let's examine the handleEvent(Event evt) method. In this method, we will handle various user events such as exiting the program, pressing the start button and pressing the stop button. The condition of the user exiting the program is processed by checking the Event.id for WINDOW_DESTROY. This is a standard way to exit a standalone program. The condition of a button press is handled by the remaining code. Initially, the Thread instance variable, runner, will be null, since the user has not started the server listening yet. The first time the start button is pressed, we create a new Thread and tie it to runner. We then start the Thread, which will, in turn, begin execution of this object's run method in the new Thread. Now that the port is set, we then set the TextField.editable to false.

Creating a Thread is similar to creating another computer and loading it with a Runnable object. When we call start, we are telling the other computer to begin executing its run() method.

Once the thread is created, the user will be able to freeze execution (with the Thread.suspend() method) or resume execution (with the Thread.resume() method) by pressing the buttons.

The run() method is the heart of this class. First, we retrieve the port number from the TextField and display a message in the TextArea, using a utility method we have created called display(). Next, we will create a new ServerSocket and construct it with the port number. Although the ServerSocket is created, we have not told it to actually start listening yet. We will do this in an infinite loop, using while(true). In that loop, we will first display a message that will inform the user that the Server is listening for connections. Second, we will make the ServerSocket object start listening for a connection, using the ServerSocket.accept() method. This method is blocking, meaning that the thread of execution will sit and wait until a connection request appears on the port with which the ServerSocket was created. Upon getting a connection request, the ServerSocket.accept() method will return a Socket object that will complete the connection. Remember that Sockets are the end-points of a connection between two computers. The ServerSocket has just provided us with our end of the connection.

The next line is tricky, since we are doing several things in a single line of code. We are constructing another Thread object using the new keyword and then starting it. Notice how we are creating and passing a Runnable object as a parameter to the Thread's constructor. That Runnable object is one that we have defined in File- Request.java. In the FileRequest's constructor, we are handing off the Socket object and a handle to this Serve object. The FileRequest object will handle the file transfer in this new Thread, while at the same time the server will listen for another connection. The FileRequest object needs the Socket, since it will actually do the file transfer. It also needs a handle to the Serve application in order to use the Serve.display() method, to display the status of the file transfers. We'll examine more on FileRequest.java later.

After displaying a message that the server has received a file request, it immediately jumps back to the top of the loop and listens for another connection. The previous file request is handled in the background by the thread running the FileRequest object. This is one of Java's advantages. It allows for multiple Threads to process several different tasks simultaneously.

The display method writes information to the text area for the user to see. Notice that it is synchronized, since many threads may want to write at the same time. This allows only one thread to write at a time while the others wait their turn. That concludes our analysis of the Serve class. Now, let's examine the FileRequest.java file (see Listing 2).

FileRequest imports the java.io.*, java.net.* and the java.util.* packages and as you've probably guessed, it also implements the Runnable interface. Its constructor initializes instance variables for the Socket and Serve objects passed in, as described earlier.

The run method here is written with a very clear style, which I like to employ whenever possible. First, let's examine exactly what this FileRequest object is supposed to do.

  1. First, it should read the request and extract the file name from it.
  2. Next, it should open the requested file, if it exists.
  3. Then, it should construct a header with specific information about the file for the browser.
  4. Finally, it should send the header and the file to the client.

The challenge is that all of these activities are error-prone. What if we can't read the request or open the file? What if the file does not exist on this server? What if we lose the connection to the browser in mid-transfer? All of these questions must be taken into account when designing this object.

In order to tackle this, I have written several nested if statements. Each task described above has its own method and is placed in sequential order. All of these methods, with the exception of createHeader(), are boolean, returning true if all went well and false if an exception was caught. Once one of these methods returns false, none of the other tasks are called. This style of coding makes the run() method very readable and easy to understand in the future, when you refer back to this program.

Now, let's look at each of these methods. We will start with the requestRead() method. In this method, we will create a DataInputStream from the Socket.getInputStream() and read from the stream in a while loop. Since this server is very basic and is an educational example, we will only handle GET requests. We will use the StringTokenizer to parse each line and look for a GET statement. When we finally find a Get statement, we will extract the fileName from the next Token. Since the Request has a slash before the file name, we need to strip the slash off by using the String.substring() method. The String will be indexed starting at zero, meaning that the second character will have an index of one. If the client did not provide a file name, we will use index.html as a default. After setting the fileName instance variable, we'll continue reading until we read a blank line. At this point, we break out of the loop and return true, if all went well. Any error that might occur would be caught and false would be returned as a result.

Next, we need to try to open the file that was requested. We will do this in the fileOpened() method. This method will first create a DataInputStream from a FileInputStream. BufferedInputStream is also used here to increase efficiency. Why use DataInputStream? I use it because I prefer it. Instead, I could have used the FileInputStream by itself. Notice that all of our Webs will be in a subdirectory called wwwroot. The fileLength instance variable is set by using the InputStream.available() method. This returns an int with the number of bytes that can be read. Next, we display the length of the file using app as a handle to the Serve application.

In the event of a FileNotFoundException, I want to set the fileName to a generic File-Not-Found html file and recall this method. This filenfound.html file will inform the browser that the requested file was not on our server. This is why we first need to check if the fileName is equal to filenfound.html. If it is, then something else went wrong and we should return false. With this method complete, the next method we will examine will be constructHeader().

The constructHeader() method creates a header that will give the browser information about this server and the requested file. Notice the if-else structure is checking the file name extensions to determine the Content-Type. After the Content-Type is determined, we construct the header as a String and place it in the header instance variable. Essentially, this is all that this method does.

Now that the header is constructed and everything else went well, we will finally send the file to the browser in the fileSent() method. Let's take a look at this method now.

The fileSent() method will create a DataOutputStream to the browser via the Socket.getOutputStream() method. Upon establishing this stream, we will write the header to the stream with the DataOutputStream.writeBytes() method. Next, we will display the header in the server just to amuse the server user. The file transfer will now take place in a loop. We will read a byte from the file (really an int, but Java does a conversion for us) and write a byte to the browser (called the client). The byte read will be -1 on end of file. The loop will continue until the end of file is reached. We use the variable bytesSent to keep track of how many bytes were actually sent. This value will be displayed later in the server's TextArea. After the transfer is complete, we flush() the OutputStream so that the browser gets all of the information which we have sent. We return true if all went well.

The last tasks to complete the run() method are closing the DataOutputStream and closing the client Socket. We'll use the OutputStream.close() and Socket.close() methods to accomplish this. Both of these methods need to be nested in a try/catch block, since they throw exceptions.

This concludes the code involved in our educational Web Server. This server is very basic. It is, however, a great example of Sockets, ServerSockets and multi-threaded programming to learn from. Now that you have a starting point to work from, you can enhance it in the area of exception handling. You may also find it enjoyable to expand this server to parse data for CGI applications. You may want to process POST and PUT requests as well as GET. I hope you have as much fun with this application as I have had.

Figure 3

About the Author
Joseph M. DiBella is the Senior Java Instructor and Curiculum Developer for Computer Educational Services in New York City. He also is the President of HMJ Electronics, a computer consulting company which develops software and Java-enhanced Web sites.

	

Listing 1: Serve.java.

import java.net.*;
import java.io.*;
import java.awt.*;

public class Serve extends Frame implements Runnable{
     Serve(){
        setTitle("Server Program");
        Panel p = new Panel();
        portInput = new TextField(String.valueOf(port),4);
        startButton = new Button("Start Server");
        stopButton = new Button("Stop Server");
        p.add(new Label("Port:"));
        p.add(portInput);
        p.add(startButton);
        p.add(stopButton);
        add("North",p);

        a = new TextArea(50,30);
        add("Center",a);
        resize(300,300);
        show();

    }
    public void run(){
        port=Integer.parseInt(portInput.getText());
        display("Server Started on Port:"+port+"\n");
        try{
            ss = new ServerSocket(port);
            while(true){
                display("listening again ** port: "+port+"\n");
                Socket s = ss.accept();
                new Thread(new FileRequest(s,this)).start();
                display("got a file request \n");
            }

        } catch(Exception e){ display("$$$$ exception "+e+"\n");}
    }
    public boolean handleEvent(Event evt){
        if(evt.id==Event.WINDOW_DESTROY){
             System.exit(0);
             return true;
        }
        if((evt.id==Event.ACTION_EVENT)&&(evt.target instanceof Button)){
                if(runner != null){
                    runner.suspend();
                    display("Server Stopped By User\n");
                }
            }
            if(evt.target == startButton){
               if(runner == null){
                runner = new Thread(this);
                runner.start();
                portInput.setEditable(false);
               }else{runner.resume();
            }
        }
        return super.handleEvent(evt);


    }

    public synchronized void display(String text){
        a.appendText(text);
    }
    public static void main(String[] args){
        new Serve();
    }
    private ServerSocket ss;
    private TextArea a;
    private int port = 80 ;
    private TextField portInput;
    private Button startButton, stopButton;
    private Thread runner=null;
}

Listing 2: FileRequest.java

import java.io.*;
import java.net.*;
import java.util.*;

class FileRequest implements Runnable{


    FileRequest(Socket s, Serve app){
        this.app=app;
        client = s;
    }
    public void run(){

        if(requestRead()){
            if(fileOpened()){
                constructHeader();
                if(fileSent()){
app.display("*File: "+fileName+" File Transfer Complete*Bytes Sent:"+bytesSent+"\n");
                }
            }
        }
          try{
            dis.close();
            client.close();
        }catch(Exception e){app.display("Error closing Socket\n"+e);}

    }


    private boolean fileSent()
    {
        try{
DataOutputStream clientStream = new DataOutputStream
(new BufferedOutputStream(client.getOutputStream()));
            clientStream.writeBytes(header);
            app.display("******** File Request *********\n"+
                        "******* "+ fileName +"*********\n"+header);
            int i;
            bytesSent = 0;
            while((i=requestedFile.read()) != -1){
                clientStream.writeByte(i);
                bytesSent++;
            }
            clientStream.flush();
            clientStream.close();
		 }catch(IOException e){return false;}
		 return true;

    }
    private boolean fileOpened()
    {
        try{
requestedFile = new DataInputStream(new BufferedInputStream 
(newFileInputStream("wwwroot/"+fileName)));
                fileLength = requestedFile.available();
                app.display(fileName+" is "+fileLength+" bytes long.\n");
            }catch(FileNotFoundException e){
                if(fileName.equals("filenfound.html")){return false;}
                fileName="filenfound.html";
                if(!fileOpened()){return false;}
            }catch(Exception e){return false;}


			return true;

    }
    private boolean requestRead()
    {
         try{
            //Open inputStream and read(parse) the request
            dis = new DataInputStream(client.getInputStream());
            String line;
            while((line=dis.readLine())!=null){

                StringTokenizer tokenizer = new StringTokenizer(line," ");
                if(!tokenizer.hasMoreTokens()){ break;}

                        if(tokenizer.nextToken().equals("GET")){

                            fileName = tokenizer.nextToken();
                            if(fileName.equals("/")){
                                fileName = "index.html";
                            }else{
                                fileName = fileName.substring(1);
                            }

                        }

            }

         }catch(Exception e){

            return false;
         }
           app.display("finished file request");

         return true;
    }


    private void constructHeader(){
        String contentType;
        if((fileName.toLowerCase().endsWith(".jpg"))||(fileName.toLowerCase().endsWith(".jpeg"))
||(fileName.toLowerCase().endsWith(".jpe"))){contentType = "image/jpg";}
        else if((fileName.toLowerCase().endsWith(".gif"))){contentType = "image/gif";}
        else if((fileName.toLowerCase().endsWith(".htm"))||
		(fileName.toLowerCase().endsWith(".html"))){contentType = "text/html";}
        else if((fileName.toLowerCase().endsWith(".qt"))||
		(fileName.toLowerCase().endsWith(".mov"))){contentType = "video/quicktime";}
        else if((fileName.toLowerCase().endsWith(".class"))){contentType = "application/octet-stream";}
        else if((fileName.toLowerCase().endsWith(".mpg"))||
(fileName.toLowerCase().endsWith(".mpeg"))||(fileName.toLowerCase().endsWith(".mpe")))
{contentType = "video/mpeg";}
        else if((fileName.toLowerCase().endsWith(".au"))||(fileName.toLowerCase().endsWith(".snd")))
            {contentType = "audio/basic";}
        else if((fileName.toLowerCase().endsWith(".wav")))
            {contentType = "audio/x-wave";}
        else {contentType = "text/plain";} //default

        header = "HTTP/1.0 200 OK\n"+
                 "Allow: GET\n"+
                 "MIME-Version: 1.0\n"+
                 "Server : HMJ Basic HTTP Server\n"+
                 "Content-Type: "+contentType + "\n"+
                 "Content-Length: "+ fileLength +
                 "\n\n";
    }

    private Serve app;
    private Socket client;
    private String fileName,header;
    private DataInputStream requestedFile, dis;
	private int fileLength, bytesSent;

}


 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.