Статьи

JavaFX, сокеты и потоки: извлеченные уроки

При рассмотрении того, как машинно-зависимые приложения могут взаимодействовать с Java / JavaFX, JNI или собственный интерфейс Java , созданный именно для такой задачи, скорее всего, станет первым механизмом, который приходит на ум. Хотя JNI работает очень хорошо, спасибо, группа из нас в конечном итоге решила не использовать его для небольшого проекта, потому что, среди прочего:

  • Ошибки в вашей реализации JNI могут очень странно повредить виртуальную машину Java, что приведет к сложной диагностике.
  • JNI может быть трудоемким и утомительным, особенно если существует разный обмен между платформами Native и Java.
  • Для каждой поддерживаемой ОС / платформы необходимо будет создать и поддерживать отдельную реализацию JNI.

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

Розетки и нарезка резьбы

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

Для этого простого примера были предоставлены две реализации абстрактного класса GenericSocket , одна с именем SocketServer.java , другая с именем SocketClient.java . Основное различие между этими двумя классами заключается в типе сокета, который они используют. SocketServer.java использует java.net.ServerSocket ,   а SocketClient.java использует java.net.Socket, Соответствующие реализации содержат детали, необходимые для установки и удаления этих немного разных типов сокетов.

Рассекая Java Socket Framework

Если вы хотите использовать предоставляемую инфраструктуру сокетов Java с JavaFX, вам нужно понять этот очень важный факт:  JavaFX не является потокобезопасным, и все манипуляции с JavaFX должны выполняться в потоке обработки JavaFX . 1 Если вы разрешите приложению JavaFX взаимодействовать с потоком, отличным от основного потока обработки, возникнут непредсказуемые ошибки. Напомним, что класс GenericSocket создал поток чтения для обработки чтения сокетов. Чтобы избежать обработки не основного потока и его ошибок с нашими классами сокетов, необходимо внести несколько изменений.

[1] Украдено у JavaFX: разработка многофункциональных интернет-приложений — спасибо Джиму Кларку

Шаг 1: Определите ресурсы вне основного потока

Первым шагом к работе в поточно-ориентированном режиме является определение тех ресурсов в вашем Java-коде, которые находятся вне основного потока, к которым может потребоваться доступ JavaFX. Для нашего примера мы определяем два абстрактных метода, первый, onMessage () , вызывается всякий раз, когда строка текста читается из сокета. Код GenericSocket.java будет вызывать этот метод при обнаружении ввода сокета. Давайте посмотрим на код SocketReaderThread внутри GenericSocket , чтобы понять, что происходит.

    class SocketReaderThread extends Thread {

@Override
public void run() {
String line;
waitForReady();
/*
* Read from from input stream one line at a time
*/
try {
if (input != null) {
while ((line = input.readLine()) != null) {
if (debugFlagIsSet(DEBUG_IO)) {
System.out.println("recv> " + line);
}
/*
* The onMessage() method has to be implemented by
* a sublclass. If used in conjunction with JavaFX,
* use Entry.deferAction() to force this method to run
* on the main thread.
*/
onMessage(line);
}
}
} catch (Exception e) {
if (debugFlagIsSet(DEBUG_EXCEPTIONS)) {
e.printStackTrace();
}
} finally {
notifyTerminate();
}
}

Поскольку onMessage () вызывается из основного потока и внутри SocketReaderThread , в комментарии говорится, что для обеспечения обработки основного потока должна быть проведена некоторая дополнительная работа, о которой мы вскоре расскажем.

Наш второй метод onClosedStatus () вызывается всякий раз, когда изменяется состояние сокета (открыт или закрыт по любой причине). Эта абстрактная подпрограмма вызывается в разных местах в GenericSocket.java — иногда в главном потоке, иногда нет. Чтобы обеспечить безопасность потоков, мы будем использовать ту же технику, что и с onMessage () .

Шаг 2: Создайте интерфейс Java с вашими идентифицированными методами

После идентификации эти сигнатуры методов должны быть объявлены внутри интерфейса Java. Например, наша структура сокетов включает файл интерфейса SocketListener.java, который выглядит следующим образом:

   package genericsocket;

public interface SocketListener {
public void onMessage(String line);
public void onClosedStatus(Boolean isClosed);
}
Шаг 3. Создайте свой класс Java, реализуя определенный интерфейс

С помощью нашего SocketListener интерфейс , определенный, давайте шаг за шагом рассмотрим, как SocketServer класс реализуется внутри SocketServer .java . Одним из первых требований является импорт специального Java-класса, который позволит нам выполнять обработку основных потоков, что достигается следующим образом:

   import com.sun.javafx.runtime.Entry;

Далее идет объявление SocketServer . Обратите внимание, что в дополнение к расширению абстрактного класса GenericSocket он также должен реализовать наш интерфейс SocketListener :

   public class SocketServer extends GenericSocket implements SocketListener {

Внутри определения SocketServer объявлена ​​переменная с именем fxListener типа SocketListener :

       private SocketListener fxListener;

Конструктор для SocketServer должен включать ссылку на fxListener . Другие аргументы используются для указания номера порта и некоторых флагов отладки.

 

       public SocketServer(SocketListener fxListener,
int port, int debugFlags) {
super(port, debugFlags);
this.fxListener = fxListener;
}

Далее давайте рассмотрим реализацию двух методов, которые объявлены в интерфейсе SocketListener . Первый, onMessage () , выглядит так:

       /**
* Called whenever a message is read from the socket. In
* JavaFX, this method must be run on the main thread and
* is accomplished by the Entry.deferAction() call. Failure to do so
* *will* result in strange errors and exceptions.
* @param line Line of text read from the socket.
*/
@Override
public void onMessage(final String line) {
Entry.deferAction(new Runnable() {

@Override
public void run() {
fxListener.onMessage(line);
}
});
}

Как отмечается в комментарии, вызов Entry.deferAction () позволяет выполнять fxListener.onMessage () в главном потоке. Он принимает в качестве аргумента экземпляр класса Runnable и в своем методе run () вызывает функцию fxListener.onMessage () . Еще одним важным моментом является то , что уведомление OnMessage () «s Строка аргумент должен быть объявлен как окончательный .

В том же ключе метод onClosedStatus () реализован следующим образом:

       /**
* Called whenever the open/closed status of the Socket
* changes. In JavaFX, this method must be run on the main thread and
* is accomplished by the Entry.deferAction() call. Failure to do so
* will* result in strange errors and exceptions.
* @param isClosed true if the socket is closed
*/
@Override
public void onClosedStatus(final Boolean isClosed) {
Entry.deferAction(new Runnable() {

@Override
public void run() {
fxListener.onClosedStatus(isClosed);
}
});
}

Еще один Runnable запланирован через Entry.deferAction () для запуска fxlistener.onClosedStatus () в главном потоке. Опять же, логический аргумент onClosedStatus () также должен быть определен как final .

Доступ к платформе в JavaFX

С этой работой позади нас, теперь мы можем интегрировать фреймворк в JavaFX. Но прежде чем углубляться в детали, давайте покажем скриншоты двух простых приложений JavaFX, SocketServer и SocketClient, которые при совместном запуске могут отправлять и получать текстовые сообщения друг другу через сокет.

Эти программы JavaFX были разработаны в NetBeans и используют недавно анонсированный инструмент NetBeans JavaFX Composer . Вы можете нажать на изображения, чтобы выполнить эти программы через Java WebStart. Примечание: в зависимости от вашей платформы ваша система может запросить разрешение, прежде чем разрешить этим приложениям работать в сети. Исходный код для приложений JavaFX и каркас сокетов в виде проектов NetBeans можно скачать здесь .

Шаг 4: Интеграция в JavaFX

Чтобы получить доступ к платформе сокетов в JavaFX, вы должны реализовать класс SocketListener , созданный для этого проекта. Чтобы дать вам представление о том, как это делается с нашим приложением JavaFX SocketServer , вот некоторые выдержки из кода файла Main.fx проекта, в частности определение нашего класса ServerSocketListener :

public class ServerSocketListener extends SocketListener {
public override function onMessage(line: String) {
insert line into recvListView.items;
}
public override function onClosedStatus(isClosed : java.lang.Boolean) {
socketClosed = isClosed;
tryingToConnect = false;
if (autoConnectCheckbox.selected) {
connectButtonAction();
}
}
}

Избавляясь от всех подробностей, метод onMessage () поместит строку текста, прочитанную из сокета, в элемент управления JavaFX ListView, который отображается в пользовательском интерфейсе программы. Метод onClosedStatus () в первую очередь обновляет локальную переменную socketClosed и пытается повторно подключить сокет, если была выбрана опция autoconnect.

Чтобы продемонстрировать, как создается сокет, мы рассмотрим функцию connectButtonAction () :

var socketServer : SocketServer;

...

public function connectButtonAction (): Void {
if (not tryingToConnect) {
if (socketClosed) {
<b>socketServer = new
SocketServer(ServerSocketListener{},
java.lang.Integer.parseInt(portTextbox.text),
javafx.util.Bits.bitOr(GenericSocket.DEBUG_STATUS,
GenericSocket.DEBUG_IO));</b>
tryingToConnect = true;
<b>socketServer.connect();</b>
}
}
}

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

Чтобы завершить это, когда пользователь нажимает кнопку «Отключить», вызывается функция disconnectButtonAction () . При вызове он разрушает экземпляр SocketServer .

   function disconnectButtonAction (): Void {
tryingToConnect = false;
socketServer.shutdown();
}

Вывод

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

С http://blogs.sun.com/jtc/