Java, Программирование

Принципы сетевого взаимодействия по протоколу UDP на языке Java

Время прочтения: 6 мин.

Взаимодействие программ по сети осуществляется посредством набора правил (протоколов). Эталонная модель взаимодействия открытых систем (OSI — Open Systems Interconnection) состоит из семи уровней (Рис. 1), где на транспортном уровне наиболее часто встречаются два типа протоколов: TCP (Transmission Control Protocol) и UDP (User Datagram Protocol).

рис.1 Схема OSI

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

Механизм сокетов

Язык Java имеет мощный инструментарий для сетевого программирования. Разработчику приходится работать в основном с протоколами транспортного и прикладного уровней. Для Java-программиста сетевое программирование — работа с сокетами — абстрактное понятие, пришедшее из мира UNIX, конечная точка соединения, характеризующаяся IP-адресом и портом.

Пример взаимодействия

В рамках одного компьютера эмулируется простое взаимодействие двух сторон по протоколу UDP, с использованием классов из библиотеки java.net. При таком подходе все стороны являются равноправными и разделение в конкретном случае на клиента и сервера сделано для удобства (приложение будет одно, работать клиент и сервер будут в отдельных потоках). На основе библиотек java.awt и javax.swing реализован простейший графический интерфейс (Рис. 2), позволяющий пользователю выбирать IP-адреса и порты обеих сторон, вводить сообщения для отправки серверу. Кроме того, на экране выводятся полученные от сервера ответы и сообщения о состоянии сторон.

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

Решением может стать написание класса – наследника класса DatagramSocket из java.net, логика которого заключается в переопределении методов родителя, добавлением в них «дестабилизации» взаимодействия. Иными словами, при приеме пакетов они отдадутся пользователю не сразу. Например, в некоторых случаях пакеты будут придерживаться до определенного момента, а затем отобразятся сразу целой группой. При этом может быть целенаправленно изменен их порядок. В другом случае пакет будет просто удален, а значит – не доставлен. В третьем – пакет будет продублирован, и так далее. Данный класс поможет выявить ошибки на этапе разработки сетевого приложения. Для программиста при его использовании не будет никаких видимых отличий от стандартного DatagramSocket, поэтому в приведенном ниже коде достаточно будет заменить DatagramSocket на название нашего класса.

I Сервер

Класс сервера реализует интерфейс Runnable (в нем метод run) для того, чтобы быть переданным в конструктор экземпляра класса Thread. DatagramServer включает в себя два поля: DatagramSocket MyServerSocket (взаимодействующий сокет) и DatagramPacket incoming (для хранения пакета, полученного от клиента и ответного сообщения). В конструкторе данного класса мы создаем наш MyServerSocket, указав значения IP-адреса и порта, к которым он будет привязан, и incoming, передав ему в конструкторе буфер для входящих данных.

public DatagramServer(String ServerIP, int ServerPort) {
	try {
		MyServerSocket = new DatagramSocket(ServerPort, Inet-Address.getByName(ServerIP));
		byte[] IncBuf = new byte[ClientServerInformation. BUFFER_SIZE];
		incoming = new DatagramPacket(IncBuf, IncBuf.length);
		GUI.AddMessage("Серевер запущен " + MyServerSocket.get-LocalSocketAddress());
	} catch (IOException e) {
		GUI.AddMessage("Не удалось запустить сервер! " + e.get-Message());
		System.exit(-1);
	}
}

В основной рабочей функции в бессрочном цикле идет прием входящих пакетов — MyServerSocker.receive(incoming) и их разбор — byte[] data = incoming.getData().

В ответном сообщении клиенту отправляется строка, символы которой идут в обратной последовательности. Отправка пакета осуществляется методом send объекта MyServerSocket. По окончанию работы сокет сервера закрывается вызовом метода close.

private void ServerRun() {
	try {
		String message, replay;
		while (true) {
			MyServerSocket.receive(incoming);
			byte[] data = incoming.getData();
			int len = incoming.getLength();
			message = new String(data, 0, len);
			GUI.AddMessage("Клиент " + incoming.getSocketAddress() + " прислал " + message);
			replay = ""; 
			int i = -1;
			while (i < len - 1)
				replay += data[len - i - 1];
			GUI.AddMessage("В ответ отправляется " + replay);
			incoming = new DatagramPacket(replay.getBytes(), len, incoming.getAddress(), incoming.getPort());
			MyServerSocket.send(reply);
		}
	} catch (Exception e) {
		GUI.AddMessage("Ошибка!" + e.getMessage());
		System.exit(-1);
	}
	finally {
		MyServerSocket.close();
	}
}

