Java — Локальный чат для городской сети.


Содержание

LAN Messenger — Чат в локальной сети без сервера

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

Возможности:

  • подключение и общение с пользователями в локальной сети;
  • мгновенный обмен сообщениями;
  • безопасный обмен сообщениями для конфиденциальности;
  • передача файлов;
  • все сообщения защищены шифрованием AES с RSA в качестве ключевого механизма обмена информацией;
  • широковещательные сообщения;
  • многоязычный пользовательский интерфейс (включая русский язык);
  • отправка уведомлений всем или определенным пользователям;
  • организация контактов;
  • прошлые переговоры
  • журналирование сообщения;
  • подключение к сети Интернет не требуется;
  • не трубуется установка сервера в сети для работы LAN Messenger;
  • поддерживаются ОС Windows, Mac и Linux.

Лицензия: GNU GPL v3

Внимание, резервная копия обновляется очень редко , так как нужна на случай удаления дистрибутива с официального сайта.

Java — Локальный чат для городской сети.

Socket-чат на Java

Это демонстрационная проект, который ставит своей целью закрепление знаний Java по созданию клиент-серверного приложения с GUI.

В проекте присутсвуют обе части чата:

Серверная часть. Поднимается только на localhost на порту 8189. Не имеет никакого GUI. Её просто нужно запустить первой (перед подключением клиентов).

Внутри есть класс для проверки авторизации BaseAuthService, в который захардкожены несколько логинов для подключения клиентов. В рамках данного проекта работа с базой данных не предусматривалось, а потом было решено сделать так. Пры логин-пароль для авторизации: login1 и pass1, login2 и pass2, login3 и pass3.

Клиентская часть. Используется для подключения к серверу на localhost на порту 8189. GUI написан на JavaFX.

Первое, что отображается при запуске клиента – это окно авторизации (первая вьюшка), где нужно вбить одну из пар логин-пароль упомянутую выше. После этого становится доступным основной интерфейс чата (вторая вьюшка) со следующими фичами:

  • Можно авторизоваться под существующим пользователем или зарегестрироваться с нуля, при этом автоматически произойдёт и авторизация.
  • Можно отправлять сообщение одновременно всем.
  • Можно отправлять личный сообщения вида /w , например, /w nick2 Привет!.
  • Можно выйти из чата написав /end или нажав кнопку Выйти. При этом попадаешь обратно в окно авторизации.
  • Можно удалить свой аккаунт, если нажать кнопку Удалиться или вписать команду /delete. При этом попадаешь обратно в окно авторизации.
  • Есть поле со списком участников чата находящихся online.

Чат далеко не идиален и его можно долго ещё допиливать:

  1. Отрефакторить код улучшив его структуру и организацию файлов.
  2. Добавить специальный класс для работы с сообщениями, чтобы не кидать в сокет просто строки, парсинг которые происходит по факту разбора.
  3. Улучшить внешний вид, сделав все иконки и закосив под нормальный клиент, чтобы хоть немного избавиться от jav’овости приложения.
  4. И т.д.

Но так или иначе, как бы мне не было стыдно за этот «проект», свою задачу приложение выполнело: с JavaFX работать научился и простое клиент-серверное приложение писать научился.

Приложение для сетевого чата

Применяя здесь множество беспрецедентных концепций, это простой чат-сервер, способный обрабатывать несколько клиентов, которые запускаются через JavaFX, и создавать свои отдельные потоки и обрабатывать их в потоке приложения (было более сложным, чем ожидалось).

Сервер чата :

Клиент чата :

Я хотел бы сосредоточиться на:

  1. Общая критика по обработке потоков.
  2. Способ обработки клиентского и клиентского ввода.
  3. Я также задаюсь вопросом об использовании flush для потоков. Кажется, это не имеет значения, но повысит ли производительность, особенно когда это произойдет через сеть? Пока что я тестировал только локально.
  4. Протокол сервера и сервера.
  5. Общая производительность и эффективность.
  6. Общая расширяемость моих вариантов дизайна.

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

Для любого заинтересованного этого является репозиторий Github.

3 ответа

Сервер

Вместо extends Thread для ClientHandler , возможно, вы также можете рассмотреть implements Runnable ). Конечно, вам придется замените new ClientHandler(socket).start() на new Thread(new ClientHandler(socket)).start() тоже.

Я не думаю, что вы выиграете большую оптимизацию , проверив, есть ли у вас какие-то писатели в первую очередь . Это приятная вещь, но я скорее сделаю ранний return вместо блоков вложенности кода. Поток может сделать следующее:

Другим преимуществом здесь является то, что ваш код isEmpty() также становится интернализованным и, следовательно, избыточным для вас, чтобы сделать это явно.

Client

Не так много комментариев, за исключением того, что, возможно, вы можете рассмотреть возможность замены сравнения String в методе call() с помощью enum -driven, например (пожалуйста, подумайте, как можно легко получить доступ к таким переменным, как out и messageArea ), Я не учитывал это в ):

Обработка выполняется полностью внутри метода handle(String) , который равномерно проходит через представление toString() каждого enum ) , в то же время заботясь о предварительном форматировании фактического ввода, чтобы «потреблять», выполнив input.substring(v.toString().length()) . Только одно замечание для обработки «INFO» : похоже, что вы жестко кодируете число клиентов, чтобы быть однозначными, поэтому я позволил вам проиллюстрировать, как вы может обойти это с помощью стратегически размещенного разделителя, например «,» .

У вас может быть больше классов

Запуск start(Stage stage) делает слишком много для того, что мне нравится. Вы создаете Анонимный класс, который охватывает логику чтения сокета и представления клиенту. Я бы создал отдельный класс, который бы извлекал эту логику. Это должно было бы передать сокет и текстовое поле, но я уверен, что это элегантно, либо с помощью конструктора, либо с классом, который будет инкапсулировать эту часть логики.

Enum — ваш друг

В настоящий момент вы используете непосредственно код String вашей команды в своем коде. Это прекрасно, потому что у вас есть только два класса, но вероятность ошибок есть. Я бы создал Enum , который будет выглядеть так:

Теперь у вас есть только один источник, и вы не можете ошибаться в команде (ну, вы все равно можете, но по крайней мере у вас будет опечатка с обеих сторон).

Runnable

Как и @rolfl, в своем комментариях , хорошая причина не наследовать Thread заключается в том, что вы не изменяете /расширяете функциональность класса Thread . Вы выполняете код, который должен выполняться в Thread , который отвечает за интерфейс Runnable . (Оба его комментария находятся на месте, поэтому я рекомендую вам прочитать их оба.)

Я до сих пор мало что знаю о потоковой передаче, но в какой-то момент вы захотите использовать ThreadPool или некоторые другие классы потоков, которые будут управлять потоком (это сложная часть потоковой передачи), и вы ‘только нужно передать те Runnable .

Не выполнять утечку

Вы используете HashSet<> непосредственно вместо использования Set<> . Это то же самое, что и с List и почти любым интерфейсом. Если вам действительно не нужна эта конкретная реализация по определенной причине, используйте интерфейс. Тогда ваш код будет свободен от блокировки с конкретной реализацией.

Сервер

Я думаю, что вы используете ключевое слово static . Не ставьте каждую статическую функцию, создавайте объект класса и вместо этого используйте объект.

У вас есть некоторые catch части, которые либо пусты, либо печатают очень мало информации. Я предлагаю вам немного узнать о различных причинах, по которым могут произойти эти исключения, и добавить некоторые комментарии о том, какие случаи вы хотите игнорировать.

Используйте композицию над наследованием, не распространяйте Thread. Вместо этого выполните Runnable , как это было предложено другими.

Это один большой метод start в ClientHandler . Вы должны разрезать его на отдельные части, например: requestName , connected , messageLoop .

Если вы используете ConcurrentHashSet для names , вам не нужно использовать synchronized (names) .

В целом кажется, что вы слишком мало синхронизировались, что может привести к ошибкам параллелизма. Вы ничего не синхронизируете при добавлении /удалении в userNames , names и writers .

Я не понимаю, почему у вас есть переменная usersConnected (которая также не является частью любой синхронизации btw). Почему бы не использовать размер одного из наборов вместо этой переменной int ?

userNames , names и writers очень связаны друг с другом. Я бы реорганизовал это на Set , где у клиента есть userName , name и writer . Этот набор может предпочтительно быть ConcurrentHashSet .

Client

Пожалуйста, по крайней мере, сделайте поля класса private .

Вы можете использовать файлы Java FXML для указания макета вместо создания макета программным способом.

И клиент, и сервер полагаются на кодировку по умолчанию , например InputStreamReader . Вместо этого используйте UTF-8. Я считаю, что если вы будете использовать инструмент findbugs, это также предупредит вас об этом.

FutureTask

Эта часть дублируется:

Вы можете легко реорганизовать это на createNamePromptTask(«Choose a screen name:»)

Кроме того, ваш FutureTask вызывает showAndWait в диалоговом окне dialog , что делает его синхронным диалогом. Я бы предпочел, чтобы диалог был создан, показан, и код продолжался. Затем, когда диалог был закрыт, вы используете event listener или обратный вызов , который выполняет out.println(/* get value of text field in dialog */); Возможно, вы сможете выполнить это, используя один из onXYZ свойства в диалоге .

Сервер в автономном режиме

Вы уверены, что единственная причина, по которой это может произойти, заключается в том, что сервер отключен? Что делать, если вы отсоединяете сетевой кабель?

Кроме того, учитывая количество раз, когда вы вызываете messageArea.appendText , вам может понадобиться извлечь сообщение message(String)

Типы сообщений

Кажется, что как только клиент подключен, он может отправлять только один тип сообщения на сервер, сообщение чата. Я рекомендую вам открыть возможность отправки других типов сообщений, добавив строку, такую ​​как «MESSAGE», в начало текущего сообщения, которое отправляется с клиента на сервер.


Весь ваш else-ifs по существу работает как Map > , то есть: вы проверяете, с какой строки начинается сообщение, и то вы справитесь с этим.

Вы можете реорганизовать этот код, чтобы использовать реальный Map >

Обратите внимание, что здесь я изменился так, что str всегда без типа сообщения . Ваш текущий код имеет такие строки, как:

Угадайте, что такое 8 ? Магическое число, да!

Вместо этого вы можете позволить одному фрагменту кода позаботиться о разделении типа сообщения и данных сообщения:

Ваши конкретные вопросы

Обработка потоков. Было бы неплохо проверить, был ли поток прерван, выполнив что-то вроде if (Thread.interrupted()) break; , когда вы находитесь внутри петля. В противном случае это выглядит нормально.

Клиент и клиентский вход: я не поклонник showAndWait , который приводит к синхронному диалогу. Ваши три тесно связанные переменные в Server userNames , names , writers также являются запахом для меня.

Промывка потока: вам не нужно вызывать его явно, потому что вы создаете PrintWriter с параметром true .

Серверный протокол: сообщение INFO имеет недостаток, который будет отображаться некорректно, если подключено 10 или более пользователей. Кроме того, лично я предпочитаю, чтобы «категории» (INFO, SUBMIT_NAME и т. Д.) Имели одинаковую длину, как я писал выше.

Производительность и эффективность должны быть прекрасными.

Расширяемость дизайна? Самое главное: Избегайте static .

KouChat

Простой чат для локальной сети.

KouChat — очень простой безсерверный Java чат-клиент для локальной сети.

KouChat интегрируется в область уведомлений, поддерживает сену темы оформления, можно создавать обычные и частные комнаты для общения, задавать тему для комнаты, можно выбрать цвет текста для отправляемых сообщений и посылать смайлики.

KouChat позволяет передавать файлы, можно включить звуковое сопровождение, можно просматривать когда собеседник начинает набирать сообщение, ведётся журнал и пр.

