com.pusher.client.connection.websocket.WebSocketConnection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pusher-java-client Show documentation
Show all versions of pusher-java-client Show documentation
This is a Java client library for Pusher, targeted at core Java and Android.
package com.pusher.client.connection.websocket;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.net.ssl.SSLException;
import com.pusher.java_websocket.handshake.ServerHandshake;
import com.google.gson.Gson;
import com.pusher.client.connection.ConnectionEventListener;
import com.pusher.client.connection.ConnectionState;
import com.pusher.client.connection.ConnectionStateChange;
import com.pusher.client.connection.impl.InternalConnection;
import com.pusher.client.util.Factory;
public class WebSocketConnection implements InternalConnection, WebSocketListener {
private static final Logger log = Logger.getLogger(WebSocketConnection.class.getName());
private static final Gson GSON = new Gson();
private static final String INTERNAL_EVENT_PREFIX = "pusher:";
private static final String PING_EVENT_SERIALIZED = "{\"event\": \"pusher:ping\"}";
private static final int MAX_RECONNECTION_ATTEMPTS = 6; //Taken from the Swift lib
private static final int MAX_RECONNECT_GAP_IN_SECONDS = 30;
private final Factory factory;
private final ActivityTimer activityTimer;
private final Map> eventListeners = new ConcurrentHashMap>();
private final URI webSocketUri;
private final Proxy proxy;
private volatile ConnectionState state = ConnectionState.DISCONNECTED;
private WebSocketClientWrapper underlyingConnection;
private String socketId;
private int reconnectAttempts = 0;
public WebSocketConnection(
final String url,
final long activityTimeout,
final long pongTimeout,
final Proxy proxy,
final Factory factory) throws URISyntaxException {
webSocketUri = new URI(url);
activityTimer = new ActivityTimer(activityTimeout, pongTimeout);
this.proxy = proxy;
this.factory = factory;
for (final ConnectionState state : ConnectionState.values()) {
eventListeners.put(state, Collections.newSetFromMap(new ConcurrentHashMap()));
}
}
/* Connection implementation */
@Override
public void connect() {
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
if (state == ConnectionState.DISCONNECTED) {
tryConnecting();
}
}
});
}
private void tryConnecting(){
try {
underlyingConnection = factory
.newWebSocketClientWrapper(webSocketUri, proxy, WebSocketConnection.this);
updateState(ConnectionState.CONNECTING);
underlyingConnection.connect();
}
catch (final SSLException e) {
sendErrorToAllListeners("Error connecting over SSL", null, e);
}
}
@Override
public void disconnect() {
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
if (state == ConnectionState.CONNECTED) {
updateState(ConnectionState.DISCONNECTING);
underlyingConnection.close();
}
}
});
}
@Override
public void bind(final ConnectionState state, final ConnectionEventListener eventListener) {
eventListeners.get(state).add(eventListener);
}
@Override
public boolean unbind(final ConnectionState state, final ConnectionEventListener eventListener) {
return eventListeners.get(state).remove(eventListener);
}
@Override
public ConnectionState getState() {
return state;
}
/* InternalConnection implementation detail */
@Override
public void sendMessage(final String message) {
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
try {
if (state == ConnectionState.CONNECTED) {
underlyingConnection.send(message);
}
else {
sendErrorToAllListeners("Cannot send a message while in " + state + " state", null, null);
}
}
catch (final Exception e) {
sendErrorToAllListeners("An exception occurred while sending message [" + message + "]", null, e);
}
}
});
}
@Override
public String getSocketId() {
return socketId;
}
/* implementation detail */
private void updateState(final ConnectionState newState) {
log.fine("State transition requested, current [" + state + "], new [" + newState + "]");
final ConnectionStateChange change = new ConnectionStateChange(state, newState);
state = newState;
final Set interestedListeners = new HashSet();
interestedListeners.addAll(eventListeners.get(ConnectionState.ALL));
interestedListeners.addAll(eventListeners.get(newState));
for (final ConnectionEventListener listener : interestedListeners) {
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
listener.onConnectionStateChange(change);
}
});
}
}
private void handleEvent(final String event, final String wholeMessage) {
if (event.startsWith(INTERNAL_EVENT_PREFIX)) {
handleInternalEvent(event, wholeMessage);
}
else {
factory.getChannelManager().onMessage(event, wholeMessage);
}
}
private void handleInternalEvent(final String event, final String wholeMessage) {
if (event.equals("pusher:connection_established")) {
handleConnectionMessage(wholeMessage);
}
else if (event.equals("pusher:error")) {
handleError(wholeMessage);
}
}
@SuppressWarnings("rawtypes")
private void handleConnectionMessage(final String message) {
final Map jsonObject = GSON.fromJson(message, Map.class);
final String dataString = (String)jsonObject.get("data");
final Map dataMap = GSON.fromJson(dataString, Map.class);
socketId = (String)dataMap.get("socket_id");
if(state != ConnectionState.CONNECTED){
updateState(ConnectionState.CONNECTED);
}
reconnectAttempts = 0;
}
@SuppressWarnings("rawtypes")
private void handleError(final String wholeMessage) {
final Map json = GSON.fromJson(wholeMessage, Map.class);
final Object data = json.get("data");
Map dataMap;
if (data instanceof String) {
dataMap = GSON.fromJson((String)data, Map.class);
}
else {
dataMap = (Map)data;
}
final String message = (String)dataMap.get("message");
final Object codeObject = dataMap.get("code");
String code = null;
if (codeObject != null) {
code = String.valueOf(Math.round((Double)codeObject));
}
sendErrorToAllListeners(message, code, null);
}
private void sendErrorToAllListeners(final String message, final String code, final Exception e) {
final Set allListeners = new HashSet();
for (final Set listenersForState : eventListeners.values()) {
allListeners.addAll(listenersForState);
}
for (final ConnectionEventListener listener : allListeners) {
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
listener.onError(message, code, e);
}
});
}
}
/* WebSocketListener implementation */
@Override
public void onOpen(final ServerHandshake handshakedata) {
// TODO: log the handshake data
}
@Override
@SuppressWarnings("unchecked")
public void onMessage(final String message) {
activityTimer.activity();
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
final Map map = GSON.fromJson(message, Map.class);
final String event = map.get("event");
handleEvent(event, message);
}
});
}
@Override
public void onClose(final int code, final String reason, final boolean remote) {
if (state == ConnectionState.DISCONNECTED || state == ConnectionState.RECONNECTING) {
log.warning("Received close from underlying socket when already disconnected." + "Close code ["
+ code + "], Reason [" + reason + "], Remote [" + remote + "]");
return;
}
//Reconnection logic
if(state == ConnectionState.CONNECTED || state == ConnectionState.CONNECTING){
if(reconnectAttempts < MAX_RECONNECTION_ATTEMPTS){
tryReconnecting();
}
else{
updateState(ConnectionState.DISCONNECTING);
cancelTimeoutsAndTransitonToDisconnected();
}
return;
}
if (state == ConnectionState.DISCONNECTING){
cancelTimeoutsAndTransitonToDisconnected();
}
}
private void tryReconnecting() {
reconnectAttempts++;
updateState(ConnectionState.RECONNECTING);
long reconnectInterval = Math.min(MAX_RECONNECT_GAP_IN_SECONDS, reconnectAttempts * reconnectAttempts);
factory.getTimers().schedule(new Runnable() {
@Override
public void run() {
underlyingConnection.removeWebSocketListener();
tryConnecting();
}
}, reconnectInterval, TimeUnit.SECONDS);
}
private void cancelTimeoutsAndTransitonToDisconnected() {
activityTimer.cancelTimeouts();
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
updateState(ConnectionState.DISCONNECTED);
factory.shutdownThreads();
}
});
}
@Override
public void onError(final Exception ex) {
factory.queueOnEventThread(new Runnable() {
@Override
public void run() {
// Do not change connection state as Java_WebSocket will also
// call onClose.
// See:
// https://github.com/leggetter/pusher-java-client/issues/8#issuecomment-16128590
// updateState(ConnectionState.DISCONNECTED);
sendErrorToAllListeners("An exception was thrown by the websocket", null, ex);
}
});
}
private class ActivityTimer {
private final long activityTimeout;
private final long pongTimeout;
private Future> pingTimer;
private Future> pongTimer;
ActivityTimer(final long activityTimeout, final long pongTimeout) {
this.activityTimeout = activityTimeout;
this.pongTimeout = pongTimeout;
}
/**
* On any activity from the server - Cancel pong timeout - Cancel
* currently ping timeout and re-schedule
*/
synchronized void activity() {
if (pongTimer != null) {
pongTimer.cancel(true);
}
if (pingTimer != null) {
pingTimer.cancel(false);
}
pingTimer = factory.getTimers().schedule(new Runnable() {
@Override
public void run() {
log.fine("Sending ping");
sendMessage(PING_EVENT_SERIALIZED);
schedulePongCheck();
}
}, activityTimeout, TimeUnit.MILLISECONDS);
}
/**
* Cancel any pending timeouts, for example because we are disconnected.
*/
synchronized void cancelTimeouts() {
if (pingTimer != null) {
pingTimer.cancel(false);
}
if (pongTimer != null) {
pongTimer.cancel(false);
}
}
/**
* Called when a ping is sent to await the response - Cancel any
* existing timeout - Schedule new one
*/
private synchronized void schedulePongCheck() {
if (pongTimer != null) {
pongTimer.cancel(false);
}
pongTimer = factory.getTimers().schedule(new Runnable() {
@Override
public void run() {
log.fine("Timed out awaiting pong from server - disconnecting");
underlyingConnection.removeWebSocketListener();
disconnect();
// Proceed immediately to handle the close
// The WebSocketClient will attempt a graceful WebSocket shutdown by exchanging the close frames
// but may not succeed if this disconnect was called due to pong timeout...
onClose(-1, "Pong timeout", false);
}
}, pongTimeout, TimeUnit.MILLISECONDS);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy