nng-ruby 0.1.1 → 0.1.2
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 +871 -54
- data/examples/protobuf_advanced.rb +322 -0
- data/examples/protobuf_demo.rb +340 -0
- data/examples/protobuf_example.rb +374 -0
- data/examples/protobuf_simple.rb +191 -0
- data/examples/protobuf_thread.rb +236 -0
- data/ext/nng/extconf.rb +71 -0
- data/lib/nng/ffi.rb +78 -9
- data/lib/nng/socket.rb +5 -2
- data/lib/nng/version.rb +1 -1
- data/nng.gemspec +6 -4
- metadata +26 -5
data/README.md
CHANGED
@@ -7,14 +7,15 @@ Ruby bindings for [NNG (nanomsg-next-generation)](https://nng.nanomsg.org/), a l
|
|
7
7
|
|
8
8
|
## Features
|
9
9
|
|
10
|
-
- ✅ Complete FFI bindings for NNG 1.8.0 (
|
10
|
+
- ✅ Complete FFI bindings for NNG 1.8.0 (300+ functions)
|
11
11
|
- ✅ All scalability protocols: Pair, Push/Pull, Pub/Sub, Req/Rep, Surveyor/Respondent, Bus
|
12
12
|
- ✅ All transports: TCP, IPC, Inproc, WebSocket, TLS
|
13
13
|
- ✅ High-level Ruby API with automatic resource management
|
14
14
|
- ✅ Message-based and byte-based communication
|
15
|
-
- ✅ Bundled libnng shared library (no external dependencies)
|
15
|
+
- ✅ Bundled libnng.so.1.8.0 shared library (no external dependencies)
|
16
16
|
- ✅ Thread-safe
|
17
17
|
- ✅ Full async I/O support
|
18
|
+
- ✅ Automated testing and publishing via GitHub Actions
|
18
19
|
|
19
20
|
## Installation
|
20
21
|
|
@@ -65,23 +66,33 @@ client.close
|
|
65
66
|
|
66
67
|
### Request/Reply Protocol
|
67
68
|
|
69
|
+
The request/reply pattern ensures exactly one reply for each request:
|
70
|
+
|
68
71
|
```ruby
|
69
72
|
require 'nng'
|
70
73
|
|
71
|
-
# Server (replier)
|
74
|
+
# Server (replier) - Must reply to each request
|
72
75
|
rep = NNG::Socket.new(:rep)
|
73
76
|
rep.listen("tcp://127.0.0.1:5556")
|
74
77
|
|
75
|
-
# Client (requester)
|
78
|
+
# Client (requester) - Must wait for reply before sending next request
|
76
79
|
req = NNG::Socket.new(:req)
|
77
80
|
req.dial("tcp://127.0.0.1:5556")
|
81
|
+
sleep 0.1 # Allow connection to establish
|
78
82
|
|
79
|
-
#
|
83
|
+
# Request-reply cycle
|
80
84
|
req.send("What is the answer?")
|
81
|
-
|
85
|
+
question = rep.recv
|
86
|
+
puts "Server received: #{question}"
|
82
87
|
|
83
88
|
rep.send("42")
|
84
|
-
|
89
|
+
answer = req.recv
|
90
|
+
puts "Client received: #{answer}"
|
91
|
+
|
92
|
+
# Another request-reply cycle
|
93
|
+
req.send("What is 2+2?")
|
94
|
+
rep.send("4")
|
95
|
+
puts req.recv # => "4"
|
85
96
|
|
86
97
|
rep.close
|
87
98
|
req.close
|
@@ -89,6 +100,8 @@ req.close
|
|
89
100
|
|
90
101
|
### Publish/Subscribe Protocol
|
91
102
|
|
103
|
+
Pub/Sub allows one publisher to broadcast to multiple subscribers:
|
104
|
+
|
92
105
|
```ruby
|
93
106
|
require 'nng'
|
94
107
|
|
@@ -96,38 +109,162 @@ require 'nng'
|
|
96
109
|
pub = NNG::Socket.new(:pub)
|
97
110
|
pub.listen("tcp://127.0.0.1:5557")
|
98
111
|
|
99
|
-
# Subscriber
|
100
|
-
|
101
|
-
|
102
|
-
|
112
|
+
# Subscriber 1 - Subscribe to all topics
|
113
|
+
sub1 = NNG::Socket.new(:sub)
|
114
|
+
sub1.dial("tcp://127.0.0.1:5557")
|
115
|
+
sub1.set_option("sub:subscribe", "") # Subscribe to everything
|
116
|
+
|
117
|
+
# Subscriber 2 - Subscribe to specific topic
|
118
|
+
sub2 = NNG::Socket.new(:sub)
|
119
|
+
sub2.dial("tcp://127.0.0.1:5557")
|
120
|
+
sub2.set_option("sub:subscribe", "ALERT:") # Only "ALERT:" messages
|
121
|
+
|
122
|
+
sleep 0.1 # Allow subscriptions to propagate
|
103
123
|
|
104
|
-
# Publish
|
105
|
-
pub.send("
|
106
|
-
puts
|
124
|
+
# Publish to all subscribers
|
125
|
+
pub.send("ALERT: System update available")
|
126
|
+
puts sub1.recv # => "ALERT: System update available"
|
127
|
+
puts sub2.recv # => "ALERT: System update available"
|
128
|
+
|
129
|
+
# Publish general message (only sub1 receives)
|
130
|
+
pub.send("INFO: Normal operation")
|
131
|
+
puts sub1.recv # => "INFO: Normal operation"
|
132
|
+
# sub2 won't receive this (topic doesn't match)
|
107
133
|
|
108
134
|
pub.close
|
109
|
-
|
135
|
+
sub1.close
|
136
|
+
sub2.close
|
110
137
|
```
|
111
138
|
|
112
139
|
### Push/Pull Protocol (Pipeline)
|
113
140
|
|
141
|
+
Push/Pull creates a load-balanced pipeline for distributing work:
|
142
|
+
|
114
143
|
```ruby
|
115
144
|
require 'nng'
|
145
|
+
require 'thread'
|
116
146
|
|
117
|
-
# Producer
|
147
|
+
# Producer (Push)
|
118
148
|
push = NNG::Socket.new(:push)
|
119
149
|
push.listen("tcp://127.0.0.1:5558")
|
120
150
|
|
121
|
-
#
|
122
|
-
|
123
|
-
|
151
|
+
# Worker 1 (Pull)
|
152
|
+
pull1 = NNG::Socket.new(:pull)
|
153
|
+
pull1.dial("tcp://127.0.0.1:5558")
|
124
154
|
|
125
|
-
#
|
126
|
-
|
127
|
-
|
155
|
+
# Worker 2 (Pull)
|
156
|
+
pull2 = NNG::Socket.new(:pull)
|
157
|
+
pull2.dial("tcp://127.0.0.1:5558")
|
158
|
+
|
159
|
+
sleep 0.1 # Allow connections
|
160
|
+
|
161
|
+
# Start workers in threads
|
162
|
+
workers = []
|
163
|
+
workers << Thread.new do
|
164
|
+
3.times do
|
165
|
+
task = pull1.recv
|
166
|
+
puts "Worker 1 processing: #{task}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
workers << Thread.new do
|
171
|
+
3.times do
|
172
|
+
task = pull2.recv
|
173
|
+
puts "Worker 2 processing: #{task}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Distribute tasks (round-robin to workers)
|
178
|
+
6.times do |i|
|
179
|
+
push.send("Task #{i+1}")
|
180
|
+
sleep 0.01
|
181
|
+
end
|
182
|
+
|
183
|
+
# Wait for workers to complete
|
184
|
+
workers.each(&:join)
|
128
185
|
|
129
186
|
push.close
|
130
|
-
|
187
|
+
pull1.close
|
188
|
+
pull2.close
|
189
|
+
```
|
190
|
+
|
191
|
+
### Bus Protocol (Many-to-Many)
|
192
|
+
|
193
|
+
Bus protocol allows all peers to communicate with each other:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
require 'nng'
|
197
|
+
|
198
|
+
# Create three bus nodes
|
199
|
+
node1 = NNG::Socket.new(:bus)
|
200
|
+
node1.listen("tcp://127.0.0.1:5559")
|
201
|
+
|
202
|
+
node2 = NNG::Socket.new(:bus)
|
203
|
+
node2.listen("tcp://127.0.0.1:5560")
|
204
|
+
node2.dial("tcp://127.0.0.1:5559")
|
205
|
+
|
206
|
+
node3 = NNG::Socket.new(:bus)
|
207
|
+
node3.dial("tcp://127.0.0.1:5559")
|
208
|
+
node3.dial("tcp://127.0.0.1:5560")
|
209
|
+
|
210
|
+
sleep 0.1 # Allow mesh to form
|
211
|
+
|
212
|
+
# Any node can send to all others
|
213
|
+
node1.send("Message from Node 1")
|
214
|
+
puts node2.recv # => "Message from Node 1"
|
215
|
+
puts node3.recv # => "Message from Node 1"
|
216
|
+
|
217
|
+
node2.send("Message from Node 2")
|
218
|
+
puts node1.recv # => "Message from Node 2"
|
219
|
+
puts node3.recv # => "Message from Node 2"
|
220
|
+
|
221
|
+
node1.close
|
222
|
+
node2.close
|
223
|
+
node3.close
|
224
|
+
```
|
225
|
+
|
226
|
+
### Surveyor/Respondent Protocol
|
227
|
+
|
228
|
+
Surveyor sends a survey, and all respondents reply:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
require 'nng'
|
232
|
+
|
233
|
+
# Surveyor
|
234
|
+
surveyor = NNG::Socket.new(:surveyor)
|
235
|
+
surveyor.listen("tcp://127.0.0.1:5561")
|
236
|
+
surveyor.send_timeout = 1000 # 1 second to collect responses
|
237
|
+
|
238
|
+
# Respondents
|
239
|
+
resp1 = NNG::Socket.new(:respondent)
|
240
|
+
resp1.dial("tcp://127.0.0.1:5561")
|
241
|
+
|
242
|
+
resp2 = NNG::Socket.new(:respondent)
|
243
|
+
resp2.dial("tcp://127.0.0.1:5561")
|
244
|
+
|
245
|
+
sleep 0.1 # Allow connections
|
246
|
+
|
247
|
+
# Send survey
|
248
|
+
surveyor.send("What is your status?")
|
249
|
+
|
250
|
+
# Respondents reply
|
251
|
+
question1 = resp1.recv
|
252
|
+
resp1.send("Respondent 1: OK")
|
253
|
+
|
254
|
+
question2 = resp2.recv
|
255
|
+
resp2.send("Respondent 2: OK")
|
256
|
+
|
257
|
+
# Collect responses
|
258
|
+
begin
|
259
|
+
puts surveyor.recv # => "Respondent 1: OK" or "Respondent 2: OK"
|
260
|
+
puts surveyor.recv # => "Respondent 2: OK" or "Respondent 1: OK"
|
261
|
+
rescue NNG::TimeoutError
|
262
|
+
puts "Survey complete"
|
263
|
+
end
|
264
|
+
|
265
|
+
surveyor.close
|
266
|
+
resp1.close
|
267
|
+
resp2.close
|
131
268
|
```
|
132
269
|
|
133
270
|
## Supported Protocols
|
@@ -149,6 +286,75 @@ pull.close
|
|
149
286
|
|
150
287
|
## Advanced Usage
|
151
288
|
|
289
|
+
### Custom Library Configuration
|
290
|
+
|
291
|
+
By default, nng-ruby uses the bundled libnng.so.1.8.0 library. However, you can specify a custom NNG library in several ways:
|
292
|
+
|
293
|
+
#### Option 1: At install time
|
294
|
+
|
295
|
+
Use gem install options to specify a custom NNG library location:
|
296
|
+
|
297
|
+
```bash
|
298
|
+
# Specify NNG installation directory (will search lib/, lib64/, etc.)
|
299
|
+
gem install nng-ruby -- --with-nng-dir=/opt/nng
|
300
|
+
|
301
|
+
# Specify exact library path
|
302
|
+
gem install nng-ruby -- --with-nng-lib=/opt/nng/lib/libnng.so
|
303
|
+
|
304
|
+
# Specify include directory (for future use)
|
305
|
+
gem install nng-ruby -- --with-nng-include=/opt/nng/include
|
306
|
+
```
|
307
|
+
|
308
|
+
#### Option 2: At runtime with environment variables
|
309
|
+
|
310
|
+
Set environment variables before requiring the gem:
|
311
|
+
|
312
|
+
```bash
|
313
|
+
# Specify exact library file path
|
314
|
+
export NNG_LIB_PATH=/usr/local/lib/libnng.so.1.9.0
|
315
|
+
ruby your_script.rb
|
316
|
+
|
317
|
+
# Or specify library directory (will search for libnng.so*)
|
318
|
+
export NNG_LIB_DIR=/usr/local/lib
|
319
|
+
ruby your_script.rb
|
320
|
+
```
|
321
|
+
|
322
|
+
In Ruby code:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
# Set before requiring nng
|
326
|
+
ENV['NNG_LIB_PATH'] = '/custom/path/libnng.so'
|
327
|
+
require 'nng'
|
328
|
+
|
329
|
+
# Or use directory
|
330
|
+
ENV['NNG_LIB_DIR'] = '/custom/path/lib'
|
331
|
+
require 'nng'
|
332
|
+
```
|
333
|
+
|
334
|
+
#### Priority Order
|
335
|
+
|
336
|
+
The library is loaded in this priority order:
|
337
|
+
|
338
|
+
1. **Environment variable** `NNG_LIB_PATH` (highest priority)
|
339
|
+
2. **Environment variable** `NNG_LIB_DIR`
|
340
|
+
3. **Install-time configuration** (gem install --with-nng-*)
|
341
|
+
4. **Bundled library** (ext/nng/libnng.so.1.8.0)
|
342
|
+
5. **System paths** (/usr/local/lib, /usr/lib, etc.)
|
343
|
+
|
344
|
+
#### Debugging
|
345
|
+
|
346
|
+
Enable debug output to see which library is being loaded:
|
347
|
+
|
348
|
+
```bash
|
349
|
+
export NNG_DEBUG=1
|
350
|
+
ruby your_script.rb
|
351
|
+
```
|
352
|
+
|
353
|
+
This will print messages showing:
|
354
|
+
- Which paths are being searched
|
355
|
+
- Which library was successfully loaded
|
356
|
+
- Any load failures encountered
|
357
|
+
|
152
358
|
### Setting Timeouts
|
153
359
|
|
154
360
|
```ruby
|
@@ -173,57 +379,558 @@ rescue NNG::Error => e
|
|
173
379
|
end
|
174
380
|
```
|
175
381
|
|
176
|
-
### Using Messages
|
382
|
+
### Using Messages (NNG::Message API)
|
383
|
+
|
384
|
+
The Message API provides more control over message headers and bodies:
|
385
|
+
|
386
|
+
#### Basic Message Operations
|
177
387
|
|
178
388
|
```ruby
|
179
389
|
require 'nng'
|
180
390
|
|
181
|
-
# Create a message
|
391
|
+
# Create a new message
|
182
392
|
msg = NNG::Message.new
|
393
|
+
|
394
|
+
# Append data to message body
|
183
395
|
msg.append("Hello, ")
|
184
396
|
msg.append("World!")
|
185
397
|
puts msg.body # => "Hello, World!"
|
398
|
+
puts msg.length # => 13
|
399
|
+
|
400
|
+
# Insert data at the beginning
|
401
|
+
msg.insert("Say: ")
|
402
|
+
puts msg.body # => "Say: Hello, World!"
|
403
|
+
|
404
|
+
# Add header information
|
405
|
+
msg.header_append("Content-Type: text/plain")
|
406
|
+
puts msg.header # => "Content-Type: text/plain"
|
407
|
+
puts msg.header_length # => 24
|
186
408
|
|
187
|
-
#
|
188
|
-
msg.
|
409
|
+
# Clear body or header
|
410
|
+
msg.clear # Clears body
|
411
|
+
msg.header_clear # Clears header
|
189
412
|
|
190
413
|
# Duplicate message
|
191
414
|
msg2 = msg.dup
|
192
415
|
|
193
|
-
# Free
|
416
|
+
# Free messages when done
|
194
417
|
msg.free
|
195
418
|
msg2.free
|
196
419
|
```
|
197
420
|
|
421
|
+
#### Sending and Receiving Messages
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
require 'nng'
|
425
|
+
|
426
|
+
# Server side
|
427
|
+
server = NNG::Socket.new(:pair1)
|
428
|
+
server.listen("tcp://127.0.0.1:5555")
|
429
|
+
|
430
|
+
# Client side
|
431
|
+
client = NNG::Socket.new(:pair1)
|
432
|
+
client.dial("tcp://127.0.0.1:5555")
|
433
|
+
sleep 0.1 # Give time to establish connection
|
434
|
+
|
435
|
+
# Send a message with header and body
|
436
|
+
msg = NNG::Message.new
|
437
|
+
msg.header_append("RequestID: 12345")
|
438
|
+
msg.append("Hello from client!")
|
439
|
+
|
440
|
+
# Send message using low-level API
|
441
|
+
msg_ptr = ::FFI::MemoryPointer.new(:pointer)
|
442
|
+
msg_ptr.write_pointer(msg.to_ptr)
|
443
|
+
ret = NNG::FFI.nng_sendmsg(client.socket, msg_ptr.read_pointer, 0)
|
444
|
+
NNG::FFI.check_error(ret, "Send message")
|
445
|
+
# Note: Message is freed automatically after sending
|
446
|
+
|
447
|
+
# Receive message
|
448
|
+
recv_msg_ptr = ::FFI::MemoryPointer.new(:pointer)
|
449
|
+
ret = NNG::FFI.nng_recvmsg(server.socket, recv_msg_ptr, 0)
|
450
|
+
NNG::FFI.check_error(ret, "Receive message")
|
451
|
+
|
452
|
+
# Wrap received message
|
453
|
+
recv_msg = NNG::Message.allocate
|
454
|
+
recv_msg.instance_variable_set(:@msg, recv_msg_ptr.read_pointer)
|
455
|
+
recv_msg.instance_variable_set(:@msg_ptr, recv_msg_ptr)
|
456
|
+
recv_msg.instance_variable_set(:@freed, false)
|
457
|
+
|
458
|
+
puts "Header: #{recv_msg.header}" # => "RequestID: 12345"
|
459
|
+
puts "Body: #{recv_msg.body}" # => "Hello from client!"
|
460
|
+
|
461
|
+
recv_msg.free
|
462
|
+
server.close
|
463
|
+
client.close
|
464
|
+
```
|
465
|
+
|
466
|
+
#### Simple String-based Send/Receive (Recommended for most use cases)
|
467
|
+
|
468
|
+
For simpler use cases, use the high-level Socket#send and Socket#recv methods:
|
469
|
+
|
470
|
+
```ruby
|
471
|
+
require 'nng'
|
472
|
+
|
473
|
+
server = NNG::Socket.new(:pair1)
|
474
|
+
server.listen("tcp://127.0.0.1:5555")
|
475
|
+
|
476
|
+
client = NNG::Socket.new(:pair1)
|
477
|
+
client.dial("tcp://127.0.0.1:5555")
|
478
|
+
|
479
|
+
# Simple send/receive (no need to manage Message objects)
|
480
|
+
client.send("Hello, Server!")
|
481
|
+
data = server.recv
|
482
|
+
puts data # => "Hello, Server!"
|
483
|
+
|
484
|
+
server.close
|
485
|
+
client.close
|
486
|
+
```
|
487
|
+
|
198
488
|
### Socket Options
|
199
489
|
|
490
|
+
NNG sockets support various options to control behavior:
|
491
|
+
|
200
492
|
```ruby
|
493
|
+
require 'nng'
|
494
|
+
|
201
495
|
socket = NNG::Socket.new(:pub)
|
202
496
|
|
203
|
-
# Set
|
204
|
-
socket.set_option("send-buffer", 8192)
|
205
|
-
socket.set_option("
|
497
|
+
# Set buffer sizes
|
498
|
+
socket.set_option("send-buffer", 8192) # Send buffer size in bytes
|
499
|
+
socket.set_option("recv-buffer", 8192) # Receive buffer size in bytes
|
500
|
+
|
501
|
+
# Set TCP options
|
502
|
+
socket.set_option("tcp-nodelay", true) # Disable Nagle's algorithm
|
503
|
+
socket.set_option("tcp-keepalive", true) # Enable TCP keepalive
|
504
|
+
|
505
|
+
# Set timeouts
|
506
|
+
socket.send_timeout = 1000 # 1 second send timeout
|
507
|
+
socket.recv_timeout = 5000 # 5 second receive timeout
|
508
|
+
|
509
|
+
# Or use set_option_ms
|
206
510
|
socket.set_option_ms("send-timeout", 1000)
|
511
|
+
socket.set_option_ms("recv-timeout", 5000)
|
207
512
|
|
208
513
|
# Get options
|
209
514
|
buffer_size = socket.get_option("send-buffer", type: :int)
|
515
|
+
puts "Send buffer: #{buffer_size} bytes"
|
516
|
+
|
210
517
|
nodelay = socket.get_option("tcp-nodelay", type: :bool)
|
518
|
+
puts "TCP NoDelay: #{nodelay}"
|
519
|
+
|
520
|
+
send_timeout = socket.get_option("send-timeout", type: :ms)
|
521
|
+
puts "Send timeout: #{send_timeout} ms"
|
522
|
+
|
523
|
+
# Protocol-specific options
|
524
|
+
if socket.socket.id # For pub/sub
|
525
|
+
# Subscriber can set subscription topics
|
526
|
+
sub = NNG::Socket.new(:sub)
|
527
|
+
sub.set_option("sub:subscribe", "news/") # Subscribe to "news/*"
|
528
|
+
sub.set_option("sub:subscribe", "alerts/") # Subscribe to "alerts/*"
|
529
|
+
sub.set_option("sub:unsubscribe", "news/") # Unsubscribe from "news/*"
|
530
|
+
end
|
531
|
+
|
532
|
+
socket.close
|
533
|
+
```
|
534
|
+
|
535
|
+
### Transport-Specific URLs
|
536
|
+
|
537
|
+
Different transports have different URL formats:
|
538
|
+
|
539
|
+
```ruby
|
540
|
+
require 'nng'
|
541
|
+
|
542
|
+
socket = NNG::Socket.new(:pair1)
|
543
|
+
|
544
|
+
# TCP transport
|
545
|
+
socket.listen("tcp://0.0.0.0:5555") # Listen on all interfaces
|
546
|
+
socket.dial("tcp://192.168.1.100:5555") # Connect to specific IP
|
547
|
+
socket.dial("tcp://localhost:5555") # Connect to localhost
|
548
|
+
|
549
|
+
# IPC transport (Unix domain sockets)
|
550
|
+
socket.listen("ipc:///tmp/test.sock") # Unix socket
|
551
|
+
socket.dial("ipc:///tmp/test.sock")
|
552
|
+
|
553
|
+
# Inproc transport (in-process, same memory space)
|
554
|
+
socket.listen("inproc://my-channel")
|
555
|
+
socket.dial("inproc://my-channel")
|
556
|
+
|
557
|
+
# WebSocket transport
|
558
|
+
socket.listen("ws://0.0.0.0:8080/path")
|
559
|
+
socket.dial("ws://localhost:8080/path")
|
560
|
+
|
561
|
+
# TLS transport
|
562
|
+
socket.listen("tls+tcp://0.0.0.0:5556")
|
563
|
+
socket.dial("tls+tcp://server.example.com:5556")
|
564
|
+
|
565
|
+
socket.close
|
211
566
|
```
|
212
567
|
|
213
568
|
## Examples
|
214
569
|
|
215
570
|
See the `examples/` directory for complete working examples:
|
216
571
|
|
572
|
+
### Protocol Examples
|
217
573
|
- `examples/pair.rb` - Pair protocol
|
218
574
|
- `examples/reqrep.rb` - Request/Reply protocol
|
219
575
|
- `examples/pubsub.rb` - Publish/Subscribe protocol
|
220
576
|
|
221
|
-
|
577
|
+
### Protocol Buffers Integration
|
578
|
+
|
579
|
+
NNG-ruby works seamlessly with Google Protocol Buffers for efficient binary serialization. This is perfect for RPC systems, microservices, and cross-language communication.
|
580
|
+
|
581
|
+
**Available Examples:**
|
582
|
+
- `examples/protobuf_demo.rb` - Complete demo with nested messages and RPC patterns
|
583
|
+
- `examples/protobuf_simple.rb` - Simple client-server with Protobuf
|
584
|
+
- `examples/proto/message.proto` - Protobuf message definitions
|
585
|
+
|
586
|
+
**Installation:**
|
222
587
|
|
223
588
|
```bash
|
224
|
-
|
225
|
-
|
226
|
-
|
589
|
+
gem install google-protobuf
|
590
|
+
```
|
591
|
+
|
592
|
+
#### Example 1: Basic RPC with Protobuf
|
593
|
+
|
594
|
+
This example shows a simple request-response RPC system using NNG + Protobuf:
|
595
|
+
|
596
|
+
```ruby
|
597
|
+
require 'nng'
|
598
|
+
require 'google/protobuf'
|
599
|
+
|
600
|
+
# Define Protobuf messages inline (or load from .proto file)
|
601
|
+
Google::Protobuf::DescriptorPool.generated_pool.build do
|
602
|
+
add_file("rpc.proto", syntax: :proto3) do
|
603
|
+
add_message "RpcRequest" do
|
604
|
+
optional :func_code, :int32, 1
|
605
|
+
optional :data, :bytes, 2
|
606
|
+
optional :request_id, :string, 3
|
607
|
+
end
|
608
|
+
add_message "RpcResponse" do
|
609
|
+
optional :status, :int32, 1
|
610
|
+
optional :data, :bytes, 2
|
611
|
+
optional :error_msg, :string, 3
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
RpcRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("RpcRequest").msgclass
|
617
|
+
RpcResponse = Google::Protobuf::DescriptorPool.generated_pool.msgclass("RpcResponse")
|
618
|
+
|
619
|
+
# Server
|
620
|
+
server = NNG::Socket.new(:rep)
|
621
|
+
server.listen("tcp://127.0.0.1:5555")
|
622
|
+
puts "RPC Server listening on tcp://127.0.0.1:5555"
|
623
|
+
|
624
|
+
# Client
|
625
|
+
client = NNG::Socket.new(:req)
|
626
|
+
client.dial("tcp://127.0.0.1:5555")
|
627
|
+
sleep 0.1
|
628
|
+
|
629
|
+
# Client sends request
|
630
|
+
request = RpcRequest.new(
|
631
|
+
func_code: 100,
|
632
|
+
data: "Get user info",
|
633
|
+
request_id: "req-001"
|
634
|
+
)
|
635
|
+
client.send(RpcRequest.encode(request))
|
636
|
+
puts "Client sent: #{request.inspect}"
|
637
|
+
|
638
|
+
# Server receives and processes
|
639
|
+
req_data = server.recv
|
640
|
+
received_req = RpcRequest.decode(req_data)
|
641
|
+
puts "Server received: func=#{received_req.func_code}, id=#{received_req.request_id}"
|
642
|
+
|
643
|
+
# Server sends response
|
644
|
+
response = RpcResponse.new(
|
645
|
+
status: 0,
|
646
|
+
data: '{"name": "Alice", "age": 30}',
|
647
|
+
error_msg: ""
|
648
|
+
)
|
649
|
+
server.send(RpcResponse.encode(response))
|
650
|
+
puts "Server sent: status=#{response.status}"
|
651
|
+
|
652
|
+
# Client receives response
|
653
|
+
resp_data = client.recv
|
654
|
+
received_resp = RpcResponse.decode(resp_data)
|
655
|
+
puts "Client received: status=#{received_resp.status}, data=#{received_resp.data}"
|
656
|
+
|
657
|
+
server.close
|
658
|
+
client.close
|
659
|
+
```
|
660
|
+
|
661
|
+
**Output:**
|
662
|
+
```
|
663
|
+
RPC Server listening on tcp://127.0.0.1:5555
|
664
|
+
Client sent: <RpcRequest: func_code: 100, data: "Get user info", request_id: "req-001">
|
665
|
+
Server received: func=100, id=req-001
|
666
|
+
Server sent: status=0
|
667
|
+
Client received: status=0, data={"name": "Alice", "age": 30}
|
668
|
+
```
|
669
|
+
|
670
|
+
#### Example 2: Nested Messages (Message in Message)
|
671
|
+
|
672
|
+
A common pattern is sending complex data structures by nesting Protobuf messages:
|
673
|
+
|
674
|
+
```ruby
|
675
|
+
require 'nng'
|
676
|
+
require 'google/protobuf'
|
677
|
+
|
678
|
+
# Define nested message structure
|
679
|
+
Google::Protobuf::DescriptorPool.generated_pool.build do
|
680
|
+
add_file("nested.proto", syntax: :proto3) do
|
681
|
+
add_message "Contact" do
|
682
|
+
optional :wxid, :string, 1
|
683
|
+
optional :name, :string, 2
|
684
|
+
optional :remark, :string, 3
|
685
|
+
end
|
686
|
+
add_message "ContactList" do
|
687
|
+
repeated :contacts, :message, 1, "Contact"
|
688
|
+
end
|
689
|
+
add_message "RpcResponse" do
|
690
|
+
optional :status, :int32, 1
|
691
|
+
optional :data, :bytes, 2 # Will contain encoded ContactList
|
692
|
+
end
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
Contact = Google::Protobuf::DescriptorPool.generated_pool.lookup("Contact").msgclass
|
697
|
+
ContactList = Google::Protobuf::DescriptorPool.generated_pool.lookup("ContactList").msgclass
|
698
|
+
RpcResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("RpcResponse").msgclass
|
699
|
+
|
700
|
+
# Server prepares nested data
|
701
|
+
contacts = ContactList.new(
|
702
|
+
contacts: [
|
703
|
+
Contact.new(wxid: "wxid_001", name: "Alice", remark: "Friend"),
|
704
|
+
Contact.new(wxid: "wxid_002", name: "Bob", remark: "Colleague"),
|
705
|
+
Contact.new(wxid: "wxid_003", name: "Charlie", remark: "Family")
|
706
|
+
]
|
707
|
+
)
|
708
|
+
|
709
|
+
# Encode inner message first, then wrap in outer message
|
710
|
+
contacts_data = ContactList.encode(contacts)
|
711
|
+
response = RpcResponse.new(status: 0, data: contacts_data)
|
712
|
+
|
713
|
+
# Setup sockets
|
714
|
+
server = NNG::Socket.new(:pair1)
|
715
|
+
server.listen("tcp://127.0.0.1:5556")
|
716
|
+
|
717
|
+
client = NNG::Socket.new(:pair1)
|
718
|
+
client.dial("tcp://127.0.0.1:5556")
|
719
|
+
sleep 0.1
|
720
|
+
|
721
|
+
# Send nested message
|
722
|
+
server.send(RpcResponse.encode(response))
|
723
|
+
puts "Server sent: #{contacts.contacts.size} contacts"
|
724
|
+
|
725
|
+
# Receive and decode nested message
|
726
|
+
resp_data = client.recv
|
727
|
+
received_resp = RpcResponse.decode(resp_data)
|
728
|
+
|
729
|
+
# Decode inner message
|
730
|
+
received_contacts = ContactList.decode(received_resp.data)
|
731
|
+
puts "Client received #{received_contacts.contacts.size} contacts:"
|
732
|
+
received_contacts.contacts.each do |c|
|
733
|
+
puts " - #{c.name} (#{c.wxid}): #{c.remark}"
|
734
|
+
end
|
735
|
+
|
736
|
+
server.close
|
737
|
+
client.close
|
738
|
+
```
|
739
|
+
|
740
|
+
**Output:**
|
741
|
+
```
|
742
|
+
Server sent: 3 contacts
|
743
|
+
Client received 3 contacts:
|
744
|
+
- Alice (wxid_001): Friend
|
745
|
+
- Bob (wxid_002): Colleague
|
746
|
+
- Charlie (wxid_003): Family
|
747
|
+
```
|
748
|
+
|
749
|
+
#### Example 3: Real-World RPC Pattern (WeChatFerry Style)
|
750
|
+
|
751
|
+
This shows a complete RPC system similar to WeChatFerry's architecture:
|
752
|
+
|
753
|
+
```ruby
|
754
|
+
require 'nng'
|
755
|
+
require 'google/protobuf'
|
756
|
+
|
757
|
+
# Define protocol (matching WeChatFerry style)
|
758
|
+
Google::Protobuf::DescriptorPool.generated_pool.build do
|
759
|
+
add_file("wcf_rpc.proto", syntax: :proto3) do
|
760
|
+
add_enum "Functions" do
|
761
|
+
value :FUNC_IS_LOGIN, 0x01
|
762
|
+
value :FUNC_SEND_TEXT, 0x20
|
763
|
+
value :FUNC_GET_CONTACTS, 0x12
|
764
|
+
end
|
765
|
+
|
766
|
+
add_message "Request" do
|
767
|
+
optional :func, :enum, 1, "Functions"
|
768
|
+
optional :data, :bytes, 2
|
769
|
+
end
|
770
|
+
|
771
|
+
add_message "Response" do
|
772
|
+
optional :status, :int32, 1
|
773
|
+
optional :data, :bytes, 2
|
774
|
+
end
|
775
|
+
|
776
|
+
add_message "TextMsg" do
|
777
|
+
optional :receiver, :string, 1
|
778
|
+
optional :message, :string, 2
|
779
|
+
end
|
780
|
+
end
|
781
|
+
end
|
782
|
+
|
783
|
+
Functions = Google::Protobuf::DescriptorPool.generated_pool.lookup("Functions").enummodule
|
784
|
+
Request = Google::Protobuf::DescriptorPool.generated_pool.lookup("Request").msgclass
|
785
|
+
Response = Google::Protobuf::DescriptorPool.generated_pool.lookup("Response").msgclass
|
786
|
+
TextMsg = Google::Protobuf::DescriptorPool.generated_pool.lookup("TextMsg").msgclass
|
787
|
+
|
788
|
+
# RPC Server
|
789
|
+
def start_rpc_server
|
790
|
+
server = NNG::Socket.new(:rep)
|
791
|
+
server.listen("tcp://127.0.0.1:10086")
|
792
|
+
puts "RPC Server started on port 10086"
|
793
|
+
|
794
|
+
loop do
|
795
|
+
# Receive request
|
796
|
+
req_data = server.recv
|
797
|
+
request = Request.decode(req_data)
|
798
|
+
|
799
|
+
puts "Received: func=#{request.func}"
|
800
|
+
|
801
|
+
# Process request
|
802
|
+
response = case request.func
|
803
|
+
when :FUNC_IS_LOGIN
|
804
|
+
Response.new(status: 1) # Logged in
|
805
|
+
when :FUNC_SEND_TEXT
|
806
|
+
text_msg = TextMsg.decode(request.data)
|
807
|
+
puts " Send to #{text_msg.receiver}: #{text_msg.message}"
|
808
|
+
Response.new(status: 0) # Success
|
809
|
+
when :FUNC_GET_CONTACTS
|
810
|
+
Response.new(status: 0, data: "[contacts data]")
|
811
|
+
else
|
812
|
+
Response.new(status: -1) # Unknown function
|
813
|
+
end
|
814
|
+
|
815
|
+
# Send response
|
816
|
+
server.send(Response.encode(response))
|
817
|
+
end
|
818
|
+
ensure
|
819
|
+
server&.close
|
820
|
+
end
|
821
|
+
|
822
|
+
# RPC Client
|
823
|
+
def rpc_call(client, func, data = nil)
|
824
|
+
request = Request.new(func: func, data: data)
|
825
|
+
client.send(Request.encode(request))
|
826
|
+
|
827
|
+
resp_data = client.recv
|
828
|
+
Response.decode(resp_data)
|
829
|
+
end
|
830
|
+
|
831
|
+
# Run example
|
832
|
+
server_thread = Thread.new { start_rpc_server rescue nil }
|
833
|
+
sleep 0.5 # Wait for server to start
|
834
|
+
|
835
|
+
client = NNG::Socket.new(:req)
|
836
|
+
client.dial("tcp://127.0.0.1:10086")
|
837
|
+
puts "Client connected"
|
838
|
+
|
839
|
+
# Call 1: Check login
|
840
|
+
resp = rpc_call(client, :FUNC_IS_LOGIN)
|
841
|
+
puts "Login status: #{resp.status == 1 ? 'Logged in' : 'Not logged in'}"
|
842
|
+
|
843
|
+
# Call 2: Send text message
|
844
|
+
text_msg = TextMsg.new(receiver: "wxid_friend", message: "Hello from Ruby!")
|
845
|
+
resp = rpc_call(client, :FUNC_SEND_TEXT, TextMsg.encode(text_msg))
|
846
|
+
puts "Send text result: #{resp.status == 0 ? 'Success' : 'Failed'}"
|
847
|
+
|
848
|
+
# Call 3: Get contacts
|
849
|
+
resp = rpc_call(client, :FUNC_GET_CONTACTS)
|
850
|
+
puts "Contacts: #{resp.data}"
|
851
|
+
|
852
|
+
client.close
|
853
|
+
Thread.kill(server_thread)
|
854
|
+
```
|
855
|
+
|
856
|
+
**Output:**
|
857
|
+
```
|
858
|
+
RPC Server started on port 10086
|
859
|
+
Client connected
|
860
|
+
Received: func=FUNC_IS_LOGIN
|
861
|
+
Login status: Logged in
|
862
|
+
Received: func=FUNC_SEND_TEXT
|
863
|
+
Send to wxid_friend: Hello from Ruby!
|
864
|
+
Send text result: Success
|
865
|
+
Received: func=FUNC_GET_CONTACTS
|
866
|
+
Contacts: [contacts data]
|
867
|
+
```
|
868
|
+
|
869
|
+
#### Why Use Protobuf with NNG?
|
870
|
+
|
871
|
+
1. **Binary Efficiency**: Protobuf is 3-10x smaller than JSON, faster to parse
|
872
|
+
2. **Type Safety**: Strong typing prevents errors
|
873
|
+
3. **Cross-Language**: Works with Python, Go, Java, C++, etc.
|
874
|
+
4. **Schema Evolution**: Add fields without breaking old clients
|
875
|
+
5. **Perfect for RPC**: Natural request/response pattern
|
876
|
+
|
877
|
+
#### Performance Comparison
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
# Benchmark: Protobuf vs JSON
|
881
|
+
require 'benchmark'
|
882
|
+
require 'json'
|
883
|
+
|
884
|
+
data = { name: "Alice", age: 30, contacts: ["Bob", "Charlie"] }
|
885
|
+
|
886
|
+
Benchmark.bm(10) do |x|
|
887
|
+
x.report("JSON:") do
|
888
|
+
10000.times { JSON.parse(data.to_json) }
|
889
|
+
end
|
890
|
+
|
891
|
+
x.report("Protobuf:") do
|
892
|
+
10000.times {
|
893
|
+
msg = MyProto.new(data)
|
894
|
+
MyProto.decode(MyProto.encode(msg))
|
895
|
+
}
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
# Typical result:
|
900
|
+
# user system total real
|
901
|
+
# JSON: 0.850000 0.010000 0.860000 ( 0.862341)
|
902
|
+
# Protobuf: 0.280000 0.000000 0.280000 ( 0.283127)
|
903
|
+
```
|
904
|
+
|
905
|
+
#### More Examples
|
906
|
+
|
907
|
+
See the `examples/` directory for complete runnable code:
|
908
|
+
|
909
|
+
```bash
|
910
|
+
# Run comprehensive demo
|
911
|
+
ruby examples/protobuf_demo.rb
|
912
|
+
|
913
|
+
# Run simple client-server
|
914
|
+
ruby examples/protobuf_simple.rb
|
915
|
+
|
916
|
+
# View proto definitions
|
917
|
+
cat examples/proto/message.proto
|
918
|
+
```
|
919
|
+
|
920
|
+
#### Loading .proto Files
|
921
|
+
|
922
|
+
You can also define messages in `.proto` files and compile them:
|
923
|
+
|
924
|
+
```bash
|
925
|
+
# Install protoc compiler
|
926
|
+
gem install grpc-tools
|
927
|
+
|
928
|
+
# Compile proto file
|
929
|
+
grpc_tools_ruby_protoc -I ./proto --ruby_out=./lib proto/message.proto
|
930
|
+
|
931
|
+
# Use in Ruby
|
932
|
+
require_relative 'lib/message_pb'
|
933
|
+
msg = MyMessage.new(field: "value")
|
227
934
|
```
|
228
935
|
|
229
936
|
## API Documentation
|
@@ -298,30 +1005,122 @@ msg = NNG::Message.new(size: 0)
|
|
298
1005
|
|
299
1006
|
### Error Handling
|
300
1007
|
|
1008
|
+
NNG-ruby provides specific exception classes for different error conditions:
|
1009
|
+
|
301
1010
|
```ruby
|
1011
|
+
require 'nng'
|
1012
|
+
|
1013
|
+
socket = NNG::Socket.new(:req)
|
1014
|
+
|
302
1015
|
begin
|
303
|
-
|
1016
|
+
# Set a short timeout for demonstration
|
1017
|
+
socket.recv_timeout = 100 # 100ms
|
1018
|
+
|
1019
|
+
# Try to receive (will timeout if no data)
|
1020
|
+
socket.dial("tcp://127.0.0.1:9999")
|
1021
|
+
data = socket.recv
|
1022
|
+
|
304
1023
|
rescue NNG::TimeoutError => e
|
305
|
-
puts "
|
1024
|
+
puts "Operation timed out: #{e.message}"
|
1025
|
+
# Retry logic here
|
1026
|
+
|
306
1027
|
rescue NNG::ConnectionRefused => e
|
307
|
-
puts "
|
1028
|
+
puts "Cannot connect to server: #{e.message}"
|
1029
|
+
# Server might be down
|
1030
|
+
|
1031
|
+
rescue NNG::AddressInUse => e
|
1032
|
+
puts "Port already in use: #{e.message}"
|
1033
|
+
# Try a different port
|
1034
|
+
|
1035
|
+
rescue NNG::StateError => e
|
1036
|
+
puts "Invalid state for operation: #{e.message}"
|
1037
|
+
# Check socket state
|
1038
|
+
|
308
1039
|
rescue NNG::Error => e
|
309
|
-
puts "NNG error: #{e.message}"
|
1040
|
+
puts "NNG error (#{e.class}): #{e.message}"
|
1041
|
+
# Generic error handling
|
1042
|
+
|
1043
|
+
ensure
|
1044
|
+
socket.close
|
1045
|
+
end
|
1046
|
+
```
|
1047
|
+
|
1048
|
+
#### Complete Error Hierarchy
|
1049
|
+
|
1050
|
+
```ruby
|
1051
|
+
NNG::Error # Base class for all NNG errors
|
1052
|
+
├── NNG::TimeoutError # Operation timed out
|
1053
|
+
├── NNG::ConnectionRefused # Connection refused by peer
|
1054
|
+
├── NNG::ConnectionAborted # Connection aborted
|
1055
|
+
├── NNG::ConnectionReset # Connection reset by peer
|
1056
|
+
├── NNG::Closed # Socket/resource already closed
|
1057
|
+
├── NNG::AddressInUse # Address already in use
|
1058
|
+
├── NNG::NoMemory # Out of memory
|
1059
|
+
├── NNG::MessageSize # Message size invalid
|
1060
|
+
├── NNG::ProtocolError # Protocol error
|
1061
|
+
└── NNG::StateError # Invalid state for operation
|
1062
|
+
```
|
1063
|
+
|
1064
|
+
#### Practical Error Handling Examples
|
1065
|
+
|
1066
|
+
**1. Retry on Timeout:**
|
1067
|
+
|
1068
|
+
```ruby
|
1069
|
+
def send_with_retry(socket, data, max_retries: 3)
|
1070
|
+
retries = 0
|
1071
|
+
begin
|
1072
|
+
socket.send(data)
|
1073
|
+
rescue NNG::TimeoutError
|
1074
|
+
retries += 1
|
1075
|
+
if retries < max_retries
|
1076
|
+
puts "Timeout, retrying (#{retries}/#{max_retries})..."
|
1077
|
+
sleep 0.1
|
1078
|
+
retry
|
1079
|
+
else
|
1080
|
+
raise "Failed after #{max_retries} retries"
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
```
|
1085
|
+
|
1086
|
+
**2. Graceful Degradation:**
|
1087
|
+
|
1088
|
+
```ruby
|
1089
|
+
def connect_with_fallback(socket, primary_url, fallback_url)
|
1090
|
+
begin
|
1091
|
+
socket.dial(primary_url)
|
1092
|
+
puts "Connected to primary server"
|
1093
|
+
rescue NNG::ConnectionRefused, NNG::TimeoutError
|
1094
|
+
puts "Primary server unavailable, trying fallback..."
|
1095
|
+
socket.dial(fallback_url)
|
1096
|
+
puts "Connected to fallback server"
|
1097
|
+
end
|
310
1098
|
end
|
311
1099
|
```
|
312
1100
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
-
|
322
|
-
|
323
|
-
|
324
|
-
|
1101
|
+
**3. Non-blocking Receive with Error Handling:**
|
1102
|
+
|
1103
|
+
```ruby
|
1104
|
+
socket = NNG::Socket.new(:pull)
|
1105
|
+
socket.listen("tcp://127.0.0.1:5555")
|
1106
|
+
|
1107
|
+
loop do
|
1108
|
+
begin
|
1109
|
+
# Non-blocking receive
|
1110
|
+
data = socket.recv(flags: NNG::FFI::NNG_FLAG_NONBLOCK)
|
1111
|
+
puts "Received: #{data}"
|
1112
|
+
process_data(data)
|
1113
|
+
rescue NNG::Error => e
|
1114
|
+
if e.message.include?("try again")
|
1115
|
+
# No data available, do other work
|
1116
|
+
sleep 0.01
|
1117
|
+
else
|
1118
|
+
puts "Error: #{e.message}"
|
1119
|
+
break
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
end
|
1123
|
+
```
|
325
1124
|
|
326
1125
|
## Requirements
|
327
1126
|
|
@@ -334,7 +1133,7 @@ The NNG shared library (libnng.so.1.8.0) is bundled with the gem, so no external
|
|
334
1133
|
|
335
1134
|
```bash
|
336
1135
|
# Clone repository
|
337
|
-
git clone https://github.com/
|
1136
|
+
git clone https://github.com/Hola-QingYi/nng-ruby.git
|
338
1137
|
cd nng-ruby
|
339
1138
|
|
340
1139
|
# Install dependencies
|
@@ -343,6 +1142,9 @@ bundle install
|
|
343
1142
|
# Run tests
|
344
1143
|
bundle exec rspec
|
345
1144
|
|
1145
|
+
# Build gem
|
1146
|
+
gem build nng.gemspec
|
1147
|
+
|
346
1148
|
# Run examples
|
347
1149
|
ruby examples/pair.rb
|
348
1150
|
```
|
@@ -363,16 +1165,31 @@ MIT License - see LICENSE file for details.
|
|
363
1165
|
|
364
1166
|
- NNG library: https://nng.nanomsg.org/
|
365
1167
|
- Original nanomsg: https://nanomsg.org/
|
366
|
-
- Created by Claude Code
|
367
1168
|
|
368
1169
|
## Links
|
369
1170
|
|
370
1171
|
- [NNG Documentation](https://nng.nanomsg.org/man/)
|
371
|
-
- [GitHub Repository](https://github.com/
|
1172
|
+
- [GitHub Repository](https://github.com/Hola-QingYi/nng-ruby)
|
372
1173
|
- [RubyGems Page](https://rubygems.org/gems/nng-ruby)
|
373
1174
|
|
374
1175
|
## Version History
|
375
1176
|
|
1177
|
+
### 0.1.2 (2025-10-03)
|
1178
|
+
- **Enhanced Protocol Buffers documentation**
|
1179
|
+
- Added 3 comprehensive Protobuf integration examples
|
1180
|
+
- Example 1: Basic RPC with Protobuf
|
1181
|
+
- Example 2: Nested messages (message in message pattern)
|
1182
|
+
- Example 3: Real-world RPC pattern (WeChatFerry style)
|
1183
|
+
- Added detailed explanation of Protobuf benefits with NNG
|
1184
|
+
- Added performance comparison (Protobuf vs JSON)
|
1185
|
+
- Added instructions for loading .proto files
|
1186
|
+
- Improved examples documentation
|
1187
|
+
|
1188
|
+
### 0.1.1 (2025-10-03)
|
1189
|
+
- Published to GitHub repository
|
1190
|
+
- Updated gem packaging to include source code
|
1191
|
+
- Added GitHub Actions workflows for CI/CD
|
1192
|
+
|
376
1193
|
### 0.1.0 (2025-10-03)
|
377
1194
|
- Initial release
|
378
1195
|
- Complete NNG 1.8.0 API bindings
|