Лицензия: GNU Lesser General Public License v3

Разработка клиент-серверного чата на Java. Часть 1. Немного теории и сервер

Краткое описание

На данный момент заканчиваю 2-й курс универститета, одной из лабораторных работ по курсу Java было написание чата. После того, как разобрался в теме сокетов, сериализации объектов и MVC, хотелось бы поделиться с читателим, тем более, что оно мне несказанно помогло при написании проекта.

Ну и, разумеется, учту все ошибки и недочеты, которые будут озвучены.

Немного теории

Итак, для начала. Проект будет состоять из двух частей: клиента и сервера. Клиент будет иметь GUI, написанный с помощью библиотеки Swing. Сервер GUI иметь не будет, только log-файл и небольшой вывод в консоль.

Для написания чата, нам понадобятся некоторые знания.

Сокеты

Поскольку взаимосвязь клиента и сервера у нас будет реализована с помощью сокетов, познакомимся с ними поближе.

Со́кеты (англ. socket — углубление, гнездо, разъём) — название программного интерфейса для обеспечения обмена данными между процессами. Сокет — абстрактный объект, представляющий конечную точку соединения.

Каждый процесс может создать слушающий сокет (серверный сокет) и привязать его к какому-нибудь порту операционной системы. Слушающий процесс обычно находится в цикле ожидания, то есть просыпается при появлении нового соединения.

Для нас это означает, что сервер будет слушать какой-либо порт (для большей универсальности, мы будем задавать порт в конфигурационном файле) и после подключения клиента, будем производить с ним какие-то действия.

Передача объектов по сети

Но просто получать сообщение от клиента нам мало. Нам хочется знать его имя, IP-адрес, а также передавать ему в ответ список подключенных пользователей. Таким образом, просто передача текстовых данных нас не устроит. У нас есть 2 выхода:
1. Передавать xml-строку с описанием всей информации
2. Передавать сериализованный объект, в котором будут храниться все необходимые нам данные в виде полей.

Не будем говорить о плюсах и минусах каждого подхода. Воспользуемся вторым вариантом. (А если понадобится, когда-нибудь потом, я напишу второй)

Итак, что такое сериализация объектов.

Сериализация — процесс перевода какой-либо структуры данных в последовательность битов. Обратной к операции сериализации является операция десериализации — восстановление начального состояния структуры данных из битовой последовательности.

Сериализация используется для передачи объектов по сети и для сохранения их в файлы.

В Java единственное, что нужно для сериализации объекта — имплементировать интерфейс Serializable, который является интерфейсом-маркером, т.е не содержит методов.

Пишем Сервер

Итак, краткое описание работы сервера.

Сервер работает в вечном цикле. Как только подключается новый клиент, он создает для работы с ним новый поток, оповещает уже подключенных клиентов о новом пользователей, а новичку отсылает какое-то количество последних сообщений в чате. Клиент же, при подключении сообщает о себе некоторую информациию, а также какое-то сообщение, идентифицирующее то, что он только что подключился.

Но помимо этого, надо не забыть о том, что клиенты могут и отключаться. То есть мы периодически должны обмениваться с клиентами сигналами (ping’овать друг друга), чтобы в случае отключения клиента (или сервера) все об этом узнали.

Итак, приступим. Создадим наш первый класс

Думаю, данный код, пока что, не нуждается в комментариях. Я не стал особо заморачиваться на обработке исключений, но нам ведь не это важно, верно?

Ах да, мы же хотели получать номер порта из конфигурационного файла. Создадим для конфига отдельный класс, в котором будем хранить статические поля — параметры нашего чата.

Удобнее всего хранить параметры в properties-файле, благо Java предоставляет удобный интерфейс для работы с ними.

Итак, наш properties-файл будет состоять всего из одной строки (пока что)

PORT=1234

Вся загрузка параметров происходит в блоке статической инициализации, поскольку полноценный конструктор в нашем случае — непозволительная роскошь.

Осталось только заменить в Server.java
ServerSocket socketListener = new ServerSocket(«1234»); на ServerSocket socketListener = new ServerSocket(Config.PORT); и добавить нужные import’ы

В дальнейшем, import’ы в коде буду упускать, поскольку любая IDE их сама подставит.

Итак, мы написали new ClientThread(); . Но что это такое, пока не решили. Пора исправить это.

Этот класс у нас будет отвечать за прием и передачу сообщений между клиентом и сервером, а значит, самый главный класс в чате — именно ClientThread.

Поскольку он работает в отдельном потоке, первое, что мы должны сделать — написать

А теперь подумаем, что же написать в методе run().
Итак, по порядку. Для начала, мы должны получить от клиента информацию «Ты кто такой?» в противном случае — «Давай, до свидания!». Как только мы узнали кто он такой, мы должны отправить ему последние сообщения в нашем чате.

Далее, периодически, мы должны его ping’овать его каким-нибудь запросом, чтобы убедиться, что не ведем общение с трупом.

Затем, мы должны получать от него сообщения, о которых должны уведомлять всех подключенных клиентов.

Ну и, как только мы перестали получать от него запросы — клиента следует удалить из списка доступнх пользователей.

На время забудем о ClientThread, а задумаемся «Каким образом будет происходить общение?»
Мы уже решили, что будем передавать сериализованный объект. Итак, чтоже должно быть в этом объекте?

Я остановился на следующем варианте:
1. Логин пользователя, отправившего это сообщение (или Server-Bot)
2. Собственно, само сообщение
3. Время отправки
4. Список доступных серверу клиентов (для отображения у пользователя)

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

Думаю, всё очевидно. Также, помимо сообщений мы хотим передавать нечто вроде ping’ов.

По сути, этот класс нам не сильно-то нужен, просто потом код будет удобнее читать

Итак, приступим к написанию ClientThread

Код немного комментирован, поэтому все должны разобраться. Осталось дописать только несколько функций.
Итак, начнем по порядку.

Данная функция рассылает какое-то сообщение всем клиентам

Ах да, мы еще забыли вписать некоторые поля класса ClientThread, которые активно использовали. Итак, класс ClientThread,java целиком

Осталось разобраться с функциями getUserList() и getChatHistory
Для начала, определим еще 3 класса

По сути, сервер готов к работе, осталось только немного модифицировать 2 класса.

Методы getChatHistory() и getUserList() сделаны синхронизированными, потому что с ними могут работать несколько потоков

И, доделаем наш конфиг, так как у нас добавились некоторые параметры

Заключение

Теперь наш сервер готов к использованию. Мы познакомились с сериализацией объектов и работой с сокетами в Java.

В следующей статье (завтра-послезавтра) мы напишем клиент для нашего чата, а пока жду комментариев, особенно относительно устройства чата, в частности — «Здесь абсолютно неправильно реаизовано XXX» (разумеется, жду только аргументированных оценок)


В любом случае, данная статья не призвана стать эталоном написания чата на Java, это лишь инструмент для понимания как вообще создавать такие приложения на хорошем примере, а не на эхо-чате в 10 строк.

Рекомендованный контент

Все ясно, но проблема NetBeans ругается :( Можно мне архив с сорсами?)

Ты нашел решение проблемы?

Поддержу вышеотписавшегося, можете ли дать ссылку на проет?

у меня осталась одна ошибка вот в этом месте this.timer = new Timer(DELAY, new ActionListener() < , как её исправить?

то Игорь: Подключайте таймер из библиотеки awt.
Вопрос к автору: где клиент.

Ребят, автор выложил клиент или нет еще?

У socketListener в конструкторе указан порт в виде String.Надо бы исправить.
ServerSocket socketListener = new ServerSocket(“1234”);

у кого-нидь была проблема с timer.stop()?
как ее решить?

getUserList() не видит этот метод, предлагает его объявить.Что делать?

Где продолжение? Ссылку, пожалуйста.

Перелыл весь сайт так и не нашел продолжения этой хорошей статьи! Может кто нибудь подскажет. дайте ссылку плизззз…..

Не знаю кому там всё ясно, но код вообще трудно читабельный. Пояснений во второй части вообще нету. Некоторые методы вообще вызываются не пойми как , так как находятся вне зоны видимости.

Хотя если не пытаться вдаваться в код, а просто понять общий принцип работы, то более менее понятно

Ну и где продолжение

хорошая статья, а вот продолжение где?

В пакете java.awt я не нашел ни одного таймера…

Алекс надо смотреть тут import java.awt.event.*;

К слову я что-то не догоняю о почему не видно вызовы getUserList и getChatHistory ? в какой класс зырить ?

В Server, там они определены. Просто из ClientThread они не видны…

Ребята кто-нибудь написал клиент к этому серверу? Поделитесь, у меня постоянно ошибки вылетают.

Ребят, вот я честно дуб-дубом в этом всем….. но мне нужна помощ как раз тех кто шарит. Хотим с кентами просто сделать свой чатик что бы там трещать на досуге, но вот проблема никто в этом не шарит, денег платить как то не охота,

кто может помочь, киньте инструкцию полную на почту tarasovdaniil22@gmail.com

Вопрос автору: а как поведёт себя сервер, если будет 10 000 000+ клиентов онлайн? сервак от количества потоков не упадёт? и сколько надо оперативной памяти под это? и ещё один вопрос не лучше ли сообщения передавать в хотя бы xml формате, на серваке брать парсером его статус(ping | text | login |…) и дальше зависимости от статуса его обрабатывать или всё таки лучше сериализованный объект передавать и на сервере не парсить а десериализовывать?

Цукерберг рекомендует:  Android - На чем пишут приложения для Android

Где можно найти клиент?

ThreadClient не очень написан. Запускать поток в конструкторе как-то не очень.

Что бы не было ошибки связанной с с timer.stop() надо подключить import javax.swing.Timer;

Проблема с классом СlientTread.
Искал ошибки, но не нашел

ServerSocket socketListener = new ServerSocket(Config.PORT);
Не пойму почему ругается, в классе Server проблема, почему то не видит в классе config, не могли бы вы помочь разобраться?

В классе ClientThread не видит методы getChatHistory и getUserList. Как решить проблему?

Эти методы находятся в классе Server, и являются статическими. Вызываются так: Server.getChatHistory(), Server.getUserList().

порт в кавычках. мдаааааа….бб ламер

в общем иди нахер…твой код безнадежно устарел…в топку его и тебя

Может кому пригодится клиент :)
Написал его за пару часов, и опыта в работе с потоками нет, поэтому не придирайтесь.

Всего в проекте 4 файла:

public class SocketClient <
private final static String address = “127.0.0.1”; // это IP-адрес компьютера, где исполняется наша серверная программа
private final static int serverPort = 6666; // здесь обязательно нужно указать порт к которому привязывается сервер

private static String userName = “”;
static Socket socket = null;

public static void main( String[] args ) <
System.out.println(“Вас приветствует клиент чата!\n”);
System.out.println(“Введите свой ник и нажмите \”Enter\””);

// Создаем поток для чтения с клавиатуры
BufferedReader keyboard = new BufferedReader( new InputStreamReader( System.in ) );
try <
// Ждем пока пользователь введет свой ник и нажмет кнопку Enter
userName = keyboard.readLine();
System.out.println();
> catch ( IOException e )

try <
try <
InetAddress ipAddress = InetAddress.getByName( address ); // создаем объект который отображает вышеописанный IP-адрес
socket = new Socket( ipAddress, serverPort ); // создаем сокет используя IP-адрес и порт сервера

// Берем входной и выходной потоки сокета, теперь можем получать и отсылать данные клиентом
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();

// Конвертируем потоки в другой тип, чтоб легче обрабатывать текстовые сообщения
ObjectOutputStream objectOutputStream = new ObjectOutputStream( outputStream );
ObjectInputStream objectInputStream = new ObjectInputStream( inputStream );

