JavaRush/Java блог/Random UA/Класи Socket та ServerSocket, або «Алло, сервер? Ти мене ...
Sergey Simonov
36 рівень

Класи Socket та ServerSocket, або «Алло, сервер? Ти мене чуєш?"

Стаття з групи Random UA
учасників
Введення: «На столі був комп'ютер, за ним був кодер ...» Класи Socket та ServerSocket, або «Алло, сервер?  Ти мене чуєш?"  - 1Якось один мій однокурсник викладав черговий результат свого вивчення Java у вигляді скріншота нової програми. Цією програмою був розрахований на багато користувачів чат. Я тоді тільки розпочинав свій власний шлях у освоєнні програмування цією мовою, але точно відзначив для себе – «хочу!». Ішов час і закінчивши роботу з черговим проектом у рамках поглиблення своїх знань програмування, я згадав про той випадок і вирішив – настав час. Якось я вже починав чисто з цікавості копати цю тему, але в моєму основному підручнику з Java (це було повне керівництво Шілдта) було надано пакету java.net лише 20 сторінок. Це і зрозуміло – книга і така велика. Там було наведено таблиці методів і конструкторів основних класів, а й усе. Наступний крок - очевидно всемогутній гугл: міріади різних статей, де представлено одне й те саме — два-три слова про сокети, і готовий приклад. Класичний підхід (як мінімум у моєму стилі навчання) - це спочатку зрозуміти, що мені потрібно з інструментів для роботи, що вони з себе уявляють, навіщо вони потрібні і тільки потім якщо вирішення завдання неочевидно колупати готові лістинги, розгвинчуючи на гайки і болтики. Але я розібрався що до чого і в результаті написав розрахований на багато користувачів чат. Зовні вийшло якось так: Класи Socket та ServerSocket, або «Алло, сервер?  Ти мене чуєш?"  - 2Тут я постараюся дати вам розуміння основ клієнт-серверних програм на основі сокетів Java на прикладі проектування чату. На курсі джавараш ви робитимете чат. Він буде кардинально іншого рівня, гарний, великий, багатофункціональний. Але завжди в першу чергу потрібно закласти фундамент, тому тут нам з вами потрібно розібратися що лежить в основі такого розділу. (Якщо ви знайшли якісь недоліки чи помилки, напишіть у ЛЗ або у коментарі під статтею). Почнемо. Голова Один: «Будинок, який…» Для пояснення як відбувається мережне з'єднання між сервером і одним клієнтом, візьмемо, що став вже класичним, приклад з багатоквартирним будинком. Допустимо, клієнту потрібно якимось чином встановити зв'язок з певним сервером. Що потрібно знати тому, хто шукає об'єкт пошуку? Так, адресаа. Сервер це не магічна сутність на хмарі, і тому він повинен знаходитися на певній машині. За аналогією з будинком, де має відбутися зустріч двох узгоджених сторін. І щоб знайти один одного в багатоквартирному будинку однієї адресаи будівлі недостатньо, необхідно вказати номер квартири, в якій відбудеться зустріч. Так і на одній обчислювальній машині може бути відразу кілька серверів, і клієнту, щоб зв'язатися з конкретним, потрібно вказати ще й номер порту по якому відбудеться з'єднання. Отже, адресаа та номер порту. Адресамає на увазі під собою ідентифікатор машини у просторі мережі Internet. Він може бути доменним ім'ям, наприклад, codegym.cc , або звичайним IP. Порт - унікальний номер, з яким пов'язаний певний сокет (цей термін буде розглянуто далі), простіше кажучи, його займає певна служба для того, щоб по ньому могли зв'язатися з нею. Так що для того, щоб відбулася зустріч як мінімум двох об'єктів на території одного (сервера) - господар місцевості (сервер) повинен зайняти конкретну квартиру (порт) на ній (машині), а другий повинен знайти місце зустрічі знаючи адресау будинку (домен або ip ), та номер квартири (порт). Голова Два: Знайомтеся, Socket Серед понять та термінів, пов'язаних із роботою в мережі, якщо одне дуже важливе – Сокет. Воно позначає точку, якою відбувається з'єднання. Простіше кажучи, сокет з'єднує у мережі дві програми. Клас Socketреалізує ідею сокету. Через його канали введення/виводу будуть спілкуватися клієнт із сервером: Класи Socket та ServerSocket, або «Алло, сервер?  Ти мене чуєш?"  - 3 Оголошується цей клас на стороні клієнта, а сервер відтворює його, отримуючи сигнал на підключення. Так відбувається спілкування у мережі. Для початку ось можливі конструктори класу Socket:
Socket(String ім'я_хоста, int порт) throws UnknownHostException, IOException
Socket(InetAddress IP-адреса, int порт) throws UnknownHostException
"ім'я_хоста" - має на увазі під собою певний вузол мережі, ip-адресау. Якщо клас сокета не зміг перетворити його на реальну, існуючу адресау, то згенерується виняток UnknownHostException. Порт є порт. Якщо як номер порту буде вказано 0, то система сама виділить вільний порт. Також при втраті з'єднання може статися виняток IOException. Слід зазначити тип адресаи у другому конструкторі - InetAddress. Він приходить на допомогу, наприклад, коли потрібно вказати як адресау доменне ім'я. Також коли під доменом мається на увазі кілька ip-адреса, то за допомогоюInetAddressможна одержати їх масив. Тим не менш, з ip він працює теж. Також можна отримати ім'я хоста, масив байт складових ip адреса і т.д. Ми трохи торкнемося його далі, але за повними відомостями доведеться пройти до офіційної документації. При ініціалізації об'єкта типу Socket, клієнт, якому той належить, повідомляє в мережі, що хоче з'єднатися з сервером про певну адресау та номер порту. Нижче представлені найчастіше використовувані методи класу Socket: InetAddress getInetAddress()- Повертає об'єкт містить дані про сокет. Якщо сокет не підключений – null int getPort()– повертає порт яким відбувається з'єднання з сервером int getLocalPort()- Повертає порт до якого прив'язаний сокет. Справа в тому, що "спілкуватися" клієнт і сервер можуть по одному порту, а порти, до яких вони прив'язані - можуть бути зовсім інші - повертає true, boolean isConnected()якщо з'єднання встановлено void connect(SocketAddress адреса)- вказує нове з'єднання boolean isClosed()- повертає true, якщо сокет закритий boolean isBound()- повертає true, якщо сокет дійсно прив'язаний до адресаи Клас Socketреалізує інтерфейс AutoCloseable, тому його можна використовувати у конструкції try-with-resources. Проте закрити сокет можна класичним чином, за допомогою close(). Голова Три: а це ServerSocket Припустимо, ми оголосабо, у вигляді класу Socket, на стороні клієнта запит на з'єднання. Як сервер розгадає наше бажання? Для цього сервер має такий клас як ServerSocket, і метод accept() у ньому. Його конструктори представлені нижче:
ServerSocket() throws IOException
ServerSocket(int порт) throws IOException
ServerSocket(int порт, int максимум_подключений) throws IOException
ServerSocket(int порт, int максимум_подключений, InetAddress локальный_адреса) throws IOException
Під час оголошення ServerSocket не потрібно вказувати адресау з'єднання, тому що спілкування відбувається на машині сервера. Тільки при багатоканальному хості потрібно вказати, до якого ip прив'язаний сокет сервера. Голова Три.Один: Сервер, який каже ні Так як надавати програмі більше ресурсів, ніж їй необхідно - і витратна і не розумна справа, тому в конструкторі ServerSocketвам пропонують оголосити максимум з'єднань, які приймає сервер при роботі. Якщо воно не вказано, то за умовчанням це число вважатиметься рівним 50. Так, за ідеєю можна припустити, що ServerSocketце такий самий сокет, тільки для сервера. Але він грає зовсім іншу роль, ніж клас Socket. Він потрібний лише на етапі створення з'єднання. Створивши об'єкт типу ServerSocketнеобхідно з'ясувати, що із сервером хтось хоче з'єднатися. Тут підключається спосіб accept(). Шуканий чекає поки хтось не захоче приєднатися до нього, і коли це відбувається повертає об'єкт типу Socket, тобто відтворений клієнтський сокет. І ось коли сокет клієнта створений за сервера, можна розпочинати двостороннє спілкування. Створити об'єкт типу Socketна стороні клієнта та відтворити його за допомогою ServerSocketна стороні сервера – ось необхідний мінімум для з'єднання. Голова Чотири: Лист "діду морозу" Вопрос: Як конкретно спілкуються клієнт та сервер? Ответ:Через потоки введення виводу. Що ми маємо? Сокет з адресаою сервера та номером порту у клієнта, і те саме, завдяки accept(), на стороні сервера. Так що розумно припустити, що спілкуватися вони будуть якраз через сокет. Для цього є два методи, які дають доступ до потоків InputStreamі OutputStreamоб'єкта типу Socket. Ось вони:
InputStream getInputStream()
OutputStream getOutputStream()
Так як читати і писати голі байти не так ефективно – потоки можна обернути в класи адаптери, буферизовані, чи ні. Наприклад:
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Щоб спілкування було двонаправленим, такі операції необхідно зробити на обох сторонах. Тепер ви можете надіслати щось за допомогою in, і прийняти за допомогою out, і навпаки. Власне, це практично єдина функція класу Socket. І так, не забувайте про спосіб flush() для BufferedWriter- він виштовхує вміст буфера. Якщо цього не зробити, інформація не буде передана, а отже, не буде отримана. Також приймаючий потік чекає покажчик кінця рядка – «\n», інакше повідомлення буде прийнято, оскільки фактично повідомлення закінчено, і є цілим. Якщо вам це здається незручним, не засмучуйтесь, завжди можна скористатися класом PrintWriter, яким потрібно обернути out, вказати другим аргументом true і тоді виштовхування з буфера буде відбуватися автоматично:
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
Також при цьому вказувати кінець рядка немає необхідності, за вас це робить цей клас. Але чи є введення/виведення рядків межами можливостей сокету? Ні, хочете оправляти об'єкти через потоки сокету? Заради Бога. Серіалізуйте їх і вперед:
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Голова П'ять: Реальний зв'язок по мережі Internet Так як для того, щоб з'єднатися по реальній мережі з реальною IP адресаою потрібно мати повноцінний сервер, а так:
  1. Наш майбутній чат, як утиліта, таких здібностей не має. Він може лише встановити з'єднання та прийняти/надіслати повідомлення. Тобто він не має реальних можливостей сервера.
  2. Наш сервер, що містить лише дані сокету та потоків вводу/виводу, не може працювати як реальний WEB-або FTP-сервер, то маючи лише це ми не зможемо з'єднатися через мережу Internet.
І до того ж, ми лише починаємо розробляти програму, а це означає, що вона не досить стабільна, щоб відразу працювати з реальною мережею, так що ми будемо використовувати як адресау для з'єднання локальний хост. Тобто за ідеєю клієнт і сервер все одно ніяк не будуть пов'язані крім як через сокет, але для налагодження програми вони перебуватиму на одній машині, без реального контакту по мережі. Для того щоб вказати в конструкторі Socket, що адресаа локальна, існує два способи:
  1. Написати як аргумент адресаи «localhost», що означає локальну заглушку. Так само для цього підходить «127.0.0.1» - це лише цифрова форма заглушки.
  2. За допомогою InetAddress:
    1. InetAddress.getByName(null)- null вказує на локальний хост
    2. InetAddress.getByName("localhost")
    3. InetAddress.getByName("127.0.0.1")
Для простоти ми будемо використовувати "localhost" типу String. Але решта варіантів теж працездатні. Голова Шість: Настав час для розмови Отже, все, що нам потрібно для реалізації сеансу розмови з сервером, у нас вже є. Залишилося зібрати це воєдино: Наступний лістинг показує, як підключається клієнт до сервера, відсилає йому одне повідомлення, а той у свою чергу підтверджує, що отримав повідомлення, використовуючи його як аргумент у своєму: "Server.java"
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    private static Socket clientSocket; // Сокет для спілкування
    private static ServerSocket server; // серверсокет
    private static BufferedReader in; // потік читання із сокету
    private static BufferedWriter out; // Потік запису в сокет

    public static void main(String[] args) {
        try {
            try  {
                server = new ServerSocket(4004); // серверсокет прослуховує порт 4004
                System.out.println("Сервер запущено!"); // добре б серверу
                // оголосити про свій запуск
                clientSocket = server.accept(); // accept() чекатиме поки
                //Хто-небудь не захоче підключитися
                try { // встановивши зв'язок та відтворивши сокет для спілкування з клієнтом можна перейти
                    // До створення потоків введення/виводу.
                    // тепер ми можемо приймати повідомлення
                    in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                    // і відправляти
                    out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                    String word = in.readLine(); // чекаємо поки клієнт щось нам напише
                    System.out.println(word);
                    // Не довго думаючи відповідає клієнту
                    out.write("Привіт, це Сервер! Підтверджую, ви написали:" + word + "\n");
                    out.flush(); // виштовхуємо все з буфера

                } finally { // у будь-якому випадку сокет буде закритий
                    clientSocket.close();
                    // потоки теж добре закрити
                    in.close();
                    out.close();
                }
            } finally {
                System.out.println("Сервер закрито!");
                    server.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
"Client.java"
import java.io.*;
import java.net.Socket;

public class Client {

    private static Socket clientSocket; // Сокет для спілкування
    private static BufferedReader reader; // нам потрібен рідер, що читає з консолі, інакше як
    // ми дізнаємося, що хоче сказати клієнт?
    private static BufferedReader in; // потік читання із сокету
    private static BufferedWriter out; // Потік запису в сокет

    public static void main(String[] args) {
        try {
            try {
                // адресаа - локальний хост, порт - 4004, такий самий як у сервера
                clientSocket = new Socket("localhost", 4004); // цим рядком ми запитуємо
                // сервер має доступ на з'єднання
                reader = new BufferedReader(new InputStreamReader(System.in));
                // читати повідомлення із сервера
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // писати туди ж
                out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                System.out.println("Ви щось хотіли сказати? Введіть це тут:");
                // якщо з'єднання відбулося і потоки успішно створені – ми можемо
                // працювати далі і запропонувати клієнту щось ввести
                // якщо ні – вилетить виняток
                String word = reader.readLine(); // чекаємо поки клієнт щось
                // не напише в консоль
                out.write(word + "\n"); // надсилаємо повідомлення на сервер
                out.flush();
                String serverWord = in.readLine(); // чекаємо, що скаже сервер
                System.out.println(serverWord); // отримавши - виводимо на екран
            } finally { // у будь-якому випадку необхідно закрити сокет та потоки
                System.out.println("Клієнт був закритий...");
                clientSocket.close();
                in.close();
                out.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }

    }
}
Зрозуміло, запускати слід спочатку сервер, бо до чого підключатиметься клієнт при запуску якщо не буде того, що його підключить? :) Висновок буде такий: / * Ви щось хотіли сказати? Введіть це тут: Алло, сервер? Ти мене чуєш? Привіт це Сервер! Підтверджую, ви написали: Алло, сервер? Ти мене чуєш? Клієнт був закритий... */ Ура! Ми навчабо сервер спілкуватися із клієнтом! Щоб спілкування відбувалося не в дві репліки, а стільки скільки завгодно, просто оберніть читання і запис потоків в цикл while (true) і вкажіть для виходу що, за певним повідомленням, наприклад, exit, цикл переривався, і програма завершилася б. Голова Сім: Розрахований на багато користувачів - краще Те, що сервер нас чує це добре, але набагато краще, якщо можна було б поспілкуватися з кимось із собі подібних. Всі вихідники я докладу в кінці статті, так що тут я показуватиму не завжди великі, але важливі шматочки коду, які дадуть можливість при правильному використанні з'єднати розрахований на багато користувачів чат. Отже, ми хочемо, щоб через сервер ми могли спілкуватися з іншим клієнтом. Як це зробити? Очевидно, що клієнтська програма має свій методmain, тобто його можна запускати окремо від сервера і паралельно з іншими клієнтами. Що це нам дає? Якимось чином потрібно що при кожному новому підключенні сервер не переходив відразу до спілкування, а записував це з'єднання в якийсь список і переходив до очікування нового підключення, а спілкуванням з конкретним клієнтом займався б якийсь допоміжний сервіс. Та й клієнти повинні писати на сервер і чекати на відповідь незалежно один від одного. На допомогу приходять нитки. Допустимо, у нас є клас, який відповідає за запам'ятовування нових підключень: У нього повинні бути вказані:
  1. Номер порту.
  2. Список, до якого записує нове з'єднання.
  3. І ServerSocket, в єдиному (!) екземплярі.
public class Server {

    public static final int PORT = 8080;
    public static LinkedList<ServerSomthing> serverList = new LinkedList<>(); // Список всіх ниток

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(PORT);
            try {
            while (true) {
                // Блокується до виникнення нової сполуки:
                Socket socket = server.accept();
                try {
                    serverList.add(new ServerSomthing(socket)); // додати нове з'єднання до списку
                } catch (IOException e) {
                    // Якщо завершиться невдачею, закривається сокет,
                    / / В іншому випадку, нитка закриє його при завершенні роботи:
                    socket.close();
                }
            }
        } finally {
            server.close();
        }
    }
}
Окей, тепер кожен відтворений сокет не загубиться, а зберігатиметься на сервері. Далі. Кожного клієнта має хтось слухати. Давайте створимо нитку із серверними функціями з попереднього розділу.
class ServerSomthing extends Thread {

    private Socket socket; // Сокет, через який сервер спілкується з клієнтом,
    // Крім нього - клієнт і сервер не пов'язані
    private BufferedReader in; // потік читання із сокету
    private BufferedWriter out; // Потік запису в сокет

    public ServerSomthing(Socket socket) throws IOException {
        this.socket = socket;
        // якщо потоку введення/виводу призведуть до генерування виключення, воно прокинеться далі
        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        start(); // Викликаємо run()
    }
    @Override
    public void run() {
        String word;
        try {

            while (true) {
                word = in.readLine();
                if(word.equals("stop")) {
                    break;                }
                for (ServerSomthing vr : Server.serverList) {
                    vr.send(word); // надіслати прийняте повідомлення з
                   // Прив'язаного клієнта всім іншим, включаючи його
                }
            }

        } catch (IOException e) {
        }
    }

    private void send(String msg) {
        try {
            out.write(msg + "\n");
            out.flush();
        } catch (IOException ignored) {}
    }
}
Отже, у конструкторі серверної нитки має бути ініціалізований сокет, через який нитка спілкуватиметься з конкретним клієнтом. Також потоки введення/виводу, і до того ж потрібно запустити нитку прямо з конструктора. Добре, але що відбуватиметься під час читання повідомлення від клієнта для серверної нитки? Відсилати назад лише своєму клієнту? Не дуже ефективно. Ми робимо розрахований на багато користувачів чат, тому нам потрібно що б кожен підключений клієнт отримав те що написав хтось один. Потрібно скористатися списком всіх серверних ниток, прив'язаних до своїх клієнтів, і надіслати кожне надіслане конкретної нитки повідомлення, щоб та надіслала його своєму клієнту:
for (ServerSomthing vr : Server.serverList) {
    vr.send(word); // Надіслати прийняте повідомлення
    // з прив'язаного клієнта решті, включаючи його
}
private void send(String msg) {
    try {
        out.write(msg + "\n");
        out.flush();
    } catch (IOException ignored) {}
}
Тепер всі клієнти дізнаються про те, що сказав один з них! Якщо ви не хочете, щоб повідомлення приходило тому, хто його відправив (він і так знає, що він написав!) просто при переборі ниток вкажіть, що б при обробці об'єктаthisцикл переходив до наступного елементу, не виконуючи з нього ніяких дій. Або ж, якщо хочете, надішліть повідомлення клієнту, в якому написано, що повідомлення успішно прийнято та надіслано. Із сервером тепер все зрозуміло. Перейдемо до клієнта, точніше до клієнтів! Там так само, за аналогією з клієнтом з попереднього розділу, тільки створюючи екземпляр потрібно як було показано в цьому розділі з сервером, створити все необхідне в конструкторі. Але якщо при створенні клієнта він ще не встиг нічого ввести, а йому вже щось відправабо? (Наприклад, історію листування тих, хто підключився до чату до нього). Так що цикли, в яких оброблятимуся надіслані повідомлення повинні бути відокремлені від тих, в яких читаються повідомлення з консолі і відправляються на сервер для пересилання іншим. На допомогу знову приходять нитки. Немає сенсу створювати клієнта як нитку.
// нитка читання повідомлень із сервера
private class ReadMsg extends Thread {
    @Override
    public void run() {

        String str;
        try {
            while (true) {
                str = in.readLine(); // Чекаємо на повідомлення з сервера
                if (str.equals("stop")) {

                    break; // виходимо з циклу, якщо прийшло "stop"
                }
                            }
        } catch (IOException e) {

        }
    }
}
// нитка відправляє повідомлення, які надходять з консолі на сервер
public class WriteMsg extends Thread {

    @Override
    public void run() {
        while (true) {
            String userWord;
            try {
               userWord = inputUser.readLine(); // Повідомлення з консолі
                if (userWord.equals("stop")) {
                    out.write("stop" + "\n");
                    break; // виходимо з циклу, якщо прийшло "stop"
                } else {
                    out.write(userWord + "\n"); // відправляємо на сервер
                }
                out.flush(); // Чистимо
            } catch (IOException e) {

            }

        }
    }
}
У конструкторі клієнта потрібно просто запустити ці нитки. А як правильно закрити ресурси клієнта, якщо той захоче вийти? Чи потрібно закривати ресурси серверної нитки? Для цього необхідно буде швидше створити окремий метод, що викликається при виході з циклу обробки повідомлень. Там потрібно буде закрити сокет та потоки введення/виводу. Той самий сигнал закінчення сесії для конкретного клієнта має бути відправлений його серверної нитки, яка має зробити теж зі своїм сокетом і видалити себе зі списку ниток в основному класі сервера. Голова Вісім: Немає межі досконалості Можна довго вигадувати нові фічі для вдосконалення свого проекту. Але що точно має бути передано новому клієнту? Я думаю, що останні десять подій, що відбулися до його приходу. Для цього необхідно створити клас, в якому до оголошеного списку заноситиметься остання дія з будь-якою серверною ниткою, і, якщо список вже повний (тобто 10 вже є), видалити перше і занести останнім, що прийшло. Для того щоб вміст цього списку отримав новий підключився, потрібно при створенні серверної нитки, в потоці виведення, відіслати їх клієнту. Як це зробити? Наприклад, так:
public void printStory(BufferedWriter writer) {
// ...
}
Серверна нитка вже створила потоки і може виводити потік виводу як аргумент. Далі просто потрібно в циклі перебору все, що необхідно передати новому клієнту. Це лише основи основ, і швидше за все така архітектура чату не підійде при створенні реального додатка. Ця програма створена у навчальних цілях і на її основі я показав, як можна змусити спілкуватися клієнта з сервером (і навпаки), як це зробити для кількох підключень, і, звичайно, як це організовано на сокетах. Нижче переставлені джерела, а також прикладено вихідний код програми, що розбирається. Це мій перший досвід написання статті) Дякуємо за увагу:)
  1. Thinking in Java Enterprise, Bruce Eckel et. Al. 2003
  2. Java 8, Повне керівництво, Герберт Шилдт, 9 видання, 2017 (Глава 22)
  3. Програмування сокетів на Java стаття про сокети
  4. Socket в офіційній документації
  5. ServerSocket в офіційній документації
  6. вихідники на GitHub
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.