Перейти к основному содержимому

Руководство по NanoHTTPD

· 8 мин. чтения

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. РЕСТ API

Как и методы HTTP, NanoHTTPD поддерживает GET, POST, PUT, DELETE, HEAD, TRACE и некоторые другие.

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

3.1. HTTP ПОЛУЧИТЬ

Во-первых, давайте взглянем на 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-ПОСТ

Ранее мы реагировали на 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");
}

Обратите внимание, что раньше, когда мы запрашивали тело запроса, мы сначала вызывали метод parseBody . Это потому, что мы хотели загрузить тело запроса для последующего извлечения.

Мы включим тело в нашу команду cURL :

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

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

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

С помощью CORS мы включаем междоменное взаимодействие. Наиболее распространенный вариант использования — вызовы AJAX из другого домена.

Первый подход, который мы можем использовать, — включить CORS для всех наших API. Используя ` аргумент –cors , мы разрешим доступ ко всем доменам. Мы также можем определить, какие домены мы разрешаем, с помощью –cors="http://dashboard.myApp.com http://admin.myapp.com"` .

Второй подход — включить CORS для отдельных API. Давайте посмотрим, как использовать addHeader для этого:

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

Обратите внимание, что зависимость 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 в место на нашем пути к классам, например, в папку src/main/resources проекта Maven.

После этого мы можем сослаться на него в вызове 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. Веб-сокеты

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.

Реализация всех этих фрагментов доступна на GitHub .