@ledsun blog

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

RubyでWebSocketサーバー

RubyでEchoサーバー - @ledsun blog の経験を踏まえまして、改めて RubyでシンプルなWebSocketサーバーをゼロからつくってみたに取りくみます。 TCPサーバーの部分の理解が進みます。 HTTPで使ったTCPコネクションをそのままつかって、送受信データをWebSocketフレームに変えたことがわかります。 よしこれならできそう!

require 'socket'
require 'digest/sha1'

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 = Digest::SHA1.base64digest(ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
  puts "response_key: #{response_key}"

  handshake_response = <<~EOS
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: #{response_key}
    \r\n
  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}")
  puts "Response: #{response.unpack('C*')}"
  socket.write response


  socket.close
end

というわけで

ブラウザでInvalid Frame Headerが出ているスクリーンショット

動きませんでした。 なんで・・・。

コネクション周りはわかりました。 ハンドシェイクの必要性がよくわかりません。 この辺は RFC 6455 - The WebSocket Protocol を読むと良さそうです。

20241103 追記

動かない原因はハンドシェイクのレスポンスでした。

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

最後に\r\nというゴミがついています。 これを消せば動きます。

ゴミがあったためクライアントはハンドシェイクレスポンスの終わりを上手く検知できていなかったようです。