sctp-socket 0.1.4 → 0.2.0
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
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/CHANGES.md +12 -0
- data/README.md +75 -0
- data/examples/client_for_server_test.rb +34 -0
- data/examples/sctp_server_example.rb +69 -0
- data/examples/server_using_sctp_server.rb +65 -0
- data/ext/sctp/socket.c +460 -65
- data/lib/sctp/server.rb +207 -0
- data/sctp-socket.gemspec +1 -1
- data.tar.gz.sig +0 -0
- metadata +6 -5
- metadata.gz.sig +0 -0
- data/ext/sctp/socket/client.c +0 -7
- data/ext/sctp/socket/server.c +0 -7
- data/ext/sctp/socket.h +0 -18
data/lib/sctp/server.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'sctp/socket'
|
3
|
+
|
4
|
+
module SCTP
|
5
|
+
# SCTP::Server provides a TCPServer-like interface for SCTP sockets.
|
6
|
+
#
|
7
|
+
# Unlike TCP which uses accept() to create new sockets for each connection,
|
8
|
+
# SCTP can operate in one-to-many mode where a single socket handles
|
9
|
+
# multiple associations. This server class provides both modes:
|
10
|
+
#
|
11
|
+
# 1. One-to-many mode (default): All connections share the server socket
|
12
|
+
# 2. One-to-one mode: Uses peeloff() to create individual sockets per association
|
13
|
+
#
|
14
|
+
# Example usage:
|
15
|
+
#
|
16
|
+
# # Basic one-to-many server
|
17
|
+
# server = SCTP::Server.new(['127.0.0.1'], 9999)
|
18
|
+
# loop do
|
19
|
+
# data, info = server.recvmsg
|
20
|
+
# puts "Received: #{data} from association #{info.association_id}"
|
21
|
+
# server.sendmsg("Echo: #{data}", association_id: info.association_id)
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # One-to-one server with peeloff
|
25
|
+
# server = SCTP::Server.new(['127.0.0.1'], 9999, one_to_one: true)
|
26
|
+
# loop do
|
27
|
+
# client = server.accept # Returns a new SCTP::Socket for the association
|
28
|
+
# Thread.new(client) do |c|
|
29
|
+
# data, info = c.recvmsg
|
30
|
+
# c.sendmsg("Echo: #{data}")
|
31
|
+
# c.close
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
class Server
|
36
|
+
attr_reader :socket, :addresses, :port, :one_to_one
|
37
|
+
|
38
|
+
# Create a new SCTP server.
|
39
|
+
#
|
40
|
+
# @param addresses [Array<String>, String, nil] IP addresses to bind to.
|
41
|
+
# If nil, binds to all available addresses.
|
42
|
+
# @param port [Integer] Port number to bind to
|
43
|
+
# @param one_to_one [Boolean] If true, uses one-to-one mode with peeloff.
|
44
|
+
# If false (default), uses one-to-many mode.
|
45
|
+
# @param backlog [Integer] Listen backlog (default: 128)
|
46
|
+
# @param socket_options [Hash] Additional socket options
|
47
|
+
def initialize(addresses = nil, port = 0, one_to_one: false, backlog: 128, **socket_options)
|
48
|
+
@addresses = Array(addresses) if addresses
|
49
|
+
@port = port
|
50
|
+
@one_to_one = one_to_one
|
51
|
+
@backlog = backlog
|
52
|
+
@socket_options = socket_options
|
53
|
+
@pending_associations = {}
|
54
|
+
|
55
|
+
# Create the main server socket
|
56
|
+
if one_to_one
|
57
|
+
@socket = SCTP::Socket.new(2, 1) # AF_INET, SOCK_STREAM
|
58
|
+
else
|
59
|
+
@socket = SCTP::Socket.new(2, 5) # AF_INET, SOCK_SEQPACKET
|
60
|
+
end
|
61
|
+
|
62
|
+
setup_socket
|
63
|
+
bind_and_listen
|
64
|
+
end
|
65
|
+
|
66
|
+
# Accept a new association (one-to-one mode only).
|
67
|
+
#
|
68
|
+
# In one-to-many mode, this method is not used. Instead, use recvmsg
|
69
|
+
# to receive data from any association.
|
70
|
+
#
|
71
|
+
# @return [SCTP::Socket] A new socket for the accepted association
|
72
|
+
# @raise [RuntimeError] If not in one-to-one mode
|
73
|
+
def accept
|
74
|
+
raise "accept() only available in one-to-one mode" unless @one_to_one
|
75
|
+
|
76
|
+
# Wait for a message to establish an association
|
77
|
+
data, info = @socket.recvmsg
|
78
|
+
association_id = info.association_id
|
79
|
+
|
80
|
+
# Check if we already have a peeled-off socket for this association
|
81
|
+
if @pending_associations[association_id]
|
82
|
+
client_socket = @pending_associations.delete(association_id)
|
83
|
+
else
|
84
|
+
# Peeloff a new socket for this association
|
85
|
+
client_socket = @socket.peeloff(association_id)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Store the initial message in the client socket for retrieval
|
89
|
+
client_socket.instance_variable_set(:@initial_message, [data, info])
|
90
|
+
|
91
|
+
# Add a method to retrieve the initial message
|
92
|
+
def client_socket.initial_message
|
93
|
+
@initial_message
|
94
|
+
end
|
95
|
+
|
96
|
+
client_socket
|
97
|
+
end
|
98
|
+
|
99
|
+
# Receive a message from any association (one-to-many mode).
|
100
|
+
#
|
101
|
+
# @param flags [Integer] Receive flags (default: 0)
|
102
|
+
# @return [Array<String, Struct>] Message data and info struct
|
103
|
+
def recvmsg(flags = 0)
|
104
|
+
@socket.recvmsg(flags)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Send a message to a specific association (one-to-many mode).
|
108
|
+
#
|
109
|
+
# @param data [String] Data to send
|
110
|
+
# @param options [Hash] Send options including :association_id
|
111
|
+
# @return [Integer] Number of bytes sent
|
112
|
+
def sendmsg(data, **options)
|
113
|
+
@socket.sendmsg(options.merge(message: data))
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get local addresses bound to this server.
|
117
|
+
#
|
118
|
+
# @return [Array<String>] Local addresses
|
119
|
+
def addr
|
120
|
+
@socket.getlocalnames
|
121
|
+
end
|
122
|
+
|
123
|
+
# Get the local port number.
|
124
|
+
#
|
125
|
+
# @return [Integer] Port number
|
126
|
+
def local_port
|
127
|
+
@socket.port
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check if the server is closed.
|
131
|
+
#
|
132
|
+
# @return [Boolean] true if closed, false otherwise
|
133
|
+
def closed?
|
134
|
+
@socket.closed?
|
135
|
+
end
|
136
|
+
|
137
|
+
# Close the server socket.
|
138
|
+
#
|
139
|
+
# @param options [Hash] Close options (e.g., linger: seconds)
|
140
|
+
def close(**options)
|
141
|
+
@socket.close(options) unless closed?
|
142
|
+
end
|
143
|
+
|
144
|
+
# Get server socket information.
|
145
|
+
#
|
146
|
+
# @return [String] String representation of the server
|
147
|
+
def to_s
|
148
|
+
if closed?
|
149
|
+
"#<SCTP::Server:closed>"
|
150
|
+
else
|
151
|
+
mode = @one_to_one ? "one-to-one" : "one-to-many"
|
152
|
+
"#<SCTP::Server:#{addr.join(',')}:#{local_port} (#{mode})>"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
alias_method :inspect, :to_s
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def setup_socket
|
161
|
+
# Apply any socket options provided
|
162
|
+
@socket_options.each do |option, value|
|
163
|
+
case option
|
164
|
+
when :reuse_addr
|
165
|
+
# SCTP typically doesn't need SO_REUSEADDR like TCP
|
166
|
+
# but we can support it for compatibility
|
167
|
+
when :autoclose
|
168
|
+
@socket.autoclose = value
|
169
|
+
when :nodelay
|
170
|
+
@socket.nodelay = value
|
171
|
+
when :init_msg
|
172
|
+
@socket.set_initmsg(value)
|
173
|
+
when :subscriptions
|
174
|
+
@socket.subscribe(value)
|
175
|
+
else
|
176
|
+
# For any other options, try to call them as methods
|
177
|
+
if @socket.respond_to?("#{option}=")
|
178
|
+
@socket.send("#{option}=", value)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Set up default subscriptions for server operation
|
184
|
+
@socket.subscribe(
|
185
|
+
data_io: true,
|
186
|
+
association: true,
|
187
|
+
address: true,
|
188
|
+
send_failure: true,
|
189
|
+
shutdown: true
|
190
|
+
)
|
191
|
+
end
|
192
|
+
|
193
|
+
def bind_and_listen
|
194
|
+
if @addresses && !@addresses.empty?
|
195
|
+
@socket.bindx(port: @port, addresses: @addresses, reuse_addr: true)
|
196
|
+
else
|
197
|
+
# Bind to all available addresses
|
198
|
+
@socket.bindx(port: @port, reuse_addr: true)
|
199
|
+
end
|
200
|
+
|
201
|
+
@socket.listen(@backlog)
|
202
|
+
|
203
|
+
# Update port if it was auto-assigned (port 0)
|
204
|
+
@port = @socket.port if @port == 0
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/sctp-socket.gemspec
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sctp-socket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Berger
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
ORVCZpRuCPpmC8qmqxUnARDArzucjaclkxjLWvCVHeFa9UP7K3Nl9oTjJNv+7/jM
|
36
36
|
WZs4eecIcUc4tKdHxcAJ0MO/Dkqq7hGaiHpwKY76wQ1+8xAh
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date: 2025-
|
38
|
+
date: 2025-08-04 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: bundler
|
@@ -114,12 +114,13 @@ files:
|
|
114
114
|
- Rakefile
|
115
115
|
- certs/djberg96_pub.pem
|
116
116
|
- examples/client_example.rb
|
117
|
+
- examples/client_for_server_test.rb
|
118
|
+
- examples/sctp_server_example.rb
|
117
119
|
- examples/server_example.rb
|
120
|
+
- examples/server_using_sctp_server.rb
|
118
121
|
- ext/sctp/extconf.rb
|
119
122
|
- ext/sctp/socket.c
|
120
|
-
-
|
121
|
-
- ext/sctp/socket/client.c
|
122
|
-
- ext/sctp/socket/server.c
|
123
|
+
- lib/sctp/server.rb
|
123
124
|
- sctp-socket.gemspec
|
124
125
|
homepage: https://github.com/djberg96/sctp-socket
|
125
126
|
licenses:
|
metadata.gz.sig
CHANGED
Binary file
|
data/ext/sctp/socket/client.c
DELETED
data/ext/sctp/socket/server.c
DELETED
data/ext/sctp/socket.h
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
#ifndef SCTP_SOCKET_INCLUDED
|
2
|
-
#define SCTP_SOCKET_INCLUDED
|
3
|
-
|
4
|
-
#include <ruby.h>
|
5
|
-
#include <netinet/sctp.h>
|
6
|
-
|
7
|
-
VALUE rb_hash_aref2(VALUE, const char*);
|
8
|
-
|
9
|
-
void Init_socket();
|
10
|
-
void Init_server();
|
11
|
-
void Init_client();
|
12
|
-
|
13
|
-
extern VALUE mSCTP;
|
14
|
-
extern VALUE cSocket;
|
15
|
-
extern VALUE cClient;
|
16
|
-
extern VALUE cServer;
|
17
|
-
|
18
|
-
#endif
|