📜 ⬆️ ⬇️

Writing chat on Vert.x 3

On Habré there are not too many articles on Vert.x, one , two, and obchelsya. Therefore, I decided to contribute and publish a small lesson, which tells you how to write a simple chat using Vert.x 3.



Content


  1. What is Vert.x?
  2. About chat
  3. Project structure
  4. Server
  5. Customer
  6. Testing
  7. Building and Running an Executable Module
  8. Full source code
  9. Useful resources

What is Vert.x?


Vert.x is an event-oriented framework running on the JVM. At the moment, the latest version of this framework 3.2. Vert.x 3 provides the following features:


More about chat


The application runs on the server, after deployment, an anonymous chat address is published, which you can join via any browser. At this address, the application transmits messages from all users in real time.
')


Go!


We will be developing in IntelliJ IDEA 15, the Community version is enough.

Project structure


Create a maven project. Unfortunately, there is no ready-made archetype for vert.x 3 (although it exists for 2), so the usual maven project is generated. Its final structure will be as follows:

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


In pom.xml we set the following dependencies. Where the vertx-core library supports Verticles (in more detail what it is, a little further), vertx-web allows you to use an event handler (and not only) and vertx-unit for unit testing.

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> 


Server


A feature of this framework is that all components must be presented in the form of Verticle.

Verticle is some analog of servlet, is atomic unit of expansion. The developers themselves describe Verticle as something similar to an actor in a model of actors . Actually, this design allows you to organize a high degree of parallelism and asynchrony, which is famous for Vert.x. In the implementation of our server, we inherit the AbstractVerticle abstract class.

The start () method we override is the entry point to the program. First, the application is deployed — the deploy () function, then the handler is hung — the handle () method.

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(); } //... } 


To deploy an application, you need to get a free port, in case you could not get it, there will be a negative value in hostPort. Next, create a router, specify the recipient's address for it and hang the handler. Finally, we run HTTP-Server on an available port.

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; } 


The process of obtaining a free port is presented in the code snippet below. First, the static field PROCESS_ARGS is checked for the presence of application launch arguments, one of which may be the application's deployment port. In case the port has not been set, the default port is 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); } 


If a parameter with the value 0 is specified as an argument of the socket creation constructor, then a random free port will be output.

When a port is already busy (for example, port 8080 is already being used by another application, but it is specified as the launch argument of the current application), a BindException exception is thrown, in which case a retry attempt is made to obtain a free port.

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; } } 


In case of successful deployment, listening to the event bus by addresses: chat.to.server (incoming events) and chat.to.client (outgoing events) begins.

After processing the next event on the bus, you need to check this event.

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); }); } 


Any events that occur on the bus can be represented by the following 7 types:
Type ofEvent
SOCKET_CREATEDoccurs when creating a socket
SOCKET_CLOSEDwhen closing a socket
SENDattempt to send a message from client to server
Publishclient posting to server
RECEIVEnotification from the server about the delivered message
REGISTERattempt to register handler
UNREGISTERattempt to cancel registered handler

In our application, we only need to handle events with the PUBLISH, REGISTER and SOCKET_CLOSED types.

An event of type PUBLISH is triggered when one of the users sends a chat message.
REGISTER - triggered when the user registers a handler. Why not SOCKET_CREATED? Because, an event with the SOCKET_CREATED type is preceded by - REGISTER, and, of course, until the client registers with the handler, it will not be able to receive events.
SOCKET_CLOSED - always occurs when the user leaves the chat or when an unexpected situation occurs.

When publishing a message, the handler is triggered and calls the publishEvent method. The destination address is checked, if it is correct, the message is retrieved, then checked and published on the event bus for all clients (including the sender).

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; } 


The notification generation for posting a message is as follows:

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; } 


Log in and out of users in the chat are processed in the following way:

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; } 


The verification of the published message is rather primitive, but for example this is enough you can complicate it yourself by checking, for example, the transfer of scripts in the form of a message and other hacks.

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


The JSON format is used for data exchange, therefore the pom.xml file needs to be updated by adding the following dependency:

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


Also, in our chat will be displayed the number of online users, because our application is multi-threaded, it is guaranteed to be thread-safety, so the easiest way to declare our counter as AtomicInteger .

Customer


Create index.html in the webroot section, as shown in the structure at the beginning of the article. To communicate with the server, or rather, with the event bus, the vertx-eventbus.js library is used.

For date formatting, we will use the date-format.js library , quite simple and convenient. In addition, we will use bootstrap version 3.3.5, sockjs.js version 0.3.4, necessary for the vertx-eventbus.js library and jquery version 1.11.3 as the html format.

The client-side event bus handler looks like this:

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); } }; 


If the event type is publish (ie, posting a message), then the data from the event (event) is formed into a tuple and attached to the message table. Otherwise, when the event type corresponds to a new or departed user, the online users counter is simply updated. The add message feature is pretty simple.

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); } 


When sending a message, it is first published to the address “chat.to.server”, where it is processed by the server, if the message passes the verification, it is sent to all clients, including and to the sender.

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(); } }); }); 


Finally, the last method, which processes the number of characters entered, by condition, the user cannot enter a message longer than 140 characters.

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); } }; 


The full version of index.html, including markup, is available at the end of the article.

After we wrote the server and client parts, it was the turn to launch the application. For start-up and convenient debugging, I recommend writing your own Verticle bootloader, although there is a simpler alternative, which I will give a little later.

The only thing is that the value that initializes the dir variable should be relevant, i.e. in fact, there must be such a path. Also, the verticleID variable must be initialized with the name of the verticle class being run, the rest of the code cannot be changed.

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(); } } 


Now that the bootloader is ready, let's create a launch configuration: Run - Edit Configuration ... - Add New Configuration (Alt + Insert) - Application. Specify the Main Class as VerticleLoader, save the configuration and run.

Configuration picture


PROFIT!

Promised alternative.

Alternative configuration
You need to create a launch configuration, as shown in the picture. In fact, the Starter class is the main class, it contains the main method, which is the entry point of the application.




Testing


Let's test the application we developed. We will do this using JUnit, so you need to re-open pom.xml and add the following dependency:

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


In setUp, we create a Vertx instance and deploy our Verticle to it. Unlike traditional JUnit methods, all current methods receive another TestContext. The task of this object is to observe the asynchrony of our tests.

In the tearDown () method for the TestContext object, asyncAssertSuccess () is called, it fails if there are problems with the Verticle shutdown.

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()); } //... } 


In the loadVerticleTest method, we check the loading of our application. We create a client and try to make sure that the application deployed at the address specified by us is available. If successful, we get the status code 200.

Then, we try to get the contents of the page, the title of which should contain the text “Chat”.

Since the request and the response are asynchronous operations, it is therefore necessary to somehow monitor this and receive notifications when the test has completed. For this, the Async object, which always calls the complete () method upon completion of the test, is used.

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(); }); }); } 


In the eventBusTest method, an event bus client is created and the handler is hung. While the customer is waiting for any events on the bus, a message is published. The handler responds to this and checks the body of the incoming event for equivalence; if successful, the test ends with a call to 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"); } 


Run the tests.

See how ...
Tab Maven Projects - Lifecycle - test - Run [test].


Building and Running an Executable Module


To do this, add the maven-shade-plugin plugin to pom.xml. Where Main-Verticle in our case should point to the class 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 the Run Maven Build command, after which chat-1.0-fat.jar appears in the target directory. To run the application, the executable and the webroot folder must be in the same directory. To deploy our application on port 12345, run the following command:
java -jar chat-1.0-fat.jar 12345


That's all. Successes!

Full source code


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> 



Useful resources


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


All Articles