Философия Java

         

Простой пример сервера и клиента


Этот пример показывает простую работу сервера и клиента используя сокеты. Все, что делает сервер - это просто ожидание соединения, затем использует Socket полученный из того соединения для создания InputStream и OutputStream. Они конвертируются в Reader и Writer, затем в BufferedReader и PrintWriter. После этого, все что он получает из BufferedReader он отправляет на PrintWriter пока не получит строку “END,” после чего, он закрывает соединение.

Клиент устанавливает соединение с сервером, затем создает OutputStream и выполняет те же операции, что и сервер. Строки текста посылаются через результирующий PrintWriter. Клиент также создает InputStream (снова, с соответствующими конверсиями и облачениями) чтобы слушать, что говорит сервер (а в нашем случае он возвращает слова назад).

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

Вот сервер:

//: c15:JabberServer.java

// Очень простой сервер, который только

// отображает то, что посылает клиент.

import java.io.*; import java.net.*;

public class JabberServer { // Выбираем номер порта за пределами 1-1024:

public static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Started: " + s); try { // Блокируем пока не произойдет соединение:

Socket socket = s.accept(); try { System.out.println( "Connection accepted: "+ socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Вывод автоматически обновляется

// классом PrintWriter:

PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } // всегда закрываем оба сокета...


} finally { System.out.println("closing..."); socket.close(); } } finally { s.close(); } } } ///:~

Вы видите, что объекту ServerSocket нужен только номер порта, не IP адрес (т.к. он запущен на этой машине!). Когда Вы вызываете метод accept( ), метод блокирует выполнение программы, пока какой-нибудь клиент не попробует соединиться. То есть, он ожидает соединение, но другие процессы могут выполняться (см. Главу 14). Когда соединение сделано, accept( ) возвращает объект Socket представляющий это соединение.





Ответственность за очищение сокетов is crafted carefully here. Если конструктор ServerSocket завершается неуспешно, программа просто завершается (обратите внивание, что мы должны считать что конструктор ServerSocket не оставляет открытых сетевых сокетов если он завершается неудачно). В этом случает, main( ) выбрасывает исключение IOException и блок try не обязателен. Если конструктор ServerSocket завершается успешно, то остальные вызовы методов должны быть окружены блоками try-finally, чтобы убедиться, что независимо от того как блок завершит работу, ServerSocket будет корректно закрыт.

Та же логика используется для Socket возвращаемого методом accept( ). Если вызов accept( ) неуспешный, то мы должны считать что Socket не существует и не держит никаких ресурсов, так что он не нуждается в очистке. Но, если вызов успешный, следующи объявления должны быть окружены блоками try-finally так что в случае неуспешного вызова Socket будет очищен. Заботиться здесь об этом обязательно, т.к. сокеты используют важные ресурсы располагающиеся не в памяти, так что Вы должны тщательно очищать их (поскольку в Java нет деструктора, чтобы сделать это за Вас).

И ServerSocket и Socket созданные методом accept( ) печатаются в System.out. Это значит, что их методы toString( ) вызываются автоматически. Вот что получается:

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] Socket[addr=127.0.0.1,PORT=1077,localport=8080]

Скоро Вы увидите как how они объединяются вместе с тем что делает клиент.



Следующая часть программы выглядит как программа для для открытия файлов для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream и объект OutputStream конвертируются в объекты Reader и Writer используя классы “конвертеры” InputStreamReader и OutputStreamWriter, соответственно. Вы можете также использовать напрямую классы из Java 1.0 InputStream и OutputStream, но с выводом есть явное преимущество при использовании Writer. Это реализуется с помощью PrintWriter, в котором перегруженный конструктор берет второй аргумент, а boolean флаг который индицирует когда какой автоматически сбрасывает вывод в конце каждого вывода println( ) (но не print( )) выражения. Каждый раз, когды Вы направляете данные в out, его буфер должен сбрасываться так информация передается о сети. Сброс важен для этого конкретного примера, т.к. клиент и сервер ждут строку данных друг от друга, перед тем, как что-то сделать. Если сброса буферов не происходит, информация не будет отправлена по сети, пока буфер полон, что вызовем много проблем в этом примере.

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

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

Бесконечный цикл while читает строки из BufferedReader in и записывает информацию вSystem.out and to the PrintWriter out. Запомните, что in и out могут быть любыми потоками, они просто соединены в сети.



Когда клиент отсылает строку, состоящую из “END,” программа прерывает выполнение цикла и закрывает Socket.

Вот клиент:

//: c15:JabberClient.java

// Очень простой клиент, который просто отсылает строки серверу

