kontena-websocket-client 0.1.0 → 0.1.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 +4 -4
- data/README.md +2 -0
- data/benchmark/.gitignore +1 -0
- data/benchmark/benchmark-client.rb +45 -0
- data/benchmark/benchmark-em.rb +66 -0
- data/benchmark/benchmark.rb +161 -0
- data/benchmark/benchmark.sh +17 -0
- data/benchmark/websocket-echo-server.go +207 -0
- data/kontena-websocket-client.gemspec +1 -1
- data/lib/kontena/websocket/client.rb +1 -1
- data/lib/kontena/websocket/client/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfd6cb9f7d3d9f6215854d22474b19f7ed67bb51
|
4
|
+
data.tar.gz: 2c3f654081f2e69fb8c783916411fda8a99dbf00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6df6327057cb56e983332987602d3ac8784850e0e8b179a4de2ee08c9cf453a34e53714bd11c6d1876769087ef7a68470bde845e6526587a20560c09991f938e
|
7
|
+
data.tar.gz: 0a12250d84f4d9cd0993257e01b8fea0decd70c020a4b9dfaf32ef5b9a58f0d35320b94d66d2975a5d13bfab28e89b2754b0ef398af75a86e2e3c3856d29d76b
|
data/README.md
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
[](https://travis-ci.org/kontena/kontena-websocket-client)
|
2
|
+
[](https://badge.fury.io/rb/kontena-websocket-client)
|
3
|
+
[](http://www.rubydoc.info/github/kontena/kontena-websocket-client/master)
|
2
4
|
|
3
5
|
# Kontena::Websocket::Client
|
4
6
|
|
@@ -0,0 +1 @@
|
|
1
|
+
websocket-echo-server
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
Thread.abort_on_exception = true
|
4
|
+
|
5
|
+
require 'kontena-websocket-client'
|
6
|
+
require_relative './benchmark'
|
7
|
+
|
8
|
+
WEBSOCKET_OPTIONS = {
|
9
|
+
connect_timeout: 1.0,
|
10
|
+
open_timeout: 1.0,
|
11
|
+
ping_timeout: 1.0,
|
12
|
+
ping_interval: nil,
|
13
|
+
write_timeout: 5.0,
|
14
|
+
}
|
15
|
+
|
16
|
+
Kontena::Websocket::Logging.initialize_logger(STDERR, LOG_LEVEL)
|
17
|
+
|
18
|
+
run_benchmark do |url, **options|
|
19
|
+
send_thread = nil
|
20
|
+
reader = BenchmarkReader.new
|
21
|
+
|
22
|
+
Kontena::Websocket::Client.connect(url, **WEBSOCKET_OPTIONS) do |client|
|
23
|
+
$logger.info "connect: #{client}"
|
24
|
+
|
25
|
+
send_thread = Thread.new {
|
26
|
+
send_stats = benchmark_sender(**options) do |msg, seq|
|
27
|
+
client.send(msg)
|
28
|
+
end
|
29
|
+
|
30
|
+
client.close()
|
31
|
+
|
32
|
+
send_stats
|
33
|
+
}
|
34
|
+
|
35
|
+
reader.start()
|
36
|
+
client.read do |message|
|
37
|
+
reader.on_message(Time.now, message)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
read_stats = reader.stop()
|
42
|
+
send_stats = send_thread.value
|
43
|
+
|
44
|
+
next send_stats, read_stats
|
45
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
Thread.abort_on_exception = true
|
4
|
+
|
5
|
+
require 'faye/websocket'
|
6
|
+
require 'eventmachine'
|
7
|
+
require_relative './benchmark'
|
8
|
+
|
9
|
+
run_benchmark do |url, **options|
|
10
|
+
send_thread = nil
|
11
|
+
reader = BenchmarkReader.new
|
12
|
+
|
13
|
+
EM.run {
|
14
|
+
ws = Faye::WebSocket::Client.new(url)
|
15
|
+
|
16
|
+
ws.on :open do |event|
|
17
|
+
$logger.info "open"
|
18
|
+
|
19
|
+
send_thread = Thread.new {
|
20
|
+
send_stats = benchmark_sender(**options) do |msg, seq|
|
21
|
+
EM.next_tick {
|
22
|
+
$logger.debug "send seq=%d" % [seq]
|
23
|
+
ws.send(msg)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
EM.next_tick {
|
28
|
+
$logger.info "close..."
|
29
|
+
ws.close()
|
30
|
+
}
|
31
|
+
|
32
|
+
send_stats
|
33
|
+
}
|
34
|
+
|
35
|
+
reader.start()
|
36
|
+
end
|
37
|
+
|
38
|
+
read = 0
|
39
|
+
|
40
|
+
ws.on :message do |event|
|
41
|
+
seq, rtt = reader.on_message(Time.now, event.data)
|
42
|
+
|
43
|
+
$logger.debug "read %d: seq=%d rtt=%.2f" % [read, seq, rtt]
|
44
|
+
|
45
|
+
read += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
ws.on :close do |event|
|
49
|
+
$logger.info "closed"
|
50
|
+
|
51
|
+
reader.stop
|
52
|
+
EM.stop
|
53
|
+
end
|
54
|
+
|
55
|
+
ws.on :error do |event|
|
56
|
+
$logger.warn "error: #{event}"
|
57
|
+
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
}
|
61
|
+
|
62
|
+
read_stats = reader.stop()
|
63
|
+
send_stats = send_thread.value
|
64
|
+
|
65
|
+
next send_stats, read_stats
|
66
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
LOG_LEVEL = ENV['LOG_LEVEL'] || Logger::WARN
|
4
|
+
|
5
|
+
$logger = Logger.new(STDERR)
|
6
|
+
$logger.level = LOG_LEVEL
|
7
|
+
$logger.progname = 'websocket-benchmark'
|
8
|
+
|
9
|
+
# @yield at given target interval
|
10
|
+
# @return after duration
|
11
|
+
def with_rate(rate, duration, &block)
|
12
|
+
t0 = Time.now
|
13
|
+
interval = 1.0 / rate
|
14
|
+
count = 0
|
15
|
+
count_miss = 0
|
16
|
+
total_yield = 0.0
|
17
|
+
|
18
|
+
while (t = Time.now) < t0 + duration
|
19
|
+
yield t
|
20
|
+
|
21
|
+
t_yield = Time.now
|
22
|
+
|
23
|
+
count += 1
|
24
|
+
total_yield += (t_yield - t)
|
25
|
+
|
26
|
+
t_next = t0 + count * interval
|
27
|
+
|
28
|
+
if t_next > t_yield
|
29
|
+
sleep t_next - t_yield
|
30
|
+
else
|
31
|
+
count_miss += 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
t_total = t - t0
|
36
|
+
|
37
|
+
return {
|
38
|
+
time: t_total,
|
39
|
+
count: count,
|
40
|
+
rate: count / t_total,
|
41
|
+
util: total_yield / t_total,
|
42
|
+
miss: (count_miss / count),
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
# @yield [seq, message]
|
47
|
+
# @return [Hash] after duration
|
48
|
+
def benchmark_sender(rate: 1000, duration: 5.0, message_size: 1000)
|
49
|
+
total_size = 0
|
50
|
+
seq = 0
|
51
|
+
|
52
|
+
padding = 'X'*(message_size - 16 - 16)
|
53
|
+
|
54
|
+
stats = with_rate(rate, duration) do |t|
|
55
|
+
message = '%15.6f %15d %s' % [t.to_f, seq, padding]
|
56
|
+
|
57
|
+
yield message, seq
|
58
|
+
|
59
|
+
total_size += message.length
|
60
|
+
seq += 1
|
61
|
+
end
|
62
|
+
|
63
|
+
return stats.merge(
|
64
|
+
bytes: total_size,
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
class BenchmarkReader
|
69
|
+
def initialize
|
70
|
+
@count = 0
|
71
|
+
@bytes = 0
|
72
|
+
@latency_total = 0.0
|
73
|
+
end
|
74
|
+
|
75
|
+
def start
|
76
|
+
@t_start = Time.now
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param time [Time] Time.now
|
80
|
+
# @param message [String]
|
81
|
+
# @return [Integer, Float] seq, rtt
|
82
|
+
def on_message(time, message)
|
83
|
+
msg_time_s, msg_seq_s, padding = message.split(' ', 3)
|
84
|
+
msg_t = msg_time_s.to_f
|
85
|
+
msg_seq = msg_seq_s.to_i
|
86
|
+
t = time.to_f
|
87
|
+
|
88
|
+
@count += 1
|
89
|
+
@bytes += message.length
|
90
|
+
@latency_total += (t - msg_t)
|
91
|
+
|
92
|
+
return msg_seq, t - msg_t
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Hash]
|
96
|
+
def stop
|
97
|
+
@t_stop = Time.now
|
98
|
+
seconds = @t_stop - @t_start
|
99
|
+
|
100
|
+
return {
|
101
|
+
time: seconds,
|
102
|
+
count: @count,
|
103
|
+
rate: @count / seconds,
|
104
|
+
bytes: @bytes,
|
105
|
+
bytes_rate: @bytes / seconds,
|
106
|
+
latency_avg: @latency_total / @count,
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
URL = 'ws://localhost:8080/echo'
|
112
|
+
RATES = [1, 10, 100, 1000, 3000, 5000, 10000]
|
113
|
+
DURATION = 5.0
|
114
|
+
MESSAGE_SIZE = 1000
|
115
|
+
|
116
|
+
HEADER = '%5s %6s/s %9s: send @ %9s/s (%6s%% %6s%%) read @ %9s/s (%6s%%) = %12s/s ~%9s'
|
117
|
+
FORMAT = '%5.2fs %6d/s %9d: send @ %9.2f/s (%6.2f%% %6.2f%%) read @ %9.2f/s (%6.2f%%) = %12s/s ~%9.6fs'
|
118
|
+
|
119
|
+
def to_si(val)
|
120
|
+
if val > 10**9
|
121
|
+
'%.3fG' % [val / 10**9]
|
122
|
+
elsif val > 10**6
|
123
|
+
'%.3fM' % [val / 10**6]
|
124
|
+
elsif val > 10**3
|
125
|
+
'%.3fK' % [val / 10**3]
|
126
|
+
else
|
127
|
+
'%.3f ' % [val]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# @yield [url, **options]
|
132
|
+
# @yieldreturn [send_stats, read_stats]
|
133
|
+
def run_benchmark()
|
134
|
+
url = ENV['URL'] || URL
|
135
|
+
|
136
|
+
rates = (ENV['RATES']&.split&.map{|r| Integer(r)} || RATES)
|
137
|
+
duration = (ENV['DURATION'] || DURATION).to_f
|
138
|
+
message_size = (ENV['MESSAGE_SIZE'] || MESSAGE_SIZE).to_i
|
139
|
+
|
140
|
+
puts HEADER % ['TIME ', 'RATE', 'COUNT', 'MESSAGES', 'UTIL', 'MISS', 'MESSAGES', 'DROP', 'BYTES', 'LATENCY']
|
141
|
+
|
142
|
+
for rate in rates
|
143
|
+
options = {
|
144
|
+
rate: rate,
|
145
|
+
duration: duration,
|
146
|
+
message_size: message_size,
|
147
|
+
}
|
148
|
+
|
149
|
+
$logger.info "benchmark: #{url} #{options}"
|
150
|
+
|
151
|
+
send_stats, read_stats = yield(url, **options)
|
152
|
+
|
153
|
+
drop_ratio = 1.0 - read_stats[:count].to_f / send_stats[:count].to_f
|
154
|
+
|
155
|
+
puts FORMAT % [
|
156
|
+
duration, rate, send_stats[:count],
|
157
|
+
send_stats[:rate], send_stats[:util] * 100.0, send_stats[:miss] * 100.0,
|
158
|
+
read_stats[:rate], drop_ratio * 100.0, to_si(read_stats[:bytes_rate]), read_stats[:latency_avg],
|
159
|
+
]
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
set -uex
|
4
|
+
|
5
|
+
DIR=$(dirname $0)
|
6
|
+
SERVER_ARGS=${SERVER_ARGS:- -drop}
|
7
|
+
BENCHMARK=${BENCHMARK:-benchmark-client.rb}
|
8
|
+
|
9
|
+
go get -d github.com/gorilla/websocket
|
10
|
+
go build -o $DIR/websocket-echo-server $DIR/websocket-echo-server.go
|
11
|
+
|
12
|
+
killall websocket-echo-server || true
|
13
|
+
$DIR/websocket-echo-server -quiet $SERVER_ARGS &
|
14
|
+
|
15
|
+
$DIR/${BENCHMARK}
|
16
|
+
|
17
|
+
trap "kill 0" INT EXIT
|
@@ -0,0 +1,207 @@
|
|
1
|
+
package main
|
2
|
+
|
3
|
+
import (
|
4
|
+
"flag"
|
5
|
+
"fmt"
|
6
|
+
"github.com/gorilla/websocket"
|
7
|
+
"log"
|
8
|
+
"net/http"
|
9
|
+
"time"
|
10
|
+
)
|
11
|
+
|
12
|
+
var websocketUpgrader = websocket.Upgrader{}
|
13
|
+
|
14
|
+
// single-goroutine read+write loop
|
15
|
+
// blocks reads if writes block
|
16
|
+
func websocketEchoSync(conn *websocket.Conn) error {
|
17
|
+
for {
|
18
|
+
if messageType, data, err := conn.ReadMessage(); err != nil {
|
19
|
+
if websocket.IsCloseError(err, 1000) {
|
20
|
+
break
|
21
|
+
} else {
|
22
|
+
return fmt.Errorf("websocket read: %v", err)
|
23
|
+
}
|
24
|
+
} else if err := conn.WriteMessage(messageType, data); err != nil {
|
25
|
+
return fmt.Errorf("websocket write: %v", err)
|
26
|
+
} else {
|
27
|
+
if options.Verbose {
|
28
|
+
log.Printf("websocket echo: %v", data)
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
return nil
|
34
|
+
}
|
35
|
+
|
36
|
+
type websocketMessage struct {
|
37
|
+
Type int
|
38
|
+
Data []byte
|
39
|
+
}
|
40
|
+
|
41
|
+
func websocketAsyncReader(conn *websocket.Conn, c chan websocketMessage) error {
|
42
|
+
defer close(c)
|
43
|
+
|
44
|
+
var messages, dropped int
|
45
|
+
var start = time.Now()
|
46
|
+
|
47
|
+
for {
|
48
|
+
if messageType, data, err := conn.ReadMessage(); err != nil {
|
49
|
+
if websocket.IsCloseError(err, 1000) {
|
50
|
+
if options.Verbose {
|
51
|
+
log.Printf("websocket read close: %v", err)
|
52
|
+
}
|
53
|
+
|
54
|
+
break
|
55
|
+
} else {
|
56
|
+
return fmt.Errorf("websocket read: %v", err)
|
57
|
+
}
|
58
|
+
} else {
|
59
|
+
var m = websocketMessage{messageType, data}
|
60
|
+
messages += 1
|
61
|
+
|
62
|
+
select {
|
63
|
+
case c <- m:
|
64
|
+
if options.Verbose {
|
65
|
+
log.Printf("websocket read: %v", m.Data)
|
66
|
+
}
|
67
|
+
|
68
|
+
default:
|
69
|
+
dropped += 1
|
70
|
+
|
71
|
+
if options.Verbose {
|
72
|
+
log.Printf("websocket drop: %v", m.Data)
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
var end = time.Now()
|
79
|
+
|
80
|
+
if !options.Quiet {
|
81
|
+
var seconds = end.Sub(start).Seconds()
|
82
|
+
|
83
|
+
log.Printf("websocket read: %d messages in %.1fs (%.2f/s, dropped %.2f%%)",
|
84
|
+
messages, seconds,
|
85
|
+
float64(messages)/seconds,
|
86
|
+
float64(dropped)/float64(messages)*100.0,
|
87
|
+
)
|
88
|
+
}
|
89
|
+
|
90
|
+
return nil
|
91
|
+
}
|
92
|
+
|
93
|
+
func websocketAsyncWriter(conn *websocket.Conn, c <-chan websocketMessage) error {
|
94
|
+
for m := range c {
|
95
|
+
if err := conn.WriteMessage(m.Type, m.Data); err != nil {
|
96
|
+
return fmt.Errorf("websocket write: %v", err)
|
97
|
+
} else {
|
98
|
+
if options.Verbose {
|
99
|
+
log.Printf("websocket write: %v", m.Data)
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
return nil
|
105
|
+
}
|
106
|
+
|
107
|
+
func websocketEchoAsync(conn *websocket.Conn) error {
|
108
|
+
var messageChan = make(chan websocketMessage, options.DropBuffer)
|
109
|
+
var readClose struct {
|
110
|
+
code int
|
111
|
+
text string
|
112
|
+
}
|
113
|
+
var readError error
|
114
|
+
|
115
|
+
// custom close handler to send close frame after reader drains the message queue
|
116
|
+
conn.SetCloseHandler(func(code int, text string) error {
|
117
|
+
readClose.code = code
|
118
|
+
readClose.text = text
|
119
|
+
|
120
|
+
return nil
|
121
|
+
})
|
122
|
+
|
123
|
+
go func() {
|
124
|
+
if err := websocketAsyncReader(conn, messageChan); err != nil {
|
125
|
+
readError = err
|
126
|
+
log.Printf("websocket read error: %v", err)
|
127
|
+
}
|
128
|
+
}()
|
129
|
+
|
130
|
+
if err := websocketAsyncWriter(conn, messageChan); err != nil {
|
131
|
+
return err
|
132
|
+
} else if readError != nil {
|
133
|
+
return readError
|
134
|
+
} else {
|
135
|
+
var closeMessage = websocket.FormatCloseMessage(readClose.code, readClose.text)
|
136
|
+
|
137
|
+
if err := conn.WriteControl(websocket.CloseMessage, closeMessage, time.Time{}); err != nil {
|
138
|
+
log.Printf("websocket write close: %v", err)
|
139
|
+
} else {
|
140
|
+
if options.Verbose {
|
141
|
+
log.Printf("websocket write close")
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
return nil
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
func EchoHandler(w http.ResponseWriter, r *http.Request) {
|
150
|
+
if websocketConn, err := websocketUpgrader.Upgrade(w, r, nil); err != nil {
|
151
|
+
log.Printf("Websocket Upgrade error: %v", err)
|
152
|
+
w.WriteHeader(500)
|
153
|
+
fmt.Fprintf(w, "%v", err)
|
154
|
+
} else {
|
155
|
+
if !options.Quiet {
|
156
|
+
log.Printf("Websocket echo connect: %v", r.RemoteAddr)
|
157
|
+
}
|
158
|
+
|
159
|
+
defer websocketConn.Close()
|
160
|
+
|
161
|
+
if options.Drop {
|
162
|
+
if err := websocketEchoAsync(websocketConn); err != nil {
|
163
|
+
log.Printf("Websocket echo error: %v", err)
|
164
|
+
return
|
165
|
+
}
|
166
|
+
} else {
|
167
|
+
if err := websocketEchoSync(websocketConn); err != nil {
|
168
|
+
log.Printf("Websocket echo error: %v", err)
|
169
|
+
return
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
if !options.Quiet {
|
174
|
+
log.Printf("Websocket echo close: %v", r.RemoteAddr)
|
175
|
+
}
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
var options struct {
|
180
|
+
Listen string
|
181
|
+
Verbose bool
|
182
|
+
Quiet bool
|
183
|
+
Drop bool
|
184
|
+
DropBuffer uint
|
185
|
+
}
|
186
|
+
|
187
|
+
func init() {
|
188
|
+
flag.StringVar(&options.Listen, "listen", "localhost:8080", "HOST:PORT")
|
189
|
+
flag.BoolVar(&options.Verbose, "verbose", false, "log echo messages")
|
190
|
+
flag.BoolVar(&options.Quiet, "quiet", false, "do not log connects")
|
191
|
+
flag.BoolVar(&options.Drop, "drop", false, "drop messages if client is sending faster than reading")
|
192
|
+
flag.UintVar(&options.DropBuffer, "drop-buffer", 1000, "message buffer length")
|
193
|
+
}
|
194
|
+
|
195
|
+
func main() {
|
196
|
+
flag.Parse()
|
197
|
+
|
198
|
+
if !options.Quiet {
|
199
|
+
log.Printf("Websocket listen: %v", options.Listen)
|
200
|
+
}
|
201
|
+
|
202
|
+
http.HandleFunc("/echo", EchoHandler)
|
203
|
+
|
204
|
+
if err := http.ListenAndServe(options.Listen, nil); err != nil {
|
205
|
+
log.Fatalf("http listen: %v", err)
|
206
|
+
}
|
207
|
+
}
|
@@ -348,7 +348,7 @@ class Kontena::Websocket::Client
|
|
348
348
|
debug "ping-pong at #{ping_at} in #{ping_delay}s"
|
349
349
|
|
350
350
|
# XXX: defer call without mutex?
|
351
|
-
@on_pong.call(ping_delay) # TODO: also pass ping_at
|
351
|
+
@on_pong.call(ping_delay) if @on_pong # TODO: also pass ping_at
|
352
352
|
end
|
353
353
|
end
|
354
354
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kontena-websocket-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kontena, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: websocket-driver
|
@@ -80,6 +80,12 @@ files:
|
|
80
80
|
- LICENSE
|
81
81
|
- README.md
|
82
82
|
- Rakefile
|
83
|
+
- benchmark/.gitignore
|
84
|
+
- benchmark/benchmark-client.rb
|
85
|
+
- benchmark/benchmark-em.rb
|
86
|
+
- benchmark/benchmark.rb
|
87
|
+
- benchmark/benchmark.sh
|
88
|
+
- benchmark/websocket-echo-server.go
|
83
89
|
- examples/websocket-echo-client.rb
|
84
90
|
- kontena-websocket-client.gemspec
|
85
91
|
- lib/kontena-websocket-client.rb
|