1. 簡介
NanoHTTPD 是一個開源、輕量級的 Java 編寫的 Web 服務器。
在本教程中,我們將創建幾個 REST API 以探索其功能。
2. 項目設置
讓我們添加 NanoHTTPD 核心依賴項 到我們的pom.xml:
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd</artifactId>
<version>2.3.1</version>
</dependency>
為了創建一個簡單的服務器,我們需要擴展 NanoHTTPD 並覆蓋其 serve 方法:
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");
}
}
我們定義了運行端口為 8080,服務器作為守護進程工作(無讀取超時)。
一旦我們啓動應用程序,URL http://localhost:8080/ 將返回 Hello world 消息。我們使用 NanoHTTPD#newFixedLengthResponse 方法作為構建 NanoHTTPD.Response 對象的便捷方式。
讓我們用 cURL 嘗試我們的項目:
> curl 'http://localhost:8080/'
Hello world
3. REST API
按照 HTTP 方法的規範,NanoHTTPD 支持 GET、POST、PUT、DELETE、HEAD、TRACE 等方法。
簡單來説,我們可以通過 method 枚舉來查找支持的 HTTP 動詞。下面我們來看看它們是如何發揮作用的。
3.1. HTTP GET
首先,讓我們來查看一下 GET。例如,我們想要在收到 GET 請求時才返回內容。
與 Java Servlet 容器不同,我們沒有 doGet 方法可用——我們只是通過 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");
}
這非常簡單,對吧?讓我們通過 curl 測試我們的新端點,看看請求參數 itemId 是否被正確讀取:
> curl 'http://localhost:8080/?itemId=23Bk8'
Requested itemId = 23Bk8
3.2. HTTP POST
我們之前對 GET 請求進行了響應,並從 URL 中讀取了一個參數。
為了處理兩個最流行的 HTTP 方法,現在是時候處理 POST (並讀取請求體) 的時候:
@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");
}
我們將包含一個請求體在我們的 cURL 命令中:
> curl -X POST -d 'deliveryAddress=Washington nr 4&quantity=5''http://localhost:8080/'
Request body = deliveryAddress=Washington nr 4&quantity=5
其餘的 HTTP 方法本質上非常相似,因此我們將會跳過它們。
4. 跨域資源共享
@Override
public Response serve(IHTTPSession session) {
Response response = newFixedLengthResponse("Hello world");
response.addHeader("Access-Control-Allow-Origin", "*");
return response;
}
現在,當我們使用 cURL 時,我們將獲得我們的 CORS 標題:
> curl -v 'http://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. 文件上傳
NanoHTTPD 具有單獨的 依賴項用於文件上傳,因此我們將其添加到我們的項目:
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd-apache-fileupload</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</code>
請注意, servlet-api 依賴項 也需要 (否則我們將收到編譯錯誤)。
NanoHTTPD 暴露的是一個名為 NanoFileUpload 的類:
@Override
public Response serve(IHTTPSession session) {
try {
List<FileItem> 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");
}
嘿,我們來試試:
> curl -F 'filename=@/pathToFile.txt' 'http://localhost:8080'
Uploaded files: 1
6. 多個路由
首先,讓我們添加所需的 對 nanolets 的依賴:
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd-nanolets</artifactId>
<version>2.3.1</version>
</dependency>
現在我們將使用 RouterNanoHTTPD 類擴展我們的主類,定義我們的運行端口並讓服務器作為守護進程運行。
addMappings 方法是我們定義處理程序的地點:
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
}
}
接下來,我們定義 addMappings 方法。讓我們定義幾個處理程序。
第一個是 IndexHandler 類,用於“/”路徑。此類隨 NanoHTTPD 庫一起提供,並且默認返回一個 Hello World 消息。我們可以通過在需要時覆蓋 getText 方法來修改響應。
addRoute("/", IndexHandler.class); // inside addMappings method
要測試我們的新路由,我們可以執行:
> curl 'http://localhost:8080'
<html><body><h2>Hello world!</h3></body></html>
其次,讓我們創建一個新的 UserHandler 類,該類繼承了現有的 DefaultHandler。 它的路由是 /users。 在這裏,我們對文本、MIME 類型和返回的代碼狀態進行了試驗:
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;
}
}
要調用此路由,我們可以再次執行 cURL 命令:
> curl -X POST 'http://localhost:8080/users'
UserA, UserB, UserC
最後,我們可以探索 GeneralHandler,並使用新的 StoreHandler 類。 我們修改了返回的消息,以包含 URL 中的 storeId 部分。
public static class StoreHandler extends GeneralHandler {
@Override
public Response get(
UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
return newFixedLengthResponse("Retrieving store for id = "
+ urlParams.get("storeId"));
}
}
讓我們檢查我們的新 API:
> curl 'http://localhost:8080/stores/123'
Retrieving store for id = 123
7. HTTPS
為了使用HTTPS,我們需要一個證書。請參閲我們關於SSL的詳細文章以獲取更多信息。
我們可以使用像 Let’s Encrypt 這樣的服務,或者可以按照以下步驟生成自簽名證書:
> 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
接下來,我們將此 keystore.jks 複製到classpath上的位置,比如 Maven 項目的 src/main/resources 文件夾。
之後,我們可以將其引用到 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
}
現在我們可以嘗試一下。請注意使用 —insecure 參數,因為 cURL 默認情況下無法驗證我們的自簽名證書:
> curl --insecure 'https://localhost:8443'
HTTPS call is a success
8. WebSockets
NanoHTTPD 支持 WebSockets。
讓我們創建一個最簡單的 WebSocket 實現。為此,我們需要擴展 NanoWSD 類。 還需要添加 NanoHTTPD WebSocket 依賴項:
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd-websocket</artifactId>
<version>2.3.1</version>
</dependency>
對於我們的實現,我們將簡單地回覆一個文本負載:
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
}
}
}
}
而不是 cURL 這次,我們將使用 wscat
> wscat -c localhost:8080
hello
hello to you
bye
bye to you
9. 結論
總而言之,我們創建了一個使用 NanoHTTPD 庫的項目。接下來,我們定義了 RESTful API 並探索了更多與 HTTP 相關的功能。最後,我們還實現了 WebSocket。