Завершение работы сервера и клиента

После завершения передачи данных необходимо закрыть входной и выходной потоки в приложениях, вызвав для объектов потоков метод close():

is.close();

os.close();

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

s.close();

Серверное приложение, кроме того, должно закрыть соединение, вызвав метод close() для объекта класса ServerSocket:

ss.close();

3.2 Конструкторы и методы класса Socket

Чаще всего для создания сокетов в клиентских приложениях используются один из двух конструкторов:

public Socket(String host, int port);

public Socket(InetAddress address, int port);

Первый из этих конструкторов позволяет указывать адрес серверного узла в виде текстовой строки (доменный адрес или IP-адрес), а второй - в виде ссылки на объект класса InetAddress. Вторым параметром задается номер порта, с использованием которого будут передаваться данные.

Заметим, что апплет может получить текстовую строку, содержащую доменный адрес того узла, с которого он был загружен на данный локальный хост, следующим вызовом:

String baseAddress=getCodeBase().getHost();

В класса Socket есть еще два конструктора, последний параметр которых задает типа сокета. Но работать с ними не рекомендуется. Для потоковых сокетов обычно используются первые два конструктора, а для работы с датаграммами - отдельный класс DatagramSocket.

Методы getInputStream() и getOutputStream() класса Socket предназначены для создания соответственно входного и выходного потоков. Эти потоки связаны с сокетом и должны быть использованы для передачи данных по каналу связи.

Методы getInetAddress() и getPort() позволяют определить адрес IP и номер порта, связанные с данным сокетом (для удаленного узла). Метод getLocalPort() возвращает для данного сокета номер локального порта.

После того, как работа с сокетом завершена, его необходимо закрыть методом close().

3.3 Пример использования потоковых сокетов

Рассмотрим в качестве примера два приложения Sеrver и Client (пример 1), работающих с потоковыми сокетами. Одно из них выполняет роль сервера, а другое служит клиентом. Эти приложения иллюстрируют применение потоковых сокетов для создания канала и передачи данных между клиентским и серверным приложением.

Приложение Server выводит на печать строку "Socket Server Application" и затем переходит в состояние ожидания соединения с клиентским приложением.

Приложение Client выводит сообщение "Socket Client Application" и устанавливает соединение с сервером, используя потоковый сокет с тем же самым номером, какой был задан в серверном приложении. В качестве запуска процесса передачи данными клиентское приложение помещает в свой выходной поток нулевой символ для передачи его серверу.

До нажатия пользователем символа ‘Q’ (или ‘q’) приложение Client в цикле: через свой входной поток получает от сервера количество переданных серверу символов; печатает это число; дает возможность пользователю ввести символ на консоли (любой символ и Ctrl-Z в качестве признака конца ввода); а затем передает этот символ серверу посредством записи его в свой выходной поток.

В свою очередь приложение Server в цикле: считывает из своего входного потока символ, переданный ему клиентом; в случае нажатия на клавишу ‘Q’ (или ‘q’) прекращает свою работу, иначе увеличивает количество переданных ему символов и записывает его в свой выходной поток для передачи его клиенту.

Отметим, что для получения и приема данных используются буферизированные потоки форматированных данных.

/*------------- Пример 1. Файл Server.java -------------*/

import java.io.*;

import java.net.*;

class Server

{ public static void main(String args[])

{ char b; int count=-1; boolean quit=false;

System.out.println("Socket Server Application");

System.out.println("---- Start ----");

try

{ // установка канала связи

ServerSocket ss=new ServerSocket(2000);

// ожидание соединения

Socket s=ss.accept();

// входной поток для приема данных от клиента

DataInputStream dataIn=new DataInputStream(

new BufferedInputStream(

s.getInputStream()));

// выходной поток для записи данных для клиента

DataOutputStream dataOut=new DataOutputStream(

new BufferedOutputStream(

s.getOutputStream()));

// цикл обработки команд от клиента

while(!quit)

{ // чтение символа, переданного клиентом

b=dataIn.readChar(); System.out.println(""+b);

if(b!=-1)

{ if(b=='Q'||b=='q') quit=true;

else

{ count++;

// передача числа клиенту

dataOut.writeInt(count);

dataOut.flush();

}

}

}

dataIn.close(); // закрытие входного потока

dataOut.close(); // закрытие выходного потока

s.close(); // закрытие сокета

ss.close(); // закрытие соединения

}

catch(IOException ioe)

{ System.out.println(ioe.toString());

}

System.out.println("---- Finish ----");

}

}

/*------------- Пример 1. Файл Client.java -------------*/

import java.io.*;

import java.net.*;

class Client

