W3docs

Java Datagram Sockets (UDP)

Отправка и получение UDP-датаграмм в Java с помощью DatagramSocket и DatagramPacket.

Socket и ServerSocket работают по протоколу TCP — надёжному, упорядоченному, потоковому соединению. DatagramSocket использует UDP, другой транспортный протокол: без установления соединения и на основе пакетов. Здесь нет «подключения»: вы отправляете независимые датаграммы по адресу и надеетесь, что они дойдут. Нет рукопожатия, нет упорядочивания и нет гарантии доставки — зато нет накладных расходов на соединение и очень низкая задержка.

На этой странице рассматривается, когда UDP является правильным выбором, два основных класса (DatagramSocket и DatagramPacket), полный пример запрос/ответ, который можно запустить, и подводные камни, с которыми сталкиваются программисты при первом знакомстве с UDP. Если вы только начинаете знакомство с сетевым программированием в Java, начните со введения в работу с сетью.

Когда UDP является правильным инструментом

UDP жертвует надёжностью ради скорости и простоты. Он подходит, когда:

  • Случайные потери допустимы — живое аудио/видео, игровое состояние, телеметрия. Потерянный кадр лучше запоздавшего.
  • Сообщения небольшие и самодостаточные — DNS-запросы, синхронизация времени по NTP.
  • Вы выполняете широковещательную/многоадресную рассылку нескольким получателям — TCP на это не способен.

Если каждый байт нужен и в правильном порядке (передача файлов, веб-страницы, базы данных), используйте TCP. Многие приложения строят собственную лёгкую надёжность поверх UDP, чтобы не нести полные затраты TCP.

Два класса

UDP в Java использует пару классов:

  • DatagramSocket — конечная точка, из которой выполняется send и в которую поступает receive. Нет отдельного «серверного сокета»; один и тот же класс выполняет обе роли, поскольку нет соединения, которое нужно принимать.
  • DatagramPacket — одна датаграмма: буфер байтов плюс, для отправки, адрес назначения и порт; для получения он заполняется адресом и портом отправителя и длиной данных.
DatagramSocket socket = new DatagramSocket(9000);     // bind to receive on 9000
byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
DatagramPacket out = new DatagramPacket(data, data.length, address, port);
socket.send(out);                                     // fire and forget

byte[] buf = new byte[1024];
DatagramPacket in = new DatagramPacket(buf, buf.length);
socket.receive(in);                                   // blocks; fills buf + sender info

Важная деталь: после receive() читайте ровно in.getLength() байтов из буфера — буфер фиксированного размера, но датаграмма может быть короче. Установите setSoTimeout(ms), чтобы потерянный пакет не блокировал receive() навсегда.

Рабочий пример: UDP запрос/ответ через loopback

Эта программа запускает приёмник в фоновом потоке, который ожидает одну датаграмму и отвечает отправителю, тогда как основной поток отправляет датаграмму и читает подтверждение — полный цикл UDP через интерфейс loopback.

java— editable, runs on the server

Что следует вынести из запуска:

  • Не было ни connect(), ни accept(). Оба конца — просто DatagramSocket; отправитель отправил пакет по адресу и порту, а получатель его принял. UDP не имеет соединения, поэтому один и тот же класс выполняет обе роли — асимметрия клиентских и серверных сокетов TCP исчезает.
  • DatagramPacket нёс как данные, так и адресную информацию. Получатель узнал, кто отправил запрос, из request.getAddress() и request.getPort(), и ответил напрямую этой конечной точке — постоянного канала нет, поэтому каждый ответ должен быть адресован явно.
  • Тело было декодировано с помощью new String(data, 0, getLength(), …), а не всего 1024-байтного буфера. Датаграмма заполняет лишь часть фиксированного буфера; чтение getLength() байтов обязательно, иначе в конец добавятся лишние данные из неиспользованного пространства буфера.
  • setSoTimeout(2000) защищал receive(). Поскольку UDP ничего не гарантирует, потерянный ответ иначе заблокировал бы выполнение навсегда; таймаут превращает «пакет не пришёл» в перехватываемое SocketTimeoutException, которое можно повторить или сообщить об ошибке.
  • Обмен здесь сработал, потому что loopback не имеет потерь и упорядочен, но API не давал таких обещаний. По реальной сети эта датаграмма могла исчезнуть, прийти дважды или прийти после более поздней — именно поэтому чувствительные к надёжности приложения выбирают TCP или строят собственную схему подтверждений поверх UDP.

Распространённые подводные камни

Несколько ловушек подстерегают почти каждого при первом использовании DatagramSocket:

  • Чтение всего буфера вместо getLength() байтов. Буфер фиксированного размера; датаграмма, как правило, нет. Всегда делайте срез с помощью new String(data, 0, packet.getLength(), …) или Arrays.copyOf(data, packet.getLength()). Повторное использование буфера усугубляет ситуацию — оставшиеся байты от предыдущей, более длинной датаграммы появляются в виде мусора в конце.
  • Отсутствие таймаута на receive(). Поскольку UDP никогда не гарантирует доставку, потерянный пакет оставляет receive() заблокированным навсегда. Вызывайте setSoTimeout(ms) и обрабатывайте возникающее SocketTimeoutException (повторить, зафиксировать или отказаться).
  • Отправка данных, превышающих размер одной датаграммы. UDP не поддерживает потоковую передачу; один send() — один пакет. Большие полезные нагрузки фрагментируются на уровне IP, и один потерянный фрагмент уничтожает всю датаграмму. Держите полезную нагрузку небольшой — примерно 512 байтов является безопасным пределом, позволяющим избежать фрагментации в большинстве сетей.
  • Забыть закрыть сокет. DatagramSocket удерживает порт ОС. Используйте try-with-resources (он реализует AutoCloseable) или закрывайте его в блоке finally, чтобы порт был освобождён.
  • Предположение, что ответ приходит оттуда, куда вы его отправили. receive() перезаписывает адрес и порт пакета фактическим отправителем. Всегда отвечайте, используя packet.getAddress()/packet.getPort(), а не жёстко заданный адрес назначения.

Для гарантированной упорядоченной доставки используйте классы TCP из раздела сокеты Java.

Практика

Практика
Агент мониторинга получает UDP-датаграммы в повторно используемый буфер 'byte[2048]' через 'socket.receive(packet)', затем преобразует весь буфер с помощью 'new String(packet.getData(), StandardCharsets.UTF_8)'. Короткие сообщения выходят с мусором в конце. Как это исправить?
Агент мониторинга получает UDP-датаграммы в повторно используемый буфер 'byte[2048]' через 'socket.receive(packet)', затем преобразует весь буфер с помощью 'new String(packet.getData(), StandardCharsets.UTF_8)'. Короткие сообщения выходят с мусором в конце. Как это исправить?
Was this page helpful?