Говоря о сетевом взаимодействии мы не можем не упомянуть модель OSI.

Сегодня в этой модели нас больше всего интересует транспортный уровень (4).

На этом уровне мы работаем с данными, которые идут “из точки А в точку Б”. Основная задача транспортного уровня — гарантировать доставку сообщения до адресата, сохраняя правильную последовательность. Есть 2 самых распространенных протокола транспортного уровня: TCP и UDP. Работают они концептуально по-разному, но каждый имеет свои преимущества, что позволяет использовать их профильно для решения определенных задач.

Сначала давай рассмотрим схему работы TCP.

TCP (Transmission Control Protocol) — это сетевой протокол, который перед обменом данными убеждается, что соединение между хостами установлено.

Это очень надежный протокол, ведь каждый раз, отправляя очередной пакет данных, он должен проверить, что предыдущий пакет дошел.

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

Именно тут и приходит его “брат-антагонист” — протокол UDP. Ему, в отличие от TCP, не так важен порядок и статус каждого пакета, он просто шлет данные без подтверждения доставки. Более того, он не занимается установкой соединения и никак не зависит от его статуса.

Его задача — просто отправлять данные по адресу. Из этого выплывает и главный минус протокола — низкая надежность, ведь он попросту может потерять куски данных. К тому же получатель должен быть готов к тому, что данные могут приходить неупорядоченно. У протокола есть и преимущество — более высокая скорость передачи, обусловленная тем, что круг задач данного протокола ограничивается отправкой данных.

Есть различия и в методе самих передаваемых данных. В TCP данные передаются потоком, что означает, что данные не имеют обозначения границ. В случае с UDP данные передаются в виде датаграмм и имеют обозначения границ, а проверка данных на целостность производит уже принимающая сторона, но только в случае успешного получения сообщения.

Подводем итог:

TCP — надежный и точный протокол, который не дает шансов на потерю данных. Сообщение всегда будет доставлено с максимальной точностью, либо не доставлено вообще. Принимающая сторона может не иметь логики по упорядочиванию данных, так как приходящие данные уже будут упорядочены. UDP — не такой надежный, но более быстрый протокол передачи данных. Логика отправляющей и принимающей сторон должна дополняться некоторыми махинациями для того, чтобы работать с этим протоколом. Но Но давай взглянеем на его работу на примере компьютерной или мобильной игры по сети. Нам уже может быть не важно, что должно было прийти 5 секунд назад, и мы можем пропустить пару пакетов, если они не успевают — игра подлагивает, но играть можно!

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

Для обмена данными отправитель и получатель создают сокеты датаграммного типа — объекты класса DatagramSocket. Класс имеет несколько конструкторов, разница которых в том, куда присоединится создаваемый сокет:

DatagramSocket () К любому свободному порту на локальной машине
DatagramSocket (int port) К указанному порту на локальной машине
DatagramSocket(int port, InetAddress addr) К указанному порту по одному из адресов локальной машины (addr)

Класс содержит множество методов для доступа к параметрам сокета и управлением им (чуть ниже мы их рассмотрим), а также методы для приема и отправки датаграмм:

send(DatagramPacket pack) Отправляет датаграммы, упакованные в пакеты
receive (DatagramPacket pack) Принимает датаграммы, упакованные в пакеты

DatagramPacket — класс, который представляет собой пакет датаграмм. Пакеты датаграмм используются для реализации службы доставки пакетов без подключения. Каждое сообщение направляется с одной машины на другую исключительно на основе информации, содержащейся в этом пакете. Несколько пакетов, отправленных с одной машины на другую, могут быть маршрутированы по-разному и могут поступать в любом порядке. Доставка пакетов не гарантируется.

Конструкторы:

DatagramPacket(byte[] buf, int length) Создает DatagramPacket для приема пакетов длины length.
DatagramPacket(byte[] buf, int length, InetAddress address, int port) Создает пакет датаграммы для отправки пакетов длины length на указанный номер порта на указанном узле.
DatagramPacket(byte[] buf, int offset, int length) Создает DatagramPacket для приема пакетов длины length, указывая смещение в буфере.
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) Создает пакет датаграммы для отправки пакетов длины length со смещением offset к указанному номеру порта на указанном узле.
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) Создает пакет датаграммы для отправки пакетов длины length со смещением offset к указанному номеру порта на указанном узле.
DatagramPacket(byte[] buf, int length, SocketAddress address) Создает пакет датаграммы для отправки пакетов длины length на указанный номер порта на указанном узле.

Из схемы работы UDP мы помним, что соединение не устанавливается и пакеты посылаются на удачу в расчете на то, что получатель их ждет. Но с помощью метода класса DatagramSocket connect(InetAddress addr, int port) можно установить соединение.

Устанавливается одностороннее соединение с хостом по адресу и порту: или на отправку, или на прием датаграмм. Такое соединение можно разорвать методом disconnect().

Давай попробуем написать код сервера (принимающей стороны) на основе DatagramSocket:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

class Recipient {

   public static void main(String[] args) {
       try {
           DatagramSocket ds = new DatagramSocket(1050);

           while (true) {
               DatagramPacket pack = new DatagramPacket(new byte[5], 5);
               ds.receive(pack);
               System.out.println(new String(pack.getData()));
           }
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}

Мы создаем объект DatagramSocket, предназначенный для прослушивания порта 1050. Получив сообщение, он выводит его в консоль. Передавать мы будем слово “Hello”, поэтому ограничиваем размер буфера пятью байтами.

Теперь создаем класс отправителя:

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

class Sender {
   private String host;
   private int port;

   Sender(String host, int port) {
       this.host = host;
       this.port = port;
   }

   private void sendMessage(String mes) {
       try {
           byte[] data = mes.getBytes();
           InetAddress address = InetAddress.getByName(host);
           DatagramPacket pack = new DatagramPacket(data, data.length, address, port);
           DatagramSocket ds = new DatagramSocket();
           ds.send(pack);
           ds.close();
       } catch (IOException e) {
           System.err.println(e);
       }
   }

   public static void main(String[] args) {
   Sender sender = new Sender("localhost", 1050);
   String message = "Hello";

   Timer timer = new Timer();
   timer.scheduleAtFixedRate(new TimerTask() {
       @Override
       public void run() {
           sender.sendMessage(message);
       }
   }, 1000, 1000);
}

}

В методе sendMessage мы создаем DatagramPacket, DatagramSocket и отправляем наше сообщение. Обрати внимание, что после отправки DatagramSocket закрывается методом close().

В консоле получателя мы видим каждую секунду приходящее сообщение “Hello”, отправленное отправителем. Значит, взаимодействие налажено и все работает правильно.