Sinatra using a websocket client to respond to a http request
Asked Answered
E

1

9

I am writing a Sinatra web server that I would like to be RESTful, but the thing is that it has to interact with another server that communicates exclusively via web sockets. So, this needs to happen:

  1. A request comes into my Sinatra server from a client
  2. My server opens a web socket to the foreign server
  3. My server asynchronously waits for messages and things from the foreign server until the socket is closed (this should only take two hundred or so milliseconds)
  4. My server sends back a response to client

I'm sure this is not too complicated to accomplish, but I'm a bit stuck on it. Basically, if the entire web socket logic can be wrapped in a single function, then that function could be made to be blocking, and that'd be that. But I don't know how to wrap the web socket logic and block it. What do you think? A simplified version of what I've got is below.

require 'sinatra'
require 'websocket-client-simple'

get '/' do
     ws = WebSocket::Client::Simple.connect(' ws://URL... ')

     ws.on :message do
          puts 'bar'
     end

     ws.on :close do
          # At this point we need to send an HTTP response back to the client. But how?
     end

     ws.on :open do
          ws.send 'foo'
     end

end

EDIT

After further thought, I realized that a way that this might be done using a thread halt and a thread wake up. This feels rather elaborate, and I'm not sure how to do this correctly with Ruby, but this is the idea:

require 'sinatra'
require 'websocket-client-simple'

get '/' do
    socketResponse('wss:// ... URL ...')

    'Got a response from the web socket server!'
end

def socketResponse(url)
    thread = Thread.new do

        ws = WebSocket::Client::Simple.connect(url)

        ws.on :message do
            puts 'bar'
            # Maybe store each response in a thread-safe array to retrieve later or something
        end

        ws.on :close do
            thread.run
        end

        ws.on :open do
            ws.send 'foo'
        end

        Thread.stop
    end
end

EDIT 2

I have made further progress. I am now using the Async Sinatra gem, which requires the Thin web server. This is how it is set up:

require 'sinatra'
require 'sinatra/async'
require 'websocket-client-simple'

set :server, 'thin'

register Sinatra::Async

aget '/' do
    puts 'Request received'

    socketResponse('wss:// ... URL ...')
end

def socketResponse(url)
    ws = WebSocket::Client::Simple.connect(url)

    puts 'Connected to web socket'

    ws.on :message do |message|
        puts 'Got message:' + message.to_s
    end

    ws.on :close do
        puts 'WS closed'
        body 'Closed ...'
    end

    ws.on :open do
        puts 'WS open'

        message = 'A nice message to process'
        ws.send message
        puts 'Sent: ' + message
    end
end

The thing is, it still isn't working. Its console output is as expected:

Request received
Connected to web socket
WS open
Sent: A nice message to process
Got message: blah blah blah
WS closed

But it is not sending any data back to the client. The body 'Closed ...' method does not seem to have any effect.

Eugenioeugenius answered 31/8, 2017 at 18:18 Comment(6)
Sidenote: the design suggested in your question is counter-productive and performance heavy. It makes more sense to keep the websocket connection open throughout as long as your application running. That's the whole point of websockets - sustaining a persistent connection.Lovejoy
This is not my real code, but only the simplest way of writing it that I could think of to demonstrate the problem. But thank you, that is a good tip.Eugenioeugenius
Just to clarify, you don't want the page to load until after the web socket code has run? You also state it should be async, so I'm confused. You can have an async function inside a route, because the route will return without any information from the async method.Brittbritta
The page's content depends on what is sent by the web socket server, so the page can't load until the WS code has run. It needs to be async only because WSs are async. It has to wait asynchronously for the WS messages, but the function above, socketResponse(), should block until the WS closes. That is the problem.Eugenioeugenius
Why not offload the web socket connection to the client, letting the client directly request and process the data using a web socket connection?Lovejoy
@Lovejoy I could do that, and in fact that is exactly what I'm currently doing. The reason I don't want to do that is basically modularity and decoupling. It would be nicer if I could have the client exclusively communicate with my server. The client code is rather messy at the moment, and I'm trying to clean it up.Eugenioeugenius
E
1

The problem was that async-sinatra was using its own threads, and so was websocket-client-simple. The solution is to use bindings and the eval function, though this is not very efficient at all. I am hoping that optimizations or better solutions are available.

require 'sinatra'
require 'sinatra/async'
require 'websocket-client-simple'

set :server, 'thin'

register Sinatra::Async

aget '/' do
    puts 'Request received'

    socketResponse('wss:// ... URL ...', binding)
end

def socketResponse(url, b)
    ws = WebSocket::Client::Simple.connect(url)

    puts 'Connected to web socket'

    ws.on :message do |message|
        puts 'Got message:' + message.to_s
    end

    ws.on :close do
        puts 'WS closed'
        EM.schedule { b.eval " body 'Closed' " }
    end

    ws.on :open do
        puts 'WS open'

        message = 'A nice message to process'
        ws.send message
        puts 'Sent: ' + message
    end
end
Eugenioeugenius answered 25/10, 2017 at 19:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.