Это задание является вторым из трех частей мультиплеерного Paint. В результате получится программа в которой пользователи сервера и клиента рисуют и видят общую картину.

Важно Обратите внимание, что в данном задании есть три части:

  1. Разминка (пункт 0) - работа с консолью, это подзадание делать и отправлять не обязательно, но оно должно существенно упростить выполнение остальной части задания
  2. Сервер
  3. Клиент

Несмотря на то, что сервер и клиент две программы - их можно создавать в одном проекте. Например можно создать классы MainServer и MainClient, где в каждом классе будет своя main-функция. Чтобы запустить какую-то одну из них - можно нажать правой кнопкой на класс, в котором она объявлена и там Run.

Дедлайн:

  • 9-1: когда-нибудь не скоро
  • 10-1: 26 февраля
  • 11-1: 26 февраля

Это задание является вводным в сетевое взаимодействие. Когда вы открываете этот сайт, играете в мультиплеерную игру, используете мессенджер и т.п. - происходит взаимодействие вашего устройства с другими устройствами посредством интернета. Т.е. программа которую вы используете отправляет очень много сообщений серверу (различного содержания: это может быть текст вашего сообщения или координаты вашего персонажа в игровом мире). Так же используемая программа получает много сообщений от других устройств: в случае чата - это входящие сообщения, в случае игры - координаты остальных игроков.

В этом задании рассматриваются две простые программы: сервер и клиент.

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

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

Ачивка TypicalBackendDeveloper: ожидать подключения многих клиентов и при получении входящего сообщения - высылать полученный текст всем подключенным клиентам (т.е. например клиент 1 отправил текст “A”, клиент 2 отправил текст “Б”, тогда сервер каждое из этих сообщений отправит этим двум клиентам, и каждый клиент увидит как свое сообщение, так и сообщения остальных клиентов).

0) Про работу с потоками данных

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

Здесь написано о том, как это делается.

Условие задачи:

В консоль вводят команды. Одна строка - одна команда. Перечень поддерживаемых команд:

  • sum a1 a2 a3 ... (произвольное количество целых чисел через пробел - поэтому поможет tokenizer.hasMoreTokens()) - выводит сумму чисел
  • print sdmasda sfa 412451 251 (сначала название команды print, затем после пробела произвольный текст) - выводит всю строку без изменений, следующую за print через пробел
  • exit - завершает исполнение программы

Пример:

Вход:

sum 1 2 3 4 5
sum 239
print Hello world!
sum -1 3 214 1
print 1 2
exit

Выход:

15
239
Hello world!
217
1 2

1) Сетевое взаимодействие

Представьте что у каждого компьютера есть несколько тысяч виртуальных разъемов. Это так называемые порты - они обладают значениями от 0 до 65536 (рекомендую для использования любой из пространства 1024—49151, например 2391). Они позволяют установить соединение между двумя компьютерами (т.е. сообщения будут идти между одним из портов первого компьютера через интернет к порту второго компьютера).

Чтобы в интернете или локальной сети можно было обращаться к другим компьютерам - у каждого есть свой адрес, так называемый ip-адрес. Это уникальный идентификатор имеющий вид a.b.c.d, где каждая буква - число от 0 до 255 (например: 5.18.192.135).

Если есть компьютер А с ip-адресом ipA и есть компьютер Б с ip-адресом ipB, то чтобы А смог общаться с Б - им нужно установить связь. Для этого компьютер А стучится (пытается установить соединение) по адресу ipB в конкретный порт, например в нашем случае пусть это будет порт 2391.

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

Итого если на компьютере Б с адресом ipB запущена программа, которая ожидает подключение клиентов по порту 2391, и есть компьютер А, на котором запустилась программа, которая пытается установить соединение с компьютером по адресу ipB и порту 2391 - то будет установлено двустороннее соединение, по которому можно обмениваться информацией.

2) Простой сервер

Итак нам нужно написать программу которая ожидает подключения клиентов по порту 2391 и когда клиент подключается - начинает ему отвечать зеркальными сообщениями (т.е. вести себя как эхо).

В Java есть специальный класс Socket, который представляет “разъем”/”сокет” соединения, т.е. один из концов канала обмена сообщениями.

Нам нужно начать ожидать подключающихся клиентов. Т.е. нам нужно поставить на выбранный порт (например 2391) швейцара, который будет ждать гостей, и когда кто-нибудь придет - установит надежное соединение между нашим сервером и гостем-клиентом для дальнейшего общения.

В Java таким швейцаром является специальный класс ServerSocket.

Создаем его в main-функции:

ServerSocket serverDoorMan = new ServerSocket(port);

Обратите внимание на параметр конструктора - нужно указать порт (т.е. номер двери выходящей наружу - на улицу), на котором наш сервер (швейцар/привратник) ожидает подключающихся клиентов. Так как порт уже может быть занят другим сервером (швейцаром), то этот конструктор может упасть с исключением IOException, об этом IDEA вам подскажет ошибкой Unhandled exception: java.io.IOException, мы просто явно пробросим эту ошибку наружу - пусть наша программа упадет (это делается через клик по этому конструктору -> Alt+Enter -> Add exception to method signature). Об этой же ошибке упомянуто в конце в секции “частые проблемы”.

Теперь надо сказать швейцару ждать гостей (accept = принимать/ожидать):

Socket socketToClient = serverDoorMan.accept();

Функция accept() приостанавливает исполнение программы до первого подключения клиента по этому порту (первого человека который придет в гости).

Когда клиент подключится и соответственно соединение будет установлено - эта функция вернет сокет который описывает установленное соединение (т.е. объект класса Socket), поэтому результат исполнения функции accept надо сохранить в переменную типа Socket - например логично назвать такую переменную socketToClient, т.к. это сокет соединения с клиентом.

У Socket (объект в переменной socketToClient) уже можно получить входной и выходной поток (через методы getInputStream и getOutputStream) - они обладают тем же типом, что и System.in и System.out. Поэтому о том, как считывать из входного потока (который возвращает getInputStream - т.е. это поток данных которые идут от клиента к серверу) и как писать в выходной поток (т.е. в результат getOutputStream - т.е. в поток данных от сервера к клиенту) в пункте 0 предложена условная задача на понимание этого (единственная разница - вторым аргументом в конструктор PrintWriter нужно передать true - это включит автоматическую отправку буфера на каждое сообщение).

Server figure

Когда клиент закроет канал - метод readLine() класса BufferedReader вернет null - и тогда сервер должен будет вернуться обратно на стадию ожидания нового клиента - т.е. опять вызвать метод accept(). (т.е. швейцар опять вернулся на свою позицию ожидания гостей)

Но как и при работе с файлами - сокет соединения с клиентом надо закрывать, т.е. после того как readLine() вернул null - требуется вызвать метод close() у объекта класса Socket, который представляет соединение с клиентом (т.е. то, что вернул метод accept()).

3) Простой клиент

Итак нам нужно написать программу которая подключается по порту 2391 к компьютеру с каким-то ip-адресом и шлет ему сообщения + получает на них ответы.

Мы сервер и клиент будем запускать на одном и том же компьютере, и для подобного случая есть зарезервированный специальный ip адрес - 127.0.0.1 (так же известный как localhost), поэтому можно считать что сервер всегда существует по такому адресу.

В Java есть специальный класс Socket, который представляет “разъем”/”сокет” соединения, т.е. один из концов канала обмена сообщениями. Для того чтобы инициировать соединение с сервером - надо создать сокет с адресом сервера к которому мы стучимся (127.0.0.1) и портом на котором нас ожидает сервер (2391). Здесь рпяит возникнет Unhandled exception: java.io.IOException, которая обсуждалась выше (конструктор ServerSocket) и ниже (в Частых проблемах).

Создаем такой сокет в main-функции:

String host = "127.0.0.1";
int port = 2391;
Socket socketToServer = new Socket(host, port);

Как и у сервера код работающий с потоками такой же как и при работе с консольным вводом-выводом. Методы получения потоков у socketToServer такие же как и у socketToClient, который обсуждался выше. Только теперь getInputStream - канал от сервера к нам (т.е. к клиенту), а getOutputStream - от нас к серверу:

Client figure

Т.е. теперь достаточно в цикле слать сообщения серверу через выходной поток получаемый через метод getOutputStream и печатать в консоль получаемые через getInputStream- поток ответные сообщения.

P.S. не забудьте вызвать метод close() у сокета соединения.

4) Сервер чата (добровольное)

Теперь мы хотим чтобы сервер пересылал все входящие сообщения всем подключенным клиентам.

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

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

Этот пункт добровольный - на ачивку, поэтому инструкция не подробная.

5) Тестирование и отправка задания

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

Отправляйте выполненное задание ввиде zip-архива src папки, и пожалуйста:

  • Тему письма называйте правильно, например: Задание 40 16-1 Полярный Коля
  • Правильно называнный zip-архив (или 7zip), внутри которого папка src с .java файлами, пример названия: 40_16_1_polyarniy_nikolay.zip

6) Частые проблемы

Ситуация: При запуске сервера выводится исключение java.net.BindException: Address already in use (Bind failed)

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

Ситуация: Не компилируется с исключением Unhandled exception: java.io.IOException

Это означает что вызов может упасть с исключением по работе с IO, т.е. с Input-Output (т.е. ввод-вывод). Чтобы начало компилироваться - достаточно пробросить эту ошибку наружу, т.к. такая прблема в нашем случае должна приводить к падению программы. Подробнее об этом написано выше рядом с описанием вызова конструтора ServerSocket.

Ситуация: При запуске клиента не устанавливается соединение с исключением java.net.ConnectException: Connection refused: connect

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

Ситуация: Подсвечивает красным код с ошибкой “Unhandled exception: java.io.IOException”

Это означает, что данная строка кода может кинуть исключительную ситуацию. Исключение называется IOException, т.к. IO - InputOutput, т.е. исключение связанное с потоком данных. Например в случае работы с файлом это может быть - “файл не найден”, а в случае работы по сети - “соединение было разорвано”. Ее можно либо обработать (например напечатав в консоль “проблемы с соединением”) с помощью блока try-catch (Alt+Enter, затем Surround with try/catch), либо пробросить дальше из этого метода, чтобы смог обработать тот, кто вызвал метод, в котором мы сейчас находимся (Alt+Enter, затем Add exception to method signature).