Вступ: «На столі був комп, за ним був кодер…»
Якось один мій однокурсник викладав черговий результат свого вивчення Java, у вигляді скриншота нової програми. Цією програмою був багатокористувацький чат. Я тоді тільки починав свій власний шлях у освоєнні програмування на цій мові, але точно відзначив для себе – «хочу!». Минув час, і, закінчивши роботу з черговим проєктом у рамках поглиблення своїх знань програмування, я згадав про той випадок і вирішив – пора.
Якось я вже починав чисто з цікавості копати цю тему, але у моєму основному підручнику з Java (це було повне керівництво Шилдта) пакету java.net було приділено всього лише 20 сторінок. Це зрозуміло – книга і так дуже велика. Там були наведені таблиці методів і конструкторів основних класів, але й усе.
Наступний крок – звісно всемогутній ґуґл: міріади статей, де описано одне й те саме — два-три слова про сокети і готовий приклад.
Класичний підхід (як мінімум у моєму стилі навчання) – це спочатку зрозуміти, що мені потрібно з інструментів для роботи, що вони собою являють, для чого потрібні, а тільки потім, якщо рішення задачі неочевидне, копати готові листинги, розбираючи їх на гвинтики й болтики.
Але я розібрався що до чого, і в результаті написав багатокористувацький чат.
Зовні вийшло якось так:
Тут я постараюся дати вам розуміння основ клієнт-серверних додатків на основі сокетів Java на прикладі проєктування чату.
На курсі javaRush ви будете робити чат. Він буде зовсім іншого рівня, красивий, великий, багатофункціональний. Але завжди в першу чергу потрібно закласти фундамент, тому тут нам із вами треба розібратися, що ж лежить в основі подібного розділу.
(Якщо ви знайшли якісь недоліки чи помилки, напишіть у особисті або в коментарях під статтею).
Почнемо.
Розділ Один: «Будинок, який…»
Щоб пояснити, як же відбувається мережеве з’єднання між сервером та одним клієнтом, візьмемо вже класичний приклад із багатоквартирним будинком.
Припустимо, клієнту потрібно якимось чином встановити зв’язок із певним сервером. Що потрібно знати тому, хто шукає об'єкт пошуку? Так, адресу. Сервер – це не магічна сутність у хмарі, і тому він повинен розташовуватися на певній машині. За аналогією з будинком, де має відбутися зустріч двох узгоджених сторін. І щоб знайти одне одного у багатоквартирному будинку, однієї адреси будівлі недостатньо, необхідно вказати номер квартири, в якій відбудеться зустріч. Так і на одній обчислювальній машині може бути одразу кілька серверів, і клієнту, щоб зв’язатися з конкретним, потрібно вказати ще й номер порту, за яким відбудеться з’єднання.
Отже, адреса та номер порту.
Адреса означає ідентифікатор машини у просторі мережі Internet. Вона може бути доменним ім’ям, наприклад, «javarush.ru», або звичайним IP.
Порт — унікальний номер, з яким пов’язаний певний сокет (цей термін буде розглянуто далі), простіше кажучи, його займає певна служба, щоб по ньому могли зв’язатися з нею.
Тож для того, щоб відбулася зустріч принаймні двох об’єктів на території одного (сервера) – хазяїн місцевості (сервер) повинен зайняти конкретну квартиру (порт) на ній (машині), а другий має знайти місце зустрічі, знаючи адресу будинку (домен чи ip) та номер квартири (порт).
Розділ Два: Знайомтесь, Socket
Серед понять і термінів, пов’язаних із роботою в мережі, є одне дуже важливе – Сокет. Воно позначає точку, через яку відбувається з’єднання. Простіше кажучи, сокет з'єднує у мережі дві програми.
Клас
Оголошується цей клас на стороні клієнта, а сервер створює його, отримуючи сигнал на підключення. Так відбувається спілкування у мережі. Для початку ось можливі конструктори класу
Якось один мій однокурсник викладав черговий результат свого вивчення Java, у вигляді скриншота нової програми. Цією програмою був багатокористувацький чат. Я тоді тільки починав свій власний шлях у освоєнні програмування на цій мові, але точно відзначив для себе – «хочу!». Минув час, і, закінчивши роботу з черговим проєктом у рамках поглиблення своїх знань програмування, я згадав про той випадок і вирішив – пора.
Якось я вже починав чисто з цікавості копати цю тему, але у моєму основному підручнику з Java (це було повне керівництво Шилдта) пакету java.net було приділено всього лише 20 сторінок. Це зрозуміло – книга і так дуже велика. Там були наведені таблиці методів і конструкторів основних класів, але й усе.
Наступний крок – звісно всемогутній ґуґл: міріади статей, де описано одне й те саме — два-три слова про сокети і готовий приклад.
Класичний підхід (як мінімум у моєму стилі навчання) – це спочатку зрозуміти, що мені потрібно з інструментів для роботи, що вони собою являють, для чого потрібні, а тільки потім, якщо рішення задачі неочевидне, копати готові листинги, розбираючи їх на гвинтики й болтики.
Але я розібрався що до чого, і в результаті написав багатокористувацький чат.
Зовні вийшло якось так:
Тут я постараюся дати вам розуміння основ клієнт-серверних додатків на основі сокетів Java на прикладі проєктування чату.
На курсі javaRush ви будете робити чат. Він буде зовсім іншого рівня, красивий, великий, багатофункціональний. Але завжди в першу чергу потрібно закласти фундамент, тому тут нам із вами треба розібратися, що ж лежить в основі подібного розділу.
(Якщо ви знайшли якісь недоліки чи помилки, напишіть у особисті або в коментарях під статтею).
Почнемо.
Розділ Один: «Будинок, який…»
Щоб пояснити, як же відбувається мережеве з’єднання між сервером та одним клієнтом, візьмемо вже класичний приклад із багатоквартирним будинком.
Припустимо, клієнту потрібно якимось чином встановити зв’язок із певним сервером. Що потрібно знати тому, хто шукає об'єкт пошуку? Так, адресу. Сервер – це не магічна сутність у хмарі, і тому він повинен розташовуватися на певній машині. За аналогією з будинком, де має відбутися зустріч двох узгоджених сторін. І щоб знайти одне одного у багатоквартирному будинку, однієї адреси будівлі недостатньо, необхідно вказати номер квартири, в якій відбудеться зустріч. Так і на одній обчислювальній машині може бути одразу кілька серверів, і клієнту, щоб зв’язатися з конкретним, потрібно вказати ще й номер порту, за яким відбудеться з’єднання.
Отже, адреса та номер порту.
Адреса означає ідентифікатор машини у просторі мережі Internet. Вона може бути доменним ім’ям, наприклад, «javarush.ru», або звичайним IP.
Порт — унікальний номер, з яким пов’язаний певний сокет (цей термін буде розглянуто далі), простіше кажучи, його займає певна служба, щоб по ньому могли зв’язатися з нею.
Тож для того, щоб відбулася зустріч принаймні двох об’єктів на території одного (сервера) – хазяїн місцевості (сервер) повинен зайняти конкретну квартиру (порт) на ній (машині), а другий має знайти місце зустрічі, знаючи адресу будинку (домен чи ip) та номер квартири (порт).
Розділ Два: Знайомтесь, Socket
Серед понять і термінів, пов’язаних із роботою в мережі, є одне дуже важливе – Сокет. Воно позначає точку, через яку відбувається з’єднання. Простіше кажучи, сокет з'єднує у мережі дві програми.
Клас Socket реалізує ідею сокета. Через його канали вводу/виводу будуть спілкуватися клієнт із сервером:
Оголошується цей клас на стороні клієнта, а сервер створює його, отримуючи сигнал на підключення. Так відбувається спілкування у мережі. Для початку ось можливі конструктори класу 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() – повертає порт, до якого прив'язаний сокет. Річ у тому, що «спілкуватися» клієнт і сервер можуть через один порт, а порти, до яких вони прив'язані – можуть бути зовсім іншими
boolean isConnected() – повертає true, якщо з'єднання встановлено
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());
Голова П'ять: Реальний зв'язок по мережі Інтернет
Оскільки для того, щоб з'єднатися по реальній мережі з реальним ip адресом потрібно мати повноцінний сервер, а оскільки:
- Наш майбутній чат, як утиліта, такими можливостями не володіє. Він може лише встановити з'єднання та прийняти/відправити повідомлення. Тобто він не володіє реальними можливостями сервера.
- Наш сервер, що містить лише дані сокета та потоків введення/виведення, не може працювати як реальний WEB- або FTP-сервер, то маючи лише це ми не зможемо з'єднатися по мережі Інтернет.
Socket, що адреса локальна, існує 2 способи:
- Написати як аргумент адреси «localhost», що означає локальну заглушку. Також для цього підходить «127.0.0.1» - це всього лише цифрова форма заглушки.
- За допомогою InetAddress:
InetAddress.getByName(null)- null вказує на локальний хостInetAddress.getByName("localhost")InetAddress.getByName("127.0.0.1")
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, то значить її можна запускати окремо від сервера і паралельно з іншими клієнтами. Що нам це дає? Якось потрібно, щоб при кожному новому під'єднанні сервер не переходив одразу до спілкування, а записував це з'єднання в якийсь список і переходив до очікування нового під'єднання, а спілкуванням з конкретним клієнтом займався б якийсь допоміжний сервіс. Також клієнти повинні писати на сервер і чекати відповіді незалежно одне від одного. На допомогу приходять потоки. Припустимо у нас є клас, що відповідає за запам'ятовування нових під'єднань: У нього мають бути вказані:
- Номер порта.
- Список, у який він записує нове з'єднання.
- І
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 цикл переходив до наступного елемента, не виконуючи над ним ніяких дій. Або ж, якщо хочете, відправте повідомлення клієнту, в якому написано, що повідомлення успішно отримано та розіслано.
З сервером тепер все зрозуміло. Перейдемо до клієнта, а точніше до клієнтів! Там все так само, за аналогією з клієнтом з минулої глави, тільки створюючи екземпляр, потрібно як було показано у цій главі з сервером, створити все необхідне у конструкторі.
Але що, якщо при створенні клієнта він ще не встиг нічого ввести, а йому вже щось відправили? (Наприклад, історію переписки тих, хто вже підключився до чату до нього). Так що цикли, у яких будуть оброблятися надіслані повідомлення, мають бути відокремлені від тих, у яких читаються повідомлення з консолі та відправляються на сервер для пересилки іншим. На допомогу знову приходять потоки. Немає сенсу створювати клієнта як потік. Зручніше зробити потік з циклом у методі run, який читає повідомлення, а також за аналогією - пише:
// потік читання повідомлень з сервера
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) {
// ...
}
Серверний потік вже створив потоки і може передати потік виводу як аргумент. Далі просто потрібно в циклі перебору передати все, що необхідно новому клієнту.
Висновок:
Це лише основи основ, і, швидше за все, така архітектура чату не підійде для створення реального додатку. Ця програма створена в навчальних цілях, і на її основі я показав, як можна змусити клієнта спілкуватися з сервером (і навпаки), як це зробити для кількох підключень, і, звичайно ж, як це організовано на сокетах.
Нижче вказані джерела, а також додано вихідний код розглянутої програми.
Це мій перший досвід написання статті :)
Дякую за увагу :)
- Thinking in Java Enterprise, by Bruce Eckel et. Al. 2003
- Java 8, Повне керівництво, Герберт Шилдт, 9 видання, 2017 (Глава 22)
- Програмування сокетів на Java стаття про сокети
- Socket в офіційній документації
- ServerSocket в офіційній документації
- вихідники на GitHub
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