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 .