sping 1.0.1 → 1.0.3
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/bin/sping +11 -11
- data/lib/last_acks.rb +23 -10
- data/lib/session.rb +179 -183
- data/lib/session_manager.rb +231 -217
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74faa9103509651e47d2f098c22b66fae80ca1ba0b71b132aa636d26bf1e0f04
|
4
|
+
data.tar.gz: 9ae6b44c52ded9e1999eb3fe467216b6d640747d11a8b69f4eed864efeb30a03
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c08fcd7358309abe6a7f67044bbf267f31cba483786dc387b78ef14e8ce32801b067bd2ca8e2a545e141d3ffe843d004a4427d1a91cba1cbdd8d154679344653
|
7
|
+
data.tar.gz: 56ea68be301c25d116657775aabf26a9fb9fdf41cf2224ff9dcd72be1b897f20b076e9d0f4a1154ec777c53f20eeff173ded69985c993c99169198290f66a092
|
data/bin/sping
CHANGED
@@ -7,10 +7,10 @@ require 'msgpack'
|
|
7
7
|
require_relative '../lib/session_manager'
|
8
8
|
|
9
9
|
MessagePack::DefaultFactory.register_type(
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
MessagePack::Timestamp::TYPE,
|
11
|
+
Time,
|
12
|
+
packer: MessagePack::Time::Packer,
|
13
|
+
unpacker: MessagePack::Time::Unpacker
|
14
14
|
)
|
15
15
|
|
16
16
|
$logger = Logger.new $stdout
|
@@ -21,18 +21,18 @@ sess_mgr.run
|
|
21
21
|
sess_mgr.run_server
|
22
22
|
|
23
23
|
ARGV.each do |host|
|
24
|
-
|
24
|
+
sess_mgr.new_session(host)
|
25
25
|
end
|
26
26
|
|
27
27
|
Signal.trap('INT') do
|
28
|
-
|
29
|
-
|
28
|
+
puts 'Stop server...'
|
29
|
+
sess_mgr.stop_server
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
puts 'Stop sessions...'
|
32
|
+
sess_mgr.stop_sessions
|
33
33
|
|
34
|
-
|
35
|
-
|
34
|
+
puts 'Stop client...'
|
35
|
+
sess_mgr.stop
|
36
36
|
end
|
37
37
|
|
38
38
|
sess_mgr.join
|
data/lib/last_acks.rb
CHANGED
@@ -2,20 +2,33 @@
|
|
2
2
|
# sharable_constant_value: literal
|
3
3
|
|
4
4
|
class LastAcks
|
5
|
+
attr_reader :size
|
5
6
|
|
6
|
-
|
7
|
+
def initialize(size = 32)
|
8
|
+
@size = size
|
9
|
+
@acks = []
|
10
|
+
@acks_mutex = Mutex.new
|
7
11
|
|
8
|
-
|
9
|
-
|
10
|
-
|
12
|
+
@size.times do |index|
|
13
|
+
@acks[index] = {
|
14
|
+
'R' => 0,
|
15
|
+
'U' => Time.at(0),
|
16
|
+
'X' => Time.at(0)
|
17
|
+
}
|
11
18
|
end
|
19
|
+
end
|
12
20
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
if @acks.length > @size
|
17
|
-
@acks.shift
|
18
|
-
end
|
21
|
+
def acks
|
22
|
+
@acks_mutex.synchronize do
|
23
|
+
return @acks.dup
|
19
24
|
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_ack(ack)
|
28
|
+
@acks_mutex.synchronize do
|
29
|
+
@acks << ack
|
20
30
|
|
31
|
+
@acks.shift if @acks.length > @size
|
32
|
+
end
|
33
|
+
end
|
21
34
|
end
|
data/lib/session.rb
CHANGED
@@ -2,228 +2,224 @@
|
|
2
2
|
# sharable_constant_value: literal
|
3
3
|
|
4
4
|
module SPing
|
5
|
+
class SessionError < StandardError; end
|
6
|
+
class TCPHandshakeError < SessionError; end
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
+
class Session
|
9
|
+
attr_reader :created, :last_rx, :madebyme
|
10
|
+
attr_accessor :session_id, :tcp_handshake_complete, :udp_handshake_complete
|
8
11
|
|
9
|
-
|
12
|
+
require 'socket'
|
13
|
+
require 'timeout'
|
14
|
+
require 'msgpack'
|
15
|
+
require_relative 'last_acks'
|
10
16
|
|
11
|
-
|
12
|
-
|
17
|
+
def initialize(host, port, socket, madebyme)
|
18
|
+
@host = host
|
19
|
+
@port = port
|
20
|
+
@socket = socket
|
21
|
+
@created = Time.now
|
22
|
+
@madebyme = madebyme
|
13
23
|
|
14
|
-
|
15
|
-
|
16
|
-
require 'msgpack'
|
17
|
-
require_relative 'last_acks'
|
24
|
+
@tcp_handshake_complete = false
|
25
|
+
@udp_handshake_complete = false
|
18
26
|
|
19
|
-
|
20
|
-
|
21
|
-
@port = port
|
22
|
-
@socket = socket
|
23
|
-
@created = Time.now
|
24
|
-
@madebyme = madebyme
|
27
|
+
@last_acks = LastAcks.new
|
28
|
+
end
|
25
29
|
|
26
|
-
|
27
|
-
|
30
|
+
def do_tcp_handshake1(socket, session_id)
|
31
|
+
socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
|
32
|
+
socket.flush
|
28
33
|
|
29
|
-
|
30
|
-
end
|
34
|
+
invite = socket.readpartial 6
|
31
35
|
|
32
|
-
|
33
|
-
socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
|
34
|
-
socket.flush
|
36
|
+
raise TCPHandshakeError, 'Peer didn\'t invite us.' unless invite.chomp == 'INVITE'
|
35
37
|
|
36
|
-
|
38
|
+
@session_id = session_id
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
end
|
40
|
+
socket.write "#{session_id}\r\n"
|
41
|
+
socket.flush
|
41
42
|
|
42
|
-
|
43
|
+
socket.close
|
43
44
|
|
44
|
-
|
45
|
-
|
45
|
+
@tcp_handshake_complete = true
|
46
|
+
end
|
46
47
|
|
47
|
-
|
48
|
+
def do_tcp_handshake2
|
49
|
+
socket = TCPSocket.new @host, @port
|
50
|
+
banner = socket.readpartial 9001
|
51
|
+
if banner.length > 9000
|
52
|
+
socket.close
|
53
|
+
raise TCPHandshakeError, 'Host banner too big'
|
54
|
+
end
|
48
55
|
|
49
|
-
|
50
|
-
|
56
|
+
unless banner.start_with? 'sping-0.3-'
|
57
|
+
socket.close
|
58
|
+
raise TCPHandshakeError, 'Host banner not sping'
|
59
|
+
end
|
51
60
|
|
52
|
-
|
53
|
-
|
54
|
-
banner = socket.readpartial 9001
|
55
|
-
if banner.length > 9000
|
56
|
-
socket.close
|
57
|
-
raise TCPHandshakeError, 'Host banner too big'
|
58
|
-
end
|
61
|
+
@remote_version = banner.chomp
|
62
|
+
$logger.info "Peer uses the following program version: #{@remote_version.dump}"
|
59
63
|
|
60
|
-
|
61
|
-
|
62
|
-
raise TCPHandshakeError, 'Host banner not sping'
|
63
|
-
end
|
64
|
+
socket.write "INVITE\r\n"
|
65
|
+
socket.flush
|
64
66
|
|
65
|
-
|
66
|
-
|
67
|
+
invite_buf = socket.readpartial 32
|
68
|
+
if invite_buf.length > 31 || invite_buf.empty?
|
69
|
+
socket.close
|
70
|
+
raise TCPHandshakeError, 'Invite banner wrong size'
|
71
|
+
end
|
67
72
|
|
68
|
-
|
69
|
-
socket.flush
|
73
|
+
@session_id = invite_buf.chomp.to_i
|
70
74
|
|
71
|
-
|
72
|
-
if inviteBuf.length > 31 || inviteBuf.length.zero?
|
73
|
-
socket.close
|
74
|
-
raise TCPHandshakeError, 'Invite banner wrong size'
|
75
|
-
end
|
75
|
+
socket.close
|
76
76
|
|
77
|
-
|
77
|
+
@tcp_handshake_complete = true
|
78
|
+
end
|
78
79
|
|
79
|
-
|
80
|
+
def set_endpoint(host, port)
|
81
|
+
@host = host
|
82
|
+
@port = port
|
83
|
+
end
|
80
84
|
|
81
|
-
|
82
|
-
|
85
|
+
def start_udp_handshake_sender(send_interval = 5)
|
86
|
+
raise 'UDP handshake sender is already running.' if @udp_handshake_sender
|
83
87
|
|
84
|
-
|
85
|
-
|
86
|
-
|
88
|
+
@udp_handshake_sender = Thread.new(send_interval) do |th_send_interval|
|
89
|
+
loop do
|
90
|
+
send_udp_handshake
|
91
|
+
sleep th_send_interval
|
87
92
|
end
|
93
|
+
end
|
94
|
+
end
|
88
95
|
|
89
|
-
|
90
|
-
|
96
|
+
def send_udp_handshake
|
97
|
+
packet = {
|
98
|
+
'Y' => 'h'.ord,
|
99
|
+
'M' => 11_181,
|
100
|
+
'V' => 3,
|
101
|
+
'S' => @session_id
|
102
|
+
}.to_msgpack
|
91
103
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
sleep send_interval
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
104
|
+
$logger.debug "Send UDP handshake to #{@host} port #{@port}."
|
105
|
+
@socket.send packet, 0, @host, @port
|
106
|
+
end
|
99
107
|
|
100
|
-
|
101
|
-
|
102
|
-
'Y' => 'h'.ord,
|
103
|
-
'M' => 11181,
|
104
|
-
'V' => 3,
|
105
|
-
'S' => @session_id
|
106
|
-
}.to_msgpack
|
108
|
+
def stop_udp_handshake_sender
|
109
|
+
raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
|
107
110
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
+
@udp_handshake_sender.kill
|
112
|
+
@udp_handshake_sender = nil
|
113
|
+
end
|
111
114
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
115
|
+
def udp_handshake_recived
|
116
|
+
stop_udp_handshake_sender if @madebyme
|
117
|
+
@udp_handshake_complete = true
|
118
|
+
end
|
119
|
+
|
120
|
+
def start_pinger
|
121
|
+
raise 'Pinger is already running.' if @pinger
|
117
122
|
|
118
|
-
|
119
|
-
|
120
|
-
|
123
|
+
@pinger = Thread.new do
|
124
|
+
loop do
|
125
|
+
ping
|
126
|
+
sleep 1
|
121
127
|
end
|
128
|
+
end
|
129
|
+
end
|
122
130
|
|
123
|
-
|
124
|
-
|
131
|
+
def stop_pinger
|
132
|
+
raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
|
125
133
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
sleep 1
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
134
|
+
@pinger.kill
|
135
|
+
@pinger = nil
|
136
|
+
end
|
133
137
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
138
|
+
def ping
|
139
|
+
current_id = (Time.now.to_i % 255) + 1
|
140
|
+
data = {
|
141
|
+
'Y' => 't'.ord,
|
142
|
+
'M' => 11_181,
|
143
|
+
'S' => @session_id,
|
144
|
+
'I' => current_id,
|
145
|
+
'T' => Time.now,
|
146
|
+
'E' => 0,
|
147
|
+
'A' => @last_acks.acks
|
148
|
+
}.to_msgpack
|
149
|
+
|
150
|
+
$logger.debug "Send ping to #{@host} port #{@port}."
|
151
|
+
@socket.send data, 0, @host, @port
|
152
|
+
end
|
139
153
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
154
|
+
def stop
|
155
|
+
@pinger&.kill
|
156
|
+
@udp_handshake_sender&.kill
|
157
|
+
end
|
158
|
+
|
159
|
+
def handle_ping(packet, rxtime, peeraddr)
|
160
|
+
if packet['E'] != 0
|
161
|
+
$logger.error "Package displays an error message: #{packet['E']} Processing aborted."
|
162
|
+
return
|
163
|
+
end
|
164
|
+
|
165
|
+
id = packet['I']
|
166
|
+
txtime = packet['T']
|
167
|
+
remote_last_acks = packet['A']
|
168
|
+
|
169
|
+
ack = {
|
170
|
+
'R' => id,
|
171
|
+
'U' => txtime,
|
172
|
+
'X' => rxtime
|
173
|
+
}
|
174
|
+
|
175
|
+
@last_acks.add_ack(ack)
|
176
|
+
@last_rx = rxtime
|
177
|
+
|
178
|
+
# This is the A
|
179
|
+
# The peer is B
|
180
|
+
# TX => Time from A to B
|
181
|
+
# RX => Time from B to A
|
182
|
+
|
183
|
+
# Remove zero acks
|
184
|
+
remote_last_acks.reject! { |block_ack| block_ack['R'].zero? }
|
185
|
+
|
186
|
+
# Sort by TX time
|
187
|
+
remote_last_acks.sort_by! { |block_ack| block_ack['U'] }
|
188
|
+
|
189
|
+
newest_remote_ack = remote_last_acks.last.to_h
|
190
|
+
|
191
|
+
# Calculate loss
|
192
|
+
tx_loss = 0
|
193
|
+
rx_loss = 0
|
194
|
+
exchanges = 0
|
195
|
+
|
196
|
+
if remote_last_acks.length == 32
|
197
|
+
# We have enough data
|
198
|
+
exchanges = 32
|
199
|
+
remote_acks = remote_last_acks.map { |ack| ack['R'] }
|
200
|
+
local_acks = @last_acks.acks.map { |ack| ack['R'] }
|
201
|
+
|
202
|
+
tip_id = (Time.now.to_i % 255) + 1
|
203
|
+
starting = tip_id - 32
|
204
|
+
|
205
|
+
last_ids = if tip_id > 32
|
206
|
+
(starting..tip_id).to_a
|
207
|
+
else
|
208
|
+
((255 + starting)..255).to_a + (1..tip_id).to_a
|
209
|
+
end
|
155
210
|
|
156
|
-
|
157
|
-
|
158
|
-
@udp_handshake_sender&.kill
|
211
|
+
last_ids[0...-1].each do |id|
|
212
|
+
tx_loss += 1 unless remote_acks.include? id
|
159
213
|
end
|
160
214
|
|
161
|
-
|
162
|
-
|
163
|
-
$logger.error "Package displays an error message: #{packet['E']} Processing aborted."
|
164
|
-
return
|
165
|
-
end
|
166
|
-
|
167
|
-
id = packet['I']
|
168
|
-
txtime = packet['T']
|
169
|
-
lastAcks = packet['A']
|
170
|
-
|
171
|
-
ack = {
|
172
|
-
'R' => id,
|
173
|
-
'U' => txtime,
|
174
|
-
'X' => rxtime
|
175
|
-
}
|
176
|
-
|
177
|
-
@last_acks.add_ack(ack)
|
178
|
-
@last_rx = rxtime
|
179
|
-
|
180
|
-
# This is the A
|
181
|
-
# The peer is B
|
182
|
-
# TX => Time from A to B
|
183
|
-
# RX => Time from B to A
|
184
|
-
|
185
|
-
# Remove zero acks
|
186
|
-
lastAcks.reject! { |ack| ack['R'] == 0 }
|
187
|
-
|
188
|
-
# Sort by TX time
|
189
|
-
lastAcks.sort_by! { |ack| ack['U'] }
|
190
|
-
|
191
|
-
newest_remote_ack = lastAcks.last.to_h
|
192
|
-
|
193
|
-
# Calculate loss
|
194
|
-
tx_loss = 0
|
195
|
-
rx_loss = 0
|
196
|
-
exchanges = 0
|
197
|
-
|
198
|
-
if lastAcks.length == 32
|
199
|
-
# We have enough data
|
200
|
-
exchanges = 32
|
201
|
-
remote_acks = lastAcks.map { |ack| ack['R'] }
|
202
|
-
local_acks = @last_acks.acks.map { |ack| ack['R'] }
|
203
|
-
|
204
|
-
tip_id = Time.now.to_i % 255 + 1
|
205
|
-
starting = tip_id - 32
|
206
|
-
|
207
|
-
last_ids = nil
|
208
|
-
if tip_id > 32
|
209
|
-
last_ids = (starting..tip_id).to_a
|
210
|
-
else
|
211
|
-
last_ids = ((255 + starting)..255).to_a + (1..tip_id).to_a
|
212
|
-
end
|
213
|
-
|
214
|
-
last_ids[0...-1].each do |id|
|
215
|
-
tx_loss += 1 unless remote_acks.include? id
|
216
|
-
end
|
217
|
-
|
218
|
-
last_ids[1..-1].each do |id|
|
219
|
-
rx_loss += 1 unless local_acks.include? id
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
tx_latency = ((newest_remote_ack['X'].to_f - newest_remote_ack['U'].to_f) * 1000.0).round(6)
|
224
|
-
rx_latency = ((rxtime - txtime) * 1000.0).round(6)
|
225
|
-
puts "[#{peeraddr}] RX: #{rx_latency}ms TX: #{tx_latency}ms [Loss RX: #{rx_loss}/#{exchanges} | Loss TX: #{tx_loss}/#{exchanges}]"
|
215
|
+
last_ids[1..].each do |id|
|
216
|
+
rx_loss += 1 unless local_acks.include? id
|
226
217
|
end
|
218
|
+
end
|
227
219
|
|
220
|
+
tx_latency = ((newest_remote_ack['X'].to_f - newest_remote_ack['U'].to_f) * 1000.0).round(6)
|
221
|
+
rx_latency = ((rxtime - txtime) * 1000.0).round(6)
|
222
|
+
puts "[#{peeraddr}] RX: #{rx_latency}ms TX: #{tx_latency}ms [Loss RX: #{rx_loss}/#{exchanges} | Loss TX: #{tx_loss}/#{exchanges}]"
|
228
223
|
end
|
224
|
+
end
|
229
225
|
end
|
data/lib/session_manager.rb
CHANGED
@@ -2,261 +2,275 @@
|
|
2
2
|
# sharable_constant_value: literal
|
3
3
|
|
4
4
|
module SPing
|
5
|
+
class SessionManagerError < StandardError; end
|
6
|
+
class OutOfSessions < SessionManagerError; end
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
+
class SessionManager
|
9
|
+
require 'socket'
|
10
|
+
require 'timeout'
|
11
|
+
require_relative 'session'
|
8
12
|
|
9
|
-
|
13
|
+
def initialize(host = '::', port = 6924)
|
14
|
+
@host = host
|
15
|
+
@port = port
|
10
16
|
|
11
|
-
|
12
|
-
|
13
|
-
require_relative 'session'
|
17
|
+
@sessions = {}
|
18
|
+
@sessions_mutex = Mutex.new
|
14
19
|
|
15
|
-
|
16
|
-
|
20
|
+
@socket = UDPSocket.new(Socket::AF_INET6)
|
21
|
+
@socket.bind @host, @port
|
22
|
+
end
|
17
23
|
|
18
|
-
|
19
|
-
|
24
|
+
def new_session(host, port = 6924)
|
25
|
+
$logger.info "Add new session for host #{host} port #{port}."
|
26
|
+
session = SPing::Session.new host, port, @socket, true
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
28
|
+
counter = 0
|
29
|
+
loop do
|
30
|
+
session_id = request_session session
|
31
|
+
break if session_id
|
24
32
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
begin
|
30
|
-
counter = 0
|
31
|
-
loop do
|
32
|
-
begin
|
33
|
-
session.do_tcp_handshake2
|
34
|
-
rescue Errno::ECONNRESET, EOFError, IO::Timeout, TCPHandshakeError => e
|
35
|
-
$logger.warn "TCP handshake failed: #{e.message}"
|
36
|
-
return
|
37
|
-
end
|
38
|
-
if ! @sessions.key? session.session_id
|
39
|
-
break
|
40
|
-
end
|
41
|
-
if counter > 5
|
42
|
-
raise OutOfSessions, 'The peer could not name a session ID that had not yet been assigned.'
|
43
|
-
end
|
44
|
-
counter += 1
|
45
|
-
end
|
46
|
-
rescue OutOfSessions => e
|
47
|
-
$logger.error "Out of session: #{e.message}"
|
48
|
-
return
|
49
|
-
end
|
50
|
-
|
51
|
-
$logger.info "TCP handshake for host #{host} port #{port} successful. Session ID is #{session.session_id}."
|
52
|
-
|
53
|
-
$logger.info "Perform UDP handshake for session ID #{session.session_id}."
|
54
|
-
session.start_udp_handshake_sender
|
55
|
-
|
56
|
-
@sessions[session.session_id] = session
|
33
|
+
# rubocop:disable Style/IfUnlessModifier
|
34
|
+
if counter > 5
|
35
|
+
raise OutOfSessions, 'The peer could not name a session ID that had not yet been assigned.'
|
57
36
|
end
|
37
|
+
# rubocop:enable Style/IfUnlessModifier
|
58
38
|
|
59
|
-
|
60
|
-
|
39
|
+
counter += 1
|
40
|
+
end
|
61
41
|
|
62
|
-
|
63
|
-
@sessions.delete(session_id)
|
64
|
-
end
|
42
|
+
$logger.info "TCP handshake for host #{host} port #{port} successful. Session ID is #{session.session_id}."
|
65
43
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
44
|
+
$logger.info "Perform UDP handshake for session ID #{session.session_id}."
|
45
|
+
session.start_udp_handshake_sender
|
46
|
+
rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
|
47
|
+
$logger.warn "TCP handshake failed: #{e.message}"
|
48
|
+
rescue OutOfSessions => e
|
49
|
+
$logger.error "Out of session: #{e.message}"
|
50
|
+
end
|
71
51
|
|
72
|
-
|
73
|
-
|
74
|
-
@gc_thread&.join
|
75
|
-
@server_thread&.join
|
76
|
-
end
|
52
|
+
def del_session(session_id)
|
53
|
+
$logger.debug "Delete session with session id #{session_id}"
|
77
54
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
loop do
|
84
|
-
Thread.new(@socket.recvfrom(10000)) do |buf|
|
85
|
-
handle_packet buf
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
@gc_thread = Thread.new do
|
90
|
-
loop do
|
91
|
-
do_gc
|
92
|
-
sleep 10
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
55
|
+
@sessions_mutex.synchronize do
|
56
|
+
@sessions[session_id].stop
|
57
|
+
@sessions.delete(session_id)
|
58
|
+
end
|
59
|
+
end
|
96
60
|
|
97
|
-
|
98
|
-
|
99
|
-
|
61
|
+
def stop_sessions
|
62
|
+
@sessions.each do |_session_id, session|
|
63
|
+
session.stop
|
64
|
+
end
|
65
|
+
end
|
100
66
|
|
101
|
-
|
67
|
+
def join
|
68
|
+
@runner&.join
|
69
|
+
@gc_thread&.join
|
70
|
+
@server_thread&.join
|
71
|
+
end
|
102
72
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
handle_client client
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
73
|
+
def run
|
74
|
+
raise 'Client is already running.' if @runner
|
75
|
+
raise 'GC is already running.' if @gc
|
110
76
|
|
111
|
-
|
77
|
+
@runner = Thread.new do
|
78
|
+
loop do
|
79
|
+
Thread.new(@socket.recvfrom(10_000)) do |buf|
|
80
|
+
handle_packet buf
|
81
|
+
end
|
112
82
|
end
|
83
|
+
end
|
84
|
+
@gc_thread = Thread.new do
|
85
|
+
loop do
|
86
|
+
do_gc
|
87
|
+
sleep 10
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
113
91
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
@server_thread.kill
|
118
|
-
@server_thread = nil
|
92
|
+
def run_server
|
93
|
+
raise 'Server is already running.' if @server_thread
|
94
|
+
raise 'Server already exist.' if @server
|
119
95
|
|
120
|
-
|
96
|
+
@server = TCPServer.new @host, @port
|
121
97
|
|
122
|
-
|
123
|
-
|
98
|
+
@server_thread = Thread.new do
|
99
|
+
loop do
|
100
|
+
Thread.new(@server.accept) do |client|
|
101
|
+
handle_client client
|
102
|
+
end
|
124
103
|
end
|
104
|
+
end
|
125
105
|
|
126
|
-
|
127
|
-
|
106
|
+
return @server_thread
|
107
|
+
end
|
128
108
|
|
129
|
-
|
130
|
-
|
109
|
+
def stop_server
|
110
|
+
raise 'Server thread is not running.' unless @server_thread
|
131
111
|
|
132
|
-
|
112
|
+
@server_thread.kill
|
113
|
+
@server_thread = nil
|
133
114
|
|
134
|
-
|
135
|
-
@gc_thread = nil
|
136
|
-
end
|
115
|
+
raise 'Server is not running.' unless @server
|
137
116
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
117
|
+
@server.close
|
118
|
+
@server = nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def stop
|
122
|
+
raise 'Session manager is not running.' unless @runner
|
123
|
+
|
124
|
+
@runner.kill
|
125
|
+
@runner = nil
|
144
126
|
|
145
|
-
|
146
|
-
|
127
|
+
raise 'Session GC is not running.' unless @gc_thread
|
128
|
+
|
129
|
+
@gc_thread.kill
|
130
|
+
@gc_thread = nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def generate_session_id
|
134
|
+
Timeout.timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
|
135
|
+
loop do
|
136
|
+
session_id = rand (2**32) - 1
|
137
|
+
return session_id unless @sessions.key? session_id
|
147
138
|
end
|
139
|
+
end
|
140
|
+
end
|
148
141
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
142
|
+
private
|
143
|
+
|
144
|
+
def do_gc
|
145
|
+
$logger.debug 'Remove outdated sessions.'
|
146
|
+
# rubocop:disable Style/HashEachMethods
|
147
|
+
# You cannot run through a map and edit it at the same time. Since this is necessary due to the threading,
|
148
|
+
# a snapshot of the key variable is run through.
|
149
|
+
@sessions.keys.each do |session_id|
|
150
|
+
# rubocop:enable Style/HashEachMethods
|
151
|
+
|
152
|
+
session = @sessions[session_id]
|
153
|
+
# It is possible that sessions no longer exist here, as we may have session IDs that have already been deleted.
|
154
|
+
# However, we can ignore this aspect here, as we are the only function that deletes sessions.
|
155
|
+
if !(session.tcp_handshake_complete && session.udp_handshake_complete)
|
156
|
+
# Handshake incomplete
|
157
|
+
if (Time.now.to_i - session.created.to_i) > 60
|
158
|
+
# TCP and/or UDP take more than 60 seconds.
|
159
|
+
$logger.debug "UDP handshake for session id #{session_id} timed out."
|
160
|
+
del_session session_id
|
161
|
+
end
|
162
|
+
elsif (Time.now.to_i - session.last_rx.to_i) > 30
|
163
|
+
# 30 seconds have elapsed since the last ping from the peer was received. The peer is probably dead.
|
164
|
+
$logger.debug "Session id #{session_id} without activity for over thirty seconds."
|
165
|
+
del_session session_id
|
170
166
|
end
|
167
|
+
end
|
168
|
+
end
|
171
169
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
packet = MessagePack.unpack(buf[0])
|
180
|
-
|
181
|
-
# Check whether the Magic Number is the correct one.
|
182
|
-
if packet['M'] != 11181
|
183
|
-
$logger.warn 'Package contains incorrect magic number. Processing is canceled.'
|
184
|
-
return
|
185
|
-
end
|
186
|
-
|
187
|
-
type = packet['Y'].chr
|
188
|
-
case type
|
189
|
-
when 'h'
|
190
|
-
handle_udp_handshake packet, peeraddr, peerport
|
191
|
-
when 't'
|
192
|
-
handle_ping packet, rxtime, peeraddr, peerport
|
193
|
-
else
|
194
|
-
$logger.warn "Package is of unknown type: #{type}"
|
195
|
-
end
|
170
|
+
def request_session(session)
|
171
|
+
session.do_tcp_handshake2
|
172
|
+
|
173
|
+
@sessions_mutex.synchronize do
|
174
|
+
unless @sessions.key? session.session_id
|
175
|
+
@sessions[session.session_id] = session
|
176
|
+
return session.session_id
|
196
177
|
end
|
178
|
+
end
|
179
|
+
|
180
|
+
return nil
|
181
|
+
end
|
182
|
+
|
183
|
+
def handle_packet(buf)
|
184
|
+
rxtime = Time.now
|
185
|
+
|
186
|
+
peeraddr = buf[1][2]
|
187
|
+
peerport = buf[1][1]
|
188
|
+
$logger.debug "Packet received from #{peeraddr} port #{peerport}."
|
189
|
+
|
190
|
+
packet = MessagePack.unpack(buf[0])
|
191
|
+
|
192
|
+
# Check whether the Magic Number is the correct one.
|
193
|
+
if packet['M'] != 11_181
|
194
|
+
$logger.warn 'Package contains incorrect magic number. Processing is canceled.'
|
195
|
+
return
|
196
|
+
end
|
197
|
+
|
198
|
+
type = packet['Y'].chr
|
199
|
+
case type
|
200
|
+
when 'h'
|
201
|
+
handle_udp_handshake packet, peeraddr, peerport
|
202
|
+
when 't'
|
203
|
+
handle_ping packet, rxtime, peeraddr, peerport
|
204
|
+
else
|
205
|
+
$logger.warn "Package is of unknown type: #{type}"
|
206
|
+
end
|
207
|
+
end
|
197
208
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
end
|
206
|
-
|
207
|
-
session.set_endpoint peeraddr, peerport
|
208
|
-
|
209
|
-
if ! session.madebyme
|
210
|
-
# If the session was not started by me, the other peer expects a
|
211
|
-
# confirmation of the handshake in which the same is sent back.
|
212
|
-
$logger.debug "Send UDP Handshake back for session id #{session_id}."
|
213
|
-
session.send_udp_handshake
|
214
|
-
end
|
215
|
-
|
216
|
-
if session.udp_handshake_complete
|
217
|
-
$logger.warn 'UDP handshake is received, although a previous one was already successful.'
|
218
|
-
return
|
219
|
-
end
|
220
|
-
|
221
|
-
session.udp_handshake_recived
|
222
|
-
$logger.info "UDP handshake for session ID #{session_id} was successful."
|
223
|
-
|
224
|
-
session.start_pinger
|
225
|
-
else
|
226
|
-
$logger.warn 'UDP handshake received for uninitiated session.'
|
227
|
-
return
|
228
|
-
end
|
209
|
+
def handle_udp_handshake(packet, peeraddr, peerport)
|
210
|
+
session_id = packet['S']
|
211
|
+
session = @sessions[session_id]
|
212
|
+
if session
|
213
|
+
if packet['V'] != 3
|
214
|
+
$logger.warn "UDP handshake uses an unsupported version: #{packet['V']}"
|
215
|
+
return
|
229
216
|
end
|
230
217
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
$logger.warn "Ping packet received for non-initiated session id #{session_id}."
|
239
|
-
end
|
218
|
+
session.set_endpoint peeraddr, peerport
|
219
|
+
|
220
|
+
unless session.madebyme
|
221
|
+
# If the session was not started by me, the other peer expects a
|
222
|
+
# confirmation of the handshake in which the same is sent back.
|
223
|
+
$logger.debug "Send UDP Handshake back for session id #{session_id}."
|
224
|
+
session.send_udp_handshake
|
240
225
|
end
|
241
226
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
session = SPing::Session.new host, port, @socket, false
|
246
|
-
|
247
|
-
$logger.debug "Session request from host #{host} port #{port}."
|
248
|
-
|
249
|
-
begin
|
250
|
-
session.do_tcp_handshake1 client, self.generate_session_id
|
251
|
-
rescue Errno::ECONNRESET, EOFError, IO::Timeout, TCPHandshakeError => e
|
252
|
-
$logger.warn "Could not establish a new session with Peer: #{e.message}"
|
253
|
-
return
|
254
|
-
rescue OutOfSessions => e
|
255
|
-
$logger.error "Could not establish a new session with Peer: #{e.message}"
|
256
|
-
return
|
257
|
-
end
|
258
|
-
|
259
|
-
@sessions[session.session_id] = session
|
227
|
+
if session.udp_handshake_complete
|
228
|
+
$logger.warn 'UDP handshake is received, although a previous one was already successful.'
|
229
|
+
return
|
260
230
|
end
|
231
|
+
|
232
|
+
session.udp_handshake_recived
|
233
|
+
$logger.info "UDP handshake for session ID #{session_id} was successful."
|
234
|
+
|
235
|
+
session.start_pinger
|
236
|
+
else
|
237
|
+
$logger.warn 'UDP handshake received for uninitiated session.'
|
238
|
+
return
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def handle_ping(packet, rxtime, peeraddr, peerport)
|
243
|
+
session_id = packet['S']
|
244
|
+
session = @sessions[session_id]
|
245
|
+
if session&.tcp_handshake_complete && session&.udp_handshake_complete
|
246
|
+
session.set_endpoint peeraddr, peerport
|
247
|
+
session.handle_ping packet, rxtime, peeraddr
|
248
|
+
else
|
249
|
+
$logger.warn "Ping packet received for non-initiated session id #{session_id}."
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def handle_client(client)
|
254
|
+
host = client.peeraddr[3]
|
255
|
+
port = client.peeraddr[1]
|
256
|
+
session = SPing::Session.new host, port, @socket, false
|
257
|
+
|
258
|
+
$logger.debug "Session request from host #{host} port #{port}."
|
259
|
+
|
260
|
+
session_id = nil
|
261
|
+
|
262
|
+
@sessions_mutex.synchronize do
|
263
|
+
session_id = generate_session_id
|
264
|
+
@sessions[session_id] = session
|
265
|
+
end
|
266
|
+
|
267
|
+
session.do_tcp_handshake1 client, session_id
|
268
|
+
rescue Errno::ECONNRESET, EOFError, IO::TimeoutError, TCPHandshakeError => e
|
269
|
+
$logger.warn "Could not establish a new session with Peer: #{e.message}"
|
270
|
+
del_session session_id
|
271
|
+
rescue OutOfSessions => e
|
272
|
+
$logger.error "Could not establish a new session with Peer: #{e.message}"
|
273
|
+
del_session session_id
|
261
274
|
end
|
275
|
+
end
|
262
276
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sping
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marek Küthe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: msgpack
|