+4

Part 1: Tự tay build lại một Web server framework cho Java

Chào người anh em 😊

Năm mới Chúc người anh em một năm mới thành công, vạn sự như ý, anh em nào đang học code thì học lẹ lẹ để còn chiến kiếm tiền nữa nhé anh em 😊 Năm mới có cành Mai cho nó không khí cái đã 😆 image.png

Sau thời gian vắng bóng hôm nay tui lại ngôi lai lên viết lại vài dòng bữa giờ mà tui build cái bánh xe (The wheel) trong chuỗi bài ngồi rãnh quá reinvent the wheel 😂

Đó chính là một cái Web server framework tên là CabinJv với concept mình dựa trên Expressjs và mình custom lại một chút cho thân thuộc với anh em Java, và có thể giành cho anh em anti Spring Boot vì nó quá cồng kềnh + anh em anti Servlet mà code trên Jetty hay gì đó. Thôi đi vào nội dung chính vấn đề luôn nhé!

Start

Giờ mình thử start một web server bằng Cabin như nào nhé:

import com.cabin.express.router.Router;
import com.cabin.express.server.CabinServer;
import com.cabin.express.server.ServerBuilder;

import java.io.IOException;

public class HServerSample {
  public static void main(String[] args) throws IOException {
    CabinServer server = new ServerBuilder().setPort(8080).build();
    Router router = new Router();
    router.get("/", (req, res) -> {
      res.writeBody("Hello World");
      res.send();
    });
    server.use(router);
    server.start();
    System.err.println("Server started at http://localhost:8080");
  }
}

Bump vậy là anh em đã start xong một web server để hello world rồi đó, quá đơn giản đúng không nào thôi anh em mình đi vào xem nó được build như thế nào nhé!

CabinJv là gì

Đây là một web framework mà mình gọi là "Simple and lightweight Web server framework for Java" với Cabin thì mình build lại từ đầu mọi thứ hết hiện tại là không dựa trên một framework có sẳn nào, mình dùng một gói mặc định của Java đó là Java NIO (NIO là viết tắt của New Input/Output). Và duy nhất một thư viện để mapper đó là Gson.

Vậy thì Cabin gồm những package nào:

  • http
  • middleware
  • router
  • server
  • worker

Sau đây chúng ta đi vào từng gói package cụ thể nhé anh em.

Cabin package

http

Trong gói http thì bao gồm Request/Response thì đây là 2 class mình wrapper một request/response của một client tới server thì có có đầy đủ các thành phần cơ bản cần phải có của một request/response ví dụ như: Header, Body, Cookie, Param, Query,...

Đây là định nghĩa Request của Cabin Framework:

public class Request {
    private String method;
    private String path;
    private String body;
    private Map<String, Object> bodyAsJson = new HashMap<>();
    private Map<String, String> queryParams = new HashMap<>();
    private Map<String, String> pathParams = new HashMap<>();
    private Map<String, String> headers = new HashMap<>();

    private static final Gson gson = new Gson();


    public Request(InputStream inputStream) throws Exception {
        parseRequest(inputStream);
        parseBodyAsJson();
    }
...

Đây là định nghĩa Response của Cabin Framework

public class Response {
    private int statusCode = 200;
    private Map<String, String> headers = new HashMap<>();
    private Map<String, String> cookies = new HashMap<>();
    private StringBuilder body = new StringBuilder();
    private final SocketChannel clientChannel;

    private static final Gson gson = new Gson();

    private static final String DEFAULT_DOMAIN = "";
    private static final String DEFAULT_PATH = "/";
    private static final String DEFAULT_EXPIRES = "";
    private static final boolean DEFAULT_HTTP_ONLY = false;
    private static final boolean DEFAULT_SECURE = false;

    public Response(SocketChannel clientChannel) {
        this.clientChannel = clientChannel;
    ...

Đơn giản thế thôi là anh em có đầy đủ các thông tin cơ bản của một Request/Response rồi, thì hiện tại chưa có session và một số thứ linh tính khác nhưng mình sẽ cập nhập trong thời gian tới,... 😁


Middleware

Middleware thì dùng để xử lý các tác vụ trước khi request đến với với handler, hiện tại nó là một chuỗi các Middleware anh em có thể sử dụng nó như một list theo thứ tự xử lý request, dưới đây là phần định nghĩa của Middleware trong Cabin Framework

Định nghĩa

public class MiddlewareChain {
    private final Iterator<Middleware> middlewareIterator;
    private final Handler routeHandler;

    /**
     * Create a new middleware chain
     *
     * @param middleware   the list of middleware to apply
     * @param routeHandler the final route handler
     */
    public MiddlewareChain(List<Middleware> middleware, Handler routeHandler) {
        this.middlewareIterator = middleware.iterator();
        this.routeHandler = routeHandler;
    }

    /**
     * Processes the next middleware in the chain or the final route handler if no middleware is left.
     *
     * @param request  the request object
     * @param response the response object
     * @throws IOException if an I/O error occurs during request processing
     */
    public void next(Request request, Response response) throws IOException {
        if (middlewareIterator.hasNext()) {
            Middleware current = middlewareIterator.next();
            current.apply(request, response, this);
        } else if (routeHandler != null) {
            routeHandler.handle(request, response);
        }
    }
}

Cách sử dụng

Ví dụ anh em có một AuthMiddleware để xác thực đơn giản như thế này thôi.

public class AuthMiddleware {
    public static final AuthMiddleware Instance = new AuthMiddleware();

    private AuthMiddleware() {
    }

    // Middleware to check if the user is authenticated
    public void checkAuth(Request req, Response res, MiddlewareChain next) throws IOException {
        System.err.println("Checking auth..., time: " + System.currentTimeMillis());
        String token = req.getHeader("Authorization");
        if (token == null || !token.equals("Bearer token")) {
            res.setStatusCode(401);
            res.writeBody("Unauthorized");
            res.send();
        }
        next.next(req, res);
    }
}

Và em anh có thể sử dụng cho Router như sau:

Router router = new Router();

router.use(AuthMiddleware.Instance::checkAuth);

Hoặc anh em có thể sử dụng cho server thì đây áp cho toàn bộ routers:

CabinServer server = new ServerBuilder().setMaxPoolSize(200).setMaxQueueCapacity(1000).build();
server.use(AuthMiddleware.Instance::checkAuth);

Cũng chỉ đơn giản thế thôi, do đang trong quá trình dev mình chỉ test happy case thôi, nên có thể trong thực tế có thể gặp một số bug, nếu anh em có dùng thì cứ để lại issue mình sẽ hot fix nó.


Router

Phần Router này xử lý các endpoit đơn giản như api/v1/users hoặc các PathParameter như: api/v1/users/${userId}/info

Định nghĩa

Đây là phần định nghĩ của Router và các sử dụng

public class Router {

    private static final String name = "Router";
    private String prefix = "";
    private final Map<String, Map<Pattern, Handler>> methodRoutes = new HashMap<>();
    private final List<Middleware> middlewares = new ArrayList<>();

    private void addRoute(String method, String path, Handler handler) {
        method = method.toUpperCase();
        methodRoutes.putIfAbsent(method, new HashMap<>());
        String regexPath = path.replaceAll(":(\\w+)", "(?<$1>[^/]+)");
        Pattern pattern = Pattern.compile("^" + regexPath + "$");
        methodRoutes.get(method).put(pattern, handler);
    }
    ...

Cách sử dụng

Sử dụng thì anh em có thể sử dụng đơn giản như thế này thôi: AppRouter.java

public class AppRouter {

    public static final AppRouter Instance = new AppRouter();
    public static final String API_PREFIX = "/api/";

    public Router registerRoutes() {
        Router router = new Router();

        router.setPrefix(API_PREFIX);

        router.get("/hello", AppHandler.Instance::hello);
        router.post("/users", AppHandler.Instance::addUser);
        router.get("/users", AppHandler.Instance::getSliceUsers);

        router.post("/products", AppHandler.Instance::addProduct);
        router.get("/products", AppHandler.Instance::getSliceProducts);

        System.err.println("Endpoints registered: " + router.getEndpoint());

        return router;
    }
}

Anh em có thể định nghĩa các method trong trong AppHandler tương tự với partern như sau:

public class AppHandler {
    public static final AppHandler Instance = new AppHandler();

    private AppHandler() {
    }

    public void hello(Request req, Response resp) {
        try {
            int appId = req.getQueryParamAsInt("appId", 0);
            long userId = req.getQueryParamAsLong("userId", 0L);
            resp.writeBody("Hello, User ID: " + userId + ", App ID: " + appId);
            resp.send();
        } catch (Exception e) {
            CabinLogger.error(e.getMessage(), e);
        }
    }

Server

Đây được coi là trái tim của Cabin Framework, hiện tại trái tim này khá đơn giản nhưng không kém phần yếu đuối nhé anh em, hiện tại nó gồm 3 thành phần chính:

BufferPool

Buffer pool dùng để đọc data của request, hiện tại mình dùng pool để tiết kiệm bộ nhớ, vì không thì mỗi request thì có thể init một Buffer mới nên khá tốn tài nguyên khi số lượng request đồng thời lớn.

public class BufferPool {
    private final Deque<ByteBuffer> buffers = new ArrayDeque<>();
    private final int bufferSize;
    private final int maxPoolSize;

    BufferPool(int bufferSize, int maxPoolSize) {
        this.bufferSize = bufferSize;
        this.maxPoolSize = maxPoolSize;
    }

    synchronized ByteBuffer getBuffer() {
        if (buffers.isEmpty()) {
            return ByteBuffer.allocate(bufferSize);
        }
        return buffers.pollFirst();
    }
    ...

CabinWorker

CabinWorker xử lý các tác vụ được phân phối từ event loop

public class CabinWorkerPool {
    private final ThreadPoolExecutor threadPoolExecutor;

    /*
     * Creates a new worker pool with the specified pool size, maximum pool size, and queue capacity.
     * @param poolSize the number of threads to keep in the pool, even if they are idle
     * @param maxPoolSize the maximum number of threads to allow in the pool
     * @param queueCapacity the queue capacity
     * @throws IllegalArgumentException if the pool size is less than 1, the maximum pool size is less than the pool size, or the queue capacity is negative
     *
     */
    public CabinWorkerPool(int poolSize, int maxPoolSize, int queueCapacity) {
        int corePoolSize = Math.max(1, poolSize);
        int maximumPoolSize = Math.max(corePoolSize, maxPoolSize);
        int maximumQueueCapacity = Math.max(0, queueCapacity);
        threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(maximumQueueCapacity), new ThreadPoolExecutor.CallerRunsPolicy());
    }

    /**
     * Submits a task for execution.
     *
     * @param task the task to execute
     * @throws IllegalStateException if the pool is stopped
     */
    public void submitTask(Runnable task, Consumer<Runnable> onBackpressure) {
        if (threadPoolExecutor.isShutdown()) {
            throw new IllegalStateException("The pool is stopped");
        }

        if (threadPoolExecutor.getQueue().remainingCapacity() == 0) {
            if (onBackpressure != null) {
                onBackpressure.accept(task);
            } else {
                throw new RejectedExecutionException("The queue is full");
            }
        } else {
            threadPoolExecutor.submit(task);
        }
    }

    public void submitTask(Runnable task) {
        if (threadPoolExecutor.isShutdown()) {
            throw new IllegalStateException("The pool is stopped");
        }
        threadPoolExecutor.submit(task);
    }
    ...

CabinServer

CabinServer nơi cấu hình các Router, Middleware, Logger,... Anh em để ý mình hiện tại server mình có 2 CabinWorker một worker dùng để xử lý Read, một worker còn lại xử lý Write để xử lý các thao tác bất đồng bộ mà không làm event loop của mình phải chờ.

public class CabinServer {
    private Selector selector;
    private final List<Router> routers = new ArrayList<>();
    private final List<Middleware> globalMiddlewares = new ArrayList<>();
    private final Map<SocketChannel, Long> connectionLastActive = new ConcurrentHashMap<>();

    // Resource logging task
    private ScheduledFuture<?> resourceLoggingTask;
    private ScheduledFuture<?> idleConnectionTask;
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    // Server configuration
    private final int port;
    private final CabinWorkerPool workerPool;
    private final CabinWorkerPool readWorkerPool;
    private final CabinWorkerPool writeWorkerPool;

    private final long connectionTimeoutMillis; // Timeout threshold (30 seconds)
    private final long idleConnectionTimeoutMillis; // Idle connection timeout threshold (60 seconds)
    ...

Cách sử dụng

Dưới dây là tổng hợp các cấu hình cho một http server đơn giản sử dụng Cabin Framework

public class CabinDemoServer {
    public static void main(String[] args) throws IOException {
        boolean enableDebug = args.length > 0 && args[0].equalsIgnoreCase("--debug");
        CabinLogger.setDebug(enableDebug);

        CabinLogger.info("Starting CabinJ Framework...");
        try {
            CabinServer server = new ServerBuilder().setMaxPoolSize(200).setMaxQueueCapacity(1000).build();
            Thread serverThread = new Thread(() -> {
                try {
                    server.use(AuthMiddleware.Instance::checkAuth);
                    server.use(AppRouter.Instance.registerRoutes());
                    server.use(ApiRouter.Instance.registerRoutes());
                    server.start();
                } catch (Exception e) {
                    CabinLogger.error("Failed to start the server", e);
                }
            });
            serverThread.start();
        } catch (Exception e) {
            CabinLogger.error("Failed to start the server", e);
        }

    }
}

Benchmark

Với cấu hình hiện tại mình thấy server của mình tải khá cao, mình có thử benchmark bằng K6 và mình deploy một sample server trên VPS bằng docker với cấu hình VPS là 4 core, 4GB RAM và cấu hình Docker như sau:

services:
  cabin-server:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - JAVA_OPTS=-Xmx512m
    command: ["java", "-jar", "CabinJ-1.0-SNAPSHOT.jar"]
    volumes:
      - gradle-cache:/root/.gradle
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 1024M
volumes:
  gradle-cache:

Cấu hình server:

public class HServer {
    private static final Logger logger = LoggerFactory.getLogger(HServer.class);

    public static void main(String[] args) throws IOException {
        CabinServer server = new ServerBuilder()
                .setPort(8080)
                .enableLogMetrics(true)
                .build();

        Router router = new Router();
        router.get("/", (req, res) -> {
            JsonObject json = new JsonObject();
            json.addProperty("message", "Hello, World!");
            res.send(json);
        });
        server.use(router);
        server.start();
        logger.info("Server started at http://localhost:8080");
    }
}

Payload test

Dưới đây mình sử dụng = máy Thinkpad T14 Gen 4, 13th Gen Intel® Core™ i5-1345U × 12 16GB RAM để chạy k6 script:

k6 run --iterations 100000 --vus 1000 index.js

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  ()  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: index.js
        output: -

     scenarios: (100.00%) 1 scenario, 1000 max VUs, 10m30s max duration (incl. graceful stop):
              * default: 100000 iterations shared among 1000 VUs (maxDuration: 10m0s, gracefulStop: 30s)


     data_received..................: 8.9 MB 469 kB/s
     data_sent......................: 8.5 MB 448 kB/s
     http_req_blocked...............: avg=586.47µs min=529ns   med=5.78µs   max=1.05s   p(90)=9.87µs   p(95)=12.11µs 
     http_req_connecting............: avg=574.35µs min=0s      med=0s       max=1.05s   p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=164.62ms min=54.62ms med=127.78ms max=3.68s   p(90)=233.1ms  p(95)=409.35ms
       { expected_response:true }...: avg=164.62ms min=54.62ms med=127.78ms max=3.68s   p(90)=233.1ms  p(95)=409.35ms
     http_req_failed................: 0.00%  0 out of 100000
     http_req_receiving.............: avg=103.99ms min=17.13µs med=81.37ms  max=2.5s    p(90)=139.27ms p(95)=223.89ms
     http_req_sending...............: avg=38.98µs  min=1.75µs  med=16.57µs  max=22.94ms p(90)=32.23µs  p(95)=70.82µs 
     http_req_tls_handshaking.......: avg=0s       min=0s      med=0s       max=0s      p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=60.59ms  min=23.57ms med=43.5ms   max=3.45s   p(90)=79.3ms   p(95)=126.59ms
     http_reqs......................: 100000 5274.064345/s
     iteration_duration.............: avg=165.33ms min=54.68ms med=128.16ms max=3.68s   p(90)=233.44ms p(95)=409.66ms
     iterations.....................: 100000 5274.064345/s
     vus............................: 1      min=1           max=1000
     vus_max........................: 1000   min=1000        max=1000

Kết luận

Trên đây là một Server đơn giản nhầm mục đích nghiên cứu cũng như pet project của mình anh em nếu hứng thú có thể xem chi tiết code ở github của mình, Cảm ơn người anh em đã đọc bài viết trong hàng vạn bài viết hay ho ngoài kia. Cảm ơn người anh em 😊😊😊

Trà Vinh - 11:58 Sáng Mùng 1 Tết Ất Tỵ 2025


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí