Internet програмиране с Java
Категория: Интернет
10.2.2002
Работа с UDP сокети
В предишната тема изяснихме как се разработват Java приложения, които си комуникират чрез TCP сокети. Както знаем от първата част на настоящия курс, TCP е протокол, който осигурява надеждна двупосочна поточноориентирана комуникационна линия. Тук ще се занимаем със средствата, които платформата Java ни дава за комуникация чрез единични UDP пакети.
Протоколът UDP осигурява изпращане и получаване на единични пакети с данни, пристигането на които не е гарантирано. Поради факта, че не установява връзка между двата компютъра, които комуникират по между си, UDP генерира по-малък мрежов трафик, отколкото TCP и затова осигурява по-голяма бързина. Той може да се използва, когато трябва да се изпращат малки по размер и независими едно от друго съобщения, но не е подходящ за изпращане на съобщения с голям размер или ако редът на тяхното пристигане е важен. Освен това, както вече знаем, успешното изпращане на един UDP пакет не гарантира, че той ще пристигне или пък че ако изпратим два UDP пакета, те ще пристигнат в същия ред, в който са изпратени. Ето защо, преди да се вземе решение да се използва комуникация по UDP, трябва да се прецени дали този протокол е подходящ.
В Java за поддръжката на UDP сокети разполагаме с класовете java.net.DatagramSocket и java.net
.DatagramPacket. Класът DatagramSocket дава възможност да се свърваме към определен мрежов интерфейс и порт, да изпращаме пакети и да получаваме пакети. Класът DatagramPacket представлява структура от данни, която описва един UDP пакет. Да илюстрираме използването на тези класове чрез един пример. Нека създадем приложението UDPDataServer, което отговаря на клиентски UDP заявки за взимане на текущата дата:
import java.net.*;
public class UDPDateServer {
public static void main(String[] args)
throws Exception {
DatagramSocket socketIn =
new DatagramSocket(12345);
DatagramSocket socketOut =
new DatagramSocket();
System.out.println(
“UDP Date Server started.”);
while (true) {
// Receive а request from a client
byte[] requestBuf = new byte[256];
DatagramPacket packetIn =
new DatagramPacket(requestBuf,
requestBuf.length);
socketIn.receive(packetIn);
int len = packetIn.getLength();
String request =
new String(requestBuf, 0, len);
// Send а response to the client
if (request.startsWith(“PORT=”)) {
int port = new Integer(request.
substring(5)).intValue();
String resp = new java.util.
Date().toString();
byte[] respBuf =
resp.getBytes();
DatagramPacket packetOut =
new DatagramPacket(respBuf,
respBuf.length,
packetIn.getAddress(),
port);
socketOut.send(packetOut);
}
}
}
}
Както се вижда от кода, сървърът отваря два UDP сокета - един на порт 12345 за получаване на UDP пакети и още един на случаен порт, през който да изпраща отговорите на клиентите. След това в безкраен цикъл получава клиентска заявка, като счита, че тя не надвишва 256 байта, извлича от нея IP адреса и порта, където да изпрати отговор, създава отговор (символен низ, съдържащ текущата дата и час) и го изпраща на клиента. IP адресът на клиента се взима от адреса, от който е дошъл пакетът със заявката, а портът се взима от текста на клиентската заявка, която трябва да е във формат “PORT=число”. Важна особеност на програмирането с UDP сокети с Java е, че един обект от класа DatagramSocket може да се използва или само за получаване, или само за изпращане на UDP пакети. Ако един сървър получава пакет, а след това изпраща отговор, той трябва да създаде два сокета, с които да извършва това, както е в нашия пример.
Нека сега напишем клиент за нашия сървър. Той трябва да изпраща на сървъра заявка, в която да задава на кой порт да се получи отговорът и след като получи този отговор, да го отпечата. Разбира се, получаването на отговор не е гарантирано и затова чакането му трябва да е ограничено откъм време. Често срещана грешка е да се опитваме да получим отговора на същия порт, от който сме изпратили заявката. На пръв поглед това изглежда разумно, защото няма да има нужда да изпращаме порта, на който да получаваме отговора. Сървърът би могъл да го вземе от пакета, с който е получил заявката. На практика този подход е грешен, защото, както вече споменахме, не можем да използваме един и същ обект от класа DatagramSocket хем за изпращане, хем за получаване на пакети. Ако използваме два DatagramSocket обекта - един за изпращане и един за получаване, не можем да ги накараме да използват един и същ порт, защото създаването на DatagramSocket обект изисква свободен порт. Ако се опитаме след изпращането на заявката да освободим порта, от който тя е изпратена и веднага да започнем да слушаме на него за отговор, има вероятност през времето, в което още не сме започнали да слушаме, отговорът вече да е пристигнал и да го изпуснем. Както се вижда, и този подход не работи. Ето защо силно се препоръчва въпросът и отговорът при UDP комуникация да са на различни портове. Ето и примерна реализация на клиент за нашия сървър:
import java.net.*;
public class UDPDateClient {
public static void main(String[] args)
throws Exception {
// Open an UDP socket for the response
DatagramSocket socketIn =
new DatagramSocket();
// Send request to the UDP Date Server
DatagramSocket socketOut =
new DatagramSocket();
String request = “PORT=” +
socketIn.getLocalPort();
byte[] requestBuf = request.getBytes();
DatagramPacket packetOut =
new DatagramPacket(requestBuf,
requestBuf.length,
InetAddress.getByName(“localhost”),
12345);
socketOut.send(packetOut);
socketOut.close();
// Receive the server response
byte[] responseBuf = new byte[256];
DatagramPacket packetIn =
new DatagramPacket(responseBuf,
responseBuf.length);
socketIn.setSoTimeout(5000);
socketIn.receive(packetIn);
int len = packetIn.getLength();
String response =
new String(responseBuf, 0, len);
System.out.println(response);
socketIn.close();
}
}
Както се вижда от кода, клиентът отваря UDP сокет за получаване на отговор от сървъра, подготвя един пакет, като записва в него този порт, след което отваря втори сокет и изпраща през него подготвения пакет на адрес localhost:12345, където се очаква да е стартиран сървърът. След това чака за отговор 5 секунди и ако за това време получи пакет с отговор, го отпечатва на конзолата, а в противен случай настъпва изключение.