«1. Введение

NanoHTTPD — это легкий веб-сервер с открытым исходным кодом, написанный на Java.

В этом уроке мы создадим несколько 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 и некоторые другие.

Проще говоря, мы можем найти поддерживаемые HTTP-глаголы с помощью метода enum. Давайте посмотрим, как это работает.

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

Это было довольно просто, верно? Давайте проведем быстрый тест, свернув нашу новую конечную точку и убедившись, что параметр запроса 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");
}
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 -X POST -d 'deliveryAddress=Washington nr 4&quantity=5''http://localhost:8080/'
Request body = deliveryAddress=Washington nr 4&quantity=5

Остальные методы HTTP очень похожи по своей природе, поэтому мы пропустим их.

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

4. Совместное использование ресурсов между источниками

> 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

Теперь, когда мы cURL, мы вернем наш заголовок CORS:

5. Загрузка файла

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

NanoHTTPD имеет отдельную зависимость для файла uploads, так что давайте добавим его в наш проект:

Обратите внимание, что зависимость servlet-api также необходима (иначе мы получим ошибку компиляции).

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

NanoHTTPD предоставляет класс NanoFileUpload:

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

Эй, давайте попробуем:

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).

6. Множественные маршруты

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

Во-первых, давайте добавим необходимую зависимость для nanolets: ~~ ~


А теперь мы расширим наш основной класс с помощью RouterNanoHTTPD, определим наш рабочий порт и запустим сервер как демон.

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 мы определим наши обработчики:

Следующим шагом будет определение нашего метода addMappings. Давайте определим несколько обработчиков.

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

Первый — это путь класса IndexHandler к «/». Этот класс поставляется с библиотекой NanoHTTPD и по умолчанию возвращает сообщение Hello World. Мы можем переопределить метод getText, когда нам нужен другой ответ:

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

И для проверки нашего нового маршрута мы можем сделать:

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

Во-вторых, давайте создадим новый класс UserHandler, который расширяет существующий DefaultHandler. Маршрут для него будет /users. Здесь мы поиграли с текстом, типом MIME и возвращенным кодом состояния:

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

Чтобы вызвать этот маршрут, мы снова выполним команду cURL:

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

Наконец, мы можем изучить GeneralHandler с помощью новый класс StoreHandler. Мы изменили возвращаемое сообщение, включив в URL раздел storeId.

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

Давайте проверим наш новый API:

7. HTTPS

Чтобы использовать HTTPS, нам понадобится сертификат. Пожалуйста, обратитесь к нашей статье о SSL для получения более подробной информации.

> 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

Мы могли бы использовать такой сервис, как Let’s Encrypt, или мы можем просто сгенерировать самоподписанный сертификат следующим образом:

Затем мы скопируем этот keystore.jks в место на нашем пути к классам, например, Папка src/main/resources проекта Maven.

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
}

После этого мы можем сослаться на него в вызове NanoHTTPD#makeSSLSocketFactory:

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

А теперь мы можем попробовать. Обратите внимание на использование параметра —insecure, потому что cURL не сможет проверить наш самозаверяющий сертификат по умолчанию:

8. WebSockets

NanoHTTPD поддерживает WebSockets.

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

«Давайте создадим простейшую реализацию WebSocket. Для этого нам нужно расширить класс NanoWSD. Нам также нужно добавить зависимость NanoHTTPD для WebSocket:

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

Для нашей реализации мы просто ответим простой текстовой полезной нагрузкой:

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

На этот раз вместо cURL мы будем использовать wscat:

9. Заключение

Подводя итог, мы создали проект, использующий библиотеку NanoHTTPD. Затем мы определили RESTful API и изучили дополнительные функции, связанные с HTTP. В конце концов, мы также внедрили WebSocket.