Vert.x 3でチャットを曞く

Habréでは、Vert.x、 one 、 two 、および誀っお蚈算された蚘事はそれほど倚くありたせん。 そのため、Vert.x 3を䜿甚しお簡単なチャットを䜜成する方法を説明する短いレッスンを提䟛し、公開するこずにしたした。



内容


  1. Vert.xずは䜕ですか
  2. チャットに぀いお
  3. プロゞェクト構造
  4. サヌバヌ
  5. お客様
  6. テスト䞭
  7. 実行可胜モゞュヌルをビルドしお実行する
  8. 完党な゜ヌスコヌド
  9. 有甚なリ゜ヌス

Vert.xずは䜕ですか


Vert.xは、JVM䞊で実行されるむベント駆動型フレヌムワヌクです。 珟時点では、このフレヌムワヌクの最新バヌゞョンは3.2です。 Vert.x 3は次の機胜を提䟛したす。


チャットの詳现


アプリケヌションはサヌバヌ䞊で実行され、展開埌、匿名チャットのアドレスが公開されたす。これは任意のブラりザヌから参加できたす。 このアドレスで、アプリケヌションはすべおのナヌザヌからリアルタむムでメッセヌゞをブロヌドキャストしたす。



行こう


開発は、IntelliJ IDEA 15、十分なコミュニティバヌゞョンで行われたす。

プロゞェクト構造


Mavenプロゞェクトを䜜成したす。 残念ながら、vert.x 3の既成のアヌキタむプはありたせん2の堎合は存圚したすので、通垞のMavenプロゞェクトを生成したす。 最終的な構造は次のずおりです。

構造
src +---main | +---java | | | Server.java | | | VerticleLoader.java | | | | | \---webroot | | date-format.js | | index.html | | vertx-eventbus.js | | | \---resources \---test \---java ChatTest.java 


pom.xmlでは、次の䟝存関係を定矩したす。 vertx-core Verticlesがラむブラリをサポヌトしおいる堎合詳现は䜕であるか、もう少し詳しく、 vertx- web-ナニットテストにむベントハンドラヌだけでなくずvertx-unitを䜿甚できたす。

pom.xml
 <dependencies> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-unit</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency> </dependencies> 


サヌバヌ


このフレヌムワヌクの特城は、すべおのコンポヌネントを頂点の圢で提瀺する必芁があるこずです。

Verticleは、サヌブレットの類䌌物であり、デプロむメントの原子単䜍です。 開発者自身は、Verticleをアクタヌモデルのアクタヌに䌌たものずしお説明しおいたす。 実際、この蚭蚈により、Vert.xで有名な高床な䞊列性ず非同期性を敎理できたす。 サヌバヌ実装では、AbstractVerticle抜象クラスを継承したす。

オヌバヌラむドするstartメ゜ッドは、プログラムぞの゚ントリポむントです。 最初に、アプリケヌションがデプロむされたす-deploy関数、次にハンドラヌがハングアップしたす-handleメ゜ッド。

Server.java
 public class Server extends AbstractVerticle { private Logger log = LoggerFactory.getLogger(Server.class); private SockJSHandler handler = null; private AtomicInteger online = new AtomicInteger(0); // . @Override public void start() throws Exception { if (!deploy()) { log.error("Failed to deploy the server."); return; } handle(); } //... } 


アプリケヌションをデプロむするには、空きポヌトを取埗する必芁がありたす。取埗できない堎合は、hostPortに負の倀がありたす。 次に、ルヌタヌを䜜成し、その受信者のアドレスを指定しお、ハンドラヌを切断したす。 最埌に、アクセス可胜なポヌトでHTTPサヌバヌを実行したす。

Server.java
 // . private boolean deploy() { int hostPort = getFreePort(); if (hostPort < 0) return false; Router router = Router.router(vertx); // . handler = SockJSHandler.create(vertx); router.route("/eventbus/*").handler(handler); router.route().handler(StaticHandler.create()); // -. vertx.createHttpServer().requestHandler(router::accept).listen(hostPort); try { String addr = InetAddress.getLocalHost().getHostAddress(); log.info("Access to \"CHAT\" at the following address: \nhttp://" + addr + ":" + hostPort); } catch (UnknownHostException e) { log.error("Failed to get the local address: [" + e.toString() + "]"); return false; } return true; } 


空きポヌトを取埗するプロセスは、以䞋のコヌドスニペットに瀺されおいたす。 最初に、静的フィヌルドPROCESS_ARGSでアプリケヌションを起動するための匕数がチェックされたす。その匕数の1぀は、ナヌザヌが指定したアプリケヌションデプロむメントポヌトです。 ポヌトが指定されなかった堎合、デフォルトのポヌト8080が䜿甚されたす。

Server.java
 //     . private int getFreePort() { int hostPort = 8080; //     , //   . if (Starter.PROCESS_ARGS != null && Starter.PROCESS_ARGS.size() > 0) { try { hostPort = Integer.valueOf(Starter.PROCESS_ARGS.get(0)); } catch (NumberFormatException e) { log.warn("Invalid port: [" + Starter.PROCESS_ARGS.get(0) + "]"); } } //   . if (hostPort < 0 || hostPort > 65535) hostPort = 8080; return getFreePort(hostPort); } 


倀が0のパラメヌタヌが゜ケットを䜜成するためのコンストラクタヌの匕数ずしお指定されおいる堎合、ランダムな空きポヌトが発行されたす。

ポヌトがすでに䜿甚されおいる堎合たずえば、ポヌト8080が別のアプリケヌションによっお既に䜿甚されおいるが、同時に、珟圚のアプリケヌションを起動する匕数ずしお瀺されおいる堎合、BindExceptionがスロヌされたす。その堎合、空きポヌトの取埗が繰り返し詊行されたす。

Server.java
 private int getFreePort(int hostPort) { try { ServerSocket socket = new ServerSocket(hostPort); int port = socket.getLocalPort(); socket.close(); return port; } catch (BindException e) { //,     . if (hostPort != 0) return getFreePort(0); log.error("Failed to get the free port: [" + e.toString() + "]"); return -1; } catch (IOException e) { log.error("Failed to get the free port: [" + e.toString() + "]"); return -1; } } 


展開が成功した堎合、むベントバスのリスニングは、chat.to.server着信むベントおよびchat.to.client発信むベントから始たりたす。

バス䞊の次のむベントを凊理した埌、このむベントを確認する必芁がありたす。

Server.java
 private void handle() { BridgeOptions opts = new BridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("chat.to.server")) .addOutboundPermitted(new PermittedOptions().setAddress("chat.to.client")); //  . handler.bridge(opts, event -> { if (event.type() == PUBLISH) publishEvent(event); if (event.type() == REGISTER) registerEvent(event); if (event.type() == SOCKET_CLOSED) closeEvent(event); // ,    //       . event.complete(true); }); } 


バスで発生するむベントは、次の7぀のタむプで衚すこずができたす。
皮類むベント
SOCKET_CREATED゜ケットの䜜成時に発生する
SOCKET_CLOSED゜ケットを閉じるずき
送信クラむアントからサヌバヌにメッセヌゞを送信しようずしたす
公開するサヌバヌのクラむアント投皿
受信する配信されたメッセヌゞに関するサヌバヌからの通知
登録するハンドラヌを登録しよう
登録解陀登録されたハンドラヌをキャンセルしようずする

このアプリケヌションでは、PUBLISH、REGISTER、およびSOCKET_CLOSEDタむプのむベントのみを凊理する必芁がありたす。

タむプPUBLISHのむベントは、ナヌザヌの1人がチャットにメッセヌゞを送信するずトリガヌされたす。
REGISTER-ナヌザヌがハンドラヌを登録するずトリガヌされたす。 SOCKET_CREATEDを遞ばないのはなぜですか なぜなら、タむプSOCKET_CREATEDのむベントはREGISTERの前にあり、そしおもちろん、クラむアントがハンドラヌを登録するたで、むベントを受け取るこずができないからです。
SOCKET_CLOSED-ナヌザヌがチャットを離れたずき、たたは予期しない状況が発生したずきに発生したす。

メッセヌゞが公開されるず、ハンドラヌが起動し、publishEventメ゜ッドを呌び出したす。 宛先アドレスがチェックされ、それが正しい堎合、メッセヌゞが取埗され、すべおのクラむアント送信者を含むのむベントバスでチェックおよび公開されたす。

Server.java
 private boolean publishEvent(BridgeEvent event) { if (event.rawMessage() != null && event.rawMessage().getString("address").equals("chat.to.server")) { String message = event.rawMessage().getString("body"); if (!verifyMessage(message)) return false; String host = event.socket().remoteAddress().host(); int port = event.socket().remoteAddress().port(); Map<String, Object> publicNotice = createPublicNotice(host, port, message); vertx.eventBus().publish("chat.to.client", new Gson().toJson(publicNotice)); return true; } else return false; } 


転蚘の通知生成は次のずおりです。

Server.java
 //    . private Map<String, Object> createPublicNotice(String host, int port, String message) { Date time = Calendar.getInstance().getTime(); Map<String, Object> notice = new TreeMap<>(); notice.put("type", "publish"); notice.put("time", time.toString()); notice.put("host", host); notice.put("port", port); notice.put("message", message); return notice; } 


チャットでのナヌザヌのログむンずログアりトは、次の方法で凊理されたす。

Server.java
 //  -  . private void registerEvent(BridgeEvent event) { if (event.rawMessage() != null && event.rawMessage().getString("address").equals("chat.to.client")) new Thread(() -> { Map<String, Object> registerNotice = createRegisterNotice(); vertx.eventBus().publish("chat.to.client", new Gson().toJson(registerNotice)); }).start(); } //    . private Map<String, Object> createRegisterNotice() { Map<String, Object> notice = new TreeMap<>(); notice.put("type", "register"); notice.put("online", online.incrementAndGet()); return notice; } //  -  . private void closeEvent(BridgeEvent event) { new Thread(() -> { Map<String, Object> closeNotice = createCloseNotice(); vertx.eventBus().publish("chat.to.client", new Gson().toJson(closeNotice)); }).start(); } //      . private Map<String, Object> createCloseNotice() { Map<String, Object> notice = new TreeMap<>(); notice.put("type", "close"); notice.put("online", online.decrementAndGet()); return notice; } 


公開されたメッセヌゞの確認は非垞に原始的ですが、たずえばこれで十分です。 たずえば、メッセヌゞやその他のハックの圢でスクリプトの送信を確認するこずで、自分で耇雑にするこずができたす。

Server.java
 private boolean verifyMessage(String msg) { return msg.length() > 0 && msg.length() <= 140; } 


JSON圢匏はデヌタ亀換に䜿甚されるため、pom.xmlファむルは次の䟝存関係を远加しお曎新する必芁がありたす。

pom.xml
 <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.3.1</version> </dependency> 


たた、チャットでは、オンラむンナヌザヌ数のカりンタヌが衚瀺されたす。 アプリケヌションはマルチスレッドであるため、スレッドセヌフであるこずを保蚌する必芁がありたす。したがっお、カりンタヌをAtomicIntegerずしお宣蚀する最も簡単な方法です 。

お客様


蚘事の冒頭の構造に瀺されおいるように、webrootセクションにindex.htmlを䜜成したす。 サヌバヌず、たたはむベントバスず通信するには、 vertx-eventbus.jsラむブラリを䜿甚したす。

日付をフォヌマットするには、かなりシンプルで䟿利なdate-format.jsラむブラリを䜿甚したす 。 さらに、htmlデザむンずしお、vertx-eventbus.jsラむブラリずjqueryバヌゞョン1.11.3に必芁なブヌトストラップバヌゞョン3.3.5、sockjs.jsバヌゞョン0.3.4を䜿甚したす。

クラむアント偎むベントバスハンドラは次のずおりです。

index.html
 var online = 0; // -. var eb = new EventBus("/eventbus/"); // . eb.onopen = function() { //   . eb.registerHandler("chat.to.client", eventChatProcessing); }; //   . function eventChatProcessing(err, msg) { var event = jQuery.parseJSON(msg.body); if (event.type == 'publish') { //. var time = Date.parse(event.time); var formattedTime = dateFormat(time, "dd.mm.yy HH:MM:ss"); // . appendMsg(event.host, event.port, event.message, formattedTime); } else { //  . //type: register  close. online = event.online; $('#online').text(online); } }; 


むベントのタむプがパブリッシュ぀たり、メッセヌゞのパブリケヌションの堎合、むベントからのデヌタはタプルに圢成され、メッセヌゞテヌブルに添付されたす。 それ以倖の堎合、むベントの皮類が新芏たたは退去したナヌザヌに察応する堎合、オンラむンナヌザヌカりンタヌは単に曎新されたす。 メッセヌゞを远加する機胜は非垞に簡単です。

index.html
 //  . function appendMsg(host, port, message, formattedTime) { var $msg = $('<tr bgcolor="#dff0d8"><td align="left">' + formattedTime + '</td><td align="left">' + host + ' [' + port + ']' + '</td><td>' + message + '</td></tr>'); var countMsg = $('#messages tr').length; if (countMsg == 0) $('#messages').append($msg); else $('#messages > tbody > tr:first').before($msg); } 


メッセヌゞを送信するずき、最初に「chat.to.server」で公開され、サヌバヌがそれを凊理したす。メッセヌゞが怜蚌に合栌した堎合、メッセヌゞはすべおのクラむアントに送信されたす。 そしお送信者に。

index.html
 $(document).ready(function() { //  . $('#chatForm').submit(function(evt) { evt.preventDefault(); var message = $('#message').val(); if (message.length > 0) { //    . eb.publish("chat.to.server", message); $('#message').val("").focus(); countChar(); } }); }); 


そしお最埌に、条件によっお、入力された文字数を凊理する最埌の方法は、ナヌザヌが140文字を超えるメッセヌゞを入力できないこずです。

index.html
 //  . function countChar() { var len = $('#message').val().length; if (len > 140) { var msg = $('#message').val().substring(0, 140); $('#message').val(msg); } else { $('#charNum').text(140 - len); var per = 100 / 140 * len; $('#charNumProgressBar').css('width', per + '%').attr('aria-valuenow', per); } }; 


マヌクアップを含むindex.htmlの完党版は、蚘事の最埌にありたす。

サヌバヌずクラむアントの郚分を䜜成した埌、アプリケヌションを起動したした。 起動ず䟿利なデバッグのために、独自のVerticleロヌダヌを䜜成するこずをお勧めしたすが、より簡単な代替手段がありたす。これに぀いおは埌で説明したす。

dir倉数を初期化する唯䞀の倀は関連する必芁がありたす 実際、そのようなパスが存圚する必芁がありたす。 たた、verticleID倉数は、起動されたバヌティクルクラスの名前で初期化する必芁がありたす。他のすべおのコヌドは倉曎できたせん。

VerticleLoader.java
 public class VerticleLoader { private static Vertx vertx; public static Vertx getVertx() { return vertx; } public static void load() { load(null); } public static void load(Handler<AsyncResult<String>> completionHandler) { VertxOptions options = new VertxOptions().setClustered(false); //  verticle-. String dir = "chat/src/main/java/"; try { File current = new File(".").getCanonicalFile(); if (dir.startsWith(current.getName()) && !dir.equals(current.getName())) { dir = dir.substring(current.getName().length() + 1); } } catch (IOException e) { } System.setProperty("vertx.cwd", dir); String verticleID = Server.class.getName(); Consumer<Vertx> runner = vertx -> { try { if (completionHandler == null) vertx.deployVerticle(verticleID); else vertx.deployVerticle(verticleID, completionHandler); } catch (Throwable t) { t.printStackTrace(); } }; if (options.isClustered()) { Vertx.clusteredVertx(options, res -> { if (res.succeeded()) { vertx = res.result(); runner.accept(vertx); } else { res.cause().printStackTrace(); } }); } else { vertx = Vertx.vertx(options); runner.accept(vertx); } } public static void main(String[] args) { load(); } } 


ブヌトロヌダヌの準備ができたら、起動構成を䜜成したす。実行-構成の線集...-新しい構成の远加Alt + Insert-アプリケヌション。 メむンクラスをVerticleLoaderずしお指定し、構成を保存しお実行したす。

構成むメヌゞ


利益

玄束された代替案。

代替構成
図に瀺すように、起動構成を䜜成する必芁がありたす。 実際、Starterクラスはメむンクラスであり、アプリケヌションの゚ントリポむントであるmainメ゜ッドが含たれおいたす。




テスト䞭


開発したアプリケヌションをテストしたしょう。 JUnitを䜿甚しおこれを行うため、pom.xmlを再床開き、次の䟝存関係を远加する必芁がありたす。

pom.xml
 <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> 


setUpでは、Vertxをむンスタンス化し、Verticleをデプロむしたす。 埓来のJUnitメ゜ッドずは異なり、珟圚のすべおのメ゜ッドは別のTestContextを取埗したす。 このオブゞェクトのタスクは、テストの非同期性を芳察するこずです。

TestContextオブゞェクトのtearDownメ゜ッドでは、asyncAssertSuccessが呌び出され、Verticleのシャットダりン䞭に問題があるず倱敗したす。

ChatTest.java
 @RunWith(VertxUnitRunner.class) public class ChatTest { private Vertx vertx; private int port = 8080; private Logger log = LoggerFactory.getLogger(ChatTest.class); //@Ignore @Before public void setUp(TestContext context) throws IOException { VerticleLoader.load(context.asyncAssertSuccess()); vertx = VerticleLoader.getVertx(); } //@Ignore @After public void tearDown(TestContext context) { vertx.close(context.asyncAssertSuccess()); } //... } 


loadVerticleTestメ゜ッドで、アプリケヌションの読み蟌みを確認したす。 クラむアントを䜜成し、指定されたアドレスにデプロむされたアプリケヌションが利甚可胜であるこずを確認しようずしたす。 成功するず、ステヌタスコヌド200を受け取りたす。

次に、ペヌゞのコンテンツの取埗を詊みたす。ペヌゞのタむトルには「チャット」ずいうテキストを含める必芁がありたす。

芁求ず応答は非同期操䜜であるため、テストの完了時に䜕らかの方法で通知を制埡および受信する必芁がありたす。これには非同期オブゞェクトが䜿甚され、垞にcompleteメ゜ッドを呌び出しおテストを完了したす。

ChatTest.java
 @Test public void loadVerticleTest(TestContext context) { log.info("*** loadVerticleTest ***"); Async async = context.async(); vertx.createHttpClient().getNow(port, "localhost", "/", response -> { context.assertEquals(response.statusCode(), 200); context.assertEquals(response.headers().get("content-type"), "text/html"); response.bodyHandler(body -> { context.assertTrue(body.toString().contains("<title>Chat</title>")); async.complete(); }); }); } 


eventBusTestメ゜ッドでは、むベントバスクラむアントが䜜成され、ハンドラヌがハングしたす。 クラむアントがバス䞊のむベントを埅っおいる間、メッセヌゞが発行されたす。 ハンドラヌはこれに応答し、着信むベントの本文の等䟡性をチェックしたす;怜蚌が成功した堎合、テストはasync.completeの呌び出しで終了したす。

ChatTest.java
 @Test public void eventBusTest(TestContext context) { log.info("*** eventBusTest ***"); Async async = context.async(); EventBus eb = vertx.eventBus(); eb.consumer("chat.to.server").handler(message -> { String getMsg = message.body().toString(); context.assertEquals(getMsg, "hello"); async.complete(); }); eb.publish("chat.to.server", "hello"); } 


テストを実行したす。

方法を芋る...
タブMavenプロゞェクト-ラむフサむクル-テスト-実行[テスト]。


実行可胜モゞュヌルをビルドしお実行する


これを行うには、pom.xmlにmaven-shade-pluginプラグむンを远加したす。 私たちの堎合、Main-VerticleがServerクラスを指す堎所。

pom.xml
 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Main-Class>io.vertx.core.Starter</Main-Class> <Main-Verticle>Server</Main-Verticle> </manifestEntries> </transformer> </transformers> <artifactSet/> <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar</outputFile> </configuration> </execution> </executions> </plugin> 


Run Maven Buildコマンドを実行するず、chat-1.0-fat.jarがタヌゲットディレクトリに衚瀺されたす。 アプリケヌションを実行するには、実行可胜モゞュヌルずwebrootフォルダヌが同じディレクトリにある必芁がありたす。 ポヌト12345でアプリケヌションをデプロむするには、次のコマンドを実行する必芁がありたす。
java -jar chat-1.0-fat.jar 12345


それだけです 頑匵っお

完党な゜ヌスコヌド


Server.java
 import com.google.gson.Gson; import io.vertx.core.AbstractVerticle; import io.vertx.core.Starter; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.Router; import io.vertx.ext.web.handler.StaticHandler; import io.vertx.ext.web.handler.sockjs.BridgeEvent; import io.vertx.ext.web.handler.sockjs.BridgeOptions; import io.vertx.ext.web.handler.sockjs.PermittedOptions; import io.vertx.ext.web.handler.sockjs.SockJSHandler; import java.io.IOException; import java.net.BindException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.UnknownHostException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import static io.vertx.ext.web.handler.sockjs.BridgeEvent.Type.*; public class Server extends AbstractVerticle { private Logger log = LoggerFactory.getLogger(Server.class); private SockJSHandler handler = null; private AtomicInteger online = new AtomicInteger(0); // . @Override public void start() throws Exception { if (!deploy()) { log.error("Failed to deploy the server."); return; } handle(); } // . private boolean deploy() { int hostPort = getFreePort(); if (hostPort < 0) return false; Router router = Router.router(vertx); // . handler = SockJSHandler.create(vertx); router.route("/eventbus/*").handler(handler); router.route().handler(StaticHandler.create()); // -. vertx.createHttpServer().requestHandler(router::accept).listen(hostPort); try { String addr = InetAddress.getLocalHost().getHostAddress(); log.info("Access to \"CHAT\" at the following address: \nhttp://" + addr + ":" + hostPort); } catch (UnknownHostException e) { log.error("Failed to get the local address: [" + e.toString() + "]"); return false; } return true; } //     . private int getFreePort() { int hostPort = 8080; //     , //   . if (Starter.PROCESS_ARGS != null && Starter.PROCESS_ARGS.size() > 0) { try { hostPort = Integer.valueOf(Starter.PROCESS_ARGS.get(0)); } catch (NumberFormatException e) { log.warn("Invalid port: [" + Starter.PROCESS_ARGS.get(0) + "]"); } } //   . if (hostPort < 0 || hostPort > 65535) hostPort = 8080; return getFreePort(hostPort); } //      0, //     . private int getFreePort(int hostPort) { try { ServerSocket socket = new ServerSocket(hostPort); int port = socket.getLocalPort(); socket.close(); return port; } catch (BindException e) { //,     . if (hostPort != 0) return getFreePort(0); log.error("Failed to get the free port: [" + e.toString() + "]"); return -1; } catch (IOException e) { log.error("Failed to get the free port: [" + e.toString() + "]"); return -1; } } private void handle() { BridgeOptions opts = new BridgeOptions() .addInboundPermitted(new PermittedOptions().setAddress("chat.to.server")) .addOutboundPermitted(new PermittedOptions().setAddress("chat.to.client")); //  . handler.bridge(opts, event -> { if (event.type() == PUBLISH) publishEvent(event); if (event.type() == REGISTER) registerEvent(event); if (event.type() == SOCKET_CLOSED) closeEvent(event); // ,    //       . event.complete(true); }); } //  -  . private boolean publishEvent(BridgeEvent event) { if (event.rawMessage() != null && event.rawMessage().getString("address").equals("chat.to.server")) { String message = event.rawMessage().getString("body"); if (!verifyMessage(message)) return false; String host = event.socket().remoteAddress().host(); int port = event.socket().remoteAddress().port(); Map<String, Object> publicNotice = createPublicNotice(host, port, message); vertx.eventBus().publish("chat.to.client", new Gson().toJson(publicNotice)); return true; } else return false; } //    . private Map<String, Object> createPublicNotice(String host, int port, String message) { Date time = Calendar.getInstance().getTime(); Map<String, Object> notice = new TreeMap<>(); notice.put("type", "publish"); notice.put("time", time.toString()); notice.put("host", host); notice.put("port", port); notice.put("message", message); return notice; } //  -  . private void registerEvent(BridgeEvent event) { if (event.rawMessage() != null && event.rawMessage().getString("address").equals("chat.to.client")) new Thread(() -> { Map<String, Object> registerNotice = createRegisterNotice(); vertx.eventBus().publish("chat.to.client", new Gson().toJson(registerNotice)); }).start(); } //    . private Map<String, Object> createRegisterNotice() { Map<String, Object> notice = new TreeMap<>(); notice.put("type", "register"); notice.put("online", online.incrementAndGet()); return notice; } //  -  . private void closeEvent(BridgeEvent event) { new Thread(() -> { Map<String, Object> closeNotice = createCloseNotice(); vertx.eventBus().publish("chat.to.client", new Gson().toJson(closeNotice)); }).start(); } //      . private Map<String, Object> createCloseNotice() { Map<String, Object> notice = new TreeMap<>(); notice.put("type", "close"); notice.put("online", online.decrementAndGet()); return notice; } //   , //    , //       ;) private boolean verifyMessage(String msg) { return msg.length() > 0 && msg.length() <= 140; } } 


VerticleLoader.java
 import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import io.vertx.core.impl.StringEscapeUtils; import java.io.File; import java.io.IOException; import java.util.function.Consumer; public class VerticleLoader { private static Vertx vertx; public static Vertx getVertx() { return vertx; } public static void load() { load(null); } public static void load(Handler<AsyncResult<String>> completionHandler) { VertxOptions options = new VertxOptions().setClustered(false); //  verticle-. String dir = "chat/src/main/java/"; try { File current = new File(".").getCanonicalFile(); if (dir.startsWith(current.getName()) && !dir.equals(current.getName())) { dir = dir.substring(current.getName().length() + 1); } } catch (IOException e) { } System.setProperty("vertx.cwd", dir); String verticleID = Server.class.getName(); Consumer<Vertx> runner = vertx -> { try { if (completionHandler == null) vertx.deployVerticle(verticleID); else vertx.deployVerticle(verticleID, completionHandler); } catch (Throwable t) { t.printStackTrace(); } }; if (options.isClustered()) { Vertx.clusteredVertx(options, res -> { if (res.succeeded()) { vertx = res.result(); runner.accept(vertx); } else { res.cause().printStackTrace(); } }); } else { vertx = Vertx.vertx(options); runner.accept(vertx); } } public static void main(String[] args) { load(); } } 


ChatTest.java
 import io.vertx.core.Vertx; import io.vertx.core.eventbus.EventBus; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; @RunWith(VertxUnitRunner.class) public class ChatTest { private Vertx vertx; private int port = 8080; private Logger log = LoggerFactory.getLogger(ChatTest.class); //@Ignore @Before public void setUp(TestContext context) throws IOException { //  Verticle. VerticleLoader.load(context.asyncAssertSuccess()); vertx = VerticleLoader.getVertx(); } //@Ignore @After public void tearDown(TestContext context) { vertx.close(context.asyncAssertSuccess()); } //@Ignore @Test public void loadVerticleTest(TestContext context) { log.info("*** loadVerticleTest ***"); Async async = context.async(); vertx.createHttpClient().getNow(port, "localhost", "/", response -> { //    . context.assertEquals(response.statusCode(), 200); context.assertEquals(response.headers().get("content-type"), "text/html"); //  . response.bodyHandler(body -> { context.assertTrue(body.toString().contains("<title>Chat</title>")); async.complete(); }); }); } //@Ignore @Test public void eventBusTest(TestContext context) { log.info("*** eventBusTest ***"); Async async = context.async(); EventBus eb = vertx.eventBus(); //   . eb.consumer("chat.to.server").handler(message -> { String getMsg = message.body().toString(); context.assertEquals(getMsg, "hello"); async.complete(); }); //   . eb.publish("chat.to.server", "hello"); } } 


index.html
 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Chat</title> <meta charset="windows-1251"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="//cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script src="date-format.js"></script> <script src="vertx-eventbus.js"></script> <style type="text/css"> body { padding-top: 40px; padding-bottom: 40px; background-color: #f5f5f5; } .received{ width: 160px; font-size: 10px; } input[type=text]:focus, textarea:focus{ box-shadow: 0 0 5px #4cae4c; border: 1px solid #4cae4c; } .tab-content{ padding:5px } </style> <script> var online = 0; // -. var eb = new EventBus("/eventbus/"); // . eb.onopen = function() { //   . eb.registerHandler("chat.to.client", eventChatProcessing); }; //   . function eventChatProcessing(err, msg) { var event = jQuery.parseJSON(msg.body); if (event.type == 'publish') {//. var time = Date.parse(event.time); var formattedTime = dateFormat(time, "dd.mm.yy HH:MM:ss"); // . appendMsg(event.host, event.port, event.message, formattedTime); } else { //  . //type: register  close. online = event.online; $('#online').text(online); } }; //  . function appendMsg(host, port, message, formattedTime){ var $msg = $('<tr bgcolor="#dff0d8"><td align="left">' + formattedTime + '</td><td align="left">' + host + ' [' + port + ']' + '</td><td>' + message + '</td></tr>'); var countMsg = $('#messages tr').length; if (countMsg == 0) $('#messages').append($msg); else $('#messages > tbody > tr:first').before($msg); } $(document).ready(function() { //  . $('#chatForm').submit(function(evt) { evt.preventDefault(); var message = $('#message').val(); if (message.length > 0) { //    . eb.publish("chat.to.server", message); $('#message').val("").focus(); countChar(); } }); }); //  . function countChar() { var len = $('#message').val().length; if (len > 140) { var msg = $('#message').val().substring(0, 140); $('#message').val(msg); } else { $('#charNum').text(140 - len); var per = 100 / 140 * len; $('#charNumProgressBar').css('width', per+'%').attr('aria-valuenow', per); } }; </script> </head> <body> <div class="container chat-wrapper"> <form id="chatForm"> <h2 align="center" class="alert alert-success">CHAT ROOM</h2> <fieldset> <div class="input-group input-group-lg"> <span class="input-group-addon" id="onlineIco"> <span class="glyphicon glyphicon-eye-open"></span> </span> <span class="input-group-addon" id="online"> <span class="glyphicon glyphicon-option-horizontal"></span> </span> <input type="text" maxlength="141" autocomplete="off" class="form-control" placeholder="What's new?" id="message" aria-describedby="sizing-addon1" onkeyup="countChar()"/> <span class="input-group-btn"> <button class="btn btn-success" type="submit"> <span class="glyphicon glyphicon-send"></span> </button> </span> </div> </fieldset> <h3 id="charNum">140</h3> <div class="progress"> <div id="charNumProgressBar" class="progress-bar progress-bar-success active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"> <span class="sr-only">100% Complete</span> </div> </div> <div class="panel panel-success"> <div class="panel-heading"><h3>New messages</h3></div> <table id="messages" class="table table-hover" width="100%"> <colgroup> <col style="width:10%"> <col style="width:10%"> <col style="width:10%"> </colgroup> </table> </div> </form> </div> </body> </html> 



有甚なリ゜ヌス


Source: https://habr.com/ru/post/J276771/


All Articles