Search code examples
rubysocketsasynchronoustcpsocket

Is there a way to be notified of new data becoming available in an SSLSocket (Ruby)?


I'm working on building a custom client on top of Ruby's SSLSocket. In order to receive data, I've been using the read and read_nonblock methods provided by the OpenSSL::Buffering module.

I'm now trying to take what I have so far, and make it so that I can define a callback (via a user-defined block) that will be run when messages are received. It looks like I basically need to implement something alone these lines:

thread = Thread.new do
  while !socket.closed?
    while (data = socket.read_nonblock(1024) rescue nil)
      @buffer << data
    end

    sleep 0.1

    # ... parse full messages from @buffer & deliver to callbacks ...
  end
end

thread.run

The problem I have with this approach is that it's not truly event-driven, and there can be up to a 100ms delay since the data was actually available. Sure, I could change the sleep time, but it just feels a bit hack-ish.

Is there a better approach I could be using for this? If not, what concerns should I have should I decide to implement a shorter/faster loop (e.g.: sleep 0.01)?


Solution

  • I suggest two way to achieve it.

    1) Using Kernel.select or IO.select method (both are the same):

    require 'socket'
    require 'openssl'
    
    s     = TCPSocket.new(host, prot) 
    ssl   = OpenSSL::SSL::SSLSocket.new(s)
    ssl.connect
    
    t = Thread.new do
      loop do
        sr, sw = IO.select [ssl]
        puts sr.first.readline
        puts '...'
      end
    end
    
    puts 'start reading'
    t.join # join the main thread
    

    The IO.select waits until some data arrived, without the busy loop. The benefit of this solution, it's only uses the standard Ruby library.

    2) Using EventMachine library:

    require 'eventmachine'
    
    module Client
    
      def post_init
        start_tls
      end
    
      def receive_data data
        # include callback code
        puts data
        puts '...'
      end
    
    end
    
    EM.run do 
      # start event loop
      EM.connect 'localhost', 9000, Client
    end
    

    EventMachine, according to the documentation, is an event-driven I/O using the Reactor pattern.

    The EventMachine has all you need out of the box. The reactor is implemented in C++ and the thread model is outside the Ruby GIL (Global Interpreter Lock) which makes the library extremely fast.

    I have been using it on production for a while and works great!

    The both approach will work as you asking for, so I would recommend to benchmark them and see which one fits best to your needs.