nng-ruby 0.1.2 → 1.0.1

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