WebSocket-сервер на Java с использованием Netty

от автора

Вступление

Здравствуйте. Некоторое время назад я решил сделать своё web-приложение которое должно быстро обмениваться данными с сервером.
Вопрос был о том, как же сделать качественную и быструю скорость передачи данных между web-приложением и сервером.
Можно было бы использовать Flash но я не стал, т.к. моё web-приложение должно корректно запускаться на iДевайсах. Тогда мой взор упал на long-polling, проведя несколько тестов, я понял, что драгоценные секунды теряются на открытие нового соединения и ожидание ответа от сервера. Немного позже я узнал, совершенно случайно, что есть еще некий WebSocket, который как раз решил бы все мои проблемы с передачей данных.

Пост про маленькую победу над сложной задачей.

Может есть что-то готовое?

Потратив некоторое время на поиски готовых решений в гугл’е, я понял, что есть несколько способов реализации поставленной задачи при использовании сервлетов:
1) JWebSocket
2) Jetty ( Вы можете посмотреть решение тут: тык )
3) Netty

Я не буду говорить ничего о JWebSocket, потому что я не смог с ним разобраться (да и не хотелось, странный он, не верю я ему). Готовое решение на Jetty, с хабра, было бы очень кстати, если бы не устарело, по видимому разработчики решили что-то с чем-то объединить и нужные классы оказались перемещены/переименованы/удалены (нужное подчеркнуть).
Надо сказать, что Java для меня очень новый язык и после таких языков как js и php давался мне давольно легко. В общем-то это мой первый опыт использования языка, не для создания окошек с кнопочками, полями и ‘Hello World’, а для чего-то более серьёзного и сложного.

Раз нет, значит будет

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

    ExecutorService bossExec = new OrderedMemoryAwareThreadPoolExecutor(1, 400000000, 2000000000, 60, TimeUnit.SECONDS);     ExecutorService ioExec = new OrderedMemoryAwareThreadPoolExecutor(3, 400000000, 2000000000, 60, TimeUnit.SECONDS);                  ServerBootstrap networkServer = new ServerBootstrap(new NioServerSocketChannelFactory(bossExec, ioExec, 3));      networkServer.setOption("connectTimeoutMillis", 10000);     networkServer.setPipelineFactory(new WebSocketServerPipelineFactory());     Channel channel = networkServer.bind(new InetSocketAddress(address, port)); 

В документации Netty используется Executors.newCachedThreadPool(), он зачем-то поднимает аж 9 потоков выполнения, как по мне это неоправданная трата ресурса. Я решил ограничиться 3мя потоками и использовал OrderedMemoryAwareThreadPoolExecutor().

Затем необходимо обрабатывать подключения и пакеты, для этого назначаются (см. документацию как, класс WebSocketServerPipelineFactory) четыре обработчика, три из них стандартные из пакетов Netty (HttpRequestDecoder, HttpChunkAggregator, HttpResponseEncoder) и один мы должны описать самостоятельно.
Обработчик WebSocketServerHandler наследуется от класса SimpleChannelUpstreamHandler из Netty. Необходимо переопределить метод messageReceived под нужды, а нужды таковы:
1) Обрабатывать запросы по протоколу http://
2) Обрабатывать запросы по протоколу ws://

Для сего пишем следующее:

    if (msg instanceof HttpRequest) {         handleHttpRequest(ctx, (HttpRequest) msg);     }else if (msg instanceof WebSocketFrame) {         handleWebSocketFrame(ctx, (WebSocketFrame) msg);     } 

Метод handleHttpRequest должен корректно подключать клиентов по протоколу websocket и только через GET, а так же отбрасывать всех любопытных по http, показывая им ошибку, например "505 HTTP Version Not Supported".

    sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, HTTP_VERSION_NOT_SUPPORTED)); 

Обрабатывать запрос на подключение ws будем примерно так:

    if(req.getMethod() == GET && req.getUri().equals(WEBSOCKET_PATH) && Values.UPGRADE.equalsIgnoreCase(req.getHeader(CONNECTION)) && WEBSOCKET.equalsIgnoreCase(req.getHeader(Names.UPGRADE))) {         WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, false);         handshaker = wsFactory.newHandshaker(req);                      if(handshaker == null){             wsFactory.sendUnsupportedWebSocketVersionResponse(ctx.getChannel());         }else{             handshaker.handshake(ctx.getChannel(), req).addListener(WebSocketServerHandshaker.HANDSHAKE_LISTENER);         }         return;     }     //Здесь должен быть ответ с ошибкой HTTP_VERSION_NO.... 

Обуславливаем это тем, что браузер присылает методом GET на наш WEBSOCKET_PATH заголовок Upgrade: websocket. Далее начинается процесс приветствия (handshake), который сам отправляет клиенту верные заголовки и в итоге переключает протокол с http на ws открывая постоянное соединение.
В документации метод handleWebSocketFrame проверяет не закрыто ли соединение, отвечает на пакет ping и проверяет текст ли пришел, после чего возвращает принятую строку назад клиенту.
Получаем строчку, которую прислал клиент:

    String clietnMsg = ((TextWebSocketFrame) frame).getText(); 

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

Заключение

Это была моя первая личная победа над сложной задачей реализации WebSocket сервера, хоть я использовал документацию Netty, кроме этого я так же читал другие ресурсы (в том числе хабр).
Надеюсь, что теперь любой желающий сможет сделать свой WebSocket сервер на Java с использованием Netty.

Постскриптум

У меня еще нет репорезитория в git, по этому предлагаю исходный код по старинке с моего сервера, зеркало на Народе

Интересное по теме:
Сервер на python для websockets
Поддержка реверс-проксирования Web Sockets в Nginx
Двунаправленный асинхронный обмен данными в веб-приложениях

ссылка на оригинал статьи http://habrahabr.ru/post/165791/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *