Последнее изменение: 1 сентября 2007г.

Реализация обработчика сетевого протокола

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

Вот о чем пойдет речь:

* * *

Итак,

Предыстория

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

Вот тут и выползла проблема. Если 250 узлов графа разложить по определенному алгоритму, а потом втиснуть в окошко пусть даже размером 500x500 – размеры этих узлов будут... как бы это сказать помягче?.. ну, небольшими. Где-нибудь 5х5 максимум. А чаще получалось так, что в них даже мышкой трудно попасть, не то, чтобы рассмотреть, что на них нарисовано и написано. А именно это и было самым важным, ибо система предназначалась для анализа и представления знаний. И у каждого объекта было название и иконка, определяемая типом объекта.

Решение напрашивалось само собой. Сделать всплывающую подсказку (tooltip). Тем более, что Java умеет интерпретировать текст подсказки в формате HTML. Но... Ладно, с текстом никаких проблем. А как быть с иконкой? Ее ведь надо указать в фрагменте HTML-текста этой подсказки. И загружаться она будет автоматически, где-то в недрах swing. То есть – в HTML-тексте надо указать графический файл, который Java сможет загрузить и отобразить. Однако формат SVG стандартная библиотека не понимает. Да, я могу сгенерировать файл сам, используя загруженные SVG-изображения, но... Куда его положить, чтобы указать в тексте подсказки URL? Ладно бы это было обычное приложение, но вся система была написана в виде апплета...

В общем, в тот момент это был тупик. Мы, конечно, выкрутились, сделали растровые копии всех векторных иконок и использовали их. Но это было некрасиво. А я не люблю некрасивых решений. И потому я продолжал искать выход.

Через какое-то время мне пришла в голову интересная мысль. А что, если указать в URL этого изображения нестандартный протокол и написать для него обработчик? Тогда все произойдет естественным образом, и я сам буду отвечать за то, какие данные выдавать.

Мысль здравая. Однако, ее реализация натолкнулась на банальный недостаток опыта. Я несколько запутался в документации, задача мне представлялась каким-то монстром. Реализация фабрик, обработчиков, прописывание свойств... Одним словом, я эту задачу отложил до лучших времен. Было это три года назад.

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

Итак, предыстория окончена, перейдем к делу.

URL, протоколы и обработчики

Обращение к ресурсам в Java осуществляется через класс java.net.URL. Этот класс представляет собой реализацию понятия Uniform Resource Locator. Его синтаксис определен RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax. И одной из частей URL является протокол.

Протоколов существует великое множество. Более чем уверен, что вы имели дело с http и ftp. Существуют также nntp, gopher, mailto и много других. Все они отличаются друг от друга – форматом обмена данными прежде всего. Возникает резонный вопрос: каким образом класс java.net.URL обрабатывает все эти протоколы?

Ответ простой. А никаким! java.net.URL вообще этим не занимается. Обработка протокола – это конкретика. И занимается этим соответствующий класс. Или классы. java.net.URL только обеспечивает загрузку соответствующего обработчика и передает ему управление всякий раз, когда это необходимо.

Итак, существуют два абстрактных класса, которые использует java.net.URLjava.net.URLStreamHandler и java.net.URLConnection. Первый из них отвечает за соединение с соответствующим ресурсом (в версии Java 5.0 может использовать прокси-сервер). Второй – за непосредственный обмен с ресурсом и получение данных. Собственно, эти два класса и надо реализовать для реализации поддержки протокола.

Однако, кроме реализации обработчика надо еще и дать понять классу java.net.URL, где этот обработчик искать. Об этом – в следующей части.

Регистрация обработчика

Каким образом java.net.URL вообще ищет обработчик для протокола?

Во-первых, проверяется наличие реализации интерфейса java.net.URLStreamHandlerFactory. Этот интерфейс содержит всего один метод, возвращающий реализацию java.net.URLStreamHandler, соответствующую переданному ему протоколу.

Недостаток этого способа в том, что реализация java.net.URLStreamHandlerFactory может быть установлена только один раз. Соответственно, совсем не факт, что нам удасться установить собственную.

Во-вторых, если реализация java.net.URLStreamHandlerFactory не найдена, или если она возвращает null для переданного ей протокола, делается попытка прочитать системное свойство java.protocol.handler.pkgs. Это свойство содержит список пакетов, в которых находятся обработчики протоколов. Разделителем в этом списке является '|'. Для каждого имени пакета, указанного в этом списке, делается попытка загрузить класс с именем <имя пакета>.<имя протокола>.Handler.

Если класс не обработчика не найден в указаных пакетах, или свойство не установлено – обработчик ищется в системном пакете. Его имя – sun.net.www.protocol. Соответственно, класс обработчика для протокола http имеет имя sun.net.www.protocol.http.Handler, для ftpsun.net.www.protocol.ftp.Handler и т.д. Гарантированно существование обработчиков для протоколов http, https, ftp, file и jar.

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

Именно этот способ мы и будем использовать для регистрации обработчика. Имя пакета – ru.skipy.net.protocol, его надо указать в системном свойстве java.protocol.handler.pkgs. Имя протокола – jarres. Соответственно, имя класса обработчика будет ru.skipy.net.protocol.jarres.Handler.

Теперь на очереди...

Реализация обработчика

Для начала хочу сказать пару слов. Прежде всего, это иллюстрация именно реализации протокола. И именно на этом заострено внимание. Я не хотел перегружать пример, и делать генерацию изображения на лету (собственно, то, из чего выплыла задача) я не стал. Это можно сделать, к примеру, с помощью Java Image IO (javax.imageio.*), и проблем тут особых возникнуть не должно.

А потому – изображение я загружаю из jar-файла, как ресурс. В принципе, я могу делать это любым другим способом, суть от этого не меняется. Собственно, отсюда и название протокола – jarres (JAR RESource).

Итак, начнем с реализации URLStreamHandler. Роль этого класса – он "знает", как создать соединение с ресурсом, на который ссылается переданный его методу openConnection экземпляр класса java.net.URL. В нашем случае этот метод только создает экземпляр нашего же наследника URLConnection JarResourceURLConnection. Как я уже упоминал, имя этого класса должно оканчиваться на jarres.Handler, а начало может быть произвольным (его просто надо указать в системном свойстве), в нашем случае – ru.skipy.net.protocol:

package ru.skipy.net.protocol.jarres;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

/**
 * URLStreamHandler for <code>jarres</code> protocol
 *
 * @author Eugene Matyushkin
 * @version 1.0
 */
public class Handler extends URLStreamHandler {

    protected URLConnection openConnection(URL url) throws IOException {
        return new JarResourceURLConnection(url);
    }

}

Как видите – все предельно просто. Поэтому перейдем к непосредственно JarResourceURLConnection. Во-первых, мы обязаны реализовать его метод connect, по той простой причине, что он абстрактный. Этот метод должен начинать обмен данными с ресурсом (посылать запрос, если это необходимо) и подготавливать чтение данных. Далее, если мы хотим читать данные, то нам надо реализовать getInputStream, потому как имеющаяся реализация просто бросает исключение UnknownServiceException.

Итак, JarResourceURLConnection выглядит следующим образом (несущественные методы и комментарии опущены):

package ru.skipy.net.protocol.jarres;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;

/**
 * URLConnection implementation for <code>jarres</code> protocol.
 *
 * @author Eugene Matyushkin
 * @version 1.0
 */
public class JarResourceURLConnection extends URLConnection {

    private String resourcePath;
    private boolean connected = false;
    private InputStream is = null;

    JarResourceURLConnection(URL url) {
        super(url);
        resourcePath = url.getPath();
    }

    public synchronized void connect() throws IOException {
        if (connected) return;
        byte data[] = readData();
        is = (data == null) ? null : new ByteArrayInputStream(data);
        connected = true;
    }

    private byte[] readData() throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        InputStream resourceStream = getClass().getResourceAsStream(resourcePath);
        if (resourceStream == null)
            return null;
        DataInputStream dis = new DataInputStream(resourceStream);
        byte[] buffer = new byte[16384];
        while (true) {
            int readed = dis.read(buffer);
            if (readed == -1)
                break;
            if (readed > 0)
                baos.write(buffer, 0, readed);
        }
        return baos.toByteArray();
    }

    public synchronized InputStream getInputStream() throws IOException {
        connect();
        if (is == null) throw new FileNotFoundException(resourcePath);
        return is;
    }

}

Небольшие комментарии. В методе connect читаются данные (если есть, что читать, а то ведь url может быть неправильным). Если данные прочитаны – создается BytaArrayInputStream.

Метод getInputStream возвращает поток, из которого клиентский код будет читать данные. Если данные прочитаны быть не могут – выбрасывается исключение FileNotFoundException.

Хочу обратить внимание на вызов connect в методе getInputStream. Теоретически клиентский код должен явно вызывать connect, и только после этого – getInputStream. Во всяком случае именно так я понимаю документацию. Практически же этого не происходит. Если вы закомментируете этот вызов, изображение не будет появляться на всплывающей подсказке.

Собственно, это всё. Реализация окончена. Осталось всё это только запустить. Для этого есть маленький тестовый пример. Весь его код я приводить не буду. Единственное, что там интересно – это обращение к ресурсу, в html-содержимом подсказки:

public static final String TOOLTIP_TEXT =
        "<html><center><b>&nbsp;&nbsp;IMAGE below!&nbsp;&nbsp;</b><br>" +
        "<img src=\"jarres:///resources/images/vzhik2.gif\"></center></html>";

Как видите, в url опущено имя хоста. Оно должно следовать за 'jarres://'. В данном случае оно не используется, однако его можно было бы использовать, если бы ресурсы, например, хранились в нескольких zip-файлах. Тогда имя хоста можно было бы расценивать как имя файла, из которого должен быть загружен ресурс. Оставшаяся часть url – '/resources/images/vzhik2.gif' – как раз и представляет собой путь к ресурсу, который используется для его загрузки.

Запуск примера

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

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

<jvmarg value="-Djava.protocol.handler.pkgs=ru.skipy.net.protocol"/>

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

Теоретически это свойство вообще можно устанавливать в приложении – через System.setProperty(String,String). Однако я бы не рекомендовал этого делать. Во-первых, это совершенно неочевидно. И если не будет отражено в документации, то пользователь этого кода может с чистой совестью установить это свойство в то, что нужно ему. Потом это значение будет переписано – и разработчик тихо сойдет с ума, пытаясь понять, почему его код не работает. А если даже это описать в документации... Общеизвестно, что документация читается в самую последнюю очередь. А иногда не читается принципиально.

А во-вторых – при некоторых обстоятельствах менеджер безопасности может не дать установить собственное свойство. Что повлечет за собой неработоспособность вашего кода.

* * *

Вот и всё. Как видите, ничего сложного в данном процессе нет. Надеюсь, кому-нибудь это поможет в его собственных задачах.

Спасибо за внимание! Если я где-то неправ – пишите!