webrocket 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/README.md +104 -0
- data/Rakefile +7 -0
- data/lib/webrocket.rb +56 -0
- data/lib/webrocket/version.rb +3 -0
- data/lib/webrocket/webrick-extender.rb +27 -0
- data/lib/webrocket/websocket.rb +164 -0
- data/sample/chat.html +47 -0
- data/sample/chat.rb +60 -0
- data/sample/echo.html +29 -0
- data/sample/echo.rb +35 -0
- data/test/test_webrocket.rb +115 -0
- data/webrocket.gemspec +24 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6f34536b978cbb71a56ae12ea296681c3f09e1d1
|
4
|
+
data.tar.gz: 210d6e96989a3657513fabc93278250800bfe491
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0e2587f050883ca1fee5a31c50b9a2c2e863aa2f4268e0bada83d66ddb159ef44be21aa1d8e8ff5e9cd358309f1b7f2654ce3283ad413647b82b6980625d58fb
|
7
|
+
data.tar.gz: 9aafb6ae9a6ae54dcd78a04e36222a06f7022c51bad724669d01b8d7c05ba5ee9be354455fffef982287d5536ea9298ff8127308b7bdbecc29ce13438c280c4e
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
[](https://travis-ci.org/unak/WEBrocket)
|
2
|
+
[](https://rubygems.org/gems/webrocket)
|
3
|
+
|
4
|
+
WEBrocket
|
5
|
+
=========
|
6
|
+
|
7
|
+
WebSocket extension for WEBrick.
|
8
|
+
This library is under development.
|
9
|
+
|
10
|
+
WebSocket specification is based on [RFC6455](http://www.rfc-editor.org/rfc/rfc6455.txt).
|
11
|
+
|
12
|
+
|
13
|
+
Requirements
|
14
|
+
------------
|
15
|
+
|
16
|
+
+ Ruby 1.9.3 or later.
|
17
|
+
|
18
|
+
|
19
|
+
Installation
|
20
|
+
------------
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
gem 'WEBrocket'
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install WEBrocket
|
33
|
+
|
34
|
+
|
35
|
+
How to use
|
36
|
+
----------
|
37
|
+
|
38
|
+
require "webrick"
|
39
|
+
require "webrocket"
|
40
|
+
|
41
|
+
# write listner
|
42
|
+
class Listener
|
43
|
+
def on_open(websocket)
|
44
|
+
# do somthing
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_close(websocket)
|
48
|
+
# do somthing
|
49
|
+
end
|
50
|
+
|
51
|
+
def on_recv(websocket, data, type)
|
52
|
+
# do something
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_shutdown
|
56
|
+
# do something
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
server = WEBrick::HTTPServer.new
|
61
|
+
server.mount_websocket("sample", Listener.new, "test")
|
62
|
+
server.start
|
63
|
+
|
64
|
+
See sample directory for more details.
|
65
|
+
|
66
|
+
|
67
|
+
Limitations
|
68
|
+
-----------
|
69
|
+
|
70
|
+
+ Fragmentation is not supported yet.
|
71
|
+
+ TLS is not supported yet.
|
72
|
+
|
73
|
+
|
74
|
+
License
|
75
|
+
-------
|
76
|
+
|
77
|
+
Copyright (c) 2013 NAKAMURA Usaku usa@garbagecollect.jp
|
78
|
+
|
79
|
+
Redistribution and use in source and binary forms, with or without
|
80
|
+
modification, are permitted provided that the following conditions are met:
|
81
|
+
|
82
|
+
1. Redistributions of source code must retain the above copyright notice,
|
83
|
+
this list of conditions and the following disclaimer.
|
84
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
85
|
+
this list of conditions and the following disclaimer in the documentation
|
86
|
+
and/or other materials provided with the distribution.
|
87
|
+
|
88
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY
|
89
|
+
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
90
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
91
|
+
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
|
92
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
93
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
94
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
95
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
96
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
97
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
98
|
+
|
99
|
+
|
100
|
+
Supplimentary
|
101
|
+
-------------
|
102
|
+
|
103
|
+
I've found that there are many projects named webrocket, but, sorry, I can't
|
104
|
+
stop naming this one the same name :)
|
data/Rakefile
ADDED
data/lib/webrocket.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "digest/sha1"
|
3
|
+
require "webrocket/webrick-extender"
|
4
|
+
require "webrocket/websocket"
|
5
|
+
require "webrocket/version"
|
6
|
+
|
7
|
+
module WEBrocket
|
8
|
+
def self.attach(server, req, res, listener, subprotocol)
|
9
|
+
key = req["Sec-WebSocket-Key"]
|
10
|
+
unless key
|
11
|
+
res.status = WEBrick::HTTPStatus::RC_BAD_REQUEST
|
12
|
+
return
|
13
|
+
end
|
14
|
+
key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
15
|
+
res["Sec-WebSocket-Accept"] = Base64.strict_encode64(Digest::SHA1.digest(key))
|
16
|
+
|
17
|
+
if req["Sec-WebSocket-Version"].to_i < 13
|
18
|
+
res.status = WEBrick::HTTPStatus::RC_BAD_REQUEST
|
19
|
+
res["Sec-WebSocket-Version"] = 13
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
if req["Sec-WebSocket-Protocol"]
|
24
|
+
protocols = req["Sec-WebSocket-Protocol"].split(/\s*,\s*/)
|
25
|
+
if protocols.include?(subprotocol)
|
26
|
+
res["Sec-WebSocket-Protocol"] = subprotocol
|
27
|
+
elsif protocols.include?("null")
|
28
|
+
res["Sec-WebSocket-Protocol"] = "null"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
res.status = WEBrick::HTTPStatus::RC_SWITCHING_PROTOCOLS
|
33
|
+
res["Upgrade"] = "websocket"
|
34
|
+
#res["Connection"] = "Upgrade"
|
35
|
+
|
36
|
+
@keep_alive = true
|
37
|
+
|
38
|
+
res.define_singleton_method(:send_response) do |sock|
|
39
|
+
begin
|
40
|
+
setup_header
|
41
|
+
@header["connection"] = "Upgrade"
|
42
|
+
send_header(sock)
|
43
|
+
ws = WEBrocket::WebSocket.new(sock, listener, subprotocol)
|
44
|
+
server.add_shutdown_listener(ws.method(:on_shutdown))
|
45
|
+
ws.start
|
46
|
+
@keep_alive = false
|
47
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex
|
48
|
+
@logger.debug(ex)
|
49
|
+
@keep_alive = false
|
50
|
+
rescue StandardError => ex
|
51
|
+
@logger.error(ex)
|
52
|
+
@keep_alive = false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "webrick/httpserver"
|
2
|
+
require "webrocket"
|
3
|
+
|
4
|
+
module WEBrick
|
5
|
+
class HTTPServer
|
6
|
+
def mount_websocket(path, listener, subprotocol)
|
7
|
+
mount_proc(path) do |req, res|
|
8
|
+
WEBrocket.attach(self, req, res, listener, subprotocol)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_shutdown_listener(prc = nil, &block)
|
13
|
+
@shutdown_listeners ||= []
|
14
|
+
@shutdown_listeners.push(prc || block)
|
15
|
+
end
|
16
|
+
|
17
|
+
alias _original_shutdown shutdown
|
18
|
+
def shutdown
|
19
|
+
if @shutdown_listeners
|
20
|
+
@shutdown_listeners.each do |listener|
|
21
|
+
listener.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
_original_shutdown
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require "webrocket"
|
2
|
+
|
3
|
+
module WEBrocket
|
4
|
+
class WebSocket
|
5
|
+
class InternalError < StandardError; end
|
6
|
+
|
7
|
+
OP_CONTINUE = 0x0
|
8
|
+
OP_TEXT = 0x1
|
9
|
+
OP_BINARY = 0x2
|
10
|
+
OP_CLOSE = 0x8
|
11
|
+
OP_PING = 0x9
|
12
|
+
OP_PONG = 0xA
|
13
|
+
|
14
|
+
CLOSE_OK = [1000].pack("n")
|
15
|
+
CLOSE_CLOSED = [1001].pack("n")
|
16
|
+
CLOSE_PROTOCOL_ERROR = [1002].pack("n")
|
17
|
+
CLOSE_BROKEN_DATA = [1003].pack("n")
|
18
|
+
CLOSE_BROKEN_MESSAGE = [1007].pack("n")
|
19
|
+
CLOSE_POLICY_ERROR = [1008].pack("n")
|
20
|
+
CLOSE_TOO_BIG = [1009].pack("n")
|
21
|
+
CLOSE_UNKNOWN_ERROR = [1011].pack("n")
|
22
|
+
|
23
|
+
def initialize(sock, listener, subprotocol)
|
24
|
+
@sock = sock
|
25
|
+
@listener = listener
|
26
|
+
@subprotocol = subprotocol
|
27
|
+
@type = nil
|
28
|
+
@reason = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :subprotocol
|
32
|
+
|
33
|
+
def start
|
34
|
+
@listener.on_open(self) if @listener.respond_to?(:on_open)
|
35
|
+
begin
|
36
|
+
while true
|
37
|
+
r = IO.select([@sock])
|
38
|
+
recv_frame if r
|
39
|
+
end
|
40
|
+
rescue IOError
|
41
|
+
@reason ||= CLOSE_CLOSED
|
42
|
+
ensure
|
43
|
+
@reason ||= CLOSE_UNKNOWN_ERROR
|
44
|
+
send_close_frame(@reason) unless @sock.closed?
|
45
|
+
@listener.on_close(self) if @listener.respond_to?(:on_close)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def send(data, type = :text)
|
50
|
+
nil until IO.select([], [@sock])
|
51
|
+
case type
|
52
|
+
when :text
|
53
|
+
op = OP_TEXT
|
54
|
+
when :binary
|
55
|
+
op = OP_BINARY
|
56
|
+
else
|
57
|
+
raise InternalError, "unknown type: #{type}"
|
58
|
+
end
|
59
|
+
send_frame(op, data)
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_shutdown
|
63
|
+
send_close_frame unless @sock.closed?
|
64
|
+
@listener.on_shutdown if @listener.respond_to?(:on_shutdown)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def send_close_frame(data = "")
|
69
|
+
send_frame(OP_CLOSE, data)
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_frame(opcode, data = "")
|
73
|
+
data = data.dup.force_encoding('binary')
|
74
|
+
if data.bytesize <= 125
|
75
|
+
@sock.write([0x80 | opcode, data.bytesize].pack("cc") + data)
|
76
|
+
elsif data.bytesize <= 0xffff
|
77
|
+
@sock.write([0x80 | opcode, 126, data.bytesize].pack("ccn") + data)
|
78
|
+
else # under 64bit
|
79
|
+
@sock.write([0x80 | opcode, 127, data.bytesize / 0x1_0000_0000, data.bytesize % 0x1_0000_0000].pack("ccNN") + data)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def recv_frame
|
84
|
+
header = @sock.read(2)
|
85
|
+
return unless header
|
86
|
+
if header.bytesize != 2
|
87
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
88
|
+
raise InternalError, "something wrong: header = '#{header}'"
|
89
|
+
end
|
90
|
+
h0, h1 = header.unpack("cc")
|
91
|
+
len = h1 & 0x7F
|
92
|
+
if len == 126
|
93
|
+
tmp = @sock.read(2)
|
94
|
+
if !tmp || tmp.bytesize != 2
|
95
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
96
|
+
raise InternalError, "something wrong: size = '#{tmp}'"
|
97
|
+
end
|
98
|
+
len = tmp.unpack("n")
|
99
|
+
elsif len == 127
|
100
|
+
tmp = @sock.read(8)
|
101
|
+
if !tmp || tmp.bytesize != 8
|
102
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
103
|
+
raise InternalError, "something wrong: size = '#{tmp}'"
|
104
|
+
end
|
105
|
+
l0, l1 = tmp.unpack("NN")
|
106
|
+
len = l0 * 0x1_0000_0000 + l1
|
107
|
+
end
|
108
|
+
if len > 0
|
109
|
+
if h1 & 0x80 != 0
|
110
|
+
key = @sock.read(4)
|
111
|
+
if !key || key.bytesize != 4
|
112
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
113
|
+
raise InternalError, "something wrong: key = '#{key}'"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
data = @sock.read(len)
|
118
|
+
if !data || data.bytesize != len
|
119
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
120
|
+
raise InternalError, "something wrong"
|
121
|
+
end
|
122
|
+
|
123
|
+
if h1 & 0x80 != 0
|
124
|
+
data = data.bytes.map.with_index{|b, i| (b ^ (key[i % 4].ord)).chr}.join
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
op = h0 & 0xF
|
129
|
+
|
130
|
+
if h0 & 0x80 == 0
|
131
|
+
# XXX
|
132
|
+
@reason = CLOSE_TOO_BIG
|
133
|
+
raise InternalError, "fragmented data is not supported yet"
|
134
|
+
end
|
135
|
+
|
136
|
+
case op
|
137
|
+
when OP_CONTINUE, OP_TEXT, OP_BINARY
|
138
|
+
if op == OP_TEXT
|
139
|
+
@type = :text
|
140
|
+
elsif op == OP_BINARY
|
141
|
+
@type = :binary
|
142
|
+
elsif !@type
|
143
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
144
|
+
raise InternalError, "continued data at the first"
|
145
|
+
end
|
146
|
+
if @type == :text
|
147
|
+
data.force_encoding('utf-8')
|
148
|
+
raise InternalError, "invalid encoding" unless data.valid_encoding?
|
149
|
+
end
|
150
|
+
@listener.on_message(self, data, @type) if @listener.respond_to?(:on_message)
|
151
|
+
when OP_CLOSE
|
152
|
+
@reason = CLOSE_OK
|
153
|
+
raise IOError
|
154
|
+
when OP_PING
|
155
|
+
send_frame(OP_PONG, data)
|
156
|
+
when OP_PONG
|
157
|
+
# nothing to do
|
158
|
+
else
|
159
|
+
@reason = CLOSE_PROTOCOL_ERROR
|
160
|
+
raise InternalError, "unknown opcode: 0x%x" % op
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
data/sample/chat.html
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
<html lang="ja">
|
2
|
+
<head>
|
3
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
4
|
+
<title>chat sample</title>
|
5
|
+
<script type="text/javascript"><!--
|
6
|
+
var conn;
|
7
|
+
|
8
|
+
function escapeHTML(str) {
|
9
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
10
|
+
}
|
11
|
+
|
12
|
+
function login() {
|
13
|
+
console.log("login");
|
14
|
+
var user = document.getElementById("text").value;
|
15
|
+
if (user != "") {
|
16
|
+
document.getElementById("text").value = "";
|
17
|
+
document.getElementById("send").innerHTML = "send";
|
18
|
+
document.getElementById("send").setAttribute("onclick", "message();");
|
19
|
+
|
20
|
+
conn = new WebSocket("ws://localhost:10080/chat", "chat");
|
21
|
+
conn.onopen = function() {
|
22
|
+
conn.send("\uFEFF" + user);
|
23
|
+
};
|
24
|
+
conn.onerror = function(error) {
|
25
|
+
var elem = document.getElementById("server");
|
26
|
+
elem.innerHTML = "<b>WebSocket error: " + escapeHTML(error) + "</b><br>\n" + elem.innerHTML;
|
27
|
+
};
|
28
|
+
conn.onmessage = function(v) {
|
29
|
+
var elem = document.getElementById("server");
|
30
|
+
elem.innerHTML = escapeHTML(v.data) + "<br>\n" + elem.innerHTML;
|
31
|
+
};
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
function message() {
|
36
|
+
console.log("message");
|
37
|
+
conn.send(document.getElementById("text").value);
|
38
|
+
document.getElementById("text").value = "";
|
39
|
+
}
|
40
|
+
--></script>
|
41
|
+
</head>
|
42
|
+
<body>
|
43
|
+
<input id="text" value=""/><button id="send" onclick="login();">login</button>
|
44
|
+
<p id="server">
|
45
|
+
</p>
|
46
|
+
</body>
|
47
|
+
</html>
|
data/sample/chat.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
#!ruby
|
2
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
|
3
|
+
require "webrick"
|
4
|
+
require "webrocket"
|
5
|
+
|
6
|
+
# write listner
|
7
|
+
class Listener
|
8
|
+
def initialize
|
9
|
+
@clients = []
|
10
|
+
@m = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_open(websocket)
|
14
|
+
p [:open, websocket]
|
15
|
+
@m.synchronize do
|
16
|
+
@clients.push(websocket)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_close(websocket)
|
21
|
+
p [:close, websocket]
|
22
|
+
@m.synchronize do
|
23
|
+
user = websocket.instance_variable_get(:@chat_user)
|
24
|
+
if user
|
25
|
+
@clients.each do |cl|
|
26
|
+
cl.send("logout #{user}", :text)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@clients.delete(websocket)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_message(websocket, data, type)
|
34
|
+
user = websocket.instance_variable_get(:@chat_user)
|
35
|
+
@m.synchronize do
|
36
|
+
@clients.each do |cl|
|
37
|
+
if user
|
38
|
+
cl.send("#{user}: #{data}", :text)
|
39
|
+
else
|
40
|
+
cl.send("login #{data}", :text)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
unless user
|
45
|
+
websocket.instance_variable_set(:@chat_user, data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def on_shutdown
|
50
|
+
p [:shutdown]
|
51
|
+
# nothing to do
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
server = WEBrick::HTTPServer.new(Port: 10080)
|
56
|
+
server.mount_websocket("/chat", Listener.new, "chat")
|
57
|
+
trap(:INT) do
|
58
|
+
server.shutdown
|
59
|
+
end
|
60
|
+
server.start
|
data/sample/echo.html
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
<html lang="ja">
|
2
|
+
<head>
|
3
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
4
|
+
<script type="text/javascript"><!--
|
5
|
+
function escapeHTML(str){
|
6
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
7
|
+
}
|
8
|
+
|
9
|
+
var conn = new WebSocket("ws://localhost:10080/echo", "echo");
|
10
|
+
conn.onopen = function(){
|
11
|
+
var elem = document.getElementById("server");
|
12
|
+
elem.innerHTML = "<b>connected</b><br>\n" + elem.innerHTML;
|
13
|
+
};
|
14
|
+
conn.onerror = function(error){
|
15
|
+
var elem = document.getElementById("server");
|
16
|
+
elem.innerHTML = "<b>WebSocket error: " + escapeHTML(error) + "</b><br>\n" + elem.innerHTML;
|
17
|
+
};
|
18
|
+
conn.onmessage = function(v){
|
19
|
+
var elem = document.getElementById("server");
|
20
|
+
elem.innerHTML = escapeHTML(v.data) + "<br>\n" + elem.innerHTML;
|
21
|
+
}
|
22
|
+
--></script>
|
23
|
+
</head>
|
24
|
+
<body>
|
25
|
+
<input id="text" value="hello!"/><button onclick="conn.send(document.getElementById('text').value);">send</button>
|
26
|
+
<p id="server">
|
27
|
+
</p>
|
28
|
+
</body>
|
29
|
+
</html>
|
data/sample/echo.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!ruby
|
2
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
|
3
|
+
require "webrick"
|
4
|
+
require "webrocket"
|
5
|
+
|
6
|
+
# write listner
|
7
|
+
class Listener
|
8
|
+
def on_open(websocket)
|
9
|
+
# do somthing
|
10
|
+
p :open
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_close(websocket)
|
14
|
+
# do somthing
|
15
|
+
p :close
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_message(websocket, data, type)
|
19
|
+
# do something
|
20
|
+
p [:message, data, type]
|
21
|
+
websocket.send(data, type)
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_shutdown
|
25
|
+
# do something
|
26
|
+
p :shutdown
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
server = WEBrick::HTTPServer.new(Port: 10080)
|
31
|
+
server.mount_websocket("/echo", Listener.new, "echo")
|
32
|
+
trap(:INT) do
|
33
|
+
server.shutdown
|
34
|
+
end
|
35
|
+
server.start
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'net/http'
|
3
|
+
require 'socket'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'webrick'
|
6
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__)))
|
7
|
+
require 'webrocket'
|
8
|
+
|
9
|
+
class TestWEBrocket < Test::Unit::TestCase
|
10
|
+
def start_test_server(&block)
|
11
|
+
logger = Object.new
|
12
|
+
logger.instance_eval do
|
13
|
+
def <<(msg)
|
14
|
+
@log ||= ''
|
15
|
+
@log << msg
|
16
|
+
end
|
17
|
+
|
18
|
+
def log
|
19
|
+
@log ||= ''
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
server = WEBrick::HTTPServer.new(
|
24
|
+
:BindAddress => "127.0.0.1",
|
25
|
+
:Port => 0,
|
26
|
+
:DocumentRoot => File.dirname(__FILE__),
|
27
|
+
:ShutdownSocketWithoutClose => true,
|
28
|
+
:ServerType => Thread,
|
29
|
+
:Logger => WEBrick::Log.new(logger),
|
30
|
+
:AccessLog => [[logger, ""]]
|
31
|
+
)
|
32
|
+
|
33
|
+
begin
|
34
|
+
th = server.start
|
35
|
+
addr = server.listeners[0].addr
|
36
|
+
block.call(server, addr[3], addr[1], logger)
|
37
|
+
ensure
|
38
|
+
server.shutdown
|
39
|
+
th.join
|
40
|
+
end
|
41
|
+
|
42
|
+
logger.log
|
43
|
+
end
|
44
|
+
|
45
|
+
class Listener
|
46
|
+
def initialize(open = proc{}, close = proc{}, message = proc{}, shutdown = proc{})
|
47
|
+
@open = open
|
48
|
+
@close = close
|
49
|
+
@message = message
|
50
|
+
@shutdown = shutdown
|
51
|
+
end
|
52
|
+
|
53
|
+
def on_open(websocket)
|
54
|
+
@open.call(websocket)
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_close(websocket)
|
58
|
+
@close.call(websocket)
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_message(websocket, data, type)
|
62
|
+
@message.call(websocket, data, type)
|
63
|
+
end
|
64
|
+
|
65
|
+
def on_shutdown
|
66
|
+
@shutdown.call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_websocket
|
71
|
+
shutdowned = false # yes, I know shutdown is a noun
|
72
|
+
start_test_server do |server, host, port, logger|
|
73
|
+
http = Net::HTTP.new(host, port)
|
74
|
+
|
75
|
+
req = Net::HTTP::Get.new('/')
|
76
|
+
http.request(req) do |res|
|
77
|
+
assert_kind_of Net::HTTPOK, res.response
|
78
|
+
end
|
79
|
+
|
80
|
+
opened = false
|
81
|
+
open = lambda {|websocket|
|
82
|
+
opened = true
|
83
|
+
}
|
84
|
+
close = lambda {|websocket|
|
85
|
+
opened = false
|
86
|
+
}
|
87
|
+
message = lambda {|websocket, data, type|
|
88
|
+
}
|
89
|
+
shutdown = lambda {
|
90
|
+
shutdowned = true
|
91
|
+
}
|
92
|
+
server.mount_websocket('/test', Listener.new(open, close, message, shutdown), 'test')
|
93
|
+
|
94
|
+
req = Net::HTTP::Get.new('/test')
|
95
|
+
req['Upgrade'] = 'websocket'
|
96
|
+
req['Connection'] = 'Upgrade'
|
97
|
+
req['Sec-WebSocket-Version'] = '13'
|
98
|
+
req['Sec-WebSocket-Key'] = Base64.encode64('0'*16)
|
99
|
+
|
100
|
+
refute opened
|
101
|
+
sock = nil
|
102
|
+
http.request(req) do |res|
|
103
|
+
assert_kind_of Net::HTTPSwitchProtocol, res.response
|
104
|
+
sock = http.instance_variable_get(:@socket).io.dup
|
105
|
+
end
|
106
|
+
|
107
|
+
assert opened
|
108
|
+
|
109
|
+
sock.shutdown
|
110
|
+
|
111
|
+
refute shutdowned
|
112
|
+
end
|
113
|
+
assert shutdowned
|
114
|
+
end
|
115
|
+
end
|
data/webrocket.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'webrocket/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "webrocket"
|
8
|
+
spec.version = WEBrocket::VERSION
|
9
|
+
spec.authors = ["U.Nakamura"]
|
10
|
+
spec.email = ["usa@garbagecollect.jp"]
|
11
|
+
spec.description = %q{WebSocket extension for WEBrick}
|
12
|
+
spec.summary = %q{WebSocket extension for WEBrick}
|
13
|
+
spec.homepage = "https://github.com/unak/WEBRocket"
|
14
|
+
spec.license = "BSD-2-Clause"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "test-unit"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: webrocket
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- U.Nakamura
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: test-unit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: WebSocket extension for WEBrick
|
56
|
+
email:
|
57
|
+
- usa@garbagecollect.jp
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".travis.yml"
|
64
|
+
- Gemfile
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- lib/webrocket.rb
|
68
|
+
- lib/webrocket/version.rb
|
69
|
+
- lib/webrocket/webrick-extender.rb
|
70
|
+
- lib/webrocket/websocket.rb
|
71
|
+
- sample/chat.html
|
72
|
+
- sample/chat.rb
|
73
|
+
- sample/echo.html
|
74
|
+
- sample/echo.rb
|
75
|
+
- test/test_webrocket.rb
|
76
|
+
- webrocket.gemspec
|
77
|
+
homepage: https://github.com/unak/WEBRocket
|
78
|
+
licenses:
|
79
|
+
- BSD-2-Clause
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.2.2
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: WebSocket extension for WEBrick
|
101
|
+
test_files:
|
102
|
+
- test/test_webrocket.rb
|