new PingThread( objectOutputStream, objectInputStream );

// Создаем поток для чтения с клавиатуры
String message = null;
System.out.println(“Наберите сообщение и нажмите \”Enter\”\n”);

while (true) < // Бесконечный цикл
message = keyboard.readLine(); // ждем пока пользователь введет что-то и нажмет кнопку Enter.
objectOutputStream.writeObject( new Message( userName, message ) ); // отсылаем введенную строку текста серверу.
>
> catch ( Exception e ) < e.printStackTrace(); >
>
finally <
try <
if ( socket != null ) < socket.close(); >
> catch ( IOException e ) < e.printStackTrace(); >
>
>
>

public class ServerListenerThread implements Runnable <
private Thread thread = null;
private ObjectOutputStream objectOutputStream = null;
private ObjectInputStream objectInputStream = null;

public ServerListenerThread( ObjectOutputStream objectOutputStream, ObjectInputStream objectInputStream ) <
this.objectOutputStream = objectOutputStream;
this.objectInputStream = objectInputStream;

thread = new Thread( this );
thread.start();
>

@Override
public void run() <
try <
while (true) <
Message messageIn = (Message) objectInputStream.readObject();
if ( messageIn instanceof Ping ) <
Ping ping = (Ping) messageIn;
objectOutputStream.writeObject( new Ping() );
> else <
System.out.println(“[ ” + messageIn.getDate().toString() + ” ] ” + messageIn.getLogin() + ” : ” + messageIn.getMessage() );
>
>
>
catch ( SocketException e ) < e.getMessage(); >
catch ( ClassNotFoundException e ) < e.getMessage(); >
catch ( IOException e ) < e.getMessage(); >
>
>

3, 4) Это файла протокола обмена с сервером: Message.java и Ping.java

Собственно вот и весь клиент. А дальше уже сами наращивайте функционал и придумывайте GUIню какую хотите)

Thinking In Java Enterprise (русский перевод) → Сетевое программирование с Сокетами и Каналами

Одна из сильных сторон Java заключается в безпроблеммной сетевой работе. Дизайнеры сетевой библиотеки Java сделали ее достаточно простой для чтения и записи файлов, за исключением случая, когда «файл» существует на удаленной машине и удаленная машина может решать что ей делать с информацией, которую вы запрашиваете или посылаете. Насколько это возможно, низлежащие детали сетевого взаимодействия были абстрагированы и о них заботится ядро JVM и локальный пакет установки Java. Программная модель, которую вы используете для такого файла, фактически, это обертка сетевого соединения («сокет») с объектом потока, так что в конечном счете вы используете те же вызовы методов, которые вы используете для других потоков. Кроме того, встроенная многопоточность Java исключительно удобна, когда вы имеете дело с такой сетевой возможностью, как обработка множества соединений одновременно.

Этот раздел является вводным в сетевое взаимедействие Java с использованием легких в понимании примеров.

Идентификация машины

Конечно, для того, чтобы передать данные с одной машины на другую необходимо убедиться, что вы подсоединились к определенной машине в сети. Ранние варианты сетей были удовлетворены предоставлением уникальных имен машинам внутри локальной сети. Однако, Java работает в пределах Internet, что требует способа для уникальной идентификации машины из любой точки всего мира. Это выполняется с помощью IP (Internet Protocol) адреса, который может существовать в двух формах:

  1. Привычная форма DNS (Domain Name System). Мое доменное имя — bruceeckel.com, и если у меня есть компьютер, называемый Opus в моем домене, его доменное имя должно быть Opus.bruceeckel.com. Это в точности имя такого рода, которое вы используете при отсылке электронной почты людям, и часто он встроен в адрес World Wide Web.
  2. Альтернативный вариант: вы можете использовать форму из четырех чисел, разделенных точками, например 123.255.28.120.

В обоих случаях IP адрес представляется как 32-х битное число [1] (так как каждое из четырех чисел не может превышать 255), и вы можете получить специальный Java объект для представления этого числа из любой из перечисленных выше форм, используя статический метод InetAddress.getByName( ), который определен в java.net. Результатом будет объект типа InetAddress, который вы можете использовать для создания «сокета», как вы это увидите далее.

В качестве простейшего примера использования InetAddress.getByName() рассмотрим, что произойдет при использовании коммутируемого доступа (dial-up Internet service provider (ISP)). При каждом дозвоне вам назначается временный IP адрес. Но пока вы соединены, ваш IP адрес имеет такую же силу, как и другие IP адреса в Internet. Если кто-либо соединится с вашей машиной использую ваш IP адрес, то он может соединится с Web сервером или FTP сервером, который запущен на вашей машине. Конечно, ему необходимо знать ваш IP адрес, а так как при каждом дозвоне вам назначается новый адрес, то как вы можете определеть какой у вас адрес?

Приведенная ниже программа использует InetAddress.getByName( ) для воспроизведения вашего IP адреса. Для ее использования вы должны знать имя вашего компьютера. Под управлением Windows 95/98 перейдите в «Settings», «Control Panel», «Network» и выберите закладку «Identification». Содержимое в поле «Computer name» является той строкой, которую необходимо поместить в командную строку.

//: c15:WhoAmI.java
// Нахождение вашего сетевого адреса, когда
// вы соединены с Internet’ом.
// <Запускается руками>Должно быть установлено соединение с Internet
//
import java.net.*;

public class WhoAmI <
public static void main ( String [] args ) throws Exception <
if ( args.length != 1 ) <
System.err.println ( «Usage: WhoAmI MachineName» ) ;
System.exit ( 1 ) ;
>
InetAddress a = InetAddress.getByName ( args [ 0 ]) ;
System.out.println ( a ) ;
>
> // /:

В моем случае, машина называется «peppy». Так что, когда я соединюсь с моим провайдером и запущу программу:

Я получу назад сообщение такого типа (конечно же, адрес отличается при каждом новом соединении):

Если я скажу этот адрес моему другу и у меня будет запущен Web Сервер на моем компьютере, он сможет соединится с сервером, перейдя по ссылке http://199.190.87.75 (только до тех пор, пока я остаюсь соединенным во время одной сессии). Иногда это может быть ручным способом распределения информации кому-то еще или использоваться для тестирования конфигурации Web сайта перед размещением его на «реальном» сервере.

Серверы и клиенты

Основное назначение сети состоит в том, чтобы позволить двум машинам соединиться и пообщаться друг с другом. Так как две машины могут найти друг друга, они могут провести милую, двусторонню беседу. Но как они могут найти друг друга? Это похоже на поиск потерянных в парке аттракционов: одна машина стоит в одном месте и слушает, пока другая машина скажет: «Эй, ты где?».

Машина, которая стоит в одном месте, называется сервером, а машина, которая ищет, называется клиентом. Это различие важно лишь до тех пор, пока клиент пробует соединится с сервером. Как только они соединятся, они становятся двумя сторонами коммуникационного процесса и более не имеет значения, какая машина принимала роль сервера, а какая принимала роль клиента.


Таким образом, работа сервера состоит в прослушивании соединения, она выполняется с помощью специального объекта, который вы создаете. Работа клиента состоит в попытке создать соединение с сервером, и это выполняется с помощью специального клиентского объекта, который вы создаете. Как только соединение установлено, вы увидите, что и клиентская, и серверная сторона соединения магическим образом превращается потоковый объект ввода/вывода, таким образом вы можете трактовать соединение, как будто вы читаете и пишете файл. Таким образом, после установки соединения, вы просто используете хорошо знакомые команды ввода/вывода из главы 11. Это одна из прекраснейших особенностей работы по сети в Java.

Тестирование программ без сети

По многим причинам, вы можете не иметь клиентской машины, серверной машины и сети, доступных для тестирования ваших программ. Вы можете выполнять упражнения в обстановке классной комнаты, или, возможно, вы пишите программы, которые еще не достаточно стабильны и не могут быть выложены в сеть. Создатели Internet Protocol учли эту возможность и создали специальный адрес, называемый localhost, IP адрес «локальной заглушки (local loopback)» для тестирования без использования сети. Общий способ для получения такого адреса в Java такой:

Если вы передадите в getByName( ) значение null, метод по умолчанию будет использовать localhost. InetAddress является тем, что вы используете для указания определенной машины, и вы должны произвести его прежде, чем вы можете двинуться далее. Вы не можете манипулировать содержимым InetAddress (но вы можете напечатать его, как это будет показано в следующем примере). Единственный способ, которым вы можете создать InetArddress, это через один из перегруженных статических методов класса getByName( ) (который является тем, что вы уже использовали), getAllByName(), или getLocalHost( ).

Вы также можете получить адрес локальной заглушки, передав строку localhost:

(предполагается, что «localhost» сконфигурирован в таблице «hosts» на вашей машине), или используя цифровую четырехзначную форму для имени, представляющем заглушку:

Все три формы произовдят одинаковый результат.

Порт: уникальное место внутри машины

IP адреса не достаточно для уникальной идентификации сервера, так как многие сервера могут существовать на одной машине. Каждая IP машина также содержит порты, и когда вы устанавливаете клиента или сервер, вы должны выбрать порт, через который и клиент, и сервер согласны соединиться.

Порт — это не физическое расположение в машине, а программная абстракция (в основном для целей учета). Клиентская программа знает, как соединится к машине через ее IP адрес, но как она может присоединится к определенной службе (потенциально, к одной из многих на этой машине)? Таким образом номер порта стал вторым уровнем адресации. Идея состоит в том, что при запросе определенного порта вы запрашиваете службу, ассоциированную с этим номером порта. Служба времени — простейший пример службы. Обычно каждая служба ассоциируется с уникальным номером порта на определенной серверной машине. Клиент должен предварительно знать, на каком порту запущена нужная ему служба.

Системные службы зарезервировали использование портов с номерам от 1 до 1024, так что вы не можете использовать этот или любой другой порт, про который вы знаете, что он задействован. Первым выбором, например, в этой книге будет порт 8080 (в память многоуважаемого 8-битного процессора 8080 от Intel в моем первом компьютере, CP/M машине).

Сокеты

Сокет — это программная абстракция, используемая для представления «терминалов» соединения между двумя машинами. Для данного соединения есть сокет на каждой машине, и вы можете представить гипотетический «кабель», включенный в сокет. Конечно, физическое оборудование и каблирование между машинами полностью неизвестно. Главное назначение абстракции состоит в том, что мы не должны знать более того, что нам необходимо.

В Java вы создаете сокет, чтобы создать соединение с другой машиной, затем вы получаете InputStream и OutputStream (или, с соответствующими конверторами, Reader и Writer) из сокета, чтобы получить возможность трактовать соединение, как объект потока ввода/вывода. Существует два класса сокетов, основанных на потоках: ServerSocket, который использует сервер для «прослушивания» входящих соединения, и Socket, который использует клиент для инициализации соединения. Как только клиент создаст сокетное соединение, ServerSocket возвратит (посредством метода accept( )) соответствующий Socket, через который может происходить коммуникация на стороне сервера. После этого вы общаетесь в соединении через Socket с Socket’ом и вы трактуете оба конца одинаково, посколько они и являются одним и тем же. На этой стадии вы используете методы getInputStream( ) и getOutputStream( ) для получения соответствующих объектов InputStream’а и outputStream’а для каждого сокета. Они должны быть обернуты внутрь буферных и форматирующих классов точно так же, как и другие объекты потоков, описанные в Главе 11.

Использование термина ServerSocket может показаться другим примером сбивающей с толку схемы именования в библиотеках Java. Вы можете подумать, что ServerSocket лучше было бы назвать «ServerConnector» или как-то подругому, без слова «Socket» внутри. Вы также можете подумать, что ServerSocket и Socket должны оба наследоваться от какого-то общего базового класса. На самом деле, два калсса имеют некоторые общие методы, но не настолько, чтобы дать им общий базовый класс. Вместо этого, работа ServerSocket’а состоит в том, чтобы ждать, пока некоторая машина не присоединится к нему, а затем он возвращает реальный Socket. Вот почему кажется, что ServerSocket назван немножко неправильно, так как его работа состоит не в том, чтобы быть реальным сокетом, а в том, чтобы создавать объект Socket’а, когда кто-то присоединяется к нему.

Однако, ServerSocket создает физический «сервер» или слушающий сокет на хост-машине. Этот сокет слушает входящие соединения, а затем возвращает «связанный» сокет (с определенными локальной и удаленной конечными точками) посредством метода accept( ). Сбивающая часть состоит в том, что оба эти сокета (слушающий и связанный) ассоциированы с одним и тем же серверным сокетом. Слушающий сокет может принять только новый запрос на соединение, а не пакет данных. Так что, не смотря на то, что ServerSocket имеет мало смыла с точки зрения программирования, в нем много смысла «физически».

Когда вы создаете ServerSocket, вы даете ему только номер порта. Вы не даете ему IP адрес, поскольку он уже есть на той машине, на которой он представлен. Однако когда вы создаете Socket, вы должны передать ему и IP адрес, и номер порта, к которому вы хотите присоединиться. (Однако Socket, который возвращается из метода ServerSocket.accept( ) уже содержит всю эту информацию.)

Простейший сервер и клиент

Этот пример покажет простейшее использование серверного и клиентского сокета. Все, что делает сервер, это ожидает соединения, затем использует сокет, полученный при соединении, для создания InputStream’а и OutputStream’а. Они конвертируются в Reader и Writer, которые оборачиваются в BufferedReader и PrintWriter. После этого все, что будет прочитано из BufferedReader’а будет переправлено в PrintWriter, пока не будет получена строка «END», означающая, что пришло время закрыть соединение.

Клиент создает соединение с сервером, затем создает OutputStream и создает некоторую обертку, как и в сервере. Строки текста посылаются через полученный PrintWriter. Клиент также создает InputStream (опять таки, с соответствующей конвертацией и оберткой), чтобы слушать, что говорит сервер (который, в данном случае, просто отсылает слова назад).

И сервер, и клиент используют одинаковый номер порта, а клиент использует адрес локальной заглушки для соединения с сервером на этой же самой машине, так что вы не можете провести тест по сети. (Для некоторых конфигураций вам может понадобиться сетевое соединения для работы программы даже, если вы не используете сетевую коммуникацию.)

//: c15:JabberServer.java
// Очень простой сервер, который просто отсылает
// назад все, что посылает клиент.
//
import java.io.*;

public class JabberServer <
// Выбираем порт вне пределов 1-1024:
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Started: » + s ) ;
try <
// Блокирует до тех пор, пока не возникнет соединение:
Socket socket = s.accept () ;
try <
System.out.println ( «Connection accepted: » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader (
socket.getInputStream ())) ;
// Вывод автоматически выталкивается из буфера PrintWriter’ом
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
while ( true ) <
String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
// Всегда закрываем два сокета.
>
finally <
System.out.println ( «closing. » ) ;
socket.close () ;
>
>
finally <
s.close () ;
>
>
> // /:

Вы можете видеть, что для ServerSocket’а необходим только номер порта, а не IP адрес (так как он запускается на локальной машине!). Когда вы вызываете accept( ), метод блокирует выполнение до тех пор, пока клиент не попробует подсоединится к серверу. То есть, сервер ожидает соединения, но другой процесс может выполнятся (смотрите Главу 14). Когда соединение установлено, метод accept( ) возвращает объект Socket, представляющий это соединение.

Здесь тщательно обработана отвественность за очистку сокета. Если конструктор ServerSocket завершится неудачей, программа просто звершится (обратите внимание, что мы должны предположить, что конструктор ServerSocket не оставляет никаких открытых сокетов, если он зваершается неудачей). По этой причине main( ) выбрасывает IOException, так что в блоке try нет необходимости. Если конструктор ServerSocket завершится успешно, то все вызовы методов должны быть помещены в блок try-finally, чтобы убедиться, что блок не будет покинут ни при каких условиях и ServerSocket будет правильно закрыт.

Аналогичная логика используется для сокета, возвращаемого из метода accept( ). Если метод accept( ) завершится неудачей, то мы должны предположить, что сокет не существует и не удерживает никаких ресурсов, так что он не нуждается в очистке. Однако если он закончится успешно, то следующие выражения должны быть помещены в блок try-finally, чтобы при каких-либо ошибках все равно произошла очистка. Позаботится об этом необходимо, потому что сокеты используют важные ресурсы, не относящиеся к памяти, так что вы должны быть прилежны и очищать их (так как в Java нет деструкторов, чтобы сделать это за вас).

И ServerSocket и Socket, производимый методом accept( ), печатаются в System.out. Это означает, что автоматически вызывается их метод toString( ). Вот что он выдаст:

Короче говоря, вы увидите как это соответствует тому, что делает клиент.

Следующая часть программы выглядит, как открытие файла для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream’а и OutputStream’а конвертируются в объекты Reader’а и Writer’а с помощью «классов-конвертеров» InputStreamReader и OutputStreamreader, соответственно. Вы можете также использовать классы из Java 1.0 InputStream и OutoutStream напрямую, но, с точки зрения вывода, есть явное преимущество в использовании этого подхода. Оно проявляется в PrintWriter’е, который имеет перегруженный конструктор, принимающий в качестве второго аргумента флаг типа boolean, указывающий, нужно ли автоматическое выталкивание буфера вывода в конце каждого выражения println( ) (но не print( )). Каждый раз, когда вы записываете в вывод, буфер вывода должен выталкиваться, чтобы информация проходила по сети. Выталкивание важно для этого конкретного примера, поскольку клиент и сервер ожидают строку от другой стороны, прежде, чем приступят к ее обработке. Если выталкивание буфера не произойдет, информация не будет помещена в сеть до тех пор, пока буфер не заполнится, что может привести к многочисленным проблемам в этом примере.

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

Обратите внимание, что фактически все открытые вами потоки, буфферезированы. В конце этой главы есть упражнение, которое покажет вам, что происходит, если вы не буфферезируете потоки (вещи становятся медленнее).

В бесконечном цикле while происходит чтение строк из входного BufferedReader’а и запись информации в System.out и в выходной PrintWriter. Обратите внимание, что вход и выход могут быть любыми потоками, так случилось, что они связаны с сетью.

Когда клиент посылает строку, содержащую «END», программа прекращает цикл и закрывает сокет.

//: c15:JabberClient.java
// Очень простой клиент, который просто посылает
// строки на сервер и читает строки,
// посылаемые сервером.
//
import java.net.*;

public class JabberClient <
public static void main ( String [] args ) throws IOException <
// Передаем null в getByName(), получая
// специальный IP адрес «локальной заглушки»
// для тестирования на машине без сети:
InetAddress addr = InetAddress.getByName ( null ) ;
// Альтернативно, вы можете использовать
// адрес или имя:
// InetAddress addr =
// InetAddress.getByName(«127.0.0.1»);
// InetAddress addr =
// InetAddress.getByName(«localhost»);
System.out.println ( «addr = » + addr ) ;
Socket socket = new Socket ( addr, JabberServer.PORT ) ;
// Помещаем все в блок try-finally, чтобы
// быть уверенным, что сокет закроется:
try <
System.out.println ( «socket = » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Вывод автоматически Output быталкивается PrintWriter’ом.
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
for ( int i = 0 ; i 10 ; i++ ) <
out.println ( «howdy » + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
finally <
System.out.println ( «closing. » ) ;
socket.close () ;
>
>
> // /:

В main( ) вы можете видеть все три способа получение InetAddress IP адреса локальной заглушки: с помощью null, localhost или путем явного указания зарезервированного адреса 127.0.0.1, если вы хотите соединится с машиной по сети, вы замените это IP адресом машины. Когда печатается InetAddress (с помощью автоматического вызова метода toString( )), то получается результат:

Цукерберг рекомендует:  Как программисту побороть зависимость от кофеина

При передачи в getByName( ) значения null, он по умолчанию ищет localhos и затем производит специальныйы адрес 127.0.0.1.

Обратите внимание, что Socket создается при указании и InetAddress’а, и номера порта. Чтобы понять, что это значит, когда будете печатать один из объектов Socket помните, что Интернет соединение уникально определяется четырьмя параметрами: клиентским хостом, клиентским номером порта, серверным хостом и серверным номером порта. Когда запускается сервер, он получает назначаемый порт (8080) на localhost (127.0.0.1). Когда запускается клиент, он располагается на следующем доступном порту на своей машине, 1077 — в данном случае, который так же оказался на той же самой машине (127.0.0.1), что и сервер. Теперь, чтобы передать данные между клиентом и сервером, каждая сторона знает, куда посылать их. Поэтому, в процессе соединения с «известным» сервером клиент посылает «обратный адрес», чтобы сервер знал, куда посылать данные. Вот что вы видите среди выводимого стороной сервера:

Это означает, что сервер просто принимает соединение с адреса 127.0.0.1 и порта 1077 во время прослушивания локального порта (8080). На клиентской стороне:

Это значит, что клиент установил соединение с адресом 127.0.0.1 по порту 8080, используя локальный порт 1077.

Вы заметите, что при каждом повторном запуске клиента номер локального порта увеличивается. Он начинается с 1025 (первый после зарезервированного блока портов) и будет увеличиваться до тех пор, пока вы не перезапустите машину, в таком случае он снова начнется с 1025. (На машинах под управлением UNIX, как только будет достигнут верхний предел диапазона сокетов, номер будет возвращен снова к наименьшему доступному номеру.)

Как только объект Socket будет создан, процесс перейдет к BufferedReader и PrintWriter, как мы это уже видели в сервере (опять таки, в обоих случаях вы начинаете с Socket’а). В данном случае, клиент инициирует обмен путем посылки строки «howdy», за которой следует число. Обратите внимание, что буфер должен опять выталкиваться (что происходит автоматически из-за второго аргумента в конструкторе PrintWriter’а). Если буфер не будет выталкиваться, процесс обмена повиснет, поскольку начальное «howdy» никогда не будет послана (буфер недостаточно заполнен, чтобы отсылка произошла автоматически). Каждая строка, посылаемая назад сервером, записывается в System.out, чтобы проверить, что все работает корректно. Для завершения обмена посылается ранее оговоренный «END». Если клиент просто разорвет соединение, то сервер выбросит исключение.

Вы можете видеть, что аналогичные меры приняты, чтобы быть уверенным в том, что сетевые ресурсы, представляемые сокетом, будут правильно очищены. Для этого используется блок try-finally.

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

Обслуживание множества клиентов

JabberServer работает, но он может обработать только одного клиента одновременно. В обычных серверах вы захотите, чтобы была возможность иметь дело со многими клиентами одновременно. Ответом является многопоточность, и в языках, которые не поддерживают многопоточность напрямую, это означает что вы встретите все возможные трудности. В Главе 14 вы видели, что многопоточность в Java проста насколько это возможно, учитывая это, можно сказать, что многопоточность весьма сложная тема. Поскольку нити (потоки) в Java достаточно прамолинейны, то создание сервера, который обрабатывает несколько клиентов, относительно простое заняте.

Основная схема состоит в создании единственного ServerSocket’а на сервере и вызове метода accept( ) для ожидания новых соединений. Когда accept( ) возвращается, вы получаете результирующий сокет и используете его для создания новой нити (потока), работа которой будет состоять в ослуживании определенного клиента. Затем вы вызовите метод accept( ) снова, чтобы подождать нового клиента.

В следующем коде сервера вы можете видеть, что он очень похож на пример JabberServer.java, за исключением того, что все операции по обслуживанию определенного клиента былы помещены внутрь отдельного thread-класса:

//: c15:MultiJabberServer.java
// Сервер, который использует многопоточность
// для обработки любого числа клиентов.
//
import java.io.*;

class ServeOneJabber extends Thread <
private Socket socket;
private BufferedReader in;
private PrintWriter out;

public ServeOneJabber ( Socket s ) throws IOException <
socket = s;
in = new BufferedReader ( new InputStreamReader ( socket.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter ( socket
.getOutputStream ())) , true ) ;
// Если любой из вышеприведенных вызовов приведет к
// возникновению исключения, то вызывающий отвечает за
// закрытие сокета. В противном случае, нить
// закроет его.
start () ; // вызываем run()
>

public void run () <
try <
while ( true ) <
String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
System.out.println ( «closing. » ) ;
>
catch ( IOException e ) <
System.err.println ( «IO Exception» ) ;
>
finally <
try <
socket.close () ;
>
catch ( IOException e ) <
System.err.println ( «Socket not closed» ) ;
>
>
>
>

public class MultiJabberServer <
static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Server Started» ) ;
try <
while ( true ) <
// Блокируется до возникновения нового соединения:
Socket socket = s.accept () ;
try <
new ServeOneJabber ( socket ) ;
>
catch ( IOException e ) <
// Если завершится неудачей, закрывается сокет,
// в противном случае, нить закроет его:
socket.close () ;
>
>
>
finally <
s.close () ;
>
>
> // /:

Нить ServeOneJabber принимает объект Socket’а, который производится методом accept( ) в main( ) при каждом новом соединении с клиентом. Затем, как и прежде, с помощью Socket, создается BufferedReader и PrintWriter с возможностью автоматического выталкивания буфера. И наконец, вызывается специальный метод нити start( ). Здесь выполняются те же действия, что и в предыдущем примере: читается что-то из сокета и затем отсылается обратно до тех пор, пока не будет прочитан специальный сигнал «END».

Ответственность за очистку сокета должна быть, опять таки, внимательно спланирована. В этом случае, сокет создается вне ServeOneJabber, так что ответственность может быть совместная. Если конструктор ServeOneJabber завершится неудачей, он просто выбросит исключение тому, кто его вызвал, и кто должен очистить нить. Но если конструктор завершился успешно, то объект ServeOneJabber принимает ответственность за очистку нити на себя, в своем методе run( ).

Обратите внимание на упрощенность MultiJabberServer. Как и прежде создается ServerSocket и вызывается метод accept( ), чтобы позволить новое соединение. Но в это время возвращаемое значение метода accept( ) (сокет) передается в конструктор для ServeOneJabber, который создает новую нить для обработки этого соединения. Когда соединение завершиется, нить просто умирает.

Если создание ServerSocket’а проваливается, то из метода main( ), как и прежде, выбрасывается исключение. Но если создание завершается успешно, внешний блок try-finally гарантирует очистку. Внутренний try-catch гарантирует только от сбоев в конструкторе ServeOneJabber. Если конструктор завершится успешно, то нить ServeOneJabber закроет соответствующий сокет.

Для проверки этого сервера, который реально обрабатывает несколько клиентов, приведенная ниже программа создает несколько клиентов (используя нити), которые соединяются с одним и тем же сервером. Максимальное допустимое число нитей определяется переменной final int MAX_THREADS.

//: c15:MultiJabberClient.java
// Клиент, который проверяет MultiJabberServer,
// запуская несколько клиентов.
//
import java.net.*;

class JabberClientThread extends Thread <
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0 ;
private int > private static int threadcount = 0 ;

public static int threadCount () <
return threadcount;
>

public JabberClientThread ( InetAddress addr ) <
System.out.println ( «Making client » + id ) ;
threadcount++;
try <
socket = new Socket ( addr, MultiJabberServer.PORT ) ;
>
catch ( IOException e ) <
System.err.println ( «Socket failed» ) ;
// Если создание сокета провалилось,
// ничего ненужно чистить.
>
try <
in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter (
socket.getOutputStream ())) , true ) ;
start () ;
>
catch ( IOException e ) <
// Сокет должен быть закрыт при любой
// ошибке, кроме ошибки конструктора сокета:
try <
socket.close () ;
>
catch ( IOException e2 ) <
System.err.println ( «Socket not closed» ) ;
>
>
// В противном случае сокет будет закрыт
// в методе run() нити.
>

