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

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

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

Обратите внимание, что несмотря на то, что клиент и сервер - являются двумя разными программами, удобно их реализовывать в рамках одного проекта. Например можно это сделать в двух разных классах (это могут быть классы ChatClient и ChatServer) с двумя main-функциями.

P.S. Игнорируйте строчки вроде synchronized (lock) { ...someCode... } внутри StreamWorker. Считайте что это то же самое, что и просто { ...someCode... }.

P.P.S. Обратите внимание, что несмотря на то, что клиент к чату можно скопировать из кода ниже - это не значит что в нем не надо разбираться. Если вы в нем не разберетесь - будет сложно реализовать сервер чата и еще сложнее - многопользовательскую рисовалку. С кодом класса StreamWorker тоже стоит разобраться.

Дедлайн:

  • 10-1: 16 марта
  • 11-1: 16 марта

0) Про чат

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

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

Задача клиента: подключиться к серверу и отправлять ему сообщения которые вводит пользователь в консоли. При получении сообщения от сервера - печатать его в консоль.

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

1) Библиотечка

Обрабатывающий потоки данных класс должен как-то оповещать о событиях (новых сообщениях). Раньше мы уже использовали KeyListener и MouseListener для обработки событий нажатия кнопок и мышки. В данном случае используется аналогичный механизм, но абстрактный класс MessageListener подходящий под наши нужды выглядит например так:

public abstract class MessageListener {

    public abstract void onMessage(String text);

    public abstract void onDisconnect();

    public void onException(Exception e) {
        e.printStackTrace();
    }

}

Создаете новый класс (правый клик по src, там New->Java Class) с названием MessageListener и копируете в этот файл приведенный выше код.

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

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

public class StreamWorker implements Runnable, Closeable {

    private final BufferedReader in;    // Входной поток данных
    private final PrintWriter out;      // Выходной поток данных
    // private  - означает, что до этого поля нельзя дотянуться извне, ведь напрямую с ним никто другой кроме данного класса работать не должен
    // final    - означает, что этот объект всегда будет один и тот же (почти то же самое, что и const), т.е. что это финальный объект

    private final List<MessageListener> listeners = new ArrayList<>(); // Зарегистрированные обработчики входящих собщений
    
    private final Object outputLock = new Object();   // Не обращайте внимания, эти два объекта
    private final Object listenerLock = new Object(); // используются в synchronized-блоках

    public StreamWorker(InputStream input, OutputStream output) {
        this.in = new BufferedReader(new InputStreamReader(input));
        this.out = new PrintWriter(output, true); // true - флаг autoFlush, он приводит к тому, что буфер будет отправляться сразу - на каждое сообщение
    }

    public void addListener(MessageListener listener) {
        this.listeners.add(listener);
    }

    // Это метод, который StreamWorker обязался реализовать в связи с реализацией интерфейса Runnable (т.к. выше написано StreamWorker implements Runnable)
    // В этом методе StreamWorker ждет поступления новых сообщений, и каждое новое сообщение передает обработчику входящих сообщений
    @Override
    public void run() {
        try {
            String s;
            // Пока входящее сообщение не отсутствует - читаем сообщения одно за другим
            while ((s = in.readLine()) != null) {
                // Отдаем полученное сообщение на обработку
                synchronized (listenerLock) {
                    for (MessageListener listener : listeners) {
                        listener.onMessage(s);
                    }
                }
            }
        } catch (SocketException e) {
            if (e.getMessage().equals("Connection reset")) {
                // Если случившаяся исключительная ситуация - разрыв соединения, то вызываем соответствующую обработку события
                synchronized (listenerLock) {
                    for (MessageListener listener : listeners) {
                        listener.onDisconnect();
                    }
                }
            } else {
                // Иначе - просто обрабатываем ошибку
                synchronized (listenerLock) {
                    for (MessageListener listener : listeners) {
                        listener.onException(e);
                    }
                }
            }
        } catch (IOException e) {
            // Провоцируем обработку случившейся исключительной ситуации (например клиент разорвал соединение)
            synchronized (listenerLock) {
                for (MessageListener listener : listeners) {
                    listener.onException(e);
                }
            }
        }
    }

    // Этот метод запускает цикл в методе run(), который будет считывать входящие сообщения и отдавать их на обработку в listeners
    public void start() {
        Thread thread = new Thread(this, "StreamWorker");
        thread.start();
    }

    // Это метод отправки сообщения
    public void sendMessage(String text) {
        synchronized (outputLock) {
            out.println(text);
        }
    }

    // Это метод, реализовать который StreamWorker обязуется в связи с реализацией интерфейса Closeable (т.к. выше написано StreamWorker implements Closeable)
    // Общая идея что Closeable = "закрываемый" как например файл. В нашем случае StreamWorker просто закрывает оба потока данных
    @Override
    public void close() throws IOException {
        in.close();  // Закрываем входной поток
        out.close(); // Закрываем выходной поток
    }
}

Создаете новый класс (правый клик по src, там New->Java Class) с названием StreamWorker и копируете в этот файл приведенный выше код.

2) Пример использования библиотеки для реализации чата

2.1) Клиент чата

Теперь на базе скопированного обработчика входящих сообщений (почтальона) достаточно просто можно реализовать клиент.

Создайте класс ChatClient. Т.к. это будет исполняемая программа - надо определить точку входа - создать main-функцию внутри класса ChatClient:

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

На этапе запуска программы разумно создать объект типа ChatClient и вызвать у него метод start (который мы тут же и реализуем):

    public static void main(String[] args) throws IOException {
        ChatClient chatClient = new ChatClient();
        chatClient.start();
    }
    
    public void start() throws IOException {
        // TODO
    }

Теперь устанавливаем соединение с сервером аналогично тому как это было сделано в предыдущем задании:

    public void start() throws IOException {
        Socket socket = new Socket(...);
        // TODO
    }

Для получения и отправки сообщений серверу можно воспользоваться StreamWorker, дав ему входной и выходной поток данных - полученных от установленного соединения с сервером, и запустив его:

StreamWorker postman = new StreamWorker(socket.getInputStream(), socket.getOutputStream());
postman.start();

Аналогично тому как обрабатывались нажатия кнопок в змейке и движения мышки в рисовалке нужно подписаться на обработку сообщений у почтальона (StreamWorker):

  • Наследовать ChatClient от MessageListener
  • Реализовать (через Alt+Enter) методы: onMessage, onDisconnect
  • Добавить текущий объект как обработчик входящих сообщений к “почтальону”: postman.addListener(this)

Теперь предыдущий кусок кода становится таким:

StreamWorker postman = new StreamWorker(socket.getInputStream(), socket.getOutputStream());
postman.addListener(this);
postman.start();

// TODO: считываем сообщения из консоли и отправляем их

Чтобы отправлять сообщения можно воспользоваться методом sendMessage(String text):

StreamWorker postman = new StreamWorker(socket.getInputStream(), socket.getOutputStream());
postman.addListener(this);
postman.start();

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String userInput = in.readLine();
while (userInput != null) {
    postman.sendMessage(userInput);
    userInput = in.readLine();
}

2.2) Сервер чата

Сервер на базе предложенной библиотечки получается из примерно следующих набросков кода:

// Создаем серверный сокет - швейцара, который будет пускать в наш чат долгожданных гостей
ServerSocket server = new ServerSocket(port);

while (true) {
    // Дожидаемся очередного гостя
    Socket client = server.accept();
    // Создаем почтальона, который будет ожидать входящие сообщения от данного пользователя, и оповещать о них нас - сервер
    StreamWorker postman = new StreamWorker(client.getInputStream(), client.getOutputStream());
    postman.addListener(this);
    // Поток, следящий за входными строчками от этого клиента запускается:
    postman.start();
    // Запоминаем почтальона, выделенного данному клиенту в перечне всех почтальонов
    postmans.add(postman);
}

Соответственно примерно так может выглядить обработка входящего сообщения:

// Пробегаем по всем почтальонам
for (int i = 0; i < postmans.size(); i++) {
    StreamWorker postman = postmans.get(i);
    // Шлем полученное сообщение каждому клиенту (включая клиента являющегося оригинальным отправителем):
    postman.sendMessage(text);
}

Чат готов!

3) Многопользовательская рисовалка

Теперь вам предлагается аналогичным образом сделать многопользовательскую программу для рисования:

Каждое сообщение в многопользовательской рисовалке будет описывать отрезок. Соответственно сообщение имеет вид:

Segment x0 y0 x1 y1

Где:

  • Segment - фиксированная строка, она указывает тип сообщения (в данном задании достаточно одного вида сообщений, но это задел на развитие рисовалки)
  • x0 y0 - пара целых чисел через пробел, описывающих координаты начала отрезка
  • x1 y1 - пара целых чисел через пробел, описывающих координаты конца отрезка

В случае если вы хотите добавить имя/идентификацтор пользователя в сообщение, то его вид может стать например таким:

Segment UserId x0 y0 x1 y1

В случае если вы хотите добавить цвет к отрезку:

Segment x0 y0 x1 y1 r g b

Где r g b - три числа в диапазоне от 0 до 255 (включительно).

3.1) С точки зрения клиента:

  • Клиент может рисовать непрерывные (пока кнопка зажата) линии движениями мышки (как в Задании 39 - Пункт 6)
  • Клиент отправляет на сервер сообщения, описывающие новые нарисованные отрезки
  • Клиент получает сообщения об отрезках и отображает их по мере рисования другими пользователями

3.2) С точки зрения сервера:

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

4) Отправка задания

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

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

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

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

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

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

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