package com.koushikdutta.async.http.socketio;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import com.koushikdutta.async.NullDataCallback;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.future.Cancellable;
import com.koushikdutta.async.future.DependentCancellable;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.async.future.TransformFuture;
import com.koushikdutta.async.http.AsyncHttpClient;
import com.koushikdutta.async.http.AsyncHttpResponse;
import com.koushikdutta.async.http.WebSocket;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
/**
* Created by koush on 7/1/13.
*/
class SocketIOConnection {
Handler handler;
AsyncHttpClient httpClient;
int heartbeat;
static ArrayList<SocketIOClient> clients = new ArrayList<SocketIOClient>();
static WebSocket webSocket;
SocketIORequest request;
public SocketIOConnection(Handler handler, AsyncHttpClient httpClient, SocketIORequest request) {
this.handler = handler;
this.httpClient = httpClient;
this.request = request;
}
public void setRequest(SocketIORequest request)
{
this.request = request;
}
public boolean isConnected() {
return webSocket != null && webSocket.isOpen();
}
Hashtable<String, Acknowledge> acknowledges = new Hashtable<String, Acknowledge>();
int ackCount;
public void emitRaw(int type, SocketIOClient client, String message, Acknowledge acknowledge) {
String ack = "";
if (acknowledge != null) {
String id = "" + ackCount++;
ack = id + "+";
acknowledges.put(id, acknowledge);
}
webSocket.send(String.format("%d:%s:%s:%s", type, ack, client.endpoint, message));
}
public void connect(SocketIOClient client) {
clients.add(client);
webSocket.send(String.format("1::%s", client.endpoint));
}
public void disconnect(SocketIOClient client) {
//clients.remove(client);
// see if we can leave this endpoint completely
boolean needsEndpointDisconnect = true;
for (SocketIOClient other: clients) {
// if this is the default endpoint (which disconnects everything),
// or another client is using this endpoint,
// we can't disconnect
if (TextUtils.equals(other.endpoint, client.endpoint) || TextUtils.isEmpty(client.endpoint)) {
needsEndpointDisconnect = false;
break;
}
}
clients.clear();
if(webSocket !=null)
{
if (needsEndpointDisconnect)
webSocket.send(String.format("0::%s", client.endpoint));
// and see if we can disconnect the socket completely
if (clients.size() > 0)
return;
webSocket.setStringCallback(null);
webSocket.setClosedCallback(null);
webSocket.close();
// webSocket = null;
}
}
Cancellable connecting;
void reconnect(final DependentCancellable child) {
if (isConnected()) {
return;
}
// if a connection is in progress, just wait.
if (connecting != null && !connecting.isDone() && !connecting.isCancelled()) {
if (child != null)
child.setParent(connecting);
return;
}
request.logi("Reconnecting socket.io");
// dont invoke onto main handler, as it is unnecessary until a session is ready or failed
request.setHandler(null);
Cancellable connecting = httpClient.executeString(request)
.then(new TransformFuture<WebSocket, String>() {
@Override
protected void transform(String result) throws Exception {
String[] parts = result.split(":");
String session = parts[0];
if (!"".equals(parts[1]))
heartbeat = Integer.parseInt(parts[1]) / 2 * 1000;
else
heartbeat = 0;
String transportsLine = parts[3];
String[] transports = transportsLine.split(",");
HashSet<String> set = new HashSet<String>(Arrays.asList(transports));
if (!set.contains("websocket"))
throw new Exception("websocket not supported");
String path=request.getUri().getPath();
String sessionUrl=request.getUri().toString();
// String sessionUrl = request.getUri().toString() + "websocket/" + session + "/";
int index =sessionUrl.indexOf('?');
if(index>-1)
{
sessionUrl=sessionUrl.replace("?", "websocket/" + session + "/?");
}
else
{
sessionUrl+="websocket/" + session + "/";
}
// ws://61.174.9.138:3080/socket.io/1/websocket/20M09pkCziKLscwZcAO4
// Log.d(" SocketIOConnection reconnect", "sessionUrl="+sessionUrl);
// Log.d(" SocketIOConnection reconnect", "heartbeat="+heartbeat);
httpClient.websocket(sessionUrl, null, null)
.setCallback(getCompletionCallback());
}
})
.setCallback(new FutureCallback<WebSocket>() {
@Override
public void onCompleted(Exception e, WebSocket result) {
if (e != null) {
reportDisconnect(e);
return;
}
reconnectDelay = 1000L;
//websocket 赋值
SocketIOConnection.this.webSocket = result;
attach();
}
});
if (child != null)
child.setParent(connecting);
}
void setupHeartbeat() {
final WebSocket ws = webSocket;
Runnable heartbeatRunner = new Runnable() {
@Override
public void run() {
if (heartbeat <= 0 || ws != webSocket || ws == null || !ws.isOpen())
return;
webSocket.send("2:::");
webSocket.getServer().postDelayed(this, heartbeat);
}
};
heartbeatRunner.run();
}
private interface SelectCallback {
void onSelect(SocketIOClient client);
}
private void select(String endpoint, SelectCallback callback) {
for (SocketIOClient client: clients) {
if (endpoint == null || TextUtils.equals(client.endpoint, endpoint)) {
callback.onSelect(client);
}
}
}
private void delayReconnect() {
if (webSocket != null || clients.size() == 0)
return;
// see if any client has disconnected,
// and that we need a reconnect
boolean disconnected = false;
for (SocketIOClient client: clients) {
if (client.disconnected) {
disconnected = true;
break;
}
}
if (!disconnected)
return;
httpClient.getServer().postDelayed(new Runnable() {
@Override
public void run() {
reconnect(null);
}
}, reconnectDelay);
reconnectDelay *= 2;
Log.d("delayReconnect", "reconnectDelay="+reconnectDelay);
}
long reconnectDelay = 4000L;
private void reportDisconnect(final Exception ex) {
if (ex != null) {
request.loge("socket.io disconnected", ex);
}
else {
request.logi("socket.io disconnected");
}
select(null, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
if (client.connected) {
client.disconnected = true;
DisconnectCallback closed = client.getDisconnectCallback();
if (closed != null)
closed.onDisconnect(ex);
}
else {
// client has never connected, this is a initial connect failure
ConnectCallback callback = client.connectCallback;
if (callback != null)
callback.onConnectCompleted(ex, client);
}
ReconnectCallback callback =client.reconnectCallback;
if(callback !=null)
callback.onReconnect();
}
});
//使用自己的延时重连机制。
// delayReconnect();
}
private void reportConnect(String endpoint) {
select(endpoint, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
if (client.isConnected())
return;
if (!client.connected) {
// normal connect
client.connected = true;
ConnectCallback callback = client.connectCallback;
if (callback != null)
callback.onConnectCompleted(null, client);
}
else if (client.disconnected) {
// reconnect
client.disconnected = false;
ReconnectCallback callback = client.reconnectCallback;
if (callback != null)
callback.onReconnect();
}
else {
// double connect?
// assert false;
}
}
});
}
private void reportJson(String endpoint, final JSONObject jsonMessage, final Acknowledge acknowledge) {
select(endpoint, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
JSONCallback callback = client.jsonCallback;
if (callback != null)
callback.onJSON(jsonMessage, acknowledge);
}
});
}
private void reportString(String endpoint, final String string, final Acknowledge acknowledge) {
select(endpoint, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
StringCallback callback = client.stringCallback;
if (callback != null)
callback.onString(string, acknowledge);
}
});
}
private void reportEvent(String endpoint, final String event, final JSONArray arguments, final Acknowledge acknowledge) {
select(endpoint, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
client.onEvent(event, arguments, acknowledge);
}
});
}
private void reportError(String endpoint, final String error) {
select(endpoint, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
ErrorCallback callback = client.errorCallback;
if (callback != null)
callback.onError(error);
}
});
}
private Acknowledge acknowledge(final String _messageId, final String endpoint) {
if (TextUtils.isEmpty(_messageId))
return null;
final String messageId = _messageId.replaceAll("\\+$", "");
return new Acknowledge() {
@Override
public void acknowledge(JSONArray arguments) {
String data = "";
if (arguments != null)
data += "+" + arguments.toString();
WebSocket webSocket = SocketIOConnection.this.webSocket;
if (webSocket == null) {
final Exception e = new SocketIOException("websocket is not connected");
select(endpoint, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
ExceptionCallback callback = client.exceptionCallback;
if (callback != null)
callback.onException(e);
}
});
return;
}
webSocket.send(String.format("6:::%s%s", messageId, data));
}
};
}
private void attach() {
setupHeartbeat();
webSocket.setDataCallback(new NullDataCallback());
webSocket.setClosedCallback(new CompletedCallback() {
@Override
public void onCompleted(final Exception ex) {
webSocket = null;
reportDisconnect(ex);
}
});
webSocket.setStringCallback(new WebSocket.StringCallback() {
@Override
public void onStringAvailable(String message) {
try {
// Log.d("attach", "Message: " + message);
String[] parts = message.split(":", 4);
int code = Integer.parseInt(parts[0]);
switch (code) {
case 0:
// disconnect
webSocket.close();
reportDisconnect(null);
break;
case 1:
// connect
reportConnect(parts[2]);
break;
case 2:
// heartbeat
webSocket.send("2::");
break;
case 3: {
// message
reportString(parts[2], parts[3], acknowledge(parts[1], parts[2]));
break;
}
case 4: {
//json message
final String dataString = parts[3];
final JSONObject jsonMessage = new JSONObject(dataString);
reportJson(parts[2], jsonMessage, acknowledge(parts[1], parts[2]));
break;
}
case 5: {
final String dataString = parts[3];
final JSONObject data = new JSONObject(dataString);
final String event = data.getString("name");
final JSONArray args = data.optJSONArray("args");
reportEvent(parts[2], event, args, acknowledge(parts[1], parts[2]));
break;
}
case 6:
// ACK
final String[] ackParts = parts[3].split("\\+", 2);
Acknowledge ack = acknowledges.remove(ackParts[0]);
if (ack == null)
return;
JSONArray arguments = null;
if (ackParts.length == 2)
arguments = new JSONArray(ackParts[1]);
ack.acknowledge(arguments);
break;
case 7:
// error
reportError(parts[2], parts[3]);
break;
case 8:
// noop
break;
default:
throw new Exception("unknown code");
}
}
catch (Exception ex) {
webSocket.setClosedCallback(null);
webSocket.close();
webSocket = null;
reportDisconnect(ex);
}
}
});
// now reconnect all the sockets that may have been previously connected
select(null, new SelectCallback() {
@Override
public void onSelect(SocketIOClient client) {
if (TextUtils.isEmpty(client.endpoint))
return;
connect(client);
}
});
}
}