nng 0.1.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '059fd5f5fe9ba741fbbbfc57ead871921b2ead9eb5fb5f9ddce30b99cbcb8e8f'
4
- data.tar.gz: 6cfb7d0644296c8d2c99f50c7626717139d40a24a59e70c87bb81a29a9db6845
3
+ metadata.gz: e49576e9e33fb3e2a0bc98702bdd42c8055aed72390b67cb2378fe428ec1c3f9
4
+ data.tar.gz: d2f7a9e2a8d50eb8bed827f7a5ec1dfbcddc96a7cb9a7d74761b1c4309a9f9ee
5
5
  SHA512:
6
- metadata.gz: be9d6c590d943a1396cbe3e3c3cb7b6086d52633fe4cf23e03c7c8664d051c6d8183bea23cfb7daf4121df2cbb86589600f4756b6f3c859bd93c946820a33d85
7
- data.tar.gz: 4417ea199f3eb0bd1130cd92c375986b5e27ab72c2a68c1cfb7204740fdb3274882e3910bebf99c2b983f4bd803b3e3476e401ea8c57faa12415a3a093f3c7c9
6
+ metadata.gz: 9925501abf685c5aec0b4f25b7d274635805dfe4597d34ce0caaaad7355a0a88556632060f265754af5a7f8f3015428adce090acabf47fd2fc6240082380ee4d
7
+ data.tar.gz: c0741f1c0e049eeefc1c4c2c9b26c57305a42cd2136b6665521e20d2fed99ab7e27a2b2c9359f8c65fb2adc5cd0d37a8313c321d7a3dee7cc7e28248305a9ffd
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ ### Breaking changes
6
+
7
+ - Rewritten C extension from scratch
8
+ - `raw` argument changed from positional bool to keyword (`raw: true`)
9
+
10
+ ### Added
11
+
12
+ - **TLS transport** — `tls+tcp://` URLs with `cert:`, `key:`, `ca:`, `verify:`, `server_name:` kwargs on `#listen` and `#dial`
13
+ - **Mutual TLS** — server-side client certificate verification
14
+ - **`NNG::Pipe`** — lightweight pipe introspection via `Message#pipe`
15
+ - `#tls_verified?` — whether peer certificate was verified
16
+ - `#tls_peer_cn` — peer certificate common name
17
+ - `#id` — pipe identifier
18
+ - **`NNG::Device`** — transparent message forwarding between sockets
19
+ - **Pipe event notifications** — `Socket::Base#on_pipe_event` yields `:connect`/`:disconnect` events with `NNG::Pipe`
20
+ - **Runtime statistics** — `NNG.stats` and `NNG.stats_for(socket)` return NNG stat snapshots as nested Hashes
21
+ - **`Sub0#subscribe` / `Sub0#unsubscribe`** — add/remove topic subscriptions at runtime
22
+ - `Socket::Base#forward(msg)` — send an existing message preserving its header, enabling stateless raw mode proxying
23
+ - `Socket::Base#raw?` — check whether a socket was opened in raw mode
24
+ - `Sub0.new(prefix:)` — subscribe to a topic prefix at construction time
25
+ - Socket option accessors:
26
+ - `name` / `name=` — socket name
27
+ - `urls` — list of listen/dial URLs
28
+ - `recv_buffer` / `recv_buffer=` — receive buffer size
29
+ - `send_buffer` / `send_buffer=` — send buffer size
30
+ - `recv_max_size` / `recv_max_size=` — max receive message size
31
+ - `recv_timeout` / `recv_timeout=` — receive timeout (ms)
32
+ - `send_timeout` / `send_timeout=` — send timeout (ms)
33
+ - `reconnect_time` / `reconnect_time=` — reconnect interval as a `Range` of seconds
34
+ - `protocol_name` / `peer_name` — read-only protocol info
35
+ - Generic option accessors: `get_opt_int`, `set_opt_int`, `get_opt_ms`, `set_opt_ms`, `get_opt_size`, `set_opt_size`, `get_opt_string`, `set_opt_string`
36
+ - `wait_readable` / `wait_writable` default to the socket's recv/send timeout
37
+ - `receive` / `send` raise `Timeout::Error` on timeout
38
+ - `NNG.nng_version` — returns nng library version as a 3-element array
39
+ - Message manipulation: `#body`, `#body_clear`, `#body_append`, `#header`, `#header=`, `#dup`, `#consumed?`
40
+ - pkg-config support in `extconf.rb` with fallback to system library path
41
+ - ZGuide messaging pattern examples (pipeline, lazy pirate, pub/sub, heartbeat, LVC, bstar, clone)
42
+ - Comprehensive specs for all protocols, TLS, raw mode, timeouts, memory management, and socket options
43
+ - Benchmarks for throughput and latency (inproc/IPC/TCP, async/threads)
44
+
45
+ ## 0.1.0
46
+
47
+ - Initial release with pair0/pair1, req0/rep0, pub0/sub0, push0/pull0, bus0, surveyor0/respondent0
48
+ - C extension using nng FFI bindings
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Adib Saad, Patrik Wenger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # rbnng
2
+
3
+ [![CI](https://github.com/paddor/rbnng/actions/workflows/ci.yml/badge.svg)](https://github.com/paddor/rbnng/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/nng?color=e9573f)](https://rubygems.org/gems/nng)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![TLS](https://img.shields.io/badge/TLS-supported-2ea44f?logo=letsencrypt&logoColor=white)](#tls)
7
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.2-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
8
+
9
+ Fast, native Ruby bindings for [nng](https://nng.nanomsg.org/) — a lightweight, broker-less messaging library for building distributed systems.
10
+
11
+ > **47k+ msg/s** inproc throughput | **57 µs** fiber roundtrip latency | TLS built-in
12
+
13
+ ---
14
+
15
+ ## Highlights
16
+
17
+ - **All scalability protocols** — req/rep, pub/sub, push/pull, pair, survey, bus
18
+ - **Native C extension** — no FFI overhead, GVL released during blocking I/O, thread-safe
19
+ - **Async-first** — first-class [async](https://github.com/socketry/async) fiber support
20
+ - **Zero-copy forwarding** — `#forward` transfers message ownership without copying
21
+ - **Raw mode** — bypass the protocol state machine for scalable proxies
22
+ - **TLS transport** — `tls+tcp://` with mTLS, certificate pinning, and peer introspection
23
+ - **Nonblock-first optimization** — tries `NNG_FLAG_NONBLOCK` before releasing the GVL for up to 26% higher throughput
24
+
25
+ ## Install
26
+
27
+ Install nng on your system:
28
+
29
+ ```sh
30
+ # macOS
31
+ brew install nng
32
+
33
+ # Debian/Ubuntu
34
+ apt install libnng-dev
35
+
36
+ # Arch
37
+ pacman -S nng
38
+ ```
39
+
40
+ Then add the gem:
41
+
42
+ ```sh
43
+ gem install nng
44
+ # or in Gemfile
45
+ gem 'nng'
46
+ ```
47
+
48
+ <details>
49
+ <summary>TLS support (optional)</summary>
50
+
51
+ Install mbedTLS to enable the `tls+tcp://` transport:
52
+
53
+ ```sh
54
+ # macOS
55
+ brew install mbedtls
56
+
57
+ # Debian/Ubuntu
58
+ apt install libmbedtls-dev
59
+
60
+ # Arch
61
+ pacman -S mbedtls
62
+ ```
63
+
64
+ </details>
65
+
66
+ ## Quick Start
67
+
68
+ ### Request / Reply
69
+
70
+ ```ruby
71
+ require 'nng'
72
+ require 'async'
73
+
74
+ Sync do |task|
75
+ rep = NNG::Socket::Rep0.new
76
+ rep.listen('tcp://127.0.0.1:5555')
77
+
78
+ req = NNG::Socket::Req0.new
79
+ req.dial('tcp://127.0.0.1:5555')
80
+
81
+ task.async do
82
+ msg = rep.receive
83
+ rep.send("re: #{msg.body}")
84
+ end
85
+
86
+ req.send('hello')
87
+ reply = req.receive
88
+ puts reply.body # => "re: hello"
89
+ end
90
+ ```
91
+
92
+ ### Pub / Sub
93
+
94
+ ```ruby
95
+ Sync do |task|
96
+ pub = NNG::Socket::Pub0.new
97
+ pub.listen('ipc:///tmp/pubsub.sock')
98
+
99
+ all = NNG::Socket::Sub0.new
100
+ all.dial('ipc:///tmp/pubsub.sock')
101
+
102
+ weather = NNG::Socket::Sub0.new(prefix: 'weather.')
103
+ weather.dial('ipc:///tmp/pubsub.sock')
104
+
105
+ sleep 0.01 # allow connections to establish
106
+
107
+ task.async do
108
+ pub.send('weather.rain')
109
+ pub.send('sports.goal')
110
+ end
111
+
112
+ puts all.receive.body # => "weather.rain"
113
+ puts weather.receive.body # => "weather.rain"
114
+ puts all.receive.body # => "sports.goal"
115
+ # weather never receives "sports.goal"
116
+ end
117
+ ```
118
+
119
+ ### Push / Pull (Pipeline)
120
+
121
+ ```ruby
122
+ Sync do |task|
123
+ pull = NNG::Socket::Pull0.new
124
+ pull.listen('inproc://pipeline')
125
+
126
+ push = NNG::Socket::Push0.new
127
+ push.dial('inproc://pipeline')
128
+
129
+ task.async { push.send('work item') }
130
+ puts pull.receive.body # => "work item"
131
+ end
132
+ ```
133
+
134
+ ### Raw Mode Proxy
135
+
136
+ Raw mode sockets bypass the protocol state machine, enabling stateless message forwarding:
137
+
138
+ ```ruby
139
+ Sync do |task|
140
+ backend = NNG::Socket::Rep0.new
141
+ backend.listen('inproc://backend')
142
+
143
+ proxy_fe = NNG::Socket::Rep0.new(raw: true)
144
+ proxy_fe.listen('inproc://frontend')
145
+
146
+ proxy_be = NNG::Socket::Req0.new(raw: true)
147
+ proxy_be.dial('inproc://backend')
148
+
149
+ client = NNG::Socket::Req0.new
150
+ client.dial('inproc://frontend')
151
+
152
+ task.async do
153
+ msg = backend.receive
154
+ backend.send("processed: #{msg.body}")
155
+ end
156
+
157
+ task.async do
158
+ msg = proxy_fe.receive
159
+ proxy_be.forward(msg)
160
+ reply = proxy_be.receive
161
+ proxy_fe.forward(reply)
162
+ end
163
+
164
+ client.send('job')
165
+ puts client.receive.body # => "processed: job"
166
+ end
167
+ ```
168
+
169
+ ### TLS
170
+
171
+ Any socket type works over TLS — just use `tls+tcp://`. The [localhost](https://github.com/socketry/localhost) gem provides self-signed credentials for development:
172
+
173
+ ```ruby
174
+ require 'localhost'
175
+
176
+ authority = Localhost::Authority.fetch
177
+
178
+ Sync do |task|
179
+ rep = NNG::Socket::Rep0.new
180
+ rep.listen('tls+tcp://127.0.0.1:5556',
181
+ cert: authority.certificate, key: authority.key)
182
+
183
+ req = NNG::Socket::Req0.new
184
+ req.dial('tls+tcp://127.0.0.1:5556',
185
+ ca: authority.issuer.certificate, server_name: 'localhost')
186
+
187
+ task.async do
188
+ msg = rep.receive
189
+ rep.send("secure: #{msg.body}")
190
+ end
191
+
192
+ req.send('hello')
193
+ reply = req.receive
194
+ puts reply.body # => "secure: hello"
195
+ puts reply.pipe.tls_peer_cn # => "localhost"
196
+ end
197
+ ```
198
+
199
+ <details>
200
+ <summary>TLS option reference</summary>
201
+
202
+ **`#listen`**
203
+ | Option | Description |
204
+ |--------|-------------|
205
+ | `cert:` | Server certificate (PEM string, `OpenSSL::X509::Certificate`, or `Pathname`) |
206
+ | `key:` | Private key (PEM string, `OpenSSL::PKey`, or `Pathname`) |
207
+ | `ca:` | CA certificate for client verification (mutual TLS) |
208
+ | `verify:` | Require client certificates (`false` by default) |
209
+
210
+ **`#dial`**
211
+ | Option | Description |
212
+ |--------|-------------|
213
+ | `ca:` | CA certificate to verify the server |
214
+ | `cert:` / `key:` | Client certificate (for mutual TLS) |
215
+ | `server_name:` | Expected server CN/SAN (defaults to host from URL) |
216
+ | `verify: false` | Skip server certificate verification |
217
+
218
+ </details>
219
+
220
+ ### Pipe Introspection
221
+
222
+ Received messages carry a pipe reference for connection-level metadata:
223
+
224
+ ```ruby
225
+ msg = rep.receive
226
+ pipe = msg.pipe
227
+
228
+ pipe.tls_verified? # => true
229
+ pipe.tls_peer_cn # => "localhost"
230
+ pipe.id # => 1
231
+ ```
232
+
233
+ ## Socket Options
234
+
235
+ ```ruby
236
+ sock = NNG::Socket::Req0.new
237
+ sock.name = 'my-socket'
238
+ sock.recv_timeout = 1.0 # seconds (default: nil = infinite)
239
+ sock.send_timeout = 1.0 # seconds (default: nil = infinite)
240
+ sock.recv_buffer = 128 # message count (default: protocol-specific)
241
+ sock.send_buffer = 128 # message count (default: protocol-specific)
242
+ sock.recv_max_size = 1_048_576 # bytes (default: 0 = no limit)
243
+ sock.reconnect_time = 0.1..30.0 # seconds min..max (default: 1.0..0.0)
244
+
245
+ sock.raw? # => false
246
+ sock.protocol_name # => "req"
247
+ sock.urls # => ["tcp://127.0.0.1:5555"]
248
+ ```
249
+
250
+ ## Protocols
251
+
252
+ | Protocol | Class | Direction |
253
+ |----------|-------|-----------|
254
+ | Pair v0 | `Pair0` | bidirectional |
255
+ | Pair v1 | `Pair1` | bidirectional |
256
+ | Request | `Req0` | send + receive |
257
+ | Reply | `Rep0` | receive + send |
258
+ | Publish | `Pub0` | send only |
259
+ | Subscribe | `Sub0` | receive only |
260
+ | Push | `Push0` | send only |
261
+ | Pull | `Pull0` | receive only |
262
+ | Survey | `Surveyor0` | send + receive |
263
+ | Respond | `Respondent0` | receive + send |
264
+ | Bus | `Bus0` | bidirectional |
265
+
266
+ All classes live under `NNG::Socket::` and support `raw: true` for raw mode.
267
+
268
+ ## Transports
269
+
270
+ | Transport | URL scheme | Notes |
271
+ |-----------|-----------|-------|
272
+ | In-process | `inproc://` | Fastest, same process only |
273
+ | IPC | `ipc://` | Unix domain sockets |
274
+ | Abstract | `abstract://` | Linux abstract namespace (no filesystem path) |
275
+ | TCP | `tcp://` | TCP/IP |
276
+ | TLS | `tls+tcp://` | TCP + TLS encryption (requires mbedTLS) |
277
+
278
+ ## Performance
279
+
280
+ Benchmarked with benchmark-ips on Linux x86_64 (NNG 1.10.0, Ruby 4.0.1 +YJIT):
281
+
282
+ #### Throughput (push/pull)
283
+
284
+ | | inproc | abstract | ipc | tcp |
285
+ |---|--------|----------|-----|-----|
286
+ | **Async** | 47.3k/s | 14.3k/s | 13.3k/s | 8.4k/s |
287
+ | **Threads** | 41.7k/s | 15.4k/s | 16.6k/s | 10.1k/s |
288
+
289
+ #### Latency (req/rep roundtrip)
290
+
291
+ | | inproc | abstract | ipc | tcp |
292
+ |---|--------|----------|-----|-----|
293
+ | **Async** | 57 µs | 180 µs | 155 µs | 204 µs |
294
+ | **Threads** | 106 µs | 118 µs | 134 µs | 151 µs |
295
+
296
+ Async fibers deliver 1.9x lower inproc latency thanks to cheap context switching. See [`bench/`](bench/) for full results and scripts.
297
+
298
+ ## Development
299
+
300
+ ```sh
301
+ bundle install
302
+ bundle exec rake compile
303
+ bundle exec rake test
304
+ ```
305
+
306
+ ## License
307
+
308
+ [MIT](LICENSE)
@@ -0,0 +1,43 @@
1
+ #include "rbnng.h"
2
+
3
+ typedef struct {
4
+ nng_socket s1;
5
+ nng_socket s2;
6
+ int rv;
7
+ } device_args_t;
8
+
9
+ static void *
10
+ device_without_gvl(void *arg)
11
+ {
12
+ device_args_t *a = arg;
13
+ a->rv = nng_device(a->s1, a->s2);
14
+ return NULL;
15
+ }
16
+
17
+ static VALUE
18
+ device_start(VALUE self, VALUE sock1, VALUE sock2)
19
+ {
20
+ rbnng_socket_t *s1, *s2;
21
+ TypedData_Get_Struct(sock1, rbnng_socket_t, &rbnng_socket_type, s1);
22
+ TypedData_Get_Struct(sock2, rbnng_socket_t, &rbnng_socket_type, s2);
23
+
24
+ if (!s1->initialized || !s2->initialized)
25
+ rb_raise(rb_eRuntimeError, "socket not initialized");
26
+
27
+ device_args_t args;
28
+ args.s1 = s1->socket;
29
+ args.s2 = s2->socket;
30
+
31
+ rb_thread_call_without_gvl(device_without_gvl, &args, RUBY_UBF_IO, NULL);
32
+
33
+ if (args.rv != 0)
34
+ raise_nng_error(args.rv);
35
+ return Qnil;
36
+ }
37
+
38
+ void
39
+ rbnng_device_init(VALUE nng_module)
40
+ {
41
+ VALUE mod = rb_define_module_under(nng_module, "Device");
42
+ rb_define_singleton_method(mod, "start", device_start, 2);
43
+ }