简体   繁体   中英

Multithreaded Java Web Server - java.net.SocketTimeoutException

I've implemented a simple HTTP/1.1 compliant multithreaded web server which handles GET and HEAD requests. When I make a request through the web server, although it works, I'm getting a SocketTimeoutException after the 12 second timeout I set.

I'm testing my webserver by running it in Eclipse and directing the browser to localhost:portnumber and then trying to open files locally. I only have the timeout value because if I don't have it, any request to read a file that does not exist simply does not return, whereas it should return a 404 Not Found error.

The number of SocketTimeoutExceptions I receive is equal to the number of sockets that were opened to handle the request. I suspect that I should be handling this exception somehow, but I'm not sure where or how to do it. Any example or explanation of how to handle this would be very useful.

My code is split into a short webserver component followed with a separate ThreadHandler class to handle the requests. When I create a new client socket, I use a new thread to handle the request. I can provide the ThreadHandler class if necessary, but it is much, much longer.

Here is the WebServer component:

public class MyWebServer
{
    public static void main(String[] args) throws Exception
    {

        int port = 5000;
        String rootpath = "~/Documents/MockWebServerDocument/";

        if(rootpath.startsWith("~" + File.separator))
        {
            rootpath = System.getProperty("user.home") + rootpath.substring(1);
        }

        File testFile = new File(rootpath);

        //If the provided rootpath doesn't exist, or isn't a directory, exit
        if(!testFile.exists() || !testFile.isDirectory())
        {
            System.out.println("The provided rootpath either does not exist, or is not a directory. Exiting!");
            System.exit(1);
        }

        //Create the server socket
        ServerSocket serverSocket = new ServerSocket(port);

        //We want to process requests indefinitely, listen for connections inside an infinite loop
        while(true)
        {
            //The server socket waits for a client to connect and make a request. The timeout ensures that
            //a read request does not block for more than the allotted period of time. 
            Socket connectionSocket = serverSocket.accept();
            connectionSocket.setSoTimeout(12*1000);

            //When a connection is received, we want to create a new HandleRequest object
            //We pass the newly created socket as an argument to HandleRequest's constructor
            HandleThreads request = new HandleThreads(connectionSocket, rootpath);

            //Create thread for the request
            Thread requestThread = new Thread(request);

            System.out.println("Starting New Thread");

            //Start thread
            requestThread.start();
        }
    }

}

Within the ThreadHandler class I read the request from the socket, parse the request and respond with appropriate response via the socket. I've implemented persistent connections, so that each socket is only closed if the request includes the "Connection: close" token within the request. However, I'm not sure if this is occurring properly, especially in the case where I try to open a file that doesn't exist and should return a 404 Not Found Error.

Does anyone have any ideas how to handle these exceptions. Should I be doing something to close the threads?

Any help would be much appreciated.

EDIT: This is the handleRequest() which I call from within a try catch statement in run()

