Initial commit

This commit is contained in:
Naoyuki Kanezawa
2013-04-29 01:55:13 +09:00
commit 29f179ed4e
18 changed files with 1281 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
*.class
# Package Files #
*.jar
*.war
*.ear
# Intellij project files
*.iml
*.ipr
*.iws
.idea/
.DS_Store
target/
node_modules/

4
.travis.yml Normal file
View File

@@ -0,0 +1,4 @@
language: java
jdk:
- openjdk7
- oraclejdk7

32
LICENSE Normal file
View File

@@ -0,0 +1,32 @@
The MIT License (MIT)
Copyright (c) 2013 Naoyuki Kanezawa
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
(The MIT License)
Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
# Socket.IO-client.java
This is the Socket.IO 1.0 Client Library for Java.
## License
MIT

69
pom.xml Normal file
View File

@@ -0,0 +1,69 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.nkzawa</groupId>
<artifactId>socket.io-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<repositories>
<repository>
<id>sonatype-oss-public</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.nkzawa</groupId>
<artifactId>engine.io-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<id>npm-install</id>
<phase>process-test-resources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<workingDirectory>./src/test/resources</workingDirectory>
<executable>npm</executable>
<arguments>
<argument>install</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,19 @@
package com.github.nkzawa.socketio.client;
import java.net.URI;
class Engine extends com.github.nkzawa.engineio.client.Socket {
Engine(URI uri, Options opts) {
super(uri, opts);
}
@Override
public void onopen() {}
@Override
public void onmessage(String s) {}
@Override
public void onclose() {}
}

View File

@@ -0,0 +1,73 @@
package com.github.nkzawa.socketio.client;
import com.github.nkzawa.socketio.parser.Parser;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public class IO {
private static final Map<String, Manager> managers = new HashMap<String, Manager>();
public static int protocol = Parser.protocol;
private IO() {}
public static Socket socket(String uri) throws URISyntaxException {
return socket(uri, null);
}
public static Socket socket(String uri, Options opts) throws URISyntaxException {
return socket(new URI(uri), opts);
}
public static Socket socket(URI uri) throws URISyntaxException {
return socket(uri, null);
}
public static Socket socket(URI uri, Options opts) throws URISyntaxException {
if (opts == null) {
opts = new Options();
}
URL parsed;
try {
parsed = Url.parse(uri);
} catch (MalformedURLException e) {
throw new URISyntaxException(uri.toString(), e.getMessage());
}
URI href = parsed.toURI();
Manager io;
if (opts.forceNew || !opts.multiplex) {
io = new Manager(href, opts);
} else {
String id = Url.extractId(parsed);
if (!managers.containsKey(id)) {
managers.put(id, new Manager(href, opts));
}
io = managers.get(id);
}
String path = uri.getPath();
return io.socket(path != null && !path.isEmpty() ? path : "/");
}
public static class Options extends com.github.nkzawa.engineio.client.Socket.Options {
public boolean forceNew;
public boolean multiplex = true;
public boolean reconnection;
public int reconnectionAttempts;
public long reconnectionDelay;
public long reconnectionDelayMax;
public long timeout = -1;
}
}

View File

@@ -0,0 +1,323 @@
package com.github.nkzawa.socketio.client;
import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.parser.Packet;
import com.github.nkzawa.socketio.parser.Parser;
import java.net.URI;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
public class Manager extends Emitter {
private static final Logger logger = Logger.getLogger("socket.io-client:manager");
/*package*/ static final int CLOSED = 0;
/*package*/ static final int OPENING = 1;
/*package*/ static final int OPEN = 2;
public static final String EVENT_OPEN = "open";
public static final String EVENT_CLOSE = "close";
public static final String EVENT_PACKET = "packet";
public static final String EVENT_ERROR = "error";
public static final String EVENT_CONNECT_ERROR = "connect_error";
public static final String EVENT_CONNECT_TIMEOUT = "connect_timeout";
public static final String EVENT_RECONNECT = "reconnect";
public static final String EVENT_RECONNECT_FAILED = "reconnect_failed";
public static final String EVENT_RECONNECT_ERROR = "reconnect_error";
/*package*/ int readyState = CLOSED;
private boolean _reconnection;
private boolean skipReconnect;
private boolean reconnecting;
private int _reconnectionAttempts;
private long _reconnectionDelay;
private long _reconnectionDelayMax;
private long _timeout;
private AtomicInteger connected = new AtomicInteger();
private AtomicInteger attempts = new AtomicInteger();
private Map<String, Socket> nsps = new ConcurrentHashMap<String, Socket>();
private Queue<On.Handle> subs = new ConcurrentLinkedQueue<On.Handle>();
private com.github.nkzawa.engineio.client.Socket engine;
private ScheduledExecutorService timeoutScheduler = Executors.newSingleThreadScheduledExecutor();
private ScheduledExecutorService reconnectScheduler = Executors.newSingleThreadScheduledExecutor();
public Manager(URI uri, IO.Options opts) {
opts = initOptions(opts);
this.engine = new Engine(uri, opts);
}
public Manager(com.github.nkzawa.engineio.client.Socket socket, IO.Options opts) {
opts = initOptions(opts);
this.engine = socket;
}
private IO.Options initOptions(IO.Options opts) {
if (opts == null) {
opts = new IO.Options();
}
if (opts.path == null) {
opts.path = "/socket.io";
}
this.reconnection(opts.reconnection);
this.reconnectionAttempts(opts.reconnectionAttempts != 0 ? opts.reconnectionAttempts : Integer.MAX_VALUE);
this.reconnectionDelay(opts.reconnectionDelay != 0 ? opts.reconnectionDelay : 1000);
this.reconnectionDelayMax(opts.reconnectionDelayMax != 0 ? opts.reconnectionDelayMax : 5000);
this.timeout(opts.timeout < 0 ? 10000 : opts.timeout);
return opts;
}
public boolean reconnection() {
return this._reconnection;
}
public Manager reconnection(boolean v) {
this._reconnection = v;
return this;
}
public int reconnectionAttempts() {
return this._reconnectionAttempts;
}
public Manager reconnectionAttempts(int v) {
this._reconnectionAttempts = v;
return this;
}
public long reconnectionDelay() {
return this._reconnectionDelay;
}
public Manager reconnectionDelay(long v) {
this._reconnectionDelay = v;
return this;
}
public long reconnectionDelayMax() {
return this._reconnectionDelayMax;
}
public Manager reconnectionDelayMax(long v) {
this._reconnectionDelayMax = v;
return this;
}
public long timeout() {
return this._timeout;
}
public Manager timeout(long v) {
this._timeout = v;
return this;
}
public Manager open(){
return open(null);
}
public Manager open(final OpenCallback fn) {
if (this.readyState == OPEN && !this.reconnecting) return this;
final com.github.nkzawa.engineio.client.Socket socket = this.engine;
final Manager self = this;
this.readyState = OPENING;
final On.Handle openSub = On.on(socket, Engine.EVENT_OPEN, new Listener() {
@Override
public void call(Object... objects) {
self.onopen();
if (fn != null) fn.call(null);
}
});
On.Handle errorSub = On.on(socket, Engine.EVENT_ERROR, new Listener() {
@Override
public void call(Object... objects) {
Object data = objects.length > 0 ? objects[0] : null;
self.cleanup();
self.emit(EVENT_CONNECT_ERROR, data);
if (fn != null) {
Exception err = new SocketIOException("Connection error",
data instanceof Exception ? (Exception)data : null);
fn.call(err);
}
}
});
if (this._timeout >= 0) {
final long timeout = this._timeout;
logger.info(String.format("connection attempt will timeout after %d", timeout));
final Future timer = timeoutScheduler.schedule(new Runnable() {
@Override
public void run() {
logger.info(String.format("connect attempt timed out after %d", timeout));
openSub.destroy();
socket.close();
socket.emit(Engine.EVENT_ERROR, new SocketIOException("timeout"));
self.emit(EVENT_CONNECT_TIMEOUT, timeout);
}
}, timeout, TimeUnit.MILLISECONDS);
On.Handle timeSub = new On.Handle() {
@Override
public void destroy() {
timer.cancel(false);
}
};
this.subs.add(timeSub);
}
this.subs.add(openSub);
this.subs.add(errorSub);
this.engine.open();
return this;
}
private void onopen() {
this.cleanup();
this.readyState = OPEN;
this.emit(EVENT_OPEN);
final com.github.nkzawa.engineio.client.Socket socket = this.engine;
this.subs.add(On.on(socket, Engine.EVENT_DATA, new Listener() {
@Override
public void call(Object... objects) {
Manager.this.ondata((String)objects[0]);
}
}));
this.subs.add(On.on(socket, Engine.EVENT_ERROR, new Listener() {
@Override
public void call(Object... objects) {
Manager.this.onerror((Exception)objects[0]);
}
}));
this.subs.add(On.on(socket, Engine.EVENT_CLOSE, new Listener() {
@Override
public void call(Object... objects) {
Manager.this.onclose();
}
}));
}
private void ondata(String data) {
this.emit(EVENT_PACKET, Parser.decode(data));
}
private void onerror(Exception err) {
this.emit(EVENT_ERROR, err);
}
public Socket socket(String nsp) {
Socket socket = this.nsps.get(nsp);
if (socket == null) {
socket = new Socket(this, nsp);
this.nsps.put(nsp, socket);
final Manager self = this;
socket.on(Socket.EVENT_CONNECT, new Listener() {
@Override
public void call(Object... objects) {
self.connected.incrementAndGet();
}
});
}
return socket;
}
/*package*/ void destroy(Socket socket) {
int connected = this.connected.decrementAndGet();
if (connected == 0) {
this.close();
}
}
/*package*/ void packet(Packet packet) {
logger.info(String.format("writing packet %s", packet));
this.engine.write(Parser.encode(packet));
}
private void cleanup() {
On.Handle sub;
while ((sub = this.subs.poll()) != null) sub.destroy();
}
private void close() {
this.skipReconnect = true;
this.cleanup();
this.engine.close();
}
private void onclose() {
this.cleanup();
if (!this.skipReconnect) {
this.reconnect();
}
}
private void reconnect() {
final Manager self = this;
int attempts = this.attempts.incrementAndGet();
if (attempts > this._reconnectionAttempts) {
this.emit(EVENT_RECONNECT_FAILED);
this.reconnecting = false;
} else {
long delay = this.attempts.get() * this.reconnectionDelay();
delay = Math.min(delay, this.reconnectionDelayMax());
logger.info(String.format("will wait %dms before reconnect attempt", delay));
this.reconnecting = true;
final Future timer = this.reconnectScheduler.schedule(new Runnable() {
@Override
public void run() {
logger.info("attempting reconnect");
self.open(new OpenCallback() {
@Override
public void call(Exception err) {
if (err != null) {
logger.info("reconnect attempt error");
self.reconnect();
self.emit(EVENT_RECONNECT_ERROR, err);
} else {
logger.info("reconnect success");
self.onreconnect();
}
}
});
}
}, delay, TimeUnit.MILLISECONDS);
this.subs.add(new On.Handle() {
@Override
public void destroy() {
timer.cancel(false);
}
});
}
}
private void onreconnect() {
int attempts = this.attempts.get();
this.attempts.set(0);
this.reconnecting = false;
this.emit(EVENT_RECONNECT, attempts);
}
public static interface OpenCallback {
public void call(Exception err);
}
}

View File

@@ -0,0 +1,23 @@
package com.github.nkzawa.socketio.client;
import com.github.nkzawa.emitter.Emitter;
public class On extends Emitter {
private On() {}
public static Handle on(final Emitter obj, final String ev, final Listener fn) {
obj.on(ev, fn);
return new Handle() {
@Override
public void destroy() {
obj.off(ev, fn);
}
};
}
public static interface Handle {
public void destroy();
}
}

View File

@@ -0,0 +1,282 @@
package com.github.nkzawa.socketio.client;
import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.parser.Packet;
import com.github.nkzawa.socketio.parser.Parser;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
public class Socket extends Emitter {
private static final Logger logger = Logger.getLogger("socket.io-client:socket");
private static final Gson gson = new Gson();
public static final String EVENT_CONNECT = "connect";
public static final String EVENT_DISCONNECT = "disconnect";
public static final String EVENT_MESSAGE = "message";
public static final String EVENT_ERROR = "error";
private static List<String> events = new ArrayList<String>() {{
add(EVENT_CONNECT);
add(EVENT_DISCONNECT);
add(EVENT_ERROR);
}};
private boolean connected;
private boolean disconnected = true;
private AtomicInteger ids = new AtomicInteger();
private String nsp;
private Manager io;
private Map<Integer, Ack> acks = new ConcurrentHashMap<Integer, Ack>();
private Queue<On.Handle> subs;
private final Queue<LinkedList<Object>> buffer = new ConcurrentLinkedQueue<LinkedList<Object>>();
public Socket(Manager io, String nsp) {
this.io = io;
this.nsp = nsp;
}
public void open() {
final Manager io = this.io;
this.subs = new ConcurrentLinkedQueue<On.Handle>() {{
add(On.on(io, Manager.EVENT_OPEN, new Listener() {
@Override
public void call(Object... objects) {
Socket.this.onopen();
}
}));
add(On.on(io, Manager.EVENT_ERROR, new Listener() {
@Override
public void call(Object... objects) {
Socket.this.onerror(objects.length > 0 ? (Exception) objects[0] : null);
}
}));
}};
if (this.io.readyState == Manager.OPEN) this.onopen();
io.open();
}
public void connect() {
this.open();
}
public Socket send(Object... args) {
this.emit(EVENT_MESSAGE, args);
return this;
}
@Override
public Emitter emit(String event, Object... args) {
if (events.contains(event)) {
super.emit(event, args);
} else {
LinkedList<Object> _args = new LinkedList<Object>(Arrays.asList(args));
if (_args.peekLast() instanceof Ack) {
Ack ack = (Ack)_args.pollLast();
return this.emit(event, ack, _args.toArray());
}
_args.offerFirst(event);
Packet packet = new Packet(Parser.EVENT, gson.toJsonTree(_args.toArray()));
this.packet(packet);
}
return this;
}
/**
* An alias method for `emit` with `ack`
*
* @param event
* @param ack
* @param args
* @return
*/
public Emitter emit(final String event, Ack ack, final Object... args) {
List<Object> _args = new ArrayList<Object>() {{
add(event);
addAll(Arrays.asList(args));
}};
Packet packet = new Packet(Parser.EVENT, gson.toJsonTree(_args.toArray()));
int ids = this.ids.getAndIncrement();
logger.info(String.format("emitting packet with ack id %d", ids));
this.acks.put(ids, ack);
packet.id = ids;
this.packet(packet);
return this;
}
private void packet(Packet packet) {
packet.nsp = this.nsp;
this.io.packet(packet);
}
private void onerror(Exception err) {
this.emit(EVENT_ERROR, err);
}
private void onopen() {
logger.info("transport is open - connecting");
if (!"/".equals(this.nsp)) {
this.packet(new Packet(Parser.CONNECT));
}
Manager io = this.io;
this.subs.add(On.on(io, Manager.EVENT_PACKET, new Listener() {
@Override
public void call(Object... objects) {
Packet packet = objects.length > 0 ? (Packet)objects[0] : null;
Socket.this.onpacket(packet);
}
}));
this.subs.add(On.on(io, Manager.EVENT_CLOSE, new Listener() {
@Override
public void call(Object... objects) {
String reason = objects.length > 0 ? (String)objects[0] : null;
Socket.this.onclose(reason);
}
}));
}
private void onclose(String reason) {
logger.info(String.format("close (%s)", reason));
this.connected = false;
this.disconnected = true;
this.emit(EVENT_DISCONNECT, reason);
}
private void onpacket(Packet packet) {
if (!this.nsp.equals(packet.nsp)) return;
switch (packet.type) {
case Parser.CONNECT:
this.onconnect();
break;
case Parser.EVENT:
this.onevent(packet);
break;
case Parser.ACK:
this.onack(packet);
break;
case Parser.DISCONNECT:
this.ondisconnect();
break;
case Parser.ERROR:
this.emit(EVENT_ERROR, packet.data);
break;
}
}
private void onevent(Packet packet) {
Type type = new TypeToken<LinkedList<Object>>(){}.getType();
LinkedList<Object> args = gson.fromJson(packet.data != null ? packet.data : new JsonArray(), type);
logger.info(String.format("emitting event %s", args));
if (packet.id >= 0) {
logger.info("attaching ack callback to event");
args.offerLast(this.ack(packet.id));
}
if (this.connected) {
String event = (String)args.pollFirst();
super.emit(event, args.toArray());
} else {
this.buffer.add(args);
}
}
private Ack ack(final int id) {
final Socket self = this;
final boolean[] sent = new boolean[] {false};
return new Ack() {
@Override
public synchronized void call(Object... args) {
if (sent[0]) return;
sent[0] = true;
logger.info(String.format("sending ack %s", args));
Packet packet = new Packet(Parser.ACK, gson.toJsonTree(args));
packet.id = id;
self.packet(packet);
}
};
}
private void onack(Packet packet) {
logger.info(String.format("calling ack %s with %s", packet.id, packet.data));
Ack fn = this.acks.remove(packet.id);
fn.call(gson.fromJson(packet.data, Object[].class));
}
private void onconnect() {
this.connected = true;
this.disconnected = false;
this.emit(EVENT_CONNECT);
this.emitBuffered();
}
private void emitBuffered() {
synchronized (this.buffer) {
LinkedList<Object> data;
while ((data = this.buffer.poll()) != null) {
String event = (String)data.pollFirst();
super.emit(event, data.toArray());
}
}
}
private void ondisconnect() {
logger.info(String.format("server disconnect (%s)", this.nsp));
this.destroy();
this.onclose("io server disconnect");
}
private void destroy() {
logger.info(String.format("destroying socket (%s)", this.nsp));
for (On.Handle sub : this.subs) {
sub.destroy();
}
this.io.destroy(this);
}
public Socket close() {
if (!this.connected) return this;
logger.info(String.format("performing disconnect (%s)", this.nsp));
this.packet(new Packet(Parser.DISCONNECT));
this.destroy();
this.onclose("io client disconnect");
return this;
}
public Socket disconnect() {
return this.close();
}
public static interface Ack {
public void call(Object... args);
}
}

View File

@@ -0,0 +1,20 @@
package com.github.nkzawa.socketio.client;
public class SocketIOException extends Exception {
public SocketIOException() {
super();
}
public SocketIOException(String message) {
super(message);
}
public SocketIOException(String message, Throwable cause) {
super(message, cause);
}
public SocketIOException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,34 @@
package com.github.nkzawa.socketio.client;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
public class Url {
private Url() {
}
public static URL parse(URI uri) throws MalformedURLException {
String protocol = uri.getScheme();
if (protocol == null || !protocol.matches("^https?|wss?$")) {
uri = uri.resolve("https://" + uri.getAuthority());
}
String path = uri.getPath();
if (path == null || path.isEmpty()) {
uri = uri.resolve("/");
}
return uri.toURL();
}
public static String extractId(URL url) {
String protocol = url.getProtocol();
int port = url.getPort();
if ((protocol.matches("^http|ws$") && port == 80) ||
(protocol.matches("^(http|ws)s$") && port == 443)) {
port = -1;
}
return protocol + "://" + url.getHost() + (port != -1 ? ":" + port : "");
}
}

View File

@@ -0,0 +1,22 @@
package com.github.nkzawa.socketio.parser;
import com.google.gson.JsonElement;
public class Packet {
public int type = -1;
public int id = -1;
public String nsp;
public JsonElement data;
public Packet() {}
public Packet(int type) {
this.type = type;
}
public Packet(int type, JsonElement data) {
this.type = type;
this.data = data;
}
}

View File

@@ -0,0 +1,131 @@
package com.github.nkzawa.socketio.parser;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
public class Parser {
private static final Logger logger = Logger.getLogger("socket.io-parser");
private static final Gson gson = new Gson();
private static final JsonParser parser = new JsonParser();
public static final int CONNECT = 0;
public static final int DISCONNECT = 1;
public static final int EVENT = 2;
public static final int ACK = 3;
public static final int ERROR = 4;
public static int protocol = 1;
public static List<String > types = new ArrayList<String>() {{
add("CONNECT");
add("DISCONNECT");
add("EVENT");
add("ACK");
add("ERROR");
}};
private Parser() {}
public static String encode(Packet obj) {
StringBuilder str = new StringBuilder();
boolean nsp = false;
str.append(obj.type);
if (obj.nsp != null && !obj.nsp.isEmpty() && !"/".equals(obj.nsp)) {
nsp = true;
str.append(obj.nsp);
}
if (obj.id >= 0) {
if (nsp) {
str.append(",");
nsp = false;
}
str.append(obj.id);
}
if (obj.data != null) {
if (nsp) str.append(",");
str.append(gson.toJson(obj.data));
}
logger.info(String.format("encoded %s as %s", obj, str));
return str.toString();
}
public static Packet decode(String str) {
Packet p = new Packet();
int i = 0;
try {
p.type = Character.getNumericValue(str.charAt(0));
types.get(p.type);
} catch (IndexOutOfBoundsException e) {
return error();
}
char next = Character.UNASSIGNED;
try {
next = str.charAt(i + 1);
} catch (IndexOutOfBoundsException e) {
// do nothing
}
if (next == '/') {
StringBuilder nsp = new StringBuilder();
while (true) {
++i;
char c = str.charAt(i);
if (c == ',') break;
nsp.append(c);
if (i + 1 == str.length()) break;
}
p.nsp = nsp.toString();
} else {
p.nsp = "/";
}
next = Character.UNASSIGNED;
try {
next = str.charAt(i + 1);
} catch (IndexOutOfBoundsException e) {
// do nothing
}
if (Character.getNumericValue(next) != -1) {
StringBuilder id = new StringBuilder();
while (true) {
++i;
Character c = str.charAt(i);
if (c == null || Character.getNumericValue(c) == -1) {
--i;
break;
}
id.append(c);
if (i + 1 == str.length()) break;
}
p.id = Integer.parseInt(id.toString());
}
try {
str.charAt(++i);
p.data = parser.parse(str.substring(i));
} catch (IndexOutOfBoundsException e) {
// do nothing
} catch (JsonParseException e) {
return error();
}
logger.info(String.format("decoded %s as %s", str, p));
return p;
}
private static Packet error() {
return new Packet(ERROR, new JsonPrimitive("parser error"));
}
}

View File

@@ -0,0 +1,137 @@
package com.github.nkzawa.socketio.client;
import com.github.nkzawa.emitter.Emitter;
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 IOTest {
final static int TIMEOUT = 3000;
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=socket.io:*,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(false);
serverError.cancel(false);
serverService.shutdown();
serverService.awaitTermination(3000, TimeUnit.MILLISECONDS);
}
@Test(timeout = TIMEOUT)
public void openAndClose() throws URISyntaxException, InterruptedException {
final BlockingQueue<String> events = new LinkedBlockingQueue<String>();
IO.Options opts = new IO.Options();
opts.forceNew = true;
socket = IO.socket("http://localhost:" + PORT, opts);
socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
@Override
public void call(Object... objects) {
System.out.println("connect:");
events.offer("connect");
}
}).on(Socket.EVENT_DISCONNECT, new Emitter.Listener() {
@Override
public void call(Object... objects) {
System.out.println("disconnect:");
events.offer("disconnect");
}
});
socket.connect();
assertThat(events.take(), is("connect"));
socket.disconnect();
assertThat(events.take(), is("disconnect"));
}
@Test(timeout = TIMEOUT)
public void message() throws URISyntaxException, InterruptedException {
final BlockingQueue<String> events = new LinkedBlockingQueue<String>();
IO.Options opts = new IO.Options();
opts.forceNew = true;
socket = IO.socket("http://localhost:" + PORT, opts);
socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
@Override
public void call(Object... objects) {
System.out.println("connect:");
socket.send("hi");
}
}).on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
@Override
public void call(Object... objects) {
System.out.println("message: " + objects);
events.offer((String) objects[0]);
}
});
socket.connect();
assertThat(events.take(), is("hello client"));
assertThat(events.take(), is("hi"));
socket.disconnect();
}
}

View File

@@ -0,0 +1,51 @@
package com.github.nkzawa.socketio.client;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertThat;
@RunWith(JUnit4.class)
public class UrlTest {
@Test
public void parse() throws MalformedURLException, URISyntaxException {
URL url = Url.parse(new URI("http://woot.com/test"));
assertThat(url.getProtocol(), is("http"));
assertThat(url.getHost(), is("woot.com"));
assertThat(url.getPath(), is("/test"));
}
@Test
public void parse_NoProtocol() throws MalformedURLException, URISyntaxException {
URL url = Url.parse(new URI("//localhost:3000"));
assertThat(url.getProtocol(), is("https"));
assertThat(url.getHost(), is("localhost"));
assertThat(url.getAuthority(), is("localhost:3000"));
assertThat(url.getPort(), is(3000));
}
@Test
public void parse_Namaspace() throws MalformedURLException, URISyntaxException {
assertThat(Url.parse(new URI("http://woot.com/woot")).getPath(), is("/woot"));
assertThat(Url.parse(new URI("http://google.com")).getPath(), is("/"));
assertThat(Url.parse(new URI("http://google.com/")).getPath(), is("/"));
}
@Test
public void extractId() throws MalformedURLException, URISyntaxException {
String id1 = Url.extractId(new URL("http://google.com:80/"));
String id2 = Url.extractId(new URL("http://google.com/"));
String id3 = Url.extractId(new URL("https://google.com/"));
assertThat(id1, is(id2));
assertThat(id1, is(not(id3)));
}
}

View File

@@ -0,0 +1,29 @@
var server = require('http').Server()
, io = require('socket.io')(server)
, port = parseInt(process.argv[2], 10) || 3000;
io.on('connection', function(socket) {
socket.send('hello client');
socket.on('message', function(data) {
console.log('message:', data);
socket.send(data);
});
socket.on('echo', function(data) {
console.log('echo:', data);
socket.emit('echoBack', data);
});
socket.on('disconnect', function() {
console.log('disconnect');
});
socket.on('error', function() {
console.log('error: ' + arguments);
});
});
server.listen(port, function() {
console.log('Socket.IO server listening on port', port);
});

View File

@@ -0,0 +1,8 @@
{
"name": "socket.io-client.java-test",
"version": "0.0.0",
"private": true,
"dependencies": {
"socket.io": "nkzawa/socket.io"
}
}