1. Cтандартизированный HTTP-client API (@since 11)

Одной из функций, которые будут включены в предстоящий выпуск JDK 11, является стандартизированный HTTP-client API, целью которого является замена унаследованного класса HttpUrlConnection , который присутствует в JDK с самых ранних лет Java. Проблема с старым API, главным образом, в том, что он старый и сложный в использовании.

Новый API поддерживает как HTTP/1.1, так и HTTP/2. Более новая версия протокола HTTP предназначена для повышения общей производительности отправки запросов клиентом и получения ответов от сервера. Это достигается путем внесения ряда изменений, таких как мультиплексирование потоков, сжатие заголовков и push promises. Кроме того, новый HTTP-клиент также изначально поддерживает WebSockets.

Новый модуль java.net.http который экспортирует пакет с тем же именем, определен в JDK 11, который содержит клиентские интерфейсы:

module java.net.http {
    exports java.net.http;
}

Пакет содержит следующие классы:

  • HttpClient главная точка входа API. Это HTTP-клиент, который используется для отправки запросов и получения ответов. Он поддерживает отправку запросов как синхронно, так и асинхронно, вызывая его методы send() и sendAsync(), соответственно. Создать экземпляр можно с помощью Builder для HttpClient. После создания экземпляр является неизменным

  • HttpRequest инкапсулирует HTTP-запрос, включая целевой URI, метод (GET, POST и т.д.), заголовки и другую информацию. Запрос создается с использованием Builder, является неизменным после создания и может быть отправлен несколько раз

  • HttpRequest.BodyPublisher если запрос имеет тело (например, в запросах POST), это объект, ответственный за публикацию содержимого тела из данного источника, например, из строки, файла и т д.

  • HttpResponse инкапсулирует ответ HTTP, включая заголовки и тело сообщения, если оно есть. Это то, что клиент получает после отправки HttpRequest

  • HttpResponse.BodyHandler функциональный интерфейс, который принимает некоторую информацию об ответе (код состояния и заголовки) и возвращает a BodySubscriber, который сам обрабатывает использование тела ответа HttpResponse.BodySubscriber подписывается на тело ответа и использует его байты в какой-либо другой форме (строка, файл или некоторый другой тип хранения)

BodyPublisher это подинтерфейс Flow.Publisher, введенный в Java 9. Аналогично, BodySubscriber это подинтерфейс Flow.Subscriber. Это означает, что эти интерфейсы согласованы с подходом reactive streams, который подходит для асинхронной отправки запросов с использованием HTTP/2.

Реализации для распространенных publishers, handlers и subscribers для тела response предварительно определены в фабричных классах BodyPublishers, BodyHandlers и BodySubscribers.

Например, чтобы создать объект, BodyHandler который обрабатывает байты тела ответа (через нижележащий BodySubscriber элемент) как строку, метод BodyHandlers.ofString() может использоваться для создания такой реализации. Если тело ответа необходимо сохранить в файл, BodyHandlers.ofFile() можно использовать метод.

1.1. Указание версии HTTP Protocol

Чтобы создать HTTP-клиент, который предпочитает HTTP/2 (по умолчанию, поэтому его version() можно опустить):

HttpClient httpClient = HttpClient.newBuilder()
               .version(Version.HTTP_2)  // this is the default
               .build();

Если указан HTTP/2, первый запрос к исходному серверу попытается использовать его.

Если сервер поддерживает новую версию протокола, то ответ будет отправлен с использованием этой версии. Все последующие запросы/ответы на этот сервер будут использовать HTTP/2.

Если сервер не поддерживает HTTP/2, будет использоваться HTTP/1.1.

1.2. Указание Proxy

Чтобы установить прокси для запроса, используется builder метод proxy() для предоставления ProxySelector. Если хост и порт прокси фиксированы, селектор прокси может быть жестко закодирован в селекторе:

HttpClient httpClient = HttpClient.newBuilder()
               .proxy(ProxySelector.of(new InetSocketAddress(proxyHost, proxyPort)))
               .build();

1.3. GET Request

Методы запроса имеют соответствующие builder методы, основанные на их фактических именах. В приведенном ниже примере GET() необязательно:

HttpRequest request = HttpRequest.newBuilder()
               .uri(URI.create("https://http2.github.io/"))
               .GET()   // this is the default
               .build();

1.4. POST Request с телом запросв

Чтобы создать запрос с телом, BodyPublisher требуется преобразовать источник тела в байты. Один из предопределенных издателей может быть создан из статических методов фабрики в BodyPublishers:

HttpRequest mainRequest = HttpRequest.newBuilder()
               .uri(URI.create("https://http2.github.io/"))
               .POST(BodyPublishers.ofString(json))
               .build();

1.5. Отправка HTTP Request

Существует два способа отправки запроса:

  • синхронно (блокировка до получения ответа)

  • асинхронно

1.5.1. Synchronously HTTP request

Для отправки в режиме блокировки мы вызываем send() метод на HTTP-клиенте, предоставляя экземпляр запроса и BodyHandler. Вот пример, который получает ответ, представляющий тело в виде строки:

HttpRequest request = HttpRequest.newBuilder()
               .uri(URI.create("https://http2.github.io/"))
               .build();
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString());
logger.info("Response status code: " + response.statusCode());
logger.info("Response headers: " + response.headers());
logger.info("Response body: " + response.body());

1.5.2. Asynchronously HTTP Request

Иногда полезно избегать блокировок, пока ответ не будет возвращен сервером. В этом случае мы можем вызвать метод sendAsync(), который возвращает CompletableFuture. A CompletableFuture предоставляет механизм для цепочки последующих действий, которые должны быть запущены после его завершения. В этом контексте возвращенное CompletableFuture завершено, когда HttpResponse получен.

httpClient.sendAsync(request, BodyHandlers.ofString())
          .thenAccept(response -> {
       logger.info("Response status code: " + response.statusCode());
       logger.info("Response headers: " + response.headers());
       logger.info("Response body: " + response.body());
});

В приведенном выше примере, sendAsyn() вернет CompletableFuture<HttpResponse<String>>, a метод thenAccept() добавляет Consumer, который сработает, когда вернется ответ.

2. InetAddress

Для работы с IP-адресами в библиотеке java.net имеется класс InetAddress. С помощью InetAddress можно определить адрес IP локального узла, а также адреса удаленного узла, заданного доменным именем. Наиболее распространенные методы класса InetAddress:

  • InetAddress getLocalHost()

  • InetAddress getByName(String host)

  • InetAddress[] getAllByName(String host)

  • byte[] getAddress()

  • String toString()

  • String getHostName()

  • boolean equals(Object obj)

При разработке сетевых приложений на начальном этапе, как правило, используют один компьютер (host). Для этого создатели протокола IP определили специальный адрес, называемый localhost - это IP-адрес "локальной заглушки" (local loopback) для работы приложений без использования сети. Общий порядок получения этого адреса в Java следующий:

InetAddress address = InetAddress.getByName(null);
address = InetAddress.getByName("localhost");

Если методу getByName() передать значение null, то по умолчанию будет использоваться localhost. Cодержимым InetAddress нельзя манипулировать. Для создания InetArddress можно использовать один из перегруженных статических методов класса getByName(), getAllByName() или getLocalHost().

3. TCP networking

Java для работы в сети имеет специальный пакет java.net, содержащий класс Socket, что в переводе означает «гнездо». Ключевыми классами для реализации взаимодействия программ по протоколу TCP являются:

  • ServerSocket - класс реализует серверный сокет, который ожидает запросы, приходящие от клиентов по сети, и может отправлять ответ

  • Socket - класс реализует клиентский сокет

3.1. Socket

Клиентский сокет Socket можно создать с использованием одного из следующих конструкторов:

  • Socket()

  • Socket(String host, int port)

  • Socket(InetAddress address, int port)

В строковой константе host можно указать как IP адрес сервера, так и его DNS имя. При этом программа автоматически выберет свободный порт на локальном компьютере и свяжет его с сокетом. При этом могут быть вызваны одно из двух видов исключений, связанного с неизвестным адресом хоста (в сети компьютер не будет найден) или отсутствием связи с этим сокетом.

Наиболее важные методы класса Socket:

  • InputStream getInputStream()

  • OutputStream getOutputStream()

  • void close()

  • void setSoTimeout(int timeout) throws SocketException

где timeout время ожидания в секундах. Если в течение этого времени никаких действий с сокетом не произведено (получение и отправка данных), то он самоликвидируется. Исключением является сокет с timeout равным 0 - "вечный" сокет.

Пример использования:

import java.io.DataOutputStream;
import java.net.Socket;

public class MyClient {
    public static void main(String[] args) {
        try {
            Socket s = new Socket("localhost", 6666);
            DataOutputStream dout = new DataOutputStream(s.getOutputStream());
            dout.writeUTF("Hello Server");
            dout.flush();
            dout.close();
            s.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

3.2. ServerSocket

Для создания серверного сокета ServerSocket можно использовать один из следующих конструкторов :

  • public ServerSocket() throws IOException

  • public ServerSocket(int port) throws IOException

  • public ServerSocket(int port, int backlog) throws IOException

  • public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

Первым параметров в конструктор необходимо передать порт port, который будет привязан к серверному сокету. Если порт занят или запрещён к использованию политикой безопасности компьютера, то вызывается исключение IOException. Если значение передавамого порта равно 0, то система сама выделит номер свободного порта. Значение полученного порта можно узнать через вызов функции getLocalPort(). Несвязанный серверный сокет ServerSocket() необходимо «связывать» с IP-адресом и портом c помощью метода bind().

Параметр backlog устанавливает максимальное количество клиентских подключений. Если количество подключений достигло предела, то следующему клиенту в подключении будет отказано.

Наиболее часто используемые методы серверного сокета ServerSocket:

  • Socket accept() - ожидание подключения клиента

  • void bind(SocketAddress endpoint) - связывание ServerSocket c определенным адресом (IP-адрес и порт)

  • void close() - закрытие сокета

  • ServerSocketChannel getChannel() - получение объекта ServerSocketChannel, связанного с сокетом

  • InetAddress getInetAddress() - получение локального адреса

  • int getLocalPort() - получение номера порта, который серверный сокет слушает

  • SocketAddress getLocalSocketAddress()- получение адреса в виде объекта SocketAddress

  • int getReceiveBufferSize()- получение размера буфера

  • boolean isClosed() - проверка, закрыт ли серверный сокет

  • void setReceiveBufferSize(int size) - определение размера буфера

После создания в приложении серверного сокета ServerSocket необходимо вызвать функцию accept(), которая переводит приложение в режим ожидания подключения клиента. Дальнейший код не выполняется, пока клиент не подключится. Как только клиент подключается функция возвращает объект класса Socket, который следует использовать для взаимодействия сервера с клиентом.

Пример использования:

import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class MyServer {
    public static void main(String[] args) {
        try {
            ServerSocket ss = new ServerSocket(6666);
            Socket s = ss.accept(); // establishes connection
            DataInputStream dis = new DataInputStream(s.getInputStream());
            String str = (String) dis.readUTF();
            System.out.println("message= " + str);
            ss.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

4. UDP networking

Классы DatagramSocket и DatagramPacket используются для программирования сокетов без подключения.

4.1. DatagramSocket

Класс DatagramSocket представляет сокет без соединения для отправки и приема пакетов datagram.

Datagram - это в основном информация, но нет гарантии что она что-то содержит, когда она доставится или доставится ли вообще.

Конструкторы класса DatagramSocket:

  • DatagramSocket() throws SocketExeption - создает сокет datagram и связывает его с доступным номером порта на localhost.

  • DatagramSocket(int port) throws SocketExeption - создает сокет datagram и связывает его с данным номером port.

  • DatagramSocket(int port, InetAddress address) throws SocketExeption - создает сокет datagram и связывает его с указанным номером port и адресом хоста InetAddress.

4.2. DatagramPacket

DatagramPacket - это сообщение, которое может быть отправлено или получено. Если вы отправляете несколько пакетов, он может поступать в любом порядке. Кроме того, доставка пакетов не гарантируется.

Чаще используют следующие конструкторы класса DatagramPacket:

  • DatagramPacket(byte [] barr, int length) - используется для приема пакетов

  • DatagramPacket(byte [] barr, int length, адрес InetAddress, int port) - используется для отправки пакетов

Пример отправки DatagramPacket по DatagramSocket

Отправитель:

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class DSender {
    public static void main(String[] args) throws Exception {
        DatagramSocket ds = new DatagramSocket();
        String str = "Welcome java";
        InetAddress ip = InetAddress.getByName("127.0.0.1");

        DatagramPacket dp = new DatagramPacket(str.getBytes(), str.length(), ip, 3000);
        ds.send(dp);
        ds.close();
    }
}

Получатель:

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class DReceiver {
    public static void main(String[] args) throws Exception {
        DatagramSocket ds = new DatagramSocket(3000);
        byte[] buf = new byte[1024];
        DatagramPacket dp = new DatagramPacket(buf, 1024);
        ds.receive(dp);
        String str = new String(dp.getData(), 0, dp.getLength());
        System.out.println(str);
        ds.close();
    }
}