0
0
Lập trình
Hưng Nguyễn Xuân 1
Hưng Nguyễn Xuân 1xuanhungptithcm

Xây dựng máy chủ tệp tĩnh trong Java với Socket

Đăng vào 1 tuần trước

• 14 phút đọc

Giới thiệu

Xây dựng một máy chủ tệp tĩnh bằng Java với socket là một nhiệm vụ thú vị và bổ ích, giúp bạn hiểu rõ hơn về cách thức hoạt động của giao thức HTTP và lập trình mạng. Trong bài viết này, chúng ta sẽ đi qua từng bước để xây dựng một máy chủ tệp tĩnh đơn giản, giúp phục vụ các tệp HTML, CSS và hình ảnh cho trình duyệt.

Nội dung chính

Cấu trúc dự án

Để bắt đầu, bạn cần tạo một dự án Java với các tệp lớp sau: Main.java, Server.java, FileUtil.java, RequestUtil.java, ResponseUtil.java, và SystemUtil.java.

Tạo một thư mục riêng biệt, thường được gọi là src/main/resources, và bên trong đó, tạo một thư mục khác tên là static. Thư mục static này sẽ phục vụ như là thư mục gốc cho tất cả các tệp tĩnh của bạn (HTML, CSS, hình ảnh, v.v.).

Điểm vào chính (Main.java)

Lớp Main là điểm vào chính của ứng dụng. Mục đích duy nhất của nó là khởi động máy chủ.

java Copy
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws InterruptedException, IOException {
        // Khởi động máy chủ trên cổng 9090
        Server.run(9090);
    }
}

Mã này gọi phương thức run của lớp Server, truyền vào số cổng 9090.

Logic máy chủ (Server.java)

Đây là trái tim của ứng dụng. Lớp Server xử lý việc tạo socket, lắng nghe kết nối và xử lý yêu cầu.

java Copy
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("Chạy trên cổng : " + port);

            // Lấy URL của thư mục 'static' từ tài nguyên
            URL resource = Server.class.getResource("/" + rootFolderText);
            assert resource != null;

            // Chuyển đổi URL thành đối tượng Path
            Path rootPath = Path.of(resource.toURI());
            if (!Files.exists(rootPath)) {
                throw new RuntimeException("Không tìm thấy đường dẫn gốc (/static)!");
            }

            // Tạo bản đồ tất cả các đường dẫn tệp có sẵn để tìm kiếm nhanh
            ConcurrentHashMap<String, Path> pathConcurrentHashMap = FileUtil.generatePath(rootPath, rootFolderText);

            // Vòng lặp máy chủ để chấp nhận kết nối đến
            while (true) {
                try (Socket connection = server.accept()) {
                    // Xử lý yêu cầu cho mỗi kết nối mới
                    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 {

        // Lấy URL yêu cầu từ yêu cầu của khách hàng
        String requestUrl = RequestUtil.requestedUrl(connection);
        // Tìm kiếm đường dẫn tệp trong bản đồ
        Path resultPath = pathConcurrentHashMap.get(requestUrl);

        // Kiểm tra xem đường dẫn có tồn tại và là tệp không
        if (resultPath != null && requestUrl.contains(".")) {
            // Nếu tìm thấy, gửi tệp như một phản hồi
            ResponseUtil.sendResponse(connection.getOutputStream(), pathConcurrentHashMap.get(requestUrl));
        } else {
            // Nếu không tìm thấy, gửi phản hồi 404 Not Found
            ResponseUtil.sendResponse(connection.getOutputStream(), "404 Not Found".getBytes());
        }
    }
}

Phương thức run tạo một ServerSocket để lắng nghe trên cổng được chỉ định. Sau đó, nó xác định thư mục static và sử dụng FileUtil để tạo một ConcurrentHashMap ánh xạ các đường dẫn tệp tương đối (ví dụ: index.html) đến các đối tượng Path tuyệt đối của chúng. Điều này cho phép tìm kiếm tệp rất nhanh.

Vòng lặp while(true) làm cho máy chủ liên tục lắng nghe các kết nối mới từ khách hàng. Khi một kết nối được chấp nhận, nó được chuyển đến phương thức server, phương thức này sử dụng RequestUtil để phân tích URL được yêu cầu và ResponseUtil để gửi phản hồi phù hợp.

Xử lý tệp (FileUtil.java)

Lớp tiện ích này chịu trách nhiệm quét thư mục static và tạo một bản đồ tất cả các tệp có sẵn.

java Copy
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() {
    }

    // Phương thức này trả về bản đồ tất cả các đường dẫn tệp trong thư mục gốc
    public static ConcurrentHashMap<String, Path> generatePath(Path rootPath, String rootFolderText) {
        ConcurrentHashMap<String, Path> pathConcurrentHashMap = new ConcurrentHashMap<>();

        // Đi qua rootPath để tìm tất cả các tệp và thư mục
        try (Stream<Path> pathStream = Files.walk(rootPath)) {
            pathStream.toList().forEach(path -> {
                // Không bao gồm thư mục gốc
                if (path.toString().endsWith(rootFolderText)) {
                    return;
                }

                String actualPath = getActualPath(path, rootFolderText);
                if (!pathConcurrentHashMap.containsKey(actualPath)) {
                    // Chỉ bao gồm các tệp trong bản đồ (đường dẫn có ".")
                    if (actualPath.contains(".")) {
                        pathConcurrentHashMap.put(actualPath, path);
                    }
                }
            });
            return pathConcurrentHashMap;
        } catch (Exception ex) {
            return new ConcurrentHashMap<>();
        }
    }

    // Phương thức này chuyển đổi một đường dẫn tuyệt đối thành đường dẫn tương đối thân thiện với URL
    public static String getActualPath(Path path, String rootFolderText) {
        // Tìm chỉ số của thư mục gốc trong đường dẫn đầy đủ
        int rootFolderIndex = path.toString().indexOf(rootFolderText);

        // Lấy phần đường dẫn sau thư mục gốc
        String nextToRootFolder = path.toString()
                .substring(rootFolderIndex + (rootFolderText.length()) + 1);
        String[] actualFolderPath;

        // Tách đường dẫn dựa trên dấu phân cách tệp của hệ điều hành
        if (SystemUtil.OS.toLowerCase().contains("windows")) {
            actualFolderPath = nextToRootFolder.split("\\\\");
        } else {
            actualFolderPath = nextToRootFolder.split("/");
        }

        // Xây dựng lại đường dẫn với dấu gạch chéo phía trước để đảm bảo tính nhất quán URL
        StringBuilder pathBuilder = new StringBuilder();
        for (String s : actualFolderPath) {
            pathBuilder.append("/")
                    .append(s);
        }

        // Xóa dấu gạch chéo đầu tiên và trả về đường dẫn
        return pathBuilder.toString().replaceFirst("/", "");
    }
}

Phương thức generatePath sử dụng Files.walk để duyệt đệ quy thư mục static. Đối với mỗi tệp được tìm thấy, phương thức getActualPath được sử dụng để tạo một chuỗi đường dẫn tương đối (ví dụ: css/style.css) có thể được sử dụng làm khóa trong bản đồ. Đây là bước quan trọng để liên kết yêu cầu của khách hàng với vị trí tệp trên đĩa.

Phân tích yêu cầu (RequestUtil.java)

Lớp này chuyên biệt cho việc phân tích yêu cầu HTTP từ khách hàng để lấy URL được yêu cầu.

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

public class RequestUtil {
    private RequestUtil() {
    }

    // Phương thức này trích xuất URL được yêu cầu từ yêu cầu của khách hàng
    public static String requestedUrl(Socket socket) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        // Đọc dòng đầu tiên của yêu cầu HTTP (ví dụ: "GET /index.html HTTP/1.1")
        String line = bufferedReader.readLine();
        if (line == null)
            return "";

        // Tách dòng theo khoảng trắng
        String[] requestLineParts = line.split("\\s+");
        if (requestLineParts.length >= 2) {
            // URL được yêu cầu là phần thứ hai
            return requestLineParts[1].replaceFirst("/", "")
                    .replaceFirst("\\\\", "");
        }

        return "";
    }
}

Phương thức requestedUrl đọc dòng đầu tiên của yêu cầu HTTP, chứa phương thức (ví dụ: GET), đường dẫn (ví dụ: /index.html) và phiên bản giao thức. Sau đó, nó trích xuất đường dẫn và trả về nó.

Gửi phản hồi (ResponseUtil.java)

