Лекция 11. Сеть

Введение

Введение

В данной лекции рассматриваются сетевые приложения на языке Java.

Предполагается использования стека протоколов TCP/IP для обмена информацией

  1. между процессами на одном компьютере;
  2. между процессами на разных компьютерах, подключенных к сети.

Сетевые приложения на языке Java строятся с использованием технологии Клиент/Сервер.

Приложения состоят из двух частей: Клиента и Сервера.

  1. Сервер запускается первым и после некоторых настроек переходит в состояние ожидания, используя определенный порт.
  2. Клиент устанавливает соединение, используя IP-адрес компьютера и порт, который прослушивает сервер.
  3. Общение двух программ выполняется через потоки ввода/вывода.
  4. После обмена информацией клиент разрывает соединение.

Соединение может быть установлено двумя способами:

  1. С помощью потоков (TCP)
  2. С помощью датаграмм (UDP)
_images/tcp-udp.png _images/prot.png

Для обмена информацией существует специальная абстракция сокет (гнездо). Гнезда создаются как у сервера, так и у клиента.

Для работы с сетью в Java предусмотрена иерархия пакетов java.net.*

Приложения можно разделить на те, в которых сервер может устанавливать одновременно только одно соединение с клиентом, и на те, в которых может устанавливаться одновременно несколько соединений.

Адреса и имена

IP адреса и доменные имена

Для адресации сервера в сети могут использоваться IP-адреса или доменные имена.

Преобразование между ними происходит с помощью класса InetAddress:

InetAddress addr = InetAddress.getByName("www.google.ru");
System.out.println(addr); // выводим IP-адрес

Для работы на локальном компьютере можно использовать IP-адрес 127.0.0.1 или имя “localhost”.

Еще можно получить локальный адрес так:

InetAddress addr = InetAddress.getByName(null);

Сокеты

На стороне сервера создаются два сокета: ServerSocket и просто Socket.

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

ServerSocket server = new ServerSocket(1234);
...
Socket client = server.accept(); // ожидание подключений

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

Для создания сокета на стороне клиента нужно указать IP-адрес и порт сервера:

Socket socket = new Socket("192.168.0.1",1234);

После установления соединения можно работать с потоками, связанными с сокетами:

InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();

Ошибки

В процессе установления соединения и обмена данными необходимо перехватывать исключения:

  • IOException - при создании серверного сокета (порт занят). При обработке пользовательского запроса (подключение к порту). При получении потока ввода/вывода. При чтении/записи сообщения из/в поток(а).
  • UnknownHostException - при соединении на стороне клиента (хост не найден).
  • NoRouteToHostException - сервер недоступен.
  • ConnectException - запрос на соединение отклонен.

Порядок перехвата исключений:

try {
    ...
} catch(UnknownHostException e) {
    ...
}
catch(NoRouteToHostException e) {
    ...
}
catch(ConnectException e) {
    ...
}
catch(IOException e) {
    ...
}

Простой пример

Для иллюстрации простого клиент/серверного приложения

на сокетах создадим две программы:

  1. Server - серверная часть (класс Server)
  2. Client - клиентская часть (класс Client)

Для связи необходимо выбрать свободный порт в системе, например 1234.

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

public class Server
{
  public static void main(String[] args) throws IOException {
    System.out.println("Старт сервера");

    // поток для чтения данных
    BufferedReader in = null;
    // поток для отправки данных
    PrintWriter    out= null;

    // серверный сокет
    ServerSocket server = null;
    // сокет для обслуживания клиента
    Socket       client = null;
    ..
// создаем серверный сокет
try {
  server = new ServerSocket(1234);
} catch (IOException e) {
  System.out.println("Ошибка связывания с портом 1234");
  System.exit(-1);
}
..
try {
  System.out.print("Ждем соединения");
  client= server.accept();
  System.out.println("Клиент подключился");
} catch (IOException e) {
  System.out.println("Не могу установить соединение");
  System.exit(-1);
}
// создаем потоки для связи с клиентом
in  = new BufferedReader(
      new InputStreamReader(client.getInputStream()));
out = new PrintWriter(client.getOutputStream(),true);
String input,output;

// цикл ожидания сообщений от клиента
System.out.println("Ожидаем сообщений");
while ((input = in.readLine()) != null) {
 if (input.equalsIgnoreCase("exit"))
   break;
 out.println("Сервер: "+input);
 System.out.println(input);
}

Закрываем все соединения

    out.close();
    in.close();
    client.close();
    server.close();
  }
}
import java.io.*;
import java.net.*;

public class Client {
  public static void main(String[] args) throws IOException {
    System.out.println("Клиент стартовал");
    Socket server = null;

    //  адрес (имя) сервера должны передаваться как параметр
    if (args.length==0) {
      System.out.println("Использование: java Client hostname");
      System.exit(-1);
    }
    ..
System.out.println("Соединяемся с сервером "+args[0]);

server = new Socket(args[0],1234);
BufferedReader in  = new BufferedReader(
   new  InputStreamReader(server.getInputStream()));
PrintWriter out =
   new PrintWriter(server.getOutputStream(),true);
BufferedReader inu =
   new BufferedReader(new InputStreamReader(System.in));

String fuser,fserver;

Основной цикл отправки сообщений серверу

while ((fuser = inu.readLine())!=null) {
   out.println(fuser);
   fserver = in.readLine();
   System.out.println(fserver);
   if (fuser.equalsIgnoreCase("close"))
     break;
   if (fuser.equalsIgnoreCase("exit"))
     break;
 }

Закрытие соединения и выход

    out.close();
    in.close();
    inu.close();
    server.close();
  }
}

Запуск сервера:

java Server

Запуск клиента (для сервера на локальной машине):

java Client localhost

Обмен двоичными данными

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

Рассмотрим реализацию сервера:

InputStream  in = null;
OutputStream out= null;
..
in  = client.getInputStream();
try {
  out = new FileOutputStream("fromClient.txt");
} catch(FileNotFoundException ex) {
  System.out.println("Ошибка создания файла!");
  System.exit(-1);
}
byte[] data =new byte[1024];
int count;

try {
  while ((count = in.read(data)) > 0) {
    out.write(data, 0, count);
  }
}
catch (IOException e) {
  System.out.println("Ошибка чтения/записи данных");
  System.exit(-1);
}

Реализация на стороне клиента:

InputStream in = null;
OutputStream out = null;
File file = null;
...
file = new File("client.txt");
try {
  in = new FileInputStream(file);
  out = server.getOutputStream();
  byte[] bytes = new byte[1024];
  int count;
  while ((count = in.read(bytes)) > 0) {
    out.write(bytes, 0, count);
  }
}
catch (IOException ex) {
  System.out.println("Ошибка ввода/вывода");
  System.exit(-1);
}

Рассмотрим пример, в котором клиент должен передать серверу массив байт определенной длины.

Фрагмент реализации сервера:

...
    in  = client.getInputStream();

    byte[] responseBytes = new byte[15];
    byte[] len=new byte[1];
    int bytesRead = 0;
    try {
       in.read(len);
       System.out.println(len[0]);
       bytesRead = in.read(responseBytes);
       System.out.println(bytesRead);
    } catch (IOException e) {
       e.printStackTrace();
    }
    for(int i=0;i<bytesRead;i++)
       System.out.printf("%x\n",responseBytes[i]);
       ...

Фрагмент кода клиента:

Socket socket;
byte[] header = {0x53, 0x41, 0x4D, 0x50, 0x4C, 0x45};
OutputStream out;
InputStream in;
BufferedOutputStream bufOut;
 ...
try {
    out = socket.getOutputStream();
    bufOut = new BufferedOutputStream(out);
    in = socket.getInputStream();
} catch (IOException e) {
    e.printStackTrace();
    return;
}
byte msgSize=(byte)header.length;
try {
    bufOut.write(msgSize);
    bufOut.flush();
    bufOut.write(header);
    bufOut.flush();
} catch (IOException e) {
    e.printStackTrace();
}

Для организации собственного протокола для обмена данными с сервером будет полезна пара функций, выполняющих преобразование int в массив байт и, наоборот.

public static final byte[] intToByteArray(int value) {
   return new byte[] {
     (byte)(value & 0xff),
     (byte)(value >> 8 & 0xff),
     (byte)(value >> 16 & 0xff),
     (byte)(value >>> 24)
   };
}
public static final int byteArrayToInt(byte[] value) {
    int ret = ((value[0] & 0xFF) << 24) |
              ((value[1] & 0xFF) << 16) |
               ((value[2] & 0xFF) << 8) |
               (value[3] & 0xFF);
    return ret;
}

Работа по стандартным протоколам

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

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

public class HttpClient
{
    public static void main(String[] args) throws IOException
    {
        Socket socket = new Socket();
        String host = "localhost";
        PrintWriter out = null;
        BufferedReader in = null;
try {
  socket.connect(new InetSocketAddress(host , 80));
  System.out.println("Соединение установлено");
  out = new PrintWriter(socket.getOutputStream(), true);
  in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}
catch (UnknownHostException e) {
  System.err.println("Невозможно соединиться с: " + host);
  System.exit(1);
}

String message = "GET / HTTP/1.0\r\n\r\n";
out.println( message );
System.out.println("Сообщение послано");
String response;
while ((response = in.readLine()) != null) {
   System.out.println( response );
}
}
}

Многопоточный сервер

Реализация сервера

Многопоточная реализация

Для организации связи с несколькими клиентами сервер нужно сделать многопоточным.

В главном потоке работает бесконечный цикл с вызовом accept. При получении запроса на соединения создается дополнительный поток со своим сокетом, который обслуживает новое соединение. Код сервера:

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

class ServerOne extends Thread {
 private Socket socket;
 private BufferedReader in;
 private PrintWriter out;

 public ServerOne(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("Получено: " + str);
            out.println(str);
         }
         System.out.println("Соединение закрыто");
      }
      catch (IOException e) {
         System.err.println("Ошибка чтения/записи");
      }
      finally {
         try {
            socket.close();
         }
         catch (IOException e) {
            System.err.println("Сокет не закрыт");
         }
      }
   }
}
public class Server {
   static final int PORT = 1234;

   public static void main(String[] args) throws IOException {
      ServerSocket s = new ServerSocket(PORT);
      System.out.println("Мультипоточный сервер стартовал");
      try {
         while (true) {
            Socket socket = s.accept();
            try {
               System.out.println("Новое соединение установлено");
               new ServerOne(socket);
            }
            catch (IOException e) {
               socket.close();
            }
         }
      }
      finally {
         s.close();
      }
   }
}

Идентификация клиентов

Для идентификации клиентов со стороны сервера можно запросить IP-адрес через сокет, возвращаемый accept:

Socket socket = s.accept();
System.out.println("Новое соединение установлено");
System.out.println("Данные клиента: "+
                 socket.getInetAddress());

Вопросы для самоконтроля