Support for WebSockets
Opened this issue · 14 comments
Hi,
Is it possible to add support for WebSockets ?
I try to use faye with jruby-rack but it doesn't work. It's now compatible with JRuby and works well with Puma server.
Thanks.
This issue seems to be lacking activity, you can change that by posting a bounty.
thanks ... and what would be the error you've seeing - did you get any advise from the faye guys ?
Faye requires rack.hjiack implementation. It's certainly the problem.
I see, for future reference http://blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/
Is it possible to implement it?
Well, have you tried :) ? ... I think it is (but I did not look closely) although maybe using EE 8 APIs, definitely should be possible to hack into a working state with Tomcat's (Trinidad) API.
I have a small example that illustrates using WebSockets with Tomcat and JRuby. I thought I would document my journey here.
My first attempt was to spin up a rails 5.1.4 application. Naturally I started using action cable. I noticed it didn't work in Trinidad or Tomcat. I switched to Puma. Everything worked. Then I learned about rack hijacking and realized it just plain wasn't supported for JRuby people :-(.
So how hard would it be to implement rack hijacking in JRuby? I entertained it briefly, but that did send me away with my tail tucked between my legs.
To start consider the following library written in java:
package gov.va.rails;
import java.io.IOException;
import java.util.Observable;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
@ServerEndpoint(value = WebSocketSupport.END_POINT)
public class WebSocketSupport {
static{
System.out.println("Rails has WebSocketSupport at: " + WebSocketSupport.END_POINT);
}
public static final String END_POINT = "/websocket/rails";
private static final Log log = LogFactory.getLog(WebSocketSupport.class);
private static final Set<WebSocketSupport> connections = new CopyOnWriteArraySet<>();
private static final IncomingMessageObserver messageNotifier = new IncomingMessageObserver();
private Session session;
public WebSocketSupport() {}
@OnOpen
public void start(Session session) {
this.session = session;
connections.add(this);
log.debug("WebSocketSupport session started!");
}
@OnClose
public void end() {
connections.remove(this);
log.debug("WebSocketSupport session ended! ");
}
@OnMessage
public void incoming(String message) {
messageNotifier.notifyObservers(new MessageHolder(this, message));
}
@OnError
public void onError(Throwable t) throws Throwable {
log.error("WebSocketSupport Error: " + t.toString(), t);
}
public Session getSession() {
return session;
}
public static boolean chat(WebSocketSupport client, String msg) {
boolean success = true;
try {
synchronized (client) {
client.getSession().getBasicRemote().sendText(msg);
}
} catch (IOException e) {
log.error("WebSocketSupport Error: Failed to send message to client", e);
connections.remove(client);
success = false;
try {
client.session.close();
} catch (IOException e1) {
// Ignore
}
}
return success;
}
public static void remove(WebSocketSupport ws) {
connections.remove(ws);
}
public static void broadcast(String msg) {
for (WebSocketSupport client : connections) {
try {
synchronized (client) {
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
log.error("WebSocketSupport Error: Failed to send message to client", e);
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
// Ignore
}
}
}
}
public static IncomingMessageObserver getMessageNotifier() {
return messageNotifier;
}
public static class IncomingMessageObserver extends Observable {
@Override
public void notifyObservers(Object o) {
setChanged();
super.notifyObservers(o);
}
}
public static class MessageHolder {
private WebSocketSupport session;
private String message;
public MessageHolder(WebSocketSupport s, String msg) {
this.session = s;
this.message = msg;
}
public WebSocketSupport getWebSocketSupport() {
return session;
}
public String getMessage() {
return message;
}
public boolean chat(String msg) {
return WebSocketSupport.chat(getWebSocketSupport(), msg);
}
}
}
This java library got its start from one of the example applications that ships with tomcat. The file is called ChatAnnotation.java and a quick search in a base Tomcat install (in my case Tomcat 8.5) should reveal it.
The only dependencies needed to compile it:
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>juli</artifactId>
<version>6.0.53</version>
</dependency>
Here is the ruby code that drives it (required in an initializer):
java_import 'gov.va.rails.WebSocketSupport' do |p, c|
'JWebSocketSupport'
end
class IncomingMessageObserver
include java.util.Observer
include Singleton
def message_received(&block)
if block_given?
@blocks ||= []
@blocks << block
end
end
def update(jobservable, messageHolder)
msg = messageHolder.get_message
websocket = messageHolder.getWebSocketSupport
@blocks.each do |block|
begin
block.call(msg, messageHolder, websocket) #wrap up in begin/rescue to prevent breaking the observer/observable notification chain
rescue => ex
$log.error(LEX("Something went wrong processing a websocket message!", ex))
end
end
end
private
def initialize
JWebSocketSupport.getMessageNotifier.addObserver(self)
end
end
MESSAGE_OBSERVER = IncomingMessageObserver.instance
#to process a message
MESSAGE_OBSERVER.message_received do |msg, chatter, websocket|
#msg is a string, chatter is gov.va.rails.WebSocketSupport$MessageHolder, websocket is gov.va.rails.WebSocketSupport
$log.always {"Message received from the client YaY!!! #{msg}"}
end
MESSAGE_OBSERVER.message_received do |msg, chatter|
received = chatter.chat("Got your message #{msg} at #{Time.now}")
end
at_exit do
JWebSocketSupport.getMessageNotifier.deleteObserver(MESSAGE_OBSERVER) #no memory leaks on undeploy!
end
And a broadcast in ruby might look like this:
JWebSocketSupport.broadcast("Hi guys, the time is #{Time.now}")
So how do you wire the code into Tomcat? Suppose the Java library is in railswebsocket.jar...
I really wanted to be able to do this in a Rails initializer:
java.lang.ClassLoader.getSystemClassLoader.addURL(java.io.File.new("#{Rails.root}/lib/websocket/railswebsocket.jar").toURI.toURL)
It doesn't work and I don't understand why :-(
Instead, I just took the jar file and dropped it into Tomcat's lib directory next to all its other jars, and I will see the static initializer get chatty.
What about Trinidad? In order to get the java import:
java_import 'gov.va.rails.WebSocketSupport' do |p, c|
'JWebSocketSupport'
end
to work I had to do this before the import:
if ENV['LOAD_WEBSOCKET_JARS']
urls = []
urls << java.io.File.new("#{Rails.root}/lib/websocket/railswebsocket.jar").toURI.toURL
urls << java.io.File.new("#{Rails.root}/lib/jars/javax.websocket-api.jar").toURI.toURL
urls << java.io.File.new("#{Rails.root}/lib/jars/juli.jar").toURI.toURL
urls.each do |url|
java.lang.ClassLoader.getSystemClassLoader.addURL(url) #put it high up on the classloader chain so Tomcat finds it and instantiates with the first 'ws://...' request.
end
end
With the environment rigged so that the above if statement is executed, this lead me to believe Trinidad doesn't have WebSocket support. I chose to use the system classloader as I assumed JRuby's classloader would be too high up for the Tomcat container to ever see my Java Websocket library (not that it worked).
@kares -- Is making this work in Trinidad feasible?
So, in short, I have to spin a war and do final testing in Tomcat where I can see my WebSockets work against the server. I hope it helps.
Cris
Hi @cshupp1,
Did you test and run on production it on you tomcat? I have the similar issue for WebSocket (ActionCable v5.2.3) on jRuby (ruby 2.5.3p0 (jruby 9.2.8.0)). I've built war file by warbler from rails project, but either Tomcat nor Jetty gave me successful, the app shown me the error:
ERROR -- : WebSocket error occurred: undefined method `write_nonblock' for nil:NilClass|
And I can confirm that the project works fine when I try to run it by rails s
.
Any helpful links or ways to support actioncable on jruby?
@moskvin What I demonstrated was integrating the Java websocket APIs into JRuby. In other words, you are not using ActionCable. I can certainly help more with that if you want.
It seems you want ActionCable though? Yes I can help with that too. All you need to do is not use a J2EE server (No Tomcat or Trinidad for example). None of them support rack hijacking. Switch to something like Puma. When I tried that Action cable worked out of the box exactly as the documentation says it will. When you are using Puma you are no longer using warbler and producing war files, so keep that in mind.
Cris
@cshupp1,
Thank you.
I just build jar by warbler gem and used following format for starting rails server on remote pc:
java -jar rails-app.jar -S rails s
@moskvin I don't see you invoking the trinidad binstub so what server are you using?
I used puma
=> Booting Puma
=> Rails 5.2.3 application starting in production
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (jruby 9.2.8.0 - ruby 2.5.3), codename: Llamas in Pajamas
* Min threads: 0, max threads: 16
* Environment: production
For reasons, I compile and deploy my Ruby on Rails web apps as war files, created by warbler and using embedded jetty.
I would now like to make use of ActionCable (websockets for rails).
After looking into it a little bit, I've learned that ActionCable requires the web server to support "rack hijacking". It seems that jetty and jruby-rack lack this support. Please stop me if this is incorrect. :-)
I'd like to add support for "rack hijack" and ActionCable, to the Jetty/jruby-rack stack - assuming that that is what I need to do to use ActionCable in my war-file-deployed Rails app.
If we boil it all down, I'm after the fastest way to add my new features (that are best implemented with websockets) to my Rails app. If that requires that I add support for rack-hijacking to jruby-rack/jetty, then I'd love to have a go at it. That said, if there's a better way forward re: support for ActionCable in warbler-generated war files, then I would appreciate that advice.
Finally, if adding rack-hijacking support to jruby-rack is the way forward, and people have thought about how it should be done, I'd appreciate reading those thoughts and advice.
@jefflasslett
It isn't that JRuby lacks rack hijacking, J2EE servers do. Adding it in will be quite the task. My memory is that it isn't implemented in rack but the application server itself. You would need to implement it in Jetty in Java.
In fact, Tomcat was the same way. When I used the websocket support in the Java libraries the websocket request never even made it to rack (unless I mis-configured things, which I did do).
The Java library, after getting the request, notifies the ruby code.
The approach I took will likely be easier. I did eventually get all my code working cleanly, but it isn't well documented. PM me if you want me to send you a link to the repo.
//Cris
@cshupp1 PM on what platform? The way you put it, I feel like I should know, but I don't :-)
I tried IRC. github doesn't seem to support direct messaging.