🎯 Цель работы
Освоить базовые принципы работы с TCP-сокетами в Java, реализовать клиент-серверное взаимодействие по модели «запрос-ответ». Научиться работать корректно управлять ресурсами.
📋 Задание
-
Реализовать серверное приложение, принимающее строку от клиента и возвращающее её в соответствии с вариантом.
-
Реализовать клиентское приложение, отправляющее тестовые строки и получающее ответ.
-
Протестировать сервер с помощью утилиты telnet (или netcat).
-
Обеспечить корректную обработку исключений и освобождение сетевых ресурсов.
🏗️ Теория
Основы сетевого взаимодействия
Сокет (Socket) - Конечная точка сетевого соединения (IP-адрес + порт). Используется для установления соединения между клиентом и сервером
ServerSocket - это «Слушающий» сокет на стороне сервера. Ждёт входящих подключений (ожидание реализовано в методе accept()).
Потоки ввода-вывода реализует передачу данных по установленному соединению, так socket.getInputStream() и его “обертки” используются для чтения, а socket.getOutputStream() для записи.
Методы accept(), read() блокируют выполнение потока, пока не произойдёт событие (подключение или поступление данных).
Корректная работа с ресурсами
Используйте try-with-resources для всех сетевых ресурсов (Socket, ServerSocket, потоки). finally блок нужен только для нестандартных ситуаций или дополнительной логики очистки
java
// С finally
Socket socket = null;
try {
socket = new Socket(...);
// работа
} catch (IOException e) {
// обработка
} finally {
if (socket != null) {
try { socket.close(); } catch (IOException e) { /* ignore */ }
}
}
При этом finally выполняется ВСЕГДА, даже при успешном завершении!
Современная альтерантива для автозакрываемых ресурсов (сокеты, потоки)
java
// С try-with-resources
try (Socket socket = new Socket(...)) {
// работа
} catch (IOException e) {
// обработка
}
Обратите внимание в этой работе!
java
// try-with-resources автоматически вызывает close() на всех ресурсах
try (clientSocket; BufferedReader in = ...; PrintWriter out = ...) {
// ...
} // Здесь автоматически вызывается out.close(), in.close(), clientSocket.close()
Указания по выполнению работы
Шаг 1. Создание простейшего сервера
java
public class EchoServer {
private static final int PORT = 12345; // Порт для прослушивания
public static void main(String[] args) {
System.out.println("Сервер запущен на порту " + PORT);
// try-with-resources для гарантированного закрытия ServerSocket
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
// Бесконечный цикл для приема клиентов (пока сервер работает)
while (true) {
// accept() блокирует, пока клиент не подключится
Socket clientSocket = serverSocket.accept();
System.out.println("Подключился клиент: " + clientSocket.getInetAddress());
// ПОКА Обрабатываем клиента в этом же потоке
handleClient(clientSocket);
}
} catch (IOException e) {
System.err.println("Ошибка в работе сервера: " + e.getMessage());
}
}
// метод обработки сообщений
private static void handleClient(Socket clientSocket) {
try (clientSocket;
//поток чтения и буферизауии сообщения от клиента
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//поток для отправки сообщения
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String inputLine;
// Читаем строку от клиента. readLine() блокируется, пока не получит '\n' или EOF.
while ((inputLine = in.readLine()) != null) {
System.out.println("Получено от клиента: " + inputLine);
// Простейший эхо-ответ: отправляем строку обратно
String response = "ЭХО: " + inputLine; // Базовая логика
out.println(response); // Отправляем ответ клиенту
}
System.out.println("Клиент отключился: " + clientSocket.getInetAddress());
} catch (IOException e) {
System.err.println("Ошибка при обработке клиента: " + e.getMessage());
}
}
}
Шаг 2. Создание простейшего клиента
public class Client {
private static final String HOST = "localhost";
private static final int PORT = 12345;
public static void main(String[] args) {
try (Socket socket = new Socket(HOST, PORT);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println("Подключено к серверу. Вводите строки (Ctrl+D/Ctrl+C для выхода):");
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput); // Отправляем серверу
String response = in.readLine(); // Читаем ответ
System.out.println("Ответ сервера: " + response);
}
} catch (UnknownHostException e) {
System.err.println("Неизвестный хост: " + HOST);
} catch (IOException e) {
System.err.println("Ошибка ввода-вывода: " + e.getMessage());
}
}
Шаг 3. Тестирование через Telnet
Запустить EchoServer.
Открыть командную строку/терминал: telnet localhost 12345
Ввести строку, нажать Enter. Увидеть ответ от сервера.

Решить проблемы с кодировкой. Обратите внимание!
Теория
Потоки ввода-вывода
// 1. Получаем низкоуровневый поток байт
OutputStream rawOutput = socket.getOutputStream();
InputStream rawInput = socket.getInputStream();
// 2. Добавляем буферизацию (для эффективности)
BufferedOutputStream bufferedOutput = new BufferedOutputStream(rawOutput);
BufferedInputStream bufferedInput = new BufferedInputStream(rawInput);
// 3. Преобразуем байты ↔ символы (с кодировкой)
OutputStreamWriter charOutput = new OutputStreamWriter(bufferedOutput, StandardCharsets.UTF_8);
InputStreamReader charInput = new InputStreamReader(bufferedInput, StandardCharsets.UTF_8);
// 4. Добавляем удобные методы для работы со строками
PrintWriter out = new PrintWriter(charOutput, true); // autoFlush автоматически сбрасывает буфер (с autoFlush=true) и Не нужно вызывать flush() после каждой отправки
BufferedReader in = new BufferedReader(charInput);
Шаг 4. Реализация индивидуального варианта
Выделите сервер в отдельный класс, реализуйте метод handleClient, следуя своему варианту.
Варианты заданий для эхо-сервера
| № | Название варианта | Описание | Пример (Клиент → Сервер) | Примечания |
|---|---|---|---|---|
| 1 | Базовый эхо | Сервер возвращает полученную строку без изменений дважды | “Hello” → “Hello Hello” | Базовый вариант |
| 2 | Эхо с номером | Сервер добавляет к ответу порядковый номер сообщения для данного соединения | “Hi” → “[1] Hi”, “Test” → “[2] Test” | Нумерация с 1 для каждого клиента |
| 3 | Реверс-эхо | Сервер возвращает строку задом наперёд | “Java” → “avaJ” | Учесть Unicode символы |
| 4 | Эхо в верхнем регистре | Все символы преобразуются к верхнему регистру | “Hello World” → “HELLO WORLD” | Locale-независимое преобразование |
| 5 | Эхо с временем | К ответу добавляется время получения сообщения | “Test” → “[14:30:25] Test” | Формат времени: HH:mm:ss |
| 6 | Ограниченное эхо | Сервер обрабатывает только первые N символов строки (N = № варианта) | N=5: “Hello World” → “Hello” | Если строка короче N - возвращается целиком |
| 7 | Эхо с префиксом | Сервер добавляет префикс “[ECHO]: “ к ответу | “Message” → “[ECHO]: Message” | Префикс добавляется всегда |
| 8 | Фильтрующее эхо | Сервер удаляет все цифры из строки перед отправкой | “Test123” → “Test” | Удалить цифры 0-9 |
| 9 | Эхо-подтверждение | Сервер отвечает “OK: [сообщение]” только если сообщение не пустое | “Hello” → “OK: Hello”, “” → “” | Для пустых строк - пустой ответ |
| 10 | Многострочное эхо | Сервер ждет отправки строки “END” и возвращает все полученные строки одной ответной строкой | “Line1”, “Line2”, “END” → “Line1\nLine2” | Разделитель - перенос строки |
| 11 | Эхо в нижнем регистре | Все символы преобразуются к нижнему регистру | “JAVA Programming” → “java programming” | Учесть локализацию |
| 12 | Эхо с длиной | К ответу добавляется длина строки в символах | “Hello” → “Hello (5 chars)” | В скобках указать длину |
| 13 | Шифрованное эхо | Каждый символ сдвигается на +1 по ASCII/Unicode | “abc” → “bcd”, “z” → “{“ | Сдвиг по таблице Unicode |
| 14 | Эхо-палиндром | Проверяет, является ли строка палиндромом | “radar” → “radar (palindrome)”, “java” → “java” | Учитывать регистр? (по варианту) |
| 15 | Эхо с удалением пробелов | Удаляет все пробелы из строки | “Hello World” → “HelloWorld” | Удалить все whitespace символы |
| 16 | Эхо слов наоборот | Переворачивает порядок слов в строке | “Java is fun” → “fun is Java” | Разделитель - пробел |
| 17 | Эхо с подсчетом слов | Возвращает строку и количество слов | “Hello Java World” → “Hello Java World [3 words]” | Слово - последовательность непробельных символов |
| 18 | Эхо-цензор | Заменяет все гласные буквы на ‘*’ | “Hello World” → “Hll W*rld” | Гласные: a, e, i, o, u, y (и их заглавные) |
| 19 | Эхо с хеш-суммой | Добавляет к ответу хеш-код строки | “Test” → “Test [hash: 275190528]” | Использовать String.hashCode() |
| 20 | Эхо только букв | Оставляет только буквы (A-Z, a-z) | “Hello123!” → “Hello” | Удалить цифры и спецсимволы |
| 21 | Эхо с задержкой | Возвращает ответ с задержкой N секунд (N = № варианта) | N=2: “Hi” → (через 2 сек) “Hi” | Использовать Thread.sleep() |
| 22 | Эхо-калькулятор | Вычисляет простое арифметическое выражение | “2 + 3” → “5”, “10 / 2” → “5” | Поддержка: +, -, *, /, целые числа |
| 23 | Эхо с кодированием Base64 | Кодирует строку в Base64 | “Hello” → “SGVsbG8=” | |
| 24 | Эхо-ротатор | Циклически сдвигает символы на N позиций | N=1: “abc” → “bca” | |
| 25 | Эхо с уникальными символами | Возвращает только уникальные символы строки | “hello” → “helo” | |
| 26 | Эхо-валидатор email | Проверяет, является ли строка email-адресом | “test@mail.com” → “Valid email” | |
| 27 | Эхо с сортировкой символов | Сортирует символы строки по алфавиту | “java” → “aajv” | |
| 28 | Эхо с частотой символов | Возвращает строку и частоту самого частого символа | “hello” → “hello (l: 2 times)” | |
| 29 | Эхо с удалением дубликатов слов | Удаляет повторяющиеся слова | “hello hello world” → “hello world” | |
| 30 | Эхо с переводом в двоичный вид | Переводит каждый символ в двоичный код | “AB” → “01000001 01000010” | |
| 31 | Эхо с шифром Цезаря | Шифрует строку шифром Цезаря со сдвигом N | N=3: “abc” → “def” | |
| 32 | Эхо с подсветкой цифр | Обозначает цифры в строке | “a1b2” → “a[1]b[2]” |
Шаг 5. Тестирование сервера и клиента на разных компьютерах
Работа в парах. Один студент запускает сервер на своем ПК, другой - клиент
- Найти и сообщить друг другу IP
- Изменить SERVER_IP на IP партнёра
- Запустить сервер и клиент на разных компьютерах
- Обсудить, какие изменения в коде потребовались по сравнению с localhost
- Модифицировать программу для удобства работы с различными IP и сокетами (подумайте об аргументах командной строки из лаб.4)
- Подумайте о проверке типа “свой-чужой”
- Самостоятельно выполните решение типичных проблем (firewall, неправильный IP)
Шаг 6*. Проверка доступности (поиск) серверов
Напиши программу, которая в текущей сети находит доступные эхо-серверы по заданному сокету (или диапазону)
public class NetworkScanner {
public static void main(String[] args) {
String baseIp = "192.168.1."; // Ваша подсеть
int port = 12345;
System.out.println("Сканируем сеть на наличие эхо-серверов...");
for (int i = 1; i <= 254; i++) {
String ip = baseIp + i;
// Пропускаем свой IP
if (ip.equals(getMyIp())) continue;
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(ip, port), 500); // Таймаут 500ms
System.out.println("✓ Найден эхо-сервер на " + ip);
// Тестируем подключение
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out.println("ping");
String response = in.readLine();
System.out.println(" Ответ: " + response);
} catch (IOException e) {
// Сервер не найден - это нормально
}
}
}
private static String getMyIp() throws SocketException {
// Возвращает IP текущего компьютера
return InetAddress.getLocalHost().getHostAddress();
}
}
Подумайте о сетевой безопасности (не оставлять сервера открытыми без необходимости)