Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

HTTP-клиент

Программам часто нужны данные из интернета: курс валют, погода, ответ другого сервиса. Их получают по протоколу HTTP: клиент отправляет серверу запрос (request), сервер возвращает ответ (response) — код статуса и текст. Для этого в Java есть встроенный клиент HttpClient (Java 11) из модуля java.net.http. Он не входит в java.base, поэтому даже в компактном файле его импортируют: import java.net.http.*;.

Сервер для опытов

Чтобы примеры работали без интернета, каждый из них сам запускает маленький сервер на вашем компьютере. Для этого в JDK есть простой HttpServer. Он отвечает текстом «Сервер получил: …» и повторяет запрос, а на адрес /missing отвечает кодом 404. Порт 0 означает «любой свободный» — настоящий номер узнают через server.getAddress().getPort(). Код сервера — в методе startServer() в конце каждого примера; разбирать его не обязательно, главное в уроке — клиент.

GET-запрос

Работа с клиентом — три шага: создать HttpClient, собрать HttpRequest с адресом и отправить его методом send. Второй аргумент send — обработчик тела (body handler): он говорит, во что превратить ответ. BodyHandlers.ofString() читает тело как строку:

import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
 
void main() throws Exception {
    HttpServer server = startServer();
    String url = "http://localhost:" + server.getAddress().getPort() + "/users/1";
 
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
 
    IO.println(response.statusCode());
    IO.println(response.body());
 
    server.stop(0);
}
 
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
    server.createContext("/", exchange -> {
        String body = new String(exchange.getRequestBody().readAllBytes());
        String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
        byte[] bytes = reply.strip().getBytes();
        int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
        exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
        exchange.sendResponseHeaders(status, bytes.length);
        exchange.getResponseBody().write(bytes);
        exchange.close();
    });
    server.start();
    return server;
}

Результат:

200
Сервер получил: GET /users/1

Для настоящего сайта меняется только адрес, например https://api.example.com/users/1. По умолчанию запрос — GET. Код 200 означает «всё хорошо». Есть и другие обработчики: ofLines() даёт поток строк, ofFile(path) сохраняет ответ в файл, discarding() тело отбрасывает.

Коды ответа и заголовки

Код статуса — трёхзначное число: 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента (404 — «не найдено»), 5xx — ошибка сервера. Кроме тела, ответ несёт заголовки — пары «имя: значение» с дополнительными сведениями. headers().firstValue(...) возвращает Optional:

import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
 
void main() throws Exception {
    HttpServer server = startServer();
    String base = "http://localhost:" + server.getAddress().getPort();
    HttpClient client = HttpClient.newHttpClient();
 
    for (String path : List.of("/users/1", "/missing")) {
        HttpRequest request = HttpRequest.newBuilder(URI.create(base + path)).build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        IO.println(path + " -> " + response.statusCode());
    }
 
    HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/users/1")).build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    IO.println(response.headers().firstValue("Content-Type").orElse("нет"));
 
    server.stop(0);
}
 
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
    server.createContext("/", exchange -> {
        String body = new String(exchange.getRequestBody().readAllBytes());
        String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
        byte[] bytes = reply.strip().getBytes();
        int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
        exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
        exchange.sendResponseHeaders(status, bytes.length);
        exchange.getResponseBody().write(bytes);
        exchange.close();
    });
    server.start();
    return server;
}

Результат:

/users/1 -> 200
/missing -> 404
text/plain; charset=utf-8

POST с JSON

Чтобы отправить данные, запрос строят с методом POST и телом (body). BodyPublishers.ofString превращает строку в тело запроса. Данные чаще всего передают в формате JSON; здесь он записан обычной строкой, а заголовок Content-Type сообщает серверу формат:

import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
 
