Программирование сервера на java

Давайте займемся программированием на уровне сокетов на Java.

СОДЕРЖАНИЕ
Цели Блока • Обзор • Тривиальный Последовательный Сервер • Простой Потоковый Сервер • Сетевая Игра В Крестики-Нолики • Многопользовательское Приложение Для Чата • Сводка

Цели подразделения

Получить навыки написания клиент-серверных приложений на Java на уровне сокетов.

Общие сведения

Мы рассмотрим четыре сетевых приложения. Написанных полностью с нуля на Java. Каждое из этих приложений использует парадигму клиент-сервер, о которой мы говорили ранее. Мы будем использовать TCP исключительно здесь. Напомним, что порты с 49152 по 65535 можно использовать для чего угодно. Поэтому мы будем использовать их.

Абстракция Java над API сокета должна использовать объект ServerSocket. Который автоматически слушает. А затем создает другой сокет на accept. Сокеты Java имеют встроенные входные и выходные потоки. Что делает программирование довольно приятным.

Четыре приложения представлены в порядке возрастания сложности:

Эти приложения взаимодействуют ненадежно.

Ни одно из этих приложений даже не пытается защитить связь. Все данные передаются между хостами полностью в открытом виде. На данном этапе цель состоит в том. Чтобы проиллюстрировать самые основные приложения и то. Как они используют услуги транспортного уровня. В реальной жизни используйте слой защищенных сокетов.

Тривиальный Последовательный Сервер

Это, пожалуй. Самый простой из возможных серверов. Он прослушивает порт 59090. Когда клиент подключается. Сервер отправляет клиенту текущую дату и время. Сокет соединения создается в блоке try-with-resources. Поэтому он автоматически закрывается в конце блока. Только после обслуживания datetime и закрытия соединения сервер вернется к ожиданию следующего клиента.

DateServer.java

импорт java.io.IOException; импорт java.io.PrintWriter; импорт java.net.ServerSocket; импорт java.net.Socket; импорт файла java.util.Дата; /** * Простой TCP-сервер. Когда клиент подключается. Он отправляет клиенту текущий 

* datetime, затем закрывает соединение. Это, пожалуй. Самый простой сервер * вы можете писать. Остерегайтесь, хотя. Что клиент должен быть полностью обслужен его * дата. Когда сервер сможет обрабатывать другой клиент. */ public class DateServer { public static void main(String[] args) бросает IOException { try (var listener = new ServerSocket(59090)) { System.out.println(в то время как (правда) { попробуйте (var socket = listener.accept()) {

var out = new PrintWriter(socket.getOutputStream(), true);

Обсуждение:

  • Этот код просто для иллюстрации; вы вряд ли когда-нибудь напишете что-то настолько простое.
  • Это не очень хорошо справляется с несколькими клиентами; каждый клиент должен ждать. Пока предыдущий клиент не будет полностью обслужен. Прежде чем он даже будет принят.
  • Как и практически во всех сокетных программах, серверный сокет просто слушает, а другой. “простой” сокет взаимодействует с клиентом.
  • ServerSocket.accept()Вызов-это БЛОКИРУЮЩИЙ ВЫЗОВ.
  • Связь сокетов всегда осуществляется с байтами; поэтому сокеты поставляются с входными и выходными потоками.

    Но , обернув выходной поток сокета с помощью aPrintWriter, мы можем указать строки для записи. Которые Java автоматически преобразует (декодирует) в байты.

  • Связь через сокеты всегда буферизуется. Это означает. Что ничего не отправляется и не принимается до тех пор. Пока буферы не заполнятся или пока вы явно не очистите буфер. Второй аргумент к PrintWriter, в данном случае trueговорит Java. Чтобы Она автоматически сбрасывалась после каждого println.
  • Мы определили все сокеты в блоке try-with-resources так. Чтобы они автоматически закрывались в конце своего блока. Явный closeвызов не требуется.
  • После отправки datetime клиенту try-блок заканчивается и коммуникационный сокет закрывается. Поэтому в этом случае закрытие соединения инициируется сервером.

Запустите сервер:

$ javac DateServer.java && java DateServer Сервер дат запущен... 

Чтобы увидеть. Что он работает (вам понадобится другое окно терминала):

$ netstat -an | grep 59090 tcp46 0 0 *.59090 *.* СЛУШАЙТЕ 

Протестируйте сервер с помощьюnc:

$ nc localhost 59090 Сб 16 февраля 18:03:34 PST 2019 

Огоnc, это потрясающе! Тем не менее. Давайте посмотрим. Как написать наш собственный клиент на Java:

DateClient.java

import java.util.Scanner;
import java.net.Socket;
import java.io.IOException;/**
* Клиент командной строки для сервера дат. В качестве единственного аргумента требуется указать IP-адрес сервера
*. Завершает работу после печати ответа.
*/

общественного класса DateClient {
   
публичный статический пустота главный(строка[] аргументы) бросает исключение IOException {
       
если (массив args.длина != 1) {
           
Система.ошибаться.метод println();
           
вернуть;
       
}
       
ВАР сокет = новый сокет(аргументы[0], 59090);
       
ВАР в = новый сканер(сокета.getInputStream());
       
система.из.метод println( + в.строки());
   
}
}

Обсуждение:

  • На стороне клиента Socketконструктор принимает IP-адрес и порт на сервере. Если запрос на подключение принят. Мы получаем объект сокета для связи.
  • Наше приложение настолько простое. Что клиент никогда не пишет на сервер. Он только читает. Поскольку мы общаемся с текстом. Самое простое. Что можно сделать. Это обернуть входной поток сокета в a Scanner. Они мощные и удобные. В нашем случае мы читаем строку текста с сервера Scanner.nextLineС.

Тестирование клиента:

$ javac DateClient.java && java DateClient 127.0.0.1 Ответ сервера: Сб 16 февраля 18:02:35 PST 2019 

Простой Потоковый Сервер

Предыдущий пример был довольно тривиальным: он не считывал никаких данных с клиента и. Что еще хуже, одновременно обслуживал только одного клиента.

Этот следующий сервер получает строки текста от клиента и отправляет обратно строки в верхнем регистре. Он эффективно обрабатывает несколько клиентов одновременно: когда клиент подключается. Сервер порождает поток, предназначенный только для этого клиента, для чтения. Прописывания и ответа. Сервер может одновременно прослушивать и обслуживать других клиентов. Поэтому мы имеем истинный параллелизм.

CapitalizeServer.java

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.Executors;/**
* Серверная программа. Которая принимает запросы от клиентов на капитализацию строк.
* Когда клиент подключается. Запускается новый поток для его обработки. Получение
* клиентских данных. Их капитализация и отправка ответа обратно-все это делается в
* потоке. Что позволяет значительно увеличить пропускную способность. Поскольку одновременно можно обрабатывать больше клиентов
.
*/

public class CapitalizeServer {

   

/**
* Запускает сервер. Когда клиент подключается. Сервер создает новый поток для выполнения
обслуживания и немедленно возвращается к прослушиванию. Приложение ограничивает
* количество потоков через пул потоков (в противном случае миллионы клиентов могут
* привести к тому. Что сервер исчерпает ресурсы. Выделив слишком много потоков).
*/

   
общественный статический пустота главный(строка[] аргументы) бросает исключение {
       
попробовать (ВАР слушатель = новый ServerSocket(59898)) {
           
системой.из.метод println();
           
ВАР бассейн = исполнителями.newFixedThreadPool(20);
           
в то время как (истинно) {
бассейна
.выполнить(новый Capitalizer(слушателя.принять()));
           
}
       
}
   
}

   

частный статический класс Capitalizer реализует интерфейс Runnable {
       
частный розетка розетка;

       

Capitalizer(гнездо гнездо) {
           
это.гнездо = розетка;
       
}

       

@переопределить
       
общественного недействительными запуска() {
           
системой.из.метод println( + гнездо);
           
попробовать {
               
ВАР в = новый сканер(сокета.getInputStream());
               
вар из = Новый printwriter ошибку(сокета.getOutputStream(), правда);
               
а (в.hasNextLine()) {
наружу
.метод println(в.строки().метод touppercase());
               
}
           
} улов (исключение е) {
               
системой.из.метод println( + гнездо);
           
} наконец {
               
попробовать {
гнезда
.закрыть();
               
} поймать (исключение IOException е) {
               
}
               
системы.из.метод println( + гнездо);
           
}
       
}
   
}
}

Обсуждение:

  • Серверный сокет. Приняв соединение. Не делает ничего, кроме запуска потока.
  • В Java вы никогда не должны создавать потоки напрямую; вместо этого используйте пул потоков и используйте службу исполнителя для управления потоками.
  • Ограничение размера пула потоков защищает нас от завала миллионами клиентов.
  • Вещи, которые выполняются в потоках. Называются задачами; они реализуют Runnableинтерфейс; они выполняют свою работу в своем runметоде.
  • Будьте осторожны. Чтобы никогда не делать слишком много работы в конструкторе задачи! Конструктор запускается в основном потоке. Поместите всю работу (кроме захвата аргументов конструктора) в runметод.
  • runМетод имеет цикл. Который продолжает читать строки из сокета. Заглавными буквами. А затем отправлять их. Обратите внимание на обертывание потоков сокетов в a Scannerи aPrintWriter, чтобы мы могли работать со строками.
  • finallyБлок закрывает розетку. Мы не могли использовать блок try-with-resources здесь. Потому что сокет был создан на основном потоке.
  • Раздражающий try-catch вокруг вызова закрытия сокета должен быть там. Потому что мы не можем добавить throws IOExceptionк сигнатуре runметода (потому что мы реализуем его из Runnableинтерфейса.

Прежде чем писать клиент. Давайте протестируем ncего . Наш сервер читает до тех пор. Пока стандартный ввод не будет исчерпан, поэтому. Когда вы закончите вводить строки. Нажмите Ctrl+D (clean exit) или Ctrl+C (abort):

$ nc 127.0.0.1 59898 yeet ЙИТ Seems t'be workin' КАЖЕТСЯ. ТЫ РАБОТАЕШЬ. Привет, мир ПРИВЕТ. МИР 

Теперь перейдем к довольно простому клиенту командной строки:

CapitalizeClient.java

импорт в Java.Ио.Класс IOException;
импорт Java на.Ио.InputStreamReader;
импорт Java на.Ио.Командой printwriter;
импорт Java на.чистая.Гнездо;
импорт Java на.утиль.Сканер;общественного класса CapitalizeClient {
   
публичный статический пустота главный(строка[] аргументы) бросает исключение {
       
если (массив args.длина != 1) {
           
Система.ошибаться.метод println();
           
вернуть;
       
}
       
попробовать (ВАР сокет = новый сокет(аргументы[0], 59898)) {
           
системой.из.метод println();
           
ВАР сканер = новый сканер(система.в);
           
ВАР в = новый сканер(сокета.getInputStream());
           
вар из = Новый printwriter ошибку(сокета.getOutputStream(), правда);
           
а (сканера.hasNextLine()) {
out
.println(scanner.nextLine());
               
System.out.println(in.nextLine());
           
}
       
}
   
}
}

Этот клиент многократно считывает строки из стандартного ввода. Отправляет их на сервер и записывает ответы сервера. Его можно использовать в интерактивном режиме:

$ javac CapitalizeClient.java && java CapitalizeClient localhost Введите строки текста. А затем Ctrl+D или Ctrl+C. Чтобы выйти hello ПРИВЕТ bye ПОКА 

Или вы можете трубить в файл!

$ python3 -c 'for a in "dog rat cat".split(): print(a)' > animals $ javac CapitalizeClient.java && java CapitalizeClient localhost Введите строки текста. Затем Ctrl+D или Ctrl+C. Чтобы выйти СОБАКА КРЫСА КОШКА 

Классные

работы Разбиваются на группы по двое. Один студент запускает сервер в одном окне терминала. А клиент-в другом. И запускает каждый. Другой студент создаст два терминальных окна. В каждом из которых будет запущен клиент. Прежде чем любой из трех клиентов отправит какие-либо данные на сервер. Запустите netstatего, чтобы убедиться. Что вы видите прослушивающий сервер и все клиентские соединения. (На Mac, netstat -an | grep tcp | grep 59898 полезно видеть только хорошие вещи.) Соотнесите выходные данные netstat с сообщениями журнала. Передаваемыми сервером и клиентом. По мере отправки данных продолжайте запускать netstat. Наблюдайте, как соединения переходят от УСТАНОВЛЕННОГО к TIME_WAIT. А затем исчезают. Записывайте все. Что происходит; мы обсудим все вместе. Когда все закончат.

Сетевая игра в Крестики-Нолики

Вот сервер для нескольких двух игроков. Он прослушивает два клиента для подключения и создает поток для каждого: первый-игрок X. А второй-игрок O. Клиент и сервер отправляют простые строковые сообщения друг другу; сообщения соответствуют протоколу Tic Tac Toe. Который я составил для этого примера.

TicTacToeServer.java

импорт в Java.Ио.Класс IOException;
импорт Java на.Ио.Командой printwriter;
импорт Java на.чистая.ServerSocket;
импорт Java на.чистая.Гнездо;
импорт Java на.утиль.Массивы;
импорт Java на.утиль.Сканер;
импорт Java на.утиль.одновременно.Исполнители;/**
* сервер для многопользовательской игры в крестики-нолики. Свободно основанный на примере из
книги * Deitel и Deitel “Java How to Program”. Для этого проекта я создал
* новый протокол уровня приложения под названием TTTP (для протокола Tic Tac Toe). Который
* полностью является простым текстом. Сообщения TTTP:
*

*


*/

общественного класса TicTacToeServer {

   

публичный статический пустота главный(строка[] аргументы) бросает исключение {
       
попробовать (ВАР слушатель = новый ServerSocket(58901)) {
           
системой.из.метод println();
           
ВАР бассейн = исполнителями.newFixedThreadPool(200);
           
в то время как (истинно) {
               
игры игры = новая игра();
бассейн
.исполнить(игра.новый плеер(слушателя.принять(), 'х'));
бассейн
.исполнить(игра.новый плеер(слушателя.принять(), 'О'));
           
}
       
}
   
}
}класс игры {

   

// клетки доски нумеруются 0-8, сверху вниз. Слева направо; если нуль пуст
   
частная плеер[] платы = новый плеер[9];

   

игрок currentPlayer;

   

общественная boolean hasWinner() {
       
return (board[0] != null && board[0] == board[1] && board[0] == board[2])
               
|| (board[3] != null && board[3] == board[4] && board[3] == board[5])
               
|| (board[6] != null && board[6] == board[7] && board[6] == board[8])
               
|| (board[0] != null && board[0] == board[3] && board[0] == board[6])
               
|| (board[1] != null && board[1] == board[4] && board[1] == board[7])
               
|| (board[2] != null && board[2] == board[5] && board[2] == board[8])
               
|| (board[0] != null && board[0] == board[4] && board[0] == board[8])
               
|| (board[2] != null && board[2] == board[4] && board[2] == board[6]);
   
}

   

public boolean boardFilledUp() {
       
return Arrays.stream(board).allMatch(p p != нуль);
   
}

   

общественного синхронизированы недействительными перемещать(инт местоположение, плеер плеер) {
       
если (игрок != currentPlayer) {
           
бросить новый IllegalStateException();
       
} еще если (игрок.противник == значение null) {
           
бросить новый IllegalStateException();
       
} еще если (Совет[месте] != значения null) {
           
бросить новый IllegalStateException();
       
}
борту
[расположение] = currentPlayer;
currentPlayer
= currentPlayer.противника;
   
}

   

/**
* игрок определяется характером знака. Который является либо 'X' или 'O'. Для
* связи с клиентом у плеера есть розетка и связанный с ней сканер
* и печатающий аппарат.
*/

   
класс игрока реализует интерфейс Runnable {
       
чар знака;
       
игрок соперника;
       
розетка розетка;
       
сканер ввода;
       
издания выходной;

       

общественные плеер(разъем гнездо, голец Марк) {
           
это.гнездо = розетка;
           
это.знак = знак;
       
}

       

@переопределить
       
общественного недействительными запуска() {
           
попробуйте {
настройка
();
processCommands
();
           
} поймать (исключение е) {
е
.печатные();
           
} наконец {
               
если (соперник != значение null && противника.выходной != значения null) {
соперника
.вывод.метод println();
               
}
               
попробуйте {
гнезда
.закрыть();
               
} поймать (исключение IOException е) {
               
}
           
}
       
}

       

частная недействительными установки() бросает исключение IOException {
вход
= новый сканер(сокета.getInputStream());
выход
= новый printwriter ошибку(сокета.getOutputStream(), правда);
выход
.метод println( + метка);
           
если (отметьте == 'х') {
currentPlayer
= это;
производства
.метод println();
           
} еще {
сопернику
= currentPlayer;
противник
.противник = это;
противник
.вывод.метод println();
           
}
       
}

       

частная пустота processCommands() {
           
в то время (ввода.hasNextLine()) {
               
ВАР командной = вход.строки();
               
если (команда.как startswith()) {
                   
возвращение;
               
} еще если (команда.как startswith()) {
processMoveCommand
(целое число.parseInt(команду.функция substring(5)));
               
}
           
}
       
}

       

частная пустота processMoveCommand(инт местонахождение) {
           
попробуйте {
переместить
(расположение, этот);
выход
.метод println();
противник
.вывод.метод println( + расположение);
               
если (hasWinner()) {
выход
.метод println();
противник
.вывод.метод println();
               
} еще если (boardFilledUp()) {
выход
.метод println();
противник
.вывод.метод println();
               
}
           
} поймать (IllegalStateException е) {
вывода
.метод println( + е.GetMessage());
           
}
       
}
   
}
}

В наши дни такие игры будут играть с клиентами в веб-браузере. А сервер будет веб-сервером (скорее всего. С помощью библиотеки WebSockets). Но сегодня мы изучаем программирование непосредственно с помощью сокетов. На пользовательских портах. С пользовательскими протоколами. Поэтому мы придерживаемся Java для наших пользовательских клиентов. Первая версия этой программы была написана примерно в 2002 году. Поэтому она используется…ждите его…Ява Качели!

TicTacToeClient.java

импорт в Java.авт.Шрифта;
импорт Java на.авт.Цвет;
импорт Java на.авт.GridLayout;
импорт Java на.авт.GridBagLayout;
импорт Java на.авт.BorderLayout;
импорт Java на.авт.события.MouseAdapter;
импорт Java на.авт.события.События mouseevent;
импорт Java на.утиль.Сканер;
импорт Java на.Ио.Командой printwriter;
импорт Java на.чистая.Гнездо;импорт пакета javax.качели.Формы JFrame;
импорт пакета javax.качели.Метка jlabel;
импорт пакета javax.качели.JOptionPane;
импорт пакета javax.качели.Для jpanel;/**
* клиент для многопользовательской игры в крестики-нолики. Свободно основанный на примере из
книги * Deitel и Deitel “Java How to Program”. Для этого проекта я создал
* новый протокол уровня приложения под названием TTTP (для протокола Tic Tac Toe). Который
* является полностью простым текстом. Сообщения TTTP являются:
*

*


*/

общественного класса TicTacToeClient {

   

отдельный элемент JFrame кадр = новый элемент JFrame();
   
частная метка jlabel messageLabel = новый метка jlabel();

   

частная квадратные[] платы = новый сквер[9];
   
индивидуальная площади currentSquare;

   

индивидуальная розетка розетка;
   
отдельный сканер в;
   
частная издания из;

   

общественные TicTacToeClient(строка адрес_сервера) бросает исключение {

оправа

= новое гнездо(адрес_сервера, 58901);
в
= новый сканер(сокета.getInputStream());
из
= Новый printwriter ошибку(сокета.getOutputStream(), правда);

messageLabel

.setBackground(цвет.светло-серый);
рамка
.полностью().добавить(messageLabel, BorderLayout.Юг);

       

ВАР boardPanel = новый компонента jpanel();
boardPanel
.setBackground(цвет.черный);
boardPanel
.то setlayout(новый GridLayout(3, 3, 2, 2));
       
для (ВАР я = 0; я борту.длина; я++) {
           
окончательный инт Дж = я;
доска
[я] = новый квадрат();
доска
[я].нажатия(новый MouseAdapter() {
               
общественного недействительными mousepressed на(события mouseevent е) {
currentSquare
= доска[Дж];
из
.метод println( + Дж);
               
}
           
});
boardPanel
.добавить(доска[я]);
       
}
кадра
.полностью().добавить(boardPanel, BorderLayout.Центр);
   
}

   

/**
* основной поток клиентов будет прослушивать сообщения от сервера. Первое
сообщение будет Затем
мы входим в цикл прослушивания любого из других сообщений и обрабатываем каждое
сообщение соответствующим образом. Сообщения
*
* игра. Если ответ нет. Цикл завершается. И серверу отправляется сообщение
*.
*/

   
общественного недействительными играть() бросает исключение {
       
попробуйте {
           
ВАР ответ = в.строки();
           
ВАР метка = ответ.используя метод charat(8);
           
ВАР opponentMark = Марк == 'Х' ? 'О' : 'Х';
рамка
.setTitle( + знак);
           
а (в.hasNextLine()) {
ответ
= в.строки();
               
если (ответ.как startswith()) {
messageLabel
.помощью setText();
currentSquare
.помощью setText(Марка);
currentSquare
.перекрасить();
               
} еще если (ответ.как startswith()) {
                   
ВАР Лок = целое число.parseInt(ответ.функция substring(15));
доска
[ЛК].помощью setText(opponentMark);
доска
[ЛК].перекрасить();
messageLabel
.помощью setText();
               
} еще если (ответ.как startswith()) {
messageLabel
.помощью setText(ответ.функция substring(8));
               
} еще если (ответ.как startswith()) {
                   
JOptionPane.showMessageDialog(рамки, );
                   
перерыв;
               
} еще если (ответ.как startswith()) {
                   
JOptionPane.showMessageDialog(кадров, );
                   
перерыв;
               
} еще если (ответ.как startswith()) {
                   
JOptionPane.showMessageDialog(рамка, );
                   
перерыв;
               
} еще если (ответ.как startswith()) {
                   
JOptionPane.showMessageDialog(рамки, );
                   
перерыв;
               
}
           
}
наружу
.метод println();
       
} поймать (исключение е) {
е
.печатные();
       
} наконец {
гнезда
.закрыть();
рамка
.метод Dispose();
       
}
   
}

   

статический класс квадрат расширяет jpanel у {
       
метка jlabel метка = новый метка jlabel();

       

общественные площади() {
setBackground
(цвет.белый);
то setlayout
(новый GridBagLayout());
этикетка
.помощью setfont(новый шрифт(, шрифт.Смелый, 40));
добавить
(этикетки);
       
}

       

общественного недействительными помощью setText(чар текст) {
этикетке
.setForeground(текст == 'х' ? Цвет.Синий : цвет.Красный);
этикетка
.помощью setText(текст + );
       
}
   
}

   

публичный статический пустота главный(строка[] аргументы) бросает исключение {
       
если (массив args.длина != 1) {
           
Система.ошибаться.метод println();
           
вернуть;
       
}
       
TicTacToeClient клиент = новый TicTacToeClient(аргументы[0]);
клиент
.рамка.setDefaultCloseOperation(форму JFrame.EXIT_ON_CLOSE);
клиент
.рамка.метод setsize(320, 320);
клиент
.рамка.функцию setvisible(истина);
клиент
.рамка.setResizable(ложь);
клиент
.играть();
   
}
}

Упражнение: Играйте в эту игру nc. Насколько же это потрясающе? Вы чувствуете себя старой школой?

nctictactoe.png

Многопользовательское Приложение Для Чата

Вот чат-сервер. Сервер должен транслировать недавно поступившие сообщения всем клиентам. Участвующим в чате. Это делается путем того. Что сервер собирает все клиентские сокеты в словарь. А затем отправляет новые сообщения каждому из них.

ChatServer.java

импорт в Java.Ио.Класс IOException;
импорт Java на.Ио.Командой printwriter;
импорт Java на.чистая.ServerSocket;
импорт Java на.чистая.Гнездо;
импорт Java на.утиль.Комплект;
импорт Java на.утиль.Для поиска HashSet;
импорт Java на.утиль.Сканер;
импорт Java на.утиль.одновременно.Исполнители;/**
* многопоточный чат сервера. Когда клиент подключается к серверу. Он запрашивает
* экранное имя. Посылая клиенту текст
* запрашивать имя до тех пор. Пока не будет получено уникальное. После того как клиент отправит
уникальное имя*. Сервер подтвердит его с помощью Затем все сообщения
* от этого клиента будет транслироваться всем другим клиентам. Которые представили
* уникальное имя экрана. Широковещательные сообщения имеют префикс
*
* Это просто обучающий пример. Поэтому он может быть улучшен во многих отношениях, например,
* лучшее ведение журнала. Другой-принимать много забавных команд. Таких как Slack.
*/

public class ChatServer {

   

// Все имена клиентов. Поэтому мы можем проверить наличие дубликатов при регистрации.
   
private static SetString>> names = new HashSet

   

// Набор всех писателей печати для всех клиентов. Используемых для broadcast.
   
private static SetPrintWriter>> writers = new HashSet

   

public static void main(Строка[] аргументы) бросает исключение {
       
системой.из.метод println();
       
ВАР бассейн = исполнителями.newFixedThreadPool(500);
       
попробовать (ВАР слушатель = новый ServerSocket(59001)) {
           
в то время как (истинно) {
бассейна
.выполнить(новый обработчик(слушателя.принять()));
           
}
       
}
   
}

   

/**
* клиент обработчик задач.
*/

   
частный статический класс обработчик реализует интерфейс Runnable {
       
частная строка имя;
       
индивидуальная розетка розетка;
       
отдельный сканер в;
       
частные издания вон;

       

/**
* создает обработчик потока. Припрятал в гнезде. Вся интересная
* работа выполняется в методе run. Помните, что конструктор вызывается из
* основной метод сервера. Поэтому он должен быть как можно короче.
*/

       
общественного обработчик(гнездо гнездо) {
           
это.гнездо = розетка;
       
}

       

/**
* услуги этого потока клиент повторно запрашивает имя до
* уникальный внесен. То признает. Имя и регистрирует
* выходной поток для клиента в глобальную сеть. Затем неоднократно получает входы и
* транслирует их.
*/

       
общественного недействительными запуска() {
           
попробуйте {
в
= новый сканер(сокета.getInputStream());
из
= Новый printwriter ошибку(сокета.getOutputStream(), правда);

               

/
               
While (true) {
out
.println(); name
= in.nextLine();
                   
if (name == null) {
                       
return;
                   
}
                   
synchronized (names) {
                       
if (!name.isBlank() && !names.contains(name)) {
names
.add(name);
                           
break;
                       
}
                   
}
               
}

               

// Теперь, когда выбрано удачное имя. Добавьте print writer сокета
               
// в набор всех writers. Чтобы этот клиент мог получать широковещательные сообщения.
               
// Но ПЕРЕД ЭТИМ пусть все остальные знают. Что новый человек присоединился!
out
.println( + name);
               
for (PrintWriter writer : writers) {
писателя
.метод println( + наименование + );
               
}
писателей
.добавить(вне);

               

// прием сообщений от клиентов и передают их.
               
в то время как (истинно) {
                   
строка ввода = в.строки();
                   
если (вход.столоверчения().как startswith()) {
                       
вернуть;
                   
}
                   
для (издания писателя : писатели) {
писателя
.метод println( + имя + + входной);
                   
}
               
}
           
} улов (исключение е) {
               
системой.из.метод println(е);
           
} наконец {
               
если (из != значения null) {
писателей
.удалить(вне);
               
}
               
если (имя != значения null) {
                   
системой.из.метод println(имя + );
имена
.удалить(имя);
                   
для (издания писателя : писатели) {
writer
.println("MESSAGE" + name + "has left");
                   
}
               
}
               
try {
socket
.close();
               
} catch (IOException e) {
               
}
           
}
       
}
   
}
}

Вот старый клиент. Собранный вместе в 2002 году с помощью Swing.

ChatClient.java

импорт в Java.авт.события.Типа actionevent;
импорт Java на.авт.события.Действие actionlistener;
импорт Java на.Ио.Класс IOException;
импорт Java на.Ио.Командой printwriter;
импорт Java на.чистая.Гнездо;
импорт Java на.утиль.Сканер;импорт Java на.авт.BorderLayout;
импорт пакета javax.качели.Формы JFrame;
импорт пакета javax.качели.JOptionPane;
импорт пакета javax.качели.Jscrollpane не;
импорт пакета javax.качели.JTextArea;
импорт пакета javax.качели.Компоненту jtextfield;/**
* простой качания-клиент для чат-сервера. Графически это фрейм
* с текстовым полем для ввода сообщений и текстовой областью для просмотра всего
диалога*.
*
* Клиент следует следующему протоколу чата. Когда сервер отправляет
* Сервер будет
* продолжать посылать запросы
*, которые уже используются. Когда сервер отправляет строку. Начинающуюся с
*
* произвольные строки. Которая будет транслироваться на все стрекочет. Подключенных к серверу.
* Когда сервер отправляет строку. Начинающуюся с
после этой строки должны отображаться в его сообщений.
*/

public class ChatClient {

   

String serverAddress;
   
Scanner in;
   
PrintWriter out;
   
JFrame рамка = новый элемент JFrame();
   
компоненту jtextfield textfield в = новый компоненту jtextfield(50);
   
JTextArea messageArea = новый JTextArea(16, 50);

   

/**
* создает клиента путем создания GUI и зарегистрировать прослушиватель с
* поле так. Чтобы при нажатии кнопки Возврат в слушателя отправляет текстовое поле
* содержимое на сервер. Однако обратите внимание. Что текстовое поле изначально НЕ
* редактируется и становится редактируемым только ПОСЛЕ того. Как клиент получает
сообщение * NAMEACCEPTED от сервера.
*/

   
public ChatClient(String serverAddress) {
       
this.serverAddress = serverAddress;

TextField

.setEditable(false);
messageArea
.setEditable(ложь);
рамка
.полностью().добавить(объект textfield, BorderLayout.Юг);
рамка
.полностью().добавить(новый jscrollpane не(messageArea), BorderLayout.Центр);
рамка
.пакет();

       

// отправить на Enter. Затем очистить. Чтобы подготовиться к следующему сообщению
текстовое поле
.addActionListener(новый действие actionlistener() {
           
общественного недействительными событий actionperformed(типа actionevent е) {
наружу
.метод println(текстовое поле.то gettext());
поле
.помощью setText();
           
}
       
});
   
}

   

частная строка в getname() {
       
возвращение JOptionPane.showInputDialog(рамки, , ,
               
JOptionPane.PLAIN_MESSAGE);
   
}

   

частная недействительными запуска() бросает исключение IOException {
       
попробуйте {
           
ВАР сокет = новый сокет(адрес_сервера, 59001);
в
= новый сканер(сокета.getInputStream());
из
= Новый printwriter ошибку(сокета.getOutputStream(), правда);

           

а (в.hasNextLine()) {
               
ВАР строка = в.строки();
               
если (строки.как startswith()) {
наружу
.метод println(метод getname());
               
} еще если (строки.как startswith()) {
                   
это.рамка.setTitle( + строка.функция substring(13));
поле
.setEditable(истина);
               
} еще если (строки.как startswith()) {
messageArea
.добавить(строка.функция substring(8) + );
               
}
           
}
       
} наконец {
рамы
.функцию setvisible(ложь);
рамка
.метод Dispose();
       
}
   
}

   

публичный статический пустота главный(строка[] аргументы) бросает исключение {
       
если (массив args.длина != 1) {
           
Система.ошибаться.метод println();
           
вернуть;
       
}
       
ВАР клиент = новый ChatClient(аргументы[0]);
клиент
.рамка.setDefaultCloseOperation(форму JFrame.EXIT_ON_CLOSE);
клиент
.рамка.функцию setvisible(истина);
клиент
.выполнить();
   
}
}

болтовня.png

Упражнение: Улучшите протокол таким образом. Чтобы он отличал системные сообщения (такие как люди. Входящие и выходящие) от сообщений чата. А затем улучшите клиент. Чтобы он использовал цвет и шрифты для выделения различных видов сообщений.

Краткие сведения

Мы покрыли:

  • Обзор клиент-серверной модели
  • Односторонняя связь между клиентом и сервером
  • Двусторонняя связь между клиентом и сервером
  • Использование потоков на сервере
  • Отслеживание состояния в сетевой многопользовательской игре
  • Как сервер может транслировать сообщения нескольким клиентам.
  • Потрясающая утилита nc
  • Некоторые…гм…Java Swing