//This method handles any requests received through the client socket
    private void handleRequest() throws Exception
    {
        //Create outputStream to send data to client socket
        DataOutputStream outToClient = new DataOutputStream(clientsocket.getOutputStream());
        //Create BufferedReader to read data in from client socket
        BufferedReader inFromClient = new BufferedReader(new InputStreamReader(clientsocket.getInputStream()));
        //Create SimpleDateFormat object to match date format expected by HTTP
        SimpleDateFormat HTTPDateFormat = new SimpleDateFormat("EEE MMM d hh:mm:ss zzz yyyy");

        //Keep running while the socket is open
        while(clientsocket.isConnected())
        {

                String line = null;
                //HashMap to record relevant header lines as they are read
                HashMap<String,String> requestLines = new HashMap<String,String>();
                String ifModSince = null;
                Date ifModifiedSince = null;
                Date lastModifiedDate = null;

                //Keep reading the request lines until the end of the request is signalled by a blank line
                while ((line = inFromClient.readLine()).length() != 0) 
                {
                    //To capture the request line
                    if(!line.contains(":"))
                    {
                        requestLines.put("Request", line);
                    }

                    //To capture the connection status
                    if(line.startsWith("Connection:"))
                    {
                        int index = line.indexOf(':');
                        String connectionStatus = line.substring(index + 2);
                        requestLines.put("Connection", connectionStatus);
                    }

                    //To capture the if-modified-since date, if present in the request
                    if(line.startsWith("If-Modified-Since"))
                    {
                        int index = line.indexOf(':');
                        ifModSince = line.substring(index + 2);
                        requestLines.put("If-Modified-Since", ifModSince);
                    }

                    System.out.println(line);

                }

                //Get the request line from the HashMap
                String requestLine = (String)requestLines.get("Request");
                //Create Stringtokenizer to help separate components of the request line
                StringTokenizer tokens = new StringTokenizer(requestLine);

                //If there are not 3 distinct components in the request line, then the request does
                //not follow expected syntax and we should return a 400 Bad Request error
                if(tokens.countTokens() != 3)
                {
                    outToClient.writeBytes("HTTP/1.1 400 Bad Request"+CRLF);
                    outToClient.writeBytes("Content-Type: text/html"+CRLF);
                    outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                    outToClient.writeBytes("Connection: keep-alive"+CRLF);
                    outToClient.writeBytes(CRLF);
                    outToClient.writeBytes("<html><head></head><body>Error 400 - Bad Request</body></html>"+CRLF);

                    outToClient.flush();
                }
                else
                {
                    //Get the specific request, whether "GET", "HEAD" or unknown
                    String command = tokens.nextToken();
                    //Get the filename from the request
                    String filename = tokens.nextToken();

                    //Tidy up the recovered filename. This method can also tidy up absolute
                    //URI requests
                    filename = cleanUpFilename(filename);

                    //If the third token does not equal HTTP/1.1, then the request does 
                    //not follow expected syntax and we should return a 404 Bad Request Error
                    if(!(tokens.nextElement().equals("HTTP/1.1")))
                    {
                        outToClient.writeBytes("HTTP/1.1 400 Bad Request"+CRLF);
                        outToClient.writeBytes("Content-Type: text/html"+CRLF);
                        outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                        outToClient.writeBytes("Connection: keep-alive"+CRLF);
                        outToClient.writeBytes(CRLF);
                        outToClient.writeBytes("<html><head></head><body>Error 400 - Bad Request</body></html>"+CRLF);
                        outToClient.flush();                    
                    }
                    else
                    {
                        //Add the supplied rootpath to the recovered filename
                        String fullFilepath = rootpath + filename;

                        //Create a new file using the full filepathname
                        File file = new File(fullFilepath);

                        //If the created file is a directory then we look to return index.html
                        if(file.isDirectory())
                        {
                            //Add index.html to the supplied rootpath
                            fullFilepath = rootpath + "index.html";

                            //Check to see if index.html exists. If not, then return Error 404: Not Found
                            if(!new File(fullFilepath).exists())
                            {
                                outToClient.writeBytes("HTTP/1.1 404 Not Found"+CRLF);
                                outToClient.writeBytes("Content-Type: text/html"+CRLF);
                                outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                                outToClient.writeBytes("Connection: keep-alive"+CRLF);
                                outToClient.writeBytes(CRLF);
                                outToClient.writeBytes("<html><head></head><body>Error 404 - index.html was not found</body></html>"+CRLF);
                                outToClient.flush();
                            }
                        }
                        //If the created file simply does not exist then we need to return Error 404: Not Found
                        else if(!file.exists())
                        {
                            System.out.println("File Doesn't Exist!");
                            outToClient.writeBytes("HTTP/1.1 404 Not Found"+CRLF);
                            outToClient.writeBytes("Content-Type: text/html"+CRLF);
                            outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                            outToClient.writeBytes("Connection: keep-alive"+CRLF);
                            outToClient.writeBytes(CRLF);
                            outToClient.writeBytes("<html><head></head><body>Error 404 - " + filename + " was not found</body></html>"+CRLF);
                            outToClient.flush();

                        }
                        //Otherwise, we have a well formed request, and we should use the specific command to
                        //help us decide how to respond
                        else
                        {
                            //Get the number of bytes in the file
                            int numOfBytes=(int)file.length();

                            //If we are given a GET request
                            if(command.equals("GET"))
                            {
                                //Open a file input stream using the full file pathname
                                FileInputStream inFile = new FileInputStream(fullFilepath);

                                //Create an array of bytes to hold the data from the file
                                byte[] fileinBytes = new byte[numOfBytes];

                                //We now check the If-Modified-Since date (if present) against the file's
                                //last modified date. If the file has not been modified, then return 304: Not Modified
                                if(ifModSince != null)
                                {
                                    //Put the string version of If-Modified-Since data into the HTTPDate Format
                                    try
                                    {
                                        ifModifiedSince = HTTPDateFormat.parse(ifModSince);
                                    }
                                    catch(ParseException e)
                                    {
                                        e.printStackTrace();
                                    }

                                    //We now need to do a bit of rearranging to get the last modified date of the file
                                    //in the correct HTTP Date Format to allow us to directly compare two date object

                                    //1. Create a new Date using the epoch time from file.lastModified()
                                    lastModifiedDate = new Date(file.lastModified());
                                    //2. Create a string version, formatted into our correct format
                                    String lastMod = HTTPDateFormat.format(lastModifiedDate);

                                    lastModifiedDate = new Date();
                                    //3. Finally, parse this string version into a Date object we can use in a comparison
                                    try
                                    {
                                        lastModifiedDate = HTTPDateFormat.parse(lastMod);
                                    }
                                    catch (ParseException e)
                                    {
                                        e.printStackTrace();
                                    }

                                    //Comparing the last modified date to the "If-Modified Since" date, if the last modified
                                    //date is before modified since date, return Status Code 304: Not Modified
                                    if((ifModifiedSince != null) && (lastModifiedDate.compareTo(ifModifiedSince) <= 0))
                                    {
                                        System.out.println("Not Modified!");
                                        //Write the header to the output stream 
                                        outToClient.writeBytes("HTTP/1.1 304 Not Modified"+CRLF);
                                        outToClient.writeBytes("Date: " + HTTPDateFormat.format(new Date())+CRLF);
                                        outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                                        outToClient.writeBytes("Last-Modified: " + lastModifiedDate+CRLF);
                                        outToClient.writeBytes("Content-Length: " + (int)file.length()+CRLF);
                                        outToClient.writeBytes(CRLF);
                                    }                                   

                                }
                                else
                                {
                                    //Read in the data from the file using the input stream and store in the byte array
                                    inFile.read(fileinBytes);

                                    //Write the header to the output stream 
                                    outToClient.writeBytes("HTTP/1.1 200 OK"+CRLF);
                                    outToClient.writeBytes("Date: " + HTTPDateFormat.format(new Date())+CRLF);
                                    outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                                    outToClient.writeBytes("Connection: keep-alive"+CRLF);
                                    outToClient.writeBytes("Last-Modified: " + HTTPDateFormat.format(file.lastModified())+CRLF);
                                    outToClient.writeBytes("Content-Length: " + numOfBytes +CRLF);
                                    outToClient.writeBytes(CRLF);

                                    //Write the file
                                    outToClient.write(fileinBytes,0,numOfBytes);
                                    outToClient.flush();                                    
                                }

                            }   
                            //If we are given a HEAD request
                            else if(command.equals("HEAD"))
                            {
                                //Write the header to the output stream 
                                outToClient.writeBytes("HTTP/1.1 200 OK"+CRLF);
                                outToClient.writeBytes("Date: " + HTTPDateFormat.format(new Date())+CRLF);
                                outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                                outToClient.writeBytes("Connection: keep-alive"+CRLF);
                                outToClient.writeBytes("Last-Modified: " + HTTPDateFormat.format(file.lastModified())+CRLF);
                                outToClient.writeBytes("Content-Length: " + numOfBytes +CRLF);
                                outToClient.writeBytes(CRLF);

                                outToClient.flush();
                            }
                            //If the command is neither GET or HEAD, then this type of request has
                            //not been implemented. In this case, we must return Error 501: Not Implemented
                            else
                            {
                                //Print the header and error information            
                                outToClient.writeBytes("HTTP/1.1 501 Not Implemented"+CRLF);
                                outToClient.writeBytes("Date: " + HTTPDateFormat.format(new Date())+CRLF);
                                outToClient.writeBytes("Server: BCServer/1.0"+CRLF);
                                outToClient.writeBytes("Connection: keep-alive"+CRLF);
                                outToClient.writeBytes("Content-Type: text/html"+CRLF);
                                outToClient.writeBytes(CRLF);
                                outToClient.writeBytes("<html><head></head><body> Desired Action Not Implemented </body></html>"+CRLF);

                                outToClient.flush();

                            }
                        }                                               
                    }
                }

                //Get the connection status for this request from the HashMap
                String connect = (String)requestLines.get("Connection");
                //If the connection status is not "keep alive" then we must close the socket
                if(!connect.equals("keep-alive"))
                {
                    // Close streams and socket.
                    outToClient.close();
                    inFromClient.close();
                    clientsocket.close();                   
                }
                //Otherwise, we can continue using this socket
                //else
                //{                 
                    //continue;
                //}
            }
    }   

The reason for setting a read timeout is to place an upper bound on the time you are prepared to spend waiting for the peer to send you data. Only you know what that limit should be, and how often you are prepared to retry the read (if at all: most probably one timeout is enough), and how long is too long, but at some point you will decide that and just close the connection. Most HTTP servers make this configurable so the user can decide.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM