🎯 Цель работы

Освоить базовые принципы работы с TCP-сокетами в Java, реализовать клиент-серверное взаимодействие по модели «запрос-ответ». Научиться работать корректно управлять ресурсами.

📋 Задание

  1. Реализовать серверное приложение, принимающее строку от клиента и возвращающее её в соответствии с вариантом.

  2. Реализовать клиентское приложение, отправляющее тестовые строки и получающее ответ.

  3. Протестировать сервер с помощью утилиты telnet (или netcat).

  4. Обеспечить корректную обработку исключений и освобождение сетевых ресурсов.

🏗️ Теория

Основы сетевого взаимодействия

Сокет (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. Тестирование сервера и клиента на разных компьютерах

Работа в парах. Один студент запускает сервер на своем ПК, другой - клиент

  1. Найти и сообщить друг другу IP
  2. Изменить SERVER_IP на IP партнёра
  3. Запустить сервер и клиент на разных компьютерах
  4. Обсудить, какие изменения в коде потребовались по сравнению с localhost
  5. Модифицировать программу для удобства работы с различными IP и сокетами (подумайте об аргументах командной строки из лаб.4)
  6. Подумайте о проверке типа “свой-чужой”
  7. Самостоятельно выполните решение типичных проблем (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();
    }
}

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