Code Play #1: WebSockets With Vert.x and Angular
In the first article in the Code Play Series, we take a look at using websockets to create a real time, multiuser canvas.
Join the DZone community and get the full member experience.
Join For FreeThis is the first of the series of Code Play where I will be dabbling with tech I've not had much chance to get deep into.
Looking around various web-socket libraries, I wanted something quick and easy to get up and running that could handle some complex eventing. Vert.x and Angular seemed a nice combination and will be used to make an app that communicates via web-sockets and the vert.x event bus. To make this a little more exciting, there will be an array of HTML canvas's that will display what other people are drawing in real time:
How this works is when somebody hits the URL, they add their name and get added to a drawing session. A canvas will be displayed for each person, when they draw on the left panel it will update the corresponding panel on the right and everybody then see's it.
A simple canvas was created that will be used to display the drawing points. On its own, it doesn't actually do anything other than expose a single method to update the canvas with the drawing points
<canvas #canvas width="320" height="480" style="border:2px solid;"></canvas>
x
export class DrawCanvasComponent implements OnInit {
@ViewChild('canvas', { static: true }) _canvas;
canvas;
ctx;
pos = { x: 0, y: 0 };
constructor() { }
ngOnInit() {
this.canvas = this._canvas.nativeElement;
this.ctx = this.canvas.getContext('2d');
}
update(drawing: DrawingStroke) {
if (drawing.type === 'draw') {
this.draw(drawing);
} else {
this.setPosition(drawing);
}
}
private setPosition(position: DrawingStroke) {
this.pos.x = position.x;
this.pos.y = position.y;
}
private draw(e: DrawingStroke) {
this.ctx.beginPath();
this.ctx.lineWidth = 5;
this.ctx.lineCap = 'round';
this.ctx.strokeStyle = '#c0392b';
this.ctx.moveTo(this.pos.x, this.pos.y); // from
this.setPosition(e);
this.ctx.lineTo(this.pos.x, this.pos.y); // to
this.ctx.stroke(); // draw it!
}
}
Capturing the Drawing Points
A wrapper component will use the drawing canvas to capture all the click events:
<div>
<button class="btn btn-outline-secondary" (click)="clear()">Clear</button>
<span>Event count: {{sentCount}}</span>
</div>
<app-draw-canvas></app-draw-canvas>
xxxxxxxxxx
export class DrawCanvasEventComponent implements OnInit {
@Output() drawEvent = new EventEmitter<DrawingStroke>();
@ViewChild(DrawCanvasComponent, { static: true }) _canvasWrapper: DrawCanvasComponent;
canvasWrapper: DrawCanvasComponent;
canvas;
ctx;
offset;
sentCount = 0;
pos: PostionEvent = { x: 0, y: 0 };
count = 0;
constructor() { }
ngOnInit() {
this.canvasWrapper = this._canvasWrapper;
this.canvas = this.canvasWrapper._canvas.nativeElement;
this.ctx = this.canvas.getContext('2d');
this.canvas.addEventListener('mousemove', this.draw.bind(this));
this.canvas.addEventListener('mousedown', this.setPosition.bind(this));
this.canvas.addEventListener('mouseup', this.setPosition.bind(this));
this.canvas.addEventListener('touchmove', this.draw.bind(this));
this.canvas.addEventListener('touchstart', this.setPosition.bind(this));
this.canvas.addEventListener('touchend', this.setPosition.bind(this));
}
}
The event listeners capture mouse movements and touch is for mobile gestures. These then eventually call the update on the canvas component above via the @ViewChild
method. Also when these events occur they use the EventEmitter
to emit the event using the @Output
method.
Setting Up The Socket Client Side
Vert.x utilizes SockJs to do it bidding but is wrapped up in the vertx3-eventbus-client
. This allows easy utilization of web-sockets. A service was created that opens up the web-socket and connects the Java vert.x back end.
x
@Injectable({
providedIn: 'root'
})
export class EventbusService {
private _eventBus: EventBus;
eventBus = new BehaviorSubject(null);
private meetingGuid;
constructor(
) {
this.init();
}
init() {
this._eventBus = new EventBus(environment.eventBus);
this._eventBus.onopen = () => {
this.eventBus.next(this._eventBus);
};
}
}
One the connection is established, the onopen
event will be fired simple! A BehaviourSubject
was used here so that any components can use the connection even after is has been established or to avoid the scenario where there is no connection yet.
A DrawEventbusService
was created to make use of this event bus service and do only draw tasks events:
xxxxxxxxxx
export class DrawEventbusService {
private serverToClient = 'server.to.client.';
private clientToServer = 'client.to.server.';
constructor(
private eventBusService: EventbusService
) { }
hostDrawingSession(user: User, eventHandlers: DrawEventbusEventHandlers) {
this.getEventBus()
.subscribe(eventBus => this.init(eventBus, user, eventHandlers));
}
joinDrawingSession(user: User, eventHandler: DrawEventbusEventHandlers['onDrawEvent']) {
this.getEventBus()
.subscribe(eventBus => { this.initDrawGuest(eventBus, user, eventHandler) });
}
sendEvent(id: string, command: string, event: any) {
event.command = command
event.meetingUUID = id;
this.getEventBus().subscribe(eventBus => {
eventBus.send(this.clientToServer + id, event);
});
}
private getEventBus() {
return this.eventBusService.eventBus
.pipe(
filter(eb => eb !== null)
);
}
}
As seen earlier, the main event bus is subscribed to and the pipe function is used to filter the initial null in the behaviour subject.
For the app the work there are 3 main interactions utilising the web-socket connection:
- host drawing session - this is used to create the drawing session
- join drawing session - when a user wants to join a session
- send event - used pass canvas draw events
Notice the client and server variables, these are outbound and inbound paths that are used in the backend in the socket handler that allow data to be sent down these channels.
Setting up The Socket Server-Side
Vert.x has a concept of 'verticles' and instead of using a traditional technique of controller -> service -> repository, the back-end has been split into verticles of logic; separation of concerns. These verticles will make use of the Vert.x event bus to communicate between each other. This looks something like:
The backend has 4 main verticles
- web socket - set's up SockJs and the routing mechanisms
- repository - the data store
- creator (events) - initialises events and listeners
- draw events - handles the events from the user and publishes the response
The WebSocketVerticl
class will set up the what's called the bridge and the is the part that does the linking between front and backend. Here the SockJs bridge is configured.
x
SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
sockJSHandler.bridge(getBridgeOptions());
private BridgeOptions getBridgeOptions() {
return new BridgeOptions()
.addOutboundPermitted(
new PermittedOptions().setAddressRegex(config.getServerToClient() + "\\.[a-z-0-9]{36}"))
.addInboundPermitted(
new PermittedOptions().setAddressRegex(config.getClientToServer() + "\\.[a-z-0-9]{36}"))
.addInboundPermitted(new PermittedOptions().setAddressRegex(config.getClientToServer() + "\\.new"));
}
There are 2 config params getServerToClient
= 'server.to.client' and getClientToServer
= 'client.to.server'. The BridgeOptions
are made up of 3 addresses — 1 outbound and 2 inbound. These are the channels that the app communicates along and checked through a regular expression where,[a-z-0-9]{36}
is a very loose UUID match. The resulting communication channel take the form of
server.to.client.6bd9e48d-4d03-4b20-ad1f-6ce7b9241128
- Outboundclient.to.server.6bd9e48d-4d03-4b20-ad1f-6ce7b9241128
- Inboundclient.to.sever.new
- Inbound
When shown how to send messages these channels will become more apparent.
The next step was to add the SockJs config to a route. So when the client creates the vert.x EventBus
object under the hood it will call <host>/eventbus/info?t=1595234808993
xxxxxxxxxx
Router router = Router.router(vertx);
router.route("/eventbus/*")
.handler(sockJSHandler)
.failureHandler(error -> log.error("error {}", error));
In addition to the routing the Angular app also needs to be added hooked up. Note: the web files are set in java/resources/webroot
x
router.route().handler(StaticHandler.create()
.setCachingEnabled(false)
.setIndexPage("/index.html"));
router.routeWithRegex("\\/.+").handler(context -> {
context.reroute("/index.html");
});
Finally hooking these all together will result in the web-socket and routes configured and ready to run with the server listening on the define port.
x
vertx.createHttpServer()
.requestHandler(router)
.listen(config.getPort(), this::httpServerResult);
The WebSocketRepositoryVerticle
simply consumes the event-bus and has handlers for new session and new guests.
xxxxxxxxxx
public void start() {
vertx.eventBus().consumer(Commands.newDrawing, this::newDrawing);
vertx.eventBus().consumer(Commands.newDrawGuest, this::newDrawGuest);
}
For persistence, the app utilizes the vert.x shared data which is a Map of types. An example of getting the data, where matched by id, fetches the map with the object drawState
as the key for the value, then parses it to the DrawState
object
xxxxxxxxxx
private Optional<DrawState> getDrawState(String id) {
LocalMap<String, String> drawSession = vertx.sharedData().getLocalMap(id);
return Optional.of(drawSession)
.filter(q -> !q.isEmpty())
.map(this::convertToDrawState);
}
private DrawState convertToDrawState(LocalMap<String, String> stringLocalMap) {
try {
return Json.decodeValue(stringLocalMap.get("drawState"), DrawState.class);
} catch (Exception e) {
log.error("error {}", e);
throw new RuntimeException("failed to parse");
}
}
At this point, the server will run but no events are setup that are listening to anything, now enter WebSocketEventHandler.
From above, the client.to.server.new
handler is set here, anytime this is called from the client a new draw session will get sent the repository and new listeners setup for draw events.
x
// message body: {"id":"85a3e1ab-a4c7-4409-aa34-dcf1e3dc73bd","name":"Dale","host":true}
private void createDrawingListener() {
vertx.eventBus().consumer(config.getClientToServer() + ".new")
.handler(message -> {
var body = new JsonObject(message.body().toString());
var drawId = body.getString("id");
createDrawing(message, body);
createListeners(drawId);
});
}
The createListeners(drawId)
then set up the listening channel. The drawId
is the ID of the session. This means that any events coming through the matching channel (eg. client.to.server.6bd9e48d-4d03-4b20-ad1f-6ce7b9241128
) will get passed on to the draw event listener via the Commands.drawEvent
channel
x
private void createListeners(String drawId) {
vertx.eventBus().consumer(config.getClientToServer() + "." + drawId,
event -> {
vertx.eventBus().send(Commands.drawEvent, event.body().toString());
});
}
Finally, the message will get picked up in the DrawEventVerticle
where it will be processed by the draw event case and broadcast to all the clients using the outbound address that clients are listening on. The second case, newDrawGuest, will simply return extra data to help initialize the front end.
x
public void start() {
vertx.eventBus().consumer(Commands.drawEvent, this::drawEvent);
}
private void drawEvent(Message message) {
var body = new JsonObject(message.body().toString());
var drawId = body.getString("meetingUUID");
switch (getCommand(body)) {
case Commands.drawEvent:
//{"x":239,"y":279,"type":"draw","userUUID":"1bacde6c-9e54-4692-8c98-d89db49c05df","command":"draw-event","meetingUUID":"1bacde6c-9e54-4692-8c98-d89db49c05df"}
vertx.eventBus().publish(config.getServerToClient() + "." + drawId, body.toString());
break;
case Commands.newDrawGuest:
// {"id":"a48ff5b2-57be-46ed-9ce8-8e6edbf67a2b","meetingUUID":"966bfc99-5bff-4503-8216-6efb64852d9d","host":false,"name":"bill","command":"new-draw-guest"}
vertx.eventBus().request(Commands.newDrawGuest, body.toString(), event -> {
var send = body.put("drawState", event.result().body()).toString();
vertx.eventBus().publish(config.getServerToClient() + "." + drawId, send);
});
break;
}
}
Now the app would work and the drawn events will do a round robin trip from the left drawing canvas, to the server and back via web socket to right hand socket grid - same as the image at the top.
The Drawing Grid
To display all the drawing canvases of each person in the UI the canvas had a new wrapper around it that contains an rxjs Subject
this simply allows it to be passed events and on subscribe, call the update function in the drawing canvas via @ViewChild
x
export class DrawingUpdaterComponent implements OnInit {
@Input() attendee: User;
@Input() drawEvent: Subject<DrawingStroke>;
@ViewChild(DrawCanvasComponent, { static: true }) canvas: DrawCanvasComponent;
constructor(
private drawService: DrawService
) { }
ngOnInit() {
this.drawEvent
.pipe(
filter(x => x !== null)
)
.subscribe((drawingStroke: DrawingStroke) => {
this.canvas.update(drawingStroke);
});
}
}
The next step is to setup a listener that will add a new canvas to the UI when somebody joins and then also update their canvas when they draw.
The draw grid template is as simple as:
xxxxxxxxxx
<div class="drawings-wrapper">
<div #drawingList></div>
</div>
Every time a new person joins they will be added via:
xxxxxxxxxx
@ViewChild('drawingList', { static: false, read: ViewContainerRef }) drawGrid: ViewContainerRef;
and injected using the Angular componentFactoryResolver
. Notice the subject being passed in - this will be used to update the canvas.
x
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(DrawingUpdaterComponent);
const cmpRef = this.drawGrid.createComponent(componentFactory);
cmpRef.instance.attendee = attendee;
cmpRef.instance.drawEvent = subject;
Each time a user is added to the session they are also added to a list of Subject's, the same subject passed into the component:
xxxxxxxxxx
interface DrawSubject {
[index: string]: Subject<DrawingStroke>
}
attendeesInList: DrawSubject = {};
So now, when listening for draw events, these are then picked up, process and update the matching subject:
xxxxxxxxxx
listenForDrawEvents() {
this.drawService.drawEvent
.pipe(
filter(x => x !== null)
)
.subscribe((event: any) => {
if (event['command'] === 'draw-event') {
this.attendeesInList[event.userUUID].next(event);
}
}
So now anytime a draw event comes in it will update the subject and it turn update the canvas.
This was a simple use case and sending an event to the server via sockets is as simple as
x
this.getEventBus().subscribe(eventBus => {
eventBus.send(this.clientToServer + id, {some: 'object'});
});
Afterthoughts
Event-Driven
I kind of like setting up the backend as verticles instead of creating a typical controller -> service -> repository technique. I can see the benefit of having these loosely coupled in that extra processing could be done by subscribing to the events. Also that these could scale quite well for the events handlers for example. What i did find was that it was a little harder to see where it went after the event bus publish. But searching for the channel names found the destinations.
Web Sockets
This came as a breeze as there was so little setup. It was like sockets weren't being used at all. The event bus also simplifies the process. There are also many events, using vert.x, that can be hooked into when a socket connection is established such as connected, event, disconnect, etc. There are also security mechanisms for authentication available so it's not wide open. I was also tinkering of the thought of replacing APIs with sockets? It would seem the right thing to do for low latency super fast events but not for standard rest calls /user etc. I also did not find a nice way to create some structure to the payload, it forces you to add an identifier such as 'command' or 'action' that would end up going into an if or case statement on the server when processing it. As with rest you already have PUT / POST etc
Speed
Some counters were added to see the number of event/calls to the server. After a 2 second 'squiggle' it can easily be 80 calls to the server and it updates the grid canvas pretty much instantly. I did an experiment and uploaded this to Heroku and asked my team to log on and see what happened. Around 10 were drawing, 1000's of event per second going to my and their screen all at the same time with hardly any lag, was pretty cool.
Conclusion
I liked using this combination of vert.x and the event bus for web-socket communication. It was fast, easy and comes with nice features like auto-reconnect and security. Along with the toolkit vert.x comes with, it can do nearly everything modern apps need to do and its reactive. A minor pain point was I struggled to find a nice solution when issuing events and how they are identified in the backend compared to rest where the url and request type map to a function. Angular helped with the rxjs eventing easily updating components. All in all a nice bit of fun to experiment with.
Hope you enjoyed the first episode of the Code Play series.
You can find and run the full example on GitHub.
Opinions expressed by DZone contributors are their own.
Comments