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 đã 😆
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