void main() throws Exception {
    HttpServer server = startServer();
    String url = "http://localhost:" + server.getAddress().getPort() + "/users";
 
    String json = """
            {"name": "Анна", "age": 28}""";
 
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder(URI.create(url))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(5))
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
 
    IO.println(response.statusCode());
    IO.println(response.body());
 
    server.stop(0);
}
 
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
    server.createContext("/", exchange -> {
        String body = new String(exchange.getRequestBody().readAllBytes());
        String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
        byte[] bytes = reply.strip().getBytes();
        int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
        exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
        exchange.sendResponseHeaders(status, bytes.length);
        exchange.getResponseBody().write(bytes);
        exchange.close();
    });
    server.start();
    return server;
}

Результат:

200
Сервер получил: POST /users {"name": "Анна", "age": 28}

timeout ограничивает ожидание ответа: если сервер молчит дольше пяти секунд, send выбросит HttpTimeoutException. Так же устроены PUT(...) и DELETE(). Разбирать JSON из ответа удобнее библиотеками Jackson или Gson — в стандартной библиотеке Java парсера JSON нет.

Асинхронные запросы

send блокирует поток, пока не придёт ответ. sendAsync возвращает сразу — CompletableFuture, в котором ответ появится позже. Так можно отправить несколько запросов одновременно и дождаться всех:

import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
 
void main() throws Exception {
    HttpServer server = startServer();
    String base = "http://localhost:" + server.getAddress().getPort();
    HttpClient client = HttpClient.newHttpClient();
 
    List<CompletableFuture<String>> futures = new ArrayList<>();
    for (String path : List.of("/news", "/weather", "/rates")) {
        HttpRequest request = HttpRequest.newBuilder(URI.create(base + path)).build();
        futures.add(client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body));
    }
 
    for (CompletableFuture<String> future : futures) {
        IO.println(future.join());
    }
 
    server.stop(0);
}
 
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
    server.createContext("/", exchange -> {
        String body = new String(exchange.getRequestBody().readAllBytes());
        String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
        byte[] bytes = reply.strip().getBytes();
        int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
        exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
        exchange.sendResponseHeaders(status, bytes.length);
        exchange.getResponseBody().write(bytes);
        exchange.close();
    });
    server.start();
    return server;
}

Результат:

Сервер получил: GET /news
Сервер получил: GET /weather
Сервер получил: GET /rates

Запросы выполняются параллельно, а join() по очереди забирает готовые результаты — в том порядке, в каком будущие значения лежат в списке. Подробнее о CompletableFuture — в уроке CompletableFuture. Один HttpClient можно и нужно переиспользовать для многих запросов: он держит соединения открытыми.

Частая ошибка

send не выбрасывает исключение, если сервер ответил ошибкой: 404 или 500 — тоже нормальный ответ. Код, который сразу берёт body(), примет текст ошибки за данные:

import com.sun.net.httpserver.HttpServer;
import java.net.http.*;
 
void main() throws Exception {
    HttpServer server = startServer();
    String url = "http://localhost:" + server.getAddress().getPort() + "/missing";
 
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
 
    if (response.statusCode() == 200) {
        IO.println("Данные: " + response.body());
    } else {
        IO.println("Ошибка, код " + response.statusCode());
    }
 
    server.stop(0);
}
 
// Учебный сервер: отвечает, какой запрос он получил
HttpServer startServer() throws IOException {
    HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
    server.createContext("/", exchange -> {
        String body = new String(exchange.getRequestBody().readAllBytes());
        String reply = "Сервер получил: " + exchange.getRequestMethod() + " " + exchange.getRequestURI() + " " + body;
        byte[] bytes = reply.strip().getBytes();
        int status = exchange.getRequestURI().getPath().equals("/missing") ? 404 : 200;
        exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
        exchange.sendResponseHeaders(status, bytes.length);
        exchange.getResponseBody().write(bytes);
        exchange.close();
    });
    server.start();
    return server;
}

Результат:

Ошибка, код 404

Всегда проверяйте statusCode(), прежде чем разбирать тело. Исключение (IOException, например ConnectException) бывает, только когда ответа нет вовсе: сервер недоступен или нет сети.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.