Friday Mar 21, 2014

Starting with Oracle Coherence and Incubator projects

I had a chance to play with Oracle Coherence and even when everything seems to be documented, it took me a while to find all necessary informations to really get started, so I decided to share my steps. The main goal of this HOWTO will be building all samples from Coherence Incubator project, branch develop-12 (requires latest Coherence – 12.1.2).

Getting Coherence

Coherence 12.1.2 can be downloaded from [here]. I’m using stand-alone install, but I’m pretty sure that everything should be mostly the same (from Coherence point of view). Downloaded zip archive contains coherence_121200.jar, so lets execute that. Oh, and I almost forgot – I’m using Java 8:

1
2
3
4
$ java -version
java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode)

And I’m on Mac OS X 10.9.2 (latest Mavericks). Installation process is pretty straightforward, you just execute java -jar ./coherence_121200.jar and use the wizard to install what you need. I choose samples to be included, but I won’t be referring to them in this post. Remember where you set your Oracle Home directory, it contains all the installed artefacts and we need to use it in next step.

Let’s assume the installation is done without any issues (I did not experienced any) and we are in Oracle_Home directory somewhere on our file system. Another thing I forgot to mention – you will need Apache Maven to be able to compile/execute projects from Incubator. I have Maven 3.1.1:

1
2
3
4
5
6
7
$ mvn -v
Apache Maven 3.1.1 (0728685237757ffbf44136acec0402957f723d9a; 2013-09-17 17:22:22+0200)
Maven home: /Users/pavel/opt/apache-maven-3.1.1
Java version: 1.8.0, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "mac os x", version: "10.9.2", arch: "x86_64", family: "mac"

Following information is taken from document available here:http://docs.oracle.com/middleware/1212/core/MAVEN.pdf.

Firstly, you need to install oracle-maven-sync plugin. It will install Coherence into your local maven repository. It is capable to publishing these artefacts to your company maven repository or something like that, but that won’t be covered here. Please see linked document to see more details about it if required.

The plugin gets installed by executing following command:

1
mvn install:install-file -DpomFile=./oracle_common/plugins/maven/com/oracle/maven/oracle-maven-sync/12.1.2/oracle-maven-sync.12.1.2.pom -Dfile=./oracle_common/plugins/maven/com/oracle/maven/oracle-maven-sync/12.1.2/oracle-maven-sync.12.1.2.jar

I assume that this command is executed from Oracle_Home directory. You will need to adjust paths if that is not your case. Then you should see something similar to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[INFO] Scanning for projects...
[INFO]                                                                        
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-install-plugin:2.4:install-file (default-cli) @ standalone-pom ---
[INFO] Installing /Users/pavel/coherence/Oracle/Middleware/Oracle_Home/oracle_common/plugins/maven/com/oracle/maven/oracle-maven-sync/12.1.2/oracle-maven-sync.12.1.2.pom to /Users/pavel/.m2/repository/com/oracle/maven/oracle-maven-sync/12.1.2-0-0/oracle-maven-sync-12.1.2-0-0.jar
[INFO] Installing /Users/pavel/coherence/Oracle/Middleware/Oracle_Home/oracle_common/plugins/maven/com/oracle/maven/oracle-maven-sync/12.1.2/oracle-maven-sync.12.1.2.pom to /Users/pavel/.m2/repository/com/oracle/maven/oracle-maven-sync/12.1.2-0-0/oracle-maven-sync-12.1.2-0-0.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.349s
[INFO] Finished at: Fri Mar 21 15:26:37 CET 2014
[INFO] Final Memory: 3M/81M
[INFO] ------------------------------------------------------------------------

Now we are ready to install Coherence binaries to our local maven repository:

1
Oracle_Home $ mvn -X com.oracle.maven:oracle-maven-sync:push -Doracle-maven-sync.oracleHome=.  -Doracle-maven-sync.testingOnly=false

Output is quite long, but if you see “BUILD SUCESS”, you can be sure that coherence jars are now installed in local maven repository. If there is some error, it is most likely related to incorrect directory, so try to play with “-Doracle-maven-sync.oracleHome” property and get it right.

Getting Coherence Incubator projects

This is an easy part. Coherence incubator is hosted on github – you obviously need git for that.

1
git clone git@github.com:coherence-community/coherence-incubator.git

thats it. Git created directory coherence-incubator where all the projects are. Little inconvenient is that the version which we want to get is in the branch, so we need to execute:

1
git checkout origin/develop-12 -b develop-12

Previous command checkouts remote branch “develop-12″ and saves it as local branch with the same name. It is not a must to create local branch, but it is easier to deal with that. Also, we will need to make some changes to get it running and you can store then in your local branch.

I’m not exactly sure why, but the project references Coherence version 12.1.2-0-1, but oracle-maven-sync plugin installs version 12.1.2-0-0, so the project can’t compile without modifications. Also, coherence-jvisualvm references binaries deployed on netbeans maven repository which is not mentioned anywhere, so we need to add it for maven to be able to download these dependencies. My complete patch is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
diff --git a/pom.xml b/pom.xml
index 785741e..c4d951a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -137,11 +137,18 @@
<junit.version>4.10</junit.version>
<miglayout.version>3.6</miglayout.version>
<mockito.version>1.9.0</mockito.version>
-        <oracle.coherence.version>12.1.2-0-1</oracle.coherence.version>
+        <oracle.coherence.version>12.1.2-0-0</oracle.coherence.version>
<oracle.tools.version>1.2.2</oracle.tools.version>
<powermock.version>1.4.12</powermock.version>
</properties>
+    <repositories>
+        <repository>
+            <id>netbeans</id>
+        </repository>
+    </repositories>
+
<dependencyManagement>
<dependencies>
<dependency>

Now, we are finally ready for

1
mvn -fae clean install

The “-fae” is here for trying to build everything possible, even when some project fails to build or test, which unfortunately happens. See my current results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[INFO] Coherence Incubator ............................... SUCCESS [1.284s]
[INFO] Coherence Incubator Common ........................ SUCCESS [30.717s]
[INFO] Coherence Incubator Command Pattern ............... SUCCESS [7.556s]
[INFO] Coherence Incubator Command Pattern (examples) .... SUCCESS [3.357s]
[INFO] Coherence Incubator Functor Pattern ............... SUCCESS [3.828s]
[INFO] Coherence Incubator Functor Pattern (examples) .... SUCCESS [2.577s]
[INFO] Coherence Incubator JVisualVM Plugin .............. SUCCESS [18.846s]
[INFO] Coherence Incubator Processing Pattern ............ SUCCESS [14.535s]
[INFO] Coherence Incubator Processing Pattern (examples) . SUCCESS [2.018s]
[INFO] Coherence Incubator Messaging Pattern ............. SUCCESS [8.903s]
[INFO] Coherence Incubator Messaging Pattern (functional tests)  SUCCESS [1:52.155s]
[INFO] Coherence Incubator Event Distribution Pattern .... SUCCESS [44.490s]
[INFO] Coherence Incubator Push Replication Pattern ...... SUCCESS [2.198s]
[INFO] Coherence Incubator Push Replication Pattern (functional tests)  FAILURE [6:49.643s]
[INFO] Coherence Incubator Push Replication Pattern (examples)  SUCCESS [1.645s]
[INFO] Coherence Incubator Push Replication Pattern *Web (examples)  SUCCESS [0.013s]
[INFO] Coherence Incubator Web Server .................... SUCCESS [2.058s]
[INFO] Coherence Incubator Web Application ............... SUCCESS [6.210s]
[INFO] Coherence Incubator Web Application Test .......... FAILURE [1:27.214s]
[INFO] Coherence Incubator Distribution .................. SUCCESS [13.540s]
[INFO] Coherence Incubator Site .......................... SUCCESS [0.019s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12:53.648s
[INFO] Finished at: Fri Mar 21 16:30:09 CET 2014
[INFO] Final Memory: 58M/210M
[INFO] ------------------------------------------------------------------------

Anyway, as you can see, most of the projects can compile and are working properly. I was interested mainly in Messaging pattern, which works as expected, so I did not do any further inquiries about in Replication pattern and Web app tests.

And that’s it. If you are interested in any of these patterns or project, see sources and tests, there are usually “hello world” like samples which are perfect for learning. Also, if you happen to discover the reason for test failures I’m getting, feel free to add comment or send me a note to pavel.bucek [at] oracle.com and I’ll update the article.

Tuesday Mar 18, 2014

WebSocket API in JDK 8 - MessageHandler issue

JDK 8 is not yet released and it is already causing some headaches for JSR 356 implementors. The issue described in this article will be about the most famous and anticipated JDK 8 feature – lambda expressions and its impact to one part of WebSocket API.

Lambda expressions usable in WebSocket API can be diminished to just a replacement for standard anonymous classes; there might be some places where you can use method references, but not in the case of message handler. Following text is mainly about addMessageHandler method:

1
2
3
public interface Session extends Closeable {
void addMessageHandler(MessageHandler handler) throws IllegalStateException;
}

There are multiple use cases when addMessageHandler method is used – for example, in any endpoint based on programmatic API, which is typically true on client side. Then you have code like:

1
2
3
4
5
6
7
8
9
10
11
public class MyEndpoint extends Endpoint {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
// handle message
}
});
}
}

which always works as expected. JDK 8 introduces lambda expressions which can (not only!) replace too verbose anonymous classes, when they have only single method – which is true for MessageHandler.Whole interface. So when that code is opened in IDE supporting Java 8, it will usually say something like “anonymous class can be replaced with lambda expression” and recommends that action to be done. The resulting code will be:

1
2
3
4
5
6
7
8
public class MyEndpoint extends Endpoint {
@Override
public void onOpen(javax.websocket.Session session, EndpointConfig config) {
session.addMessageHandler((MessageHandler.Whole<String>) message -> {
// handle message
});
}
}

It is nicer, part of the information is hidden, but my guess is it might not be seen as a drawback for most cases. And 3 lines of code were saved, so thumbs up! Well.. there is the last thing.. it doesn’t work.

Lambda expressions are not anonymous classes. Anonymous class can be replaced by using lambda expression and it still compiles, but the generic parameter is lost, or at least there is no way how to obtain it via standard reflection API. And that is the problem for any JSR 356 implementation – type parameter here is used for selecting appropriate Decoder (another part of the API). Any lambda expression used instead regular generic anonymous class will be treaded as MessageHandler.Whole<Object>.

What can we do with this? Surprisingly, not much. I will try to file a backwards compatibility issue/challenge agains JDK 8, but from the initial reactions on jdk8-dev mailing list, it does not seem like something anybody is willing to fix. There is one nice constructive proposal from Simone Bordet, which would at least make the variant with lambda not compilable. That would be great workaround (which will require WebSocket API 1.0.1 release and potentially changes in other APIs), but I still consider this being an issue which should be fixed in more generic way. And last but not least – I don’t think that new version of JDK should break any existing working API.

I’ll keep this post updated whenever I got additional information. Feel free to comment or suggest a solution!

EDIT:
Credit for discovering this issue should go to user ayyrk, reporter of TYRUS-302

Thursday Mar 06, 2014

Tyrus 1.5

New version of Tyrus was released today, so let’s do a quick summary of what is new compared to previous version and so on.

What’s new?

Maven archetype was (finally) added, so if you want to generate simple application, test it and maybe start playing with the code and modifying it to something more complex, you can. All you need is maven and little space on your hard drive. Then you can execute following command:

1
2
3
4
mvn archetype:generate -DarchetypeArtifactId=tyrus-archetype-echo \
-DarchetypeGroupId=org.glassfish.tyrus.archetypes -DinteractiveMode=false \
-DgroupId=com.example -DartifactId=echo -Dpackage=com.example \
-DarchetypeVersion=1.5

and the project should be created.

What’s fixed?

  • Close reason for dropped connection is now 1006, as it should be.
  • Standalone Server is not leaking daemon threads any more.
  • Java SE 8 runtime issues.
  • “Host” header parsing issue which caused failed handshakes when using IPv6 localhost address ([::1]).
  •  Internal InputStream implementation now correctly returns ‘-1′ to InputStream#read() only when end of input is reached. Thanks Raghu for contributing the fix!

Java SE 8 related issue is worth a short explanation, since the change introduced by this fix goes beyond SE 8. Original problem was about some bug in Java SE 7 which somehow corrected Tyrus behaviour, so it was not noticed by our tests – bridge methods are not returned from Class.getMethods() call there. This was fixed in Java SE 8 and it caused some troubles in Tyrus implementation, because we just did not expect them to be returned.

The other part of this issue was correcting Tyrus in terms of handling inherited methods. JSR 356 describes how these should be handled in little bit cryptic way, but it basically states that annotations are not inherited, which is already defined in Java language specification. Unfortunately, Tyrus prior this version was considering annotated methods (@OnOpen, @OnMessage, …) only from the very same class as the registered one, so inherited methods were always ignored. This is now changed, so you can have something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BaseEndpoint {
@OnOpen
public void onOpen(Session session) throws IOException {
// do something.
}
}
@ServerEndpoint("/echo")
public class EchoEndpoint extends BaseEndpoint {
@OnMessage
public void echo(Session session, String message) throws IOException {
// do something else.
}
}

both methods – onOpen and onMessage will be considered as part of EchoEndpoint class. Please note that annotations are still not inherited, so if you for example declare BaseEndpoint class as abstract with onOpen method annotated with @OnOpen, overriding method won’t be called by Tyrus unless you “re-add” @OnOpen annotation to new method:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class BaseEndpoint {
public abstract void onOpen(Session session) throws IOException;
}
@ServerEndpoint("/echo")
public class EchoEndpoint extends BaseEndpoint {
@OnOpen // this has to be here!
@Override
public void onOpen(Session session) throws IOException {
// do something.
}
}

Feel free to ask here or send us a note to users@tyrus.java.net if you have any questions related to this release or anything related to our WebSocket implementation.

Links

Wednesday Feb 12, 2014

Oracle Toplink team is hiring!

Oracle Toplink team is looking for Java Developers! If you want to became part of the team which is working on Toplink, EclipseLink, JAX-B, JAX-WS and related projects and products, this is the time to update your CV and share it with us. This job position is based in Prague, Czech Republic.

Formal job offer can be found on linked.in.

If you are interested and have any questions, feel free to contact me directly via this blog or via pavel.bucek at oracle.com.

Monday Feb 03, 2014

WebSocket Client Reconnect

Another new feature was recently added to Tyrus (Java API for WebSocket Reference Implementation): Client ReconnectHandler. Some client use cases require almost persistent client-to-server connection and don’t really care about lower layer issues, like unstable internet connection.

Tyrus Client now include possibility of registering ReconnectHandler, which can help with these scenarios. Let’s see ReconnectHandler declaration:

1
2
3
4
5
6
7
8
9
10
public class ReconnectHandler {
public boolean onDisconnect(CloseReason closeReason) {
return false;
}
public boolean onConnectFailure(Exception exception) {
return false;
}
}

Method onDisconnect is executed whenever client endpoint @OnClose annotated method is called, method onConnectFailure is executed whenever client has some troubles connecting to remote endpoint (network issues, server returning HTTP Status 500, …). Both methods can return boolean value, which indicates whether client should try to reconnect or not.

It is perfectly fine to wait in any of these methods for some time – I would recommend it for onConnectFailure, because there usually is some reason for network failure and repeating requests without any delay can prolong them or even make them worse. Also I would recommend include some kind of counter, which would set the number of reconnect attempts.

Example implementation could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ClientManager client = ClientManager.createClient();
ClientManager.ReconnectHandler reconnectHandler = new ClientManager.ReconnectHandler() {
private int counter = 0;
@Override
public boolean onDisconnect(CloseReason closeReason) {
counter++;
if (counter <= 3) {
System.out.println("### Reconnecting... (reconnect count: " + counter + ")");
return true;
} else {
return false;
}
}
@Override
public boolean onConnectFailure(Exception exception) {
counter++;
if (counter <= 3) {
System.out.println("### Reconnecting... (reconnect count: " + counter + ") " + exception.getMessage());
// Thread.sleep(...) or something other "sleep-like" expression can be put here - you might want
// to do it here to avoid potential DDoS when you don't limit number of reconnects.
return true;
} else {
return false;
}
}
};
client.getProperties().put(ClientManager.RECONNECT_HANDLER, reconnectHandler);
client.connectToServer(...)

Friday Jan 24, 2014

Updating Tyrus in Glassfish

This article is inspired by similar one about Jersey and will provide similar information. Thanks to Michal for creating such comprehensive instructions.

Fortunately, Tyrus does not depend on HK2 so the task here is lot easier. To be absolutely honest, I did expect some issues with Grizzly dependency in Tyrus client, but changes are backwards compatible (applies to Tyrus 1.4), so you can update Tyrus to any version of released Glassfish very easily.

Which version of Tyrus am I using?

You can get this from tyrus-core.jar manifest:

1
2
$ unzip -p $GLASSFISH_HOME/glassfish/modules/tyrus-core.jar META-INF/MANIFEST.MF | grep Bundle-Version
Bundle-Version: 1.0.0

This means you are using Tyrus 1.0. I strongly recommend to upgrade. Latest version now is Tyrus 1.4 and the output will look like:

1
2
$ unzip -p $GLASSFISH_HOME/glassfish/modules/tyrus-core.jar META-INF/MANIFEST.MF | grep Bundle-Version
Bundle-Version: 1.4.0

Glassfish distributions

Table below contains overview of current Glassfish 4.x builds:

Glassfish version Download link Tyrus version
4.0 (Java EE 7 RI) [download] 1.0
4.0.1 b01 [download] 1.0
4.0.1 b02 [download] 1.2.1
4.0.1 b03 [download] 1.2.1
4.0.1 latest nightly [download] latest

Updating to Tyrus 1.4

1
2
3
4
5
6
7
8
9
10
11
$ rm $GLASSFISH_HOME/glassfish/modules/tyrus-*jar
$ unzip -j ./websocket-ri-archive-1.4.zip "websocket-ri-archive-1.4/lib/*" -d $GLASSFISH_HOME/glassfish/modules/
Archive:  ./websocket-ri-archive-1.4.zip
inflating: [path]/modules/tyrus-client-1.4.jar
inflating: [path]/modules/tyrus-container-glassfish-cdi-1.4.jar
inflating: [path]/modules/tyrus-container-grizzly-client-1.4.jar
inflating: [path]/modules/tyrus-container-servlet-1.4.jar
inflating: [path]/modules/tyrus-core-1.4.jar
inflating: [path]/modules/tyrus-server-1.4.jar
inflating: [path]/modules/tyrus-spi-1.4.jar

And that’s it. Remember to restart Glassfish instance after replacing Tyrus jar files.

Note

As of now (1/24/2013) latest nightly build of Glassfish contains Tyrus 1.3.3. Next nightly should contain latest Tyrus release – version 1.4.

Links

Tuesday Jan 21, 2014

WebSocket Client on Android – Tyrus

Running some Java EE libraries or frameworks in non-standard VM is not an uncommon task and same is for Tyrus. I have to admit that this task was driven mainly by issue report TYRUS-256 from Reza. There is no official support from Tyrus running on Dalvik and I'm not even sure if it is a good idea, but important fact is that it works and you can do it if you want :-).

Whole issue which blocked runtime from being able to run on Android was in usage of javax.naming.InitialContext class. There is no simple alternative to it, but fortunately there is always a possibility to do little bit of reflection hacking to get rid of the dependency if it's not there. The rest was only about creating the sample application and testing it on my phone, which was not that hard. I have to give kudos to IntelliJ IDEA team for their support, but not for Android Studio - it uses gradle as build tool and it seems like you cannot include java library because android plugin clashes with java plugin (I'm not very familiar with gradle as you might have noticed). Using and build script and build in support was better for my task.

The application I used for testing is available on github in my personal workspace: https://github.com/pavelbucek/tyrus-client-android-test. Feel free to test it and/or provide pull requests. I would be particularly interested in gradle build script.

That's it for today. If you are using Tyrus on Android or if you have any related comments, please share them with us on Tyrus mailing list or as an enhancement request.

Thursday Jan 16, 2014

Tyrus 1.4

I’m pleased to announce that Tyrus 1.4 was released today. It contains all features mentioned recently on my blog: Extensions support (including compression extensions implementation), shared client container and many other improvements and bug fixes. Binaries will be available on maven central repository soon and Tyrus 1.4 will be integrated into Glassfish trunk.

As always, feel free to contribute! You can file an enhancement or a bug or just ask if there is something unclear in the documentation.

Release notes

Bugs

  • [TYRUS-136] – org.glassfish.tyrus.server.Server does not stop correctly when DeploymentException is thrown
  • [TYRUS-263] – SSL via HTTP PROXY – wss://echo.websocket.org Handshake error. “Response code was not 101: 200″
  • [TYRUS-269] – Parallel connection to ServerEndpoint with URI template mix up response to client instances …
  • [TYRUS-270] – The title of file README.html of Draw sample is mis-typed from “Draw Sample” to “Chat Sample”
  • [TYRUS-271] – “?null” is added to every request without query params
  • [TYRUS-272] – TyrusServerConfiguration incompatible with jetty’s jsr 356 implementation.
  • [TYRUS-273] – EJB component provider needs to provide method which will be invoked.
  • [TYRUS-275] – Tyrus client has bug causing limitation of # of open web sockets
  • [TYRUS-276] – Session accepts messages after idle timeout (@OnMessage is triggered)
  • [TYRUS-277] – session.setMaxIdleTimeout(0) does not reset/cancel the timeout
  • [TYRUS-280] – Extension parsed does not allow parameter without value
  • [TYRUS-281] – Client does CDI/EJB lookup
  • [TYRUS-282] – Session.setMaxIdleTimeout(..) does not work as expected for negative values
  • [TYRUS-288] – WebSocket connections are automatically closed after 30 idle seconds in standalone mode

Improvements

  • [TYRUS-56] – content root directory configuration in Server class for static content
  • [TYRUS-160] – Tyrus tests won’t compile with JDK 1.6
  • [TYRUS-183] – UTF8 validation logic should be separated from frame parsing
  • [TYRUS-265] – Improve GrizzlyWriter implementation
  • [TYRUS-266] – Support for WebSocket extensions
  • [TYRUS-268] – In-memory transport for testing / performance analysis

New Features

  • [TYRUS-193] – Support Tyrus with different transports
  • [TYRUS-283] – CompressionExtension (permessage-compression)
  • [TYRUS-286] – Shared client container improvement (stop when there is no open session)
  • [TYRUS-287] – Cannot create non-daemon threads with Tyrus server standalone mode

Tasks

  • [TYRUS-246] – Investigate and fix issues related to ServletTest#testWebSocketBroadcast
  • [TYRUS-264] – Client SPI

Wednesday Jan 08, 2014

WebSocket Extensions in Tyrus

There is always room for another experimental feature :-) This one is maybe little less experimental than broadcast support, but please implement presented APIs with one important fact in your mind – it can change any time.

What is WebSocket Extension?

You can think of WebSocket Extension as a filter, which processes all incoming and outgoing frames. Frame is the smallest unit in WebSocket protocol which can be transferred on the wire – it contains some some metadata (frame type, opcode, payload length, etc.) and of course payload itself.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

Figure taken from RFC 6455

If you are interested about some more details related to WebSocket protocol specification, please see linked RFC document (RFC 6455).

What can be achieved by WebSocket Extension?

Almost everything. You can change every single bit of incoming or outgoing frame, including control frames (close, ping and pong). There are some RFC drafts trying to standardise extensions like per message compression (used to be per frame compression) and multiplexing extension (now expired).

Tyrus already has support for per message compression extension and exposes interfaces which allow users to write custom extensions with completely different functionality.

When should I consider implementing WebSocket Extensions?

This is maybe the most important question. WebSocket Extensions can do almost everything, but you should not use them for use cases achievable by other means. Why? Majority of WebSocket use cases are about communication with browsers and javascript client cannot really influence which exception is going to be used. Browser must support your particular extension (by default or it can be enabled by some custom module).

You can easily use custom extension when using Tyrus java client, so if there is no browser interaction in your application, it should be easier to distribute your extensions to involved parties and you might lift the threshold when deciding whether something will be done by extension or by application logic.

Java API for WebSocket and Extensions

API currently contains following extension representation (javadoc removed):

1
2
3
4
5
6
7
8
9
10
public interface Extension {
String getName();
List getParameters();
interface Parameter {
String getName();
String getValue();
}
}

and the specification (JSR 356) limits extension definition only for handshake purposes. To sum that up, users can only declare Extension with static parameters (no chance to set parameters based on request extension parameters) and that’s it. These extensions don’t have any processing part, so the work must be done somewhere else. As you might already suspect, this is not ideal state. Usability of extensions specified like this is very limited, it is basically just a marker class which has some influence on handshake headers. You can get list of negotiated extensions in the runtime (Session.getNegotiatedExtensions()) but there is no way how you could access frame fields other than payload itself.

Proposed Extension API

I have to repeat warning already presented at the start of this blog post – anything mentioned below might be changed without notice. There are some TODO items which will most likely require some modification of presented API, not to mention that RFC drafts of WebSocket Extensions are not final yet. There might be even bigger modification needed – for example, multiplexing draft specifies different frame representation, use of RSV bits is not standardised etc. So please take following as a usable proof of concept and feel free to use them in agile projects.

Firstly, we need to create frame representation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Frame {
public boolean isFin() { .. }
public boolean isRsv1() { .. }
public boolean isRsv2() { .. }
public boolean isRsv3() { .. }
public boolean isMask() { .. }
public byte getOpcode() { .. }
public long getPayloadLength() { .. }
public int getMaskingKey() { .. }
public byte[] getPayloadData() { .. }
public boolean isControlFrame() { .. }
public static Builder builder() { .. }
public static Builder builder(Frame frame) { .. }
public final static class Builder {
public Builder() { .. }
public Builder(Frame frame) { .. }
public Frame build() { .. }
public Builder fin(boolean fin) { .. }
public Builder rsv1(boolean