Lớp này xử lý việc gửi phản hồi HTTP trở lại cho khách hàng.

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

public class ResponseUtil {

    private ResponseUtil(){}

    // Gửi một mảng byte như một phản hồi HTTP
    public static void sendResponse(OutputStream outputStream, byte[] bytes) throws IOException {
        // Gửi dòng trạng thái HTTP và tiêu đề
        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());

        // Gửi nội dung tệp thực tế (mảng byte)
        outputStream.write(bytes);
        outputStream.flush();
    }

    // Phương thức quá tải để gửi tệp từ một Path cho trước
    public static void sendResponse(OutputStream outputStream, Path path) throws IOException {
        if (Files.exists(path)) {
            // Đọc tất cả byte từ tệp và gửi chúng
            byte[] bytes = Files.readAllBytes(path);
            sendResponse(outputStream, bytes);
        } else {
            // Nếu tệp không tồn tại, gửi phản hồi 404
            sendResponse(outputStream, "404 Not Found!".getBytes());
        }
    }
}

Phương thức sendResponse được quá tải để xử lý cả byte[]Path. Phần quan trọng nhất ở đây là gửi các tiêu đề HTTP trước, theo sau là một dòng trống (\r\n\r\n), và sau đó là nội dung thực tế của tệp. Các tiêu đề rất quan trọng để trình duyệt có thể hiểu đúng phản hồi. Các tiêu đề Cache-Control được bao gồm để ngăn trình duyệt lưu cache các tệp.

Tiện ích hệ thống (SystemUtil.java)

Đây là một lớp tiện ích đơn giản để xác định hệ điều hành, được sử dụng trong FileUtil để xử lý các dấu phân cách đường dẫn tệp khác nhau.

java Copy
public class SystemUtil {
    private SystemUtil(){}

    // Một hằng số để lưu trữ tên của hệ điều hành
    public static final String OS = System.getProperty("os.name");
}

Lớp này đơn giản chỉ cung cấp một biến hằng OS để kiểm tra hệ điều hành.

Chạy máy chủ và kiểm tra

Để chạy máy chủ, chỉ cần thực hiện lớp Main. Bạn sẽ thấy thông báo "Chạy trên cổng : 9090" trong console.

Để kiểm tra máy chủ, hãy tạo một tệp có tên index.html trong thư mục static. Sau đó, bạn có thể mở trình duyệt web và điều hướng đến http://localhost:9090/index.html. Máy chủ sẽ xử lý yêu cầu của bạn, tìm tệp index.html, và gửi nội dung của nó đến trình duyệt, sau đó trình duyệt sẽ hiển thị trang. Nếu bạn cố gắng truy cập một tệp không tồn tại, bạn sẽ nhận được thông báo "404 Not Found".

Thực tiễn tốt nhất

  • Sử dụng thư viện: Xem xét việc sử dụng các thư viện như Apache Commons IO để xử lý tệp dễ dàng hơn.
  • Quản lý ngoại lệ: Đảm bảo quản lý ngoại lệ tốt hơn để xử lý các lỗi trong quá trình yêu cầu.
  • Bảo mật: Đảm bảo rằng máy chủ của bạn không cho phép truy cập đến các tệp nhạy cảm.

Các vấn đề thường gặp

  • Lỗi 404: Nếu bạn nhận được lỗi 404, hãy kiểm tra xem tệp có tồn tại trong thư mục static không.
  • Lỗi kết nối: Nếu không thể kết nối đến máy chủ, hãy đảm bảo rằng cổng 9090 không bị chặn bởi tường lửa.

Mẹo hiệu suất

  • Cache: Cân nhắc sử dụng cache để tăng tốc độ truy cập tệp tĩnh.
  • Nén tệp: Sử dụng nén tệp để giảm kích thước dữ liệu truyền tải.

Kết luận

Xây dựng một máy chủ tệp tĩnh bằng Java không chỉ giúp bạn hiểu rõ hơn về lập trình mạng mà còn giúp bạn phát triển các kỹ năng cần thiết cho việc xây dựng ứng dụng web. Hãy thử nghiệm với mã nguồn, mở rộng các tính năng và chia sẻ kinh nghiệm của bạn với cộng đồng lập trình viên. Nếu bạn có bất kỳ câu hỏi nào, hãy để lại câu hỏi của bạn dưới bài viết này!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào