Коли ми говоримо про мережну взаємодію, ми не можемо не згадати про модель OSI.

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

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

Рівень Тип даних (PDU) Функції Приклади
Транспортний (transport) Сегменти (segment) / Датаграми (datagram) Прямий зв’язок між кінцевими пунктами і надійність TCP, UDP, SCTP, Порти

Спочатку давай розглянемо схему роботи 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”, що надіслане відправником. Отже, взаємодію налагоджено, і все працює правильно.