diff --git a/History.md b/History.md index 02f66f43..18e4f07b 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,56 @@ +2.1.0 / 2022-07-10 +================== + +### Bug Fixes + +* ensure randomizationFactor is always between 0 and 1 ([0cbf01e](https://github.com/socketio/socket.io-client-java/commit/0cbf01eb2501b3098eacd22594966a719b20c31e)) +* prevent socket from reconnecting after middleware failure ([95ecf22](https://github.com/socketio/socket.io-client-java/commit/95ecf222d25de390d8c0f2ffade37b608cf448eb)) +* increase the readTimeout value of the default OkHttpClient ([fb531fa](https://github.com/socketio/engine.io-client-java/commit/fb531fab30968a4b65a402c81f37e92dd5671f33)) (from `engine.io-client`) + +### Features + +* emit with timeout ([fca3b95](https://github.com/socketio/socket.io-client-java/commit/fca3b9507d5bc79d3c41ab6e119efccd23669ca6)) + +This feature allows to send a packet and expect an acknowledgement from the server within the given delay. + +Syntax: + +```java +socket.emit("hello", "world", new AckWithTimeout(5000) { + @Override + public void onTimeout() { + // ... + } + + @Override + public void onSuccess(Object... args) { + // ... + } +}); +``` + +* implement catch-all listeners ([c7d50b8](https://github.com/socketio/socket.io-client-java/commit/c7d50b8ae9787e9ebdff50aa5d36f88433fc50b9)) + +Syntax: + +```java +socket.onAnyIncoming(new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); + +socket.onAnyOutgoing(new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); +``` + + 2.0.1 / 2021-04-27 ================== diff --git a/pom.xml b/pom.xml index 66b4d2f2..e6ff6051 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 io.socket socket.io-client - 2.0.1 + 2.1.0 jar socket.io-client Socket.IO Client Library for Java @@ -30,7 +30,7 @@ https://github.com/socketio/socket.io-client-java scm:git:https://github.com/socketio/socket.io-client-java.git scm:git:https://github.com/socketio/socket.io-client-java.git - socket.io-client-2.0.1 + socket.io-client-2.1.0 @@ -62,7 +62,7 @@ io.socket engine.io-client - 2.0.0 + 2.1.0 org.json diff --git a/src/main/java/io/socket/backo/Backoff.java b/src/main/java/io/socket/backo/Backoff.java index f5199213..81e1dddd 100644 --- a/src/main/java/io/socket/backo/Backoff.java +++ b/src/main/java/io/socket/backo/Backoff.java @@ -3,6 +3,9 @@ import java.math.BigDecimal; import java.math.BigInteger; +/** + * Imported from https://github.com/mokesmokes/backo + */ public class Backoff { private long ms = 100; @@ -23,7 +26,10 @@ public long duration() { .multiply(new BigDecimal(ms)).toBigInteger(); ms = (((int) Math.floor(rand * 10)) & 1) == 0 ? ms.subtract(deviation) : ms.add(deviation); } - return ms.min(BigInteger.valueOf(this.max)).longValue(); + return ms + .min(BigInteger.valueOf(this.max)) + .max(BigInteger.valueOf(this.ms)) + .longValue(); } public void reset() { @@ -46,6 +52,10 @@ public Backoff setFactor(int factor) { } public Backoff setJitter(double jitter) { + boolean isValid = jitter >= 0 && jitter < 1; + if (!isValid) { + throw new IllegalArgumentException("jitter must be between 0 and 1"); + } this.jitter = jitter; return this; } diff --git a/src/main/java/io/socket/client/AckWithTimeout.java b/src/main/java/io/socket/client/AckWithTimeout.java new file mode 100644 index 00000000..88c43c73 --- /dev/null +++ b/src/main/java/io/socket/client/AckWithTimeout.java @@ -0,0 +1,35 @@ +package io.socket.client; + +import java.util.Timer; +import java.util.TimerTask; + +public abstract class AckWithTimeout implements Ack { + private final long timeout; + private final Timer timer = new Timer(); + + /** + * + * @param timeout delay in milliseconds + */ + public AckWithTimeout(long timeout) { + this.timeout = timeout; + } + + @Override + public final void call(Object... args) { + this.timer.cancel(); + this.onSuccess(args); + } + + public final void schedule(TimerTask task) { + this.timer.schedule(task, this.timeout); + } + + public final void cancelTimer() { + this.timer.cancel(); + } + + public abstract void onSuccess(Object... args); + public abstract void onTimeout(); + +} diff --git a/src/main/java/io/socket/client/Socket.java b/src/main/java/io/socket/client/Socket.java index 05feff39..49bc3784 100644 --- a/src/main/java/io/socket/client/Socket.java +++ b/src/main/java/io/socket/client/Socket.java @@ -9,6 +9,7 @@ import org.json.JSONObject; import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; @@ -63,6 +64,9 @@ public class Socket extends Emitter { private final Queue> receiveBuffer = new LinkedList<>(); private final Queue> sendBuffer = new LinkedList<>(); + private ConcurrentLinkedQueue onAnyIncomingListeners = new ConcurrentLinkedQueue<>(); + private ConcurrentLinkedQueue onAnyOutgoingListeners = new ConcurrentLinkedQueue<>(); + public Socket(Manager io, String nsp, Manager.Options opts) { this.io = io; this.nsp = nsp; @@ -210,8 +214,32 @@ public void run() { Packet packet = new Packet<>(Parser.EVENT, jsonArgs); if (ack != null) { - logger.fine(String.format("emitting packet with ack id %d", ids)); - Socket.this.acks.put(ids, ack); + final int ackId = Socket.this.ids; + + logger.fine(String.format("emitting packet with ack id %d", ackId)); + + if (ack instanceof AckWithTimeout) { + final AckWithTimeout ackWithTimeout = (AckWithTimeout) ack; + ackWithTimeout.schedule(new TimerTask() { + @Override + public void run() { + // remove the ack from the map (to prevent an actual acknowledgement) + acks.remove(ackId); + + // remove the packet from the buffer (if applicable) + Iterator> iterator = sendBuffer.iterator(); + while (iterator.hasNext()) { + if (iterator.next().id == ackId) { + iterator.remove(); + } + } + + ackWithTimeout.onTimeout(); + } + }); + } + + Socket.this.acks.put(ackId, ack); packet.id = ids++; } @@ -226,6 +254,14 @@ public void run() { } private void packet(Packet packet) { + if (packet.type == Parser.EVENT) { + if (!onAnyOutgoingListeners.isEmpty()) { + Object[] argsAsArray = toArray((JSONArray) packet.data); + for (Listener listener : onAnyOutgoingListeners) { + listener.call(argsAsArray); + } + } + } packet.nsp = this.nsp; this.io.packet(packet); } @@ -298,6 +334,7 @@ private void onpacket(Packet packet) { break; case Parser.CONNECT_ERROR: + this.destroy(); super.emit(EVENT_CONNECT_ERROR, packet.data); break; } @@ -316,6 +353,12 @@ private void onevent(Packet packet) { if (this.connected) { if (args.isEmpty()) return; + if (!this.onAnyIncomingListeners.isEmpty()) { + Object[] argsAsArray = args.toArray(); + for (Listener listener : this.onAnyIncomingListeners) { + listener.call(argsAsArray); + } + } String event = args.remove(0).toString(); super.emit(event, args.toArray()); } else { @@ -405,6 +448,12 @@ private void destroy() { this.subs = null; } + for (Ack ack : acks.values()) { + if (ack instanceof AckWithTimeout) { + ((AckWithTimeout) ack).cancelTimer(); + } + } + this.io.destroy(); } @@ -477,5 +526,49 @@ private static Object[] toArray(JSONArray array) { } return data; } + + public Socket onAnyIncoming(Listener fn) { + this.onAnyIncomingListeners.add(fn); + return this; + } + + public Socket offAnyIncoming() { + this.onAnyIncomingListeners.clear(); + return this; + } + + public Socket offAnyIncoming(Listener fn) { + Iterator it = this.onAnyIncomingListeners.iterator(); + while (it.hasNext()) { + Listener listener = it.next(); + if (listener == fn) { + it.remove(); + break; + } + } + return this; + } + + public Socket onAnyOutgoing(Listener fn) { + this.onAnyOutgoingListeners.add(fn); + return this; + } + + public Socket offAnyOutgoing() { + this.onAnyOutgoingListeners.clear(); + return this; + } + + public Socket offAnyOutgoing(Listener fn) { + Iterator it = this.onAnyOutgoingListeners.iterator(); + while (it.hasNext()) { + Listener listener = it.next(); + if (listener == fn) { + it.remove(); + break; + } + } + return this; + } } diff --git a/src/site/markdown/android.md b/src/site/markdown/android.md new file mode 100644 index 00000000..a33b2f92 --- /dev/null +++ b/src/site/markdown/android.md @@ -0,0 +1,59 @@ +# Android + + + +## How to keep a Socket.IO client running in the background? + +Long story short, you probably shouldn't. The Socket.IO client is not meant to be used in a [background service](https://developer.android.com/guide/components/services?hl=en), as it will keep an open TCP connection to the server and quickly drain the battery of your users. + +It is totally usable in the foreground though. + +See also: https://developer.android.com/training/connectivity + +## How to reach an HTTP server? + +Starting with Android 9 (API level 28) you need to explicitly allow cleartext traffic to be able to reach an HTTP server (e.g. a local server at `http://192.168.0.10`): + +- either for all domains: + +`app/src/main/AndroidManifest.xml` + +```xml + + + + + + ... + + +``` + +- or for a restricted list of domains: + +`app/src/main/AndroidManifest.xml` + +```xml + + + + + + ... + + +``` + +`app/src/main/res/xml/network_security_config.xml` + +```xml + + + + localhost + 192.168.0.10 + + +``` + +Reference: https://developer.android.com/training/articles/security-config diff --git a/src/site/markdown/changelog.md b/src/site/markdown/changelog.md index 98ea025b..d197c228 100644 --- a/src/site/markdown/changelog.md +++ b/src/site/markdown/changelog.md @@ -1,4 +1,69 @@ +## [2.1.0](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-2.0.1...socket.io-client-2.1.0) (2022-07-10) + + +### Bug Fixes + +* ensure randomizationFactor is always between 0 and 1 ([0cbf01e](https://github.com/socketio/socket.io-client-java/commit/0cbf01eb2501b3098eacd22594966a719b20c31e)) +* prevent socket from reconnecting after middleware failure ([95ecf22](https://github.com/socketio/socket.io-client-java/commit/95ecf222d25de390d8c0f2ffade37b608cf448eb)) +* increase the readTimeout value of the default OkHttpClient ([fb531fa](https://github.com/socketio/engine.io-client-java/commit/fb531fab30968a4b65a402c81f37e92dd5671f33)) (from `engine.io-client`) + +### Features + +* emit with timeout ([fca3b95](https://github.com/socketio/socket.io-client-java/commit/fca3b9507d5bc79d3c41ab6e119efccd23669ca6)) + +This feature allows to send a packet and expect an acknowledgement from the server within the given delay. + +Syntax: + +```java +socket.emit("hello", "world", new AckWithTimeout(5000) { + @Override + public void onTimeout() { + // ... + } + + @Override + public void onSuccess(Object... args) { + // ... + } +}); +``` + +* implement catch-all listeners ([c7d50b8](https://github.com/socketio/socket.io-client-java/commit/c7d50b8ae9787e9ebdff50aa5d36f88433fc50b9)) + +Syntax: + +```java +socket.onAnyIncoming(new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); + +socket.onAnyOutgoing(new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); +``` + + + +## [2.0.1](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-2.0.0...socket.io-client-2.0.1) (2021-04-27) + + +### Bug Fixes + +* fix usage with ws:// scheme ([67fd5f3](https://github.com/socketio/socket.io-client-java/commit/67fd5f34a31c63f7884f82ab39386ad343527590)) +* ensure buffered events are sent in order ([4885e7d](https://github.com/socketio/socket.io-client-java/commit/4885e7d59fad78285448694cb5681e8a9ce809ef)) +* ensure the payload format is valid ([e8ffe9d](https://github.com/socketio/socket.io-client-java/commit/e8ffe9d1383736f6a21090ab959a2f4fa5a41284)) +* emit a CONNECT_ERROR event upon connection failure ([d324e7f](https://github.com/socketio/socket.io-client-java/commit/d324e7f396a444ddd556c3d70a85a28eefb1e02b)) + + + ## [2.0.0](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-1.0.1...socket.io-client-2.0.0) (2020-12-14) diff --git a/src/site/markdown/emitting_events.md b/src/site/markdown/emitting_events.md index fce31092..4526a491 100644 --- a/src/site/markdown/emitting_events.md +++ b/src/site/markdown/emitting_events.md @@ -1,6 +1,6 @@ # Emitting events -See also: https://socket.io/docs/v3/emitting-events/ +See also: https://socket.io/docs/v4/emitting-events/ **Table of content** @@ -77,6 +77,27 @@ Events are great, but in some cases you may want a more classic request-response You can add a callback as the last argument of the `emit()`, and this callback will be called once the other side acknowledges the event: +### From client to server + +*Client* + +```java +// Java 7 +socket.emit("update item", 1, new JSONObject(singletonMap("name", "updated")), new Ack() { + @Override + public void call(Object... args) { + JSONObject response = (JSONObject) args[0]; + System.out.println(response.getString("status")); // "ok" + } +}); + +// Java 8 and above +socket.emit("update item", 1, new JSONObject(singletonMap("name", "updated")), (Ack) args -> { + JSONObject response = (JSONObject) args[0]; + System.out.println(response.getString("status")); // "ok" +}); +``` + *Server* ```js @@ -91,15 +112,55 @@ io.on("connection", (socket) => { }); ``` +### From server to client + +*Server* + +```js +io.on("connection", (socket) => { + socket.emit("hello", "please acknowledge", (response) => { + console.log(response); // prints "hi!" + }); +}); +``` + *Client* ```java -socket.emit("update item", 1, new JSONObject(singletonMap("name", "updated")), new Ack() { +// Java 7 +socket.on("hello", new Emitter.Listener() { @Override public void call(Object... args) { - JSONObject response = (JSONObject) args[0]; - System.out.println(response.getString("status")); // "ok" + System.out.println(args[0]); // "please acknowledge" + if (args.length > 1 && args[1] instanceof Ack) { + ((Ack) args[1]).call("hi!"); + } + } +}); + +// Java 8 and above +socket.on("hello", args -> { + System.out.println(args[0]); // "please acknowledge" + if (args.length > 1 && args[1] instanceof Ack) { + ((Ack) args[1]).call("hi!"); } }); ``` +## With timeout + +Starting with version `2.1.0`, you can now assign a timeout to each emit: + +```java +socket.emit("hello", "world", new AckWithTimeout(5000) { + @Override + public void onTimeout() { + // ... + } + + @Override + public void onSuccess(Object... args) { + // ... + } +}); +``` diff --git a/src/site/markdown/faq.md b/src/site/markdown/faq.md new file mode 100644 index 00000000..b1defeac --- /dev/null +++ b/src/site/markdown/faq.md @@ -0,0 +1,401 @@ +# Frequently asked questions + + + +## How to deal with cookies + +In order to store the cookies sent by the server and include them in all subsequent requests, you need to create an OkHttpClient with a custom [cookie jar](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-cookie-jar/). + +You can either implement your own cookie jar: + +```java +public class MyApp { + + public static void main(String[] argz) throws Exception { + IO.Options options = new IO.Options(); + + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .cookieJar(new MyCookieJar()) + .build(); + + options.callFactory = okHttpClient; + options.webSocketFactory = okHttpClient; + + Socket socket = IO.socket(URI.create("https://example.com"), options); + + socket.connect(); + } + + private static class MyCookieJar implements CookieJar { + private Set cache = new HashSet<>(); + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + for (Cookie cookie : cookies) { + this.cache.add(new WrappedCookie(cookie)); + } + } + + @Override + public List loadForRequest(HttpUrl url) { + List cookies = new ArrayList<>(); + Iterator iterator = this.cache.iterator(); + while (iterator.hasNext()) { + Cookie cookie = iterator.next().cookie; + if (isCookieExpired(cookie)) { + iterator.remove(); + } else if (cookie.matches(url)) { + cookies.add(cookie); + } + } + return cookies; + } + + private static boolean isCookieExpired(Cookie cookie) { + return cookie.expiresAt() < System.currentTimeMillis(); + } + } + + private static class WrappedCookie { + private final Cookie cookie; + + public WrappedCookie(Cookie cookie) { + this.cookie = cookie; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WrappedCookie)) return false; + WrappedCookie that = (WrappedCookie) o; + return that.cookie.name().equals(this.cookie.name()) + && that.cookie.domain().equals(this.cookie.domain()) + && that.cookie.path().equals(this.cookie.path()) + && that.cookie.secure() == this.cookie.secure() + && that.cookie.hostOnly() == this.cookie.hostOnly(); + } + + @Override + public int hashCode() { + int hash = 17; + hash = 31 * hash + cookie.name().hashCode(); + hash = 31 * hash + cookie.domain().hashCode(); + hash = 31 * hash + cookie.path().hashCode(); + hash = 31 * hash + (cookie.secure() ? 0 : 1); + hash = 31 * hash + (cookie.hostOnly() ? 0 : 1); + return hash; + } + } +} +``` + +Or use a package like [PersistentCookieJar](https://github.com/franmontiel/PersistentCookieJar): + +```java +public class MyApp { + + public static void main(String[] argz) throws Exception { + IO.Options options = new IO.Options(); + + ClearableCookieJar cookieJar = new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(context)); + + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .cookieJar(cookieJar) + .build(); + + options.callFactory = okHttpClient; + options.webSocketFactory = okHttpClient; + + Socket socket = IO.socket(URI.create("https://example.com"), options); + + socket.connect(); + } +} +``` + +## How to use with AWS Load Balancing + +When scaling to multiple Socket.IO servers, you must ensure that all the HTTP requests of a given session reach the same server (explanation [here](https://socket.io/docs/v4/using-multiple-nodes/#why-is-sticky-session-required)). + +Sticky sessions can be enabled on AWS Application Load Balancers, which works by sending a cookie (`AWSALB`) to the client. + +Please see [above](#how-to-deal-with-cookies) for how to deal with cookies. + +Reference: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html + +## How to force TLS v1.2 and above + +This library relies on the OkHttp library to create HTTP requests and WebSocket connections. + +Reference: https://square.github.io/okhttp/ + +We currently depend on version `3.12.12`, which is the last version that supports Java 7+ and Android 2.3+ (API level 9+). With this version, the OkHttpClient allows `TLSv1` and `TLSv1.1` by default ([MODERN_TLS](https://square.github.io/okhttp/security/tls_configuration_history/#modern_tls-versions_1) configuration). + +You can overwrite it by providing your own OkHttp client: + +```java +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .connectionSpecs(Arrays.asList( + ConnectionSpec.RESTRICTED_TLS + )) + .readTimeout(1, TimeUnit.MINUTES) // important for HTTP long-polling + .build(); + +IO.Options options = new IO.Options(); +options.callFactory = okHttpClient; +options.webSocketFactory = okHttpClient; + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +Note: we will upgrade to OkHttp 4 in the next major version. + +## How to create a lot of clients + +By default, you won't be able to create more than 5 Socket.IO clients (any additional client will be disconnected with "transport error" or "ping timeout" reason). That is due to the default OkHttp [dispatcher](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-dispatcher/), whose `maxRequestsPerHost` is set to 5 by default. + +You can overwrite it by providing your own OkHttp client: + +```java +int MAX_CLIENTS = 100; + +Dispatcher dispatcher = new Dispatcher(); +dispatcher.setMaxRequests(MAX_CLIENTS * 2); +dispatcher.setMaxRequestsPerHost(MAX_CLIENTS * 2); + +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .dispatcher(dispatcher) + .readTimeout(1, TimeUnit.MINUTES) // important for HTTP long-polling + .build(); + +IO.Options options = new IO.Options(); +options.callFactory = okHttpClient; +options.webSocketFactory = okHttpClient; + +for (int i = 0; i < MAX_CLIENTS; i++) { + Socket socket = IO.socket(URI.create("https://example.com"), options); +} +``` + +Note: we use `MAX_CLIENTS * 2` because a client in HTTP long-polling mode will have one long-running GET request for receiving data from the server, and will create a POST request for sending data to the server. + +## How to properly close a client + +Calling `socket.disconnect()` may not be sufficient, because the underlying OkHttp client [creates](https://github.com/square/okhttp/blob/06d38cb795d82d086f13c595a62ce0cbe60904ac/okhttp/src/main/java/okhttp3/Dispatcher.java#L65-L66) a ThreadPoolExecutor that will prevent your Java program from quitting for 60 seconds. + +As a workaround, you can manually shut down this ThreadPoolExecutor: + +```java +Dispatcher dispatcher = new Dispatcher(); + +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .dispatcher(dispatcher) + .readTimeout(1, TimeUnit.MINUTES) // important for HTTP long-polling + .build(); + +IO.Options options = new IO.Options(); +options.callFactory = okHttpClient; +options.webSocketFactory = okHttpClient; + +Socket socket = IO.socket(URI.create("https://example.com"), options); + +socket.connect(); + +// then later + +socket.disconnect(); +dispatcher.executorService().shutdown(); +``` + +## How to map the event arguments to POJO + +This library uses the [JSONTokener](https://developer.android.com/reference/org/json/JSONTokener) class from the `org.json` package in order to parse the packets that are sent by the server, which means you will receive [JSONObjects](https://developer.android.com/reference/org/json/JSONObject) in your listeners. + +Here's how you can convert these JSONObjects to Plain Old Java Objects (POJO): + +- [with Jackson](#With_Jackson) +- [with Gson](#With_Gson) + +### With Jackson + +`pom.xml` + +```xml + + + + + com.fasterxml.jackson.core + jackson-core + 2.13.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.3 + + + com.fasterxml.jackson.datatype + jackson-datatype-json-org + 2.13.3 + + ... + + ... + +``` + +Maven repository: + +- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core +- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind +- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-json-org + +`src/main/java/MyApp.java` + +```java +public class MyApp { + private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JsonOrgModule()); + + public static void main(String[] argz) throws Exception { + Socket socket = IO.socket(URI.create("https://example.com")); + + socket.on("my-event", (args) -> { + MyObject object = MAPPER.convertValue(args[0], MyObject.class); + + // ... + }); + + socket.connect(); + } + + public static class MyObject { + public int id; + public String label; + } +} +``` + +### With Gson + +`pom.xml` + +```xml + + + + + com.google.code.gson + gson + 2.8.9 + + ... + + ... + +``` + +Maven repository: + +- https://mvnrepository.com/artifact/com.google.code.gson/gson + +`src/main/java/MyApp.java` + +You can either call `toString()` on the `JSONObject`: + +```java +public class MyApp { + private static final Gson GSON = new Gson(); + + public static void main(String[] argz) throws Exception { + Socket socket = IO.socket(URI.create("https://example.com")); + + socket.on("my-event", (args) -> { + MyObject object = GSON.fromJson(args[0].toString(), MyObject.class); + + // ... + }); + + socket.connect(); + } + + public static class MyObject { + public int id; + public String label; + } +} +``` + +Or manually convert the `JSONObject` to a `JsonObject` (for performance purposes): + +```java +public class MyApp { + private static final Gson GSON = new Gson(); + + public static void main(String[] argz) throws Exception { + Socket socket = IO.socket(URI.create("https://example.com")); + + socket.on("my-event", (args) -> { + MyObject object = GSON.fromJson(map(args[0]), MyObject.class); + + // ... + }); + + socket.connect(); + } + + public static class MyObject { + public int id; + public String label; + } + + public static JsonObject map(JSONObject source) throws JSONException { + JsonObject output = new JsonObject(); + + Iterator iterator = source.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = source.get(key); + + if (value instanceof JSONObject) { + output.add(key, map((JSONObject) value)); + } else if (value instanceof JSONArray) { + output.add(key, map((JSONArray) value)); + } else if (value instanceof Number) { + output.addProperty(key, (Number) value); + } else if (value instanceof String) { + output.addProperty(key, (String) value); + } else if (value instanceof Boolean) { + output.addProperty(key, (Boolean) value); + } else if (value instanceof Character) { + output.addProperty(key, (Character) value); + } + } + + return output; + } + + public static JsonArray map(JSONArray source) throws JSONException { + JsonArray output = new JsonArray(); + + for (int i = 0; i < source.length(); i++) { + Object value = source.get(i); + + if (value instanceof JSONObject) { + output.add(map((JSONObject) value)); + } else if (value instanceof JSONArray) { + output.add(map((JSONArray) value)); + } else if (value instanceof Number) { + output.add((Number) value); + } else if (value instanceof String) { + output.add((String) value); + } else if (value instanceof Boolean) { + output.add((Boolean) value); + } else if (value instanceof Character) { + output.add((Character) value); + } + } + + return output; + } +} +``` \ No newline at end of file diff --git a/src/site/markdown/initialization.md b/src/site/markdown/initialization.md index efef638f..0311b39e 100644 --- a/src/site/markdown/initialization.md +++ b/src/site/markdown/initialization.md @@ -25,7 +25,7 @@ Socket socket = IO.socket("wss://example.com"); // OK, similar to the example ab Socket socket = IO.socket("192.168.0.1:1234"); // NOT OK, missing the scheme part ``` -The path represents the [Namespace](https://socket.io/docs/v3/namespaces/), and not the actual path (see [below](#path)) of the HTTP requests: +The path represents the [Namespace](https://socket.io/docs/v4/namespaces/), and not the actual path (see [below](#path)) of the HTTP requests: ```java Socket socket = IO.socket(URI.create("https://example.com")); // the main namespace @@ -76,7 +76,7 @@ Whether to create a new Manager instance. A Manager instance is in charge of the low-level connection to the server (established with HTTP long-polling or WebSocket). It handles the reconnection logic. -A Socket instance is the interface which is used to sends events to — and receive events from — the server. It belongs to a given [namespace](https://socket.io/docs/v3/namespaces). +A Socket instance is the interface which is used to sends events to — and receive events from — the server. It belongs to a given [namespace](https://socket.io/docs/v4/namespaces). A single Manager can be attached to several Socket instances. @@ -131,7 +131,7 @@ IO.Options options = IO.Options.builder() Socket socket = IO.socket(URI.create("https://example.com"), options); ``` -Note: in that case, sticky sessions are not required on the server side (more information [here](https://socket.io/docs/v3/using-multiple-nodes/)). +Note: in that case, sticky sessions are not required on the server side (more information [here](https://socket.io/docs/v4/using-multiple-nodes/)). #### `upgrade` @@ -153,7 +153,17 @@ It is the name of the path that is captured on the server side. The server and the client values must match: -*Server* +*Client* + +```java +IO.Options options = IO.Options.builder() + .setPath("/my-custom-path/") + .build(); + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +*JavaScript Server* ```js import { Server } from "socket.io"; @@ -167,17 +177,7 @@ io.on("connection", (socket) => { }); ``` -*Client* - -```java -IO.Options options = IO.Options.builder() - .setPath("/my-custom-path/") - .build(); - -Socket socket = IO.socket(URI.create("https://example.com"), options); -``` - -Please note that this is different from the path in the URI, which represents the [Namespace](https://socket.io/docs/v3/namespaces/). +Please note that this is different from the path in the URI, which represents the [Namespace](https://socket.io/docs/v4/namespaces/). Example: @@ -200,14 +200,6 @@ Additional query parameters (then found in `socket.handshake.query` object on th Example: -*Server* - -```js -io.on("connection", (socket) => { - console.log(socket.handshake.query); // prints { x: '42', EIO: '4', transport: 'polling' } -}); -``` - *Client* ```java @@ -218,6 +210,14 @@ IO.Options options = IO.Options.builder() Socket socket = IO.socket(URI.create("https://example.com"), options); ``` +*JavaScript Server* + +```js +io.on("connection", (socket) => { + console.log(socket.handshake.query); // prints { x: '42', EIO: '4', transport: 'polling' } +}); +``` + Note: The `socket.handshake.query` object contains the query parameters that were sent during the Socket.IO handshake, it won't be updated for the duration of the current session, which means changing the `query` on the client-side will only be effective when the current session is closed and a new one is created: ```java @@ -237,14 +237,6 @@ Additional headers (then found in `socket.handshake.headers` object on the serve Example: -*Server* - -```js -io.on("connection", (socket) => { - console.log(socket.handshake.headers); // prints { accept: '*/*', authorization: 'bearer 1234', connection: 'Keep-Alive', 'accept-encoding': 'gzip', 'user-agent': 'okhttp/3.12.12' } -}); -``` - *Client* ```java @@ -255,6 +247,14 @@ IO.Options options = IO.Options.builder() Socket socket = IO.socket(URI.create("https://example.com"), options); ``` +*JavaScript Server* + +```js +io.on("connection", (socket) => { + console.log(socket.handshake.headers); // prints { accept: '*/*', authorization: 'bearer 1234', connection: 'Keep-Alive', 'accept-encoding': 'gzip', 'user-agent': 'okhttp/3.12.12' } +}); +``` + Note: Similar to the `query` option above, the `socket.handshake.headers` object contains the headers that were sent during the Socket.IO handshake, it won't be updated for the duration of the current session, which means changing the `extraHeaders` on the client-side will only be effective when the current session is closed and a new one is created: ```java @@ -266,6 +266,84 @@ socket.io().on(Manager.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { }); ``` +#### `callFactory` + +The [OkHttpClient instance](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/) to use for HTTP long-polling requests. + +```java +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .readTimeout(1, TimeUnit.MINUTES) // important for HTTP long-polling + .build(); + +IO.Options options = new IO.Options(); +options.callFactory = okHttpClient; + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +#### `webSocketFactory` + +The [OkHttpClient instance](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/) to use for WebSocket connections. + +```java +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .minWebSocketMessageToCompress(2048) + .build(); + +IO.Options options = new IO.Options(); +options.webSocketFactory = okHttpClient; + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +### Manager options + +These settings will be shared by all Socket instances attached to the same Manager. + +#### `reconnection` + +Default value: `true` + +Whether reconnection is enabled or not. If set to `false`, you need to manually reconnect. + +#### `reconnectionAttempts` + +Default value: `Integer.MAX_VALUE` + +The number of reconnection attempts before giving up. + +#### `reconnectionDelay` + +Default value: `1_000` + +The initial delay before reconnection in milliseconds (affected by the [randomizationFactor](#randomizationfactor) value). + +#### `reconnectionDelayMax` + +Default value: `5_000` + +The maximum delay between two reconnection attempts. Each attempt increases the reconnection delay by 2x. + +#### `randomizationFactor` + +Default value: `0.5` + +The randomization factor used when reconnecting (so that the clients do not reconnect at the exact same time after a server crash, for example). + +Example with the default values: + +- 1st reconnection attempt happens between 500 and 1500 ms (`1000 * 2^0 * ()`) +- 2nd reconnection attempt happens between 1000 and 3000 ms (`1000 * 2^1 * ()`) +- 3rd reconnection attempt happens between 2000 and 5000 ms (`1000 * 2^2 * ()`) +- next reconnection attempts happen after 5000 ms + +#### `timeout` + +Default value: `20_000` + +The timeout in milliseconds for each connection attempt. + + ### Socket options These settings are specific to the given Socket instance. @@ -274,18 +352,10 @@ These settings are specific to the given Socket instance. Default value: - -Credentials that are sent when accessing a namespace (see also [here](https://socket.io/docs/v3/middlewares/#Sending-credentials)). +Credentials that are sent when accessing a namespace (see also [here](https://socket.io/docs/v4/middlewares/#sending-credentials)). Example: -*Server* - -```js -io.on("connection", (socket) => { - console.log(socket.handshake.auth); // prints { token: 'abcd' } -}); -``` - *Client* ```java @@ -296,6 +366,14 @@ IO.Options options = IO.Options.builder() Socket socket = IO.socket(URI.create("https://example.com"), options); ``` +*JavaScript Server* + +```js +io.on("connection", (socket) => { + console.log(socket.handshake.auth); // prints { token: 'abcd' } +}); +``` + You can update the `auth` map when the access to the Namespace is denied: ```java @@ -314,3 +392,135 @@ Or manually force the Socket instance to reconnect: options.auth.put("token", "efgh"); socket.disconnect().connect(); ``` + +## SSL connections + +### With a keystore + +```java +HostnameVerifier hostnameVerifier = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession sslSession) { + return hostname.equals("example.com"); + } +}; + +KeyStore ks = KeyStore.getInstance("JKS"); +File file = new File("path/to/the/keystore.jks"); +ks.load(new FileInputStream(file), "password".toCharArray()); + +KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); +kmf.init(ks, "password".toCharArray()); + +TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); +tmf.init(ks); + +SSLContext sslContext = SSLContext.getInstance("TLS"); +sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .hostnameVerifier(hostnameVerifier) + .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) tmf.getTrustManagers()[0]) + .readTimeout(1, TimeUnit.MINUTES) // important for HTTP long-polling + .build(); + +IO.Options options = new IO.Options(); +options.callFactory = okHttpClient; +options.webSocketFactory = okHttpClient; + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +### Trust all certificates + +Please use with caution, as this defeats the whole purpose of using secure connections. + +This is equivalent to `rejectUnauthorized: false` for the JavaScript client. + +```java +HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession sslSession) { + return true; + } +}; + +X509TrustManager trustManager = new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) { + // not implemented + } + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) { + // not implemented + } +}; + +SSLContext sslContext = SSLContext.getInstance("TLS"); +sslContext.init(null, new TrustManager[] { trustManager }, null); + +OkHttpClient okHttpClient = new OkHttpClient.Builder() + .hostnameVerifier(hostnameVerifier) + .sslSocketFactory(sslContext.getSocketFactory(), trustManager) + .readTimeout(1, TimeUnit.MINUTES) // important for HTTP long-polling + .build(); + +IO.Options options = new IO.Options(); +options.callFactory = okHttpClient; +options.webSocketFactory = okHttpClient; + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +## Multiplexing + +The Java client does support multiplexing: this allows to split the logic of your application into distinct modules, while using one single WebSocket connection to the server. + +Reference: https://socket.io/docs/v4/namespaces/ + +```java +Socket socket = IO.socket(URI.create("https://example.com")); // the main namespace +Socket productSocket = IO.socket(URI.create("https://example.com/product")); // the "product" namespace +Socket orderSocket = IO.socket(URI.create("https://example.com/order")); // the "order" namespace + +// all 3 sockets share the same Manager +System.out.println(socket.io() == productSocket.io()); // true +System.out.println(socket.io() == orderSocket.io()); // true +``` + +Please note that multiplexing will be disabled in the following cases: + +- multiple creation for the same namespace + +```java +Socket socket = IO.socket(URI.create("https://example.com")); +Socket socket2 = IO.socket(URI.create("https://example.com")); + +System.out.println(socket.io() == socket2.io()); // false +``` + +- different domains + +```java +Socket socket = IO.socket(URI.create("https://first.example.com")); +Socket socket2 = IO.socket(URI.create("https://second.example.com")); + +System.out.println(socket.io() == socket2.io()); // false +``` + +- usage of the [forceNew](#forceNew) option + +```java +IO.Options options = IO.Options.builder() + .setForceNew(true) + .build(); + +Socket socket = IO.socket(URI.create("https://example.com")); +Socket socket2 = IO.socket(URI.create("https://example.com/admin"), options); + +System.out.println(socket.io() == socket2.io()); // false +``` diff --git a/src/site/markdown/installation.md b/src/site/markdown/installation.md index 1eb05d80..a14593a8 100644 --- a/src/site/markdown/installation.md +++ b/src/site/markdown/installation.md @@ -3,7 +3,7 @@ | Client version | Socket.IO server | | -------------- | ---------------- | | 0.9.x | 1.x | -| 1.x | 2.x (or 3.1.x / 4.x with [`allowEIO3: true`](https://socket.io/docs/v4/server-initialization/#allowEIO3)) | +| 1.x | 2.x (or 3.1.x / 4.x with [`allowEIO3: true`](https://socket.io/docs/v4/server-options/#alloweio3)) | | 2.x | 3.x / 4.x | ## Installation @@ -17,7 +17,7 @@ Add the following dependency to your `pom.xml`. io.socket socket.io-client - 2.0.1 + 2.1.0 ``` @@ -26,8 +26,18 @@ Add the following dependency to your `pom.xml`. Add it as a gradle dependency for Android Studio, in `build.gradle`: ```groovy -compile ('io.socket:socket.io-client:2.0.1') { +implementation ('io.socket:socket.io-client:2.1.0') { // excluding org.json which is provided by Android exclude group: 'org.json', module: 'json' } ``` + +## Dependency tree + +| `socket.io-client` | `engine.io-client` | `okhttp` | +|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| `2.1.0` ([diff](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-2.0.1...socket.io-client-2.1.0)) | `2.1.0` ([diff](https://github.com/socketio/engine.io-client-java/compare/engine.io-client-2.0.0...engine.io-client-2.1.0)) | `3.12.12` | +| `2.0.1` ([diff](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-2.0.0...socket.io-client-2.0.1)) | `2.0.0` | `3.12.12` | +| `2.0.0` ([diff](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-1.0.1...socket.io-client-2.0.0)) | `2.0.0` ([diff](https://github.com/socketio/engine.io-client-java/compare/engine.io-client-1.0.1...engine.io-client-2.0.0)) | `3.12.12` | +| `1.0.1` ([diff](https://github.com/socketio/socket.io-client-java/compare/socket.io-client-1.0.0...socket.io-client-1.0.1)) | `1.0.1` ([diff](https://github.com/socketio/engine.io-client-java/compare/engine.io-client-1.0.0...engine.io-client-1.0.1)) | `3.12.12` ([changelog](https://square.github.io/okhttp/changelogs/changelog_3x/#version-31212)) | +| `1.0.0` | `1.0.0` | `3.8.1` | diff --git a/src/site/markdown/listening_to_events.md b/src/site/markdown/listening_to_events.md index 6caf0a61..843fb182 100644 --- a/src/site/markdown/listening_to_events.md +++ b/src/site/markdown/listening_to_events.md @@ -1,6 +1,6 @@ # Listening to events -See also: https://socket.io/docs/v3/listening-to-events/ +See also: https://socket.io/docs/v4/listening-to-events/ **Table of content** @@ -69,3 +69,27 @@ Removes all listeners (for any event). ```java socket.off(); ``` + +## Catch-all listeners + +### For incoming packets + +```java +socket.onAnyIncoming(new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); +``` + +### For outgoing packets + +```java +socket.onAnyOutgoing(new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); +``` diff --git a/src/site/markdown/logging.md b/src/site/markdown/logging.md new file mode 100644 index 00000000..0dc9adbd --- /dev/null +++ b/src/site/markdown/logging.md @@ -0,0 +1,213 @@ +# Logging + +This library uses JUL (`java.util.logging`) for its debug logs. + +Here's how you can display those logs, depending on your logging library: + + + +## Usage with JUL + +`src/main/resources/logging.properties` + +```properties +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = ALL + +.level = INFO +io.socket.level = FINE +``` + +`src/main/java/MyApp.java` + +```java +public class MyApp { + private static final Logger logger = Logger.getLogger("MyApp"); + + public static void main(String[] argz) throws Exception { + InputStream stream = MyApp.class.getResourceAsStream("logging.properties"); + LogManager.getLogManager().readConfiguration(stream); + + Socket socket = IO.socket(URI.create("https://example.com")); + + socket.on(Socket.EVENT_CONNECT, args -> logger.info("connected!")); + + socket.connect(); + } +} +``` + +Reference: https://docs.oracle.com/en/java/javase/17/core/java-logging-overview.html + +## Usage with Log4j2 + +`pom.xml` + +```xml + + + + + org.apache.logging.log4j + log4j-api + 2.18.0 + + + org.apache.logging.log4j + log4j-core + 2.18.0 + + + org.apache.logging.log4j + log4j-jul + 2.18.0 + + ... + + ... + +``` + +Maven repository: + +- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api +- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core +- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-jul + +`src/main/resources/log4j2.xml` + +```xml + + + + + + + + + + + + + + + + +``` + +`src/main/java/MyApp.java` + +Either by setting the `java.util.logging.manager` environment variable: + +```java +public class MyApp { + private static final Logger logger; + + static { + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + logger = LogManager.getLogger(MyApp.class); + } + + public static void main(String[] argz) throws Exception { + Socket socket = IO.socket(URI.create("https://example.com")); + + socket.on(Socket.EVENT_CONNECT, args -> logger.info("connected!")); + + socket.connect(); + } +} +``` + +Or with the `Log4jBridgeHandler` class: + +```java +public class MyApp { + private static final Logger logger = LogManager.getLogger(MyApp.class); + + public static void main(String[] argz) throws Exception { + Log4jBridgeHandler.install(true, "", true); + + Socket socket = IO.socket(URI.create("https://example.com")); + + socket.on(Socket.EVENT_CONNECT, args -> logger.info("connected!")); + + socket.connect(); + } +} +``` + +Reference: https://logging.apache.org/log4j/2.x/log4j-jul/index.html + +## Usage with Slf4j + logback + +`pom.xml` + +```xml + + + + + ch.qos.logback + logback-classic + 1.2.11 + + + org.slf4j + jul-to-slf4j + 1.7.36 + + ... + + ... + +``` + +Maven repository: + +- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic +- https://mvnrepository.com/artifact/org.slf4j/jul-to-slf4j + +`src/main/resources/logback.xml` + +```xml + + + true + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + +``` + +`src/main/java/MyApp.java` + +```java +public class MyApp { + private static final Logger logger = LoggerFactory.getLogger(MyApp.class); + + static { + SLF4JBridgeHandler.install(); + } + + public static void main(String[] argz) throws Exception { + Socket socket = IO.socket(URI.create("https://example.com")); + + socket.on(Socket.EVENT_CONNECT, args -> logger.info("connected!")); + + socket.connect(); + } +} +``` + +Reference: https://www.slf4j.org/manual.html diff --git a/src/site/markdown/migrating_from_1_x.md b/src/site/markdown/migrating_from_1_x.md index f13bebf3..37c56550 100644 --- a/src/site/markdown/migrating_from_1_x.md +++ b/src/site/markdown/migrating_from_1_x.md @@ -7,12 +7,12 @@ Here is the compatibility table: | Java client version | Socket.IO server | | -------------- | ---------------- | | 0.9.x | 1.x | -| 1.x | 2.x (or 3.1.x / 4.x with [`allowEIO3: true`](https://socket.io/docs/v4/server-initialization/#allowEIO3)) | +| 1.x | 2.x (or 3.1.x / 4.x with [`allowEIO3: true`](https://socket.io/docs/v4/server-options/#alloweio3)) | | 2.x | 3.x / 4.x | **Important note:** due to the backward incompatible changes to the Socket.IO protocol, a 2.x Java client will not be able to reach a 2.x server, and vice-versa -Since the Java client matches the Javascript client quite closely, most of the changes listed in the migration guide [here](https://socket.io/docs/v3/migrating-from-2-x-to-3-0) also apply to the Java client: +Since the Java client matches the Javascript client quite closely, most of the changes listed in the migration guide [here](https://socket.io/docs/v4/migrating-from-2-x-to-3-0) also apply to the Java client: - [A middleware error will now emit an Error object](#A_middleware_error_will_now_emit_an_Error_object) - [The Socket `query` option is renamed to `auth`](#The_Socket_query_option_is_renamed_to_auth) diff --git a/src/site/markdown/socket_instance.md b/src/site/markdown/socket_instance.md index 26457164..5a40be45 100644 --- a/src/site/markdown/socket_instance.md +++ b/src/site/markdown/socket_instance.md @@ -113,7 +113,7 @@ socket.on("data", new Emitter.Listener() { ### `Socket.EVENT_CONNECT_ERROR` -This event is fired when the server does not accept the connection (in a [middleware function](https://socket.io/docs/v3/middlewares/#Sending-credentials)). +This event is fired when the server does not accept the connection (in a [middleware function](https://socket.io/docs/v4/middlewares/#sending-credentials)). You need to manually reconnect. You might need to update the credentials: @@ -144,7 +144,7 @@ Here is the list of possible reasons: Reason | Description ------ | ----------- -`io server disconnect` | The server has forcefully disconnected the socket with [socket.disconnect()](https://socket.io/docs/v3/server-api/#socket-disconnect-close) +`io server disconnect` | The server has forcefully disconnected the socket with [socket.disconnect()](https://socket.io/docs/v4/server-api/#socketdisconnectclose) `io client disconnect` | The socket was manually disconnected using `socket.disconnect()` `ping timeout` | The server did not respond in the `pingTimeout` range `transport close` | The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G) diff --git a/src/site/site.xml b/src/site/site.xml index 3f81c927..5d1d34ac 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -30,6 +30,9 @@ + + + diff --git a/src/test/java/io/socket/Fiddle.java b/src/test/java/io/socket/Fiddle.java new file mode 100644 index 00000000..ca4d76d9 --- /dev/null +++ b/src/test/java/io/socket/Fiddle.java @@ -0,0 +1,39 @@ +package io.socket; + +import io.socket.client.IO; +import io.socket.client.Socket; +import io.socket.emitter.Emitter; + +import java.net.URI; + +public class Fiddle { + + public static void main(String[] argz) throws Exception { + IO.Options options = new IO.Options(); + + Socket socket = IO.socket(URI.create("http://localhost:3000"), options); + + socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println("connect"); + } + }); + + socket.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println("connect_error: " + args[0]); + } + }); + + socket.on(Socket.EVENT_DISCONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println("disconnect due to: " + args[0]); + } + }); + + socket.connect(); + } +} diff --git a/src/test/java/io/socket/backo/BackoffTest.java b/src/test/java/io/socket/backo/BackoffTest.java index a268829f..8ae61de4 100644 --- a/src/test/java/io/socket/backo/BackoffTest.java +++ b/src/test/java/io/socket/backo/BackoffTest.java @@ -44,4 +44,10 @@ public void durationOverflow() { } } } + + @Test(expected = IllegalArgumentException.class) + public void ensureJitterIsValid() { + Backoff b = new Backoff(); + b.setJitter(2); + } } diff --git a/src/test/java/io/socket/client/SocketTest.java b/src/test/java/io/socket/client/SocketTest.java index 7cc76fd5..3db641db 100644 --- a/src/test/java/io/socket/client/SocketTest.java +++ b/src/test/java/io/socket/client/SocketTest.java @@ -4,18 +4,17 @@ import io.socket.util.Optional; import org.json.JSONException; import org.json.JSONObject; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.Timer; import java.util.TimerTask; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.*; import static java.util.Collections.singletonMap; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -287,4 +286,168 @@ public void call(Object... args) { assertThat(values.take(), is("first")); assertThat(values.take(), is("second")); } + + @Test(timeout = TIMEOUT) + public void shouldTimeoutAfterTheGivenDelayWhenSocketIsNotConnected() throws InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue<>(); + + socket = client(); + + socket.emit("event", new AckWithTimeout(50) { + @Override + public void onSuccess(Object... args) { + fail(); + } + + @Override + public void onTimeout() { + values.offer(true); + } + }); + + assertThat(values.take(), is(true)); + } + + @Test(timeout = TIMEOUT) + public void shouldTimeoutWhenTheServerDoesNotAcknowledgeTheEvent() throws InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue<>(); + + socket = client(); + + socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.emit("unknown", new AckWithTimeout(50) { + @Override + public void onTimeout() { + values.offer(true); + } + + @Override + public void onSuccess(Object... args) { + fail(); + } + }); + } + }); + + socket.connect(); + + assertThat(values.take(), is(true)); + } + + @Test(timeout = TIMEOUT) + public void shouldTimeoutWhenTheServerDoesNotAcknowledgeTheEventInTime() throws InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue<>(); + + socket = client(); + + socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.emit("ack", new AckWithTimeout(0) { + @Override + public void onTimeout() { + values.offer(true); + } + + @Override + public void onSuccess(Object... args) { + fail(); + } + }); + } + }); + + socket.connect(); + + assertThat(values.take(), is(true)); + } + + @Test(timeout = TIMEOUT) + public void shouldNotTimeoutWhenTheServerDoesAcknowledgeTheEvent() throws InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue<>(); + + socket = client(); + + socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.emit("ack", 1, "2", new byte[] { 3 }, new AckWithTimeout(200) { + @Override + public void onTimeout() { + fail(); + } + + @Override + public void onSuccess(Object... args) { + for (Object arg : args) { + values.offer(arg); + } + } + }); + } + }); + + socket.connect(); + + assertThat((Integer) values.take(), is(1)); + assertThat((String) values.take(), is("2")); + assertThat((byte[]) values.take(), is(new byte[] { 3 })); + } + + @Test(timeout = TIMEOUT) + public void shouldCallCatchAllListenerForIncomingPackets() throws InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue<>(); + + socket = client(); + + socket.on("message", new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.emit("echo", 1, "2", new byte[] { 3 }); + + socket.onAnyIncoming(new Emitter.Listener() { + @Override + public void call(Object... args) { + for (Object arg : args) { + values.offer(arg); + } + } + }); + } + }); + + socket.connect(); + + assertThat((String) values.take(), is("echoBack")); + assertThat((Integer) values.take(), is(1)); + assertThat((String) values.take(), is("2")); + assertThat((byte[]) values.take(), is(new byte[] { 3 })); + } + + @Test(timeout = TIMEOUT) + public void shouldCallCatchAllListenerForOutgoingPackets() throws InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue<>(); + + socket = client(); + + socket.emit("echo", 1, "2", new byte[] { 3 }); + + socket.onAnyOutgoing(new Emitter.Listener() { + @Override + public void call(Object... args) { + for (Object arg : args) { + values.offer(arg); + } + } + }); + + socket.connect(); + + assertThat((String) values.take(), is("echo")); + assertThat((Integer) values.take(), is(1)); + assertThat((String) values.take(), is("2")); + assertThat((byte[]) values.take(), is(new byte[] { 3 })); + } }