Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for WebSockets #167

Open
pierrickrouxel opened this issue Jan 28, 2014 · 14 comments
Open

Support for WebSockets #167

pierrickrouxel opened this issue Jan 28, 2014 · 14 comments
Labels
Milestone

Comments

@pierrickrouxel
Copy link

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.

@kares kares added the feature label Feb 25, 2014
@kares
Copy link
Member

kares commented Feb 25, 2014

thanks ... and what would be the error you've seeing - did you get any advise from the faye guys ?

@pierrickrouxel
Copy link
Author

Faye requires rack.hjiack implementation. It's certainly the problem.

@kares
Copy link
Member

kares commented Feb 25, 2014

@pierrickrouxel
Copy link
Author

Is it possible to implement it?

@kares
Copy link
Member

kares commented Feb 25, 2014

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.

@cshupp1
Copy link

cshupp1 commented Feb 5, 2018

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

@moskvin
Copy link

moskvin commented Aug 20, 2019

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?

@cshupp1
Copy link

cshupp1 commented Aug 20, 2019

@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

@moskvin
Copy link

moskvin commented Aug 20, 2019

@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

@cshupp1
Copy link

cshupp1 commented Aug 20, 2019

@moskvin I don't see you invoking the trinidad binstub so what server are you using?

@moskvin
Copy link

moskvin commented Aug 21, 2019

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

@jefflasslett
Copy link

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.

@cshupp1
Copy link

cshupp1 commented Mar 5, 2020

@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

@jefflasslett
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants