Распределенные вычисления

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

Программист должен знать множество деталей о сети, а иногда, также об аппаратном обеспечении.

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

Однако, основная идея распределенного программирования не так сложна, и легко резюмируется в библиотеках Java. Вы хотите:

 

Сетевое программирование

Одним из больших достижений Java является безболезненное общение в сети.

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

Насколько возможно, внутренние детали сетевого общения абстрагированы и обрабатываются внутри JVM и локальной машины, на которой установлена среда Java.

Модель программирования такая же, как Вы используете в файле; в действительности, Вы просто окутываете сетевое соединение (“сокет”) потоковым объектом, и Вы действуете используя те же вызовы методов, что и с другими потоковыми объектами.

К тому же, встроенная в Java множественность процессов очень удобна при общении в сети: установка нескольких соединений сразу.

.

Идентификация машины

Конечно, чтобы убедиться, что соединение установлено с конкретной машиной в сети, должен быть способ уникальной идентификации машины в сети.

Раньше, при работе в сети было достаточно предоставить уникальные имена для машин внутри локальной сети. Но, Java работает в Internet, который требует уникальной идентификации машины с любой другой во всей сети по всему миру.

Это достигается с помощью IP (Internet Protocol) адресации, которая может иметь две формы:

  1. Обычная DNS (Domain Name System) форма. Мое доменное имя - bruceeckel.com, и если у меня есть компьютер с именем Opus в моем домене, его доменное имя может быть Opus.bruceeckel.com. Это в точности тип имени, который Вы используете при отсылке почты, и очень часто включается в WWW адрес.
  2. С другой стороны, Вы можете использовать “четырехточечную” форма, в которой четыре номера разделены точками, например 123.255.28.120.

В обоих случаях, IP адрес представляется как 32 битное значение (каждое число из 4-х не может превышать 255), и Вы можете получить специальный объект Java для представления этого номера из формы, представленной выше с помощью метода static InetAddress.getByName( ) в пакете java.net.

Результат это объект типа InetAddress, который Вы можете использовать для создания “сокета”, как Вы позднее увидите.

Следующая программа использует InetAddress.getByName( ) для определения Вашего IP адреса. Чтобы использовать его, Вы должны знать имя своего компьютера. В Windows, зайдите в “Settings”, “Control Panel”, “Network”, а затем выберите страничку “Identification”. “Computer name” это имя, которое необходимо задать в командной строке.

//: c15:WhoAmI.java
// Определяет Ваш сетевой адрес
// когда Вы подключены к Internet.
import java.net.*;
 
public class WhoAmI {
  public static void main(String[] args) 
                               throws Exception {
    if(args.length != 1) {
      System.err.println("Usage: WhoAmI MachineName");
      System.exit(1);
    }
    InetAddress a = InetAddress.getByName(args[0]);
    System.out.println(a);
  }
} ///:~

.

Сервера и клиенты

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

Но как они находят друг друга? Это как потеряться в парке развлечений: одна машина должна оставаться на месте и слушать, пока другая машина не скажет, “Эй! Где ты?”

Машина, которая “остается на одном месте” называется сервером, а тот, который ищет, называется клиентом. Это различие важно только когда клиент пытается подключиться к серверу. Как только они соединятся, происходит процесс двухстороннего общения и не важно, что один является сервером, а другой - клиентом.

Итак, работа сервера - слушать соединение, и это выполняется с помощью специального серверного объекта, который Вы создаете.

Работа клиента - попытаться создать соединение с сервером, что выполняется с помощью специального клиентского объекта.

Как только соединение установлено, Вы увидите, что у клиента и сервера соединение магически превращается в потоковый объект ввода/вывода, и с этого момента Вы можете рассматривать соединение как файл, который Вы можете читать, и в который Вы можете записывать.

Т.о., после установления соединения Вы будете использовать уже знакомые Вам команды ввода/вывода из Главы 11. Это одно из отличных расширений сетевой библиотеки Java.

 

Тестирование программ без наличия сети

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

Создатели интернет протокола были осведомлены о таких проблемах, и они создали специальный адрес, называемый localhost, “локальная петля”, который является IP адресом для тестирования без наличия сети.

Обычный способ получения этого адреса в Java это:

InetAddress addr = InetAddress.getByName(null);

Вы можете создать адрес локальной петли, установкой строкового параметра localhost:

InetAddress.getByName("localhost");

, либо с помощью четырехточечной формы для именования зарезервированного IP адреса для петли:

InetAddress.getByName("127.0.0.1");

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

 

Порт: уникальное место внутри машины

IP адреса недостаточно для индикации уникального сервера, т.к. много серверов может существовать на одной машине.

Каждая машина в IP также содержит порты, и когда Вы устанавливаете клиента или сервера Вы должны выбрать порт, по которому сервер и клиент договорились соединиться.

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

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

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

Системные службы резервируют номера портов с 1 по 1024, так что Вы не должны использовать ни один из портов, который Вы знаете, что он используется.

Первый выбор порта для примеров в этой книге это порт номер 8080 (в память почтенного и древнего 8-битного чипа Intel 8080 на моем первом компьютере, с операционной системой CP/M).

 

Сокеты

Сокет это программная абстракция, используемая для представления “терминалов” соединений между двумя машинами.

Для данного соединения, существует сокет на каждой машине, и Вы можете представить гипотетический “кабель” соединяющий две машины, каждый конец которого вставлен в сокет. Конечно, какое аппаратное обеспечение и кабель между ними неизвестно. Основной смысл абстракции в том, что нам не нужно знать больше, чем необходимо.

В Java, Вы создаете сокет для установления соединения с другой машиной, затем Вы получаете InputStream и OutputStream (либо с помощью соответствующих преобразователей, Reader и Writer) из сокета, который соответствующим образом представляет соединение, как потоковый объект ввода вывода.

Есть два класса сокетов, основанных на потоках: ServerSocket - используется сервером, чтобы “слушать” входящие соединения и Socket - используется клиентом для инициирования соединения.

Как только клиент создает соединение по сокету, ServerSocket возвращает (с помощью метода accept( ) ) соответствующий объект Socket по которому будет происходить связь на стороне сервера.

Начиная с этого момента, у Вас появляется соединение Socket к Socket, и Вы считаете эти соединения одинаковыми, потому что они действительно одинаковые.

В результате, Вы используете методы getInputStream( ) и getOutputStream( ) для создания соответствующих объектов InputStream и OutputStream из каждого Socket.

Они должны быть обернуты внутри буферов и форматирующих классов, как и любой другой потоковый объект.

ServerSocket может показаться еще одним примером запутанной схемы имен в библиотеках Java. Вы можете подумать, что ServerSocket лучше назвать “ServerConnector” либо как-нибудь иначе без слова “Socket” в нем.

ServerSocket кажется немного неправильно названным, т.к. его работа в действительности не быть сокетом, а просто создавать объект Socket, когда кто-то другой к нему подключается.

Однако ServerSocket создает физический “сервер” либо слушающий сокет на серверной машине. Этот сокет слушает входящие соединения и затем возвращает “установленный” сокет (с определенными локальными и удаленными конечными точками) посредством метода accept( ).

Когда Вы создаете ServerSocket, Вы задаете для него только номер порта. Вам не нужно задавать IP адрес, т.к. он уже существует на машине. Однако когда Вы создаете Socket, Вы должны задать и IP адрес и номер порта машины, с которой Вы хотите соединиться. (Тем не менее, Socket который возвращается методом ServerSocket.accept( ) уже содержит всю эту информацию.)

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

Вот сервер:

//: c15:JabberServer.java

 

import java.io.*;

import java.net.*;

 

public class JabberServer { 

 

  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();  // blocked!

      try {

        System.out.println("Connection accepted: "+ socket);

       

        BufferedReader in =

          new BufferedReader(

            new InputStreamReader(

              socket.getInputStream()));

       

        PrintWriter out =

          new PrintWriter(

            new BufferedWriter(

              new OutputStreamWriter(

                socket.getOutputStream())),true);  // flush

        

        while (true) { 

          String str = in.readLine();

          if (str.equals("END")) break;

          str = "Echoing: " + str;

          System.out.println(str);

          out.println("\n"+str);

        }

     

      } finally {

             System.out.println("closing Socket");

             socket.close();

      }

    } finally {

           System.out.println("closing ServerSocket");

           s.close();

    }

  }

} ///:~

Вот клиент:

//: 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) соединение, которое сохраняется, пока не произойдет явный разрыв соединения. (Подписанное соединение может еще быть разорвано неявно, если одна сторона , либо промежуточное соединение, разрушается.) Это значит, что обе стороны заблокированы в общении и соединение постоянно открыто. Кажется, что это просто логический подход к передаче данных, однако это дает дополнительную нагрузку на сеть.

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

 

Обслуживание нескольких клиентов

import java.io.*;

import java.net.*;

class ServeOneJabber extends Thread {

       private Socket socket;

       private BufferedReader in;

       private PrintWriter out;

       public ServeOneJabber(Socket s) throws IOException {

         socket = s;

         in =

           new BufferedReader(

             new InputStreamReader(

               socket.getInputStream()));

         out =

           new PrintWriter(

             new BufferedWriter(

               new OutputStreamWriter(

                 socket.getOutputStream())), true);

 

         start();

       }

       public void run() {

         try {

           while (true) { 

             String str = in.readLine();

             if (str.equals("END")) break;

             System.out.println("Echoing: " + str);

             out.println("\n"+str);

           }

           System.out.println("closing...");

         } catch(IOException e) {

           System.err.println("IO Exception");

         } finally {

           try {

             socket.close();

           } catch(IOException e) {

             System.err.println("Socket not closed");

           }

         }

     }

}

public class MultiJabberServer { 

  static final int PORT = 8088;

  public static void main(String[] args) throws IOException {

    ServerSocket s = new ServerSocket(PORT);

    System.out.println("Server Started "+s);

    try {

      while(true) {

         Socket socket = s.accept();

        try {

          new ServeOneJabber(socket);

        } catch(IOException e) {

          socket.close();

        }

      }

    } finally {

      s.close();

    }

  }

} ///:~