Hướng dẫn về NanoHTTPD

1. Giới thiệu

NanoHTTPD là một máy chủ web mã nguồn mở, nhẹ, được viết bằng Java.

Trong hướng dẫn này, chúng tôi sẽ tạo một vài API REST để khám phá các tính năng của nó.

2. Thiết lập dự án

Hãy thêm phần phụ thuộc lõi NanoHTTPD vào pom.xml của chúng tôi :

 org.nanohttpd nanohttpd 2.3.1 

Để tạo một máy chủ đơn giản, chúng ta cần mở rộng NanoHTTPD và ghi đè phương thức phục vụ của nó :

public class App extends NanoHTTPD { public App() throws IOException { super(8080); start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); } public static void main(String[] args ) throws IOException { new App(); } @Override public Response serve(IHTTPSession session) { return newFixedLengthResponse("Hello world"); } }

Chúng tôi đã xác định cổng đang chạy của mình là 8080 và máy chủ hoạt động như một daemon (không có thời gian chờ đọc).

Khi chúng tôi khởi động ứng dụng, URL // localhost: 8080 / sẽ trả về thông báo Hello world . Chúng tôi đang sử dụng phương thức NanoHTTPD # newFixedLengthResponse như một cách thuận tiện để xây dựng đối tượng NanoHTTPD.Response .

Hãy thử dự án của chúng tôi với cURL:

> curl '//localhost:8080/' Hello world

3. REST API

Theo cách của các phương thức HTTP, NanoHTTPD cho phép GET, POST, PUT, DELETE, HEAD, TRACE và một số phương thức khác.

Nói một cách đơn giản, chúng ta có thể tìm thấy các động từ HTTP được hỗ trợ thông qua phương thức enum. Hãy xem điều này diễn ra như thế nào.

3.1. HTTP GET

Đầu tiên, chúng ta hãy xem xét GET. Ví dụ, giả sử rằng chúng tôi muốn trả lại nội dung chỉ khi ứng dụng nhận được yêu cầu GET.

Không giống như các vùng chứa Java Servlet, chúng tôi không có sẵn phương thức doGet - thay vào đó, chúng tôi chỉ kiểm tra giá trị thông qua getMethod :

@Override public Response serve(IHTTPSession session) { if (session.getMethod() == Method.GET) { String itemIdRequestParameter = session.getParameters().get("itemId").get(0); return newFixedLengthResponse("Requested itemId = " + itemIdRequestParameter); } return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "The requested resource does not exist"); }

Điều đó khá đơn giản, phải không? Hãy chạy thử nghiệm nhanh bằng cách uốn cong điểm cuối mới của chúng tôi và thấy rằng mụcId tham số yêu cầu được đọc chính xác:

> curl '//localhost:8080/?itemId=23Bk8' Requested itemId = 23Bk8

3.2. ĐĂNG HTTP

Trước đây, chúng tôi đã phản ứng với một GET và đọc một tham số từ URL.

Để đề cập đến hai phương thức HTTP phổ biến nhất, đã đến lúc chúng ta phải xử lý POST (và do đó đọc nội dung yêu cầu):

