- PVSM.RU - https://www.pvsm.ru -
На данный момент заканчиваю 2-й курс универститета, одной из лабораторных работ по курсу Java было написание чата. После того, как разобрался в теме сокетов, сериализации объектов и MVC, хотелось бы поделиться с читателим, тем более, что оно мне несказанно помогло при написании проекта.
Ну и, разумеется, учту все ошибки и недочеты, которые будут озвучены.
Итак, для начала. Проект будет состоять из двух частей: клиента и сервера. Клиент будет иметь GUI, написанный с помощью библиотеки Swing. Сервер GUI иметь не будет, только log-файл и небольшой вывод в консоль.
Для написания чата, нам понадобятся некоторые знания.
Поскольку взаимосвязь клиента и сервера у нас будет реализована с помощью сокетов, познакомимся с ними поближе.
Со́кеты (англ. socket — углубление, гнездо, разъём) — название программного интерфейса для обеспечения обмена данными между процессами. Сокет — абстрактный объект, представляющий конечную точку соединения.
Каждый процесс может создать слушающий сокет (серверный сокет) и привязать его к какому-нибудь порту операционной системы. Слушающий процесс обычно находится в цикле ожидания, то есть просыпается при появлении нового соединения.
Для нас это означает, что сервер будет слушать какой-либо порт (для большей универсальности, мы будем задавать порт в конфигурационном файле) и после подключения клиента, будем производить с ним какие-то действия.
ServerSocket socketListener = new ServerSocket("1234"); //Слушаем порт 1234
while (true) {
Socket client = null;
while (client == null) {
client = socketListener.accept(); //Пытаемся соединиться с клиентом
}
//Как только подключились, можем как-то с ним взаимодействовать
}
Но просто получать сообщение от клиента нам мало. Нам хочется знать его имя, IP-адрес, а также передавать ему в ответ список подключенных пользователей. Таким образом, просто передача текстовых данных нас не устроит. У нас есть 2 выхода:
1. Передавать xml-строку с описанием всей информации
2. Передавать сериализованный объект, в котором будут храниться все необходимые нам данные в виде полей.
Не будем говорить о плюсах и минусах каждого подхода. Воспользуемся вторым вариантом. (А если понадобится, когда-нибудь потом, я напишу второй)
Итак, что такое сериализация объектов.
Сериализация — процесс перевода какой-либо структуры данных в последовательность битов. Обратной к операции сериализации является операция десериализации — восстановление начального состояния структуры данных из битовой последовательности.
Сериализация используется для передачи объектов по сети и для сохранения их в файлы.
В Java единственное, что нужно для сериализации объекта — имплементировать интерфейс Serializable, который является интерфейсом-маркером, т.е не содержит методов.
Итак, краткое описание работы сервера.
Сервер работает в вечном цикле. Как только подключается новый клиент, он создает для работы с ним новый поток, оповещает уже подключенных клиентов о новом пользователей, а новичку отсылает какое-то количество последних сообщений в чате. Клиент же, при подключении сообщает о себе некоторую информациию, а также какое-то сообщение, идентифицирующее то, что он только что подключился.
Но помимо этого, надо не забыть о том, что клиенты могут и отключаться. То есть мы периодически должны обмениваться с клиентами сигналами (ping'овать друг друга), чтобы в случае отключения клиента (или сервера) все об этом узнали.
Итак, приступим. Создадим наш первый класс
package anexroid.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
public class Server {
public static void main(String[] args) {
try {
//Создаем слушатель
ServerSocket socketListener = new ServerSocket("1234");
while (true) {
Socket client = null;
while (client == null) {
client = socketListener.accept();
}
new ClientThread(client); //Создаем новый поток, которому передаем сокет
}
} catch (SocketException e) {
System.err.println("Socket exception");
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O exception");
e.printStackTrace();
}
}
}
Думаю, данный код, пока что, не нуждается в комментариях. Я не стал особо заморачиваться на обработке исключений, но нам ведь не это важно, верно?
Ах да, мы же хотели получать номер порта из конфигурационного файла. Создадим для конфига отдельный класс, в котором будем хранить статические поля — параметры нашего чата.
Удобнее всего хранить параметры в properties-файле, благо Java предоставляет удобный интерфейс для работы с ними.
Итак, наш properties-файл будет состоять всего из одной строки (пока что)
PORT=1234
package anexroid.server
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
public class Config {
private static final String PROPERTIES_FILE = "./server.properties";
public static int PORT;
static {
Properties properties = new Properties();
FileInputStream propertiesFile = null;
try {
propertiesFile = new FileInputStream(PROPERTIES_FILE);
properties.load(propertiesFile);
PORT = Integer.parseInt(properties.getProperty("PORT"));
} catch (FileNotFoundException ex) {
System.err.println("Properties config file not found");
} catch (IOException ex) {
System.err.println("Error while reading file");
} finally {
try {
propertiesFile.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
Вся загрузка параметров происходит в блоке статической инициализации, поскольку полноценный конструктор в нашем случае — непозволительная роскошь.
Осталось только заменить в Server.java
ServerSocket socketListener = new ServerSocket("1234");
на ServerSocket socketListener = new ServerSocket(Config.PORT);
и добавить нужные import'ы
В дальнейшем, import'ы в коде буду упускать, поскольку любая IDE их сама подставит.
Итак, мы написали new ClientThread();
. Но что это такое, пока не решили. Пора исправить это.
Этот класс у нас будет отвечать за прием и передачу сообщений между клиентом и сервером, а значит, самый главный класс в чате — именно ClientThread.
Поскольку он работает в отдельном потоке, первое, что мы должны сделать — написать
public class ClientThread extends Thread {
private Socket socket;
public ClientThread(Socket socket) {
this.socket = socket;
this.start();
}
public void run() {
}
А теперь подумаем, что же написать в методе run().
Итак, по порядку. Для начала, мы должны получить от клиента информацию «Ты кто такой?» в противном случае — «Давай, до свидания!». Как только мы узнали кто он такой, мы должны отправить ему последние сообщения в нашем чате.
Далее, периодически, мы должны его ping'овать его каким-нибудь запросом, чтобы убедиться, что не ведем общение с трупом.
Затем, мы должны получать от него сообщения, о которых должны уведомлять всех подключенных клиентов.
Ну и, как только мы перестали получать от него запросы — клиента следует удалить из списка доступнх пользователей.
На время забудем о ClientThread, а задумаемся «Каким образом будет происходить общение?»
Мы уже решили, что будем передавать сериализованный объект. Итак, чтоже должно быть в этом объекте?
Я остановился на следующем варианте:
1. Логин пользователя, отправившего это сообщение (или Server-Bot)
2. Собственно, само сообщение
3. Время отправки
4. Список доступных серверу клиентов (для отображения у пользователя)
Для успешной сериализации/десериализации, класс в клиенте и сервере должен быть одинаковым. Поэтому позаботимся сразу и о том, и о другом.
public class Message implements Serializable {
private String login;
private String message;
private String[] users;
private Date time;
//Конструктор, которым будет пользоваться клиент
public Message(String login, String message){
this.login = login;
this.message = message;
this.time = java.util.Calendar.getInstance().getTime();
}
//Конструктор, которым будет пользоваться сервер
public Message(String login, String message, String[] users){
this.login = login;
this.message = message;
this.time = java.util.Calendar.getInstance().getTime();
this.users = users;
}
public void setOnlineUsers(String[] users) {
this.users = users;
}
public String getLogin() {
return this.login;
}
public String getMessage() {
return this.message;
}
public String[] getUsers() {
return this.users;
}
public String getDate(){
Time tm = new Time(this.time.getTime());
return tm.toString();
}
}
Думаю, всё очевидно. Также, помимо сообщений мы хотим передавать нечто вроде ping'ов.
public class Ping extends Message {
public Ping() {
super("ping", "ping");
}
}
По сути, этот класс нам не сильно-то нужен, просто потом код будет удобнее читать
Итак, приступим к написанию ClientThread
public void run() {
try {
//Создаем потоки ввода-вывода для работы с сокетом
final ObjectInputStream inputStream = new ObjectInputStream(this.socket.getInputStream());
final ObjectOutputStream outputStream = new ObjectOutputStream(this.socket.getOutputStream());
//Читаем Message из потока
this.c = (Message) inputStream.readObject();
//Читаем логин отправителя
this.login = this.c.getLogin();
//Что же нам прислали?
if (! this.c.getMessage().equals(Config.HELLO_MESSAGE)) { //Если это не регистрационное сообщение
System.out.println("[" + this.c.getLogin() + "]: " + this.c.getMessage());
getChatHistory().addMessage(this.c); //То добавляем его к истории чата
} else {
outputStream.writeObject(getChatHistory()); //Иначе, отправляем новичку историю чата
this.broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been connect")); //И сообщаем всем клиентам, что подключился новый пользователь
}
//Добавляем к списку пользователей - нового
getUserList().addUser(login, socket, outputStream, inputStream);
//Для ответа, указываем список доступных пользователей
this.c.setOnlineUsers(getUserList().getUsers());
//Передаем всем сообщение пользователя
this.broadcast(getUserList().getClientsList(), this.c);
//Запускаем таймер
this.timer = new Timer(DELAY, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
try { //Если количество входящих пакетов от клиента рано исходящему, значит клиент еще не в ауте
if (inPacks == outPacks) {
outputStream.writeObject(new Ping());
outPacks++;
System.out.println(outPacks + " out");
} else { //Иначе, в ауте
throw new SocketException();
}
} catch (SocketException ex1) {
System.out.println("packages not clash");
System.out.println(login + " disconnected!");
//Удаляем клиента из списка доступных и информируем всех
getUserList().deleteUser(login);
broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been disconnect", getUserList().getUsers()));
flag = true;
timer.stop();
} catch (IOException ex2) {
ex2.printStackTrace();
}
}
});
this.timer.start();
//Начинаем пинговать клиента
outputStream.writeObject(new Ping());
this.outPacks++;
System.out.println(outPacks + " out");
//А теперь нам остается только ждать от него сообщений
while (true) {
//Как только пинг пропал - заканчиваем
if(this.flag) {
this.flag = false;
break;
}
//Принимаем сообщение
this.c = (Message) inputStream.readObject();
//Если это ping
if (this.c instanceof Ping) {
this.inPacks++;
System.out.println(this.inPacks + " in");
} else if (! c.getMessage().equals(Config.HELLO_MESSAGE)) {
System.out.println("[" + login + "]: " + c.getMessage());
getChatHistory().addMessage(this.c);
} else {
outputStream.writeObject(getChatHistory());
this.broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been connect"));
}
this.c.setOnlineUsers(getUserList().getUsers());
if (! (c instanceof Ping) && ! c.getMessage().equals(Config.HELLO_MESSAGE)) {
System.out.println("Send broadcast Message: "" + c.getMessage() + """);
this.broadcast(getUserList().getClientsList(), this.c);
}
}
} catch (SocketException e) {
System.out.println(login + " disconnected!");
this.broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been disconnect", getUserList().getUsers()));
this.timer.stop();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
Код немного комментирован, поэтому все должны разобраться. Осталось дописать только несколько функций.
Итак, начнем по порядку.
Данная функция рассылает какое-то сообщение всем клиентам
private void broadcast(ArrayList<Client> clientsArrayList, Message message) {
try {
for (Client client : clientsArrayList) {
client.getThisObjectOutputStream().writeObject(message);
}
} catch (SocketException e) {
System.out.println("in broadcast: " + login + " disconnected!");
getUserList().deleteUser(login);
this.broadcast(getUserList().getClientsList(), new Message("System", "The user " + login + " has been disconnected", getUserList().getUsers()));
timer.stop();
} catch (IOException e) {
e.printStackTrace();
}
}
Ах да, мы еще забыли вписать некоторые поля класса ClientThread, которые активно использовали. Итак, класс ClientThread,java целиком
public class ClientThread extends Thread {
private final static int DELAY = 30000;
private Socket socket;
private Message c;
private String login;
private int inPacks = 0;
private int outPacks = 0;
private boolean flag = false;
private Timer timer;
public ClientThread(Socket socket) {
this.socket = socket;
this.start();
}
public void run() {
try {
final ObjectInputStream inputStream = new ObjectInputStream(this.socket.getInputStream());
final ObjectOutputStream outputStream = new ObjectOutputStream(this.socket.getOutputStream());
this.c = (Message) inputStream.readObject();
this.login = this.c.getLogin();
if (! this.c.getMessage().equals(Config.HELLO_MESSAGE)) {
System.out.println("[" + this.c.getLogin() + "]: " + this.c.getMessage());
getChatHistory().addMessage(this.c);
} else {
outputStream.writeObject(getChatHistory());
this.broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been connect"));
}
getUserList().addUser(login, socket, outputStream, inputStream);
this.c.setOnlineUsers(getUserList().getUsers());
this.broadcast(getUserList().getClientsList(), this.c);
this.timer = new Timer(DELAY, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
try {
if (inPacks == outPacks) {
outputStream.writeObject(new Ping());
outPacks++;
System.out.println(outPacks + " out");
} else {
throw new SocketException();
}
} catch (SocketException ex1) {
System.out.println("packages not clash");
System.out.println(login + " disconnected!");
getUserList().deleteUser(login);
broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been disconnect", getUserList().getUsers()));
flag = true;
timer.stop();
} catch (IOException ex2) {
ex2.printStackTrace();
}
}
});
this.timer.start();
outputStream.writeObject(new Ping());
this.outPacks++;
System.out.println(outPacks + " out");
while (true) {
if(this.flag) {
this.flag = false;
break;
}
this.c = (Message) inputStream.readObject();
if (this.c instanceof Ping) {
this.inPacks++;
System.out.println(this.inPacks + " in");
} else if (! c.getMessage().equals(Config.HELLO_MESSAGE)) {
System.out.println("[" + login + "]: " + c.getMessage());
getChatHistory().addMessage(this.c);
} else {
outputStream.writeObject(getChatHistory());
this.broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been connect"));
}
this.c.setOnlineUsers(getUserList().getUsers());
if (! (c instanceof Ping) && ! c.getMessage().equals(Config.HELLO_MESSAGE)) {
System.out.println("Send broadcast Message: "" + c.getMessage() + """);
this.broadcast(getUserList().getClientsList(), this.c);
}
}
} catch (SocketException e) {
System.out.println(login + " disconnected!");
getUserList().deleteUser(login);
broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been disconnect", getUserList().getUsers()));
this.timer.stop();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private void broadcast(ArrayList<Client> clientsArrayList, Message message) {
try {
for (Client client : clientsArrayList) {
client.getThisObjectOutputStream().writeObject(message);
}
} catch (SocketException e) {
System.out.println("in broadcast: " + login + " disconnected!");
getUserList().deleteUser(login);
this.broadcast(getUserList().getClientsList(), new Message("Server-Bot", "The user " + login + " has been disconnected", getUserList().getUsers()));
timer.stop();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Осталось разобраться с функциями getUserList()
и getChatHistory
Для начала, определим еще 3 класса
public class Client {
private Socket socket;
private ObjectOutputStream oos;
private ObjectInputStream ois;
public Client(Socket socket){
this.socket = socket;
}
public Client(Socket socket , ObjectOutputStream oos , ObjectInputStream ois ){
this.socket = socket;
this.oos = oos;
this.ois = ois;
}
public Socket getSocket() {
return this.socket;
}
public ObjectOutputStream getThisObjectOutputStream() {
return this.oos;
}
public ObjectInputStream getThisObjectInputStream() {
return this.ois;
}
public void setThisObjectOutputStream(ObjectOutputStream oos) {
this.oos = oos;
}
public void setThisObjectInputStream(ObjectInputStream ois) {
this.ois = ois;
}
}
public class UsersList {
private Map<String, Client> onlineUsers = new HashMap<String, Client>();
public void addUser(String login, Socket socket, ObjectOutputStream oos, ObjectInputStream ois) {
System.out.println( login +" connected" );
if (!this.onlineUsers.containsKey(login)) {
this.onlineUsers.put(login , new Client(socket, oos, ois));
} else {
int i = 1;
while(this.onlineUsers.containsKey(login)) {
login = login + i;
i++;
}
this.onlineUsers.put(login , new Client(socket, oos, ois));
}
}
public void deleteUser(String login) {
this.onlineUsers.remove(login);
}
public String[] getUsers() {
return this.onlineUsers.keySet().toArray(new String[0]);
}
public ArrayList<Client> getClientsList() {
ArrayList<Client> clientsList = new ArrayList<Client>(this.onlineUsers.entrySet().size());
String s = "";
for(Map.Entry<String, Client> m : this.onlineUsers.entrySet()){
clientsList.add(m.getValue());
System.out.println(m.getKey());
s = s + m.getKey();
}
return clientsList;
}
}
public class ChatHistory implements Serializable {
private List<Message> history;
public ChatHistory() {
this.history = new ArrayList<Message>(Config.HISTORY_LENGTH);
}
public void addMessage(Message message){
if (this.history.size() > Config.HISTORY_LENGTH){
this.history.remove(0);
}
this.history.add(message);
}
public List<Message> getHistory(){
return this.history;
}
}
По сути, сервер готов к работе, осталось только немного модифицировать 2 класса.
public class Server {
private static UsersList list = new UsersList();
private static ChatHistory chatHistory = new ChatHistory();
public static void main(String[] args) {
try {
//Создаем слушатель
ServerSocket socketListener = new ServerSocket("1234");
while (true) {
Socket client = null;
while (client == null) {
client = socketListener.accept();
}
new ClientThread(client); //Создаем новый поток, которому передаем сокет
}
} catch (SocketException e) {
System.err.println("Socket exception");
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O exception");
e.printStackTrace();
}
}
public synchronized static UsersList getUserList() {
return list;
}
public synchronized static ChatHistory getChatHistory() {
return chatHistory;
}
}
Методы getChatHistory()
и getUserList()
сделаны синхронизированными, потому что с ними могут работать несколько потоков
И, доделаем наш конфиг, так как у нас добавились некоторые параметры
public class Config {
private static final String PROPERTIES_FILE = "./server.properties";
public static int PORT;
public static int HISTORY_LENGTH;
public static String HELLO_MESSAGE;
static {
Properties properties = new Properties();
FileInputStream propertiesFile = null;
try {
propertiesFile = new FileInputStream(PROPERTIES_FILE);
properties.load(propertiesFile);
PORT = Integer.parseInt(properties.getProperty("PORT"));
HISTORY_LENGTH = Integer.parseInt(properties.getProperty("HISTORY_LENGTH"));
HELLO_MESSAGE = properties.getProperty("HELLO_MESSAGE");
} catch (FileNotFoundException ex) {
System.err.println("Properties config file not found");
} catch (IOException ex) {
System.err.println("Error while reading file");
} finally {
try {
propertiesFile.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
Теперь наш сервер готов к использованию. Мы познакомились с сериализацией объектов и работой с сокетами в Java.
В следующей статье (завтра-послезавтра) мы напишем клиент для нашего чата, а пока жду комментариев, особенно относительно устройства чата, в частности — «Здесь абсолютно неправильно реаизовано XXX» (разумеется, жду только аргументированных оценок)
В любом случае, данная статья не призвана стать эталоном написания чата на Java, это лишь инструмент для понимания как вообще создавать такие приложения на хорошем примере, а не на эхо-чате в 10 строк.
Автор: Anexroid
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/10098
Нажмите здесь для печати.