Лекция 11. Сеть¶
Введение¶
Введение¶
В данной лекции рассматриваются сетевые приложения на языке Java.
Предполагается использования стека протоколов TCP/IP для обмена информацией
- между процессами на одном компьютере;
- между процессами на разных компьютерах, подключенных к сети.
Сетевые приложения на языке Java строятся с использованием технологии Клиент/Сервер.
Приложения состоят из двух частей: Клиента и Сервера.
- Сервер запускается первым и после некоторых настроек переходит в состояние ожидания, используя определенный порт.
- Клиент устанавливает соединение, используя IP-адрес компьютера и порт, который прослушивает сервер.
- Общение двух программ выполняется через потоки ввода/вывода.
- После обмена информацией клиент разрывает соединение.
Соединение может быть установлено двумя способами:
- С помощью потоков (TCP)
- С помощью датаграмм (UDP)
Для обмена информацией существует специальная абстракция сокет (гнездо). Гнезда создаются как у сервера, так и у клиента.
Для работы с сетью в 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) {
...
}
Простой пример¶
Для иллюстрации простого клиент/серверного приложения
на сокетах создадим две программы:
- Server - серверная часть (класс Server)
- 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());