@ledsun blog

無味の味は佳境に入らざればすなわち知れず

RubyでつくるWebSocketサーバーにprotocol-websocket gemを取り入れる その1

Async::WebSocket gemの素振り その8 - @ledsun blogにてRubyでつくるWebSocketサーバーにprotocol-websocket gemを取り入れると良さそうなことがわかりました。 やってみましょう。

require 'socket'
require 'protocol/websocket/headers'

server = TCPServer.new 'localhost',
                       2345

loop do
  # ここら辺はecho_server.rbと同じ。純粋なTCP通信
  socket = server.accept

  # HTTPリクエストを読み込む
  http_request = ""
  while (line = socket.gets) && (line != "\r\n")
    http_request += line
  end
  puts http_request

  # WebSocketリクエストかどうかを判定する
  unless match = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
    puts "Not a websocket request"
    socket.close
    next
  end

  ws_key = match[1]
  puts "ws_key: #{ws_key}"

  response_key = ::Protocol::WebSocket::Headers::Nounce.accept_digest(ws_key)
  puts "response_key: #{response_key}"

  handshake_response = <<~EOS
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: #{response_key}

  EOS

  # ソケットそのものはHTTP通信で使われているものと同じ
  socket.write handshake_response
  puts 'Handshake response sent'

  # ここからはWebSocket通信
  first_byte = socket.getbyte
  fin = first_byte & 0b10000000
  opcode = first_byte & 0b00001111

  raise 'fin bit is not set' unless fin
  raise 'opcode is not a text' unless opcode == 0x1

  second_byte = socket.getbyte
  is_masked = second_byte & 0b10000000
  payload_size = second_byte & 0b01111111

  raise 'mask bit is not set' unless is_masked
  raise 'payload size > 125 is not supported' unless payload_size <= 125

  puts "Payload size: #{payload_size}"

  mask = 4.times.map { socket.getbyte }
  puts "Mask: #{mask}"

  data = payload_size.times.map.with_index { socket.getbyte ^ mask[_2 % 4] }
  puts "Data: #{data.pack('C*')}"

  # クライアントにデータを返す
  response_message = "Loud and clear!"
  response = [0b10000001,
              response_message.size,
              response_message
            ].pack("CCA#{response_message.size}")
  socket.write response

  socket.close
end

最も置き換えが簡単そうなaccept_nonceの計算を::Protocol::WebSocket::Headers::Nounce.accept_digestに置き換えました。 なお、このサーバーでは、前回まで使っていたclint.rbはうごきません。 代わりにブラウザからWebSocketでメッセージを送るindex.htmlを使って動作確認します。

<html>
<head>
    <title>WebSocket Client</title>
    <script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.6.2-2024-11-03-a/dist/browser.script.iife.js"></script>
</head>
<body>
  <script type="text/ruby">
    require "js"

    ws = JS.global[:WebSocket].new("ws://localhost:2345")
    ws[:onopen] = -> (event) {
      ws.send("Hello, Server")
    }
    ws[:onmessage] = ->(event) {
      p event[:data]
    }
  </script>
</body>
</html>