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>