-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
web_socket.cr
149 lines (131 loc) · 4.49 KB
/
web_socket.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
class HTTP::WebSocket
getter? closed = false
# :nodoc:
def initialize(io : IO)
initialize(Protocol.new(io))
end
# :nodoc:
def initialize(@ws : Protocol)
@buffer = Bytes.new(4096)
@current_message = IO::Memory.new
end
# Opens a new websocket using the information provided by the URI. This will also handle the handshake
# and will raise an exception if the handshake did not complete successfully. This method will also raise
# an exception if the URI is missing the host and/or the path.
#
# Please note that the scheme will only be used to identify if TLS should be used or not. Therefore, schemes
# apart from `wss` and `https` will be treated as the default which is `ws`.
#
# ```
# HTTP::WebSocket.new(URI.parse("ws://websocket.example.com/chat")) # Creates a new WebSocket to `websocket.example.com`
# HTTP::WebSocket.new(URI.parse("wss://websocket.example.com/chat")) # Creates a new WebSocket with TLS to `websocket.example.com`
# HTTP::WebSocket.new(URI.parse("http://websocket.example.com:8080/chat")) # Creates a new WebSocket to `websocket.example.com` on port `8080`
# HTTP::WebSocket.new(URI.parse("ws://websocket.example.com/chat"), # Creates a new WebSocket to `websocket.example.com` with an Authorization header
# HTTP::Headers{"Authorization" => "Bearer authtoken"})
# ```
def self.new(uri : URI | String, headers = HTTP::Headers.new)
new(Protocol.new(uri, headers: headers))
end
# Opens a new websocket to the target host. This will also handle the handshake
# and will raise an exception if the handshake did not complete successfully.
#
# ```
# HTTP::WebSocket.new("websocket.example.com", "/chat") # Creates a new WebSocket to `websocket.example.com`
# HTTP::WebSocket.new("websocket.example.com", "/chat", tls: true) # Creates a new WebSocket with TLS to `ẁebsocket.example.com`
# ```
def self.new(host : String, path : String, port = nil, tls = false, headers = HTTP::Headers.new)
new(Protocol.new(host, path, port, tls, headers))
end
def on_ping(&@on_ping : String ->)
end
def on_pong(&@on_pong : String ->)
end
def on_message(&@on_message : String ->)
end
def on_binary(&@on_binary : Bytes ->)
end
def on_close(&@on_close : String ->)
end
protected def check_open
raise IO::Error.new "Closed socket" if closed?
end
def send(message)
check_open
@ws.send(message)
end
# It's possible to send a PING frame, which the client must respond to
# with a PONG, or the server can send an unsolicited PONG frame
# which the client should not respond to.
#
# See `#pong`.
def ping(message = nil)
check_open
@ws.ping(message)
end
# Server can send an unsolicited PONG frame which the client should not respond to.
#
# See `#ping`.
def pong(message = nil)
check_open
@ws.pong(message)
end
def stream(binary = true, frame_size = 1024)
check_open
@ws.stream(binary: binary, frame_size: frame_size) do |io|
yield io
end
end
def close(message = nil)
return if closed?
@closed = true
@ws.close(message)
end
def run
loop do
begin
info = @ws.receive(@buffer)
rescue IO::EOFError
@on_close.try &.call("")
break
end
case info.opcode
when Protocol::Opcode::PING
@current_message.write @buffer[0, info.size]
if info.final
message = @current_message.to_s
@on_ping.try &.call(message)
pong(message) unless closed?
@current_message.clear
end
when Protocol::Opcode::PONG
@current_message.write @buffer[0, info.size]
if info.final
@on_pong.try &.call(@current_message.to_s)
@current_message.clear
end
when Protocol::Opcode::TEXT
@current_message.write @buffer[0, info.size]
if info.final
@on_message.try &.call(@current_message.to_s)
@current_message.clear
end
when Protocol::Opcode::BINARY
@current_message.write @buffer[0, info.size]
if info.final
@on_binary.try &.call(@current_message.to_slice)
@current_message.clear
end
when Protocol::Opcode::CLOSE
@current_message.write @buffer[0, info.size]
if info.final
message = @current_message.to_s
@on_close.try &.call(message)
close(message) unless closed?
@current_message.clear
break
end
end
end
end
end
require "./web_socket/*"