@Override public Response serve(IHTTPSession session) { if (session.getMethod() == Method.POST) { try { session.parseBody(new HashMap()); String requestBody = session.getQueryParameterString(); return newFixedLengthResponse("Request body = " + requestBody); } catch (IOException | ResponseException e) { // handle } } return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "The requested resource does not exist"); }
Lưu ý rằng trước đây khi chúng tôi yêu cầu phần thân yêu cầu, đầu tiên chúng tôi gọi phương thức parseBody . Đó là vì chúng tôi muốn tải phần thân yêu cầu để truy xuất sau này.

Chúng tôi sẽ bao gồm một phần thân trong lệnh cURL của chúng tôi :

> curl -X POST -d 'deliveryAddress=Washington nr 4&quantity=5''//localhost:8080/' Request body = deliveryAddress=Washington nr 4&quantity=5

Các phương thức HTTP còn lại có bản chất rất giống nhau, vì vậy chúng tôi sẽ bỏ qua những phương thức đó.

4. Chia sẻ tài nguyên đa nguồn gốc

Sử dụng CORS, chúng tôi cho phép giao tiếp giữa các miền. Trường hợp sử dụng phổ biến nhất là các cuộc gọi AJAX từ một miền khác. Cách tiếp cận đầu tiên mà chúng tôi có thể sử dụng là bật CORS cho tất cả các API của chúng tôi. Sử dụng đối số - -cors , chúng tôi sẽ cho phép truy cập vào tất cả các miền. Chúng tôi cũng có thể xác định miền nào chúng tôi cho phép với –cors = ”// dashboard.myApp.com //admin.myapp.com” . Cách tiếp cận thứ hai là kích hoạt CORS cho các API riêng lẻ. Hãy xem cách sử dụng addHeader để đạt được điều này:
@Override public Response serve(IHTTPSession session) { Response response = newFixedLengthResponse("Hello world"); response.addHeader("Access-Control-Allow-Origin", "*"); return response; }

Bây giờ khi chúng ta cURL , chúng ta sẽ lấy lại tiêu đề CORS của mình:

> curl -v '//localhost:8080' HTTP/1.1 200 OK Content-Type: text/html Date: Thu, 13 Jun 2019 03:58:14 GMT Access-Control-Allow-Origin: * Connection: keep-alive Content-Length: 11 Hello world

5. Tải lên tệp

NanoHTTPD có một phần phụ thuộc riêng cho việc tải lên tệp, vì vậy hãy thêm nó vào dự án của chúng tôi:

 org.nanohttpd nanohttpd-apache-fileupload 2.3.1   javax.servlet javax.servlet-api 4.0.1 provided 

Xin lưu ý rằng phụ thuộc servlet-api cũng cần thiết (nếu không, chúng tôi sẽ gặp lỗi biên dịch).

Những gì NanoHTTPD thể hiện là một lớp được gọi là NanoFileUpload :

@Override public Response serve(IHTTPSession session) { try { List files = new NanoFileUpload(new DiskFileItemFactory()).parseRequest(session); int uploadedCount = 0; for (FileItem file : files) { try { String fileName = file.getName(); byte[] fileContent = file.get(); Files.write(Paths.get(fileName), fileContent); uploadedCount++; } catch (Exception exception) { // handle } } return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, "Uploaded files " + uploadedCount + " out of " + files.size()); } catch (IOException | FileUploadException e) { throw new IllegalArgumentException("Could not handle files from API request", e); } return newFixedLengthResponse( Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "Error when uploading"); }

Này, chúng ta hãy thử nó:

> curl -F '[email protected]/pathToFile.txt' '//localhost:8080' Uploaded files: 1

6. Nhiều tuyến đường

Một nanolet giống như một servlet nhưng có cấu hình rất thấp. Chúng ta có thể sử dụng chúng để xác định nhiều tuyến đường được phục vụ bởi một máy chủ duy nhất (không giống như các ví dụ trước với một tuyến đường).

Đầu tiên, hãy thêm phụ thuộc bắt buộc cho nanolet :

 org.nanohttpd nanohttpd-nanolets 2.3.1 

Và bây giờ chúng ta sẽ mở rộng lớp chính của chúng ta bằng cách sử dụng RouterNanoHTTPD, xác định cổng đang chạy của chúng ta và để máy chủ chạy dưới dạng daemon.

The addMappings method is where we'll define our handlers:

public class MultipleRoutesExample extends RouterNanoHTTPD { public MultipleRoutesExample() throws IOException { super(8080); addMappings(); start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); } @Override public void addMappings() { // todo fill in the routes } }

The next step is to define our addMappings method. Let's define a few handlers.

The first one is an IndexHandler class to “/” path. This class comes with the NanoHTTPD library and returns by default a Hello World message. We can override the getText method when we want a different response:

addRoute("/", IndexHandler.class); // inside addMappings method

And to test our new route we can do:

> curl '//localhost:8080' 

Hello world!

Secondly, let's create a new UserHandler class which extends the existing DefaultHandler. The route for it will be /users. Here we played around with the text, MIME type, and the status code returned:

public static class UserHandler extends DefaultHandler { @Override public String getText() { return "UserA, UserB, UserC"; } @Override public String getMimeType() { return MIME_PLAINTEXT; } @Override public Response.IStatus getStatus() { return Response.Status.OK; } }

To call this route we'll issue a cURL command again:

> curl -X POST '//localhost:8080/users' UserA, UserB, UserC

Finally, we can explore the GeneralHandler with a new StoreHandler class. We modified the returned message to include the storeId section of the URL.

public static class StoreHandler extends GeneralHandler { @Override public Response get( UriResource uriResource, Map urlParams, IHTTPSession session) { return newFixedLengthResponse("Retrieving store for id = " + urlParams.get("storeId")); } }

Let's check our new API:

> curl '//localhost:8080/stores/123' Retrieving store for id = 123

7. HTTPS

In order to use the HTTPS, we'll need a certificate. Please refer to our article on SSL for more in-depth information.

We could use a service like Let's Encrypt or we can simply generate a self-signed certificate as follows:

> keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1 -validity 9999

Next, we'd copy this keystore.jks to a location on our classpath, like say the src/main/resources folder of a Maven project.

After that, we can reference it in a call to NanoHTTPD#makeSSLSocketFactory:

public class HttpsExample extends NanoHTTPD { public HttpsExample() throws IOException { super(8080); makeSecure(NanoHTTPD.makeSSLSocketFactory( "/keystore.jks", "password".toCharArray()), null); start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); } // main and serve methods }

And now we can try it out. Please notice the use of the —insecure parameter, because cURL won't be able to verify our self-signed certificate by default:

> curl --insecure '//localhost:8443' HTTPS call is a success

8. WebSockets

NanoHTTPD supports WebSockets.

Let's create the simplest implementation of a WebSocket. For this, we'll need to extend the NanoWSD class. We'll also need to add the NanoHTTPD dependency for WebSocket:

 org.nanohttpd nanohttpd-websocket 2.3.1 

For our implementation, we'll just reply with a simple text payload:

public class WsdExample extends NanoWSD { public WsdExample() throws IOException { super(8080); start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); } public static void main(String[] args) throws IOException { new WsdExample(); } @Override protected WebSocket openWebSocket(IHTTPSession ihttpSession) { return new WsdSocket(ihttpSession); } private static class WsdSocket extends WebSocket { public WsdSocket(IHTTPSession handshakeRequest) { super(handshakeRequest); } //override onOpen, onClose, onPong and onException methods @Override protected void onMessage(WebSocketFrame webSocketFrame) { try { send(webSocketFrame.getTextPayload() + " to you"); } catch (IOException e) { // handle } } } }

Instead of cURL this time, we'll use wscat:

> wscat -c localhost:8080 hello hello to you bye bye to you

9. Conclusion

Tóm lại, chúng tôi đã tạo một dự án sử dụng thư viện NanoHTTPD. Tiếp theo, chúng tôi đã xác định các API RESTful và khám phá thêm các chức năng liên quan đến HTTP. Cuối cùng, chúng tôi cũng đã triển khai một WebSocket.

Việc triển khai tất cả các đoạn mã này có sẵn trên GitHub.