Introducing the Demo The Software The Server: GlassFish 4 (promoted build B88), Oracle The server-side Java app can generate random price quotes The Client: HTML5 Ext JS framework, Sencha The Browser: Chrome , Google Charles Proxy The Goal 1. The server generates random stock prices for a 12-stock portfolio 2. The user sends a request for price quote for a stock 3. The HTML client displays the received price 4. Observe what went over the network
The Server: Rest @Path("stock") public class RestResource { @GET @Produces(value = MediaType.APPLICATION_JSON) @Path("/{ticker}") public String getRandomValue(@PathParam(value = "ticker") String ticker) { return new Gson().toJson(RandomStocksGenerator.getDataForTicker(ticker)); } }
The Client: AJAX in Ext JS 'mypanel button[action=doRestCall]': click: this.onRestCall }
onRestCall: function (btn) { var ticker = Ext.ComponentQuery.query('mypanel textfield[name=ticker]')[0].getValue(); var rest_url = "http://" + document.location.host + document.location.pathname + "rest/stock/"; rest_url = rest_url + ticker; Ext.Ajax.request({ url: rest_url, scope: this, success: function (response) { var a = Ext.JSON.decode(response.responseText); a.price = parseFloat(a.price).toFixed(4); console.log(a); } });
What’s SSE
• It’s not a request-response mode
• The browser subscribes to events from server by creating EventSource pointing to this server
• The server can send data to client at any time
• The browser receives an event + data
Browser Subscribes to SSE var source = new EventSource('http://localhost:8080/stock/events'); source.addEventListener('open', function(e) { // Connection was opened. }, false); source.addEventListener(’priceChanged', function(e) { var priceQuote = JSON.parse(e.data); … }, false); source.addEventListener('error', function(e) { if(e.readyState == EventSource.CLOSED) { // Connection was closed. } }, false);
To listen to any messages: source.onmessage = function(e) {….};
Pushing SSE from Server
The server sends events as text messages with MIME `text/event-stream`. Each message starts with data: and end with a pair /n/n: 'data: {"price": "123.45"}/n/n` The browser concatenates all these messages separating them with /n.
Pushing from Glassfish (Jersey) @Path("stock") public class SseResource { private static final SseBroadcaster BROADCASTER = new SseBroadcaster(); private boolean isRunning = false; private Timer broadcastTimer; @GET @Path("stock-generator") @Produces(SseFeature.SERVER_SENT_EVENTS) public EventOutput itemEvents() { final EventOutput eventOutput = new EventOutput(); BROADCASTER.add(eventOutput); if (!isRunning) startBroadcastTask(); return eventOutput; }
Broadcasting in Jersey protected void startBroadcastTask() { broadcastTimer = new Timer(); broadcastTimer.schedule( new SseBroadcastTask(BROADCASTER, 0), 0, 300); this.isRunning = true; }
public class SseBroadcastTask extends TimerTask { private final SseBroadcaster owner; public SseBroadcastTask(SseBroadcaster owner, int timeout) { this.owner = owner; } @Override public void run() { OutboundEvent event = new OutboundEvent.Builder().data( String.class, RandomStocksGenerator.getRandomValues().toJson()).build(); owner.broadcast(event); …
Introducing the SSE Demo The Software The Server: GlassFish 4 (promoted build B88), Oracle The Java app can generate random price quotes The Client: HTML5 Ext JS framework, Sencha The Chrome Browser, Google
The Goal 1. The server generates random stock prices for a 12-stock portfolio 2. The server pushes 3 price quotes per second using SSE 3. The HTML client shows the prices in a data grid
WebSocket
• Standard W3C protocol (RFC6455)
• Java EE 7 includes WebSocket API (JSR-356)
• Web Browsers include window.WebSocket object. No plugins required.
How WebSocket Works
Establish a socket connection via HTTP for the initial handshake.
Switch the protocol from HTTP to a socket-based protocol.
Send messages in both directions simultaneously.
This is not a request-response model!
Protocol Upgrade: HTTP à WebSocket
• Client sends Upgrade HTTP-request • Server confirms Upgrade
• Client receives Upgrade response from server • Client changes WebSocket.readyState to open
W3C: WebSocket Interface [Constructor(DOMString url, optional (DOMString or DOMString[]) protocols)] //<1> interface WebSocket : EventTarget { readonly attribute DOMString url; const unsigned short CONNECTING = 0; const unsigned short OPEN = 1; const unsigned short CLOSING = 2; const unsigned short CLOSED = 3; readonly attribute unsigned short readyState; readonly attribute unsigned long bufferedAmount; // networking [TreatNonCallableAsNull] attribute Function? onopen; [TreatNonCallableAsNull] attribute Function? onerror; [TreatNonCallableAsNull] attribute Function? onclose; readonly attribute DOMString extensions; readonly attribute DOMString protocol; / void close([Clamp] optional unsigned short code, optional DOMString reason); // messaging [TreatNonCallableAsNull] attribute Function? onmessage; attribute DOMString binaryType; void send(DOMString data); void send(ArrayBufferView data); void send(Blob data); };
WebSocket Endpoint @ServerEndpoint(value = "/stock-generator", encoders = { StockMessageEncoder.class }, decoders = { StockMessageDecoder.class }) public class StocksEndpoint { @OnOpen public void onOpen(Session session) { … } @OnMessage public void onMessage(StockMessage message, Session client) { } @OnClose public void onClose(Session session) {… … } … }
public class StockMessageDecoder implements Decoder.Text<StockMessage> { @Override public StockMessage decode(String s) throws DecodeException { Gson gson = new Gson(); return gson.fromJson(s, StockMessage.class); } … }
public class StockMessageEncoder implements Encoder.Text<StockMessage> { @Override public String encode(StockMessage object) throws EncodeException { return new Gson().toJson(object); } … }
Decoder
Encoder
Heartbeats: Pings and Pongs
Heartbeats is a mechanism to check that connection is still alive. If a WebSocket implementation receives a ping it has to respond with a pong ASAP.
There is no JavaScript API to support Pings and Pongs.
The Software The Server: GlassFish 4 (promoted build B88), Oracle The Java app can generate random price quotes The Client: HTML5 Ext JS framework, Sencha The Chrome Browser, Google Charles proxy
Introducing the WebSocket Demo
The Goal 1. The server generates random stock prices for a 12-stock portfolio 2. The server pushes 20 price quotes per second using WebSocket 3. The HTML client shows the prices in a data grid
What was Pushed via WebSocket
The size of each data push: The length price quote data: from 42 to 45 bytes WebSocket added overhead: 2 bytes
{"symbol": "APPL", "id": 555, "price": "451.29"}
Comparing Overheads Http Request-Response Each roundtrip has: request header: 429 bytes + 268 bytes response header + 46 bytes data Server-Sent Events Initial request header 406 bytes + 268 bytes response header Each push: 46 bytes data + 8 bytes message wrapper WebSocket Initial upgrade request: 406 bytes + 268 bytes Each push 46 bytes data + 2 bytes message wrapper It’s full-duplex, not HTTP-based, works much faster.
Reducing kilobytes of data to 2 bytes... and reducing latency from 150ms to 50 ms is far more than marginal. In fact, these two factors alone are enough to make WebSocket seriously interesting to Google.
Ian Hickson, Google