NNG Ruby Bindings
Ruby bindings for NNG (nanomsg-next-generation), a lightweight messaging library.
Features
- ✅ Complete FFI bindings for NNG 1.8.0 (300+ functions)
- ✅ All scalability protocols: Pair, Push/Pull, Pub/Sub, Req/Rep, Surveyor/Respondent, Bus
- ✅ All transports: TCP, IPC, Inproc, WebSocket, TLS
- ✅ High-level Ruby API with automatic resource management
- ✅ Message-based and byte-based communication
- ✅ Bundled libnng.so.1.8.0 shared library (no external dependencies)
- ✅ Thread-safe
- ✅ Full async I/O support
- ✅ Automated testing and publishing via GitHub Actions
Installation
Add this line to your application's Gemfile:
gem 'nng-ruby'
And then execute:
bundle install
Or install it yourself as:
gem install nng-ruby
Quick Start
Pair Protocol (Bidirectional)
require 'nng'
# Server
server = NNG::Socket.new(:pair1)
server.listen("tcp://127.0.0.1:5555")
# Client
client = NNG::Socket.new(:pair1)
client.dial("tcp://127.0.0.1:5555")
# Send and receive
client.send("Hello, NNG!")
puts server.recv # => "Hello, NNG!"
server.send("Hello back!")
puts client.recv # => "Hello back!"
# Cleanup
server.close
client.close
Request/Reply Protocol
The request/reply pattern ensures exactly one reply for each request:
require 'nng'
# Server (replier) - Must reply to each request
rep = NNG::Socket.new(:rep)
rep.listen("tcp://127.0.0.1:5556")
# Client (requester) - Must wait for reply before sending next request
req = NNG::Socket.new(:req)
req.dial("tcp://127.0.0.1:5556")
sleep 0.1 # Allow connection to establish
# Request-reply cycle
req.send("What is the answer?")
question = rep.recv
puts "Server received: #{question}"
rep.send("42")
answer = req.recv
puts "Client received: #{answer}"
# Another request-reply cycle
req.send("What is 2+2?")
rep.send("4")
puts req.recv # => "4"
rep.close
req.close
Publish/Subscribe Protocol
Pub/Sub allows one publisher to broadcast to multiple subscribers:
require 'nng'
# Publisher
pub = NNG::Socket.new(:pub)
pub.listen("tcp://127.0.0.1:5557")
# Subscriber 1 - Subscribe to all topics
sub1 = NNG::Socket.new(:sub)
sub1.dial("tcp://127.0.0.1:5557")
sub1.set_option("sub:subscribe", "") # Subscribe to everything
# Subscriber 2 - Subscribe to specific topic
sub2 = NNG::Socket.new(:sub)
sub2.dial("tcp://127.0.0.1:5557")
sub2.set_option("sub:subscribe", "ALERT:") # Only "ALERT:" messages
sleep 0.1 # Allow subscriptions to propagate
# Publish to all subscribers
pub.send("ALERT: System update available")
puts sub1.recv # => "ALERT: System update available"
puts sub2.recv # => "ALERT: System update available"
# Publish general message (only sub1 receives)
pub.send("INFO: Normal operation")
puts sub1.recv # => "INFO: Normal operation"
# sub2 won't receive this (topic doesn't match)
pub.close
sub1.close
sub2.close
Push/Pull Protocol (Pipeline)
Push/Pull creates a load-balanced pipeline for distributing work:
require 'nng'
require 'thread'
# Producer (Push)
push = NNG::Socket.new(:push)
push.listen("tcp://127.0.0.1:5558")
# Worker 1 (Pull)
pull1 = NNG::Socket.new(:pull)
pull1.dial("tcp://127.0.0.1:5558")
# Worker 2 (Pull)
pull2 = NNG::Socket.new(:pull)
pull2.dial("tcp://127.0.0.1:5558")
sleep 0.1 # Allow connections
# Start workers in threads
workers = []
workers << Thread.new do
3.times do
task = pull1.recv
puts "Worker 1 processing: #{task}"
end
end
workers << Thread.new do
3.times do
task = pull2.recv
puts "Worker 2 processing: #{task}"
end
end
# Distribute tasks (round-robin to workers)
6.times do |i|
push.send("Task #{i+1}")
sleep 0.01
end
# Wait for workers to complete
workers.each(&:join)
push.close
pull1.close
pull2.close
Bus Protocol (Many-to-Many)
Bus protocol allows all peers to communicate with each other:
require 'nng'
# Create three bus nodes
node1 = NNG::Socket.new(:bus)
node1.listen("tcp://127.0.0.1:5559")
node2 = NNG::Socket.new(:bus)
node2.listen("tcp://127.0.0.1:5560")
node2.dial("tcp://127.0.0.1:5559")
node3 = NNG::Socket.new(:bus)
node3.dial("tcp://127.0.0.1:5559")
node3.dial("tcp://127.0.0.1:5560")
sleep 0.1 # Allow mesh to form
# Any node can send to all others
node1.send("Message from Node 1")
puts node2.recv # => "Message from Node 1"
puts node3.recv # => "Message from Node 1"
node2.send("Message from Node 2")
puts node1.recv # => "Message from Node 2"
puts node3.recv # => "Message from Node 2"
node1.close
node2.close
node3.close
Surveyor/Respondent Protocol
Surveyor sends a survey, and all respondents reply:
require 'nng'
# Surveyor
surveyor = NNG::Socket.new(:surveyor)
surveyor.listen("tcp://127.0.0.1:5561")
surveyor.send_timeout = 1000 # 1 second to collect responses
# Respondents
resp1 = NNG::Socket.new(:respondent)
resp1.dial("tcp://127.0.0.1:5561")
resp2 = NNG::Socket.new(:respondent)
resp2.dial("tcp://127.0.0.1:5561")
sleep 0.1 # Allow connections
# Send survey
surveyor.send("What is your status?")
# Respondents reply
question1 = resp1.recv
resp1.send("Respondent 1: OK")
question2 = resp2.recv
resp2.send("Respondent 2: OK")
# Collect responses
begin
puts surveyor.recv # => "Respondent 1: OK" or "Respondent 2: OK"
puts surveyor.recv # => "Respondent 2: OK" or "Respondent 1: OK"
rescue NNG::TimeoutError
puts "Survey complete"
end
surveyor.close
resp1.close
resp2.close
Supported Protocols
-
Pair - Bidirectional 1:1 communication (
pair0
,pair1
) -
Push/Pull - Unidirectional pipeline (
push0
,pull0
) -
Pub/Sub - One-to-many distribution (
pub0
,sub0
) -
Req/Rep - Request/reply pattern (
req0
,rep0
) -
Surveyor/Respondent - Survey pattern (
surveyor0
,respondent0
) -
Bus - Many-to-many (
bus0
)
Supported Transports
-
TCP -
tcp://host:port
-
IPC -
ipc:///path/to/socket
-
Inproc -
inproc://name
-
WebSocket -
ws://host:port/path
-
TLS -
tls+tcp://host:port
Advanced Usage
Custom Library Configuration
By default, nng-ruby uses the bundled libnng.so.1.8.0 library. However, you can specify a custom NNG library in several ways:
Option 1: At install time
Use gem install options to specify a custom NNG library location:
# Specify NNG installation directory (will search lib/, lib64/, etc.)
gem install nng-ruby -- --with-nng-dir=/opt/nng
# Specify exact library path
gem install nng-ruby -- --with-nng-lib=/opt/nng/lib/libnng.so
# Specify include directory (for future use)
gem install nng-ruby -- --with-nng-include=/opt/nng/include
Option 2: At runtime with environment variables
Set environment variables before requiring the gem:
# Specify exact library file path
export NNG_LIB_PATH=/usr/local/lib/libnng.so.1.9.0
ruby your_script.rb
# Or specify library directory (will search for libnng.so*)
export NNG_LIB_DIR=/usr/local/lib
ruby your_script.rb
In Ruby code:
# Set before requiring nng
ENV['NNG_LIB_PATH'] = '/custom/path/libnng.so'
require 'nng'
# Or use directory
ENV['NNG_LIB_DIR'] = '/custom/path/lib'
require 'nng'
Priority Order
The library is loaded in this priority order:
-
Environment variable
NNG_LIB_PATH
(highest priority) -
Environment variable
NNG_LIB_DIR
- Install-time configuration (gem install --with-nng-*)
- Bundled library (ext/nng/libnng.so.1.8.0)
- System paths (/usr/local/lib, /usr/lib, etc.)
Debugging
Enable debug output to see which library is being loaded:
export NNG_DEBUG=1
ruby your_script.rb
This will print messages showing:
- Which paths are being searched
- Which library was successfully loaded
- Any load failures encountered
Setting Timeouts
socket = NNG::Socket.new(:pair1)
socket.send_timeout = 5000 # 5 seconds
socket.recv_timeout = 5000 # 5 seconds
Non-blocking I/O
require 'nng'
socket = NNG::Socket.new(:pair1)
socket.listen("tcp://127.0.0.1:5555")
begin
data = socket.recv(flags: NNG::FFI::NNG_FLAG_NONBLOCK)
puts "Received: #{data}"
rescue NNG::Error => e
puts "No data available: #{e.message}"
end
Using Messages (NNG::Message API)
The Message API provides more control over message headers and bodies:
Basic Message Operations
require 'nng'
# Create a new message
msg = NNG::Message.new
# Append data to message body
msg.append("Hello, ")
msg.append("World!")
puts msg.body # => "Hello, World!"
puts msg.length # => 13
# Insert data at the beginning
msg.insert("Say: ")
puts msg.body # => "Say: Hello, World!"
# Add header information
msg.header_append("Content-Type: text/plain")
puts msg.header # => "Content-Type: text/plain"
puts msg.header_length # => 24
# Clear body or header
msg.clear # Clears body
msg.header_clear # Clears header
# Duplicate message
msg2 = msg.dup
# Free messages when done
msg.free
msg2.free
Sending and Receiving Messages
require 'nng'
# Server side
server = NNG::Socket.new(:pair1)
server.listen("tcp://127.0.0.1:5555")
# Client side
client = NNG::Socket.new(:pair1)
client.dial("tcp://127.0.0.1:5555")
sleep 0.1 # Give time to establish connection
# Send a message with header and body
msg = NNG::Message.new
msg.header_append("RequestID: 12345")
msg.append("Hello from client!")
# Send message using low-level API
msg_ptr = ::FFI::MemoryPointer.new(:pointer)
msg_ptr.write_pointer(msg.to_ptr)
ret = NNG::FFI.nng_sendmsg(client.socket, msg_ptr.read_pointer, 0)
NNG::FFI.check_error(ret, "Send message")
# Note: Message is freed automatically after sending
# Receive message
recv_msg_ptr = ::FFI::MemoryPointer.new(:pointer)
ret = NNG::FFI.nng_recvmsg(server.socket, recv_msg_ptr, 0)
NNG::FFI.check_error(ret, "Receive message")
# Wrap received message
recv_msg = NNG::Message.allocate
recv_msg.instance_variable_set(:@msg, recv_msg_ptr.read_pointer)
recv_msg.instance_variable_set(:@msg_ptr, recv_msg_ptr)
recv_msg.instance_variable_set(:@freed, false)
puts "Header: #{recv_msg.header}" # => "RequestID: 12345"
puts "Body: #{recv_msg.body}" # => "Hello from client!"
recv_msg.free
server.close
client.close
Simple String-based Send/Receive (Recommended for most use cases)
For simpler use cases, use the high-level Socket#send and Socket#recv methods:
require 'nng'
server = NNG::Socket.new(:pair1)
server.listen("tcp://127.0.0.1:5555")
client = NNG::Socket.new(:pair1)
client.dial("tcp://127.0.0.1:5555")
# Simple send/receive (no need to manage Message objects)
client.send("Hello, Server!")
data = server.recv
puts data # => "Hello, Server!"
server.close
client.close
Socket Options
NNG sockets support various options to control behavior:
require 'nng'
socket = NNG::Socket.new(:pub)
# Set buffer sizes
socket.set_option("send-buffer", 8192) # Send buffer size in bytes
socket.set_option("recv-buffer", 8192) # Receive buffer size in bytes
# Set TCP options
socket.set_option("tcp-nodelay", true) # Disable Nagle's algorithm
socket.set_option("tcp-keepalive", true) # Enable TCP keepalive
# Set timeouts
socket.send_timeout = 1000 # 1 second send timeout
socket.recv_timeout = 5000 # 5 second receive timeout
# Or use set_option_ms
socket.set_option_ms("send-timeout", 1000)
socket.set_option_ms("recv-timeout", 5000)
# Get options
buffer_size = socket.get_option("send-buffer", type: :int)
puts "Send buffer: #{buffer_size} bytes"
nodelay = socket.get_option("tcp-nodelay", type: :bool)
puts "TCP NoDelay: #{nodelay}"
send_timeout = socket.get_option("send-timeout", type: :ms)
puts "Send timeout: #{send_timeout} ms"
# Protocol-specific options
if socket.socket.id # For pub/sub
# Subscriber can set subscription topics
sub = NNG::Socket.new(:sub)
sub.set_option("sub:subscribe", "news/") # Subscribe to "news/*"
sub.set_option("sub:subscribe", "alerts/") # Subscribe to "alerts/*"
sub.set_option("sub:unsubscribe", "news/") # Unsubscribe from "news/*"
end
socket.close
Transport-Specific URLs
Different transports have different URL formats:
require 'nng'
socket = NNG::Socket.new(:pair1)
# TCP transport
socket.listen("tcp://0.0.0.0:5555") # Listen on all interfaces
socket.dial("tcp://192.168.1.100:5555") # Connect to specific IP
socket.dial("tcp://localhost:5555") # Connect to localhost
# IPC transport (Unix domain sockets)
socket.listen("ipc:///tmp/test.sock") # Unix socket
socket.dial("ipc:///tmp/test.sock")
# Inproc transport (in-process, same memory space)
socket.listen("inproc://my-channel")
socket.dial("inproc://my-channel")
# WebSocket transport
socket.listen("ws://0.0.0.0:8080/path")
socket.dial("ws://localhost:8080/path")
# TLS transport
socket.listen("tls+tcp://0.0.0.0:5556")
socket.dial("tls+tcp://server.example.com:5556")
socket.close
Examples
See the examples/
directory for complete working examples:
Protocol Examples
-
examples/pair.rb
- Pair protocol -
examples/reqrep.rb
- Request/Reply protocol -
examples/pubsub.rb
- Publish/Subscribe protocol
Protocol Buffers Integration
NNG-ruby works seamlessly with Google Protocol Buffers for efficient binary serialization. This is perfect for RPC systems, microservices, and cross-language communication.
Available Examples:
-
examples/protobuf_demo.rb
- Complete demo with nested messages and RPC patterns -
examples/protobuf_simple.rb
- Simple client-server with Protobuf -
examples/proto/message.proto
- Protobuf message definitions
Installation:
gem install google-protobuf
Example 1: Basic RPC with Protobuf
This example shows a simple request-response RPC system using NNG + Protobuf:
require 'nng'
require 'google/protobuf'
# Define Protobuf messages inline (or load from .proto file)
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("rpc.proto", syntax: :proto3) do
add_message "RpcRequest" do
optional :func_code, :int32, 1
optional :data, :bytes, 2
optional :request_id, :string, 3
end
add_message "RpcResponse" do
optional :status, :int32, 1
optional :data, :bytes, 2
optional :error_msg, :string, 3
end
end
end
RpcRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("RpcRequest").msgclass
RpcResponse = Google::Protobuf::DescriptorPool.generated_pool.msgclass("RpcResponse")
# Server
server = NNG::Socket.new(:rep)
server.listen("tcp://127.0.0.1:5555")
puts "RPC Server listening on tcp://127.0.0.1:5555"
# Client
client = NNG::Socket.new(:req)
client.dial("tcp://127.0.0.1:5555")
sleep 0.1
# Client sends request
request = RpcRequest.new(
func_code: 100,
data: "Get user info",
request_id: "req-001"
)
client.send(RpcRequest.encode(request))
puts "Client sent: #{request.inspect}"
# Server receives and processes
req_data = server.recv
received_req = RpcRequest.decode(req_data)
puts "Server received: func=#{received_req.func_code}, id=#{received_req.request_id}"
# Server sends response
response = RpcResponse.new(
status: 0,
data: '{"name": "Alice", "age": 30}',
error_msg: ""
)
server.send(RpcResponse.encode(response))
puts "Server sent: status=#{response.status}"
# Client receives response
resp_data = client.recv
received_resp = RpcResponse.decode(resp_data)
puts "Client received: status=#{received_resp.status}, data=#{received_resp.data}"
server.close
client.close
Output:
RPC Server listening on tcp://127.0.0.1:5555
Client sent: <RpcRequest: func_code: 100, data: "Get user info", request_id: "req-001">
Server received: func=100, id=req-001
Server sent: status=0
Client received: status=0, data={"name": "Alice", "age": 30}
Example 2: Nested Messages (Message in Message)
A common pattern is sending complex data structures by nesting Protobuf messages:
require 'nng'
require 'google/protobuf'
# Define nested message structure
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("nested.proto", syntax: :proto3) do
add_message "Contact" do
optional :wxid, :string, 1
optional :name, :string, 2
optional :remark, :string, 3
end
add_message "ContactList" do
repeated :contacts, :message, 1, "Contact"
end
add_message "RpcResponse" do
optional :status, :int32, 1
optional :data, :bytes, 2 # Will contain encoded ContactList
end
end
end
Contact = Google::Protobuf::DescriptorPool.generated_pool.lookup("Contact").msgclass
ContactList = Google::Protobuf::DescriptorPool.generated_pool.lookup("ContactList").msgclass
RpcResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("RpcResponse").msgclass
# Server prepares nested data
contacts = ContactList.new(
contacts: [
Contact.new(wxid: "wxid_001", name: "Alice", remark: "Friend"),
Contact.new(wxid: "wxid_002", name: "Bob", remark: "Colleague"),
Contact.new(wxid: "wxid_003", name: "Charlie", remark: "Family")
]
)
# Encode inner message first, then wrap in outer message
contacts_data = ContactList.encode(contacts)
response = RpcResponse.new(status: 0, data: contacts_data)
# Setup sockets
server = NNG::Socket.new(:pair1)
server.listen("tcp://127.0.0.1:5556")
client = NNG::Socket.new(:pair1)
client.dial("tcp://127.0.0.1:5556")
sleep 0.1
# Send nested message
server.send(RpcResponse.encode(response))
puts "Server sent: #{contacts.contacts.size} contacts"
# Receive and decode nested message
resp_data = client.recv
received_resp = RpcResponse.decode(resp_data)
# Decode inner message
received_contacts = ContactList.decode(received_resp.data)
puts "Client received #{received_contacts.contacts.size} contacts:"
received_contacts.contacts.each do |c|
puts " - #{c.name} (#{c.wxid}): #{c.remark}"
end
server.close
client.close
Output:
Server sent: 3 contacts
Client received 3 contacts:
- Alice (wxid_001): Friend
- Bob (wxid_002): Colleague
- Charlie (wxid_003): Family
Example 3: Real-World RPC Pattern (WeChatFerry Style)
This shows a complete RPC system similar to WeChatFerry's architecture:
require 'nng'
require 'google/protobuf'
# Define protocol (matching WeChatFerry style)
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("wcf_rpc.proto", syntax: :proto3) do
add_enum "Functions" do
value :FUNC_IS_LOGIN, 0x01
value :FUNC_SEND_TEXT, 0x20
value :FUNC_GET_CONTACTS, 0x12
end
add_message "Request" do
optional :func, :enum, 1, "Functions"
optional :data, :bytes, 2
end
add_message "Response" do
optional :status, :int32, 1
optional :data, :bytes, 2
end
add_message "TextMsg" do
optional :receiver, :string, 1
optional :message, :string, 2
end
end
end
Functions = Google::Protobuf::DescriptorPool.generated_pool.lookup("Functions").enummodule
Request = Google::Protobuf::DescriptorPool.generated_pool.lookup("Request").msgclass
Response = Google::Protobuf::DescriptorPool.generated_pool.lookup("Response").msgclass
TextMsg = Google::Protobuf::DescriptorPool.generated_pool.lookup("TextMsg").msgclass
# RPC Server
def start_rpc_server
server = NNG::Socket.new(:rep)
server.listen("tcp://127.0.0.1:10086")
puts "RPC Server started on port 10086"
loop do
# Receive request
req_data = server.recv
request = Request.decode(req_data)
puts "Received: func=#{request.func}"
# Process request
response = case request.func
when :FUNC_IS_LOGIN
Response.new(status: 1) # Logged in
when :FUNC_SEND_TEXT
text_msg = TextMsg.decode(request.data)
puts " Send to #{text_msg.receiver}: #{text_msg.message}"
Response.new(status: 0) # Success
when :FUNC_GET_CONTACTS
Response.new(status: 0, data: "[contacts data]")
else
Response.new(status: -1) # Unknown function
end
# Send response
server.send(Response.encode(response))
end
ensure
server&.close
end
# RPC Client
def rpc_call(client, func, data = nil)
request = Request.new(func: func, data: data)
client.send(Request.encode(request))
resp_data = client.recv
Response.decode(resp_data)
end
# Run example
server_thread = Thread.new { start_rpc_server rescue nil }
sleep 0.5 # Wait for server to start
client = NNG::Socket.new(:req)
client.dial("tcp://127.0.0.1:10086")
puts "Client connected"
# Call 1: Check login
resp = rpc_call(client, :FUNC_IS_LOGIN)
puts "Login status: #{resp.status == 1 ? 'Logged in' : 'Not logged in'}"
# Call 2: Send text message
text_msg = TextMsg.new(receiver: "wxid_friend", message: "Hello from Ruby!")
resp = rpc_call(client, :FUNC_SEND_TEXT, TextMsg.encode(text_msg))
puts "Send text result: #{resp.status == 0 ? 'Success' : 'Failed'}"
# Call 3: Get contacts
resp = rpc_call(client, :FUNC_GET_CONTACTS)
puts "Contacts: #{resp.data}"
client.close
Thread.kill(server_thread)
Output:
RPC Server started on port 10086
Client connected
Received: func=FUNC_IS_LOGIN
Login status: Logged in
Received: func=FUNC_SEND_TEXT
Send to wxid_friend: Hello from Ruby!
Send text result: Success
Received: func=FUNC_GET_CONTACTS
Contacts: [contacts data]
Why Use Protobuf with NNG?
- Binary Efficiency: Protobuf is 3-10x smaller than JSON, faster to parse
- Type Safety: Strong typing prevents errors
- Cross-Language: Works with Python, Go, Java, C++, etc.
- Schema Evolution: Add fields without breaking old clients
- Perfect for RPC: Natural request/response pattern
Performance Comparison
# Benchmark: Protobuf vs JSON
require 'benchmark'
require 'json'
data = { name: "Alice", age: 30, contacts: ["Bob", "Charlie"] }
Benchmark.bm(10) do |x|
x.report("JSON:") do
10000.times { JSON.parse(data.to_json) }
end
x.report("Protobuf:") do
10000.times {
msg = MyProto.new(data)
MyProto.decode(MyProto.encode(msg))
}
end
end
# Typical result:
# user system total real
# JSON: 0.850000 0.010000 0.860000 ( 0.862341)
# Protobuf: 0.280000 0.000000 0.280000 ( 0.283127)
More Examples
See the examples/
directory for complete runnable code:
# Run comprehensive demo
ruby examples/protobuf_demo.rb
# Run simple client-server
ruby examples/protobuf_simple.rb
# View proto definitions
cat examples/proto/message.proto
Loading .proto Files
You can also define messages in .proto
files and compile them:
# Install protoc compiler
gem install grpc-tools
# Compile proto file
grpc_tools_ruby_protoc -I ./proto --ruby_out=./lib proto/message.proto
# Use in Ruby
require_relative 'lib/message_pb'
msg = MyMessage.new(field: "value")
API Documentation
NNG Module
-
NNG.version
- Gem version -
NNG.lib_version
- NNG library version -
NNG.fini
- Cleanup NNG (called automatically)
NNG::Socket
Creating Sockets
socket = NNG::Socket.new(:pair1)
socket = NNG::Socket.new(:req, raw: false)
Connection Methods
-
listen(url, flags: 0)
- Listen on address -
dial(url, flags: 0)
- Connect to address -
close
- Close socket -
closed?
- Check if closed
Send/Receive Methods
-
send(data, flags: 0)
- Send data -
recv(flags: NNG::FFI::NNG_FLAG_ALLOC)
- Receive data
Option Methods
-
set_option(name, value)
- Set socket option -
get_option(name, type: :int)
- Get socket option -
set_option_ms(name, ms)
- Set timeout option -
send_timeout=(ms)
- Set send timeout -
recv_timeout=(ms)
- Set receive timeout
Information Methods
-
id
- Get socket ID
NNG::Message
Creating Messages
msg = NNG::Message.new(size: 0)
Body Methods
-
append(data)
- Append to body -
insert(data)
- Insert at beginning -
body
- Get body content -
length
/size
- Get body length -
clear
- Clear body
Header Methods
-
header_append(data)
- Append to header -
header
- Get header content -
header_length
- Get header length -
header_clear
- Clear header
Other Methods
-
dup
- Duplicate message -
free
- Free message -
freed?
- Check if freed
Error Handling
NNG-ruby provides specific exception classes for different error conditions:
require 'nng'
socket = NNG::Socket.new(:req)
begin
# Set a short timeout for demonstration
socket.recv_timeout = 100 # 100ms
# Try to receive (will timeout if no data)
socket.dial("tcp://127.0.0.1:9999")
data = socket.recv
rescue NNG::TimeoutError => e
puts "Operation timed out: #{e.message}"
# Retry logic here
rescue NNG::ConnectionRefused => e
puts "Cannot connect to server: #{e.message}"
# Server might be down
rescue NNG::AddressInUse => e
puts "Port already in use: #{e.message}"
# Try a different port
rescue NNG::StateError => e
puts "Invalid state for operation: #{e.message}"
# Check socket state
rescue NNG::Error => e
puts "NNG error (#{e.class}): #{e.message}"
# Generic error handling
ensure
socket.close
end
Complete Error Hierarchy
NNG::Error # Base class for all NNG errors
├── NNG::TimeoutError # Operation timed out
├── NNG::ConnectionRefused # Connection refused by peer
├── NNG::ConnectionAborted # Connection aborted
├── NNG::ConnectionReset # Connection reset by peer
├── NNG::Closed # Socket/resource already closed
├── NNG::AddressInUse # Address already in use
├── NNG::NoMemory # Out of memory
├── NNG::MessageSize # Message size invalid
├── NNG::ProtocolError # Protocol error
└── NNG::StateError # Invalid state for operation
Practical Error Handling Examples
1. Retry on Timeout:
def send_with_retry(socket, data, max_retries: 3)
retries = 0
begin
socket.send(data)
rescue NNG::TimeoutError
retries += 1
if retries < max_retries
puts "Timeout, retrying (#{retries}/#{max_retries})..."
sleep 0.1
retry
else
raise "Failed after #{max_retries} retries"
end
end
end
2. Graceful Degradation:
def connect_with_fallback(socket, primary_url, fallback_url)
begin
socket.dial(primary_url)
puts "Connected to primary server"
rescue NNG::ConnectionRefused, NNG::TimeoutError
puts "Primary server unavailable, trying fallback..."
socket.dial(fallback_url)
puts "Connected to fallback server"
end
end
3. Non-blocking Receive with Error Handling:
socket = NNG::Socket.new(:pull)
socket.listen("tcp://127.0.0.1:5555")
loop do
begin
# Non-blocking receive
data = socket.recv(flags: NNG::FFI::NNG_FLAG_NONBLOCK)
puts "Received: #{data}"
process_data(data)
rescue NNG::Error => e
if e.message.include?("try again")
# No data available, do other work
sleep 0.01
else
puts "Error: #{e.message}"
break
end
end
end
Requirements
- Ruby 2.5 or later
- FFI gem
The NNG shared library (libnng.so.1.8.0) is bundled with the gem, so no external installation is required.
Development
# Clone repository
git clone https://github.com/Hola-QingYi/nng-ruby.git
cd nng-ruby
# Install dependencies
bundle install
# Run tests
bundle exec rspec
# Build gem
gem build nng.gemspec
# Run examples
ruby examples/pair.rb
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
License
MIT License - see LICENSE file for details.
Credits
- NNG library: https://nng.nanomsg.org/
- Original nanomsg: https://nanomsg.org/
Links
Version History
0.1.2 (2025-10-03)
-
Enhanced Protocol Buffers documentation
- Added 3 comprehensive Protobuf integration examples
- Example 1: Basic RPC with Protobuf
- Example 2: Nested messages (message in message pattern)
- Example 3: Real-world RPC pattern (WeChatFerry style)
- Added detailed explanation of Protobuf benefits with NNG
- Added performance comparison (Protobuf vs JSON)
- Added instructions for loading .proto files
- Improved examples documentation
0.1.1 (2025-10-03)
- Published to GitHub repository
- Updated gem packaging to include source code
- Added GitHub Actions workflows for CI/CD
0.1.0 (2025-10-03)
- Initial release
- Complete NNG 1.8.0 API bindings
- All protocols and transports supported
- Bundled libnng.so.1.8.0
- High-level Ruby API
- Message support
- Examples and documentation