DEV Community

Sadiul Hakim
Sadiul Hakim

Posted on

Building a static file server in Java with sockets

Building a static file server in Java with sockets involves several key steps. The provided code demonstrates a structured approach to this task by separating concerns into different utility classes. The core idea is to listen for incoming client connections, parse their HTTP requests to identify the requested file, and then send the file's content back as an HTTP response. If the file isn't found, a "404 Not Found" response is sent.

Let's break down the process step by step, explaining the role of each class.

Step 1: Setting up the Project Structure

First, you'll need a Java project with the following class files: Main.java, Server.java, FileUtil.java, RequestUtil.java, ResponseUtil.java, and SystemUtil.java.

Create a dedicated folder, usually named src/main/resources, and within it, create another folder named static. This static folder will serve as the root directory for all your static files (HTML, CSS, images, etc.).

Step 2: The Main Entry Point (Main.java)

The Main class is the entry point of the application. Its sole purpose is to start the server.

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws InterruptedException, IOException {
        // Starts the server on port 9090
        Server.run(9090);
    }
}
Enter fullscreen mode Exit fullscreen mode

This code calls the run method of the Server class, passing the port number 9090.


Step 3: The Server Logic (Server.java)

This is the heart of the application. The Server class handles creating the socket, listening for connections, and processing requests.

import org.live_server.util.FileUtil;
import org.live_server.util.RequestUtil;
import org.live_server.util.ResponseUtil;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Server {
    private static final Logger LOGGER = Logger.getLogger(Server.class.getName());
    private static final String rootFolderText = "static";

    public Server() {
    }

    public static void run(int port) {
        try (ServerSocket server = new ServerSocket(port)) {
            System.out.println("Running on port : " + port);

            // Get the URL of the 'static' folder from the resources
            URL resource = Server.class.getResource("/" + rootFolderText);
            assert resource != null;

            // Convert the URL to a Path object
            Path rootPath = Path.of(resource.toURI());
            if (!Files.exists(rootPath)) {
                throw new RuntimeException("Could not find root path (/static)!");
            }

            // Generate a map of all available file paths for quick lookup
            ConcurrentHashMap<String, Path> pathConcurrentHashMap = FileUtil.generatePath(rootPath, rootFolderText);

            // The server loop to accept incoming connections
            while (true) {
                try (Socket connection = server.accept()) {
                    // Process the request for each new connection
                    server(connection, pathConcurrentHashMap);
                } catch (Exception ex) {
                    LOGGER.log(Level.INFO, ex.getMessage());
                    break;
                }
            }
        } catch (Exception ex) {
            LOGGER.log(Level.INFO, ex.getMessage());
        }
    }

    private static void server(Socket connection, ConcurrentHashMap<String, Path> pathConcurrentHashMap) throws IOException {

        // Get the requested URL from the client's request
        String requestUrl = RequestUtil.requestedUrl(connection);
        // Look up the file path in the map
        Path resultPath = pathConcurrentHashMap.get(requestUrl);

        // Check if the path exists and is a file
        if (resultPath != null && requestUrl.contains(".")) {
            // If found, send the file as a response
            ResponseUtil.sendResponse(connection.getOutputStream(), pathConcurrentHashMap.get(requestUrl));
        } else {
            // If not found, send a 404 Not Found response
            ResponseUtil.sendResponse(connection.getOutputStream(), "404 Not Found".getBytes());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The run method creates a ServerSocket to listen on the specified port. It then locates the static resource folder and uses the FileUtil to create a ConcurrentHashMap that maps relative file paths (e.g., index.html) to their absolute Path objects. This allows for very fast file lookups.

The while(true) loop makes the server continuously listen for new client connections. When a connection is accepted, it's passed to the server method, which uses RequestUtil to parse the requested URL and ResponseUtil to send the appropriate response.


Step 4: Handling Files (FileUtil.java)

This utility class is responsible for scanning the static directory and creating a map of all available files.

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

public class FileUtil {

    private FileUtil() {
    }

    // This method returns a map of all file paths within the root directory
    public static ConcurrentHashMap<String, Path> generatePath(Path rootPath, String rootFolderText) {
        ConcurrentHashMap<String, Path> pathConcurrentHashMap = new ConcurrentHashMap<>();

        // Walk through the rootPath to find all files and folders
        try (Stream<Path> pathStream = Files.walk(rootPath)) {
            pathStream.toList().forEach(path -> {
                // Do not include the root folder itself
                if (path.toString().endsWith(rootFolderText)) {
                    return;
                }

                String actualPath = getActualPath(path, rootFolderText);
                if (!pathConcurrentHashMap.containsKey(actualPath)) {
                    // Only include files in the map (paths with a '.')
                    if (actualPath.contains(".")) {
                        pathConcurrentHashMap.put(actualPath, path);
                    }
                }
            });
            return pathConcurrentHashMap;
        } catch (Exception ex) {
            return new ConcurrentHashMap<>();
        }
    }

    // This method converts an absolute path to a relative URL-friendly path
    public static String getActualPath(Path path, String rootFolderText) {
        // Find the index of the root folder in the full path
        int rootFolderIndex = path.toString().indexOf(rootFolderText);

        // Get the path segment after the root folder
        String nextToRootFolder = path.toString()
                .substring(rootFolderIndex + (rootFolderText.length()) + 1);
        String[] actualFolderPath;

        // Split the path based on the operating system's file separator
        if (SystemUtil.OS.toLowerCase().contains("windows")) {
            actualFolderPath = nextToRootFolder.split("\\\\");
        } else {
            actualFolderPath = nextToRootFolder.split("/");
        }

        // Rebuild the path with forward slashes for URL consistency
        StringBuilder pathBuilder = new StringBuilder();
        for (String s : actualFolderPath) {
            pathBuilder.append("/")
                    .append(s);
        }

        // Remove the leading slash and return the path
        return pathBuilder.toString().replaceFirst("/", "");
    }
}
Enter fullscreen mode Exit fullscreen mode

The generatePath method uses Files.walk to recursively traverse the static directory. For each file found, the getActualPath method is used to create a relative path string (e.g., css/style.css) that can be used as a key in the map. This is a crucial step for correctly linking the client's request to the file's location on the disk.


Step 5: Parsing Requests (RequestUtil.java)

This class is dedicated to parsing the HTTP request from the client to get the requested URL.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class RequestUtil {
    private RequestUtil() {
    }

    // This method extracts the requested URL from the client's request
    public static String requestedUrl(Socket socket) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        // Read the first line of the HTTP request (e.g., "GET /index.html HTTP/1.1")
        String line = bufferedReader.readLine();
        if (line == null)
            return "";

        // Split the line by spaces
        String[] requestLineParts = line.split("\\s+");
        if (requestLineParts.length >= 2) {
            // The requested URL is the second part
            return requestLineParts[1].replaceFirst("/", "")
                    .replaceFirst("\\\\", "");
        }

        return "";
    }
}
Enter fullscreen mode Exit fullscreen mode

The requestedUrl method reads the first line of the HTTP request, which contains the method (e.g., GET), the path (e.g., /index.html), and the protocol version. It then extracts the path and returns it.


Step 6: Sending Responses (ResponseUtil.java)

This class handles sending the HTTP response back to the client.

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class ResponseUtil {

    private ResponseUtil(){}

    // Sends a byte array as an HTTP response
    public static void sendResponse(OutputStream outputStream, byte[] bytes) throws IOException {
        // Send the HTTP status line and headers
        outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
        outputStream.write("Cache-Control: no-cache, no-store, must-revalidate\r\n".getBytes());
        outputStream.write("Pragma: no-cache\r\n".getBytes());
        outputStream.write("Expires: 0\r\n".getBytes());
        outputStream.write("\r\n".getBytes());

        // Send the actual file content (the byte array)
        outputStream.write(bytes);
        outputStream.flush();
    }

    // Overloaded method to send a file from a given Path
    public static void sendResponse(OutputStream outputStream, Path path) throws IOException {
        if (Files.exists(path)) {
            // Read all bytes from the file and send them
            byte[] bytes = Files.readAllBytes(path);
            sendResponse(outputStream, bytes);
        } else {
            // If the file does not exist, send a 404 response
            sendResponse(outputStream, "404 Not Found!".getBytes());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The sendResponse method is overloaded to handle both byte[] and Path inputs. The most important part here is sending the HTTP headers first, followed by a blank line (\r\n\r\n), and then the actual content of the file. The headers are crucial for the browser to correctly interpret the response. The Cache-Control headers are included to prevent the browser from caching the files.


Step 7: System Utilities (SystemUtil.java)

This is a simple utility class to determine the operating system, which is used in FileUtil to handle different file path separators.

public class SystemUtil {
    private SystemUtil(){}

    // A constant to store the name of the operating system
    public static final String OS = System.getProperty("os.name");
}
Enter fullscreen mode Exit fullscreen mode

This class simply provides a constant OS variable for checking the operating system.


Step 8: Running the Server and Testing it

To run the server, simply execute the Main class. You'll see the message "Running on port : 9090" in the console.

To test the server, create a file named index.html inside your static directory. You can then open a web browser and navigate to http://localhost:9090/index.html. The server will process your request, find the index.html file, and send its content to the browser, which will then render the page. If you try to access a file that doesn't exist, you'll get a "404 Not Found" message.

Top comments (0)