public void run () <
try <
for ( int i = 0 ; i 25 ; i++ ) <
out.println ( «Client » + id + «: » + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
catch ( IOException e ) <
System.err.println ( «IO Exception» ) ;
>
finally <
// Всегда закрывает:
try <
socket.close () ;
>
catch ( IOException e ) <
System.err.println ( «Socket not closed» ) ;
>
threadcount—; // Завершаем эту нить
>
>
>

public class MultiJabberClient <
static final int MAX_THREADS = 40 ;

public static void main ( String [] args ) throws IOException,
InterruptedException <
InetAddress addr = InetAddress.getByName ( null ) ;
while ( true ) <
if ( JabberClientThread.threadCount () )
new JabberClientThread ( addr ) ;
Thread.currentThread () .sleep ( 100 ) ;
>
>
> // /:

Конструктор JabberClientThread принимает InetAddress и использует его для открытия сокета. Вероятно, вы заметили шаблон: сокет всегда используется для создания определенного рода объектов Reader’а и Writer’а (или InputStream и/или OutputStream), которые являются тем единственным путем, которым может быть использован сокет. (Вы можете, конечно, написать класс или два для автоматизации этого процесса вместо набора этого текста, если вас это беспокоит.) Далее, start( ) выполняет инициализацию нити и запуск run( ). Здесь сообщение посылается на сервер, а информация с сервера отображается на экране. Однако, нить имеет ограниченноен время жизни и, в конечном счете, завершается. Обратите внимание, что сокет очищается, если конструктор завершился неудачей после создания сокета, но перед тем, как конструктор завершится. В противном случае, ответственность за вызов close( ) для сокета ложиться на метод run( ).

Threadcount хранит информацию о том, сколько в настоящее время существует объектов JabberClientThread. Эта переменная инкрементируется, как часть конструктора и декрементируется при выходе из метода run( ) (что означает, что нить умерла). В методе MultiJabberClient.main( ) вы можете видеть, что количество нитей проверяется, и если их много, то нить более не создается. Затем метод засыпает. Таким образом, некоторые нити, в конечном счете, умрут, и другие будут созданы. Вы можете поэкспериментировать с MAX_THREADS, чтобы увидеть, когда ваша конкретная система почувствует затруднения со множеством соединений.

Дейтаграммы

Пример, который вы недавно видели, использует Transmission Control Protocol (TCP, также известный, как сокет, основанный на потоках), который предназначен для наибольшей надежности и гарантии, что данные будут доставлены. Он позволяет передавать повторно потерянные данные, он обеспечивает множественные пути через различные маршрутизаторы в случае, если один из них отвалится, а байты будут доставлены в том порядке, в котором они посланы. Весь этот контроль и надежность добавляют накладные расходы: TCP сильно перегружен.

Существует второй потокол, называемый User Datagram Protocol (UDP), который не гарантирует, что пакет будет доставлен и не гарантирует, что пакеты достигнут точки назначения в том же порядке, в котором они были отправлены. Он называется «ненадежным протоколом» (TCP является «надежным протоколом»), что звучит плохо, но так как он намного быстрее, он может быть полезнее. Существуют приложения, такие как аудио сигнал, в которых не критично, если несколько пакетов потеряются здесь или там, а скорость жизненно необходима. Или например сервер времени, для которого реально не имеет значения, если одно из сообщений будет потеряно. Также, некоторые приложения могут быть способны отправлять UDP сообщения к серверу и затем считать, если нет ответа в разумный период времени, что сообщения были потеряны.


Обычно вы будете выполнять ваше прямое сетевое программирование с помощью TCP, и только иногда вы будете использовать UDP. Есть более общее толкование UDP, включая пример, в первой редакции этой книги (доступра на CR-ROM’е, сопровождающем это книгу или может быть свободно загружено с www.BruceEckel.com).

Использование URL’ов из апплета

Для апплета есть возможность стать причиной отображения любого URL с помощью Web броузера, в котором запущен апплет. Вы можете сделать это с помощью следующей строки:

в которой u является объектом типа URL. Вот простой пример, который перенаправляет вас на другую страницу. Хотя вы просто перенаправляете на HTML страницу, вы можете также перенаправить на вывод, который дает CGI программа.

public class ShowHTML extends JApplet <
JButton send = new JButton ( «Go» ) ;
JLabel l = new JLabel () ;

public void init () <
Container cp = getContentPane () ;
cp.setLayout ( new FlowLayout ()) ;
send.addActionListener ( new Al ()) ;
cp.add ( send ) ;
cp.add ( l ) ;
>

class Al implements ActionListener <
public void actionPerformed ( ActionEvent ae ) <
try <
// Это может быть CGI программа вместо
// HTML страницы.
URL u = new URL ( getDocumentBase () , «FetcherFrame.html» ) ;
// Отображается вывод URL с помощью
// Web броузера, как обычная страница:
getAppletContext () .showDocument ( u ) ;
>
catch ( Exception e ) <
l.setText ( e.toString ()) ;
>
>
>

public static void main ( String [] args ) <
Console.run ( new ShowHTML () , 100 , 50 ) ;
>
> // /:

Красота класса URL состоит в том, что он отлично защищает вас. Вы можете соединится с Web серверами без знания многого из того, что происходит за занавесом.

Чтение файла с сервера

Вариация приведенной выше программы, читающей файл, расположенный на сервере. В этом случае файл указывается клиентом:

public class Fetcher extends JApplet <
JButton fetchIt = new JButton ( «Fetch the Data» ) ;
JTextField f = new JTextField ( «Fetcher.java» , 20 ) ;
JTextArea t = new JTextArea ( 10 , 40 ) ;

public void init () <
Container cp = getContentPane () ;
cp.setLayout ( new FlowLayout ()) ;
fetchIt.addActionListener ( new FetchL ()) ;
cp.add ( new JScrollPane ( t )) ;
cp.add ( f ) ;
cp.add ( fetchIt ) ;
>

public class FetchL implements ActionListener <
public void actionPerformed ( ActionEvent e ) <
try <
URL url = new URL ( getDocumentBase () , f.getText ()) ;
t.setText ( url + «n» ) ;
InputStream is = url.openStream () ;
BufferedReader in = new BufferedReader (
new InputStreamReader ( is )) ;
String line;
while (( line = in.readLine ()) != null )
t.append ( line + «n» ) ;
>
catch ( Exception ex ) <
t.append ( ex.toString ()) ;
>
>
>

public static void main ( String [] args ) <
Console.run ( new Fetcher () , 500 , 300 ) ;
>
> // /:

Создание объекта URL похоже на предыдущий пример — getDocumentBase( ) является начальной точкой, как и прежде, но в то же время, имя файла читается из JTextField. Как только объект URL создан, его строковая версия помещается в JTextArea, так что вы можем видеть, как он выглядит. Затем из URL’а получается InputStream, который в данном случае может просто производить поток символов из файла. После конвертации в Reader и буферизации, каждая строка читается и добавляется в JTextArea. Обратите внимание, что JTextArea помещается внутрь JScrollPane, так что скроллирование обрабатывается автоматически.

Мультиплексирование, Основанное на Переключении в JDK 1.4

Когда вы читаете из сокета или пишете в него, вам нужно сделать передачу данных рациональной. Давайте рассмотрим сначала операцию записи. Когда вы пишите данные на уровне приложения (TCP или UDP сокет), вы пишите данные в рабочий буфер системы. Эти данные, в конечном счете, формируют (TCP или UDP) пакеты, которые необходимо передать на машину назначения по сети. Когда вы пишите в сокет и, если в буфере нет достаточно доступного места, запись может блокироваться. Если вы читаете из сокета и нет достаточного количества информации для чтения из буфера операционной системы, куда попадают данные после получения из сети, чтение будет блокировано. Если есть нить (поток) для операции чтения или записи, эта нить не может делать ничего и может стать причиной снижения произовдительности вашей программы. До появления JDK 1.4 не было способа вывести такую нить из заблокированного состояния. С помощью каналов вы можете выполнить асинхронную операцию закрытия на канале и нить, блокированная на этом канале примет AsynchronousCloseException.

Асинхронный ввод-вывод в Java достигается тем же способом, который дает вызов метода select( ) в UNIX подобных системах. Вы можете дать список дескрипторов (чтения или записи) в функцию select( ) и она отследит этот дескриптор на возникновение некоторых событий. Для дескриптора, представляющего сокет, из которого вы читаете, данные в буфере операционной системы для этого буфера представляются событием. Для дескрипторов, представляющих сокеты, в которые вы пишите, наличие места для записи во внутреннем буфере операционной системы для этого сокета представляется событием. Поэтому вызов метода select( ) исследует различные дескрипторы для проверки событий.

Что, если вы просто читаете и пишите в дескриптор когда бы вы не захотели? Select может обрабатывать множество дескрипторов, что позволит вам мониторить множество сокетов. Рассмотрим пример чат-сервера, когда сервер имеет соединения с различными клиентами. Тип данных, достигающих сервера, перемежается. Сервер предназначен для чтения данных из сокета и отображения их в GUI, то есть для показа каждому клиенту — чтобы достич этого, вы читаете данные от каджого клиента и пишите эти данные всем остальным клиентам. Например 5 клиентов: 1, 2, 3, 4 и 5. Если сервер запрограммирован на выполнение чтения от 1 и записи в 2, 3, 4 и 5, затем происходит чтения от 2 и запись в 1, 3, 4, 5 и так далее, то может так случиться, что пока нить сервера заблокирована на чтении одного из клиентских сокетов, могут появиться данные на других сокетах. Одно из решений состоит в том, чтобы создавать различные нити для кадого клиента (до JDK1.4). Но это не масштабируемое решение. Вместо этого вы можете иметь селектор, основанный на механизме, следящем за всеми клиентскими сокетами. Он знает какой сокет имеет данные для чтения без блокирования. Но если единственная нить выполняет эту работу (выбор и запись каждому клиенту) он не будет хорошо откликаться. Таким образом в таких ситуациях одна нить мониторит сокеты на чтение, выбирает сокет, из которого можно осуществить чтение, и делегирует остальную ответственность (запись другим клиентам) другой нити (нитям) или пулу нитей.

Этот шаблон называется шаблоном реактора, когда события отсоединяются от действия, ассоциированного с событиями (Pattern Oriented Software Architecture — Doug Schmidt).

В JDK 1.4 вы создаете канал, регестрируете объект Селектора в канале, который (объект) будет следить за событиями в канале. Многие каналы регестрируют один и тот же объект Селектора. Единственная нить, которая вызывает Selector.select(), наблюдает множество каналов. Каждый из классов ServerSocket, Socket и DatagramSocket имеют метод getChannel( ), но он возвращает null за исключением того случая, когда канал создается с помощью вызова метода open( ) (DatagramChannel.open( ), SocketChannel.open( ), ServerSocketChannel.open( )). Вам необходимо ассоциировать сокет с этим каналом.

Вы мультиплексируете несколько каналов (то есть сокеты), используя Селектор. Статический вызов Selector.select( ) блокирует выполнение до возникновения события в одном из каналов. Существует так же и не блокирующая версия этого метода, которая принимает количество милисекунд для засыпания или блокирования до того момента, когда вызов метода завершится.

ByteBuffer используется для копирования данных из канала и в канал. ByteBuffer является потоком октетов и вы декодируете этот поток, как символы. Со стороны клиента в MultiJabberClient.java это выполняется путем использования классов Writer’а и OutputStreamWriter’а. Эти классы конвертируют символы в поток байтов.

Приведенная ниже программа NonBlockingIO.java объясняет, как вы можете использовать Селектор и Канал для выполнения мультиплексирования. Эта программа требует запущенного Сервера. Она может стать причиной исключения на сервере, но ее назначение не в коммуникации с сервером, а в том, чтобы показать, как работает select( ).

//: TIEJ:X1:NonBlockingIO.java
// Сокет и Селектор сконфигурированы для не блокированного
// Соединения с JabberServer.java
//
import java.net.*;
import java.nio.channels.*;
import java.util.*;
import java.io.*;

/**
* Цель: Показать как использовать селектор. Нет чтения/записи, просто
* показывается готовность к совершению операции.
*
* Алгоритм: -> Создаем селектор. -> Создаем канал -> Связываем сокет,
* ассоциированный с каналом, с -> Конфигурируем канал, как
* не блокирующий -> Регестрируем канал в селекторе. -> Вызываем метод select( ),
* чтобы он блокировал выполнение до тех пор, пока канал не будет готов. (как
* это предполагается методом select(long timeout) -> Получаем множество ключей,
* относящихся к готовому каналу для работы, основной интерес состоит в том,
* когда они зарегестрированя с помощью селектора. -> Перебираем ключи. -> Для
* каждого ключа проверяем, что соответствующий канал готов к работе, в которой
* он заинтересован. -> Если он готов, печатаем сообщение о готовности.
*
* Примечание: -> Необходим запущенный MultiJabberServer на локальной машине. Вы
* запускаете его и соединяетесь с локальным MultiJabberServer -> Он может стать
* причиной исключения в MultiJabberServer, но это исключение ожидаемо.
*/
public class NonBlockingIO <
public static void main ( String [] args ) throws IOException <
if ( args.length 2 ) <
System.out.println ( «Usage: java » ) ;
System.exit ( 1 ) ;
>
int cPort = Integer.parseInt ( args [ 0 ]) ;
int sPort = Integer.parseInt ( args [ 1 ]) ;
SocketChannel ch = SocketChannel.open () ;
Selector sel = Selector.open () ;
try <
ch.socket () .bind ( new InetSocketAddress ( cPort )) ;
ch.configureBlocking ( false ) ;
// Канал заинтересован в выполнении чтения/записи/соединении
ch.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT ) ;
// Разблокируем, когда готовы к чтению/записи/соединению
sel.select () ;
// Ключи, относящиеся к готовому каналу, канал заинтересован
// в работе, которая может быть выполненаin can be
// без блокирования.
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
// Если связанный с ключом канал готов к соединению?
// if((key.readyOps() & SelectionKey.OP_CONNECT) != 0) <
if ( key.isConnectable ()) <
InetAddress ad = InetAddress.getLocalHost () ;
System.out.println ( «Connect will not block» ) ;
// Вы должны проверить возвращаемое значение,
// чтобы убедиться, что он соединен. Этот не блокированный
// вызов может вернуться без соединения, когда
// нет сервера, к которому вы пробуете подключиться
// Поэтому вы вызываете finishConnect(), который завершает
// операцию соединения.
if ( !ch.connect ( new InetSocketAddress ( ad, sPort )))
ch.finishConnect () ;
>
// Если канал, связанный с ключом, готов к чтению?
// if((key.readyOps() & SelectionKey.OP_READ) != 0)
if ( key.isReadable ())
System.out.println ( «Read will not block» ) ;
// Готов ли канал, связанный с ключом, к записи?
// if((key.readyOps() & SelectionKey.OP_WRITE) != 0)
if ( key.isWritable ())
System.out.println ( «Write will not block» ) ;
>
>
finally <
ch.close () ;
sel.close () ;
>
>
> // /:

Как указано выше, вам необходимо создать канал, используя вызов метода open( ). SocketChannel.open( ) создает канал. Так как он наследован от AbstractSelectableChannel (DatagramChannel и SocketChannel), он имеет функциональность для регистрации себя в селекторе. Вызов метода регистрации совершает это. В качестве аргумента он принимает Селектор для регистрации канала, и события, которые интересны для этого канала. Здесь показано, что SocketChannel заинтересован в соединении, чтении и записи — поэтому в вызове метода регистрации указано SelectionKey.OP_CONNECT, SelectionKey.OP_READ и SelectionKey.OP_WRITE наряду с Селектором.

Цукерберг рекомендует:  Эффект загнутых уголков с использованием только CSS

Статический вызов метода Selector.select( ) наблюдает все каналы, зарегистрированные в нем, относительно тех событий, которые указаны (второй аргумент при регистрации). Вы можете иметь каналы, заинтересованные в нескольких событиях.

Следующий пример работает так же, как и JabberClient1.java, но использует Селектор.

//: TIEJ:X1:JabberClient1.java
// Очень простой клиент, которй просто посылает строки на сервер
// и читает строки, посылаемые сервером.
//
import java.net.*;
import java.util.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class JabberClient1 <
public static void main ( String [] args ) throws IOException <
if ( args.length 1 ) <
System.out.println ( «Usage: java JabberClient1 » ) ;
System.exit ( 1 ) ;
>
int clPrt = Integer.parseInt ( args [ 0 ]) ;
SocketChannel sc = SocketChannel.open () ;
Selector sel = Selector.open () ;
try <
sc.configureBlocking ( false ) ;
sc.socket () .bind ( new InetSocketAddress ( clPrt )) ;
sc.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT ) ;
int i = 0 ;
// По причине ассинхронной природы, вы не знаете
// когда чтение и запись закончены, поэтому вам необходимо
// следить за этим, переменная boolean written используется для
// переключения между чтением и записью. Во время записи
// отосланные назад символы должны быть прочитаны.
// Переменная boolean done используется для проверки, когда нужно
// прервать цикл.
boolean written = false, done = false ;
// JabberServer.java, которому этот клиент подсоединяется, пишет с
// помощью
// BufferedWriter.println(). Этот метод выполняет
// перекодировку в соответствии с кодовой страницей по умолчанию
String encoding = System.getProperty ( «file.encoding» ) ;
Charset cs = Charset.forName ( encoding ) ;
ByteBuffer buf = ByteBuffer.allocate ( 16 ) ;
while ( !done ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
sc = ( SocketChannel ) key.channel () ;
if ( key.isConnectable () && !sc.isConnected ()) <
InetAddress addr = InetAddress.getByName ( null ) ;
boolean success = sc.connect ( new InetSocketAddress (
addr, JabberServer.PORT )) ;
if ( !success )
sc.finishConnect () ;
>
if ( key.isReadable () && written ) <
if ( sc.read (( ByteBuffer ) buf.clear ()) > 0 ) <
written = false ;
String response = cs
.decode (( ByteBuffer ) buf.flip ()) .toString () ;
System.out.print ( response ) ;
if ( response.indexOf ( «END» ) != — 1 )
done = true ;
>
>
if ( key.isWritable () && !written ) <
if ( i 10 )
sc.write ( ByteBuffer.wrap ( new String ( «howdy » + i
+ ‘n’ ) .getBytes ())) ;
else if ( i == 10 )
sc.write ( ByteBuffer.wrap ( new String ( «ENDn» )
.getBytes ())) ;
written = true ;
i++;
>
>
>
>
finally <
sc.close () ;
sel.close () ;
>
>
> // /:

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

//: TIEJ:X1:MultiJabberServer1.java
// Имеет туж е семантику, что и многопоточный
// MultiJabberServer
//
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;

/**
* Сервер принимает соединения не блокирующим способом. Когда соединение
* установлено, создается сокет, который регистрируется с селектором для
* чтения/записи. Чтение/запись выполняется над этим сокетом, когда селектор
* разблокируется. Эта программа работает точно так же, как и MultiJabberServer.
*/
public class MultiJabberServer1 <
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
// Канал будет читать данные в ByteBuffer, посылаемые
// методом PrintWriter.println(). Декодирование этого потока
// байт требует кодовой страницы для кодировки по умолчанию.
String encoding = System.getProperty ( «file.encoding» ) ;
// Инициализируем здесь, так как мы не хотим создавать новый
// экземпляр кодировки каждый раз, когда это необходимо
// Charset cs = Charset.forName(
// System.getProperty(«file.encoding»));
Charset cs = Charset.forName ( encoding ) ;
ByteBuffer buffer = ByteBuffer.allocate ( 16 ) ;
SocketChannel ch = null ;
ServerSocketChannel ssc = ServerSocketChannel.open () ;
Selector sel = Selector.open () ;
try <
ssc.configureBlocking ( false ) ;
// Локальныйы адрес, на котором он будет слушать соединения
// Примечание: Socket.getChannel() возвращает null, если с ним не
// ассоциирован канал, как показано ниже.
// т.е выражение (ssc.socket().getChannel() != null) справедливо
ssc.socket () .bind ( new InetSocketAddress ( PORT )) ;
// Канал заинтересован в событиях OP_ACCEPT
SelectionKey key = ssc.register ( sel, SelectionKey.OP_ACCEPT ) ;
System.out.println ( «Server on port: » + PORT ) ;
while ( true ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey skey = ( SelectionKey ) it.next () ;
it.remove () ;
if ( skey.isAcceptable ()) <
ch = ssc.accept () ;
System.out.println ( «Accepted connection from:»
+ ch.socket ()) ;
ch.configureBlocking ( false ) ;
ch.register ( sel, SelectionKey.OP_READ ) ;
>
else <
// Обратите внимание, что не выполняется проверка, если
// в канал
// можно писать или читать — для упрощения.
ch = ( SocketChannel ) skey.channel () ;
ch.read ( buffer ) ;
CharBuffer cb = cs.decode (( ByteBuffer ) buffer.flip ()) ;
String response = cb.toString () ;
System.out.print ( «Echoing : » + response ) ;
ch.write (( ByteBuffer ) buffer.rewind ()) ;
if ( response.indexOf ( «END» ) != — 1 )
ch.close () ;
buffer.clear () ;
>
>
>
>
finally <
if ( ch != null )
ch.close () ;
ssc.close () ;
sel.close () ;
>
>
> // /:

Здесь приведена простейшая реализация Пула Нитей. В этой реализации нет полинга (занят-ожидает) нитей. Она полностью основана на методах wait( ) и notify( ).

//: TIEJ:X1:Worker.java
// Instances of Worker are pooled in threadpool
//
//
import java.io.*;
import java.util.logging.*;

public class Worker extends Thread <
public static final Logger logger = Logger.getLogger ( «Worker» ) ;
private String workerId;
private Runnable task;
// Необходима ссылка на пул нитей в котором существует нить, чтобы
// нить могла добавить себя в пул нитей по завершению работы.
private ThreadPool threadpool;
static <
try <
logger.setUseParentHandlers ( false ) ;
FileHandler ferr = new FileHandler ( «WorkerErr.log» ) ;
ferr.setFormatter ( new SimpleFormatter ()) ;
logger.addHandler ( ferr ) ;
>
catch ( IOException e ) <
System.out.println ( «Logger not initialized..» ) ;
>
>

public Worker ( String id, ThreadPool pool ) <
worker > threadpool = pool;
start () ;
>

// ThreadPool, когда ставит в расписание задачу, использует этот метод
// для делегирования задачи Worker-нити. Кроме того для установки
// задачи (типа Runnable) он также переключает ожидающий метод
// run() на начало выполнения задачи.
public void setTask ( Runnable t ) <
task = t;
synchronized ( this ) <
notify () ;
>
>

public void run () <
try <
while ( !threadpool.isStopped ()) <
synchronized ( this ) <
if ( task != null ) <
try <
task.run () ; // Запускаем задачу
>
catch ( Exception e ) <
logger.log ( Level.SEVERE,
«Exception in source Runnable task» , e ) ;
>
// Возвращает себя в пул нитей
threadpool.putWorker ( this ) ;
>
wait () ;
>
>
System.out.println ( this + » Stopped» ) ;
>
catch ( InterruptedException e ) <
throw new RuntimeException ( e ) ;
>
>

public String toString () <
return «Worker : » + workerId;
>
> // /:

Основной алгоритм:
while true:

  1. Проверить очередь задач.
  2. Если она пуста, подождать, пока в очередь будет добавлена задача.
    (вызов метода addTask( ) добавляет задачу и уведомляет очередь для разблокирования)
  3. Пробуем получить рабочую (Worker) нить из пула нитей.
  4. Если нет ни одной доступной нити, ожидаем в пуле нитей.
    (Когда нить освободится, она уведомит пул нитей для разблокировки)
  5. На этой стадии есть задачи в очереди и есть свободная рабочая нить.
  6. Делегируем задачу из очереди рабочей нити.

//: TIEJ:X1:ThreadPool.java
// Пул нитей, которые выполняют задачи.
//
import java.util.*;

public class ThreadPool extends Thread <
private static final int DEFAULT_NUM_WORKERS = 5 ;
private LinkedList workerPool = new LinkedList () ,
taskList = new LinkedList () ;
private boolean stopped = false ;

public ThreadPool () <
this ( DEFAULT_NUM_WORKERS ) ;
>

public ThreadPool ( int numOfWorkers ) <
for ( int i = 0 ; i )
workerPool.add ( new Worker ( «» + i, this )) ;
start () ;
>

public void run () <
try <
while ( !stopped ) <
if ( taskList.isEmpty ()) <
synchronized ( taskQueue ) <
// Если очередь пустая, подождать, пока будет добавлена
// задача
taskList.wait () ;
>
>
else if ( workerPool.isEmpty ()) <
synchronized ( workerPool ) <
// Если нет рабочих нитей, подождать, пока
// пока не появится
workerPool.wait () ;
>
>
// Запускаем следующую задачу из расписания задач
getWorker () .setTask (( Runnable ) taskList.removeLast ()) ;
>
>
catch ( InterruptedException e ) <
throw new RuntimeException ( e ) ;
>
>

public void addTask ( Runnable task ) <
taskList.addFirst ( task ) ;
synchronized ( taskList ) <
taskList.notify () ; // Если добавлена новая задача, уведомляем
>
>

public void putWorker ( Worker worker ) <
workerPool.addFirst ( worker ) ;
// Здесь может быть случай, когда вы будете иметь пул из 5 нитей,
// а будет требоваться больше. Это происходит тогда, когда требуется
// рабочая нить,
// но ее нет (свободной), тогда просто блокируем пул нитей.
// Это событие, при котором появляется свободная рабочая нить в пуле
// нитей
// Поэтому эта нить посылает уведомление и разблокирует
// нить ThreadPool, ожидающую пул нитей
synchronized ( workerPool ) <
workerPool.notify () ;
>
>

private Worker getWorker () <
return ( Worer ) workerPool.removeLast () ;
>

public boolean isStopped () <
return stopped;
>

public void stopThreads () <
stopped = true ;
Iterator it = workerPool.iterator () ;
while ( it.hasNext ()) <
Worker w = ( Worker ) it.next () ;
synchronized ( w ) <
w.notify () ;
>
>
> // Junit test

public void testThreadPool () <
ThreadPool tp = new ThreadPool () ;
for ( int i = 0 ; i 10 ; i++ ) <
tp.addTask ( new Runnable () <
public void run () <
System.out.println ( «A» ) ;
>
>) ;
>
tp.stopThreads () ;
>
> // /:

Следующий пример MultiJabberServer2.java использует пул нитей. Это шаблон Реактора. Как установлено выше, события отделяются от ассоциированных с ними действий. Пул нитей ассинхронно разделяет действия, ассоциированные с событиями. В системах масштаба предприятия такое разделение обычно достигается путем использования Системы Cообщений Java — Java Messaging System (JMS).

//: TIEJ:X1:MultiJabberServer2.java
// Семантика аналогична MultiJabberServer1, с использованием пула нитей.
//
import java.io.*;

class ServeOneJabber implements Runnable <
private SocketChannel channel;
private Selector sel;

public ServeOneJabber ( SocketChannel ch ) throws IOException <
channel = ch;
sel = Selector.open () ;
>

public void run () <
ByteBuffer buffer = ByteBuffer.allocate ( 16 ) ;
boolean read = false, done = false ;
String response = null ;
try <
channel.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE ) ;
while ( !done ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
if ( key.isReadable () && !read ) <
if ( channel.read ( buffer ) > 0 )
read = true ;
CharBuffer cb = MultiJabberServer2.CS
.decode (( ByteBuffer ) buffer.flip ()) ;
response = cb.toString () ;
>
if ( key.isWritable () && read ) <
System.out.print ( «Echoing : » + response ) ;
channel.write (( ByteBuffer ) buffer.rewind ()) ;
if ( response.indexOf ( «END» ) != — 1 )
done = true ;
buffer.clear () ;
read = false ;
>
>
>
>
catch ( IOException e ) <
// будет поймано Worker.java и залогировано.
// Необходимо выбросить исключение времени выполнения, так как мы не
// можем
// оставить IOException
throw new RuntimeException ( e ) ;
>
finally <
try <
channel.close () ;
>
catch ( IOException e ) <
System.out.println ( «Channel not closed.» ) ;
// Выбрасываем это, чтобы рабочая нить могла залогировать.
throw new RuntimeException ( e ) ;
>
>
>
>

public class MultiJabberServer2 <
public static final int PORT = 8080 ;
private static String encoding = System.getProperty ( «file.encoding» ) ;
public static final Charset CS = Charset.forName ( encoding ) ;
// Создаем пул нитей с 20 рабочими нитями.
private static ThreadPool pool = new ThreadPool ( 20 ) ;

public static void main ( String [] args ) throws IOException <
ServerSocketChannel ssc = ServerSocketChannel.open () ;
Selector sel = Selector.open () ;
try <
ssc.configureBlocking ( false ) ;
ssc.socket () .bind ( new InetSocketAddress ( PORT )) ;
SelectionKey key = ssc.register ( sel, SelectionKey.OP_ACCEPT ) ;
System.out.println ( «Server on port: » + PORT ) ;
while ( true ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey skey = ( SelectionKey ) it.next () ;
it.remove () ;
if ( skey.isAcceptable ()) <
SocketChannel channel = ssc.accept () ;
System.out.println ( «Accepted connection from:»
+ channel.socket ()) ;
channel.configureBlocking ( false ) ;
// Отделяем события и ассоциированное действие
pool.addTask ( new ServeOneJabber ( channel )) ;
>
>
>
>
finally <
ssc.close () ;
sel.close () ;
>
>
> // /:

Это минимальное обновления для JabberServer.java. Изначально, когда клиент посылает ‘END’, JabberServer не отправляет его назад. Эта версия JabberServer отсылает строку ‘END’ назад. Эти изменения были сделаны, чтобы упростить JabberClient1.java.

//: TIEJ:X1:JabberServer.java
// Очень простой сервер, который просто
// отсылает назад то, что получил от клиента.
//
import java.io.*;


public class JabberServer <
// Выбираем порт за пределами диапазона 1-1024:
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Started: » + s ) ;
try <
// Блокируем до возникновения соединения:
Socket socket = s.accept () ;
try <
System.out.println ( «Connection accepted: » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader (
socket.getInputStream ())) ;
// Вывод автоматически выталкивается PrintWriter’ом:
BufferedWriter out = new BufferedWriter ( new OutputStreamWriter (
socket.getOutputStream ())) ;
while ( true ) <
String str = in.readLine () ;
System.out.println ( «Echoing: » + str ) ;
out.write ( str, 0 , str.length ()) ;
out.newLine () ;
out.flush () ;
if ( str.equals ( «END» ))
break ;
>
// Всегда закрываем два сокета.
>
finally <
System.out.println ( «closing. » ) ;
socket.close () ;
>
>
finally <
s.close () ;
>
>
> // /:

Еще о работе с сетью

На самом деле есть очень много тем, касающихся сетевой работы, которые могут быть освещены в этой вводной статье. Сетевая работа Java также предоставляет четкую и всестороннюю поддержку для URL, включая обработчики протоколов для различных типов содержимого, которое может быть доступно на Интернет сайте. Вы можете найти полное и подробное описание других особенностей сетевого взаимодействия Java в книге Elliotte Rusty Harold «Java Network Programming» (O’Reilly, 1997).

Задача по проектированию чат-сервера

Условие задачи

Как бы вы подошли к проектированию чат-сервера? Предоставьте информацию о компонентах внутренней системы (backend), классах и методах. Перечислите самые трудные задачи, которые необходимо решить.

Решение

Разработка чат-сервера — огромный проект, команды из многих людей проводят месяцы и годы, пытаясь создать хороший чат-сервер. Вы должны сосредоточиться на отдельном аспекте задачи. Ваше решение не обязано точно отражать реальность, но должно давать хорошее представление о фактической реализации.

Мы займёмся основными аспектами управления пользователями и организацией общения: добавлением пользователя, созданием беседы, обновлением статуса и т. д. Для экономии времени и места мы не будем углубляться в сетевые аспекты задачи и не будем рассматривать вопросы передачи данных между клиентами и сервером.

Предполагается, что «дружба» взаимна: вы дружите со мной только в том случае, если и я дружу с вами. Наша чат-система будет поддерживать как групповые, так и приватные беседы. Мы не будем рассматривать голосовой чат, видеочат и пере­дачу файлов.

Какие конкретные операции должен поддерживать чат?

Мы приведем несколько примеров:

  • Вход и выход из чата;
  • Добавление запроса (отправка, приём и отклонение);
  • Обновление информации о статусе;
  • Создание приватных и групповых чатов;
  • Добавление новых сообщений в приватные и групповые чаты.

Это далеко не полный список. Если вам хватит времени, добавьте дополнительные действия.

Что можно узнать из этих требований?

Необходимо реализовать концепцию пользователей, статуса добавления запроса, статуса подключения и сообщений.

Какие базовые компоненты должны присутствовать в системе?

Система будет состоять из базы данных, клиентов и серверов. Мы не включаем эти части в объектно-ориентированный проект, но можем обсудить общее видение системы.

База данных будет использоваться для долгосрочного хранения данных, например списка пользователей и архивов чата. В большинстве случаев подойдет SQL-бaзa данных, но, если нам понадобится большая масштабируемость, можно использовать BigTable или другую аналогичную систему. Подробнее вы можете узнать в нашем разделе, посвящённом базам данных.

Для обмена данными между клиентами и серверами подойдет XML. Хотя его компактность оставляет желать лучшего, он наиболее удобен для восприятия как человеком, так и компью­тером. Использование XML также упрощает отладку приложения, а это имеет большое значение.

Примечание переводчика Ещё лучше для передачи данных подойдёт формат JSON.

Сервер будет состоять из множества компьютеров. Данные будут распределены между машинами, что требует «переключения» с одного устройства на другое. Возможно, некоторые данные придется перераспределять между машинами. Чтобы сократить время поиска, нужно избегать узких мест. Например, если аутентифика­цией пользователей занимается только одна машина, то её выход из строя закроет доступ к системе миллионам пользователей.

Какие основные объекты и методы должны поддерживаться системой?

Ключевые объекты нашей системы — пользователи, беседы и сообщения о статусах. Всё это можно реализовать в классе UserManager . Если бы мы уделяли больше вни­мания сетевым аспектам задачи или другим компонентам, вероятно, нам пришлось бы создать для них дополнительные объекты.

Метод receivedAddRequest класса User оповещает пользователя В о том, что пользо­ватель А запросил добавление его в список контактов. Пользователь В соглашается или отклоняет запрос (при помощи UserManager.approveAddRequest или rejectAddRequest ), а класс UserManager обеспечивает добавление пользователей в списки контактов двух пользователей.

Метод sentAddRequest класса User вызывается UserManager для добавления AddRequest в список запросов пользователя А. Поэтому последовательность операций должна выглядеть так:

  1. Пользователь А нажимает кнопку «добавить пользователя» в клиентской программе, запрос отправляется на сервер.
  2. Пользователь В вызывает requestAddUser(User В) .
  3. Этот метод вызывает UserManager.addUser .
  4. UserManager вызывает методы UserA.sentAddRequest и UserB.receivedAddRequest .

Это всего лишь один из возможных способов проектирования подобных взаимо­действий, но не единственный и даже не самый лучший.

Класс Conversation реализован как абстрактный, потому что все экземпляры Conversation должны относиться либо к классу GroupChat , либо к классу PrivateChat , а каждый из этих классов обладает собственной функциональностью.

Классы AddRequest и UserStatus — простые классы с минимальной функциональ­ностью. Их основное назначение — группировка данных, используемых другими классами.

Какие проблемы окажутся самыми трудными (или интересными)?

Попробуйте ответить на следующие вопросы:

Как определить активных пользователей?

Подключённый пользователь — не всегда активный пользователь. Если сеанс не был корректно завершён, сессия может «зависнуть», расходуя ресурсы сервера и искажая реальное количество активных пользователей. Как с достаточно большой точностью определить активность пользователей?

Что делать с информационными конфликтами?

Часть информации должна храниться в оперативной памяти, а часть — в базе данных. Что случится, если синхронизация между ними будет нарушена? Какую информацию считать правильной?

Что делать с возрастающим количеством пользователей?

Как на этапе проектирования заложить многократное увеличение количества пользователей?

Как защититься от DоS-атак?

Клиенты могут передавать данные на сервер, но что, если сервер попытаются «завалить» по­током запросов? Как предотвратить атаку?

10. Урок по Java. Сетевое программирование

В этом уроке мы посмотрим три примера из сетевого программирования в Java

Создадим первый проект, в нем класс Example

Наша программа выведет ip и имя хоста (на примере яндекса)

Это все конечно весело, давайте перейдем к реализации посложнее

В консоли Eclipse внизу вводите сообщение, на него увидите ответ.

Реализация UDP сервера и клиента на Java:

Перейдем к реализации TCP сервера и клиента:

Внизу Eclipse в консоли вводите сообщение, Вы увидите ответ.

создание удаленного сервера и подключение к нему через сокеты на java

нашел вариант реализации клиент-серверного взаимодействия посредством UDP на java. Сервер:

Проблема в том,что здесь взаимодействие идет через localhost и работает в рамках одного устройства. я уже несколько дней потратил на то,чтобы понять что туда передать вместо localhost,чтобы можно было запустить сервер на одном устройстве,А клиент на абсолютно другом,подключенному к другой сети,пробовал и local ip пихать и ip, который выдавали различные сайты. все не работает. подскажите пожалуйста UPD:покопавшись,я наткнулся на такую вещь как NAT и то,что я не подключаюсь напрямую к интернету,так что собственно как можно это обойти?

2 ответа 2

решил данный вопрос и решил поделиться,мало ли кого-то на что-то натолкнет. как сказал @Victor мой ip из вне был недоступен,так роутер предоставлял единый внешний ip для всех подключенных устройств,а мой ip был внутренним,не буду особо расписывать данный момент,т.к. могу что-то сказать не правильно(просто загуглите NAT). решил проблему подключением к интернету не через роутер,а напрямую воткнув кабель в ноутбук,таким образом я напрямую(!) подключился к интернету и ip моего ноутбука стал доступен из вне,что и позволило подключиться к моему серверу человеку из другого города=) свой ip можно узнать вбив ipconfig в командную строку(для Windows, для Unix не знаю) или проверив его на одном из множества сайтов. Если они совпадают,значит вы напрямую подключены к интернету и можно смело указывать данный ip вместо localhost. Если что-то написано не правильно-дополните в комментариях.

Ну, собственно, подключение будет идти по вашему ip (локальному, или внешнему), никакой магии в этом нет. Просто вместо localhost подставляете ip-адрес вашего сервера(например 192.168.1.30) и радуетесь жизни. другое дело, если у вас нет внешнего ip-адреса, например у вас локальная сеть предприятия. В таком случае достучаться до вашего ip из вне будет весьма проблемно, и есть повод задуматься об отдельном сервере для этого дела.

В коде проблем никаких нет. При общении в пределах локальной сети проблем быть так же не должно. Если же вам нужно настроить ваш роутер\сервер для доступа из вне, то увы, это не java, а настройки сети.

Всё ещё ищете ответ? Посмотрите другие вопросы с метками java клиент-сервер или задайте свой вопрос.

Похожие

Подписаться на ленту

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

дизайн сайта / логотип © 2020 Stack Exchange Inc; пользовательское содержимое попадает под действие лицензии cc by-sa 4.0 с указанием ссылки на источник. rev 2020.11.14.35433

G0Chat — бесплатный чат для корпоративных и локальных сетей

Сегодня без корпоративного чата почти не обойтись в организации или офисе.

Пересылка документов или просто переписка с сотрудниками — это главные задачи которые являются приоритетными для такого программного обеспечения как чаты и мессенджеры.

G0Chat — бесплатный кроссплатформенный чат для корпоративных и локальных сетей.

G0Chat — окно клиентской части программы

G0Chat имеет много опций и полезных настроек:

  • Это бесплатное программное обеспечение.
  • Кроссплатформенная программа — поддерживаются такие ОС как Windows и Linux.
  • Единичный вход для пользователя.
  • Передача файлов: передача файлов работает даже если оба клиента находятся за nat-ом.
  • Также в программе можно передавать картинки на лету.
  • Интересной особенностью является наличие ботов для чата.
  • Оффлайн сообщения будут доставлены, когда получатель подключится к чату.
  • Звуковые оповещения, оповещения в системном трее, анимация сообщений.
  • Шифрование и сжатие данных.
  • Каналы: возможность создания публичных и приватных каналов.
  • Передача аудио сигнала, возможность общения голосом, можно проводить аудио конференции с несколькими участниками.
  • Статусы пользователя.

Основные опции в меню настройки:

В поле с названием «IP Сервера» нужно ввести IP адрес компьютера, на котором запущен G0Chat сервер.

В программе есть возможность шаринга файлов — предоставление возможности просмотра и скачивания файлов для вех пользователей.

Окно расшаренных ресурсов пользователя.

Затем в клиентском окне программы переходим в главное меню «Общее» -> «Аккаунт» -> «Регистрация».

Заполняем появившуюся форму своими данными так, как на изображении и жмём кнопку «Регистрация».

Несколько слов о сервере G0Chat

Сам сервер легко администрируется. Всё что нужно — это установить сервер и запустить его на одном из компьютеров в вашей корпоративной сети.

Из окна с графическим пользовательским интерфейсом можно управлять работой сервера: настроить порт, управлять каналами, удалять пользователей при необходимости, также можно включить ведение логов.

Как сервер, так и клиент легко и быстро устанавливаются, потребуется только выбрать папку установки.

Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих