Подробно отчитываюсь: Comet for Java+Flash: Tomcat vs Jetty.
От: Дм.Григорьев  
Дата: 15.02.08 21:24
Оценка: 46 (9)
Всем привет.

Отчитываюсь по мотивам этой ветки
Автор: Дм.Григорьев
Дата: 17.01.08
. Итак, после получения ссылки на описание технологииComet, задача была переформулирована так: сделать полнодуплексное соединение с Flash-клиентом, используя Comet с и минимумумом геморроя. Геморроя, к сожалению, получился далеко не минимум, а в сумме недели три, если не больше. Надеюсь, нижеизложенное кому-нибудь сократит этот путь, полный боли и разочарований.




В двух словах:

1. Jetty 6.1.7 — дерьмо корявое (ИМХО, разумеется ИМХО ).
2. Tomcat 6.0.14 — рулит, но не без глюков (которые, разумеется, в документации не описаны ), пример в дистрибутиве не полнодуплексный и не рабочий. Ниже я кратко описываю, что и как.




Jetty 6.1.7

Самый нижний и тривиальный уровень, называемый Continuations, — это грязный хак над стандартным Servlet API, который может быть использован для отдачи контента с сервера, но не с клиента.

Вторая альтернатива — реализация протокола Bayeux, который (сам протокол) тоже заточен под одностороннюю передачу, и что ещё хуже — под Ajax. Формат сообщения JSON (JavaScript Object Notation), а мне мечталось XML. Кроме того, пример cometd в дистрибутиве инстанциирует внедрённый контейнер, вместо того, чтобы гладко ложиться в виде war-файла в стандартный сервер.

Короче, на этом месте я понял, что становиться экспертом по исходникам и настройке Jetty мне совершенно не улыбается, и я запоздало переключился на Tomcat 6, который мне тоже успели посоветовать.




Tomcat 6.0.14

Эти ребята, вместо грязных хаков над стандартным Servlet API, поступили мудро: расширили его. Путём примешивания к сервлету интерфейса CometProcessor и реализации метода event(), обычный сервлет превращается в Comet-сервлет, и вместо метода service() теперь у него будет всегда вызываться метод event(). Общий жизненный цикл соединения описан по вышеприведённый ссылке верно. А теперь ньюансы.



1. В файле CATALINA_HOME/conf/server.xml нужно вместо стандартного коннектора на порт 8080 нужно прописать следующее:

<!--Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /-->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" connectionTimeout="20000" redirectPort="8443" />




2. И сервер, и клиент должны использовать Content-Encoding: chunked! (см. RFC 2616, section 3.6.1)

Сервер генерирует chunks сам, при условии, что коннект не закрывается до того, как сервер успевает отдать заголовки. Если же коннект закрывается раньше, сервер вычисляет размер ответа в буфере и вместо chunked-encoding отдаёт заголовок Content-Length. Это требование HTTP/1.1 — либо chunked-encoding, либо Content-Length. Клиент должен это учитывать. В моих тестовых примерах (чат), при маленьких размерах передаваемых с сервера пакетов данных, каждый chunk умещается в одном IP-пакете, соответственно данные IP-пакета выглядели так (я побил на строчки для удобочитаемости):

1d
\r\n
dimgel: Hello world!!! There?
\r\n


Сначала идёт 16-ричное строковое представление длины данных (переменное количество байт). Затем перевод строки CRLF. Затем собственно данные (любые, в т.ч. двоичные). Завершает chunk снова перевод строки CRLF. Поле длины учитывает только длину данных, без переводов строк и самого поля длины.

На Flash-клиенте, всю работу с chunks приходится делать вручную. И вот тут первый трабл: Tomcat ожидает завершающий \r\n в начале следующего пакета, а не в конце текущего! Иными словами, с Flash-клиента я отдаю данные следующим образом:

private flash.net.Socket _socket;

private function onSocketConnect(e: Event): void {
    // ATTENTION! Tomcat 6.0.14 expects chunk tail CRLF before next chunk, not after current one. :/
    // For this reason, second CRLF which delimits headers from body is sent by sendImp().
    var s = "GET " + _uri + " HTTP/1.1" +
            "\r\nHost: " + _host + 
            "\r\nTransfer-Encoding: chunked" +
            "\r\n";
    _socket.writeUTFBytes(s);
    sendImp(PACKET_TYPE_SESSION, sessionId, ...);
}

private function sendImp(type: int, data: ByteArray, ...): void {
    var chunkSize: int = data.length + ...;
    var chunkSizeHex: String = new Number(chunkSize).toString(16);
    
    // ATTENTION! Tomcat 6.0.14 expects chunk tail CRLF before next chunk, not after current one. :/
    _socket.writeUTFBytes("\r\n");
    _socket.writeUTFBytes(chunkSizeHex + "\r\n");
    ...
    _socket.writeBytes(data);
    _socket.flush();
}


В данном примере я в качестве первого запроса сразу же отдаю сохранённый код сессии (своего рода кука; использовать браузерные куки во флеше я избегаю).

Кстати, если вы ошибётесь и будете передавать с клиента неправильно сформированные chunks (ну, например, поле длины неверно укажете), на сервере из ServletInputStream сначала полезет мусор, а уж потом вылетит IOException (и статус 500, соответственно), затем при следующей попытке повторного подключения клиента вылетит ещё одно исключение со статусом 500, и только при второй попытке сервер наконец подключит. В общем, сказка.



3. На стороне сервера вообще ничего не будет работать, если в методе CometProcessor.event(CometEvent event) не вызвать event.setTimeout(5000). Что этот метод делает и зачем, я так и не понял. Число я взял практически с потолка, а точнее из чужого примера, ссылку на который я дам в конце. Итак, на серверной стороне мой метод event() выглядит следующим образом:

public final void event(CometEvent event) throws IOException, ServletException {
    EventType t = event.getEventType();
    EventSubType tt = event.getEventSubType();
    HttpServletRequest request = event.getHttpServletRequest();
    HttpServletResponse response = event.getHttpServletResponse();

    log("event(): " + t.name() + ", " + (tt == null ? "null" : tt.name()) + ", " + response);

    event.setTimeout(5000);

    switch (t) {
        case BEGIN:
            connected(request, response);
            received(request, response);
            break;
        case READ:
            received(request, response);
            break;
        case END:
        case ERROR:
            if (tt != EventSubType.TIMEOUT) {
                disconnected(event, response);
            }
    }
}


Метод received(), вызываемый в ветке BEGIN, в моём случае читает запрос PACKET_TYPE_SESSION и отдаёт ответ с обновлённым кодом сессии. Этим я, кроме обмена "куками" для восстановления ранее прерванной сессии, убиваю ещё одного зайца: форсирую chunked-coding в ответе сервера. Хочу также заметить, что повторная отдача кода сессии не требуется до тех пор, пока соединение открыто.






Ещё раз спасибо всем ответившим в той самой ветке, ссылку на которую я давал в начале этого самого сообщения. Особая благодарность выражается Гуглу, в котором я (правда, далеко не сразу, а только когда совсем уже отчаялся) нашёл-таки вот это обсуждение, где приведён (якобы) работающий пример сервлета и рабочая ссылка на работающего тестового клиента, с которых, я, собственно, всё и подсмотрел.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
http://dimgel.ru/lib.web — thin, stateless, strictly typed Scala web framework.
Re: Подробно отчитываюсь: Comet for Java+Flash: Tomcat vs Je
От: Дм.Григорьев  
Дата: 15.02.08 23:16
Оценка:
Кстати, если chunks большие, собирать их на клиенте нужно тоже ручками. Т.е. и chunk может быть порван на несколько событий onData, и пакет может быть порван на несколько chunks. На сервере обработку полученных с клиента chunks выполняет tomcat, так что ручками приходится собирать только собственные пакеты. Стало быть, мой формат пакета также содержит поле длины.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
http://dimgel.ru/lib.web — thin, stateless, strictly typed Scala web framework.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.