// и читает строки, которые посылает сервер

import java.net.*; import java.io.*;

public class JabberClient { public static void main(String[] args) throws IOException { // Установка параметра в null в getByName()

// возвращает специальный IP address - "Локальную петлю",

// для тестирования на одной машине без наличия сети

InetAddress addr = InetAddress.getByName(null); // Альтернативно Вы можете использовать

// адрес или имя:

// InetAddress addr =

// InetAddress.getByName("127.0.0.1");

// InetAddress addr =

// InetAddress.getByName("localhost");

System.out.println("addr = " + addr); Socket socket = new Socket(addr, JabberServer.PORT); // Окружаем все блоками try-finally to make

// чтобы убедиться что сокет закрывается:

try { System.out.println("socket = " + socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Вывод автоматически сбрасывается

// с помощью PrintWriter:

PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); for(int i = 0; i < 10; i ++) { out.println("howdy " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } finally { System.out.println("closing..."); socket.close(); } } } ///:~

В методе main( ) Вы видите все три пути для возврата IP адреса локальной петли: используя null, localhost, либо явно зарезервированный адрес 127.0.0.1. Конечно, если Вы хотите соединиться с машиной в сети Вы подставляете IP адрес этой машины. Когда InetAddress addr печатается (с помощью автоматического вызова метода toString( )) получается следующий результат:

localhost/127.0.0.1

Подстановкой параметра null в getByName( ), она по умолчанию использует localhost, и это создает специальный адрес 127.0.0.1.



Обратите внимание, что Socket названный socket создается и с типом InetAddress и с номером порта. Чтобы понимать, что это значит, кгда Вы печаете один из этих объектов Socket, помните, что соединение с Интернет определяется уникально этими четырьмя элементами данных: clientHost, clientPortNumber, serverHost, и serverPortNumber. Когда сервер запускается, он берет присвоенный ему порт (8080) на localhost (127.0.0.1). Когда клиент приходит, распределяется следующий доступный порт на той же машине, в нашем случае - 1077, который, так случилось, оказался расположен на той же самой машине (127.0.0.1), что и сервер. Теперь, необходимо данные перемещать между клиентом и сервером, каждая сторона должнва знать, куда их посылать. Поэтому, во время процесса соединения с “известным” сервером, клиент посылает “обратный адрес”, так что сервер знает, куда отсылать его данные. Вот, что Вы видите в примере серверной части:

Socket[addr=127.0.0.1,port=1077,localport=8080]

Это значит, что сервер тоьлко что принял соединение с адреса 127.0.0.1 и порта 1077 когда слушал свой локальный порт (8080). На стороне клиента:

Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

это значит, что клиент создал соединение с адресом 127.0.0.1 и портом 8080, используя локальный порт 1077.

Вы увидите, что каждый раз, когда Вы запускаете нового клиента, номер локального порта увеличивается. Отсчет начинается с 1025 (предыдущие являются зарезервированными) и продолжается до того момента, пока Вы не перезагрузите машину, после чего он снова начинается с 1025. (На UNIX машинах, как только достигается максимальное число сокетов, нумерация начинается с самого меньшего доступного номера.)

Как только объект Socket создан, процесс превода его в BufferedReader и PrintWriter тот же самый, что и в серверной части (снова, в обоих случаях Вы начинаете с Socket). Здесь, клиент инициирует соединение отсылкой строки “howdy” следующе за номером. Обратите внимание, что буфер должен быть снова сброшен (что происходит автоматически по второму аргументу в конструкторе PrintWriter). Если буфер не будет сброшен, все общение зависнет, т.к. строка “howdy” никогда не будет отослана (буфер не будет достаточно полным, чтобы выполнить отсылку автоматически). Каждая строка, отсылаемая сервером обратно записывается в System.out для проверки, что все работает правильно. Для прекращения общения, отсылается условный знак - строка “END”. Если клиент прервывает соединение, то сервер выбрасывает исключение.

Вы видите, что такая же забота здесь тоже присутствует, чтобы убедиться, что ресурсы представленные Socket корректно освобождаются, с помощью блока try-finally.

Сокеты создают “подписанное” (dedicated) соединение, которое сохраняется, пока не произойдет явный разрыв соединения. (Подписанное соединение может еще быть разорвано неявно, если одна сторона , либо промежуточное соединение, разрушается.) Это значит, что обе стороны заблокированы в общении и соединение постоянно открыто. Кажется, что это просто логический подход к передаче данных, однако это дает дополнительную нагрузку на сеть. Позже, в этой главе Вы увидите другой метод передачи данных по сети, в котором соединения являются временными.


Содержание раздела