Initial commit
This commit is contained in:
93
src/main/java/com/github/nkzawa/emitter/Emitter.java
Normal file
93
src/main/java/com/github/nkzawa/emitter/Emitter.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package com.github.nkzawa.emitter;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
public class Emitter {
|
||||
|
||||
private ConcurrentMap<String, ConcurrentLinkedQueue<Listener>> callbacks
|
||||
= new ConcurrentHashMap<String, ConcurrentLinkedQueue<Listener>>();
|
||||
|
||||
private ConcurrentMap<Listener, Listener> onceCallbacks = new ConcurrentHashMap<Listener, Listener>();
|
||||
|
||||
|
||||
public Emitter on(String event, Listener fn) {
|
||||
ConcurrentLinkedQueue<Listener> callbacks = this.callbacks.get(event);
|
||||
if (callbacks == null) {
|
||||
callbacks = new ConcurrentLinkedQueue <Listener>();
|
||||
ConcurrentLinkedQueue<Listener> _callbacks = this.callbacks.putIfAbsent(event, callbacks);
|
||||
if (_callbacks != null) {
|
||||
callbacks = _callbacks;
|
||||
}
|
||||
}
|
||||
callbacks.add(fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Emitter once(final String event, final Listener fn) {
|
||||
Listener on = new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
Emitter.this.off(event, this);
|
||||
fn.call(args);
|
||||
}
|
||||
};
|
||||
|
||||
this.onceCallbacks.put(fn, on);
|
||||
this.on(event, on);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Emitter off() {
|
||||
this.callbacks.clear();
|
||||
this.onceCallbacks.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Emitter off(String event) {
|
||||
ConcurrentLinkedQueue<Listener> callbacks = this.callbacks.remove(event);
|
||||
if (callbacks != null) {
|
||||
for (Listener fn : callbacks) {
|
||||
this.onceCallbacks.remove(fn);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Emitter off(String event, Listener fn) {
|
||||
ConcurrentLinkedQueue<Listener> callbacks = this.callbacks.get(event);
|
||||
if (callbacks != null) {
|
||||
Listener off = this.onceCallbacks.remove(fn);
|
||||
callbacks.remove(off != null ? off : fn);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Emitter emit(String event, Object... args) {
|
||||
ConcurrentLinkedQueue<Listener> callbacks = this.callbacks.get(event);
|
||||
if (callbacks != null) {
|
||||
callbacks = new ConcurrentLinkedQueue<Listener>(callbacks);
|
||||
for (Listener fn : callbacks) {
|
||||
fn.call(args);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<Listener> listeners(String event) {
|
||||
ConcurrentLinkedQueue<Listener> callbacks = this.callbacks.get(event);
|
||||
return new ArrayList<Listener>(callbacks);
|
||||
}
|
||||
|
||||
public boolean hasListeners(String event) {
|
||||
return !this.listeners(event).isEmpty();
|
||||
}
|
||||
|
||||
public static interface Listener {
|
||||
public void call(Object... args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.nkzawa.engineio.client;
|
||||
|
||||
|
||||
public class EngineIO {
|
||||
|
||||
public EngineIO() {}
|
||||
|
||||
}
|
||||
551
src/main/java/com/github/nkzawa/engineio/client/Socket.java
Normal file
551
src/main/java/com/github/nkzawa/engineio/client/Socket.java
Normal file
@@ -0,0 +1,551 @@
|
||||
package com.github.nkzawa.engineio.client;
|
||||
|
||||
import com.github.nkzawa.emitter.Emitter;
|
||||
import com.github.nkzawa.engineio.client.transports.Polling;
|
||||
import com.github.nkzawa.engineio.client.transports.PollingXHR;
|
||||
import com.github.nkzawa.engineio.client.transports.WebSocket;
|
||||
import com.github.nkzawa.engineio.parser.Packet;
|
||||
import com.github.nkzawa.engineio.parser.Parser;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.apache.http.Consts;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
public abstract class Socket extends Emitter {
|
||||
|
||||
private static final Logger logger = Logger.getLogger("engine.io-client:socket");
|
||||
|
||||
private static final Runnable noop = new Runnable() {
|
||||
@Override
|
||||
public void run() {}
|
||||
};
|
||||
|
||||
public static final Sockets sockets = new Sockets();
|
||||
public static final int protocol = Parser.protocol;
|
||||
|
||||
private boolean secure;
|
||||
private boolean upgrade;
|
||||
private boolean timestampRequests;
|
||||
private boolean upgrading;
|
||||
private int port;
|
||||
private int policyPort;
|
||||
private int prevBufferLen;
|
||||
private long pingInterval;
|
||||
private long pingTimeout;
|
||||
private String id;
|
||||
private String hostname;
|
||||
private String path;
|
||||
private String timestampParam;
|
||||
private String readyState = "";
|
||||
private List<String> transports;
|
||||
private List<String> upgrades;
|
||||
private List<NameValuePair> query;
|
||||
private ConcurrentLinkedQueue<Packet> writeBuffer = new ConcurrentLinkedQueue<Packet>();
|
||||
private ConcurrentLinkedQueue<Runnable> callbackBuffer = new ConcurrentLinkedQueue<Runnable>();
|
||||
private Transport transport;
|
||||
private Future pingTimeoutTimer;
|
||||
private Future pingIntervalTimer;
|
||||
|
||||
private ScheduledExecutorService heartbeatScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
|
||||
public Socket(String uri) throws URISyntaxException {
|
||||
this(uri, null);
|
||||
}
|
||||
|
||||
public Socket(URI uri) {
|
||||
this(uri, null);
|
||||
}
|
||||
|
||||
public Socket(String uri, Options opts) throws URISyntaxException {
|
||||
this(new URI(uri), opts);
|
||||
}
|
||||
|
||||
public Socket(URI uri, Options opts) {
|
||||
this(Options.fromURI(uri, opts));
|
||||
}
|
||||
|
||||
public Socket(Options opts) {
|
||||
if (opts.host != null) {
|
||||
String[] pieces = opts.host.split(":");
|
||||
opts.hostname = pieces[0];
|
||||
if (pieces.length > 1) {
|
||||
opts.port = Integer.parseInt(pieces[pieces.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
this.secure = opts.secure;
|
||||
this.hostname = opts.hostname != null ? opts.hostname : "localhost";
|
||||
this.port = opts.port != 0 ? opts.port : (this.secure ? 443 : 80);
|
||||
this.query = URLEncodedUtils.parse(opts.query, Consts.UTF_8);
|
||||
this.upgrade = opts.upgrade;
|
||||
this.path = (opts.path != null ? opts.path : "/engine.io").replaceAll("/$", "") + "/";
|
||||
this.timestampParam = opts.timestampParam != null ? opts.timestampParam : "t";
|
||||
this.timestampRequests = opts.timestampRequests;
|
||||
this.transports = new ArrayList<String>(Arrays.asList(
|
||||
opts.transports != null ? opts.transports : new String[] {"polling", "websocket"}));
|
||||
this.policyPort = opts.policyPort != 0 ? opts.policyPort : 843;
|
||||
|
||||
Socket.sockets.add(this);
|
||||
Socket.sockets.evs.emit("add", this);
|
||||
}
|
||||
|
||||
public Socket open() {
|
||||
this.readyState = "opening";
|
||||
Transport transport = this.createTransport(this.transports.get(0));
|
||||
this.setTransport(transport);
|
||||
transport.open();
|
||||
return this;
|
||||
}
|
||||
|
||||
private Transport createTransport(String name) {
|
||||
logger.info(String.format("creating transport '%s'", name));
|
||||
List<NameValuePair> query = new ArrayList<NameValuePair>(this.query);
|
||||
|
||||
query.add(new BasicNameValuePair("EIO", String.valueOf(Parser.protocol)));
|
||||
query.add(new BasicNameValuePair("transport", name));
|
||||
if (this.id != null) {
|
||||
query.add(new BasicNameValuePair("sid", this.id));
|
||||
}
|
||||
|
||||
Transport.Options opts = new Transport.Options();
|
||||
opts.hostname = this.hostname;
|
||||
opts.port = this.port;
|
||||
opts.secure = this.secure;
|
||||
opts.path = this.path;
|
||||
opts.query = query;
|
||||
opts.timestampRequests = this.timestampRequests;
|
||||
opts.timestampParam = this.timestampParam;
|
||||
opts.policyPort = this.policyPort;
|
||||
|
||||
if ("websocket".equals(name)) {
|
||||
return new WebSocket(opts);
|
||||
} else if ("polling".equals(name)) {
|
||||
return new PollingXHR(opts);
|
||||
}
|
||||
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
private void setTransport(Transport transport) {
|
||||
final Socket self = this;
|
||||
|
||||
if (this.transport != null) {
|
||||
logger.info("clearing existing transport");
|
||||
this.transport.off();
|
||||
}
|
||||
|
||||
this.transport = transport;
|
||||
|
||||
transport.on("drain", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
self.onDrain();
|
||||
}
|
||||
}).on("packet", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
self.onPacket(args.length > 0 ? (Packet) args[0] : null);
|
||||
}
|
||||
}).on("error", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
self.onError(args.length > 0 ? (Exception) args[0] : null);
|
||||
}
|
||||
}).on("close", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
self.onClose("transport close");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void probe(final String name) {
|
||||
logger.info(String.format("probing transport '%s'", name));
|
||||
final Transport[] transport = new Transport[] {this.createTransport(name)};
|
||||
final boolean[] failed = new boolean[] {false};
|
||||
final Socket self = this;
|
||||
|
||||
final Listener onerror = new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
if (failed[0]) return;
|
||||
|
||||
failed[0] = true;
|
||||
|
||||
// TODO: handle error
|
||||
Exception err = args.length > 0 ? (Exception)args[0] : null;
|
||||
Transport.TransportException error = new Transport.TransportException("probe error", err);
|
||||
error.transport = transport[0].name;
|
||||
|
||||
transport[0].close();
|
||||
transport[0] = null;
|
||||
logger.info(String.format("probing transport '%s' failed because of error: %s", name, err));
|
||||
self.emit("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
transport[0].once("open", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
if (failed[0]) return;
|
||||
|
||||
logger.info(String.format("probe transport '%s' opened", name));
|
||||
Packet packet = new Packet("ping", "probe");
|
||||
transport[0].send(new Packet[] {packet});
|
||||
transport[0].once("packet", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
if (failed[0]) return;
|
||||
Packet msg = (Packet)args[0];
|
||||
if ("pong".equals(msg.type) && "probe".equals(msg.data)) {
|
||||
logger.info(String.format("probe transport '%s' pong", name));
|
||||
self.upgrading = true;
|
||||
self.emit("upgrading", transport[0]);
|
||||
|
||||
logger.info(String.format("pausing current transport '%s'", self.transport.name));
|
||||
((Polling)self.transport).pause(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (failed[0]) return;
|
||||
if ("close".equals(self.readyState) ||
|
||||
"closing".equals(self.readyState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("changing transport and sending upgrade packet");
|
||||
transport[0].off("error", onerror);
|
||||
self.emit("upgrade", transport);
|
||||
self.setTransport(transport[0]);
|
||||
Packet packet = new Packet("upgrade", null);
|
||||
transport[0].send(new Packet[]{packet});
|
||||
transport[0] = null;
|
||||
self.upgrading = false;
|
||||
self.flush();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info(String.format("probe transport '%s' failed", name));
|
||||
Transport.TransportException err = new Transport.TransportException("probe error");
|
||||
err.transport = transport[0].name;
|
||||
self.emit("error", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
transport[0].once("error", onerror);
|
||||
|
||||
this.once("close", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
if (transport[0] != null) {
|
||||
logger.info("socket closed prematurely - aborting probe");
|
||||
failed[0] = true;
|
||||
transport[0].close();
|
||||
transport[0] = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.once("upgrading", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
Transport to = (Transport)args[0];
|
||||
if (transport[0] != null && !to.name.equals(transport[0].name)) {
|
||||
logger.info(String.format("'%s' works - aborting '%s'", to.name, transport[0].name));
|
||||
transport[0].close();
|
||||
transport[0] = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
transport[0].open();
|
||||
}
|
||||
|
||||
private void onOpen() {
|
||||
logger.info("socket open");
|
||||
this.readyState = "open";
|
||||
this.emit("open");
|
||||
this.onopen();
|
||||
this.flush();
|
||||
|
||||
if ("open".equals(this.readyState) && this.upgrade && this.transport instanceof Polling) {
|
||||
logger.info("starting upgrade probes");
|
||||
for (String upgrade: this.upgrades) {
|
||||
this.probe(upgrade);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onPacket(Packet packet) {
|
||||
if ("opening".equals(this.readyState) || "open".equals(this.readyState)) {
|
||||
logger.info(String.format("socket received: type '%s', data '%s'", packet.type, packet.data));
|
||||
|
||||
this.emit("packet", packet);
|
||||
this.emit("heartbeat");
|
||||
|
||||
if ("open".equals(packet.type)) {
|
||||
this.onHandshake(new JsonParser().parse(packet.data).getAsJsonObject());
|
||||
} else if ("pong".equals(packet.type)) {
|
||||
this.ping();
|
||||
} else if ("error".equals(packet.type)) {
|
||||
// TODO: handle error
|
||||
Exception err = new Exception("server error");
|
||||
// err.code = packet.data;
|
||||
this.emit("error", err);
|
||||
} else if ("message".equals(packet.type)) {
|
||||
this.emit("data", packet.data);
|
||||
this.emit("message", packet.data);
|
||||
this.onmessage(packet.data);
|
||||
}
|
||||
} else {
|
||||
logger.info(String.format("packet received with socket readyState '%s'", this.readyState));
|
||||
}
|
||||
}
|
||||
|
||||
private void onHandshake(JsonObject data) {
|
||||
this.emit("handshake", data);
|
||||
this.id = data.get("sid").getAsString();
|
||||
|
||||
Iterator<NameValuePair> i = this.transport.query.iterator();
|
||||
while (i.hasNext()) {
|
||||
NameValuePair pair = i.next();
|
||||
if ("sid".equals(pair.getName())) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
this.transport.query.add(new BasicNameValuePair("sid", data.get("sid").getAsString()));
|
||||
|
||||
List<String> upgrades = new ArrayList<String >();
|
||||
for (JsonElement upgrade : data.get("upgrades").getAsJsonArray()) {
|
||||
upgrades.add(upgrade.getAsString());
|
||||
}
|
||||
this.upgrades = this.filterUpgrades(upgrades);
|
||||
|
||||
this.pingInterval = data.get("pingInterval").getAsLong();
|
||||
this.pingTimeout = data.get("pingTimeout").getAsLong();
|
||||
this.onOpen();
|
||||
this.ping();
|
||||
|
||||
this.off("heartbear", this.onHeartbeatAsListener);
|
||||
this.on("heartbear", this.onHeartbeatAsListener);
|
||||
}
|
||||
|
||||
private final Listener onHeartbeatAsListener = new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
Socket.this.onHeartbeat(args.length > 0 ? (Long)args[0]: 0);
|
||||
}
|
||||
};
|
||||
|
||||
private synchronized void onHeartbeat(long timeout) {
|
||||
if (this.pingTimeoutTimer != null) {
|
||||
pingTimeoutTimer.cancel(true);
|
||||
}
|
||||
|
||||
if (timeout <= 0) {
|
||||
timeout = this.pingInterval + this.pingTimeout;
|
||||
}
|
||||
|
||||
final Socket self = this;
|
||||
this.pingTimeoutTimer = this.heartbeatScheduler.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if ("closed".equals(self.readyState)) return;
|
||||
self.onClose("ping timeout");
|
||||
}
|
||||
}, timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private synchronized void ping() {
|
||||
if (this.pingIntervalTimer != null) {
|
||||
pingIntervalTimer.cancel(true);
|
||||
}
|
||||
|
||||
final Socket self = this;
|
||||
this.pingIntervalTimer = this.heartbeatScheduler.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info(String.format("writing ping packet - expecting pong within %sms", self.pingTimeout));
|
||||
self.sendPacket("ping");
|
||||
self.onHeartbeat(self.pingTimeout);
|
||||
}
|
||||
}, this.pingInterval, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void onDrain() {
|
||||
this.callbacks();
|
||||
for (int i = 0; i < this.prevBufferLen; i++) {
|
||||
this.writeBuffer.poll();
|
||||
this.callbackBuffer.poll();
|
||||
}
|
||||
|
||||
this.prevBufferLen = 0;
|
||||
if (this.writeBuffer.size() == 0) {
|
||||
this.emit("drain");
|
||||
} else {
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void callbacks() {
|
||||
Iterator<Runnable> iter = this.callbackBuffer.iterator();
|
||||
for (int i = 0; i < this.prevBufferLen && iter.hasNext(); i++) {
|
||||
Runnable callback = iter.next();
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
if (!"closed".equals(this.readyState) && this.transport.writable &&
|
||||
!this.upgrading && this.writeBuffer.size() != 0) {
|
||||
logger.info(String.format("flushing %d packets in socket", this.writeBuffer.size()));
|
||||
this.prevBufferLen = this.writeBuffer.size();
|
||||
this.transport.send(this.writeBuffer.toArray(new Packet[0]));
|
||||
this.emit("flush");
|
||||
}
|
||||
}
|
||||
|
||||
public void write(String msg) {
|
||||
this.write(msg, null);
|
||||
}
|
||||
|
||||
public void write(String msg, Runnable fn) {
|
||||
this.send(msg, fn);
|
||||
}
|
||||
|
||||
public void send(String msg) {
|
||||
this.send(msg, null);
|
||||
}
|
||||
|
||||
public void send(String msg, Runnable fn) {
|
||||
this.sendPacket("message", msg, fn);
|
||||
}
|
||||
|
||||
private void sendPacket(String type) {
|
||||
this.sendPacket(type, null, null);
|
||||
}
|
||||
|
||||
private void sendPacket(String type, String data, Runnable fn) {
|
||||
if (fn == null) {
|
||||
// ConcurrentLinkedList does not permit `null`.
|
||||
fn = noop;
|
||||
}
|
||||
|
||||
Packet packet = new Packet(type, data);
|
||||
this.emit("packetCreate", packet);
|
||||
this.writeBuffer.offer(packet);
|
||||
this.callbackBuffer.offer(fn);
|
||||
this.flush();
|
||||
}
|
||||
|
||||
public Socket close() {
|
||||
if ("opening".equals(this.readyState) || "open".equals(this.readyState)) {
|
||||
this.onClose("forced close");
|
||||
logger.info("socket closing - telling transport to close");
|
||||
this.transport.close();
|
||||
this.transport.off();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void onError(Exception err) {
|
||||
logger.info(String.format("socket error %s", err));
|
||||
this.emit("error", err);
|
||||
this.onClose("transport error", err);
|
||||
}
|
||||
|
||||
private void onClose(String reason) {
|
||||
this.onClose(reason, null);
|
||||
}
|
||||
|
||||
private void onClose(String reason, Exception desc) {
|
||||
if ("opening".equals(this.readyState) || "open".equals(this.readyState)) {
|
||||
logger.info(String.format("socket close with reason: %s", reason));
|
||||
if (this.pingIntervalTimer != null) {
|
||||
this.pingIntervalTimer.cancel(true);
|
||||
}
|
||||
if (this.pingTimeoutTimer != null) {
|
||||
this.pingTimeoutTimer.cancel(true);
|
||||
}
|
||||
this.readyState = "closed";
|
||||
this.emit("close", reason, desc);
|
||||
this.onclose();
|
||||
// TODO:
|
||||
// clean buffer in next tick, so developers can still
|
||||
// gra the buffers on `close` event
|
||||
// setTimeout(function() {}
|
||||
// self.writeBuffer = [];
|
||||
// self.callbackBuffer = [];
|
||||
// );
|
||||
this.writeBuffer.clear();
|
||||
this.callbackBuffer.clear();
|
||||
this.id = null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String > filterUpgrades(List<String> upgrades) {
|
||||
List<String> filteredUpgrades = new ArrayList<String>();
|
||||
for (String upgrade : upgrades) {
|
||||
if (this.transports.contains(upgrade)) {
|
||||
filteredUpgrades.add(upgrade);
|
||||
}
|
||||
}
|
||||
return filteredUpgrades;
|
||||
}
|
||||
|
||||
public abstract void onopen();
|
||||
|
||||
public abstract void onmessage(String data);
|
||||
|
||||
public abstract void onclose();
|
||||
|
||||
public static class Options extends Transport.Options {
|
||||
|
||||
public String host;
|
||||
public String query;
|
||||
public String[] transports;
|
||||
public boolean upgrade = true;
|
||||
|
||||
|
||||
private static Options fromURI(URI uri, Options opts) {
|
||||
if (opts == null) {
|
||||
opts = new Options();
|
||||
}
|
||||
|
||||
opts.host = uri.getHost();
|
||||
opts.secure = "https".equals(uri.getScheme()) || "wss".equals(uri.getScheme());
|
||||
opts.port = uri.getPort();
|
||||
|
||||
String query = uri.getQuery();
|
||||
if (query != null) {
|
||||
opts.query = uri.getQuery();
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Sockets extends ConcurrentLinkedQueue<Socket> {
|
||||
|
||||
public Emitter evs = new Emitter();
|
||||
}
|
||||
}
|
||||
123
src/main/java/com/github/nkzawa/engineio/client/Transport.java
Normal file
123
src/main/java/com/github/nkzawa/engineio/client/Transport.java
Normal file
@@ -0,0 +1,123 @@
|
||||
package com.github.nkzawa.engineio.client;
|
||||
|
||||
|
||||
import com.github.nkzawa.emitter.Emitter;
|
||||
import com.github.nkzawa.engineio.parser.Packet;
|
||||
import com.github.nkzawa.engineio.parser.Parser;
|
||||
import org.apache.http.NameValuePair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Transport extends Emitter {
|
||||
|
||||
public boolean writable;
|
||||
public String name;
|
||||
public List<NameValuePair> query;
|
||||
|
||||
protected boolean secure;
|
||||
protected boolean timestampRequests;
|
||||
protected int port;
|
||||
protected String path;
|
||||
protected String hostname;
|
||||
protected String timestampParam;
|
||||
protected String readyState = "";
|
||||
|
||||
public Transport() {
|
||||
// TODO: remove
|
||||
}
|
||||
|
||||
public Transport(Options opts) {
|
||||
this.path = opts.path;
|
||||
this.hostname = opts.hostname;
|
||||
this.port = opts.port;
|
||||
this.secure = opts.secure;
|
||||
this.query = opts.query;
|
||||
this.timestampParam = opts.timestampParam;
|
||||
this.timestampRequests = opts.timestampRequests;
|
||||
}
|
||||
|
||||
protected Transport onError(String msg, Exception desc) {
|
||||
// TODO:
|
||||
Exception err = new Exception(msg);
|
||||
// err.type = "TransportError";
|
||||
// err.description = desc;
|
||||
this.emit("error", err);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Emitter open() {
|
||||
if ("closed".equals(this.readyState) || "".equals(this.readyState)) {
|
||||
this.readyState = "opening";
|
||||
this.doOpen();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Transport close() {
|
||||
if ("opening".equals(this.readyState) || "open".equals(this.readyState)) {
|
||||
this.doClose();
|
||||
this.onClose();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void send(Packet[] packets) {
|
||||
if ("open".equals(this.readyState)) {
|
||||
this.write(packets);
|
||||
} else {
|
||||
throw new RuntimeException("Transport not open");
|
||||
}
|
||||
}
|
||||
|
||||
protected void onOpen() {
|
||||
this.readyState = "open";
|
||||
this.writable = true;
|
||||
this.emit("open");
|
||||
}
|
||||
|
||||
protected void onData(String data) {
|
||||
this.onPacket(Parser.decodePacket(data));
|
||||
}
|
||||
|
||||
protected void onPacket(Packet packet) {
|
||||
this.emit("packet", packet);
|
||||
}
|
||||
|
||||
protected void onClose() {
|
||||
this.readyState = "closed";
|
||||
this.emit("close");
|
||||
}
|
||||
|
||||
abstract protected void write(Packet[] packets);
|
||||
|
||||
abstract protected void doOpen();
|
||||
|
||||
abstract protected void doClose();
|
||||
|
||||
|
||||
public static class Options {
|
||||
|
||||
public String hostname;
|
||||
public String path;
|
||||
public String timestampParam;
|
||||
public boolean secure;
|
||||
public boolean timestampRequests;
|
||||
public int port;
|
||||
public int policyPort;
|
||||
public List<NameValuePair> query;
|
||||
|
||||
}
|
||||
|
||||
public static class TransportException extends Exception {
|
||||
|
||||
public String transport;
|
||||
|
||||
public TransportException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public TransportException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.github.nkzawa.engineio.client.transports;
|
||||
|
||||
|
||||
import com.github.nkzawa.engineio.client.Transport;
|
||||
import com.github.nkzawa.engineio.parser.Packet;
|
||||
import com.github.nkzawa.engineio.parser.Parser;
|
||||
import org.apache.http.Consts;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
abstract public class Polling extends Transport {
|
||||
|
||||
private static final Logger logger = Logger.getLogger("engine.io-client:polling");
|
||||
|
||||
private boolean polling;
|
||||
|
||||
|
||||
public Polling(Options opts) {
|
||||
super(opts);
|
||||
this.name = "polling";
|
||||
}
|
||||
|
||||
protected void doOpen() {
|
||||
this.poll();
|
||||
}
|
||||
|
||||
public void pause(final Runnable onPause) {
|
||||
int pending = 0;
|
||||
final Polling self = this;
|
||||
|
||||
this.readyState = "paused";
|
||||
|
||||
final Runnable pause = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("paused");
|
||||
self.readyState = "paused";
|
||||
onPause.run();
|
||||
}
|
||||
};
|
||||
|
||||
if (this.polling || !this.writable) {
|
||||
final int[] total = new int[] {0};
|
||||
|
||||
if (this.polling) {
|
||||
logger.info("we are currently polling - waiting to pause");
|
||||
total[0]++;
|
||||
this.once("pollComplete", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
logger.info("pre-pause polling complete");
|
||||
if (--total[0] == 0) {
|
||||
pause.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.writable) {
|
||||
logger.info("we are currently writing - waiting to pause");
|
||||
total[0]++;
|
||||
this.once("drain", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
logger.info("pre-pause writing complete");
|
||||
if (--total[0] == 0) {
|
||||
pause.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
pause.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void poll() {
|
||||
logger.info("polling");
|
||||
this.polling = true;
|
||||
this.doPoll();
|
||||
this.emit("poll");
|
||||
}
|
||||
|
||||
protected void onData(String data) {
|
||||
final Polling self = this;
|
||||
logger.info(String.format("polling got data %s", data));
|
||||
|
||||
Parser.decodePayload(data, new Parser.DecodePayloadCallback() {
|
||||
@Override
|
||||
public boolean call(Packet packet, int index, int total) {
|
||||
if ("opening".equals(self.readyState)) {
|
||||
self.onOpen();
|
||||
}
|
||||
|
||||
if ("close".equals(packet.type)) {
|
||||
self.onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
self.onPacket(packet);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!"closed".equals(this.readyState)) {
|
||||
this.polling = false;
|
||||
this.emit("pollComplete");
|
||||
|
||||
if ("open".equals(this.readyState)) {
|
||||
this.poll();
|
||||
} else {
|
||||
logger.info(String.format("ignoring poll - transport state '%s'", this.readyState));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void doClose() {
|
||||
logger.info("sending close packet");
|
||||
this.send(new Packet[] {new Packet("close", null)});
|
||||
}
|
||||
|
||||
protected void write(Packet[] packets) {
|
||||
final Polling self = this;
|
||||
this.writable = false;
|
||||
this.doWrite(Parser.encodePayload(packets), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
self.writable = true;
|
||||
self.emit("drain");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected String uri() {
|
||||
List<NameValuePair> query = this.query;
|
||||
if (query == null) {
|
||||
query = new ArrayList<NameValuePair>();
|
||||
}
|
||||
String schema = this.secure ? "https" : "http";
|
||||
String port = "";
|
||||
|
||||
if (this.timestampRequests) {
|
||||
Iterator<NameValuePair> i = query.iterator();
|
||||
while (i.hasNext()) {
|
||||
NameValuePair pair = i.next();
|
||||
if (this.timestampParam.equals(pair.getName())) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
query.add(new BasicNameValuePair(this.timestampParam,
|
||||
String.valueOf(new Date().getTime())));
|
||||
}
|
||||
|
||||
String _query = URLEncodedUtils.format(query, Consts.UTF_8);
|
||||
|
||||
if (this.port > 0 && (("https".equals(schema) && this.port != 443)
|
||||
|| ("http".equals(schema) && this.port != 80))) {
|
||||
port = ":" + this.port;
|
||||
}
|
||||
|
||||
if (_query.length() > 0) {
|
||||
_query = "?" + _query;
|
||||
}
|
||||
|
||||
return new StringBuilder()
|
||||
.append(schema).append("://").append(this.hostname)
|
||||
.append(port).append(this.path).append(_query).toString();
|
||||
}
|
||||
|
||||
abstract protected void doWrite(String data, Runnable fn);
|
||||
|
||||
abstract protected void doPoll();
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.github.nkzawa.engineio.client.transports;
|
||||
|
||||
|
||||
import com.github.nkzawa.emitter.Emitter;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class PollingXHR extends Polling {
|
||||
|
||||
private static final Logger logger = Logger.getLogger("engine.io-client:polling-xhr");
|
||||
|
||||
private Request sendXhr;
|
||||
private Request pollXhr;
|
||||
|
||||
public PollingXHR(Options opts) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
protected Request request() {
|
||||
return this.request(null);
|
||||
}
|
||||
|
||||
protected Request request(Request.Options opts) {
|
||||
if (opts == null) {
|
||||
opts = new Request.Options();
|
||||
}
|
||||
opts.uri = this.uri();
|
||||
return new Request(opts);
|
||||
}
|
||||
|
||||
protected void doWrite(String data, final Runnable fn) {
|
||||
Request.Options opts = new Request.Options();
|
||||
opts.method = "POST";
|
||||
opts.data = data;
|
||||
Request req = this.request(opts);
|
||||
final PollingXHR self = this;
|
||||
req.on("success", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
fn.run();
|
||||
}
|
||||
});
|
||||
req.on("error", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
Exception err = args.length > 0 && args[0] instanceof Exception ? (Exception)args[0] : null;
|
||||
self.onError("xhr post error", err);
|
||||
}
|
||||
});
|
||||
req.create();
|
||||
this.sendXhr = req;
|
||||
}
|
||||
|
||||
protected void doPoll() {
|
||||
logger.info("xhr poll");
|
||||
Request req = this.request();
|
||||
final PollingXHR self = this;
|
||||
req.on("data", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
String data = args.length > 0 ? (String)args[0] : null;
|
||||
self.onData(data);
|
||||
}
|
||||
});
|
||||
req.on("error", new Listener() {
|
||||
@Override
|
||||
public void call(Object... args) {
|
||||
Exception err = args.length > 0 && args[0] instanceof Exception ? (Exception)args[0] : null;
|
||||
self.onError("xhr poll error", err);
|
||||
}
|
||||
});
|
||||
req.create();
|
||||
this.pollXhr = req;
|
||||
}
|
||||
|
||||
private static class Request extends Emitter {
|
||||
|
||||
String method;
|
||||
String uri;
|
||||
String data;
|
||||
HttpURLConnection xhr;
|
||||
|
||||
public Request(Options opts) {
|
||||
this.method = opts.method != null ? opts.method : "GET";
|
||||
this.uri = opts.uri;
|
||||
this.data = opts.data;
|
||||
}
|
||||
|
||||
public void create() {
|
||||
final Request self = this;
|
||||
try {
|
||||
URL url = new URL(this.uri);
|
||||
xhr = (HttpURLConnection)url.openConnection();
|
||||
xhr.setRequestMethod(this.method);
|
||||
} catch (IOException e) {
|
||||
this.onError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("POST".equals(this.method)) {
|
||||
xhr.setDoOutput(true);
|
||||
xhr.setRequestProperty("Content-type", "text/plain;charset=UTF-8");
|
||||
}
|
||||
|
||||
logger.info(String.format("sending xhr with url %s | data %s", this.uri, this.data));
|
||||
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
if (this.data != null) {
|
||||
byte[] data = this.data.getBytes("UTF-8");
|
||||
xhr.setFixedLengthStreamingMode(data.length);
|
||||
xhr.getOutputStream().write(data);
|
||||
}
|
||||
|
||||
String line;
|
||||
StringBuilder data = new StringBuilder();
|
||||
reader = new BufferedReader(new InputStreamReader(xhr.getInputStream()));
|
||||
while ((line = reader.readLine()) != null) {
|
||||
data.append(line);
|
||||
}
|
||||
this.onData(data.toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
this.onError(e);
|
||||
} finally {
|
||||
try {
|
||||
if (reader != null) reader.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
public void onSuccess() {
|
||||
this.emit("success");
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
public void onData(String data) {
|
||||
this.emit("data", data);
|
||||
this.onSuccess();
|
||||
}
|
||||
|
||||
public void onError(Exception err) {
|
||||
this.emit("error", err);
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
if (xhr != null) {
|
||||
xhr.disconnect();
|
||||
xhr = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void abort() {
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
public static class Options {
|
||||
|
||||
String uri;
|
||||
String method;
|
||||
String data;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.github.nkzawa.engineio.client.transports;
|
||||
|
||||
|
||||
import com.github.nkzawa.engineio.client.Transport;
|
||||
import com.github.nkzawa.engineio.parser.Packet;
|
||||
import com.github.nkzawa.engineio.parser.Parser;
|
||||
import org.apache.http.Consts;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.java_websocket.client.WebSocketClient;
|
||||
import org.java_websocket.drafts.Draft_17;
|
||||
import org.java_websocket.handshake.ServerHandshake;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class WebSocket extends Transport {
|
||||
|
||||
private WebSocketClient socket;
|
||||
private Future bufferedAmountId;
|
||||
|
||||
private ScheduledExecutorService drainScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
|
||||
public WebSocket(Options opts) {
|
||||
super(opts);
|
||||
this.name = "websocket";
|
||||
}
|
||||
|
||||
protected void doOpen() {
|
||||
if (!this.check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final WebSocket self = this;
|
||||
try {
|
||||
this.socket = new WebSocketClient(new URI(this.uri()), new Draft_17()) {
|
||||
@Override
|
||||
public void onOpen(ServerHandshake serverHandshake) {
|
||||
self.onOpen();
|
||||
}
|
||||
@Override
|
||||
public void onClose(int i, String s, boolean b) {
|
||||
self.onClose();
|
||||
}
|
||||
@Override
|
||||
public void onMessage(String s) {
|
||||
self.onData(s);
|
||||
}
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
self.onError("websocket error", e);
|
||||
}
|
||||
};
|
||||
this.socket.connect();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void write(Packet[] packets) {
|
||||
final WebSocket self = this;
|
||||
this.writable = false;
|
||||
for (Packet packet : packets) {
|
||||
this.socket.send(Parser.encodePacket(packet));
|
||||
}
|
||||
|
||||
final Runnable ondrain = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
self.writable = true;
|
||||
self.emit("drain");
|
||||
}
|
||||
};
|
||||
|
||||
if (this.socket.getConnection().hasBufferedData()) {
|
||||
this.bufferedAmountId = this.drainScheduler.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!self.socket.getConnection().hasBufferedData()) {
|
||||
self.bufferedAmountId.cancel(true);
|
||||
ondrain.run();
|
||||
}
|
||||
}
|
||||
}, 50, 50, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
this.drainScheduler.schedule(ondrain, 0, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
if (this.bufferedAmountId != null) {
|
||||
this.bufferedAmountId.cancel(true);
|
||||
}
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
protected void doClose() {
|
||||
if (this.socket != null) {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
private String uri() {
|
||||
List<NameValuePair> query = this.query;
|
||||
if (query == null) {
|
||||
query = new ArrayList<NameValuePair>();
|
||||
}
|
||||
String schema = this.secure ? "wss" : "ws";
|
||||
String port = "";
|
||||
|
||||
if (this.port > 0 && (("wss".equals(schema) && this.port != 443)
|
||||
|| ("ws".equals(schema) && this.port != 80))) {
|
||||
port = ":" + this.port;
|
||||
}
|
||||
|
||||
if (this.timestampRequests) {
|
||||
Iterator<NameValuePair> i = query.iterator();
|
||||
while (i.hasNext()) {
|
||||
NameValuePair pair = i.next();
|
||||
if (this.timestampParam.equals(pair.getName())) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
query.add(new BasicNameValuePair(this.timestampParam,
|
||||
String.valueOf(new Date().getTime())));
|
||||
}
|
||||
|
||||
String _query = URLEncodedUtils.format(query, Consts.UTF_8);
|
||||
if (_query.length() > 0) {
|
||||
_query = "?" + _query;
|
||||
}
|
||||
|
||||
return new StringBuilder()
|
||||
.append(schema).append("://").append(this.hostname)
|
||||
.append(port).append(this.path).append(_query).toString();
|
||||
}
|
||||
|
||||
private boolean check() {
|
||||
// for checking if the websocket is available. Should we remove?
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
19
src/main/java/com/github/nkzawa/engineio/parser/Packet.java
Normal file
19
src/main/java/com/github/nkzawa/engineio/parser/Packet.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.github.nkzawa.engineio.parser;
|
||||
|
||||
|
||||
public class Packet {
|
||||
|
||||
public String type;
|
||||
public String data;
|
||||
|
||||
public Packet(String type, String data) {
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("{\"type\": \"%s\", \"data\": \"%s\"}", this.type, this.data);
|
||||
}
|
||||
|
||||
}
|
||||
116
src/main/java/com/github/nkzawa/engineio/parser/Parser.java
Normal file
116
src/main/java/com/github/nkzawa/engineio/parser/Parser.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package com.github.nkzawa.engineio.parser;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Parser {
|
||||
|
||||
public static final int protocol = 2;
|
||||
public static final Map<String, Integer> packets = new HashMap<String, Integer>() {{
|
||||
put("open", 0);
|
||||
put("close", 1);
|
||||
put("ping", 2);
|
||||
put("pong", 3);
|
||||
put("message", 4);
|
||||
put("upgrade", 5);
|
||||
put("noop", 6);
|
||||
}};
|
||||
public static final Map<Integer, String> bipackets = new HashMap<Integer, String>();
|
||||
static {
|
||||
for (Map.Entry<String, Integer> entry : packets.entrySet()) {
|
||||
bipackets.put(entry.getValue(), entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private static Packet err = new Packet("error", "parser error");
|
||||
|
||||
|
||||
private Parser() {}
|
||||
|
||||
public static String encodePacket(Packet packet) {
|
||||
String encoded = String.valueOf(packets.get(packet.type));
|
||||
|
||||
if (packet.data != null) {
|
||||
encoded += packet.data;
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
public static Packet decodePacket(String data) {
|
||||
int type = Character.getNumericValue(data.charAt(0));
|
||||
if (type < 0 || type >= packets.size()) {
|
||||
return err;
|
||||
}
|
||||
|
||||
return new Packet(bipackets.get(type), data.length() > 1 ? data.substring(1) : null);
|
||||
}
|
||||
|
||||
public static String encodePayload(Packet[] packets) {
|
||||
if (packets.length == 0) {
|
||||
return "0:";
|
||||
}
|
||||
|
||||
StringBuilder encoded = new StringBuilder();
|
||||
for (Packet packet : packets) {
|
||||
String message = encodePacket(packet);
|
||||
encoded.append(message.length()).append(":").append(message);
|
||||
}
|
||||
|
||||
return encoded.toString();
|
||||
}
|
||||
|
||||
public static void decodePayload(String data, DecodePayloadCallback callback) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
callback.call(err, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder length = new StringBuilder();
|
||||
for (int i = 0, l = data.length(); i < l; i++) {
|
||||
char chr = data.charAt(i);
|
||||
|
||||
if (':' != chr) {
|
||||
length.append(chr);
|
||||
} else {
|
||||
int n;
|
||||
try {
|
||||
n = Integer.parseInt(length.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
callback.call(err, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
String msg;
|
||||
try {
|
||||
msg = data.substring(i + 1, i + 1 + n);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
callback.call(err, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.length() != 0) {
|
||||
Packet packet = decodePacket(msg);
|
||||
if (err.type.equals(packet.type) && err.data.equals(packet.data)) {
|
||||
callback.call(err, 0, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean ret = callback.call(packet, i + n, l);
|
||||
if (!ret) return;
|
||||
}
|
||||
|
||||
i += n;
|
||||
length = new StringBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
if (length.length() > 0) {
|
||||
callback.call(err, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static interface DecodePayloadCallback {
|
||||
public boolean call(Packet packet, int index, int total);
|
||||
}
|
||||
}
|
||||
134
src/test/java/com/github/nkzawa/engineio/client/SocketTest.java
Normal file
134
src/test/java/com/github/nkzawa/engineio/client/SocketTest.java
Normal file
@@ -0,0 +1,134 @@
|
||||
package com.github.nkzawa.engineio.client;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class SocketTest {
|
||||
|
||||
final static int PORT = 3000;
|
||||
|
||||
private Process serverProcess;
|
||||
private ExecutorService serverService;
|
||||
private Future serverOutout;
|
||||
private Future serverError;
|
||||
private Socket socket;
|
||||
|
||||
@Before
|
||||
public void startServer() throws IOException, InterruptedException {
|
||||
System.out.println("Starting server ...");
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
serverProcess = Runtime.getRuntime().exec(
|
||||
"node src/test/resources/index.js " + PORT, new String[] {"DEBUG=engine*"});
|
||||
serverService = Executors.newCachedThreadPool();
|
||||
serverOutout = serverService.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(serverProcess.getInputStream()));
|
||||
String line;
|
||||
try {
|
||||
line = reader.readLine();
|
||||
latch.countDown();
|
||||
do {
|
||||
System.out.println("SERVER OUT: " + line);
|
||||
} while ((line = reader.readLine()) != null);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
serverError = serverService.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(serverProcess.getErrorStream()));
|
||||
String line;
|
||||
try {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.err.println("SERVER ERR: " + line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
latch.await(3000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@After
|
||||
public void stopServer() throws InterruptedException {
|
||||
System.out.println("Stopping server ...");
|
||||
serverProcess.destroy();
|
||||
serverOutout.cancel(true);
|
||||
serverError.cancel(true);
|
||||
serverService.shutdown();
|
||||
serverService.awaitTermination(3000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openAndClose() throws URISyntaxException, InterruptedException {
|
||||
final BlockingQueue<String> events = new LinkedBlockingQueue<String>();
|
||||
|
||||
socket = new Socket("ws://localhost:" + PORT) {
|
||||
@Override
|
||||
public void onopen() {
|
||||
System.out.println("onopen:");
|
||||
events.offer("onopen");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onmessage(String data) {}
|
||||
|
||||
@Override
|
||||
public void onclose() {
|
||||
System.out.println("onclose:");
|
||||
events.offer("onclose");
|
||||
}
|
||||
}.open();
|
||||
|
||||
assertThat(events.take(), is("onopen"));
|
||||
socket.close();
|
||||
assertThat(events.take(), is("onclose"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void messages() throws URISyntaxException, InterruptedException {
|
||||
final BlockingQueue<String> events = new LinkedBlockingQueue<String>();
|
||||
|
||||
socket = new Socket("ws://localhost:" + PORT) {
|
||||
@Override
|
||||
public void onopen() {
|
||||
System.out.println("onopen:");
|
||||
socket.send("hi");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onmessage(String data) {
|
||||
System.out.println("onmessage: " + data);
|
||||
events.offer(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onclose() {}
|
||||
};
|
||||
socket.open();
|
||||
|
||||
assertThat(events.take(), is("hello client"));
|
||||
assertThat(events.take(), is("hi"));
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
31
src/test/resources/index.js
Normal file
31
src/test/resources/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
var engine = require('engine.io')
|
||||
, port = parseInt(process.argv[2], 10) || 3000
|
||||
, server = engine.listen(port, function() {
|
||||
console.log('Engine.IO server listening on port', port);
|
||||
});
|
||||
|
||||
server.on('connection', function(socket) {
|
||||
socket.send('hello client');
|
||||
|
||||
socket.on('packet', function(packet) {
|
||||
console.log('packet:', packet);
|
||||
});
|
||||
|
||||
socket.on('packetCreate', function(packet) {
|
||||
console.log('packetCreate:', packet);
|
||||
});
|
||||
|
||||
socket.on('message', function(message) {
|
||||
console.log('message:', message);
|
||||
socket.send(message);
|
||||
});
|
||||
|
||||
socket.on('close', function(reason, desc) {
|
||||
console.log('close:', reason, desc);
|
||||
});
|
||||
|
||||
socket.on('error', function(err) {
|
||||
console.log('error:', err);
|
||||
});
|
||||
});
|
||||
|
||||
8
src/test/resources/package.json
Normal file
8
src/test/resources/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "engine.io-client.java-test",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"engine.io": "0.5.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user