Всем привет.
Отчитываюсь по мотивам
этой веткиАвтор: Дм.Григорьев
Дата: 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>>