2505. Building Web Server with Java Socket
Http and Socket
Build a tiny http server with Java Sockets.
1. Web Server
Use ServerSocket
to create a web server and let it monitor at port 2540
. Whenever new http request comes in, this web server will create HttpWorker to handle it. The server does nothing but dispatches requests to workers.
public class WebServer { /** * Start a Server Socket to monitor client requests and dispatches the http * request to HttpWorkers. */ public static void main(String args[]){ // The maximum queue length for incoming connection int queue_len = 6; // Port number for http request int port = 2540; // A reference of the client socket Socket socket; try{ // Setup the server socket ServerSocket servsocket = new ServerSocket(port, queue_len); System.out.println("Web Server is starting up, listening at port " + port + "."); System.out.println("You can access http://localhost:2540 now."); while(true){ // Make the server socket wait for the next client request socket = servsocket.accept(); // Local reader from the client BufferedReader reader =new BufferedReader(new InputStreamReader(socket.getInputStream())); // Assign http requests to HttpWorker String req = ""; String clientRequest = ""; while ((clientRequest = reader.readLine()) != null) { if (req.equals("")) { req = clientRequest; } if (clientRequest.equals("")) { // If the end of the http request, stop break; } } if (req != null && !req.equals("")) { new HttpWorker(req, socket).start(); } } } catch(IOException ex){ //Handle the exception System.out.println(ex); } finally { System.out.println("Server has been shutdown!"); } } }
2. Worker
Each HttpWorker runs as a separate thread. Each request will be assigned to one worker. The worker can handle three types of requests.
- Display content of static files.
- Show directory structure, like File Explorer in windows.
- Execute dynamic CGI request.
In addition, it also can handle invalid requests.
class HttpWorker extends Thread { Socket socket; String clientRequest; /** * Construct * @param s, the socket which is to be monitored */ public HttpWorker (String req, Socket s) { socket = s; clientRequest = req; } /** * Start to work, after being assigned tasks by the server */ public void run(){ try{ // Clear list each time for handling new request LogUtil.clear(); // Local reader from the client //BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // Output stream to the client PrintStream printer = new PrintStream(socket.getOutputStream()); LogUtil.write(""); LogUtil.write("Http Worker is working..."); LogUtil.write(clientRequest); if (!clientRequest.startsWith("GET") || clientRequest.length() < 14 || !(clientRequest.endsWith("HTTP/1.0") || clientRequest.endsWith("HTTP/1.1"))) { // bad request LogUtil.write("400(Bad Request): " + clientRequest); String errorPage = buildErrorPage("400", "Bad Request", "Your browser sent a request that this server could not understand."); printer.println(errorPage); } else { String req = clientRequest.substring(4, clientRequest.length()-9).trim(); if (req.indexOf("..") > -1 || req.indexOf("/.ht") > -1 || req.endsWith("~")) { // hack attack LogUtil.write("403(Forbidden): " + req); String errorPage = buildErrorPage("403", "Forbidden", "You don't have permission to access the requested URL " + req); printer.println(errorPage); } else { if (!req.startsWith("/images/") && !req.endsWith("favicon.ico")) { // Avoid printing messages/logs for icon requests // Accept the http get request from the client // String clientRequest = reader.readLine(); //LogUtil.write("> New request received: " + clientRequest); } // Decode url, eg. New%20folder -> New folder req = URLDecoder.decode(req, "UTF-8"); // Remove the last slash if exists if (req.endsWith("/")) { req = req.substring(0, req.length() - 1); } // Handle requests if (req.indexOf(".")>-1) { // Request for single file if (req.indexOf(".fake-cgi")>-1) { // CGI request LogUtil.write("> This is a [CGI] request.."); handleCGIRequest(req, printer); } else { // Single file request if (!req.startsWith("/images/")&&!req.startsWith("/favicon.ico")) { LogUtil.write("> This is a [SINGLE FILE] request.."); } handleFileRequest(req, printer); } } else { // Request for directory LogUtil.write("> This is a [DIRECTORY EXPLORE] request.."); handleExploreRequest(req, printer); } } } // Save logs to file LogUtil.save(true); socket.close(); } catch(IOException ex){ // Handle the exception System.out.println(ex); } } /** * Handle CGI(fake) request * @param req, get request from client * @param printer, output printer */ private void handleCGIRequest(String req, PrintStream printer) throws UnsupportedEncodingException { // Parse the url to key-value pair Map<String, String> params = parseUrlParams(req); // Try to convert num1 and num2 to integer Integer number1 = tryParse(params.get("num1")); Integer number2 = tryParse(params.get("num2")); // Validate the input params if (number1 == null || number2 == null) { String errormsg = "Invalid parameter: " + params.get("num1") + " or " + params.get("num2") + ", both must be integer!"; LogUtil.write(">> " + errormsg); String errorPage = buildErrorPage("500", "Internal Server Error", errormsg); printer.println(errorPage); } else { LogUtil.write(">> " + number1 + " + " + number2 + " = " + (number1+number2)); StringBuilder sbContent = new StringBuilder(); sbContent.append("Dear " + params.get("person") + ", the sum of "); sbContent.append(params.get("num1") + " and " + params.get("num2") + " is "); sbContent.append(number1+number2); sbContent.append("."); String htmlPage = buildHtmlPage(sbContent.toString(), "Fake-CGI: Add Two Numbers"); String htmlHeader = buildHttpHeader("aa.html", htmlPage.length()); printer.println(htmlHeader); printer.println(htmlPage); } } /** * Handle single file request * @param req, get request from client * @param printer, output printer */ private void handleFileRequest(String req, PrintStream printer) throws FileNotFoundException, IOException { // Get the root folder of the webserver String rootDir = getRootFolder(); // Get the real file path String path = Paths.get(rootDir, req).toString(); // Try to open the file File file = new File(path); if (!file.exists() || !file.isFile()) { // If not exists or not a file printer.println("No such resource:" + req); LogUtil.write(">> No such resource:" + req); } else { // It's a file if (!req.startsWith("/images/")&&!req.startsWith("/favicon.ico")) { LogUtil.write(">> Seek the content of file: " + file.getName()); } // Print header String htmlHeader = buildHttpHeader(path, file.length()); printer.println(htmlHeader); // Open file to input stream InputStream fs = new FileInputStream(file); byte[] buffer = new byte[1000]; while (fs.available()>0) { printer.write(buffer, 0, fs.read(buffer)); } fs.close(); } } /** * Handle file and directory explore request * @param req, get request from client * @param printer, output printer */ private void handleExploreRequest(String req, PrintStream printer) { // Get the root folder of the webserver String rootDir = getRootFolder(); // Get the real file path String path = Paths.get(rootDir, req).toString(); // Try to open the directory File file = new File (path) ; if (!file.exists()) { // If the directory does not exist printer.println("No such resource:" + req); LogUtil.write(">> No such resource:" + req); } else { // If exists LogUtil.write(">> Explore the content under folder: " + file.getName()); // Get all the files and directory under current directory File[] files = file.listFiles(); Arrays.sort(files); // Build file/directory structure in html format StringBuilder sbDirHtml = new StringBuilder(); // Title line sbDirHtml.append("<table>"); sbDirHtml.append("<tr>"); sbDirHtml.append(" <th>Name</th>"); sbDirHtml.append(" <th>Last Modified</th>"); sbDirHtml.append(" <th>Size(Bytes)</th>"); sbDirHtml.append("</tr>"); // Parent folder, show it if current directory is not root if (!path.equals(rootDir)) { String parent = path.substring(0, path.lastIndexOf(File.separator)); if (parent.equals(rootDir)) { // The first level parent = "../"; } else { // The second or deeper levels parent = parent.replace(rootDir, ""); } // Replace backslash to slash parent = parent.replace("\\", "/"); // Parent line sbDirHtml.append("<tr>"); sbDirHtml.append(" <td><img src=\""+buildImageLink(req,"images/folder.png")+"\"></img><a href=\"" + parent +"\">../</a></td>"); sbDirHtml.append(" <td></td>"); sbDirHtml.append(" <td></td>"); sbDirHtml.append("</tr>"); } // Build lines for directories List<File> folders = getFileByType(files, true); for (File folder: folders) { LogUtil.write(">>> Directory: " + folder.getName()); sbDirHtml.append("<tr>"); sbDirHtml.append(" <td><img src=\""+buildImageLink(req,"images/folder.png")+"\"></img><a href=\""+buildRelativeLink(req, folder.getName())+"\">"+folder.getName()+"</a></td>"); sbDirHtml.append(" <td>" + getFormattedDate(folder.lastModified()) + "</td>"); sbDirHtml.append(" <td></td>"); sbDirHtml.append("</tr>"); } // Build lines for files List<File> fileList = getFileByType(files, false); for (File f: fileList) { LogUtil.write(">>> File: " + f.getName()); sbDirHtml.append("<tr>"); sbDirHtml.append(" <td><img src=\""+buildImageLink(req, getFileImage(f.getName()))+"\" width=\"16\"></img><a href=\""+buildRelativeLink(req, f.getName())+"\">"+f.getName()+"</a></td>"); sbDirHtml.append(" <td>" + getFormattedDate(f.lastModified()) + "</td>"); sbDirHtml.append(" <td>" + f.length() + "</td>"); sbDirHtml.append("</tr>"); } sbDirHtml.append("</table>"); String htmlPage = buildHtmlPage(sbDirHtml.toString(), ""); String htmlHeader = buildHttpHeader(path, htmlPage.length()); printer.println(htmlHeader); printer.println(htmlPage); } } /** * Build http header * @param path, path of the request * @param length, length of the content * @return, header text */ private String buildHttpHeader(String path, long length) { StringBuilder sbHtml = new StringBuilder(); sbHtml.append("HTTP/1.1 200 OK"); sbHtml.append("\r\n"); sbHtml.append("Content-Length: " + length); sbHtml.append("\r\n"); sbHtml.append("Content-Type: "+ getContentType(path)); sbHtml.append("\r\n"); return sbHtml.toString(); } /** * Build http page * @param content, content of the page * @param header1, h1 content * @return, page text */ private String buildHtmlPage(String content, String header) { StringBuilder sbHtml = new StringBuilder(); sbHtml.append("<!DOCTYPE html>"); sbHtml.append("<html>"); sbHtml.append("<head>"); sbHtml.append("<style>"); sbHtml.append(" table { width:50%; } "); sbHtml.append(" th, td { padding: 3px; text-align: left; }"); sbHtml.append("</style>"); sbHtml.append("<title>My Web Server</title>"); sbHtml.append("</head>"); sbHtml.append("<body>"); if (header != null && !header.isEmpty()) { sbHtml.append("<h1>" + header + "</h1>"); } else { sbHtml.append("<h1>File Explorer in Web Server </h1>"); } sbHtml.append(content); sbHtml.append("<hr>"); sbHtml.append("<p>*This page is returned by Web Server.</p>"); sbHtml.append("</body>"); sbHtml.append("</html>"); return sbHtml.toString(); } /** * Build error page for bad request * @param code, http cde: 400, 301, 200 * @param title, page title * @param msg, error message * @return, page text */ private String buildErrorPage(String code, String title, String msg) { StringBuilder sbHtml = new StringBuilder(); sbHtml.append("HTTP/1.1 " + code + " " + title + "\r\n\r\n"); sbHtml.append("<!DOCTYPE html>"); sbHtml.append("<html>"); sbHtml.append("<head>"); sbHtml.append("<title>" + code + " " + title + "</title>"); sbHtml.append("</head>"); sbHtml.append("<body>"); sbHtml.append("<h1>" + code + " " + title + "</h1>"); sbHtml.append("<p>" + msg + "</p>"); sbHtml.append("<hr>"); sbHtml.append("<p>*This page is returned by Web Server.</p>"); sbHtml.append("</body>"); sbHtml.append("</html>"); return sbHtml.toString(); } /** * Get file or directory list * @param filelist, original file/directory list * @param isfolder, flag indicates looking for file or directory list * @return, file/directory list */ private List<File> getFileByType(File[] filelist, boolean isfolder) { List<File> files = new ArrayList<File>(); if (filelist == null || filelist.length == 0) { return files; } for (int i = 0; i < filelist.length; i++) { if (filelist[i].isDirectory() && isfolder) { files.add(filelist[i]); } else if (filelist[i].isFile() && !isfolder) { files.add(filelist[i]); } } return files; } /** * Parse parameter from url to key value pair * @param url, url from client * @return, pair list */ private Map<String, String> parseUrlParams(String url) throws UnsupportedEncodingException { HashMap<String, String> mapParams = new HashMap<String, String>(); if (url.indexOf("?") < 0) { return mapParams; } url = url.substring(url.indexOf("?") + 1); String[] pairs = url.split("&"); for (String pair : pairs) { int index = pair.indexOf("="); mapParams.put(URLDecoder.decode(pair.substring(0, index), "UTF-8"), URLDecoder.decode(pair.substring(index + 1), "UTF-8")); } return mapParams; } /** * Get root path * @return, path of the current location */ private String getRootFolder() { String root = ""; try{ File f = new File("."); root = f.getCanonicalPath(); } catch(IOException ex){ ex.printStackTrace(); } return root; } /** * Convert date to specified format * @param lastmodified, long value represents date * @return, formatted date in string */ private String getFormattedDate(long lastmodified) { if (lastmodified < 0) { return ""; } Date lm = new Date(lastmodified); String lasmod = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(lm); return lasmod; } /** * Build relative link * @param current, current request * @param filename, file name * @return, formatted file name */ private String buildRelativeLink(String req, String filename) { if (req == null || req.equals("") || req.equals("/")) { return filename; } else { return req + "/" +filename; } } /** * Build image link for icons * @param current, current request * @param filename, file name * @return, formatted file name */ private String buildImageLink(String req, String filename) { if (req == null || req.equals("") || req.equals("/")) { return filename; } else { String imageLink = filename; for(int i = 0; i < req.length(); i++) { if (req.charAt(i) == '/') { // For each downstairs level, need a upstairs level path imageLink = "../" + imageLink; } } return imageLink; } } /** * Get file icon according to its extension * @param path, file path * @return, icon path */ private static String getFileImage(String path) { if (path == null || path.equals("") || path.lastIndexOf(".") < 0) { return "images/file.png"; } String extension = path.substring(path.lastIndexOf(".")); switch(extension) { case ".class": return "images/class.png"; case ".html": return "images/html.png"; case ".java": return "images/java.png"; case ".txt": return "images/text.png"; case ".xml": return "images/xml.png"; default: return "images/file.png"; } } /** * Get MIME type according to file extension * @param path, file path * @return, MIME type */ private static String getContentType(String path) { if (path == null || path.equals("") || path.lastIndexOf(".") < 0) { return "text/html"; } String extension = path.substring(path.lastIndexOf(".")); switch(extension) { case ".html": case ".htm": return "text/html"; case ".txt": return "text/plain"; case ".ico": return "image/x-icon .ico"; case ".wml": return "text/html"; //text/vnd.wap.wml default: return "text/plain"; } } /** * Parse string to integer, return null if unable to convert * @param text, string value * @return, integer value */ private Integer tryParse(String text) { try { return Integer.parseInt(text); } catch (NumberFormatException e) { return null; } } }
3. LogUtil
A helper class for logging. Log will be persisted to WebServerLogs.txt.
public class LogUtil { private static final String FILE_LOG = "WebServerLogs.txt"; private static List<String> logs = new LinkedList<String>(); private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * Write log to local storage list * @param log, the content of the log */ public static void write(String log) { write(log, true); } /** * Write log to local storage list * @param log, the content of the log * @param print, print to screen */ public static void write(String log, boolean print) { String message = sdf.format(new Date()) + " " + log; // Store new log logs.add(message); if(print) { // Print the log System.out.println(message); } } /** * Save logs to the specified file * @param append, true is append, false is override */ public static void save(boolean append) { try { if (logs!=null && logs.size()>0) { // Open the log FileWriter fileWriterLog = new FileWriter(FILE_LOG, append); // User BufferedWriter to add new line BufferedWriter bufferedWriterLog = new BufferedWriter(fileWriterLog); // Go through all the logs and write them to the file for (String str : logs) { // Write the current log bufferedWriterLog.write(str); // One log one line bufferedWriterLog.newLine(); } // Always close files. bufferedWriterLog.close(); } } catch(FileNotFoundException ex) { System.out.println("Unable to open file '" + FILE_LOG + "'"); } catch(IOException ex) { ex.printStackTrace(); } } /** * Clear log */ public static void clear() { logs.clear(); } }
4. Auxiliary Resources
Some images, and static files. The final project looks as follows.
5. Testing
Right click on ‘WebServer.java’-> Run As->Java Application. You will see the logs in console. The web server starts working.
Open web browser, access ‘http://localhost:2540/’. All of the files in the root directory are displayed.
Click on the folder ‘src’ and navigate to the deeper layers. The path in browser’s address bar changes accordingly, files in each directory are properly displayed.
Click on file ‘WebServer.java’, its content shows up.
Go back to the root folder, view content of txt file.
View html file.
Let’s test the CGI function. In the root directory, click ‘addnums.html’.
If numbers are correctly set, we can get the proper result.
If invalid input are passed to the server, it returns 500 error with specific error message.
Check the logs in WebServerLogs.txt. These logs are also available in the console of Eclipse.