Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sinatra using a websocket client to respond to a http request

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.

like image 728
tschwab Avatar asked Aug 31 '17 18:08

tschwab


People also ask

Can you use WebSockets with HTTP?

Do websocket implementations use http protocol internally? Yes, initially, then they switch to the webSocket protocol. All webSocket connections start with an HTTP request with a header that requests an upgrade to the webSocket protocol.

Should I use WebSockets instead of HTTP?

WebSockets allow for a higher amount of efficiency compared to REST because they do not require the HTTP request/response overhead for each message sent and received. When a client wants ongoing updates about the state of the resource, WebSockets are generally a good fit.

Can WebSocket and HTTP on same port?

Yes it can run on the same port, in fact it must run on the same port; the raison d'etre of websocket handshake is so that the websocket can run on the same connection without confusing intermediaries that doesn't understand websocket.

How does a WebSocket client work?

WebSockets are used to provide a connection between a client and a server so that both parties can send data at any time. The client uses a process known as the WebSocket handshake, which helps establish a connection between the server and the client.


1 Answers

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
like image 80
tschwab Avatar answered Oct 12 '22 01:10

tschwab