Heroku.com: как получить реализацию Websockets на java (native подход)
Для реализации одной идеи возникла потребность разместить в интернете где-нибудь приложение с поддержкой Websockets, которое будет доступно из любого места. Главное условие — бесплатно. Ранее я уже делал вещание с камеры на websocket и java. Этот пример с небольшими изменениями я планировал попробовать разместить в глобальной сети. В качестве целевой площадки выбор пал на Heroku.com.
На heroku есть описание того, как сделать Websockets на Java, но пример для традиционного программиста на java выглядит странно, поэтому было решено его не использовать, а пойти традиционным путём с war.
https://devcenter.heroku.com/articles/play-java-websockets
На сайте есть два примера. Один для Jetty, другой для Tomcat.
Tomcat — https://devcenter.heroku.com/articles/java-webapp-runner
Jetty — https://devcenter.heroku.com/articles/deploy-a-java-web-application-that-launches-with-jetty-runner
Оба примера очень похожи, суть которых в использовании специального jar, который запускает веб-контейнер для war файла.
Последние версии Jetty и Tomcat реализуют JSR 356, который описывает стандартный интерфейс для реализации WebSockets на java. Поэтому было решено подготовить тестовое приложение для экспериментов с использованием ServerEndpoint аннотации. Оно прекрасно работало на локальном сервере, но попытка запустить его с помощью runner не увенчалась успехом. Runner для Jetty и Tomcat просто не видели аннотаций, даже для сервлетов. Но они прекрасно обрабатывали web.xml.
До JSR 356 веб-серверы имели свои собственные реализации WebSockets, которые работали на основе сервлетов. Поэтому такой сервлет можно прописать в web.xml и runner его увидит. Попытка с Jetty не увенчалась успехом, а вот эксперимент с Tomcat 7 получился.
Сперва нужно создать с помощью Maven Dynamic Web Project:
mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp
После этого нужно добавить runner в pom.xm
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.3</version> <executions> <execution> <phase>package</phase> <goals><goal>copy</goal></goals> <configuration> <artifactItems> <artifactItem> <groupId>com.github.jsimone</groupId> <artifactId>webapp-runner</artifactId> <version>7.0.57.2</version> <destFileName>webapp-runner.jar</destFileName> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin>
Тут используется версия 7.057.2, чтоб запускать war с использованием Tomcat 7.
Дальше подготовим классы и файлы для нашего примера:
PingServlet.java
package info.privateblog.ping; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import org.apache.catalina.websocket.StreamInbound; import org.apache.catalina.websocket.WebSocketServlet; public class PingServlet extends WebSocketServlet { @Override protected StreamInbound createWebSocketInbound(String s, HttpServletRequest httpServletRequest) { return new ProxySocketConnection(); } }
PingSocketConnection.java
package info.privateblog.ping; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.util.concurrent.ArrayBlockingQueue; import org.apache.catalina.websocket.MessageInbound; import org.apache.catalina.websocket.WsOutbound; public class PingSocketConnection extends MessageInbound { public static ArrayBlockingQueue<WsOutbound> connections = new ArrayBlockingQueue<WsOutbound>(100); private WsOutbound outbound; @Override protected void onTextMessage(CharBuffer charBuffer) throws IOException { System.out.println("onTextMessage"); broadcast(charBuffer.toString()); } @Override protected void onOpen(WsOutbound outbound) { System.out.println("onOpen"); this.outbound = outbound; connections.add(outbound); for (int i = 0; i < 30; i++) { try { outbound.writeTextMessage(CharBuffer.wrap("Ping: " + i)); } catch (IOException e1) {} try { Thread.currentThread().sleep(500); } catch (InterruptedException e) {} } } @Override protected void onClose(int status) { System.out.println("onClose"); connections.remove(this.outbound); } private void broadcast(String message) { for (WsOutbound connection : connections) { try { CharBuffer buffer = CharBuffer.wrap(message); connection.writeTextMessage(buffer); } catch (IOException e) { e.printStackTrace(); } } } @Override protected void onBinaryMessage(ByteBuffer arg0) throws IOException { System.out.println("onBinaryMessage"); } }
web.xml
... <servlet> <servlet-name>Ping</servlet-name> <servlet-class>info.privateblog.ping.PingServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Ping</servlet-name> <url-pattern>/ping</url-pattern> </servlet-mapping> ...
index.html
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Index</title> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script> </head> <body> <div id="ping"></div> <script type="text/javascript"> $(function() { var loc = window.location, new_uri; if (loc.protocol === "https:") { new_uri = "wss:"; } else { new_uri = "ws:"; } new_uri += "//" + loc.host; new_uri += loc.pathname + "/ping"; var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket var dateSocket = new WS(new_uri) var receiveEvent = function(event) { $("#ping").html("Last ping: "+event.data); } dateSocket.onmessage = receiveEvent; }) </script> </body> </html>
Теперь можно попробовать собрать локально
mvn package
и запустить
java -jar target/dependency/webapp-runner.jar target/*.war
Если приложение запустится на порту 8080, то можно переходить к следующему шагу — размещение на сервере Heroku.
Для начала нужно в основной директории проекта создать файл Procfile для запуска приложения
web: java $JAVA_OPTS -jar target/dependency/webapp-runner.jar —port $PORT target/*.war
Добавим проект в репозиторий
git init
git add .
git commit -m «Ready to deploy»
Создадим приложение в Heroku и отправим код на сервер
heroku create
git push heroku master
если компиляции прошла успешно, то можно запустить приложение
heroku open
У меня приложение сразу не запустилось, поэтому пришлось ещё выполнять
heroku scale web=1
Полезные команды для heroku:
heroku logs -t - посмотреть логи
heroku ps — посмотреть запущенные приложения