Skip to content

Instantly share code, notes, and snippets.

@FaAway
Created March 19, 2016 12:56
Show Gist options
  • Save FaAway/cef83d94b578a8dacda8 to your computer and use it in GitHub Desktop.
Save FaAway/cef83d94b578a8dacda8 to your computer and use it in GitHub Desktop.
javarush level30.lesson15.big01
package com.javarush.test.level30.lesson15.big01.client;
import com.javarush.test.level30.lesson15.big01.ConsoleHelper;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Created by FarAway on 12.03.2016.
*/
//18.1.
public class BotClient extends Client{
private static final String WELCOME_TEXT = "Привет чатику. Я бот. Понимаю команды: дата, день, месяц, год, время, час, минуты, секунды.";
private static volatile Set<String> botNames = new HashSet<>();
//18.2.
public class BotSocketThread extends SocketThread {
//19.1.
@Override
protected void clientMainLoop() throws IOException, ClassNotFoundException {
sendTextMessage(WELCOME_TEXT);
super.clientMainLoop();
}
//19.2.
@Override
protected void processIncomingMessage(String message) {
//19.2.1.
ConsoleHelper.writeMessage(message);
//19.2.2.
String[] messageParts = message.split(": ");
if (messageParts.length == 2) {
String messageAuthor = messageParts[0];
String messageText = messageParts[1].toLowerCase();
String dateTimeformat = null;
switch (messageText) {
case "дата":
dateTimeformat = "d.MM.YYYY";
break;
case "день":
dateTimeformat = "d";
break;
case "месяц":
dateTimeformat = "MMMM";
break;
case "год":
dateTimeformat = "YYYY";
break;
case "время":
dateTimeformat = "H:mm:ss";
break;
case "час":
dateTimeformat = "H";
break;
case "минуты":
dateTimeformat = "m";
break;
case "секунды":
dateTimeformat = "s";
break;
}
if (dateTimeformat != null) {
String reply = String.format("Информация для %s: %s",
messageAuthor,
new SimpleDateFormat(dateTimeformat).format(Calendar.getInstance().getTime())
);
sendTextMessage(reply);
}
}
}
}
//18.3.
//18.3.1.
@Override
protected SocketThread getSocketThread() {
return new BotSocketThread();
}
//18.3.2.
@Override
protected boolean shouldSentTextFromConsole() {
return false;
}
//18.3.3.
@Override
protected String getUserName() {
String botName;
if (botNames.size() >= 100) throw new RuntimeException("Число ботов превысило допустимый предел");
do {
botName = String.format("date_bot_%02d", new Random().nextInt(100));
} while (botNames.contains(botName));
botNames.add(botName);
return botName;
}
//18.4.
public static void main(String[] args) {
new BotClient().run();
}
}
package com.javarush.test.level30.lesson15.big01.client;
import com.javarush.test.level30.lesson15.big01.Connection;
import com.javarush.test.level30.lesson15.big01.ConsoleHelper;
import com.javarush.test.level30.lesson15.big01.Message;
import com.javarush.test.level30.lesson15.big01.MessageType;
import java.io.IOException;
import java.net.Socket;
/**
* Created by FarAway on 11.03.2016.
*/
public class Client {
protected Connection connection;
private volatile boolean clientConnected = false;
public class SocketThread extends Thread {
//15.1.
protected void processIncomingMessage(String message) {
ConsoleHelper.writeMessage(message);
}
protected void informAboutAddingNewUser(String userName) {
ConsoleHelper.writeMessage(userName + " присоединился к чату");
}
protected void informAboutDeletingNewUser(String userName) {
ConsoleHelper.writeMessage(userName + " покинул чат");
}
protected void notifyConnectionStatusChanged(boolean clientConnected) {
Client.this.clientConnected = clientConnected;
synchronized (Client.this) {
Client.this.notify();
}
}
//16.1.
protected void clientHandshake() throws IOException,ClassNotFoundException {
boolean accepted = false;
//16.1.1.
while (!accepted) {
Message message = connection.receive();
switch (message.getType()) {
//16.1.2.
case NAME_REQUEST:
String clientName = getUserName();
connection.send(new Message(MessageType.USER_NAME, clientName));
break;
//16.1.3.
case NAME_ACCEPTED:
notifyConnectionStatusChanged(true);
return;
//16.1.4.
default:
throw new IOException("Unexpected MessageType");
}
}
}
//17
public void run() {
//17.1.
String serverAddress = getServerAddress();
int serverPort = getServerPort();
try {
//17.2.
Socket socket = new Socket(serverAddress, serverPort);
//17.3.
Client.this.connection = new Connection(socket);
//17.4.
clientHandshake();
//17.5.
clientMainLoop();
} catch (IOException e) {
notifyConnectionStatusChanged(false);
} catch (ClassNotFoundException e) {
notifyConnectionStatusChanged(false);
}
}
//16.2.
protected void clientMainLoop() throws IOException, ClassNotFoundException {
//16.2.6.
while (!Thread.currentThread().isInterrupted()) { //but interrupted checking for daemon isn't necessary
//16.2.1.
Message message = connection.receive();
switch (message.getType()) {
//16.2.2.
case TEXT:
processIncomingMessage(message.getData());
break;
//16.2.3.
case USER_ADDED:
informAboutAddingNewUser(message.getData());
break;
//16.2.4.
case USER_REMOVED:
informAboutDeletingNewUser(message.getData());
break;
//16.2.5.
default:
throw new IOException("Unexpected MessageType");
}
}
}
}
protected String getServerAddress() {
ConsoleHelper.writeMessage("Введите адрес сервера:");
return ConsoleHelper.readString();
}
protected int getServerPort() {
ConsoleHelper.writeMessage("Введите порт сервера:");
return ConsoleHelper.readInt();
}
protected String getUserName(){
ConsoleHelper.writeMessage("Введите имя пользователя:");
return ConsoleHelper.readString();
}
protected boolean shouldSentTextFromConsole() {
return true;
}
protected SocketThread getSocketThread() {
return new SocketThread();
}
protected void sendTextMessage(String text) {
try {
connection.send(new Message(MessageType.TEXT, text));
} catch (IOException e) {
ConsoleHelper.writeMessage("Ошибка при отправке сообщения. Соединение будет закрыто.");
clientConnected = false;
}
}
public void run() {
//14.1.1.
SocketThread socketThread = getSocketThread();
//14.1.2.
socketThread.setDaemon(true);
//14.1.3.
socketThread.start();
//14.1.4.
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
ConsoleHelper.writeMessage("Выход из программы по ошибке.");
return;
}
}
//14.1.5.
if (clientConnected)
ConsoleHelper.writeMessage("Соединение установлено. Для выхода наберите команду 'exit'.");
else
ConsoleHelper.writeMessage("Произошла ошибка во время работы клиента.");
//14.1.6
while (clientConnected) {
String text = ConsoleHelper.readString();
if (text.toLowerCase().equals("exit")) break;
//14.1.7.
if (shouldSentTextFromConsole())
sendTextMessage(text);
}
}
//14.2.
public static void main(String[] args) {
Client client = new Client();
client.run();
}
}
package com.javarush.test.level30.lesson15.big01.client;
/**
* Created by FarAway on 12.03.2016.
*/
//21.1.
public class ClientGuiController extends Client{
private ClientGuiModel model;
private ClientGuiView view;
public ClientGuiController() {
//21.2.
model = new ClientGuiModel();
//21.3.
view = new ClientGuiView(this);
}
//21.4.
public class GuiSocketThread extends SocketThread {
//21.4.1.
@Override
protected void processIncomingMessage(String message) {
model.setNewMessage(message);
view.refreshMessages();
}
//21.4.2.
@Override
protected void informAboutAddingNewUser(String userName) {
model.addUser(userName);
view.refreshUsers();
}
//21.4.3.
@Override
protected void informAboutDeletingNewUser(String userName) {
model.deleteUser(userName);
view.refreshUsers();
}
//21.4.4.
@Override
protected void notifyConnectionStatusChanged(boolean clientConnected) {
view.notifyConnectionStatusChanged(clientConnected);
}
}
//21.5.1.
@Override
protected SocketThread getSocketThread() {
return new GuiSocketThread();
}
//21.5.2.
@Override
public void run() {
SocketThread socketThread = getSocketThread();
socketThread.run();
}
//21.5.3.
@Override
protected String getServerAddress() {
return view.getServerAddress();
}
@Override
protected int getServerPort() {
return view.getServerPort();
}
@Override
protected String getUserName() {
return view.getUserName();
}
//21.6.
public ClientGuiModel getModel() {
return model;
}
//21.7.
public static void main(String[] args) {
ClientGuiController controller = new ClientGuiController();
controller.run();
}
}
package com.javarush.test.level30.lesson15.big01.client;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Created by FarAway on 12.03.2016.
*/
//20.1.
public class ClientGuiModel {
//20.2.
private final Set<String> allUserNames = new HashSet<>();
//20.3.
private String newMessage;
//20.4.
public Set<String> getAllUserNames() {
return Collections.unmodifiableSet(allUserNames) ;
}
//20.5.
public String getNewMessage() {
return newMessage;
}
//20.5.
public void setNewMessage(String newMessage) {
this.newMessage = newMessage;
}
//20.6.
public void addUser(String newUserName) {
allUserNames.add(newUserName);
}
//20.7.
public void deleteUser(String userName) {
allUserNames.remove(userName);
}
}
package com.javarush.test.level30.lesson15.big01.client;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ClientGuiView {
private final ClientGuiController controller;
private JFrame frame = new JFrame("Чат");
private JTextField textField = new JTextField(50);
private JTextArea messages = new JTextArea(10, 50);
private JTextArea users = new JTextArea(10, 10);
public ClientGuiView(ClientGuiController controller) {
this.controller = controller;
initView();
}
private void initView() {
textField.setEditable(false);
messages.setEditable(false);
users.setEditable(false);
frame.getContentPane().add(textField, BorderLayout.NORTH);
frame.getContentPane().add(new JScrollPane(messages), BorderLayout.WEST);
frame.getContentPane().add(new JScrollPane(users), BorderLayout.EAST);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
textField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
controller.sendTextMessage(textField.getText());
textField.setText("");
}
});
}
public String getServerAddress() {
return JOptionPane.showInputDialog(
frame,
"Введите адрес сервера:",
"Конфигурация клиента",
JOptionPane.QUESTION_MESSAGE);
}
public int getServerPort() {
while (true) {
String port = JOptionPane.showInputDialog(
frame,
"Введите порт сервера:",
"Конфигурация клиента",
JOptionPane.QUESTION_MESSAGE);
try {
return Integer.parseInt(port.trim());
} catch (Exception e) {
JOptionPane.showMessageDialog(
frame,
"Был введен некорректный порт сервера. Попробуйте еще раз.",
"Конфигурация клиента",
JOptionPane.ERROR_MESSAGE);
}
}
}
public String getUserName() {
return JOptionPane.showInputDialog(
frame,
"Введите ваше имя:",
"Конфигурация клиента",
JOptionPane.QUESTION_MESSAGE);
}
public void notifyConnectionStatusChanged(boolean clientConnected) {
textField.setEditable(clientConnected);
if (clientConnected) {
JOptionPane.showMessageDialog(
frame,
"Соединение с сервером установлено",
"Чат",
JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(
frame,
"Клиент не подключен к серверу",
"Чат",
JOptionPane.ERROR_MESSAGE);
}
}
public void refreshMessages() {
messages.append(controller.getModel().getNewMessage() + "\n");
}
public void refreshUsers() {
ClientGuiModel model = controller.getModel();
StringBuilder sb = new StringBuilder();
for (String userName : model.getAllUserNames()) {
sb.append(userName).append("\n");
}
users.setText(sb.toString());
}
}
package com.javarush.test.level30.lesson15.big01;
import java.io.Closeable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.SocketAddress;
/**
* Created by FarAway on 10.03.2016.
*/
public class Connection implements Closeable {
final private Socket socket;
final private ObjectOutputStream out;
final private ObjectInputStream in;
public Connection(Socket socket) throws IOException{
this.socket = socket;
out = new ObjectOutputStream(socket.getOutputStream());
in = new ObjectInputStream(socket.getInputStream());
}
public void send(Message message) throws IOException {
synchronized(out) {
out.writeObject(message);
}
}
public Message receive() throws IOException, ClassNotFoundException {
synchronized(in) {
return (Message)in.readObject();
}
}
public SocketAddress getRemoteSocketAddress() {
return socket.getRemoteSocketAddress();
}
public void close() throws IOException {
in.close();
out.close();
socket.close();
}
}
package com.javarush.test.level30.lesson15.big01;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Created by FarAway on 10.03.2016.
*/
public class ConsoleHelper {
private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
public static void writeMessage(String message) {
System.out.println(message);
}
public static String readString() {
String line = null;
while (line == null)
try { line = reader.readLine();}
catch (IOException e) {
writeMessage("Произошла ошибка при попытке ввода текста. Попробуйте еще раз.");
}
return line;
}
public static int readInt() {
int number = 0;
while (true)
try {
number = Integer.parseInt(readString());
break;
}
catch (NumberFormatException e) {
writeMessage("Произошла ошибка при попытке ввода числа. Попробуйте еще раз.");
}
return number;
}
}
package com.javarush.test.level30.lesson15.big01;
import java.io.Serializable;
/**
* Created by FarAway on 10.03.2016.
*/
public class Message implements Serializable {
final private MessageType type;
final private String data;
public Message(MessageType type) {
this.type = type;
this.data = null;
}
public Message(MessageType type, String data) {
this.type = type;
this.data = data;
}
public MessageType getType() {
return type;
}
public String getData() {
return data;
}
@Override
public String toString() {
return "Message{" +
"type=" + type +
", data='" + data + '\'' +
'}';
}
}
package com.javarush.test.level30.lesson15.big01;
/**
* Created by FarAway on 10.03.2016.
*/
public enum MessageType {
NAME_REQUEST,
USER_NAME,
NAME_ACCEPTED,
TEXT,
USER_ADDED,
USER_REMOVED,
}
package com.javarush.test.level30.lesson15.big01;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
/**
* Created by FarAway on 10.03.2016.
*/
public class Server {
final private static Map<String, Connection> connectionMap = new java.util.concurrent.ConcurrentHashMap<>();
private static class Handler extends Thread {
private Socket socket;
public Handler(Socket socket) {
super();
this.socket = socket;
}
@Override
public void run() {
//Task 11.1
ConsoleHelper.writeMessage("Установлено соединение с удаленным клиентом с адресом: " +
socket.getRemoteSocketAddress());
//Task 11.2
Connection connection = null;
String clientName = null;
try {
connection = new Connection(socket);
//Task 11.3
clientName = serverHandshake(connection);
//Task 11.4
sendBroadcastMessage(new Message(MessageType.USER_ADDED, clientName));
//Task 11.5
sendListOfUsers(connection, clientName);
//Task 11.6
serverMainLoop(connection, clientName);
} catch (IOException e) {
handleHandlerException(e, connection);
} catch (ClassNotFoundException e) {
handleHandlerException(e, connection);
}
// disconnecting client
if (clientName != null) {
connectionMap.remove(clientName);
sendBroadcastMessage(new Message(MessageType.USER_REMOVED, clientName));
}
ConsoleHelper.writeMessage(String.format("Соединение с удаленным адресом (%s) закрыто.", socket.getRemoteSocketAddress()));
}
private void handleConnectionExcetion(Exception e) {
ConsoleHelper.writeMessage("Произошла ошибка при попытке установить соединение с клиентом с адресом: " +
socket.getRemoteSocketAddress() + "%n" +
"Тип ошибки: " + e.getClass().getSimpleName() + "%n" +
"Текст ошибки: " + e.getMessage());
try { socket.close(); } catch (IOException e_) { /* NOP */ }
}
private void handleHandShakeExcetion(Exception e, Connection connection) {
ConsoleHelper.writeMessage("Произошла ошибка при \"рукопожатии\" с клиентом с адресом: " +
socket.getRemoteSocketAddress() + "%n" +
"Тип ошибки: " + e.getClass().getSimpleName() + "%n" +
"Текст ошибки: " + e.getMessage());
try {connection.close(); socket.close(); } catch (IOException e_) { /* NOP */ }
}
private void handleHandlerException(Exception e, Connection connection) {
ConsoleHelper.writeMessage("Произошла ошибка при обмене данными с удаленным адресом: " +
socket.getRemoteSocketAddress() + "%n" +
"Тип ошибки: " + e.getClass().getSimpleName() + "%n" +
"Текст ошибки: " + e.getMessage());
try {
if (connection != null)
connection.close();
socket.close();
} catch (IOException e_) { /* NOP */ }
}
private String serverHandshake(Connection connection) throws IOException, ClassNotFoundException {
boolean accepted = false;
String name = null;
while (!accepted) {
connection.send(new Message(MessageType.NAME_REQUEST));
Message message = connection.receive();
if (message.getType() == MessageType.USER_NAME) {
name = message.getData();
if (!name.isEmpty() && connectionMap.get(name) == null) {
connectionMap.put(name, connection);
connection.send(new Message(MessageType.NAME_ACCEPTED));
accepted = true;
}
}
}
return name;
}
private void sendListOfUsers(Connection connection, String userName) throws IOException {
for (String clientName : connectionMap.keySet()) {
if (!clientName.equals(userName))
connection.send(new Message(MessageType.USER_ADDED, clientName));
}
}
private void serverMainLoop(Connection connection, String userName) throws
IOException, ClassNotFoundException{
while (!Thread.currentThread().isInterrupted()) {
Message message = connection.receive();
if (message.getType() == MessageType.TEXT) {
String messageText = userName + ": " + message.getData();
sendBroadcastMessage(new Message(MessageType.TEXT, messageText));
} else ConsoleHelper.writeMessage(
String.format("Ошибка! Недопустимый тип сообщения (MessageType.%s) от клиента: %s",
message.getType().toString() ,userName)
);
}
}
}
public static void main(String[] args){
ConsoleHelper.writeMessage("Введите порт сервера:");
int port = ConsoleHelper.readInt();
try (ServerSocket serverSocket = new ServerSocket(port)) {
ConsoleHelper.writeMessage("Сервер запущен на порту: " + port);
while (true) {
Socket socket = serverSocket.accept();
new Handler(socket).start();
}
} catch (IOException e) {
ConsoleHelper.writeMessage(e.getMessage());
}
}
public static void sendBroadcastMessage(Message message) {
for (String clientName : connectionMap.keySet()) {
try {
connectionMap.get(clientName).send(message);
} catch (IOException e) {
ConsoleHelper.writeMessage("Не могу отправить сообщение клиенту с именем: " + clientName);
}
}
}
}
Задание 22.
Итак, подведем итог:
• Ты написал сервер для обмена текстовыми сообщениями;
• Ты написал консольный клиент, который умеет подключаться к серверу и
обмениваться сообщениями с другими участниками;
• Ты написал бот клиента, который может принимать запросы и отправлять данные о
текущей дате и времени;
• Ты написал клиента для чата с графическим интерфейсом.
Что можно добавить или улучшить:
• Можно добавить поддержку приватных сообщений (когда сообщение отправляется не
всем, а какому-то конкретному участнику).
• Можно расширить возможности бота, попробовать научить его отвечать на
простейшие вопросы или время от времени отправлять шутки.
• Добавить возможность пересылки файлов между пользователями.
• Добавить контекстное меню в графический клиент, например, для отправки
приватного сообщения кому-то из списка участников.
• Добавить раскраску сообщений в графическом клиенте в зависимости от отправителя.
• Добавить блокировку сервером участников за что-либо, например, ненормативную
лексику в сообщениях.
• Добавить еще миллион фич и полезностей!
Ты научился:
• Работать с сокетами
• Пользоваться сериализацией и десериализацией
• Создавать многопоточные приложения, синхронизировать их, применять модификатор
volatile, пользоваться классами из библиотеки java.util.concurrent
• Применять паттерн MVC
• Использовать внутренние и вложенные классы
• Работать с библиотекой Swing
• Применять классы Calendar и SimpleDateFormat
Поздравляю, так держать!
***************************************************************************************
Задание 21.
У меня есть отличнейшая новость для тебя. Компонент представление (View) уже готов. Я
добавил класс ClientGuiView. Он использует библиотеку javax.swing. Ты должен как следует
разобрать в каждой строчке этого класса. Если тебе все понятно – это замечательно, если
нет – обязательно найди ответы на свои вопросы с помощью дебага, документации или
поиска в Интернет.
Осталось написать компонент контроллер (Controller):
21.1. Создай класс ClientGuiController унаследованный от Client.
21.2. Создай и проинициализируй поле, отвечающее за модель ClientGuiModel
model.
21.3. Создай и проинициализируй поле, отвечающее за представление ClientGuiView
view. Подумай, что нужно передать в конструктор при инициализации объекта.
21.4. Добавь внутренний класс GuiSocketThread унаследованный от SocketThread.
Класс GuiSocketThread должен быть публичным. В нем переопредели следующие
методы:
21.4.1. void processIncomingMessage(String message) – должен устанавливать новое
сообщение у модели и вызывать обновление вывода сообщений у
представления.
21.4.2. void informAboutAddingNewUser(String userName) – должен добавлять нового
пользователя в модель и вызывать обновление вывода пользователей у
отображения.
21.4.3. void informAboutDeletingNewUser(String userName) – должен удалять
пользователя из модели и вызывать обновление вывода пользователей у
отображения.
21.4.4. void notifyConnectionStatusChanged(boolean clientConnected) – должен вызывать
аналогичный метод у представления.
21.5. Переопредели методы в классе ClientGuiController:
21.5.1. SocketThread getSocketThread() – должен создавать и возвращать объект типа
GuiSocketThread.
21.5.2. void run() – должен получать объект SocketThread через метод getSocketThread()
и вызывать у него метод run(). Разберись, почему нет необходимости вызывать
метод run в отдельном потоке, как мы это делали для консольного клиента.
21.5.3. getServerAddress(), getServerPort(),getUserName(). Они должны вызывать
одноименные методы из представления (view).
21.6. Объяви метод ClientGuiModel getModel(), который должен возвращать модель.
21.7. Объяви метод main(), который должен создавать новый объект
ClientGuiController и вызывать у него метод run().
Запусти клиента с графическим окном, нескольких консольных клиентов и убедись, что
все работает корректно.
***************************************************************************************
Задание 20.
Консольный клиент мы уже реализовали, чат бота тоже сделали, почему бы не сделать
клиента с графическим интерфейсом? Он будет так же работать с нашим сервером, но
иметь графическое окно, кнопки и т.д. Итак, приступим. При написании графического
клиента будет очень уместно воспользоваться паттерном MVC (Model-View-Controller). Ты
уже должен был с ним сталкиваться, если необходимо, освежи свои знания про MVC с
помощью Интернет. В нашей задаче самая простая реализация будет у класса,
отвечающего за модель (Model). Давай напишем его:
20.1. Создай класс ClientGuiModel в пакете client. Все классы клиента должны в этом
пакете.
20.2. Добавь в него множество(set) строк в качестве final поля allUserNames.
В нем будет храниться список всех участников чата. Проинициализируй его.
20.3. Добавь поле String newMessage, в котором будет храниться новое сообщение,
которое получил клиент.
20.4. Добавь геттер для allUserNames, запретив модифицировать возвращенное
множество. Разберись, как это можно сделать с помощью метода класса Collections.
20.5. Добавь сеттер и геттер для поля newMessage.
20.6. Добавь метод void addUser(String newUserName), который должен добавлять
имя участника во множество, хранящее всех участников.
20.7. Добавь метод void deleteUser(String userName), который будет удалять имя
участника из множества.
***************************************************************************************
Задание 19.
Сегодня будем реализовывать класс BotSocketThread, вернее переопределять некоторые
его методы, весь основной функционал он уже унаследовал от SocketThread.
19.1. Переопредели метод clientMainLoop():
19.1.1. С помощью метода sendTextMessage() отправь сообщение с текстом
"Привет чатику. Я бот. Понимаю команды: дата, день, месяц, год, время, час, минуты, секунды."
19.1.2. Вызови реализацию clientMainLoop() родительского класса.
19.2. Переопредели метод processIncomingMessage(String message). Он должен
следующим образом обрабатывать входящие сообщения:
19.2.1. Вывести в консоль текст полученного сообщения message.
19.2.2. Получить из message имя отправителя и текст сообщения. Они разделены ": ".
19.2.3. Отправить ответ в зависимости от текста принятого сообщения. Если текст
сообщения:
"дата" – отправить сообщение содержащее текущую дату в формате "d.MM.YYYY";
"день" – в формате"d";
"месяц" - "MMMM";
"год" - "YYYY";
"время" - "H:mm:ss";
"час" - "H";
"минуты" - "m";
"секунды" - "s".
Указанный выше формат используй для создания объекта SimpleDateFormat. Для
получения текущей даты необходимо использовать класс Calendar и метод
getTime().
Ответ должен содержать имя клиента, который прислал запрос и ожидает ответ,
например, если Боб отправил запрос "время", мы должны отправить ответ
"Информация для Боб: 12:30:47".
Наш бот готов. Запусти сервер, запусти бота, обычного клиента и убедись, что все работает правильно.
Помни, что message бывают разных типов и не всегда содержат ":"
***************************************************************************************
Задание 18.
Иногда бывают моменты, что не находится достойного собеседника. Не общаться же с
самим собой :). Давай напишем бота, который будет представлять собой клиента, который
автоматически будет отвечать на некоторые команды. Проще всего реализовать бота,
который сможет отправлять текущее время или дату, когда его кто-то об этом попросит. С
него и начнем:
18.1. Создай новый класс BotClient в пакете client. Он должен быть унаследован от
Client.
18.2. В классе BotClient создай внутренний класс BotSocketThread унаследованный от
SocketThread. Класс BotSocketThread должен быть публичным.
18.3. Переопредели методы:
18.3.1. getSocketThread(). Он должен создавать и возвращать объект класса
BotSocketThread.
18.3.2. shouldSentTextFromConsole(). Он должен всегда возвращать false. Мы не хотим,
чтобы бот отправлял текст введенный в консоль.
18.3.3. getUserName(), метод должен генерировать новое имя бота, например:
date_bot_XX, где XX – любое число от 0 до 99. Этот метод должен возвращать
каждый раз новое значение, на случай, если на сервере захотят
зарегистрироваться несколько ботов, у них должны быть разные имена.
18.4. Добавь метод main. Он должен создавать новый объект BotClient и вызывать у
него метод run().
***************************************************************************************
Задание 17.
Последний, но самый главный метод класса SocketThread – это метод void run(). Добавь
его. Его реализация с учетом уже созданных методов выглядит очень просто. Давай
напишем ее:
17.1. Запроси адрес и порт сервера с помощью методов getServerAddress() и
getServerPort().
17.2. Создай новый объект класса java.net.Socket, используя данные, полученные в
п.17.1.
17.3. Создай объект класса Connection, используя сокет из п.17.2.
17.4. Вызови метод, реализующий "рукопожатие" клиента с сервером
(clientHandshake()).
17.5. Вызови метод, реализующий основной цикл обработки сообщений сервера.
17.6. При возникновении исключений IOException или ClassNotFoundException
сообщи главному потоку о проблеме, используя notifyConnectionStatusChanged и false
в качестве параметра.
Клиент готов, можешь запустить сервер, несколько клиентов и проверить как все работает.
***************************************************************************************
Задание 16.
Теперь все готово, чтобы дописать необходимые методы класса SocketThread.
16.1. Добавь защищенный метод clientHandshake() throws IOException,
ClassNotFoundException. Этот метод будет представлять клиента серверу. Он должен:
16.1.1. В цикле получать сообщения, используя соединение connection
16.1.2. Если тип полученного сообщения NAME_REQUEST (сервер запросил имя),
запросить ввод имени пользователя с помощью метода getUserName(), создать
новое сообщение с типом USER_NAME и введенным именем, отправить
сообщение серверу.
16.1.3. Если тип полученного сообщения NAME_ACCEPTED (сервер принял имя), значит
сервер принял имя клиента, нужно об этом сообщить главному потоку, он этого
очень ждет. Сделай это с помощью метода notifyConnectionStatusChanged(),
передав в него true. После этого выйди из метода.
16.1.4. Если пришло сообщение с каким-либо другим типом, кинь исключение
IOException("Unexpected MessageType").
16.2. Добавь защищенный метод void clientMainLoop() throws IOException,
ClassNotFoundException. Этот метод будет реализовывать главный цикл обработки
сообщений сервера. Внутри метода:
16.2.1. Получи сообщение от сервера, используя соединение connection.
16.2.2. Если это текстовое сообщение (тип TEXT), обработай его с помощью метода
processIncomingMessage().
16.2.3. Если это сообщение с типом USER_ADDED, обработай его с помощью метода
informAboutAddingNewUser().
16.2.4. Если это сообщение с типом USER_REMOVED, обработай его с помощью метода
informAboutDeletingNewUser().
16.2.5. Если клиент получил сообщение какого-либо другого типа, кинь исключение
IOException("Unexpected MessageType").
16.2.6. Размести код из пунктов 16.2.1 – 16.2.5 внутри бесконечного цикла. Цикл будет
завершен автоматически если произойдет ошибка (будет кинуто исключение) или
поток, в котором работает цикл, будет прерван.
***************************************************************************************
Задание 15.
Напишем реализацию класса SocketThread. Начнем с простых вспомогательных методов.
Добавь методы, которые будут доступны классам потомкам и не доступны остальным
классам вне пакета:
15.1. void processIncomingMessage(String message) – должен выводить текст message в
консоль
15.2. void informAboutAddingNewUser(String userName) – должен выводить в консоль
информацию о том, что участник с именем userName присоединился к чату
15.3. void informAboutDeletingNewUser(String userName) – должен выводить в
консоль, что участник с именем userName покинул чат
15.4. void notifyConnectionStatusChanged(boolean clientConnected) – этот метод
должен:
15.4.1. Устанавливать значение поля clientConnected класса Client в соответствии с
переданным параметром.
15.4.2. Оповещать (пробуждать ожидающий) основной поток класса Client. Подсказка:
используй синхронизацию на уровне объекта внешнего класса и метод notify. Для
класса SocketThread внешним классом является класс Client.
***************************************************************************************
Задание 14.
Приступим к написанию главного функционала класса Client.
14.1. Добавь метод void run(). Он должен создавать вспомогательный поток
SocketThread, ожидать пока тот установит соединение с сервером, а после этого
в цикле считывать сообщения с консоли и отправлять их серверу. Условием выхода
из цикла будет отключение клиента или ввод пользователем команды 'exit'.
Для информирования главного потока, что соединение установлено во
вспомогательным потоке, используй методы wait и notify объекта класса Client.
Реализация метода run должна:
14.1.1. Создавать новый сокетный поток с помощью метода getSocketThread.
14.1.2. Помечать созданный поток как daemon, это нужно для того, чтобы при выходе
из программы вспомогательный поток прервался автоматически.
14.1.3. Запустить вспомогательный поток.
14.1.4. Заставить текущий поток ожидать, пока он не получит нотификацию из другого
потока. Подсказка: используй wait и синхронизацию на уровне объекта. Если во
время ожидания возникнет исключение, сообщи об этом пользователю и выйди
из программы.
14.1.5. После того, как поток дождался нотификации, проверь значение
clientConnected. Если оно true – выведи "Соединение установлено. Для выхода
наберите команду 'exit'.". Если оно false – выведи "Произошла ошибка во время
работы клиента.".
14.1.6. Считывай сообщения с консоли пока клиент подключен. Если будет введена
команда 'exit', то выйди из цикла.
14.1.7. После каждого считывания, если метод shouldSentTextFromConsole()
возвращает true, отправь считанный текст с помощью метода sendTextMessage().
14.2. Добавь метод main(). Он должен создавать новый объект класса Client и
вызывать у него метод run().
***************************************************************************************
Задание 13.
Продолжаем реализацию вспомогательных методов класса Client. Добавь в класс методы,
которые будут доступны классам потомкам, но не доступны из других классов вне пакета:
13.1. String getServerAddress() – должен запросить ввод адреса сервера у
пользователя и вернуть введенное значение. Адрес может быть строкой, содержащей
ip, если клиент и сервер запущен на разных машинах или ‘localhost’, если клиент и
сервер работают на одной машине.
13.2. int getServerPort() – должен запрашивать ввод порта сервера и возвращать его.
13.3. String getUserName() – должен запрашивать и возвращать имя пользователя.
13.4. boolean shouldSentTextFromConsole() – в данной реализации клиента всегда
должен возвращать true (мы всегда отправляем текст введенный в консоль). Этот
метод может быть переопределен, если мы будем писать какой-нибудь другой
клиент, унаследованный от нашего, который не должен отправлять введенный в
консоль текст.
13.5. SocketThread getSocketThread() – должен создавать и возвращать новый объект
класса SocketThread.
13.6. void sendTextMessage(String text) – создает новое текстовое сообщение,
используя переданный текст и отправляет его серверу через соединение connection.
Если во время отправки произошло исключение IOException, то необходимо вывести
информацию об этом пользователю и присвоить false полю clientConnected.
***************************************************************************************
Задание 12.
Приступим к написанию клиента. Клиент, в начале своей работы, должен: запросить у
пользователя адрес и порт сервера, подсоединиться к указанному адресу, получить запрос
имени от сервера, спросить имя у пользователя, отправить имя пользователя серверу,
дождаться принятия имени сервером. После этого клиент может обмениваться текстовыми
сообщениями с сервером. Обмен сообщениями будет происходить в двух параллельно
работающих потоках. Один будет заниматься чтением из консоли и отправкой
прочитанного серверу, а второй поток будет получать данные от сервера и выводить их в
консоль. Начнем реализацию клиента:
12.1. Создай пакет client. В дальнейшем все классы, отвечающие за реализацию
клиентов, создавай в этом пакете.
12.2. Создай класс Client.
12.3. Создай внутренний класс SocketThread унаследованный от Thread в классе
Client. Он будет отвечать за поток, устанавливающий сокетное соединение и
читающий сообщения сервера. Класс должен иметь публичный модификатор доступа.
12.4. Создай поле Connection connection в классе Client. Используй модификатор
доступа, который позволит обращаться к этому полю из класса потомков, но запретит
обращение из других классов вне пакета.
12.5. Добавь поле-флаг boolean clientConnected в класс Client. Проинициализируй его
значением false. В дальнейшем оно будет устанавливаться в true, если клиент
подсоединен к серверу или в false в противном случае. При объявлении этого поля
используй ключевое слово, которое позволит гарантировать что каждый поток,
использующий поле clientConnected, работает с актуальным, а не кэшированным его
значением.
***************************************************************************************
Задание 11.
Пришло время написать главный метод класса Handler, который будет вызывать все
вспомогательные методы, написанные ранее. Добавим метод void run() в класс Handler.
Он должен:
11.1. Выводить сообщение, что установлено новое соединение с удаленным
адресом, который можно получить с помощью метода getRemoteSocketAddress
11.2. Создавать Connection, используя поле Socket
11.3. Вызывать метод, реализующий рукопожатие с клиентом, сохраняя имя нового
клиента
11.4. Рассылать всем участникам чата информацию об имени присоединившегося
участника (сообщение с типом USER_ADDED). Подумай, какой метод подойдет для
этого лучше всего.
11.5. Сообщать новому участнику о существующих участниках
11.6. Запускать главный цикл обработки сообщений сервером
11.7. Обеспечить закрытие соединения при возникновении исключения
11.8. Отловить все исключения типа IOException и ClassNotFoundException, вывести в
консоль информацию, что произошла ошибка при обмене данными с удаленным
адресом
11.9. После того как все исключения обработаны, если п.11.3 отработал и возвратил
нам имя, мы должны удалить запись для этого имени из connectionMap и разослать
всем остальным участникам сообщение с типом USER_REMOVED и сохраненным
именем.
11.10. Последнее, что нужно сделать в методе run() – вывести сообщение,
информирующее что соединение с удаленным адресом закрыто.
Наш сервер полностью готов. Попробуй его запустить.
***************************************************************************************
Задание 10.
Этап третий – главный цикл обработки сообщений сервером.
Добавь приватный метод void serverMainLoop(Connection connection, String userName) throws
IOException, ClassNotFoundException, где значение параметров такое же, как и у метода
sendListOfUsers. Он должен:
10.1. Принимать сообщение клиента
10.2. Если принятое сообщение – это текст (тип TEXT), то формировать новое
текстовое сообщение путем конкатенации: имени клиента, двоеточия, пробела и
текста сообщения. Например, если мы получили сообщение с текстом "привет чат" от
пользователя "Боб", то нужно сформировать сообщение "Боб: привет чат".
10.3. Отправлять сформированное сообщение всем клиентам с помощью метода
sendBroadcastMessage.
10.4. Если принятое сообщение не является текстом, вывести сообщение об ошибке
10.5. Организовать бесконечный цикл, внутрь которого перенести функционал
пунктов 10.1-10.4.
***************************************************************************************
Задание 9.
Этап второй, но не менее важный – отправка клиенту (новому участнику) информации об
остальных клиентах (участниках) чата. Для этого:
9.1. Добавь приватный метод void sendListOfUsers(Connection connection, String userName) throws
IOException, где connection – соединение с участником, которому будем слать
информацию, а userName – его имя. Метод должен:
9.2. Пройтись по connectionMap
9.3. У каждого элемента из п.9.2 получить имя клиента, сформировать команду с типом
USER_ADDED и полученным именем
9.4. Отправить сформированную команду через connection
9.5. Команду с типом USER_ADDED и именем равным userName отправлять не нужно,
пользователь и так имеет информацию о себе
***************************************************************************************
Задание 8.
Класс Handler должен реализовывать протокол общения с клиентом, описанный в Задании
3. Выделим из протокола отдельные этапы и реализуем их с помощью отдельных методов:
Этап первый – это этап рукопожатия (знакомства сервера с клиентом). Реализуем его с
помощью приватного метода String serverHandshake(Connection connection) throws IOException,
ClassNotFoundException. Метод в качестве параметра принимает соединение connection, а
возвращает имя нового клиента.
Реализация метода должна:
8.1. Сформировать и отправить команду запроса имени пользователя
8.2. Получить ответ клиента
8.3. Проверить, что получена команда с именем пользователя
8.4. Достать из ответа имя, проверить, что оно не пустое и пользователь с таким именем
еще не подключен (используй connectionMap)
8.5. Добавить нового пользователя и соединение с ним в connectionMap
8.6. Отправить клиенту команду информирующую, что его имя принято
8.7. Если какая-то проверка не прошла, заново запросить имя клиента
8.8. Вернуть принятое имя в качестве возвращаемого значения
***************************************************************************************
Задание 7.
Т.к. сервер может одновременно работать с несколькими клиентами, нам понадобится
метод для отправки сообщения сразу всем.
Добавь в класс Server:
7.1. Статическое поле Map<String, Connection> connectionMap, где ключом будет имя
клиента, а значением - соединение с ним.
7.2. Инициализацию поля из п.7.1 с помощью подходящего Map из библиотеки
java.util.concurrent, т.к. работа с этим полем будет происходить из разных потоков и
нужно обеспечить потокобезопасность.
7.3. Статический метод void sendBroadcastMessage(Message message), который должен
отправлять сообщение message по всем соединениям из connectionMap. Если при
отправке сообщение произойдет исключение IOException, нужно отловить его и
сообщить пользователю, что не смогли отправить сообщение.
***************************************************************************************
Задание 6.
Приступим к самому важному – написанию сервера Server. Сервер должен поддерживать
множество соединений с разными клиентами одновременно. Это можно реализовать с
помощью следующего алгоритма:
- Сервер создает серверное сокетное соединение
- В цикле ожидает, когда какой-то клиент подключится к сокету
- Создает новый поток обработчик Handler, в котором будет происходить обмен
сообщениями с клиентом.
- Ожидает следующее соединение.
Добавь:
6.1. В класс Server приватный статический вложенный класс Handler, унаследованный от
Thread.
6.2. В класс Handler поле типа Socket.
6.3. В класс Handler конструктор, принимающий в качестве параметра Socket и
инициализирующий им соответствующее поле класса.
6.4. В класс Server добавь метод main, он должен:
6.4.1. Запрашивать порт сервера, используя ConsoleHelper
6.4.2. Создавать серверный сокет java.net.ServerSocket, используя порт из п.6.4.1
6.4.3. Выводить сообщение, что сервер запущен
6.4.4. В бесконечном цикле слушать и принимать входящие сокетные соединения
серверного сокета из п.6.4.2
6.4.5. Создавать и запускать новый поток Handler, передавая в конструктор сокет из
п.6.4.4
6.4.6. После создания потока обработчика Handler переходить на новый шаг цикла
6.4.7. Предусмотреть закрытие серверного сокета в случае возникновения исключения.
Если исключение Exception все же произошло, поймать его и вывести сообщение
об ошибке.
***************************************************************************************
Задание 5.
Клиент и сервер будут общаться через сокетное соединение. Одна сторона будет
записывать данные в сокет, а другая читать. Их общение представляет собой обмен
сообщениями Message. Класс Connection будет выполнять роль обвертки над классом
java.net.Socket, которая должна будет уметь сериализовать и десериализовать объекты
типа Message в сокет. Методы этого класса должны быть готовы к вызову из разных
потоков.
Добавь в класс Connection:
5.1. Final поля:
5.1.1. Socket socket
5.1.2. ObjectOutputStream out
5.1.3. ObjectInputStream in
5.2. Конструктор, который должен принимать Socket в качестве параметра и
инициализировать поля класса. Для инициализации полей in и out используй
соответствующие потоки сокета. Конструктор может бросать исключение IOException.
Создать объект класса ObjectOutputStream нужно до того, как будет создаваться объект
класса ObjectInputStream, иначе может возникнуть взаимная блокировка потоков,
которые хотят установить соединение через класс Connection. Более подробно об этом
ты можешь прочитать в спецификации класса ObjectInputStream.
5.3. Метод void send(Message message) throws IOException. Он должен записывать
(сериализовать) сообщение message в ObjectOutputStream. Этот метод будет
вызываться из нескольких потоков. Позаботься, чтобы запись в объект
ObjectOutputStream была возможна только одним потоком в определенный момент
времени, остальные желающие ждали завершения записи. При этом другие методы
класса Connection не должны быть заблокированы.
5.4. Метод Message receive() throws IOException, ClassNotFoundException. Он должен читать
(десериализовать) данные из ObjectInputStream. Сделай так, чтобы операция чтения
не могла быть одновременно вызвана несколькими потоками, при этом вызов других
методы класса Connection не блокировать.
5.5. Метод SocketAddress getRemoteSocketAddress(), возвращающий удаленный адрес
сокетного соединения.
5.6. Метод void close() throws IOException, который должен закрывать все ресурсы класса.
Класс Connection должен реализовывать интерфейс Closeable.
***************************************************************************************
Задание 4.
Сообщение Message – это данные которые одна сторона отправляет, а вторая принимает.
Каждое сообщение должно иметь тип MessageType, а некоторые и дополнительные
данные, например, текстовое сообщение должно содержать текст. Т.к. сообщения будут
создаваться в одной программе, а читаться в другой, удобно воспользоваться механизмом
сериализации для перевода класса в последовательность битов и наоборот.
Добавь в класс Message:
4.1. Поддержку интерфейса Serializable
4.2. Final поле типа MessageType type, которое будет содержать тип сообщения
4.3. Final поле типа String data, которое будет содержать данные сообщения
4.4. Геттеры для этих полей
4.5. Конструктор, принимающий только MessageType, он должен проинициализировать
поле type переданным параметром, а поле data с помощью null.
4.6. Конструктор, принимающий MessageType type и String data. Он должен также
инициализировать все поля класса.
***************************************************************************************
Задание 3.
Прежде, чем двигаться дальше, нужно разработать протокол общения клиента и сервера.
Сформулируем основные моменты протокола:
- Когда новый клиент хочет подсоединиться к серверу, сервер должен запросить имя
клиента.
- Когда клиент получает запрос имени от сервера он должен отправить свое имя серверу.
- Когда сервер получает имя клиента он должен принять это имя или запросить новое.
- Когда новый клиент добавился к чату, сервер должен сообщить остальным участникам о
новом клиенте.
- Когда клиент покидает чат, сервер должен сообщить остальным участникам об этом.
- Когда сервер получает текстовое сообщение от клиента, он должен переслать его всем
остальным участникам чата.
Добавь для каждого пункта вышеописанного протокола соответствующее значение в enum
MessageType:
3.1. NAME_REQUEST – запрос имени
3.2. USER_NAME – имя пользователя
3.3. NAME_ACCEPTED – имя принято
3.4. TEXT – текстовое сообщение
3.5. USER_ADDED – пользователь добавлен
3.6. USER_REMOVED – пользователь удален
***************************************************************************************
Задание 2.
Первым делом, для удобства работы с консолью реализуем класс ConsoleHelper. В
дальнейшем, вся работа с консолью должна происходить через этот класс.
Добавь в него:
2.1. Статическое поле типа BufferedReader, проинициализируй его с помощью System.in
2.2. Добавь статический метод writeMessage(String message), который должен выводить
сообщение message в консоль
2.3. Добавь статический метод String readString(), который должен считывать строку с
консоли. Если во время чтения произошло исключение, вывести пользователю
сообщение "Произошла ошибка при попытке ввода текста. Попробуйте еще раз." И
повторить ввод. Метод не должен пробрасывать исключения IOException наружу.
2.4. Добавь статический метод int readInt(). Он должен возвращать введенное число и
использовать метод readString(). Внутри метода обработать исключение
NumberFormatException. Если оно произошло вывести сообщение "Произошла ошибка
при попытке ввода числа. Попробуйте еще раз." И повторить ввод числа.
В этой задаче и далее, если не указано дополнительно другого, то все поля класса должны
быть приватными, а методы публичными.
***************************************************************************************
Задание 1.
Сегодня мы напишем чат. Набор программ с помощью которого можно будет
обмениваться текстовыми сообщения. Набор будет состоять из одного сервера и
нескольких клиентов, по одному для каждого участника чата.
Начнем с сервера. Нам понадобятся классы:
1.1. Server – основной класс сервера
1.2. MessageType – enum, который отвечает за тип сообщений пересылаемых между
клиентом и сервером
1.3. Message – класс, отвечающий за пересылаемые сообщения
1.4. Connection – класс соединения между клиентом и сервером
1.5. ConsoleHelper – вспомогательный класс, для чтения или записи в консоль
Объяви эти классы.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment