diff --git a/README.md b/README.md index dd9cc38..4eed16b 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,7 @@ See also: ## Table of content - [Compatibility](#compatibility) -- [Installation](#installation) - - [Maven](#maven) - - [Gradle](#gradle) -- [Usage](#usage) -- [Features](#features) +- [Documentation](#documentation) - [License](#license) ## Compatibility @@ -27,180 +23,11 @@ See also: | 1.x | 2.x | | 2.x | 3.x | -## Installation -The latest artifact is available on Maven Central. +## Documentation -### Maven -Add the following dependency to your `pom.xml`. +The documentation can be found [here](https://socketio.github.io/socket.io-client-java/installation.html). -```xml - - - io.socket - socket.io-client - 2.0.0 - - -``` - -### Gradle -Add it as a gradle dependency for Android Studio, in `build.gradle`: - -```groovy -compile ('io.socket:socket.io-client:2.0.0') { - // excluding org.json which is provided by Android - exclude group: 'org.json', module: 'json' -} -``` - -## Usage -Socket.IO-client Java has almost the same api and features with the original JS client. You use `IO#socket` to initialize `Socket`: - -```java -import io.socket.client.IO; -import io.socket.client.Socket; -... - -Socket socket = IO.socket("http://localhost"); -socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { - - @Override - public void call(Object... args) { - socket.emit("foo", "hi"); - socket.disconnect(); - } - -}).on("event", new Emitter.Listener() { - - @Override - public void call(Object... args) {} - -}).on(Socket.EVENT_DISCONNECT, new Emitter.Listener() { - - @Override - public void call(Object... args) {} - -}); -socket.connect(); -``` - -This Library uses [org.json](https://github.com/stleary/JSON-java) to parse and compose JSON strings: - -```java -// Sending an object -JSONObject obj = new JSONObject(); -obj.put("hello", "server"); -obj.put("binary", new byte[42]); -socket.emit("foo", obj); - -// Receiving an object -socket.on("foo", new Emitter.Listener() { - @Override - public void call(Object... args) { - JSONObject obj = (JSONObject)args[0]; - } -}); -``` - -Options are supplied as follows: - -```java -IO.Options opts = new IO.Options(); -opts.forceNew = true; -opts.reconnection = false; - -socket = IO.socket("http://localhost", opts); -``` - -You can supply query parameters with the `query` option. NB: if you don't want to reuse a cached socket instance when the query parameter changes, you should use the `forceNew` option, the use case might be if your app allows for a user to logout, and a new user to login again: - -```java -IO.Options opts = new IO.Options(); -opts.forceNew = true; -opts.query = "auth_token=" + authToken; -Socket socket = IO.socket("http://localhost", opts); -``` - -You can get a callback with `Ack` when the server received a message: - -```java -socket.emit("foo", "woot", new Ack() { - @Override - public void call(Object... args) {} -}); -``` - -And vice versa: - -```java -// ack from client to server -socket.on("foo", new Emitter.Listener() { - @Override - public void call(Object... args) { - Ack ack = (Ack) args[args.length - 1]; - ack.call(); - } -}); -``` - -SSL (HTTPS, WSS) settings: - -```java -OkHttpClient okHttpClient = new OkHttpClient.Builder() - .hostnameVerifier(myHostnameVerifier) - .sslSocketFactory(mySSLContext.getSocketFactory(), myX509TrustManager) - .build(); - -// default settings for all sockets -IO.setDefaultOkHttpWebSocketFactory(okHttpClient); -IO.setDefaultOkHttpCallFactory(okHttpClient); - -// set as an option -opts = new IO.Options(); -opts.callFactory = okHttpClient; -opts.webSocketFactory = okHttpClient; -socket = IO.socket("https://localhost", opts); -``` - -See the Javadoc for more details. - -http://socketio.github.io/socket.io-client-java/apidocs/ - -### Transports and HTTP Headers -You can access transports and their HTTP headers as follows. - -```java -// Called upon transport creation. -socket.io().on(Manager.EVENT_TRANSPORT, new Emitter.Listener() { - @Override - public void call(Object... args) { - Transport transport = (Transport)args[0]; - - transport.on(Transport.EVENT_REQUEST_HEADERS, new Emitter.Listener() { - @Override - public void call(Object... args) { - @SuppressWarnings("unchecked") - Map> headers = (Map>)args[0]; - // modify request headers - headers.put("Cookie", Arrays.asList("foo=1;")); - } - }); - - transport.on(Transport.EVENT_RESPONSE_HEADERS, new Emitter.Listener() { - @Override - public void call(Object... args) { - @SuppressWarnings("unchecked") - Map> headers = (Map>)args[0]; - // access response headers - String cookie = headers.get("Set-Cookie").get(0); - } - }); - } -}); -``` - -## Features -This library supports all of the features the JS client does, including events, options and upgrading transport. Android is fully supported. +The source of this documentation is in the `src/site/` directory of the repository. Pull requests are welcome! ## License diff --git a/src/site/markdown/initialization.md b/src/site/markdown/initialization.md new file mode 100644 index 0000000..efef638 --- /dev/null +++ b/src/site/markdown/initialization.md @@ -0,0 +1,316 @@ +# Initialization + +**Table of content** + + + +## Creation of a Socket instance + +```java +URI uri = URI.create("https://example.com"); +IO.Options options = IO.Options.builder() + // ... + .build(); + +Socket socket = IO.socket(uri, options); +``` + +Unlike the JS client (which can infer it from the `window.location` object), the URI is mandatory here. + +The [scheme](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax) part of the URI is also mandatory. Both `ws://` and `http://` can be used interchangeably. + +```java +Socket socket = IO.socket("https://example.com"); // OK +Socket socket = IO.socket("wss://example.com"); // OK, similar to the example above +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: + +```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 +``` + +## Default values + +```java +IO.Options options = IO.Options.builder() + // IO factory options + .setForceNew(false) + .setMultiplex(true) + + // low-level engine options + .setTransports(new String[] { Polling.NAME, WebSocket.NAME }) + .setUpgrade(true) + .setRememberUpgrade(false) + .setPath("/socket.io/") + .setQuery(null) + .setExtraHeaders(null) + + // Manager options + .setReconnection(true) + .setReconnectionAttempts(Integer.MAX_VALUE) + .setReconnectionDelay(1_000) + .setReconnectionDelayMax(5_000) + .setRandomizationFactor(0.5) + .setTimeout(20_000) + + // Socket options + .setAuth(null) + .build(); +``` + +## Description + +### IO factory options + +These settings will be shared by all Socket instances attached to the same Manager. + +#### `forceNew` + +Default value: `false` + +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 single Manager can be attached to several Socket instances. + +The following example will reuse the same Manager instance for the 3 Socket instances (one single WebSocket connection): + +```java +IO.Options options = IO.Options.builder() + .setForceNew(false) + .build(); + +Socket socket = IO.socket(URI.create("https://example.com"), options); // the main namespace +Socket productSocket = IO.socket(URI.create("https://example.com/product"), options); // the "product" namespace +Socket orderSocket = IO.socket(URI.create("https://example.com/order"), options); // the "order" namespace +``` + +The following example will create 3 different Manager instances (and thus 3 distinct WebSocket connections): + +```java +IO.Options options = IO.Options.builder() + .setForceNew(true) + .build(); + +Socket socket = IO.socket(URI.create("https://example.com"), options); // the main namespace +Socket productSocket = IO.socket(URI.create("https://example.com/product"), options); // the "product" namespace +Socket orderSocket = IO.socket(URI.create("https://example.com/order"), options); // the "order" namespace +``` + +#### `multiplex` + +Default value: `true` + +The opposite of `forceNew`: whether to reuse an existing Manager instance. + +### Low-level engine options + +#### `transports` + +Default value: `new String[] { Polling.NAME, WebSocket.NAME }` + +The low-level connection to the Socket.IO server can either be established with: + +- HTTP long-polling: successive HTTP requests (`POST` for writing, `GET` for reading) +- [WebSocket](https://en.wikipedia.org/wiki/WebSocket) + +The following example disables the HTTP long-polling transport: + +```java +IO.Options options = IO.Options.builder() + .setTransports(new String[] { WebSocket.NAME }) + .build(); + +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/)). + +#### `upgrade` + +Default value: `true` + +Whether the client should try to upgrade the transport from HTTP long-polling to something better. + +#### `rememberUpgrade` + +Default value: `false` + +If true and if the previous WebSocket connection to the server succeeded, the connection attempt will bypass the normal upgrade process and will initially try WebSocket. A connection attempt following a transport error will use the normal upgrade process. It is recommended you turn this on only when using SSL/TLS connections, or if you know that your network does not block websockets. + +#### `path` + +Default value: `/socket.io/` + +It is the name of the path that is captured on the server side. + +The server and the client values must match: + +*Server* + +```js +import { Server } from "socket.io"; + +const io = new Server(8080, { + path: "/my-custom-path/" +}); + +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/). + +Example: + +```java +IO.Options options = IO.Options.builder() + .setPath("/my-custom-path/") + .build(); + +Socket socket = IO.socket(URI.create("https://example.com/order"), options); +``` + +- the Socket instance is attached to the "order" Namespace +- the HTTP requests will look like: `GET https://example.com/my-custom-path/?EIO=4&transport=polling&t=ML4jUwU` + +#### `query` + +Default value: - + +Additional query parameters (then found in `socket.handshake.query` object on the server-side). + +Example: + +*Server* + +```js +io.on("connection", (socket) => { + console.log(socket.handshake.query); // prints { x: '42', EIO: '4', transport: 'polling' } +}); +``` + +*Client* + +```java +IO.Options options = IO.Options.builder() + .setQuery("x=42") + .build(); + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +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 +socket.io().on(Manager.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + options.query = "y=43"; + } +}); +``` + +#### `extraHeaders` + +Default value: - + +Additional headers (then found in `socket.handshake.headers` object on the server-side). + +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 +IO.Options options = IO.Options.builder() + .setExtraHeaders(singletonMap("authorization", singletonList("bearer 1234"))) + .build(); + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +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 +socket.io().on(Manager.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + options.extraHeaders.put("authorization", singletonList("bearer 5678")); + } +}); +``` + +### Socket options + +These settings are specific to the given Socket instance. + +#### `auth` + +Default value: - + +Credentials that are sent when accessing a namespace (see also [here](https://socket.io/docs/v3/middlewares/#Sending-credentials)). + +Example: + +*Server* + +```js +io.on("connection", (socket) => { + console.log(socket.handshake.auth); // prints { token: 'abcd' } +}); +``` + +*Client* + +```java +IO.Options options = IO.Options.builder() + .setAuth(singletonMap("token", "abcd")) + .build(); + +Socket socket = IO.socket(URI.create("https://example.com"), options); +``` + +You can update the `auth` map when the access to the Namespace is denied: + +```java +socket.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() { + @Override + public void call(Object... args) { + options.auth.put("token", "efgh"); + socket.connect(); + } +}); +``` + +Or manually force the Socket instance to reconnect: + +```java +options.auth.put("token", "efgh"); +socket.disconnect().connect(); +``` diff --git a/src/site/markdown/socket_instance.md b/src/site/markdown/socket_instance.md new file mode 100644 index 0000000..2645716 --- /dev/null +++ b/src/site/markdown/socket_instance.md @@ -0,0 +1,158 @@ +# The Socket instance + +**Table of content** + + + +- [Javadoc](apidocs/index.html?io/socket/client/Socket.html) + +Besides [emitting](emitting_events.html) and [listening to](listening_to_events.html) events, the Socket instance has a few attributes that may be of use in your application: + +## Socket#id + +Each new connection is assigned a random 20-characters identifier. + +This identifier is synced with the value on the server-side. + +*Server* + +```js +io.on("connection", (socket) => { + console.log(socket.id); // x8WIv7-mJelg7on_ALbx +}); +``` + +*Client* + +```java +socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println(socket.id()); // x8WIv7-mJelg7on_ALbx + } +}); + +socket.on(Socket.EVENT_DISCONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println(socket.id()); // null + } +}); +``` + +## Socket#connected + +This attribute describes whether the socket is currently connected to the server. + +```java +socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println(socket.connected()); // true + } +}); + +socket.on(Socket.EVENT_DISCONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println(socket.connected()); // false + } +}); +``` + +## Lifecycle + +Lifecycle diagram + +## Events + +### `Socket.EVENT_CONNECT` + +This event is fired by the Socket instance upon connection / reconnection. + +```java +socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); +``` + +Please note that you shouldn't register event handlers in the `connect` handler itself, as a new handler will be registered every time the Socket reconnects: + +```java +// BAD +socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.on("data", new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } + }); + } +}); + +// GOOD +socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); + +socket.on("data", new Emitter.Listener() { + @Override + public void call(Object... args) { + // ... + } +}); +``` + +### `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)). + +You need to manually reconnect. You might need to update the credentials: + +```java +socket.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() { + @Override + public void call(Object... args) { + options.auth.put("authorization", "bearer 1234"); + socket.connect(); + } +}); +``` + +### `Socket.EVENT_DISCONNECT` + +This event is fired upon disconnection. + +```java +socket.on(Socket.EVENT_DISCONNECT, new Emitter.Listener() { + @Override + public void call(Object... args) { + System.out.println(socket.id()); // null + } +}); +``` + +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 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) +`transport error` | The connection has encountered an error (example: the server was killed during a HTTP long-polling cycle) + +Note: those events, along with `disconnecting`, `newListener` and `removeListener`, are special events that shouldn't be used in your application: + +```js +// BAD, will throw an error +socket.emit("disconnect"); +``` diff --git a/src/site/resources/images/client_socket_events.png b/src/site/resources/images/client_socket_events.png new file mode 100644 index 0000000..c2ea34c Binary files /dev/null and b/src/site/resources/images/client_socket_events.png differ diff --git a/src/site/site.xml b/src/site/site.xml index fbd329e..3f81c92 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -25,9 +25,10 @@ - + +