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.
Что следует вынести из запуска:
- Не было ни
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.