Как написать на java чат

Всем доброго времени суток. На связи Алексей Гулынин. В данной статье я бы хотел написать серверную часть простого чата на Java. Все комментарии будут даны в коде.

Скачать уже готовый проект полностью можно по ссылке.

Проект серверной части будет состоять из 3 файлов:

  • Server.java — класс, который описывает логику работы сервера.
  • ClientHandler.java — класс, в котором обрабатывается подключение клиента к серверу.
  • Main.java.

В классе Main будет просто создаваться экземпляр нашего сервера:

package server;

public class Main {

  public static void main(String[] args) {
    Server server = new Server();
  }
}

Класс Server:

package server;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

public class Server {
  // порт, который будет прослушивать наш сервер
  static final int PORT = 3443;
  // список клиентов, которые будут подключаться к серверу
  private ArrayList<ClientHandler> clients = new ArrayList<ClientHandler>();

  public Server() {
    // сокет клиента, это некий поток, который будет подключаться к серверу
    // по адресу и порту
    Socket clientSocket = null;
    // серверный сокет
    ServerSocket serverSocket = null;
    try {
      // создаём серверный сокет на определенном порту
      serverSocket = new ServerSocket(PORT);
      System.out.println("Сервер запущен!");
      // запускаем бесконечный цикл
      while (true) {
        // таким образом ждём подключений от сервера
        clientSocket = serverSocket.accept();
        // создаём обработчик клиента, который подключился к серверу
        // this - это наш сервер
        ClientHandler client = new ClientHandler(clientSocket, this);
        clients.add(client);
        // каждое подключение клиента обрабатываем в новом потоке
        new Thread(client).start();
      }
    }
    catch (IOException ex) {
      ex.printStackTrace();
    }
    finally {
      try {
        // закрываем подключение
        clientSocket.close();
        System.out.println("Сервер остановлен");
        serverSocket.close();
      }
      catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }
		
  // отправляем сообщение всем клиентам
  public void sendMessageToAllClients(String msg) {
    for (ClientHandler o : clients) {
      o.sendMsg(msg);
    }

  }

  // удаляем клиента из коллекции при выходе из чата
  public void removeClient(ClientHandler client) {
    clients.remove(client);
  }

}

Класс ClientHandler:

package server;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Scanner;

// реализуем интерфейс Runnable, который позволяет работать с потоками
public class ClientHandler implements Runnable {
  // экземпляр нашего сервера
  private Server server;
  // исходящее сообщение
  private PrintWriter outMessage;
  // входящее собщение
  private Scanner inMessage;
  private static final String HOST = "localhost";
  private static final int PORT = 3443;
  // клиентский сокет
  private Socket clientSocket = null;
  // количество клиента в чате, статичное поле
  private static int clients_count = 0;

  // конструктор, который принимает клиентский сокет и сервер
  public ClientHandler(Socket socket, Server server) {
    try {
      clients_count++;
      this.server = server;
      this.clientSocket = socket;
      this.outMessage = new PrintWriter(socket.getOutputStream());
      this.inMessage = new Scanner(socket.getInputStream());
    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }
  // Переопределяем метод run(), который вызывается когда
  // мы вызываем new Thread(client).start();
  @Override
  public void run() {
    try {
      while (true) {
        // сервер отправляет сообщение
        server.sendMessageToAllClients("Новый участник вошёл в чат!");
        server.sendMessageToAllClients("Клиентов в чате = " + clients_count);
        break;
      }

      while (true) {
        // Если от клиента пришло сообщение
        if (inMessage.hasNext()) {
        String clientMessage = inMessage.nextLine();
	// если клиент отправляет данное сообщение, то цикл прерывается и 
	// клиент выходит из чата
        if (clientMessage.equalsIgnoreCase("##session##end##")) {
          break;
        }
	// выводим в консоль сообщение (для теста)
        System.out.println(clientMessage);
	// отправляем данное сообщение всем клиентам
        server.sendMessageToAllClients(clientMessage);
      }
      // останавливаем выполнение потока на 100 мс
      Thread.sleep(100);
    }
  }
  catch (InterruptedException ex) {
    ex.printStackTrace();
  }
  finally {
    this.close();
  }
}
  // отправляем сообщение
  public void sendMsg(String msg) {
    try {
      outMessage.println(msg);
      outMessage.flush();
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
  // клиент выходит из чата
  public void close() {
    // удаляем клиента из списка
    server.removeClient(this);
    clients_count--;
    server.sendMessageToAllClients("Клиентов в чате = " + clients_count);
  }
}

В данной статье мы создали серверную часть простого чата на Java.

На связи был Алексей Гулынин, оставляйте свои комментарии, увидимся в следующих статьях.

Краткое описание

На данный момент заканчиваю 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’овать друг друга), чтобы в случае отключения клиента (или сервера) все об этом узнали.

Итак, приступим. Создадим наш первый класс

Server.java

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

Config.java

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. Список доступных серверу клиентов (для отображения у пользователя)

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

Message.java

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’ов.

Ping.java

public class Ping extends Message {
	public Ping() {
		super("ping", "ping");
	}
}

По сути, этот класс нам не сильно-то нужен, просто потом код будет удобнее читать

Итак, приступим к написанию ClientThread

ClientThread.run()

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();
      }
}

Код немного комментирован, поэтому все должны разобраться. Осталось дописать только несколько функций.
Итак, начнем по порядку.

Данная функция рассылает какое-то сообщение всем клиентам

ClientThread.broadcast()

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 целиком

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 класса

Client.java

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;
    }
}

UserList.java

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;
    }

}

ChatHistory.java

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 класса.

Server.java

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() сделаны синхронизированными, потому что с ними могут работать несколько потоков

И, доделаем наш конфиг, так как у нас добавились некоторые параметры

server.properties

PORT=5000
HISTORY_LENGTH=50
HELLO_MESSAGE=User join to the chat(Auto-message)

Config.java

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

Details
Written by  
Last Updated on 18 July 2019   |   Print  Email

In this Java network programming tutorial, you will learn how to create a chat application in Java using Socket programming. Source code is provided for you to download.

 

1. Overview of the Java Chat Application

The Java Chat application you are going to build is a console application that is launched from the command line. The server and clients can run on different computers in the same network, e.g. Local Area Network (LAN).

There can be multiple clients connect to a server and they can chat to each other, just like in a chat room where everyone can see other users’ messages. There’s no private chat between two users, for simplicity.

After getting connected to the server, a user must provide his or her name to enter the chat. The server sends a list of currently online users to the new user.

Every user is notified when a new user arrives and when a user has gone. Each message is prefixed with the username to keep track who sent the message.

And finally, the user says ‘bye’ to quit the chat.

The application consists of two parts: server and client. Each part can run independently on separate computers.

Now, let’s see how to code this Java chat application in details.

 

2. Create the Chat Server Program

The server is implemented by two classes: ChatServer and UserThread.

The ChatServer class starts the server, listening on a specific port. When a new client gets connected, an instance of UserThread is created to serve that client. Since each connection is processed in a separate thread, the server is able to handle multiple clients at the same time.

The following is source code of the ChatServer class:

package net.codejava.networking.chat.server;

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * This is the chat server program.
 * Press Ctrl + C to terminate the program.
 *
 * @author www.codejava.net
 */
public class ChatServer {
	private int port;
	private Set<String> userNames = new HashSet<>();
	private Set<UserThread> userThreads = new HashSet<>();

	public ChatServer(int port) {
		this.port = port;
	}

	public void execute() {
		try (ServerSocket serverSocket = new ServerSocket(port)) {

			System.out.println("Chat Server is listening on port " + port);

			while (true) {
				Socket socket = serverSocket.accept();
				System.out.println("New user connected");

				UserThread newUser = new UserThread(socket, this);
				userThreads.add(newUser);
				newUser.start();

			}

		} catch (IOException ex) {
			System.out.println("Error in the server: " + ex.getMessage());
			ex.printStackTrace();
		}
	}

	public static void main(String[] args) {
		if (args.length < 1) {
			System.out.println("Syntax: java ChatServer <port-number>");
			System.exit(0);
		}

		int port = Integer.parseInt(args[0]);

		ChatServer server = new ChatServer(port);
		server.execute();
	}

	/**
	 * Delivers a message from one user to others (broadcasting)
	 */
	void broadcast(String message, UserThread excludeUser) {
		for (UserThread aUser : userThreads) {
			if (aUser != excludeUser) {
				aUser.sendMessage(message);
			}
		}
	}

	/**
	 * Stores username of the newly connected client.
	 */
	void addUserName(String userName) {
		userNames.add(userName);
	}

	/**
	 * When a client is disconneted, removes the associated username and UserThread
	 */
	void removeUser(String userName, UserThread aUser) {
		boolean removed = userNames.remove(userName);
		if (removed) {
			userThreads.remove(aUser);
			System.out.println("The user " + userName + " quitted");
		}
	}

	Set<String> getUserNames() {
		return this.userNames;
	}

	/**
	 * Returns true if there are other users connected (not count the currently connected user)
	 */
	boolean hasUsers() {
		return !this.userNames.isEmpty();
	}
}

As you can see, the ChatServer class has two Set collections to keep track the names and threads of the connected clients. Set is used because it doesn’t allow duplication and the order of elements does not matter:

private Set<String> userNames = new HashSet<>();
private Set<UserThread> userThreads = new HashSet<>();

An important method in the ChatServer class is broadcast() which deliver a message from one client to all others clients:

void broadcast(String message, UserThread excludeUser) {
	for (UserThread aUser : userThreads) {
		if (aUser != excludeUser) {
			aUser.sendMessage(message);
		}
	}
}

The UserThread class is responsible for reading messages sent from the client and broadcasting messages to all other clients. First, it sends a list of online users to the new user. Then it reads the username and notifies other users about the new user.

The following code is of the UserThread class:

package net.codejava.networking.chat.server;

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * This thread handles connection for each connected client, so the server
 * can handle multiple clients at the same time.
 *
 * @author www.codejava.net
 */
public class UserThread extends Thread {
	private Socket socket;
	private ChatServer server;
	private PrintWriter writer;

	public UserThread(Socket socket, ChatServer server) {
		this.socket = socket;
		this.server = server;
	}

	public void run() {
		try {
			InputStream input = socket.getInputStream();
			BufferedReader reader = new BufferedReader(new InputStreamReader(input));

			OutputStream output = socket.getOutputStream();
			writer = new PrintWriter(output, true);

			printUsers();

			String userName = reader.readLine();
			server.addUserName(userName);

			String serverMessage = "New user connected: " + userName;
			server.broadcast(serverMessage, this);

			String clientMessage;

			do {
				clientMessage = reader.readLine();
				serverMessage = "[" + userName + "]: " + clientMessage;
				server.broadcast(serverMessage, this);

			} while (!clientMessage.equals("bye"));

			server.removeUser(userName, this);
			socket.close();

			serverMessage = userName + " has quitted.";
			server.broadcast(serverMessage, this);

		} catch (IOException ex) {
			System.out.println("Error in UserThread: " + ex.getMessage());
			ex.printStackTrace();
		}
	}

	/**
	 * Sends a list of online users to the newly connected user.
	 */
	void printUsers() {
		if (server.hasUsers()) {
			writer.println("Connected users: " + server.getUserNames());
		} else {
			writer.println("No other users connected");
		}
	}

	/**
	 * Sends a message to the client.
	 */
	void sendMessage(String message) {
		writer.println(message);
	}
}

Then it enters a loop of reading message from the user and sending it to all other users, until the user sends ‘bye’ indicating he or she is going to quit. And finally it notifies other users about the disconnection of this user and closes the connection.

 

3. Create the Chat Client Program

The client is implemented by three classes: ChatClient, ReadThread and WriteThread.

The ChatClient starts the client program, connects to a server specified by hostname/IP address and port number. Once the connection is made, it creates and starts two threads ReadThread and WriteThread.

Here is source code of the ChatClient class:

package net.codejava.networking.chat.client;

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

/**
 * This is the chat client program.
 * Type 'bye' to terminte the program.
 *
 * @author www.codejava.net
 */
public class ChatClient {
	private String hostname;
	private int port;
	private String userName;

	public ChatClient(String hostname, int port) {
		this.hostname = hostname;
		this.port = port;
	}

	public void execute() {
		try {
			Socket socket = new Socket(hostname, port);

			System.out.println("Connected to the chat server");

			new ReadThread(socket, this).start();
			new WriteThread(socket, this).start();

		} catch (UnknownHostException ex) {
			System.out.println("Server not found: " + ex.getMessage());
		} catch (IOException ex) {
			System.out.println("I/O Error: " + ex.getMessage());
		}

	}

	void setUserName(String userName) {
		this.userName = userName;
	}

	String getUserName() {
		return this.userName;
	}


	public static void main(String[] args) {
		if (args.length < 2) return;

		String hostname = args[0];
		int port = Integer.parseInt(args[1]);

		ChatClient client = new ChatClient(hostname, port);
		client.execute();
	}
}

The ReadThread is responsible for reading input from the server and printing it to the console repeatedly, until the client disconnects. This class is implemented as follows:

package net.codejava.networking.chat.client;

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

/**
 * This thread is responsible for reading server's input and printing it
 * to the console.
 * It runs in an infinite loop until the client disconnects from the server.
 *
 * @author www.codejava.net
 */
public class ReadThread extends Thread {
	private BufferedReader reader;
	private Socket socket;
	private ChatClient client;

	public ReadThread(Socket socket, ChatClient client) {
		this.socket = socket;
		this.client = client;

		try {
			InputStream input = socket.getInputStream();
			reader = new BufferedReader(new InputStreamReader(input));
		} catch (IOException ex) {
			System.out.println("Error getting input stream: " + ex.getMessage());
			ex.printStackTrace();
		}
	}

	public void run() {
		while (true) {
			try {
				String response = reader.readLine();
				System.out.println("n" + response);

				// prints the username after displaying the server's message
				if (client.getUserName() != null) {
					System.out.print("[" + client.getUserName() + "]: ");
				}
			} catch (IOException ex) {
				System.out.println("Error reading from server: " + ex.getMessage());
				ex.printStackTrace();
				break;
			}
		}
	}
}

And the WriteThread is responsible for reading input from the user and sending it to the server, continuously until the user types ‘bye’ to end the chat. This class is implemented as follows:

package net.codejava.networking.chat.client;

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

/**
 * This thread is responsible for reading user's input and send it
 * to the server.
 * It runs in an infinite loop until the user types 'bye' to quit.
 *
 * @author www.codejava.net
 */
public class WriteThread extends Thread {
	private PrintWriter writer;
	private Socket socket;
	private ChatClient client;

	public WriteThread(Socket socket, ChatClient client) {
		this.socket = socket;
		this.client = client;

		try {
			OutputStream output = socket.getOutputStream();
			writer = new PrintWriter(output, true);
		} catch (IOException ex) {
			System.out.println("Error getting output stream: " + ex.getMessage());
			ex.printStackTrace();
		}
	}

	public void run() {

		Console console = System.console();

		String userName = console.readLine("nEnter your name: ");
		client.setUserName(userName);
		writer.println(userName);

		String text;

		do {
			text = console.readLine("[" + userName + "]: ");
			writer.println(text);

		} while (!text.equals("bye"));

		try {
			socket.close();
		} catch (IOException ex) {

			System.out.println("Error writing to server: " + ex.getMessage());
		}
	}
}

The reasons for running these two threads simultaneously is that the reading operation always blocks the current thread (both reading user’s input from command line and reading server’s input via network). That means if the current thread is waiting for the user’s input, it can’t read input from the server.

Therefore, two separate threads are used to make the client responsive: it can display messages from other users while reading message from the current user.

That’s how the chat application is designed. For more details, you can read the comments in the source code provided. But there are no many comments because the code is self-explanatory.

 

4. How to Run the Chat Server

You need to specify the port number when running the server program from the command line. For example:

java ChatServer 8989

This starts the server listening on the port number 8989, and you see the following output in the server once it started:

Chat Server is listening on port 8989

The server is waiting for new clients forever, so you have to press Ctrl + C to terminate it.

 

5. How to Run a Chat Client

To run the client, you need to specify the server’s hostname/IP address and port number in the command line. For example:

java ChatClient localhost 8989

This tells the client to connect to the server at localhost on port 8989. Then you see the following message in the server’s console:

New user connected

And in the client’s console:

Connected to chat server
No other users connected

You see, the server tells the client how many users are connected, but there’s no user at this time. Then the program asks for the username:

Enter your name:_

Enter a username, say John, and then you can start chatting:

Enter your name: John
[John]:_

Now, let’s start the second client with username is Peter. At this time, you see the server tells that there’s one online user is John:

Connected users: [John]

The user John is notified about new user Peter:

New user connected: Peter

Type some messages from John and Peter and you see each user sees other’s messages, just like talking in a chat room.

Now, John wants to quit so he types ‘bye’- the client program terminates, and you see the following output in the server’s console:

The user John quitted

Peter also gets a message from the server:

John has quitted.

That’s basically how the chat application is running. You can test it with more clients and the application is still running smoothly. The following screenshot illustrates a test with 4 clients:

Chat Console Application Testing

Now, it’s your time to play around with this chat application with the source code attached.

 

Related Java Network Tutorials:

  • Java InetAddress Examples
  • Java Socket Client Examples (TCP/IP)
  • Java Socket Server Examples (TCP/IP)
  • Java UDP Client Server Program Example

Other Java network tutorials:

  • How to use Java URLConnection and HttpURLConnection
  • Java URLConnection and HttpURLConnection Examples
  • Java HttpURLConnection to download file from an HTTP URL
  • Java HTTP utility class to send GET/POST request
  • Java upload files by sending multipart request programmatically

About the Author:

Nam Ha Minh is certified Java programmer (SCJP and SCWCD). He started programming with Java in the time of Java 1.4 and has been falling in love with Java since then. Make friend with him on Facebook and watch his Java videos you YouTube.

Add comment

Время на прочтение
6 мин

Количество просмотров 52K

Всем привет. В преддверии старта курса «Разработчик на Spring Framework» мы подготовили для вас еще один полезный перевод. Но, прежде чем перейти к статье, хотим поделиться с вами бесплатной записью урока от наших преподавателей по теме: «Рефакторинг кода приложений на Spring», а также предлагаем посмотреть запись вебинара из которого вы сможете подробно узнать о программе курса и формате обучения.

А теперь перейдем к статье


В статье Building Scalable Facebook-like Notification using Server-Sent Event and Redis для отправки сообщений от сервера клиенту мы использовали Server-sent Events. Также там было упомянуто о WebSocket — технологии двунаправленной связи между сервером и клиентом.

В этой статье мы посмотрим на один из распространенных примеров использования WebSocket. Мы напишем приложение для обмена приватными сообщениями.

Ниже на видео продемонстрировано то, что мы собираемся сделать.

Введение в WebSockets и STOMP

WebSocket — это протокол для двусторонней связи между сервером и клиентом.
WebSocket, в отличие от HTTP, протокола прикладного уровня, является протоколом транспортного уровня (TCP). Хотя для первоначальной установки соединения используется HTTP, но потом соединение «обновляется» до TCP-соединения, используемого в WebSocket.

WebSocket — протокол низкого уровня, который не определяет форматы сообщений. Поэтому WebSocket RFC определяет подпротоколы, описывающие структуру и стандарты сообщений. Мы будем использовать STOMP поверх WebSockets (STOMP over WebSockets).

Протокол STOMP (Simple / Streaming Text Oriented Message Protocol) определяет правила обмена сообщениями между сервером и клиентом.

STOMP похож на HTTP и работает поверх TCP, используя следующие команды:

  • CONNECT
  • SUBSCRIBE
  • UNSUBSCRIBE
  • SEND
  • BEGIN
  • COMMIT
  • ACK

Спецификацию и полный список команд STOMP можно найти здесь.

Архитектура

  • Сервис аутентификации (Auth Service) ответственен за аутентификацию и управление пользователями. Здесь мы не будем изобретать колесо и воспользуемся сервисом аутентификации из статьи JWT and Social Authentication using Spring Boot.
  • Сервис чата (Chat Service) ответственен за настройку WebSocket, обработку STOMP-сообщений, а также за сохранение и обработку сообщений пользователей.
  • Клиент (Chat Client) — это приложение на ReactJS, использующее STOMP-клиента для подключения и подписки на чат. Также здесь находится пользовательский интерфейс.

Модель сообщения

Первое, о чем нужно подумать — это модель сообщения. ChatMessage выглядит следующим образом:

public class ChatMessage {
   @Id
   private String id;
   private String chatId;
   private String senderId;
   private String recipientId;
   private String senderName;
   private String recipientName;
   private String content;
   private Date timestamp;
   private MessageStatus status;
}

Класс ChatMessage довольно простой, с полями, необходимыми для идентификации отправителя и получателя.

В нем также есть поле статуса, указывающее доставлено ли сообщение клиенту.

public enum MessageStatus {
    RECEIVED, DELIVERED
}

Когда сервер получает сообщение из чата, он не отправляет сообщение адресату напрямую, а отправляет уведомление (ChatNotification), чтобы оповестить клиента о получении нового сообщения. После этого клиент сам может получить новое сообщение. Как только клиент получит сообщение, оно помечается как доставленное (DELIVERED).

Уведомление выглядит следующим образом:

public class ChatNotification {
    private String id;
    private String senderId;
    private String senderName;
}

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

Настройка WebSocket и STOMP в Spring

Первым делом настраиваем конечную точку STOMP и брокер сообщений.

Для этого создаем класс WebSocketConfig с аннотациями @Configuration и @EnableWebSocketMessageBroker.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker( "/user");
        config.setApplicationDestinationPrefixes("/app");
        config.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry
                .addEndpoint("/ws")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
        DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
        resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setObjectMapper(new ObjectMapper());
        converter.setContentTypeResolver(resolver);
        messageConverters.add(converter);
        return false;
    }
}

Первый метод конфигурирует простой брокер сообщений в памяти с одним адресом с префиксом /user для отправки и получения сообщений. Адреса с префиксом /app предназначены для сообщений, обрабатываемых методами с аннотацией @MessageMapping, которые мы обсудим в следующем разделе.

Второй метод регистрирует конечную точку STOMP /ws. Эта конечная точка будет использоваться клиентами для подключения к STOMP-серверу. Здесь также включается резервный SockJS, который будет использоваться, если WebSocket будет недоступен.

Последний метод настраивает конвертер JSON, который используется Spring’ом для преобразования сообщений из/в JSON.

Контроллер для обработки сообщений

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

@Controller
public class ChatController {

    @Autowired private SimpMessagingTemplate messagingTemplate;
    @Autowired private ChatMessageService chatMessageService;
    @Autowired private ChatRoomService chatRoomService;

    @MessageMapping("/chat")
    public void processMessage(@Payload ChatMessage chatMessage) {
        var chatId = chatRoomService
                .getChatId(chatMessage.getSenderId(), chatMessage.getRecipientId(), true);
        chatMessage.setChatId(chatId.get());

        ChatMessage saved = chatMessageService.save(chatMessage);
        
        messagingTemplate.convertAndSendToUser(
                chatMessage.getRecipientId(),"/queue/messages",
                new ChatNotification(
                        saved.getId(),
                        saved.getSenderId(),
                        saved.getSenderName()));
    }
}

С помощью аннотации @MessageMapping мы настраиваем, что при отправке сообщения в /app/chat вызывается метод processMessage. Обратите внимание, что к маппингу добавится сконфигурированный ранее application-префикс /app.

Этот метод сохраняет сообщение в MongoDB, а затем вызывает метод convertAndSendToUser для отправки уведомления адресату.

Метод convertAndSendToUser добавляет префикс /user и recipientId к адресу /queue/messages. Конечный адрес будет выглядеть так:

/user/{recipientId}/queue/messages

Все подписчики данного адреса (в нашем случае один) получат сообщение.

Генерация chatId

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

Класс ChatRoom выглядит следующим образом:

public class ChatRoom {
    private String id;
    private String chatId;
    private String senderId;
    private String recipientId;
}

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

JavaScript-клиент

В этом разделе мы создадим JavaScript-клиента, который будет отправлять сообщения на WebSocket/STOMP-сервер и получать их оттуда.

Мы будем использовать SockJS и Stomp.js для общения с сервером с использованием STOMP over WebSocket.

const connect = () => {
    const Stomp = require("stompjs");
    var SockJS = require("sockjs-client");
    SockJS = new SockJS("http://localhost:8080/ws");
    stompClient = Stomp.over(SockJS);
    stompClient.connect({}, onConnected, onError);
  };

Метод connect() устанавливает соединение с /ws, где ожидает подключений наш сервер, и также определяет callback-функцию onConnected, которая будет вызвана при успешном подключении, и onError, вызываемую, если при подключении к серверу произошла ошибка.

const onConnected = () => {
    console.log("connected");

    stompClient.subscribe(
      "/user/" + currentUser.id + "/queue/messages",
      onMessageReceived
    );
  };

Метод onConnected() подписывается на определенный адрес и получает все отправляемые туда сообщения.

const sendMessage = (msg) => {
    if (msg.trim() !== "") {
      const message = {
        senderId: currentUser.id,
        recipientId: activeContact.id,
        senderName: currentUser.name,
        recipientName: activeContact.name,
        content: msg,
        timestamp: new Date(),
      };
        
      stompClient.send("/app/chat", {}, JSON.stringify(message));
    }
  };

В конце метода sendMessage() сообщение отправляется по адресу /app/chat, который указан в нашем контроллере.

Заключение

В этой статье мы рассмотрели все важные моменты создания чата с использованием Spring Boot и STOMP over WebSocket.

Мы также создали JavaScript-клиент с применением библиотек SockJs и Stomp.js.

Пример исходного кода можно найти здесь.


Узнать о курсе подробнее.


Читать ещё

  • Интеграционное тестирование в SpringBoot с TestContainers-стартером
  • Что нового в Spring Data (Klara Dan von) Neumann

Prerequisites : Introducing threads in socket programming
In the above article, a simple date time server was created which handled multiple user requests at the same time using threading. It explains the basic concepts of threading in network programming. The same concepts can be used with very slight modification to extend the above idea and create a chatting application similar to facebook messenger, whatsapp, etc. 
The following article covers the implementation of such an application with a detailed explanation, limitations, and their solutions. 
In this set, we will discuss Server side programming(Server.java), Client side programming(Client.java) is discussed in Set 2.

Server Side Programming(Server.java)

1. Server class : The main server implementation is easy and similar to the previous article. The following points will help understand Server implementation :

  1. The server runs an infinite loop to keep accepting incoming requests.
  2. When a request comes, it assigns a new thread to handle the communication part.
  3. The server also stores the client name into a vector, to keep a track of connected devices. The vector stores the thread object corresponding to the current request. The helper class uses this vector to find the name of recipient to which message is to be delivered. As this vector holds all the streams, handler class can use it to successfully deliver messages to specific clients.
  4. Invoke the start() method.

2. ClientHandler class : Similar to previous article, we create a helper class for handling various requests. This time, along with the socket and streams, we introduce a name variable. This will hold the name of the client that is connected to the server. The following points will help understand ClientHandler implementation :

  • Whenever the handler receives any string, it breaks it into the message and recipient part. It uses Stringtokenizer for this purpose with ‘#’ as the delimiter. Here it is assumed that the string is always of the format: 
message # recipient
  • It then searches for the name of recipient in the connected clients list, stored as a vector in the server. If it finds the recipients name in the clients list, it forwards the message on its output stream with the name of the sender prefixed to the message. 

Java

import java.io.*;

import java.util.*;

import java.net.*;

public class Server

{

    static Vector<ClientHandler> ar = new Vector<>();

    static int i = 0;

    public static void main(String[] args) throws IOException

    {

        ServerSocket ss = new ServerSocket(1234);

        Socket s;

        while (true)

        {

            s = ss.accept();

            System.out.println("New client request received : " + s);

            DataInputStream dis = new DataInputStream(s.getInputStream());

            DataOutputStream dos = new DataOutputStream(s.getOutputStream());

            System.out.println("Creating a new handler for this client...");

            ClientHandler mtch = new ClientHandler(s,"client " + i, dis, dos);

            Thread t = new Thread(mtch);

            System.out.println("Adding this client to active client list");

            ar.add(mtch);

            t.start();

            i++;

        }

    }

}

class ClientHandler implements Runnable

{

    Scanner scn = new Scanner(System.in);

    private String name;

    final DataInputStream dis;

    final DataOutputStream dos;

    Socket s;

    boolean isloggedin;

    public ClientHandler(Socket s, String name,

                            DataInputStream dis, DataOutputStream dos) {

        this.dis = dis;

        this.dos = dos;

        this.name = name;

        this.s = s;

        this.isloggedin=true;

    }

    @Override

    public void run() {

        String received;

        while (true)

        {

            try

            {

                received = dis.readUTF();

                System.out.println(received);

                if(received.equals("logout")){

                    this.isloggedin=false;

                    this.s.close();

                    break;

                }

                StringTokenizer st = new StringTokenizer(received, "#");

                String MsgToSend = st.nextToken();

                String recipient = st.nextToken();

                for (ClientHandler mc : Server.ar)

                {

                    if (mc.name.equals(recipient) && mc.isloggedin==true)

                    {

                        mc.dos.writeUTF(this.name+" : "+MsgToSend);

                        break;

                    }

                }

            } catch (IOException e) {

                e.printStackTrace();

            }

        }

        try

        {

            this.dis.close();

            this.dos.close();

        }catch(IOException e){

            e.printStackTrace();

        }

    }

}

Output:

New client request received : Socket[addr=/127.0.0.1,port=61818,localport=1234]
Creating a new handler for this client...
Adding this client to active client list
New client request received : Socket[addr=/127.0.0.1,port=61819,localport=1234]
Creating a new handler for this client...
Adding this client to active client list

Limitations:
Although the above implementation of server manages to handle most of the scenarios, there are some shortcomings in the approach defined above.

  • One clear observation from above programs is that if the number of clients grew large, the searching time would increase in the handler class. To avoid this increase, two hash maps can be used. One with name as the key, and index in active list as the value. Another with index as key, and associated handler object as value. This way, we can quickly look up the two hashmaps for matching recipient. It is left to the readers to implement this hack to increase efficiency of the implementation.
  • Another thing to notice is that this implementation doesn’t work well when users disconnect from the server. A lot of errors would be thrown because disconnection is not handled in this implementation. It can easily be implemented as in previous basic TCP examples. It is also left for the reader to implement this feature in the program.

There is a huge difference in the client program(Client.java) than the previous articles, so it will be discussed in Set 2 of this series.

Related Article : Multi-threaded chat Application | Set 2

This article is contributed by Rishabh Mahrsee. If you like GeeksforGeeks and would like to contribute, you can also write an article using write.geeksforgeeks.org or mail your article to review-team@geeksforgeeks.org. See your article appearing on the GeeksforGeeks main page and help other Geeks.
Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.
 

Prerequisites : Introducing threads in socket programming
In the above article, a simple date time server was created which handled multiple user requests at the same time using threading. It explains the basic concepts of threading in network programming. The same concepts can be used with very slight modification to extend the above idea and create a chatting application similar to facebook messenger, whatsapp, etc. 
The following article covers the implementation of such an application with a detailed explanation, limitations, and their solutions. 
In this set, we will discuss Server side programming(Server.java), Client side programming(Client.java) is discussed in Set 2.

Server Side Programming(Server.java)

1. Server class : The main server implementation is easy and similar to the previous article. The following points will help understand Server implementation :

  1. The server runs an infinite loop to keep accepting incoming requests.
  2. When a request comes, it assigns a new thread to handle the communication part.
  3. The server also stores the client name into a vector, to keep a track of connected devices. The vector stores the thread object corresponding to the current request. The helper class uses this vector to find the name of recipient to which message is to be delivered. As this vector holds all the streams, handler class can use it to successfully deliver messages to specific clients.
  4. Invoke the start() method.

2. ClientHandler class : Similar to previous article, we create a helper class for handling various requests. This time, along with the socket and streams, we introduce a name variable. This will hold the name of the client that is connected to the server. The following points will help understand ClientHandler implementation :

  • Whenever the handler receives any string, it breaks it into the message and recipient part. It uses Stringtokenizer for this purpose with ‘#’ as the delimiter. Here it is assumed that the string is always of the format: 
message # recipient
  • It then searches for the name of recipient in the connected clients list, stored as a vector in the server. If it finds the recipients name in the clients list, it forwards the message on its output stream with the name of the sender prefixed to the message. 

Java

import java.io.*;

import java.util.*;

import java.net.*;

public class Server

{

    static Vector<ClientHandler> ar = new Vector<>();

    static int i = 0;

    public static void main(String[] args) throws IOException

    {

        ServerSocket ss = new ServerSocket(1234);

        Socket s;

        while (true)

        {

            s = ss.accept();

            System.out.println("New client request received : " + s);

            DataInputStream dis = new DataInputStream(s.getInputStream());

            DataOutputStream dos = new DataOutputStream(s.getOutputStream());

            System.out.println("Creating a new handler for this client...");

            ClientHandler mtch = new ClientHandler(s,"client " + i, dis, dos);

            Thread t = new Thread(mtch);

            System.out.println("Adding this client to active client list");

            ar.add(mtch);

            t.start();

            i++;

        }

    }

}

class ClientHandler implements Runnable

{

    Scanner scn = new Scanner(System.in);

    private String name;

    final DataInputStream dis;

    final DataOutputStream dos;

    Socket s;

    boolean isloggedin;

    public ClientHandler(Socket s, String name,

                            DataInputStream dis, DataOutputStream dos) {

        this.dis = dis;

        this.dos = dos;

        this.name = name;

        this.s = s;

        this.isloggedin=true;

    }

    @Override

    public void run() {

        String received;

        while (true)

        {

            try

            {

                received = dis.readUTF();

                System.out.println(received);

                if(received.equals("logout")){

                    this.isloggedin=false;

                    this.s.close();

                    break;

                }

                StringTokenizer st = new StringTokenizer(received, "#");

                String MsgToSend = st.nextToken();

                String recipient = st.nextToken();

                for (ClientHandler mc : Server.ar)

                {

                    if (mc.name.equals(recipient) && mc.isloggedin==true)

                    {

                        mc.dos.writeUTF(this.name+" : "+MsgToSend);

                        break;

                    }

                }

            } catch (IOException e) {

                e.printStackTrace();

            }

        }

        try

        {

            this.dis.close();

            this.dos.close();

        }catch(IOException e){

            e.printStackTrace();

        }

    }

}

Output:

New client request received : Socket[addr=/127.0.0.1,port=61818,localport=1234]
Creating a new handler for this client...
Adding this client to active client list
New client request received : Socket[addr=/127.0.0.1,port=61819,localport=1234]
Creating a new handler for this client...
Adding this client to active client list

Limitations:
Although the above implementation of server manages to handle most of the scenarios, there are some shortcomings in the approach defined above.

  • One clear observation from above programs is that if the number of clients grew large, the searching time would increase in the handler class. To avoid this increase, two hash maps can be used. One with name as the key, and index in active list as the value. Another with index as key, and associated handler object as value. This way, we can quickly look up the two hashmaps for matching recipient. It is left to the readers to implement this hack to increase efficiency of the implementation.
  • Another thing to notice is that this implementation doesn’t work well when users disconnect from the server. A lot of errors would be thrown because disconnection is not handled in this implementation. It can easily be implemented as in previous basic TCP examples. It is also left for the reader to implement this feature in the program.

There is a huge difference in the client program(Client.java) than the previous articles, so it will be discussed in Set 2 of this series.

Related Article : Multi-threaded chat Application | Set 2

This article is contributed by Rishabh Mahrsee. If you like GeeksforGeeks and would like to contribute, you can also write an article using write.geeksforgeeks.org or mail your article to review-team@geeksforgeeks.org. See your article appearing on the GeeksforGeeks main page and help other Geeks.
Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.
 

Программирование на Java – перспективное направление у современных разработчиков. Согласно данным Google данный язык кодинга пользуется огромным спросом, несмотря на то, что был создан изначально весьма давно. А именно – в 1995 году. С тех пор Джава перетерпел много модификаций. Нынче это – один из самых перспективных языков программирования. Весьма прост в освоении, что значительно упрощает задачи, поставленные перед новичками.

Особенности Java

Google и другие поисковые системы помогут понять, что это за язык программирования. Он имеет ряд ключевых особенностей. А именно:

  • относительно простой синтаксис;
  • отличное комьюнити;
  • множество документации (в том числе на русском языке);
  • наличие ООП;
  • собственный движок.

New programs создавать при помощи данного варианта способен даже начинающий программист.

Основное предназначение Java – это работа с Сетью. Идеально подходит для веб-программирования. Но и «обычные» утилиты посредством соответствующего семейства пишутся без существенных затруднений.

Сервер типа http – определение

При работе с сетью (new или old – не так важно) компьютеры подключаются к так называемым веб-серверам. Без них невозможна работа в интернете.

HTTP-сервер – это веб server. Имеет непосредственное отношение как к «железу» компьютера, так и к программному обеспечению:

  1. В качестве аппаратного устройства это – new машина, которая отвечает за хранение ресурсов того или иного сайта. Включает в себя доставку на устройство юзера через интернет-обозреватели и иные утилиты. Чаще всего подключается к интернету. Доступ предоставляется через доменные имена.
  2. Как ПО, согласно Google, веб-сервер представляет собой некое «приложение», совмещающее в себе функционал для контроля доступа web-пользователей к размещенным на сервере документов. HTTP Sever – часть программного обеспечения, понимающая URLs и HTTP-протоколы (они нужны для просмотра и отображения страничек в Сети).

Google указывает на то, что, когда браузер нуждается в каком-то файле, помещенном на веб-сервере, происходит запрос посредством http. Когда запрос доходит до нужного «железа», соответствующий сервер (программное обеспечение) осуществляет обратную передачу через упомянутый ранее «канал».

Виды серверов

Если разработчик на Джаве решил создать new servers, важно понимать – для опубликования сайта требуется подобрать один из нескольких видов оных. Упомянутый элемент бывает:

  1. Статическим (static void main). Носит название стека. Включает в себя компьютер с сервером HTTP. Последний будет посылать new файлы в интернет-обозреватель без каких-либо корректировок.
  2. Динамическим. Включает в себя статическую «модель» и дополнительное ПО. Чаще всего – базы данные или серверные приложения. Последние будут вносить изменения в документы перед тем, как отправить их в обозреватель Сети.

За счет new servers можно отображать страницы в браузерах. Итоговый результат удобен и понятен пользователям без навыков в сфере программирования. Для своей работы, согласно Google, рассматриваемый элемент может задействовать шаблоны информации из БД Пример – Википедия. Это – не полноценный сайт, а HTML-шаблон. За счет соответствующего приема удается значительно ускорить сопровождение web-софта.

Все наготове – встроенные возможности Java

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

Для Джава поставленная задача не выступает в качестве тривиальной. Язык программирования содержит встроенные возможности, посредством которых new http создается без существенных затруднений. Всего 100 строчек кода – и перед разработчиком окажется весьма неплохой вариант, поддерживающий обработку запросов и иные HTTP-команд.

HTTPServer

Серверные возможности в Джаве предоставляются через SDK. Они имеют следующие особенности:

  • название – HttpServer;
  • пакетный класс – com.sun.net;
  • запись: httpServer server = httpServer.create (new InetSocketAddress(“localhost”, 8001), 0));.

Приведенный пример – это создание экземпляра в пределах локального узла. Номер порта – 8001. Аргумент 0 здесь выступает для организации так называемой обратной регистрации.

Как выполняется запрос – очередность

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

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

  1. Клиент формирует запрос. Происходит его отправка.
  2. Ставится очередь операционной системы.
  3. Происходит передача на сервер для дальнейшей обработки.
  4. Одновременные запросы ставятся в очередь. Их количество определяется ОС автоматически.

Важно: предложенный пример не требует отправлять в очередь никаких «команд», так как имеет значение аргумента, равное нулю.

О коде

Вот пример, который поможет лучше разобрать в изучаемой сфере даже без Google:

Здесь происходит создание контекста test. Он выступает корнем контекста утилиты. Второй параметр – экземпляр так называемого обработчика. Он будет работать с HTTP-командами.

Теперь допускается применение потокового пула. В приведенном примере их 10 штук:

New Thread PoolExecutor ThreadPoolExecutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(10);

Далее new server требует запуска. Операция осуществляется путем задействования кода: server.start();.

Handler

А вот интерфейс согласно данным Google, использующий метод handle():

Внимание: в приведенном примере вышедший за рамки изображения код имеет вид   private void (может быть и public static void) handleResponse(HttpExchange httpExchange, String requestParamValue)  throws  IOException {

        OutputStream outputStream = httpExchange.getResponseBody();

        StringBuilder htmlBuilder = new StringBuilder();

Кодификация обрабатывает запрос, затем отправляет ответ непосредственно клиенту. Обработка осуществляется через класс HttpExchange.

Запрос GET

Об обработке запроса Get необходимо знать следующее:

  • написавшие его будут использовать метод HandleGETRequest;
  • далее происходит вызов getRequestURL(), который принадлежит классу HttpExchange.

Несмотря на то, что это – минимум, обрабатывающий единичный запрос, он поможет справиться с самыми разными задачами.

Работа с ответом

После того, как произошел поиск ответа, его нужно направить клиенту. Делается это через handleResponse(). Пользователь получит выходной поток через обращение к методу gerResponseBody(). Чуть позже удастся записать информацию из HTML в выходные потоки.

Response header – это крайне важный момент. Если он будет упущен, в обозревателе Сети юзер увидит ошибку ERR_Empty_Response. В случае, когда все хорошо, браузер покажет тот или иной ответ.

Близкое знакомство – собственный чат через WebSoket и SpringBoot на Java

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

В Google можно отыскать немало new идей относительно того, как создать собственный chat. При определенной сноровке соответствующий ресурс будет действительно уникальным: со смайликами, эмодзи, анимацией и другими элементами.

Для написания подобного контента в Java можно использовать:

  • WebSoket;
  • SpringBoot.

При помощи соответствующих элементов даже новичок сможет без труда разобраться с поставленной задачей. Ему не придется долго изучать Google, а также «непонятные» элементы кода (типа ioexception e, public void run, override public и так далее – на первых порах в них можно запутаться).

Определение WebSoket

WebSoket – это протокол, при помощи которого осуществляется установка двусторонней связи клиент-сервер. Переключение, как говорит Google, происходит после специального http-запроса. Его формирует и отправляет клиент: Upgrade: websocket.

При поддержке вебсокетов будет получен ответ «Yes». Далее произойдет общение через new протоколы WebSocket. С HTTP оный не имеет ничего общего.

Как создать приложение

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

  1. Зайти на страницу.
  2. Выбрать в списке Spring Boot 2.
  3. Указать имя группы и артефакта проекта.
  4. Активировать зависимость «вебсокета».
  5. Провести генерацию new project.

Далее предстоит разархивировать проект, а также сделать import java в редактор. Именем утилиты будет – Maven. Дополнительно необходимо создать пакеты config, controller и model.

Настройка

Теперь предстоит работать с public class, а также с такими элементами как static final int и private static. Ведь самое простое позади – далее требуется провести настройку «вебсокета».

Начинается процесс с конечной точки и брокера сообщений. Проводится операция в config. Класс конфигурации будет иметь следующий вид:

Здесь:

  • аннотация @configuration – устанавливается обязательно в классе конфигурации Spring;
  • аннотация @EnableWebSocketMessageBroker – активирует new WebsokcketServer;
  • метод registerStompEndpoints() – отвечает за регистрацию конечной точки, которую клиенты задействуют для подключения к серверу;
  • configMessageBroker() – настройка брокера для отправки сообщений между клиентами.

В рассматриваемом примере задействован встроенный брокер. Это самый простой вариант.

Модели сообщений

Следующие исходники – это настройка моделей сообщений. Создается пакет model, в котором после размещается класс ChatMessage:

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

Контроллер сообщений

Google поможет разобраться в том, как создавать собственные чаты. Чтобы в них можно было отправлять сообщения и получать оные, стоит:

  • сделать пакет controller;
  • разместить внутри класс chatController;
  • внутри ЧатКонтроллер присутствуют методы, которые отвечают за доставку сообщений от одного пользователя и трансляцию всем остальным.

Для настройки контроллера используется следующая кодификация:

События и Front-End

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

Статистика – тоже немаловажный нюанс. If юзер хочет получить полноценный чат, ему предстоит выйти за пределы Джавы.

Чтобы справиться с поставленной задачей требуется:

  • сделать папку static;
  • расположить ее по пути scr/main/resources.

Выглядеть это будет так:

HTML и скрипты

В Google также говорится о том, что для работы полноценного чата требуется создать HTML-файл и JavaScript.

В первом случае используется new запись:


Скрипт Джавы требуется для соединения с итоговой точкой, а также отправки/получения сообщений. Он будет иметь имя main.js:



Стилизация

Исходники CSS можно создавать самостоятельно или подключать уже готовые варианты. Данная «опция» отвечает за внешний вид программы.

Здесь можно найти исходники соответствующего кода (пункт 7).

Запуск

Все, что теперь остается – это проверить Spring через Boot-файл с функцией main в корне иерархии имеющихся папок.

После запуска веб-сервера требуется перейти по адресу и пользоваться созданным контентом.

Для того, чтобы лучше разбираться в соответствующей сфере и термины int port, close и иные составляющие Джавы не были чем-то непонятным, стоит закончить специализированные курсы. Они без Google помогут освоить Java, а также всего его тонкости.

Понравилась статья? Поделить с друзьями:
  • Как написать на английском павлович
  • Как написать на английском отчество евгеньевна
  • Как написать на английском орех
  • Как написать на английском она очень добрая
  • Как написать на английском она коричневая