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.
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 (libnng 1.9.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
- # Send request and get reply
83
+ # Request-reply cycle
80
84
  req.send("What is the answer?")
81
- puts rep.recv # => "What is the answer?"
85
+ question = rep.recv
86
+ puts "Server received: #{question}"
82
87
 
83
88
  rep.send("42")
84
- puts req.recv # => "42"
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
- sub = NNG::Socket.new(:sub)
101
- sub.dial("tcp://127.0.0.1:5557")
102
- sub.set_option("sub:subscribe", "") # Subscribe to all topics
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 messages
105
- pub.send("Hello, subscribers!")
106
- puts sub.recv # => "Hello, subscribers!"
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
- sub.close
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
- # Consumer
122
- pull = NNG::Socket.new(:pull)
123
- pull.dial("tcp://127.0.0.1:5558")
151
+ # Worker 1 (Pull)
152
+ pull1 = NNG::Socket.new(:pull)
153
+ pull1.dial("tcp://127.0.0.1:5558")
124
154
 
125
- # Send work
126
- push.send("Task 1")
127
- puts pull.recv # => "Task 1"
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
- pull.close
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
- # Add header
188
- msg.header_append("Type: Greeting")
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 message
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 options
204
- socket.set_option("send-buffer", 8192)
205
- socket.set_option("tcp-nodelay", true)
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
- Run examples:
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
- ruby examples/pair.rb
225
- ruby examples/reqrep.rb
226
- ruby examples/pubsub.rb
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
- socket.send("data")
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 "Timeout: #{e.message}"
1024
+ puts "Operation timed out: #{e.message}"
1025
+ # Retry logic here
1026
+
306
1027
  rescue NNG::ConnectionRefused => e
307
- puts "Connection refused: #{e.message}"
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
- Available error classes:
314
- - `NNG::Error` - Base error
315
- - `NNG::TimeoutError`
316
- - `NNG::ConnectionRefused`
317
- - `NNG::ConnectionAborted`
318
- - `NNG::ConnectionReset`
319
- - `NNG::Closed`
320
- - `NNG::AddressInUse`
321
- - `NNG::NoMemory`
322
- - `NNG::MessageSize`
323
- - `NNG::ProtocolError`
324
- - `NNG::StateError`
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/yourusername/nng-ruby.git
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/yourusername/nng-ruby)
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