Вызов данной функции происходит в переопределенном методе run.

II Клиент

Реализация класса клиента во многом схожа с тем, что было написано для сервера. Клиент также реализует интерфейс Runnable. Помимо полей DatagramSocket MyClientSocket и DatagramPacket outcoming, аналогичных соответствующим полям класса DatagramServer, он хранит значения IP-адреса и порта сервера в полях String ServerIP и int ServerPort.

В конструкторе мы не создаем заранее пакет, так как его размер будет зависеть от длины отправляемого сообщения.

public DatagramClient(String ClientIP, int ClientPort, String aServerIP, int aServerPort) {
	try {
		MyClientSocket = new MyIoDatagramSocket(ClientPort, InetAddress.getByName(ClientIP));
		ServerIP = aServerIP; 
		ServerPort = aServerPort;
		GUI.AddMessage("Клиент запущен: " + MyClientSocket.getLocalSocketAddress());
	} catch (IOException e) {
		GUI.AddMessage("Не удалось запустить клиента!" + e.getMessage());
		System.exit(-1);
	}
}
public DatagramClient(String ClientIP, int ClientPort, String aServerIP, int aServerPort) {
	try {
		MyClientSocket = new MyIoDatagramSocket(ClientPort, InetAddress.getByName(ClientIP));
		ServerIP = aServerIP; 
		ServerPort = aServerPort;
		GUI.AddMessage("Клиент запущен: " + MyClientSocket.getLocalSocketAddress());
	} catch (IOException e) {
		GUI.AddMessage("Не удалось запустить клиента!" + e.getMessage());
		System.exit(-1);
	}
}

Во время работы программа будет ожидать на экране ввод пользователем сообщения для отправки серверу. Введенная строка преобразуется в массив байт – byte[] OutBuf = message.getBytes() и затем отправляется в сформированном пакете outcoming, ранее упомянутой функцией send. Далее пакет формируется заново для приема ответа, и прием осуществляется аналогичным образом, как в случае с сервером, посредством функций receive и getData. При завершении работы с сокетом клиента его необходимо закрыть.

private void ClientRun() {
	String message;
	try {
		while (true) {
			GUI.AddMessage("Введите сообщение серверу: ");
			message = GUI.WaitEnterTextMessage();
			byte[] OutBuf = message.getBytes();
			outcoming = new DatagramPacket(OutBuf, OutBuf.length, InetAddress.getByName(ServerIP), ServerPort);
			MyClientSocket.send(outcoming);
			byte[] IncBuf = new byte[65536];
			outcoming = new DatagramPacket(IncBuf, IncBuf. length);
			MyClientSocket.receive(outcoming);
			byte[] data = outcoming.getData();
			message = new String(data, 0, outcoming.getLength());
			GUI.AddMessage("Сервер прислал ответ: " + message);
		}
	} catch (Exception e) {
		GUI.AddMessage("Ошибка!" + e.getMessage());
		System.exit(-1);
	}
	finally {
		MyClientSocket.close();
	}
}

III Основная функция программы

В главной функции программы идет создание экземпляров классов DatagramServer и DatagramClient, а также двух потоков MyServerThread и MyClientThread, в которые помещаются наши клиент и сервер. Затем ожидается нажатие пользователем на форме кнопки «Запустить» – while (GUI.WhileNotRun), и, после этого, обе стороны сетевого взаимодействия включаются в работу.

public static void main(String[] args) {
	GUI.createGUI();
	DatagramServer MyServer = new DatagramServer(GUI.ServerIP, GUI.ServerPort);
	Thread MyServerThread = new Thread(MyServer);
	DatagramClient MyClient = new DatagramClient(GUI.ClientIP, GUI.ClientPort, GUI.ServerIP, GUI.ServerPort);
	Thread MyClientThread = new Thread(MyClient);
	while (GUI.WhileNotRun) ;
	MyServerThread.start();
	MyClientThread.start();
}

В статье продемонстрирована основная последовательность действий при построении сетевых приложений, базирующихся на протоколе UDP. В нее входят: создание сокетов с привязкой к конкретному IP-адресу и порту, формирование сообщения и его отправка, прием данного сообщения и отправка ответа согласно некоторой логике, прием сообщения-ответа, закрытие сокета

Советуем почитать