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
функциональный интерфейс, который принимает некоторую информацию об ответе (код состояния и заголовки) и возвращает aBodySubscriber
, который сам обрабатывает использование тела ответа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();
}
}