A Guide to NanoHTTPD – NanoHTTPD指南

最后修改: 2019年 7月 5日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

NanoHTTPD is an open-source, lightweight, web server written in Java.

NanoHTTPD是一个开源的、轻量级的、用Java编写的Web服务器。

In this tutorial, we’ll create a few REST APIs to explore its features.

在本教程中,我们将创建一些REST API来探索其功能。

2. Project Setup

2.项目设置

Let’s add the NanoHTTPD core dependency to our pom.xml:

让我们在我们的pom.xml中添加NanoHTTPD核心依赖项

<dependency>
    <groupId>org.nanohttpd</groupId>
    <artifactId>nanohttpd</artifactId>
    <version>2.3.1</version>
</dependency>

To create a simple server, we need to extend NanoHTTPD and override its serve method:

为了创建一个简单的服务器,我们需要扩展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");
    }
}

We defined our running port as 8080 and server to work as a daemon (no read timeout).

我们将运行端口定义为8080,服务器作为一个守护程序工作(没有读取超时)。

Once we’ll start the application, the URL http://localhost:8080/ will return the Hello world message. We’re using NanoHTTPD#newFixedLengthResponse method as a convenient way of building a NanoHTTPD.Response object.

一旦我们启动应用程序,URL http://localhost:8080/将返回Hello world消息。我们使用NanoHTTPD#newFixedLengthResponse方法作为构建NanoHTTPD.Response对象的方便方法。

Let’s try our project with cURL:

让我们用cURL试试我们的项目。

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

3. REST API

3.REST API

In the way of HTTP methods, NanoHTTPD allows GET, POST, PUT, DELETE, HEAD, TRACE, and several others.

在HTTP方法方面,NanoHTTPD允许GET、POST、PUT、DELETE、HEAD、TRACE和其他几种方法。

Simply put, we can find supported HTTP verbs via the method enum. Let’s see how this plays out.

简单地说,我们可以通过方法枚举找到支持的HTTP动词。让我们来看看这一点是如何实现的。

3.1. HTTP GET

HTTP GET

First, let’s take a look at GET. Say, for example, that we want to return content only when the application receives a GET request.

首先,让我们看一下GET。比如说,我们想只在应用程序收到GET请求时返回内容。

Unlike Java Servlet containers, we don’t have a doGet method available – instead, we just check the value via getMethod:

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");
}

That was pretty simple, right? Let’s run a quick test by curling our new endpoint and see that the request parameter itemId is read correctly:

这很简单,对吗?让我们通过卷曲我们的新端点运行一个快速测试,看看请求参数itemId是否被正确读取。

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

3.2. HTTP POST

3.2 HTTP POST

We previously reacted to a GET and read a parameter from the URL.

我们之前对一个GET作出反应,从URL中读取一个参数。

In order to cover the two most popular HTTP methods, it’s time for us to handle a POST (and thus read the request body):

为了涵盖两种最流行的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");
}
Notice that before when we asked for the request body, we first called the parseBody method. That’s because we wanted to load the request body for later retrieval.

We’ll include a body in our cURL command:

我们将在我们的cURL命令中包含一个主体。

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

The remaining HTTP methods are very similar in nature, so we’ll skip those.

其余的HTTP方法在性质上非常相似,所以我们将跳过这些。

4. Cross-Origin Resource Sharing

4.跨源头资源共享

Using CORS, we enable cross-domain communication. The most common use case is AJAX calls from a different domain.
 
The first approach that we can use is to enable CORS for all our APIs. Using the -cors argument, we’ll allow access to all domains. We can also define which domains we allow with –cors=”http://dashboard.myApp.com http://admin.myapp.com”.
 
The second approach is to enable CORS for individual APIs. Let’s see how to use addHeader to achieve this:
@Override 
public Response serve(IHTTPSession session) {
    Response response = newFixedLengthResponse("Hello world"); 
    response.addHeader("Access-Control-Allow-Origin", "*");
    return response;
}

Now when we cURL, we’ll get our CORS header back:

现在当我们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. File Upload

5.文件上传

NanoHTTPD has a separate dependency for file uploads, so let’s add it to our project:

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>

Please note that the servlet-api dependency is also needed (otherwise we’ll get a compilation error).

请注意,还需要servlet-api依赖性(否则我们会得到一个编译错误)。

What NanoHTTPD exposes is a class called NanoFileUpload:

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");
}

Hey, let’s try it out:

嘿,让我们试试吧。

> curl -F 'filename=@/pathToFile.txt' 'http://localhost:8080'
Uploaded files: 1

6. Multiple Routes

6.多条路线

A nanolet is like a servlet but has a very low profile. We can use them to define many routes served by a single server (unlike previous examples with one route).

Firstly, let’s add the required dependency for nanolets:

首先,让我们为nanolets添加所需的依赖。

<dependency>
    <groupId>org.nanohttpd</groupId>
    <artifactId>nanohttpd-nanolets</artifactId>
    <version>2.3.1</version>
</dependency>

And now we’ll extend our main class using the RouterNanoHTTPD, define our running port and have the server run as a daemon.

现在我们将使用RouterNanoHTTPD扩展我们的主类,定义我们的运行端口,并让服务器作为一个守护程序运行。

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

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
    }
}

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

下一步是定义我们的addMappings方法。让我们定义几个处理程序。

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:

第一个是一个IndexHandler类到”/”路径。这个类是NanoHTTPD库中的,默认返回一个Hello World消息。当我们想要一个不同的响应时,我们可以覆盖getText方法:

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

And to test our new route we can do:

而为了测试我们的新路线,我们可以做。

> curl 'http://localhost:8080' 
<html><body><h2>Hello world!</h3></body></html>

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:

其次,让我们创建一个新的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;
    }
}

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

为了调用这个路由,我们将再次发出一个cURL命令。

> curl -X POST 'http://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.

最后,我们可以用一个新的StoreHandler类来探索GeneralHandler。我们修改了返回的消息,以包括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"));
    }
}

Let’s check our new API:

让我们检查一下我们的新API。

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

7. HTTPS

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.

为了使用HTTPS,我们将需要一个证书。请参考我们关于SSL的文章,以获得更深入的信息。

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

我们可以使用像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

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

接下来,我们把这个keystore.jks复制到classpath上的某个位置,比如Maven项目的src/main/resources文件夹。

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

之后,我们可以在调用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:

现在我们可以试试了。请注意使用了-insecure参数,因为cURL默认情况下无法验证我们的自签名证书。

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

8. WebSockets

8.WebSockets

NanoHTTPD supports WebSockets.

NanoHTTPD支持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:

让我们创建一个最简单的WebSocket的实现。为此,我们需要扩展NanoWSD类。我们还需要为WebSocket添加NanoHTTPD依赖项:

<dependency>
    <groupId>org.nanohttpd</groupId>
    <artifactId>nanohttpd-websocket</artifactId>
    <version>2.3.1</version>
</dependency>

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:

这次我们不使用cURL,而使用wscat

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

9. Conclusion

9.结语

To sum it up, we’ve created a project that uses the NanoHTTPD library. Next, we defined RESTful APIs and explored more HTTP related functionalities. In the end, we also implemented a WebSocket.

总而言之,我们已经创建了一个使用NanoHTTPD库的项目。接下来,我们定义了RESTful APIs,并探索了更多的HTTP相关功能。最后,我们还实现了一个WebSocket。

The implementation of all these snippets is available over on GitHub.

所有这些片段的实现都可以在GitHub上找到