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);
}
}
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());
}
}
}
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("/", "");
}
}
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 "";
}
}
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());
}
}
}
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");
}
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)