{ public static void main(String args[])

{ int b=0; boolean quit=false;

System.out.println("Socket Client Application");

try

{ // сокет для связи с сервером

// в процессе отладки сетевых приложений, когда

// сервер и клиент работают на одном узле,

// в качестве адреса сервера используется строка

// "localhost" - «сервер работает на этом же узле»

Socket s=new Socket("localhost",2000);

// в рабочей версии клиента указывается IP-адрес сервера

//Socket s=new Socket("194.84.124.60",2000);

//Socket s=new Socket("gosha",2000);

//Socket s=new Socket("gosha.vvsu.ru",2000);

// входной поток для приема данных от сервера

DataInputStream dataIn=new DataInputStream(

new BufferedInputStream(

s.getInputStream()));

// выходной поток для передачи данных серверу

DataOutputStream dataOut=new DataOutputStream(

new BufferedOutputStream(

s.getOutputStream()));

// инициализирующая передача данных серверу

dataOut.writeChar(0); dataOut.flush();

// цикл ввода команд серверу

while(!quit)

{ // чтения числа, переданного сервером

if(b!=-1)

{ b=dataIn.readInt();

System.out.println(" "+b);

}

// ввод пользователем символа

b=System.in.read();

if(b!=-1)

{ if((char)b=='Q'||(char)b=='q') quit=true;

// передача символа серверу

dataOut.writeChar(b);

dataOut.flush();

}

}

dataIn.close(); // закрытие входного потока

dataOut.close(); // закрытие выходного потока

s.close(); // закрытие сокета

}

catch(IOException ioe)

{ System.out.println(ioe.toString());

}

}

}

4. Датаграммные сокеты (несвязываемые датаграммы)

Протокол TCP/IP поддерживает также доставку несвязываемых датаграмм и их возврат с помощью UDP (User Datagram Packet, Пакет пользовательских датаграмм). Датаграммы UDP, также как и сокеты TCP, позволяют связаться с удаленным хостом по протоколу TCP/IP. В отличие от сокетов TCP, осуществляющих связь с логическим соединением, датаграммы, UDP связываются без логического соединения. Если сокет TCP можно сравнить с телефонным звонком, то датаграмму UDP можно сравнить с телеграммой. Доставка датаграммы UDP не гарантирована, и даже если такая датаграмма доставлена, нет гарантии, что она доставлена правильно и, возможно, придется иметь дело с утерянными или неупорядоченными пакетами данных.

Из-за ненадежности UDP большинство программистов при написании сетевых приложений предпочитают работать с сокетами TCP. Однако они работают быстрее потоковых сокетов и обеспечивают возможность широковещательной рассылки пакетов данных одновременно всем узлам сети.

Для работы с датаграммами приложение должно создать объект на базе класса DatagramSocket, а также подготовить объект класса DatagramPacket, в который будет записан принятый от партнера по сети блок данных.

Канал, а также входные и выходные потоки создавать не нужно. Данные передаются и принимаются методами send() и receive(), определенными в классе DatagramSocket.

4.1 Конструкторы и методы класса DatagramSocket

Рассмотрим конструкторы и методы класса DatagramSocket, предназначенный для создания и использования несвязанных датаграмм.

public DatagramSocket(int port);

public DatagramSocket();

Первый из этих конструкторов позволяет определить порт для сокета, второй предполагает использование любого свободного порта.

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

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

С помощью метода getLocalPort() приложение всегда может узнать номер порта, закрепленного за данным сокетом.

Прием и передача данных на датаграммном сокете выполняется соответственно с помощью методов receive() и send() (метод receive() ждет(!) получения датаграммы). В качестве параметра этим методам передается ссылка на пакет данных (соответственно принимаемый и передаваемый), определенный как объект класса DatagramPacket (см. далее).

Так как «сборка мусора» в Java выполняется только для объектов, находящихся в оперативной памяти, то такие объекты как потоковые и датаграммные сокеты следует закрывать самостоятельно. В классе DatagramSocket для этого определен метод close().

Конструкторы и методы класса DatagramPacket

Перед тем как принимать или предавать данные с использованием методов receive() и send() класса DatagramSocket, необходимо подготовить объекты класса DatagramPacket. Метод receive() запишет в такой объект принятые данные, а метод send() перешлет данные из объекта класса DatagramPacket узлу, адрес которого указан в пакете.

Подготовка объекта класса DatagramPacket для приема пакетов выполняется с помощью следующего конструктора:

public DatagramPacket(byte buf[], int length);

Этому конструктору передается ссылка на массив buf, в который нужно будет записать данные и размер этого массива length.

Если необходимо подготовить пакет для передачи, то следует воспользоваться конструктором, который дополнительно позволяет задать IP-адрес узла и номер порта адресата:

public DatagramPacket(byte buf[], int length, InetAddress add, int port);

Таким образом, информация о том, в какой узел и на какой порт необходимо доставить пакет данных, хранится не в сокете, а в пакете, то есть в объекте класса DatagramPacket.

Метод getData() класса DatagramPacket возвращает ссылке на массив данных пакета. Размер пакета, данные из которого храняться в этом массиве, легко определить с помощью метода getLength(). Методы getAddress() и getPort() позволяют определить адрес и номер порта узла , откуда пришел пакет, или узла, для которого предназначен пакет.