Class: WebSocketClient

Inherits:
Object
  • Object
show all
Defined in:
lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url) ⇒ WebSocketClient

Returns a new instance of WebSocketClient.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 43

def initialize(url)
  @uri = URI.parse(url)
  raise "Only ws:// or wss:// URLs are supported" unless %w[ws wss].include?(@uri.scheme)

  @host = @uri.host
  @port = @uri.port || default_port
  @path = (@uri.path.nil? || @uri.path.empty?) ? '/' : @uri.path
  @path += "?#{@uri.query}" if @uri.query

  @socket = open_socket
  @handshake = WebSocket::Handshake::Client.new(url: url) # masks handled by client frames
  perform_handshake
  @incoming = WebSocket::Frame::Incoming::Client.new(version: @handshake.version)
end

Class Method Details

.add_or_get_client(url) ⇒ Object



8
9
10
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 8

def add_or_get_client(url)
  cache[url] && cache[url].open? ? cache[url] : (cache[url]&.close; cache[url] = new(url))
end

.close_client(url, suppress_close_frame: false) ⇒ Object



24
25
26
27
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 24

def close_client(url, suppress_close_frame: false)
  cache[url]&.close(suppress_close_frame: suppress_close_frame)
  cache.delete(url)
end

.get_state(url) ⇒ Object

Returns :OPEN, :CLOSED, or nil (no client)



30
31
32
33
34
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 30

def get_state(url)
  c = cache[url]
  return nil unless c
  c.open? ? :OPEN : :CLOSED
end

.send_message(url, message) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 12

def send_message(url, message)
  client = add_or_get_client(url)
  client.send_message(message)
  return client.receive_message
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ETIMEDOUT, IOError, SystemCallError
  # Socket likely died due to server idle-timeout; rebuild without sending CLOSE
  close_client(url, suppress_close_frame: true)
  client = add_or_get_client(url)
  client.send_message(message)
  client.receive_message
end

Instance Method Details

#close(suppress_close_frame: false) ⇒ Object



129
130
131
132
133
134
135
136
137
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 129

def close(suppress_close_frame: false)
  begin
    send_close_reply unless suppress_close_frame || @socket.closed?
  rescue Errno::EPIPE, Errno::ECONNRESET, IOError, SystemCallError
    # peer already gone — ignore
  ensure
    @socket.close unless @socket.closed?
  end
end

#open?Boolean

Returns:

  • (Boolean)


122
123
124
125
126
127
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 122

def open?
  return false if @socket.closed?
  true
rescue IOError, SystemCallError
  false
end

#receive_message(timeout: 5) ⇒ Object

Returns Array<Integer> (bytes) or nil on timeout



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 77

def receive_message(timeout: 5)
  deadline = Time.now + timeout
  payload_chunks = []

  loop do
    # try to parse any buffered data first
    if (frame = @incoming.next)
      case frame.type
      when :binary, :text, :continuation
        payload_chunks << frame.data
        return payload_chunks.join.bytes
      when :ping
        send_pong(frame.data)
      when :close
        send_close_reply
        return nil
      end
      next
    end

    # no complete frame yet—read from socket or timeout
    remaining = deadline - Time.now
    return nil if remaining <= 0

    if IO.select([@socket], nil, nil, [remaining, 0.05].max)
      begin
        chunk = @socket.read_nonblock(64 * 1024, exception: false)
        case chunk
        when nil
          # peer closed
          return nil
        when :wait_readable
          next
        else
          @incoming << chunk
        end
      rescue IO::WaitReadable
        next
      rescue EOFError
        return nil
      end
    end
  end
end

#send_message(message) ⇒ Object

message can be an Array<Integer 0..255> or a String of bytes



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/javonet-ruby-sdk/core/web_socket_client/web_socket_client.rb', line 59

def send_message(message)
  data =
    case message
    when String then message.b
    when Array  then message.pack('C*')
    else raise ArgumentError, "Unsupported message type: #{message.class}"
    end

  frame = WebSocket::Frame::Outgoing::Client.new(
    version: @handshake.version,
    data: data,
    type: :binary
  )
  write_all(frame.to_s)
  true
end