@ledsun blog

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

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

RubyでつくるWebSocketサーバーにprotocol-websocket gemを取り入れる その3 - @ledsun blog ではWEBrickを使ってHTTPレスポンス文字列を作成しました。 今度は protocol-websocket を使ってHTTPレスポンス文字列を作成します。

require 'socket'
require 'webrick'
require 'protocol/websocket/headers'
require 'protocol/http/headers'
require 'protocol/http/response'

def read_headers_from(socket)
  request = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
  request.parse(socket)
  request.header
end

def calculate_accept_nonce_from(headers)
  key = headers[Protocol::WebSocket::Headers::SEC_WEBSOCKET_KEY].first
  Protocol::WebSocket::Headers::Nounce.accept_digest(key)
end

server = TCPServer.new 'localhost',
                       2345

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

  # HTTPリクエストheaderを読み込む
  headers = read_headers_from socket

  # WebSocketリクエストかどうかを判定する
  unless headers["upgrade"] = Protocol::WebSocket::Headers::PROTOCOL
    puts "Not a websocket request"
    socket.close
    next
  end

  response_key = calculate_accept_nonce_from headers
  puts "response_key: #{response_key}"

  response_headers = Protocol::HTTP::Headers.new
  response_headers.add Protocol::WebSocket::Headers::SEC_WEBSOCKET_ACCEPT, response_key
  response = Protocol::HTTP::Response.new("HTTP/1.1",
                                          101,
                                          response_headers,
                                          nil,
                                          Protocol::WebSocket::Headers::PROTOCOL)
  response_string = <<~HTTP
    #{response.version} #{response.status} Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    #{response.headers.to_h.map { |k, v| "#{k}: #{v.join}" }.join("\n")}
  
  HTTP

  puts response_string
  # ソケットそのものはHTTP通信で使われているものと同じ
  socket.write response_string
  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

レスポンス文字列作成部分だけを抜き出します。 protocol-websocketでは

response_headers = Protocol::HTTP::Headers.new
response_headers.add Protocol::WebSocket::Headers::SEC_WEBSOCKET_ACCEPT, response_key
response = Protocol::HTTP::Response.new("HTTP/1.1",
                                        101,
                                        response_headers,
                                        nil,
                                        Protocol::WebSocket::Headers::PROTOCOL)

response_string = <<~HTTP
  #{response.version} #{response.status} Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  #{response.headers.to_h.map { |k, v| "#{k}: #{v.join}" }.join("\n")}

HTTP

socket.write response_string

Protocol::HTTP::Responseにはレスポンス文字列を返すメソッドがなかったので、なかなか面倒になってしまいました。

WebSocketでは

response = WEBrick::HTTPResponse.new(WEBrick::Config::HTTP)
response.status = 101
response.upgrade! Protocol::WebSocket::Headers::PROTOCOL
response['Sec-WebSocket-Accept'] = response_key
response.send_response socket

でした。 どうやらHTTPリクエストやHTTPレスポンスを素朴に扱いたい場合は、WEBrickを使う方が便利そうです。

次回はいよいよWebSocketのFrameを作る部分でs。う