Java tutorial
/* * Copyright 2010 kk-electronic a/s. * * This file is part of KKPortal. * * KKPortal is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * KKPortal is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with KKPortal. If not, see <http://www.gnu.org/licenses/>. * */ package com.kk_electronic.kkportal.core.rpc; import java.util.Date; import com.google.gwt.core.client.GWT; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.logical.shared.OpenEvent; import com.google.gwt.event.logical.shared.OpenHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; import com.google.gwt.http.client.RequestCallback; import com.google.gwt.http.client.RequestException; import com.google.gwt.http.client.Response; import com.google.inject.Inject; import com.google.inject.Singleton; import com.kk_electronic.kkportal.core.event.FrameReceivedEvent; import com.kk_electronic.kkportal.core.event.FrameReceivedHandler; import com.kk_electronic.kkportal.core.event.FrameSentEvent; import com.kk_electronic.kkportal.core.event.FrameSentHandler; /** * The comet class tries to emulate the a socket connection to the server by doing long polling. * You should only ever use one of such class due to limitations in the number of Http requests * a client can make to the server. * As per RFC2616: "A single-user client SHOULD NOT maintain more than 2 connections with any * server or proxy.". * The real limit is somewhat varying but designing the protocol should only rely on two. * Long pooling is made with a http request to the server that gets delayed on the server side * until a response from the server is made. Then the server closes the connection immediately * due to the fact some proxies only send the repsonse back to the client after the server * closes the connection, so the only real way to flush is by closing the connection. * * Transmission from client to server is done using the second available request. * * To cope with with the limit of connections only one such frame can be sent at time. This can * usually be coped with by bundling like seen in the {@link RpcDispatcher}. This is true for * both directions. * * The interface to the comet class is made to be as similiar to a normal websocket so a smooth * transition can be made sometime in the future. * @author Jes Andersen * */ @Singleton public class Comet implements WebSocket { private RequestCallback connectCallback = new RequestCallback() { @Override public void onError(Request request, Throwable exception) { status = WebSocketStatus.CLOSED; GWT.log("SOCKET-Could not open connection to portalserver", exception); CloseEvent.fire(Comet.this, Comet.this); } @Override public void onResponseReceived(Request request, Response response) { switch (response.getStatusCode()) { case Response.SC_CREATED: status = WebSocketStatus.OPEN; rxUrl = response.getText(); GWT.log("SOCKET-Connection to portalserver established"); OpenEvent.fire(Comet.this, Comet.this); poll(); break; default: onError(request, new Exception("Unknown status code returned from portalserver")); break; } } }; private RequestCallback txCallback = new RequestCallback() { @Override public void onError(Request request, Throwable exception) { GWT.log("SOCKET-Failed to send requests to portalserver", exception); } @Override public void onResponseReceived(Request request, Response response) { txRequest = null; switch (response.getStatusCode()) { case Response.SC_CREATED: case Response.SC_ACCEPTED: rxUrl = response.getText(); break; default: onError(request, new Exception("Unknown Status Code returned from portalserver")); } FrameSentEvent.fire(Comet.this); } }; private RequestCallback rxCallback = new RequestCallback() { @Override public void onError(Request request, Throwable exception) { GWT.log("SOCKET-Failure during communication with portalserver", exception); close(); } @Override public void onResponseReceived(Request request, Response response) { switch (response.getStatusCode()) { case Response.SC_OK: GWT.log("SOCKET-portalserver receiving @" + new Date().getTime() + " : " + response.getText()); FrameReceivedEvent.fire(Comet.this, response.getText()); poll(); break; case 0: case Response.SC_GONE: GWT.log("SOCKET-Lost connection to portalserver"); close(); break; default: onError(request, new Exception("Unknown status code returned from portal server when receiving responses")); break; } } }; private final HandlerManager eventBus; /** * We simulate the status of the connection to the server. */ WebSocketStatus status = WebSocketStatus.CLOSED; /** * This is the relative url to get messages from the server on */ protected String rxUrl; Request txRequest; private String url; private void poll() { if (!status.equals(WebSocketStatus.OPEN)) return; RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url + rxUrl); try { builder.sendRequest(null, rxCallback); } catch (RequestException e) { GWT.log("SOCKET-Failed to get responses to portalserver", e); } } /** * Creates the class with no sideeffects. * @param eventBus the eventbus to send {@link FrameReceivedEvent}, {@link FrameSentEvent}, {@link OpenEvent} and {@link CloseEvent} on. */ @Inject public Comet(HandlerManager eventBus) { this.eventBus = eventBus; } /** * listen to the connection lost messages */ @Override public HandlerRegistration addCloseHandler(CloseHandler<WebSocket> handler) { return eventBus.addHandler(CloseEvent.getType(), handler); } /** * listen to incoming frames */ @Override public HandlerRegistration addFrameReceivedHandler(FrameReceivedHandler handler) { return eventBus.addHandler(FrameReceivedEvent.getType(), handler); } /** * listen to frames successfully sent. * This is most useful for if you have a queue for frame for transmission since * only one frame can be transmitting at a time. */ @Override public HandlerRegistration addFrameSentHandler(FrameSentHandler handler) { return eventBus.addHandler(FrameSentEvent.getType(), handler); } /** * listen to when connections have actually been made. */ @Override public HandlerRegistration addOpenHandler(OpenHandler<WebSocket> handler) { return eventBus.addHandler(OpenEvent.getType(), handler); } /** * Closes the current connection */ @Override public void close() { //TODO: Abort tx and rx status = WebSocketStatus.CLOSED; rxUrl = null; CloseEvent.fire(this, this); } /** * The connect call POST an empty request to that and expects to receive a HTTP 201 Created * with the url of where it can receive server frames. * @param url when opening a connection this url is used * @param subprotocol not used yet, mostly here for compatibility with websocket protocol */ @Override public void connect(String url, String subprotocol) { if (status.equals(WebSocketStatus.CLOSED)) { this.url = url; RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, url); try { /* * for now we post not an real empty request but the json variant. * TODO: change the server so it can accept an empty body */ builder.sendRequest("[]", connectCallback); } catch (RequestException e) { GWT.log("SOCKET-Failed to connect to portalserver", e); } this.status = WebSocketStatus.CONNECTING; } } @Override public void fireEvent(GwtEvent<?> event) { eventBus.fireEvent(event); } @Override public boolean isConnected() { return status.equals(WebSocketStatus.OPEN); } /** * is currently transmitting a message so send() should not be called until this returns * false. add a {@link Comet#addFrameSentHandler(FrameSentHandler)} for a efficient * way of knowning when it is possible to send again. */ @Override public boolean isTxBusy() { return txRequest != null; } /** * send a frame to the server. should not be called if {@link Comet#isTxBusy()} returns * true, since it this creates too many connections to the server. */ @Override public void send(String s) { if (!status.equals(WebSocketStatus.OPEN)) return; RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, url + rxUrl); try { builder.sendRequest(s, txCallback); } catch (RequestException e) { GWT.log("SOCKET-Failed to send requests to portalserver", e); } GWT.log("SOCKET-portalserver sending @" + new Date().getTime() + " : " + s); FrameSentEvent.fire(this); } }