-->

WebSocket handshake with Ruby and EM::WebSocket::S

2019-02-19 06:08发布

问题:

I am trying to create a simple WebSocket connection in JavaScript against my Rails app. I get the following:

WebSocket connection to 'ws://localhost:4000/' failed: Error during WebSocket handshake: 'Sec-WebSocket-Accept' header is missing

What am I doing wrong? Here is my code:

JavaScript:

var socket = new WebSocket('ws://localhost:4000');

socket.onopen = function() {
  var handshake =
    "GET / HTTP/1.1\n" +
    "Host: localhost\n" +
    "Upgrade: websocket\n" +
    "Connection: Upgrade\n" +
    "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\n" +
    "Sec-WebSocket-Protocol: quote\n" +
    "Sec-WebSocket-Version: 13\n" +
    "Origin: http://localhost\n";

  socket.send(handshake);
};

socket.onmessage = function(data) {
  console.log(data);
};

Ruby:

require 'rubygems'
require 'em-websocket-server'

module QuoteService
  class WebSocket < EventMachine::WebSocket::Server
    def on_connect
      handshake_response =  "HTTP/1.1 101 Switching Protocols\n"
      handshake_response << "Upgrade: websocket\n"
      handshake_response << "Connection: Upgrade\n"
      handshake_response << "Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\n"
      handshake_response << "Sec-WebSocket-Protocol: quote\n"

      send_message(handshake_response)
    end

    def on_receive(data)
      puts 'RECEIVED: ' + data
    end
  end
end

EventMachine.run do
  print 'Starting WebSocket server...'
  EventMachine.start_server '0.0.0.0', 4000, QuoteService::WebSocket
  puts 'running'
end

The handshake headers are per Wikipedia.

回答1:

1) I think that once the connection is open the request and response have already occurred, so sending headers at that point is too late. In addition, headers have to end with a blank line, which you omitted.

2) According to the demos, you don't even have to set headers in the client or the server--the ruby module automatically takes care of the headers on the server side, and html5 automatically takes care of the headers on the client side. I think this should work:

require "em-websocket-server"

class EchoServer < EM::WebSocket::Server

  def on_connect
    EM::WebSocket::Log.debug "Connected"
    puts "I felt a connection."
  end

  def on_receive msg
    puts "RECEIVED: #{msg}"
    send_message msg
  end

end

EM.run do
  myhost = "0.0.0.0"
  myport = 8000
  puts "Starting WebSocket server.  Listening on port #{myport}..."
  EM.start_server myhost, myport, EchoServer
end  

html file:

<!DOCTYPE html> <html> <head><title>Test</title>

<script type="text/javascript">

  var myWebSocket = new WebSocket("ws://localhost:8000");

  myWebSocket.onopen = function(evt)    { 
    console.log("Connection open. Sending message..."); 
    myWebSocket.send("Hello WebSockets!");       };

  myWebSocket.onmessage = function(evt)    { 
    console.log(evt.data);
    myWebSocket.close();   };

  myWebSocket.onclose = function(evt)    { 
    console.log("Connection closed.");    };

  myWebSocket.onerror = function(err)   {
    alert(err.name + " => " + err.message);   } </script>

</head> <body>   <div>Hello</div> </body> </html>

And it does work in Safari 5.1.9 (which is an older browser): I see the expected output on both the server and the client. However, the code does not work in Firefox 21: I get the error message...

Firefox can't establish a connection to the server at ws://localhost:8000/.
    var myWebSocket = new WebSocket("ws://localhost:8000");

I notice that in both Firebug and Safari Developer Tools, the server does not send a Sec-WebSocket-Accept header:

Response Headers

Connection          Upgrade
Upgrade         WebSocket
WebSocket-Location  ws://localhost:8000/
WebSocket-Origin    null


Request Headers

Accept                  text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding         gzip, deflate
Accept-Language         en-US,en;q=0.5
Cache-Control           no-cache
Connection          keep-alive, Upgrade
DNT                 1
Host                    localhost:8000
Origin                  null
Pragma                  no-cache
Sec-WebSocket-Key   r9xT+ywe533EHF09wxelkg==
Sec-WebSocket-Version   13
Upgrade                 websocket
User-Agent          Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:21.0) Gecko/20100101 Firefox/21.0

Nothing I tried would make the code work in Firefox 21.0. To check whether Firefox 21.0 even supports websockets, I went to:

http://www.websocket.org/echo.html  

and it said my browser does support websockets.

3) Is there any reason you have to use the em-websocket-server module? The last modification for that module on github was three years ago. And whenever you see require rubygems in ruby code, that should alert you that the code is old. I tried the newer em-websocket module, and I was able to successfully transfer data back and forth using websockets on both Firefox 21.0 and Safari 5.1.9:

require 'em-websocket'

myhost = "0.0.0.0"
myport = 8000

EM.run {
  puts "Listening on port #{myport}..."

  EM::WebSocket.run(:host => myhost, :port => myport, :debug => false) do |ws|

    ws.onopen do |handshake|
      path = handshake.path
      query_str = handshake.query
      origin = handshake.origin

      puts "WebSocket opened:"
      puts "\t path  \t\t -> #{path}" 
      puts "\t query_str \t -> #{query_str}"
      puts "\t origin \t -> #{origin}"
    end 

    ws.onmessage { |msg|
      ws.send "Pong: #{msg}"
    }
    ws.onclose {
      puts "WebSocket closed"
    }
    ws.onerror { |e|
      puts "Error: #{e.message}"
    }
  end
}

Same client side code. Now the response headers include Sec-WebSocket-Accept:

Response Headers

Connection          Upgrade
Sec-WebSocket-Accept    LyIm6d+kAAqkcTR744tVK9HMepY=
Upgrade                 websocket


Request Headers

Accept  text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Cache-Control   no-cache
Connection  keep-alive, Upgrade
DNT 1
Host    localhost:8000
Origin  null
Pragma  no-cache
Sec-WebSocket-Key   pbK8lFHQAF+arl9tFvHn/Q==
Sec-WebSocket-Version   13
Upgrade websocket
User-Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:21.0) Gecko/20100101 Firefox/21.0

In your code, I don't think you are setting any headers. Instead, you are just sending messages back and forth that happen to contain characters that look like headers. Apparently, your browser requires the Sec-WebSocket-Accept header in the response before it will allow the connection, and when the em-websocket-server module fails to set that header in the response, your browser refuses the connection.

The relevant source code for em-websockets-server looks like this:

module EM
  module WebSocket
    module Protocol
      module Version76

        # generate protocol 76 compatible response headers
        def response
          response = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
          response << "Upgrade: WebSocket\r\n"
          response << "Connection: Upgrade\r\n"
          response << "Sec-WebSocket-Origin: #{origin}\r\n"
          response << "Sec-WebSocket-Location: #{scheme}://#{host}#{path}\r\n"

          if protocol
            response << "Sec-WebSocket-Protocol: #{protocol}\r\n"
          end

          response << "\r\n"
          response << Digest::MD5.digest(keyset)

          response
        end

As you can see, it doesn't set the Sec-WebSocket-Accept header. That code is in a module called Version76, and searching google for websockets version 76 yields an obsolete protocol(which contains an example of a request and response):

http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76

Here is the current websockets protocol(which also contains an example of a request and response):

http://tools.ietf.org/html/rfc6455

Conclusion: em-websockets-server is obsolete.