
Введение
В этой статье будет показан пример создания небольшого многопользовательского чата с помощью сокетов. Для его реализации вам понадобиться Java и Maven.
Создание структуры проекта
Структуру проекта будем создавать с помощью Maven. Для этого откройте терминал и перейдите в директорию, в которой хотите создать проект, и введите следующую команду:
mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.5
После ввода команды, настройте проект, введя такие данные как groupId, artifactId и т.п. После ввода этих данных у вас должно получиться что-то вроде этого:

После этого напишите символ 'y' и нажмите Enter. В текущей директории у вас создался проект с названием, которое вы указывали в 'artifactId'.
Теперь нужно чучуть подшаманить в файле pom.xml. Найдите в нем тег 'maven.compiler.release' и измените его значение на вашу версию Java.

Затем, чтобы запустить проект, нам следует для удобство указать имя jar архива, который в дальнейшем будем запускать, и указать для maven главный файл проекта.


Заходим в файл App.java и изменим сгенерированный код на что-то подобное:

Теперь мы готовы запустить проект. Для этого соберем его в jar архив, введя следующую команду в терминале, находясь в корне проекта.
mvn package
Проект собрался, теперь мы можем его запустить, введя следующую команду:
java -jar target/example-chat.jar
После выполнения команды мы увидим успешное выполнение программы.
Начало работы
Конечное приложение после своего запуска позволит как запустить серверную часть, указав для нее порт, так и клиентскую часть, в которой будем указывать ip сервера и порт для подключения.
Начнем с того, что напишем код, который будет спрашивать у пользователя что он хочет, запустить сервер или же клиент:
package ru.example.chat;
import java.util.Scanner;
import ru.example.chat.client.MySocketClient;
import ru.example.chat.server.MySocketServer;
public class App {
public static void main(String[] args) {
Scanner reader = new Scanner(System.in);
int decision = 0;
while (decision != 3) {
System.out.print(
"___Меню___nn"
+ "1. Клиентn"
+ "2. Серверn"
+ "3. Выходn"
+ "| "
);
decision = reader.nextInt();
switch (decision) {
case 1:
// Вызываем статичный метод start для запуска клиента
MySocketClient.start();
break;
case 2:
// Вызываем статичный метод start для запуска сервера
MySocketServer.start();
break;
case 3:
return;
default:
System.out.println("[INFO] Моя твоя не понимать");
break;
}
}
}
}
Серверная часть
Рассмотрим код серверной части:
package ru.example.chat.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class MySocketServer {
private static int port;
// список пользователей, которые вошли в чат
public static List<User> users = new ArrayList<>();
private static ServerSocket serverSocket;
private static BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in)
);
public static void start() {
boolean correctPort = false;
System.out.print(
"___Настройка сервера самого крутого чата___nn"
);
// цикл будет работать, пока пользователь не введет корректный порт
while (!correctPort) {
System.out.print("Введите порт: ");
try {
port = Integer.parseInt(
reader.readLine()
);
} catch (IOException e) {
System.out.println("[ERROR] Че-то пошло не так, давай еще раз");
continue;
} catch (NumberFormatException e) {
System.out.println("[ERROR] Будь добр, введи цифру");
continue;
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
continue;
}
correctPort = true;
}
try {
// запускаем сервер на указанном порту
serverSocket = new ServerSocket(port);
System.out.println("___Стартовал сервер самого крутого чата___n");
while (true) {
// ожидаем подключение нового пользователя
Socket newUser = serverSocket.accept();
System.out.println(
"[INFO] Новое подключениеn"
+ "[INFO] IP: " + newUser.getInetAddress()
);
// добавляем нового пользователя в наш список пользователей
// создавая для него отдельный поток (см. конструктор класса User)
// который будет слушать сообщения от него
users.add(
new User(newUser)
);
}
} catch (IOException e) {
System.out.println("[ERROR] Произошла ошибка при старте сервера");
return;
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
return;
}
}
}
При запуске статичного метода start происходит настройка сервера, после чего пытаемся запустить сервер на порту, который указал пользователь. Затем сервер ожидает нового подключения, после этого происходит добавление его в список пользователей и создается для него новый поток, который ожидает от него сообщения. Это выглядит примерно так:

Клиентская часть
Рассмотрим код клиентской части:
package ru.example.chat.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
public class MySocketClient {
private static String domain;
private static int port;
private static BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in)
);
public static void start() {
boolean correctPort = false, correctDomain = false;
System.out.print(
"___Настройка клиента самого крутого чата___nn"
);
// цикл будет работать, пока пользователь не введет корректный домен
while (!correctDomain) {
System.out.print(
"Введите домен сервера: "
);
try {
domain = reader.readLine();
} catch (IOException e) {
System.out.println("[ERROR] Че-то пошло не так, давай еще раз");
continue;
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
continue;
}
correctDomain = true;
}
// цикл будет работать, пока пользователь не введет корректный порт
while (!correctPort) {
System.out.print("Введите порт: ");
try {
port = Integer.parseInt(
reader.readLine()
);
} catch (IOException e) {
System.out.println("[ERROR] Че-то пошло не так, давай еще раз");
continue;
} catch (NumberFormatException e) {
System.out.println("[ERROR] Будь добр, введи цифру");
continue;
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
continue;
}
correctPort = true;
}
try {
// пробуем подключиться к серверу, создавая экземляр класса Socket
// который передаем в конструктор самописного класса ClientSocket
ClientSocket clientSocket = new ClientSocket(
new Socket(domain, port)
);
String message = "";
// цикл работает, пока сообщение, введенное пользователем
// не равно строке #exit
while (!message.equals("#exit")) {
System.out.print("| ");
// получаем сообщение от пользователя из консоли
message = reader.readLine();
// отправка сообщения на сервер
clientSocket.sendMessage(message);
}
System.out.println("[INFO] Закрытие чата...");
clientSocket.closeClient();
} catch (UnknownHostException e) {
System.out.println("[ERROR] Неизвестный хост");
} catch (IOException e) {
System.out.println("[ERROR] Че-то пошло не так");
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
}
}
}
В клиентской части мы запрашиваем ip или домен сервера, а также порт, на котором он был запущен. Затем мы пытаемся подключится к серверу на основании введенных данных, и в случае успеха мы внутри конструктора класса ClientSocket создаем экземпляр класса MessageListener, который занимается прослушкой сообщений с сервера в отдельном потоке. Код класса ClientSocket:
package ru.example.chat.client;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class ClientSocket {
private Socket socket;
private BufferedReader in;
private BufferedWriter out;
// самописный класс, который занимается прослушкой сообщений с сервера
private MessageListener messageListener;
public ClientSocket(Socket socket) throws IOException {
this.socket = socket;
// получаем input stream нашего сокета
// для получения данных с сервера
this.in = new BufferedReader(
new InputStreamReader(
socket.getInputStream()
)
);
// получаем output stream нашего сокета
// для отправки данных на сервер
this.out = new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream()
)
);
// получаем экземпляр нашего самописного класса
// передавая ему ссылку на текущий объект
this.messageListener = new MessageListener(
this
);
}
// метод занимается отправкой данных на сервер
public void sendMessage(String message) {
try {
this.out.write(message + "n");
this.out.flush();
} catch (IOException e) {
System.out.println("[ERROR] Ошибка при отправке сообщения");
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
}
}
// метод занимается получением данных с сервера
public String getMessage() {
try {
return this.in.readLine();
} catch (IOException e) {
System.out.println("[ERROR] Ошибка при получении сообщения");
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
}
return "";
}
public void closeClient() {
try {
this.socket.close();
this.in.close();
this.out.close();
this.messageListener.stopListener();
} catch (IOException e) {
System.out.println("[ERROR] Ошибка");
} catch (Exception e) {
System.out.println("[ERROR] Ошибка");
}
}
}
Главный поток постоянно занят тем, что ждет сообщение пользователя из консоли, после его получения отправляет сообщение на сервер. Процесс создания объектов на клиенте можно представить так:

Пример работы
После запуска приложения нам отрисуется меню, в котором мы выберем 2-й пункт для запуска сервера, затем введем порт, например 8080 после чего увидим сообщение об успешном старте сервера:

Теперь откроем новую сессию терминала и после запуска приложения выберем 1-й пункт для старта клиента, после чего нам будет предложено ввести домен сервера, в нашем случае localhost и порт 8080:

В тоже время на серверной стороне зарегистрировано новое подключение:

Для начала общения в чате достаточно просто ввести свое имя и начать общение.

Заключение
Надеюсь у меня получилось показать, как реализовать клиент-серверное приложение на Java с помощью сокетов.
Исходный код приложения можете посмотреть на GitHub.
Ссылка на GitHub - https://github.com/ZhadanD/example-chat
Автор: DVZH7