├── lib ├── ffi-rzmq │ ├── version.rb │ ├── libc.rb │ ├── device.rb │ ├── exceptions.rb │ ├── poll_item.rb │ ├── poll_items.rb │ ├── util.rb │ ├── context.rb │ ├── constants.rb │ ├── poll.rb │ ├── libzmq.rb │ └── message.rb ├── io_extensions.rb └── ffi-rzmq.rb ├── .gitignore ├── Gemfile ├── Rakefile ├── ext └── README ├── .travis.yml ├── AUTHORS.txt ├── .bnsignore ├── spec ├── support │ ├── test.crt │ └── test.key ├── device_spec.rb ├── reqrep_spec.rb ├── spec_helper.rb ├── message_spec.rb ├── context_spec.rb ├── pushpull_spec.rb ├── multipart_spec.rb ├── poll_spec.rb ├── nonblocking_recv_spec.rb └── socket_spec.rb ├── examples ├── v2api │ ├── remote_throughput.rb │ ├── request_response.rb │ ├── pub.rb │ ├── local_throughput.rb │ ├── reqrep_poll.rb │ ├── local_lat_poll.rb │ ├── local_lat.rb │ ├── sub.rb │ ├── remote_lat.rb │ ├── publish_subscribe.rb │ ├── xreqxrep_poll.rb │ ├── throughput_measurement.rb │ └── latency_measurement.rb ├── v3api │ ├── request_response.rb │ ├── pub.rb │ ├── remote_throughput.rb │ ├── reqrep_poll.rb │ ├── local_throughput.rb │ ├── local_lat_poll.rb │ ├── sub.rb │ ├── local_lat.rb │ ├── remote_lat.rb │ ├── publish_subscribe.rb │ ├── xreqxrep_poll.rb │ ├── latency_measurement.rb │ └── throughput_measurement.rb └── README.rdoc ├── ffi-rzmq.gemspec ├── README.rdoc └── History.txt /lib/ffi-rzmq/version.rb: -------------------------------------------------------------------------------- 1 | module ZMQ 2 | VERSION = "1.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | 6 | *.rbc 7 | .redcar/ 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "jruby-openssl", :platform => :jruby 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /ext/README: -------------------------------------------------------------------------------- 1 | To avoid loading a system-wide 0mq library, place 2 | the C libraries here. This let's you run your Ruby 3 | code with a 0mq C library build that is different 4 | from the system-wide one. This can be handy for 5 | rolling upgrades or testing. 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: sudo apt-get install libzmq3-dev 2 | script: bundle exec rspec 3 | language: ruby 4 | rvm: 5 | - 1.9.2 6 | - 1.9.3 7 | - 2.0.0 8 | - ruby-head 9 | - jruby-18mode 10 | - jruby-19mode 11 | - jruby-head 12 | - rbx-18mode 13 | - rbx-19mode 14 | 15 | matrix: 16 | allow_failures: 17 | - rvm: ruby-head 18 | - rvm: jruby-head 19 | - rvm: 1.9.2 20 | -------------------------------------------------------------------------------- /lib/io_extensions.rb: -------------------------------------------------------------------------------- 1 | class IO 2 | if defined? JRUBY_VERSION 3 | require 'jruby' 4 | def posix_fileno 5 | case self 6 | when STDIN, $stdin 7 | 0 8 | when STDOUT, $stdout 9 | 1 10 | when STDERR, $stderr 11 | 2 12 | else 13 | JRuby.reference(self).getOpenFile.getMainStream.getDescriptor.getChannel.getFDVal 14 | end 15 | end 16 | else 17 | alias :posix_fileno :fileno 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Chuck Remes, github: chuckremes 2 | 3 | Andrew Cholakian, github: andrewvc 4 | 5 | Ar Vicco, github: arvicco 6 | 7 | Ben Mabey, github: bmabey 8 | 9 | Julien Ammous, github: schmurfy 10 | 11 | Zachary Belzer, github: zbelzer 12 | 13 | Cory Forsyth, github: bantic 14 | 15 | Stefan Kaes, github: skaes 16 | 17 | Dmitry Ustalov, github: eveel 18 | 19 | Patrik Sundberg, github: sundbp 20 | 21 | Pawel Pacana, github: pawelpacana 22 | 23 | Brian Ford, github: brixen 24 | 25 | Nilson Santos F. Jr., github: nilsonfsj 26 | -------------------------------------------------------------------------------- /.bnsignore: -------------------------------------------------------------------------------- 1 | # The list of files that should be ignored by Mr Bones. 2 | # Lines that start with '#' are comments. 3 | # 4 | # A .gitignore file can be used instead by setting it as the ignore 5 | # file in your Rakefile: 6 | # 7 | # Bones { 8 | # ignore_file '.gitignore' 9 | # } 10 | # 11 | # For a project with a C extension, the following would be a good set of 12 | # exclude patterns (uncomment them if you want to use them): 13 | # *.[oa] 14 | # *~ 15 | announcement.txt 16 | coverage 17 | doc 18 | pkg 19 | *.tmproj 20 | *.gem 21 | *.rbc 22 | *.html 23 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/libc.rb: -------------------------------------------------------------------------------- 1 | 2 | module LibC 3 | extend FFI::Library 4 | # figures out the correct libc for each platform including Windows 5 | library = ffi_lib(FFI::Library::LIBC).first 6 | 7 | # Size_t not working properly on Windows 8 | find_type(:size_t) rescue typedef(:ulong, :size_t) 9 | 10 | # memory allocators 11 | attach_function :malloc, [:size_t], :pointer 12 | attach_function :free, [:pointer], :void 13 | 14 | # get a pointer to the free function; used for ZMQ::Message deallocation 15 | Free = library.find_symbol('free') 16 | 17 | # memory movers 18 | attach_function :memcpy, [:pointer, :pointer, :size_t], :pointer 19 | end # module LibC 20 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/device.rb: -------------------------------------------------------------------------------- 1 | module ZMQ 2 | 3 | class Device 4 | attr_reader :device 5 | 6 | def self.create(device_type, frontend, backend) 7 | dev = nil 8 | begin 9 | dev = new(device_type, frontend, backend) 10 | rescue ArgumentError 11 | dev = nil 12 | end 13 | 14 | dev 15 | end 16 | 17 | def initialize(device_type, frontend, backend) 18 | [["frontend", frontend], ["backend", backend]].each do |name, socket| 19 | unless socket.is_a?(ZMQ::Socket) 20 | raise ArgumentError, "Expected a ZMQ::Socket, not a #{socket.class} as the #{name}" 21 | end 22 | end 23 | 24 | LibZMQ.zmq_device(device_type, frontend.socket, backend.socket) 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICRzCCAbCgAwIBAgIJAIhPfXaKAijbMA0GCSqGSIb3DQEBBQUAMCIxCzAJBgNV 3 | BAYTAlhZMRMwEQYDVQQIEwpOZXJ2ZXJsYW5kMB4XDTEyMDgyODE2NDIzMloXDTEz 4 | MDgyODE2NDIzMlowIjELMAkGA1UEBhMCWFkxEzARBgNVBAgTCk5lcnZlcmxhbmQw 5 | gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPTe1/MFg4gGjB5Kx/XzP5HQys2k 6 | R+X7+h+cG7jz9p8mNiW/G54WQX7z4EzLrSFrNdXhe6NRXzqcVtyNl9NPh4QpPpOi 7 | ddOjNWworai6d7NShK4gFzL62qY5gSsZ4TYLxfSDEy6Zggy0fFFu8C7iHJVo/1kY 8 | A1OxikDkbfHX0rW3AgMBAAGjgYQwgYEwHQYDVR0OBBYEFEbSBVWuzrSmust9Sa6J 9 | sm7Tg40KMFIGA1UdIwRLMEmAFEbSBVWuzrSmust9Sa6Jsm7Tg40KoSakJDAiMQsw 10 | CQYDVQQGEwJYWTETMBEGA1UECBMKTmVydmVybGFuZIIJAIhPfXaKAijbMAwGA1Ud 11 | EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAS0+17FjKmjCPHfJApTZgVXGD/LHC 12 | Bdzo+bl/hWpMyF27CUJxdZOMRpHqpE/Qv77jG/tfHnuArYPwDcB8AIErmvhNKCZx 13 | GLm19kTd4K1Y5JcAqkOHBma1e1V/g4ryWvUtpQkqD6tQxX0ctlBmXXK/Jsj/wG0W 14 | NNCQq2S19aNZcGo= 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /spec/support/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQD03tfzBYOIBoweSsf18z+R0MrNpEfl+/ofnBu48/afJjYlvxue 3 | FkF+8+BMy60hazXV4XujUV86nFbcjZfTT4eEKT6TonXTozVsKK2ounezUoSuIBcy 4 | +tqmOYErGeE2C8X0gxMumYIMtHxRbvAu4hyVaP9ZGANTsYpA5G3x19K1twIDAQAB 5 | AoGBALdY8BsAIudD98Bqv+RxuUSGMIPfoRIcJMFsUvmeeifaJasHuDcbdPkIxAbc 6 | boraSpoV1kyIDiTFkOhdgLPxFYaxlHGN7c/WqaGMtTMUuKgyItXPEFd9vHOWImIM 7 | gJKaYGSrKbAsiFt1mGBKEPRDE1TwnrPZAKj9mGJA1gtzq6GxAkEA/z3HvaXoBuGw 8 | 9f7uSzURSIkL+HYPejr83IBeqFdH+roxhgkwh+WMWBbNicCu0LsM00QAuZXlnqX7 9 | FUaKguL1GQJBAPWZK/+Gs7LF6GqkANu+FOVhe9zxbffvZ+ibvDraB7fvgay+TwYi 10 | 88J6IhSp30F/tUtTGRl2UyszXLXbAAxpC08CQCwQTFVPOPlHKTeupRDSvoMZNbnV 11 | F+LwIAspFi5VsxVz42zSVVCArnPeq+kmHIfoYtRuHvnrCNMUsH4ByZPC/rECQQDO 12 | GJ+Haq5ZkyKaes4NmNFIPCoJGsDBkrGLzUSDznszq1USdREzgRk1VfBLjtG+0UB9 13 | 2VnyuAzK7+sY4JKF15CZAkAewe1vMGsFAXYojCI+wsDVlvlGTFmAZYnXE4hUVft+ 14 | j/XwOPp7WiWl9dgJ25wQrkYOrYMkZ9SqHO0SSxeSo2yT 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /examples/v2api/remote_throughput.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length != 3 5 | puts "usage: ruby remote_throughput.rb " 6 | Process.exit 7 | end 8 | 9 | connect_to = ARGV[0] 10 | message_size = ARGV[1].to_i 11 | message_count = ARGV[2].to_i 12 | 13 | def assert(rc) 14 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 15 | end 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::PUB) 20 | rescue ContextError => e 21 | STDERR.puts "Could not allocate a context or socket!" 22 | raise 23 | end 24 | 25 | assert(s.setsockopt(ZMQ::LINGER, 1_000)) 26 | assert(s.connect(connect_to)) 27 | 28 | contents = "#{'0'*message_size}" 29 | 30 | i = 0 31 | while i < message_count 32 | msg = ZMQ::Message.new(contents) 33 | assert(s.sendmsg(msg)) 34 | i += 1 35 | end 36 | 37 | assert(s.close) 38 | 39 | ctx.terminate 40 | -------------------------------------------------------------------------------- /examples/v2api/request_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | def assert(rc) 6 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 7 | end 8 | 9 | link = "tcp://127.0.0.1:5555" 10 | 11 | begin 12 | ctx = ZMQ::Context.new 13 | s1 = ctx.socket(ZMQ::REQ) 14 | s2 = ctx.socket(ZMQ::REP) 15 | rescue ContextError => e 16 | STDERR.puts "Failed to allocate context or socket" 17 | raise 18 | end 19 | 20 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 21 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 22 | 23 | assert(s2.bind(link)) 24 | assert(s1.connect(link)) 25 | 26 | payload = "#{ '3' * 2048 }" 27 | sent_msg = ZMQ::Message.new(payload) 28 | received_msg = ZMQ::Message.new 29 | 30 | assert(s1.sendmsg(sent_msg)) 31 | assert(s2.recvmsg(received_msg)) 32 | 33 | result = payload == received_msg.copy_out_string ? "Request received" : "Received wrong payload" 34 | 35 | p result 36 | 37 | assert(s1.close) 38 | assert(s2.close) 39 | 40 | ctx.terminate 41 | -------------------------------------------------------------------------------- /examples/v3api/request_response.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | def assert(rc) 6 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 7 | end 8 | 9 | link = "tcp://127.0.0.1:5555" 10 | 11 | begin 12 | ctx = ZMQ::Context.new 13 | s1 = ctx.socket(ZMQ::REQ) 14 | s2 = ctx.socket(ZMQ::REP) 15 | rescue ContextError => e 16 | STDERR.puts "Failed to allocate context or socket" 17 | raise 18 | end 19 | 20 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 21 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 22 | 23 | assert(s2.bind(link)) 24 | assert(s1.connect(link)) 25 | 26 | payload = "#{ '3' * 2048 }" 27 | sent_msg = ZMQ::Message.new(payload) 28 | received_msg = ZMQ::Message.new 29 | 30 | assert(s1.sendmsg(sent_msg)) 31 | assert(s2.recvmsg(received_msg)) 32 | 33 | result = payload == received_msg.copy_out_string ? "Request received" : "Received wrong payload" 34 | 35 | p result 36 | 37 | assert(s1.close) 38 | assert(s2.close) 39 | 40 | ctx.terminate 41 | -------------------------------------------------------------------------------- /examples/v2api/pub.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length != 3 5 | puts "usage: ruby remote_throughput.rb " 6 | Process.exit 7 | end 8 | 9 | connect_to = ARGV[0] 10 | message_size = ARGV[1].to_i 11 | message_count = ARGV[2].to_i 12 | 13 | def assert(rc) 14 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 15 | end 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::PUB) 20 | rescue ContextError => e 21 | STDERR.puts "Could not allocate a context or socket!" 22 | raise 23 | end 24 | 25 | #assert(s.setsockopt(ZMQ::LINGER, 1_000)) 26 | #assert(s.setsockopt(ZMQ::RCVHWM, 0)) 27 | assert(s.setsockopt(ZMQ::HWM, 10)) 28 | assert(s.bind(connect_to)) 29 | 30 | # the sleep gives the downstream SUB socket a chance to register its 31 | # subscription filters with this PUB socket 32 | puts "Hit any key to start publishing" 33 | STDIN.gets 34 | 35 | i = 0 36 | while i < message_count 37 | msg = ZMQ::Message.new(i.to_s) 38 | assert(s.sendmsg(msg)) 39 | puts i 40 | i += 1 41 | end 42 | 43 | sleep 10 44 | assert(s.close) 45 | 46 | ctx.terminate 47 | -------------------------------------------------------------------------------- /examples/v3api/pub.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length != 3 5 | puts "usage: ruby remote_throughput.rb " 6 | Process.exit 7 | end 8 | 9 | connect_to = ARGV[0] 10 | message_size = ARGV[1].to_i 11 | message_count = ARGV[2].to_i 12 | 13 | def assert(rc) 14 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 15 | end 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::PUB) 20 | rescue ContextError => e 21 | STDERR.puts "Could not allocate a context or socket!" 22 | raise 23 | end 24 | 25 | #assert(s.setsockopt(ZMQ::LINGER, 1_000)) 26 | #assert(s.setsockopt(ZMQ::RCVHWM, 0)) 27 | assert(s.setsockopt(ZMQ::SNDHWM, 100)) 28 | assert(s.bind(connect_to)) 29 | 30 | # the sleep gives the downstream SUB socket a chance to register its 31 | # subscription filters with this PUB socket 32 | puts "Hit any key to start publishing" 33 | STDIN.gets 34 | 35 | i = 0 36 | while i < message_count 37 | msg = ZMQ::Message.new(i.to_s) 38 | assert(s.sendmsg(msg)) 39 | puts i 40 | i += 1 41 | end 42 | 43 | sleep 10 44 | assert(s.close) 45 | 46 | ctx.terminate 47 | -------------------------------------------------------------------------------- /examples/v3api/remote_throughput.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length != 3 5 | puts "usage: ruby remote_throughput.rb " 6 | Process.exit 7 | end 8 | 9 | connect_to = ARGV[0] 10 | message_size = ARGV[1].to_i 11 | message_count = ARGV[2].to_i 12 | 13 | def assert(rc) 14 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 15 | end 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::PUB) 20 | rescue ContextError => e 21 | STDERR.puts "Could not allocate a context or socket!" 22 | raise 23 | end 24 | 25 | #assert(s.setsockopt(ZMQ::LINGER, 1_000)) 26 | #assert(s.setsockopt(ZMQ::RCVHWM, 0)) 27 | #assert(s.setsockopt(ZMQ::SNDHWM, 0)) 28 | assert(s.connect(connect_to)) 29 | 30 | # the sleep gives the downstream SUB socket a chance to register its 31 | # subscription filters with this PUB socket 32 | sleep 1 33 | 34 | contents = "#{'0'*message_size}" 35 | 36 | i = 0 37 | while i < message_count 38 | msg = ZMQ::Message.new(contents) 39 | assert(s.sendmsg(msg)) 40 | puts i 41 | i += 1 42 | end 43 | 44 | sleep 10 45 | assert(s.close) 46 | 47 | ctx.terminate 48 | -------------------------------------------------------------------------------- /ffi-rzmq.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "ffi-rzmq/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "ffi-rzmq" 7 | s.version = ZMQ::VERSION 8 | s.authors = ["Chuck Remes"] 9 | s.email = ["git@chuckremes.com"] 10 | s.homepage = "http://github.com/chuckremes/ffi-rzmq" 11 | s.summary = %q{This gem wraps the ZeroMQ (0mq) networking library using Ruby FFI (foreign function interface).} 12 | s.description = %q{This gem wraps the ZeroMQ networking library using the ruby FFI (foreign 13 | function interface). It's a pure ruby wrapper so this gem can be loaded 14 | and run by any ruby runtime that supports FFI. That's all of the major ones - 15 | MRI, Rubinius and JRuby.} 16 | 17 | s.rubyforge_project = "ffi-rzmq" 18 | 19 | s.files = `git ls-files`.split("\n") 20 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 22 | s.require_paths = ["lib"] 23 | 24 | s.add_runtime_dependency "ffi"#, [">= 1.0.9"] 25 | s.add_development_dependency "rspec", ["~> 2.6"] 26 | s.add_development_dependency "rake" 27 | end 28 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/exceptions.rb: -------------------------------------------------------------------------------- 1 | 2 | module ZMQ 3 | 4 | class ZeroMQError < StandardError 5 | attr_reader :source, :result_code, :error_code, :message 6 | 7 | def initialize source, result_code, error_code, message 8 | @source = source 9 | @result_code = result_code 10 | @error_code = error_code 11 | @message = "msg [#{message}], error code [#{error_code}], rc [#{result_code}]" 12 | super message 13 | end 14 | end # call ZeroMQError 15 | 16 | 17 | class ContextError < ZeroMQError 18 | # True when the exception was raised due to the library 19 | # returning EINVAL. 20 | # 21 | # Occurs when he number of app_threads requested is less 22 | # than one, or the number of io_threads requested is 23 | # negative. 24 | # 25 | def einval?() EINVAL == @error_code; end 26 | 27 | # True when the exception was raised due to the library 28 | # returning ETERM. 29 | # 30 | # The associated context was terminated. 31 | # 32 | def eterm?() ETERM == @error_code; end 33 | 34 | end # class ContextError 35 | 36 | 37 | class MessageError < ZeroMQError 38 | # True when the exception was raised due to the library 39 | # returning ENOMEM. 40 | # 41 | # Only ever raised by the #Message class when it fails 42 | # to allocate sufficient memory to send a message. 43 | # 44 | def enomem?() ENOMEM == @error_code; end 45 | end 46 | 47 | end # module ZMQ 48 | -------------------------------------------------------------------------------- /examples/v2api/local_throughput.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length != 3 5 | puts "usage: ruby local_throughtput.rb " 6 | Process.exit 7 | end 8 | 9 | def assert(rc) 10 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 11 | end 12 | 13 | bind_to = ARGV[0] 14 | message_size = ARGV[1].to_i 15 | message_count = ARGV[2].to_i 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::SUB) 20 | rescue ContextError => e 21 | STDERR.puts "Failed to allocate context or socket!" 22 | raise 23 | end 24 | 25 | assert(s.setsockopt(ZMQ::LINGER, 100)) 26 | assert(s.setsockopt(ZMQ::SUBSCRIBE, "")) 27 | 28 | assert(s.bind(bind_to)) 29 | 30 | msg = ZMQ::Message.new 31 | assert(s.recvmsg(msg)) 32 | 33 | start_time = Time.now 34 | 35 | i = 1 36 | while i < message_count 37 | assert(s.recvmsg(msg)) 38 | i += 1 39 | end 40 | 41 | end_time = Time.now 42 | 43 | elapsed = (end_time.to_f - start_time.to_f) * 1000000 44 | if elapsed == 0 45 | elapsed = 1 46 | end 47 | 48 | throughput = message_count * 1000000 / elapsed 49 | megabits = throughput * message_size * 8 / 1000000 50 | 51 | puts "message size: %i [B]" % message_size 52 | puts "message count: %i" % message_count 53 | puts "mean throughput: %i [msg/s]" % throughput 54 | puts "mean throughput: %.3f [Mb/s]" % megabits 55 | 56 | assert(s.close) 57 | 58 | ctx.terminate 59 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/poll_item.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | require 'io_extensions' 3 | 4 | module ZMQ 5 | class PollItem 6 | extend Forwardable 7 | 8 | def_delegators :@poll_item, :pointer, :readable?, :writable? 9 | attr_accessor :pollable, :poll_item 10 | 11 | def initialize(zmq_poll_item = nil) 12 | @poll_item = zmq_poll_item || LibZMQ::PollItem.new 13 | end 14 | 15 | def self.from_pointer(pointer) 16 | self.new(LibZMQ::PollItem.new(pointer)) 17 | end 18 | 19 | def self.from_pollable(pollable) 20 | item = self.new 21 | item.pollable = pollable 22 | case 23 | when pollable.respond_to?(:socket) 24 | item.socket = pollable.socket 25 | when pollable.respond_to?(:posix_fileno) 26 | item.fd = pollable.posix_fileno 27 | when pollable.respond_to?(:io) 28 | item.fd = pollable.io.posix_fileno 29 | end 30 | item 31 | end 32 | 33 | def closed? 34 | case 35 | when pollable.respond_to?(:closed?) 36 | pollable.closed? 37 | when pollable.respond_to?(:socket) 38 | pollable.socket.nil? 39 | when pollable.respond_to?(:io) 40 | pollable.io.closed? 41 | end 42 | end 43 | 44 | def socket=(arg); @poll_item[:socket] = arg; end 45 | 46 | def socket; @poll_item[:socket]; end 47 | 48 | def fd=(arg); @poll_item[:fd] = arg; end 49 | 50 | def fd; @poll_item[:fd]; end 51 | 52 | def events=(arg); @poll_item[:events] = arg; end 53 | 54 | def events; @poll_item[:events]; end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /examples/v2api/reqrep_poll.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | def assert(rc) 6 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 7 | end 8 | 9 | link = "tcp://127.0.0.1:5554" 10 | 11 | begin 12 | ctx = ZMQ::Context.new 13 | s1 = ctx.socket(ZMQ::REQ) 14 | s2 = ctx.socket(ZMQ::REP) 15 | rescue ContextError => e 16 | STDERR.puts "Failed to allocate context or socket!" 17 | raise 18 | end 19 | 20 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 21 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 22 | 23 | assert(s1.connect(link)) 24 | assert(s2.bind(link)) 25 | 26 | poller = ZMQ::Poller.new 27 | poller.register_readable(s2) 28 | poller.register_writable(s1) 29 | 30 | start_time = Time.now 31 | @unsent = true 32 | 33 | until @done do 34 | assert(poller.poll_nonblock) 35 | 36 | # send the message after 5 seconds 37 | if Time.now - start_time > 5 && @unsent 38 | payload = "#{ '3' * 1024 }" 39 | 40 | puts "sending payload nonblocking" 41 | assert(s1.send_string(payload, ZMQ::NonBlocking)) 42 | @unsent = false 43 | end 44 | 45 | # check for messages after 1 second 46 | if Time.now - start_time > 1 47 | poller.readables.each do |sock| 48 | received_msg = '' 49 | assert(sock.recv_string(received_msg, ZMQ::NonBlocking)) 50 | 51 | puts "message received [#{received_msg}]" 52 | @done = true 53 | end 54 | end 55 | end 56 | 57 | puts "executed in [#{Time.now - start_time}] seconds" 58 | 59 | assert(s1.close) 60 | assert(s2.close) 61 | 62 | ctx.terminate 63 | -------------------------------------------------------------------------------- /examples/v3api/reqrep_poll.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | def assert(rc) 6 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 7 | end 8 | 9 | link = "tcp://127.0.0.1:5554" 10 | 11 | begin 12 | ctx = ZMQ::Context.new 13 | s1 = ctx.socket(ZMQ::REQ) 14 | s2 = ctx.socket(ZMQ::REP) 15 | rescue ContextError => e 16 | STDERR.puts "Failed to allocate context or socket!" 17 | raise 18 | end 19 | 20 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 21 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 22 | 23 | assert(s1.connect(link)) 24 | assert(s2.bind(link)) 25 | 26 | poller = ZMQ::Poller.new 27 | poller.register_readable(s2) 28 | poller.register_writable(s1) 29 | 30 | start_time = Time.now 31 | @unsent = true 32 | 33 | until @done do 34 | assert(poller.poll_nonblock) 35 | 36 | # send the message after 5 seconds 37 | if Time.now - start_time > 5 && @unsent 38 | payload = "#{ '3' * 1024 }" 39 | 40 | puts "sending payload nonblocking" 41 | assert(s1.send_string(payload, ZMQ::NonBlocking)) 42 | @unsent = false 43 | end 44 | 45 | # check for messages after 1 second 46 | if Time.now - start_time > 1 47 | poller.readables.each do |sock| 48 | received_msg = '' 49 | assert(sock.recv_string(received_msg, ZMQ::NonBlocking)) 50 | 51 | puts "message received [#{received_msg}]" 52 | @done = true 53 | end 54 | end 55 | end 56 | 57 | puts "executed in [#{Time.now - start_time}] seconds" 58 | 59 | assert(s1.close) 60 | assert(s2.close) 61 | 62 | ctx.terminate 63 | -------------------------------------------------------------------------------- /examples/v3api/local_throughput.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length != 3 5 | puts "usage: ruby local_throughtput.rb " 6 | Process.exit 7 | end 8 | 9 | def assert(rc) 10 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 11 | end 12 | 13 | bind_to = ARGV[0] 14 | message_size = ARGV[1].to_i 15 | message_count = ARGV[2].to_i 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::SUB) 20 | rescue ContextError => e 21 | STDERR.puts "Failed to allocate context or socket!" 22 | raise 23 | end 24 | 25 | #assert(s.setsockopt(ZMQ::LINGER, 100)) 26 | assert(s.setsockopt(ZMQ::SUBSCRIBE, "")) 27 | #assert(s.setsockopt(ZMQ::RCVHWM, 0)) 28 | #assert(s.setsockopt(ZMQ::SNDHWM, 0)) 29 | 30 | assert(s.bind(bind_to)) 31 | sleep 1 32 | 33 | msg = ZMQ::Message.new 34 | msg = '' 35 | assert(s.recv_string(msg)) 36 | #assert(s.recvmsg(msg)) 37 | 38 | start_time = Time.now 39 | 40 | i = 1 41 | while i < message_count 42 | #assert(s.recvmsg(msg)) 43 | assert(s.recv_string(msg)) 44 | puts i 45 | i += 1 46 | end 47 | 48 | end_time = Time.now 49 | 50 | elapsed = (end_time.to_f - start_time.to_f) * 1000000 51 | if elapsed == 0 52 | elapsed = 1 53 | end 54 | 55 | throughput = message_count * 1000000 / elapsed 56 | megabits = throughput * message_size * 8 / 1000000 57 | 58 | puts "message size: %i [B]" % message_size 59 | puts "message count: %i" % message_count 60 | puts "mean throughput: %i [msg/s]" % throughput 61 | puts "mean throughput: %.3f [Mb/s]" % megabits 62 | 63 | assert(s.close) 64 | 65 | ctx.terminate 66 | -------------------------------------------------------------------------------- /examples/v2api/local_lat_poll.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length < 3 5 | puts "usage: ruby local_lat.rb " 6 | exit 7 | end 8 | 9 | link = ARGV[0] 10 | message_size = ARGV[1].to_i 11 | roundtrip_count = ARGV[2].to_i 12 | 13 | def assert(rc) 14 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 15 | end 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s1 = ctx.socket(ZMQ::REQ) 20 | s2 = ctx.socket(ZMQ::REP) 21 | rescue ContextError => e 22 | STDERR.puts "Failed to allocate context or socket!" 23 | raise 24 | end 25 | 26 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 27 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 28 | 29 | assert(s1.connect(link)) 30 | assert(s2.bind(link)) 31 | 32 | poller = ZMQ::Poller.new 33 | poller.register_readable(s2) 34 | poller.register_readable(s1) 35 | 36 | 37 | start_time = Time.now 38 | 39 | # kick it off 40 | message = ZMQ::Message.new("a" * message_size) 41 | assert(s1.sendmsg(message, ZMQ::NonBlocking)) 42 | 43 | i = roundtrip_count 44 | 45 | until i.zero? 46 | i -= 1 47 | 48 | assert(poller.poll_nonblock) 49 | 50 | poller.readables.each do |socket| 51 | received_message = '' 52 | assert(socket.recv_string(received_message, ZMQ::NonBlocking)) 53 | assert(socket.sendmsg(ZMQ::Message.new(received_message), ZMQ::NonBlocking)) 54 | end 55 | end 56 | 57 | elapsed_usecs = (Time.now.to_f - start_time.to_f) * 1_000_000 58 | latency = elapsed_usecs / roundtrip_count / 2 59 | 60 | puts "mean latency: %.3f [us]" % latency 61 | puts "received all messages in %.3f seconds" % (elapsed_usecs / 1_000_000) 62 | 63 | assert(s1.close) 64 | assert(s2.close) 65 | 66 | ctx.terminate 67 | -------------------------------------------------------------------------------- /examples/v3api/local_lat_poll.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | if ARGV.length < 3 5 | puts "usage: ruby local_lat.rb " 6 | exit 7 | end 8 | 9 | link = ARGV[0] 10 | message_size = ARGV[1].to_i 11 | roundtrip_count = ARGV[2].to_i 12 | 13 | def assert(rc) 14 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 15 | end 16 | 17 | begin 18 | ctx = ZMQ::Context.new 19 | s1 = ctx.socket(ZMQ::REQ) 20 | s2 = ctx.socket(ZMQ::REP) 21 | rescue ContextError => e 22 | STDERR.puts "Failed to allocate context or socket!" 23 | raise 24 | end 25 | 26 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 27 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 28 | 29 | assert(s1.connect(link)) 30 | assert(s2.bind(link)) 31 | 32 | poller = ZMQ::Poller.new 33 | poller.register_readable(s2) 34 | poller.register_readable(s1) 35 | 36 | 37 | start_time = Time.now 38 | 39 | # kick it off 40 | message = ZMQ::Message.new("a" * message_size) 41 | assert(s1.sendmsg(message, ZMQ::NonBlocking)) 42 | 43 | i = roundtrip_count 44 | 45 | until i.zero? 46 | i -= 1 47 | 48 | assert(poller.poll_nonblock) 49 | 50 | poller.readables.each do |socket| 51 | received_message = '' 52 | assert(socket.recv_string(received_message, ZMQ::NonBlocking)) 53 | assert(socket.sendmsg(ZMQ::Message.new(received_message), ZMQ::NonBlocking)) 54 | end 55 | end 56 | 57 | elapsed_usecs = (Time.now.to_f - start_time.to_f) * 1_000_000 58 | latency = elapsed_usecs / roundtrip_count / 2 59 | 60 | puts "mean latency: %.3f [us]" % latency 61 | puts "received all messages in %.3f seconds" % (elapsed_usecs / 1_000_000) 62 | 63 | assert(s1.close) 64 | assert(s2.close) 65 | 66 | ctx.terminate 67 | -------------------------------------------------------------------------------- /examples/v3api/sub.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | #if ARGV.length != 3 5 | # puts "usage: ruby local_throughtput.rb " 6 | # Process.exit 7 | #end 8 | p ZMQ::Util.version 9 | 10 | def assert(rc) 11 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 12 | end 13 | 14 | bind_to = ARGV[0] 15 | message_size = ARGV[1].to_i 16 | message_count = ARGV[2].to_i 17 | sleep_time = ARGV[3].to_f 18 | 19 | begin 20 | ctx = ZMQ::Context.new 21 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::SUB) 22 | rescue ContextError => e 23 | STDERR.puts "Failed to allocate context or socket!" 24 | raise 25 | end 26 | 27 | #assert(s.setsockopt(ZMQ::LINGER, 100)) 28 | assert(s.setsockopt(ZMQ::SUBSCRIBE, "")) 29 | assert(s.setsockopt(ZMQ::RCVHWM, 20)) 30 | #assert(s.setsockopt(ZMQ::RCVHWM, 0)) 31 | #assert(s.setsockopt(ZMQ::SNDHWM, 0)) 32 | 33 | assert(s.connect(bind_to)) 34 | sleep 1 35 | 36 | msg = ZMQ::Message.new 37 | msg = '' 38 | assert(s.recv_string(msg)) 39 | raise unless msg.to_i == 0 40 | 41 | start_time = Time.now 42 | 43 | i = 1 44 | while i < message_count 45 | assert(s.recv_string(msg)) 46 | msg_i = msg.to_i 47 | puts "missed [#{msg_i - i}] messages" 48 | i = msg_i 49 | sleep(sleep_time) 50 | end 51 | 52 | end_time = Time.now 53 | 54 | elapsed = (end_time.to_f - start_time.to_f) * 1000000 55 | if elapsed == 0 56 | elapsed = 1 57 | end 58 | 59 | throughput = message_count * 1000000 / elapsed 60 | megabits = throughput * message_size * 8 / 1000000 61 | 62 | puts "message size: %i [B]" % message_size 63 | puts "message count: %i" % message_count 64 | puts "mean throughput: %i [msg/s]" % throughput 65 | puts "mean throughput: %.3f [Mb/s]" % megabits 66 | 67 | assert(s.close) 68 | 69 | ctx.terminate 70 | -------------------------------------------------------------------------------- /examples/v2api/local_lat.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2007-2010 iMatix Corporation 3 | # 4 | # This file is part of 0MQ. 5 | # 6 | # 0MQ is free software; you can redistribute it and/or modify it under 7 | # the terms of the Lesser GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 0MQ is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # Lesser GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the Lesser GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 20 | 21 | if ARGV.length < 3 22 | puts "usage: ruby local_lat.rb " 23 | exit 24 | end 25 | 26 | bind_to = ARGV[0] 27 | message_size = ARGV[1].to_i 28 | roundtrip_count = ARGV[2].to_i 29 | 30 | def assert(rc) 31 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 32 | end 33 | 34 | begin 35 | ctx = ZMQ::Context.new 36 | s = ctx.socket(ZMQ::REP) 37 | rescue ContextError => e 38 | STDERR.puts "Failed to allocate context or socket!" 39 | raise 40 | end 41 | 42 | assert(s.setsockopt(ZMQ::LINGER, 100)) 43 | assert(s.setsockopt(ZMQ::HWM, 100)) 44 | 45 | assert(s.bind(bind_to)) 46 | 47 | roundtrip_count.times do 48 | string = '' 49 | assert(s.recv_string(string, 0)) 50 | 51 | raise "Message size doesn't match, expected [#{message_size}] but received [#{string.size}]" if message_size != string.size 52 | 53 | assert(s.send_string(string, 0)) 54 | end 55 | 56 | assert(s.close) 57 | 58 | ctx.terminate 59 | -------------------------------------------------------------------------------- /examples/v3api/local_lat.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2007-2010 iMatix Corporation 3 | # 4 | # This file is part of 0MQ. 5 | # 6 | # 0MQ is free software; you can redistribute it and/or modify it under 7 | # the terms of the Lesser GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 0MQ is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # Lesser GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the Lesser GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 20 | 21 | if ARGV.length < 3 22 | puts "usage: ruby local_lat.rb " 23 | exit 24 | end 25 | 26 | bind_to = ARGV[0] 27 | message_size = ARGV[1].to_i 28 | roundtrip_count = ARGV[2].to_i 29 | 30 | def assert(rc) 31 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 32 | end 33 | 34 | begin 35 | ctx = ZMQ::Context.new 36 | s = ctx.socket(ZMQ::REP) 37 | rescue ContextError => e 38 | STDERR.puts "Failed to allocate context or socket!" 39 | raise 40 | end 41 | 42 | assert(s.setsockopt(ZMQ::LINGER, 100)) 43 | assert(s.setsockopt(ZMQ::RCVHWM, 100)) 44 | assert(s.setsockopt(ZMQ::SNDHWM, 100)) 45 | 46 | assert(s.bind(bind_to)) 47 | 48 | roundtrip_count.times do 49 | string = '' 50 | assert(s.recv_string(string, 0)) 51 | 52 | raise "Message size doesn't match, expected [#{message_size}] but received [#{string.size}]" if message_size != string.size 53 | 54 | assert(s.send_string(string, 0)) 55 | end 56 | 57 | assert(s.close) 58 | 59 | ctx.terminate 60 | -------------------------------------------------------------------------------- /examples/v2api/sub.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | #if ARGV.length != 3 5 | # puts "usage: ruby local_throughtput.rb " 6 | # Process.exit 7 | #end 8 | p ZMQ::Util.version 9 | 10 | def assert(rc) 11 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 12 | end 13 | 14 | bind_to = ARGV[0] 15 | message_size = ARGV[1].to_i 16 | message_count = ARGV[2].to_i 17 | sleep_time = ARGV[3].to_f 18 | 19 | begin 20 | ctx = ZMQ::Context.new 21 | s = ZMQ::Socket.new(ctx.pointer, ZMQ::SUB) 22 | rescue ContextError => e 23 | STDERR.puts "Failed to allocate context or socket!" 24 | raise 25 | end 26 | 27 | #assert(s.setsockopt(ZMQ::LINGER, 100)) 28 | assert(s.setsockopt(ZMQ::IDENTITY, rand(999_999).to_s)) 29 | assert(s.setsockopt(ZMQ::SUBSCRIBE, "")) 30 | assert(s.setsockopt(ZMQ::HWM, 1)) 31 | #assert(s.setsockopt(ZMQ::RCVHWM, 0)) 32 | #assert(s.setsockopt(ZMQ::SNDHWM, 0)) 33 | 34 | assert(s.connect(bind_to)) 35 | sleep 1 36 | 37 | msg = ZMQ::Message.new 38 | msg = '' 39 | assert(s.recv_string(msg)) 40 | raise unless msg.to_i == 0 41 | 42 | start_time = Time.now 43 | 44 | i = 1 45 | while i < message_count - 1 46 | assert(s.recv_string(msg)) 47 | msg_i = msg.to_i 48 | missed = (msg_i - i) - 1 49 | puts "missed [#{missed}] messages" if missed > 0 50 | i = msg_i 51 | 52 | start = Time.now 53 | while (Time.now - start) < sleep_time 54 | end 55 | end 56 | 57 | end_time = Time.now 58 | 59 | elapsed = (end_time.to_f - start_time.to_f) * 1000000 60 | if elapsed == 0 61 | elapsed = 1 62 | end 63 | 64 | throughput = message_count * 1000000 / elapsed 65 | megabits = throughput * message_size * 8 / 1000000 66 | 67 | puts "message size: %i [B]" % message_size 68 | puts "message count: %i" % message_count 69 | puts "mean throughput: %i [msg/s]" % throughput 70 | puts "mean throughput: %.3f [Mb/s]" % megabits 71 | 72 | assert(s.close) 73 | 74 | ctx.terminate 75 | -------------------------------------------------------------------------------- /spec/device_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 3 | 4 | module ZMQ 5 | describe Device do 6 | include APIHelper 7 | 8 | before(:all) do 9 | @ctx = Context.new 10 | poller_setup 11 | @front_endpoint = "inproc://device_front_test" 12 | @back_endpoint = "inproc://device_back_test" 13 | @mutex = Mutex.new 14 | end 15 | 16 | after(:all) do 17 | @ctx.terminate 18 | end 19 | 20 | def create_streamer 21 | @device_thread = false 22 | 23 | Thread.new do 24 | back = @ctx.socket(ZMQ::PULL) 25 | back.bind(@back_endpoint) 26 | front = @ctx.socket(ZMQ::PUSH) 27 | front.bind(@front_endpoint) 28 | @mutex.synchronize { @device_thread = true } 29 | Device.new(ZMQ::STREAMER, back, front) 30 | back.close 31 | front.close 32 | end 33 | end 34 | 35 | def wait_for_device 36 | loop do 37 | can_break = false 38 | @mutex.synchronize do 39 | can_break = true if @device_thread 40 | end 41 | break if can_break 42 | end 43 | end 44 | 45 | it "should create a device without error given valid opts" do 46 | create_streamer 47 | wait_for_device 48 | end 49 | 50 | it "should be able to send messages through the device" do 51 | create_streamer 52 | wait_for_device 53 | 54 | pusher = @ctx.socket(ZMQ::PUSH) 55 | connect_to_inproc(pusher, @back_endpoint) 56 | puller = @ctx.socket(ZMQ::PULL) 57 | connect_to_inproc(puller, @front_endpoint) 58 | 59 | poll_it_for_read(puller) do 60 | pusher.send_string("hello") 61 | end 62 | 63 | res = '' 64 | rc = puller.recv_string(res, ZMQ::NonBlocking) 65 | res.should == "hello" 66 | 67 | pusher.close 68 | puller.close 69 | end 70 | 71 | it "should raise an ArgumentError when trying to pass non-socket objects into the device" do 72 | lambda { 73 | Device.new(ZMQ::STREAMER, 1,2) 74 | }.should raise_exception(ArgumentError) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/poll_items.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | require 'ostruct' 3 | 4 | module ZMQ 5 | class PollItems 6 | include Enumerable 7 | extend Forwardable 8 | 9 | def_delegators :@pollables, :size, :empty? 10 | 11 | def initialize 12 | @pollables = {} 13 | @item_size = LibZMQ::PollItem.size 14 | @item_store = nil 15 | end 16 | 17 | def address 18 | clean 19 | @item_store 20 | end 21 | 22 | def get pollable 23 | return unless entry = @pollables[pollable] 24 | clean 25 | pointer = @item_store + (@item_size * entry.index) 26 | item = ZMQ::PollItem.from_pointer(pointer) 27 | item.pollable = pollable 28 | item 29 | end 30 | alias :[] :get 31 | 32 | def <<(poll_item) 33 | @dirty = true 34 | @pollables[poll_item.pollable] = OpenStruct.new(:index => size, :data => poll_item) 35 | end 36 | alias :push :<< 37 | 38 | def delete pollable 39 | if @pollables.delete(pollable) 40 | @dirty = true 41 | clean 42 | true 43 | else 44 | false 45 | end 46 | end 47 | 48 | def each &blk 49 | clean 50 | @pollables.each_key do |pollable| 51 | yield get(pollable) 52 | end 53 | end 54 | 55 | def inspect 56 | clean 57 | str = "" 58 | each { |item| str << "ptr [#{item[:socket]}], events [#{item[:events]}], revents [#{item[:revents]}], " } 59 | str.chop.chop 60 | end 61 | 62 | def to_s; inspect; end 63 | 64 | private 65 | 66 | # Allocate a contiguous chunk of memory and copy over the PollItem structs 67 | # to this block. Note that the old +@store+ value goes out of scope so when 68 | # it is garbage collected that native memory should be automatically freed. 69 | def clean 70 | if @dirty 71 | @item_store = FFI::MemoryPointer.new @item_size, size, true 72 | 73 | offset = 0 74 | @pollables.each_with_index do |(pollable, entry), index| 75 | entry.index = index 76 | LibC.memcpy(@item_store + offset, entry.data.pointer, @item_size) 77 | offset += @item_size 78 | end 79 | 80 | @dirty = false 81 | end 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /examples/v2api/remote_lat.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2007-2010 iMatix Corporation 3 | # 4 | # This file is part of 0MQ. 5 | # 6 | # 0MQ is free software; you can redistribute it and/or modify it under 7 | # the terms of the Lesser GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 0MQ is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # Lesser GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the Lesser GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | 20 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 21 | 22 | if ARGV.length < 3 23 | puts "usage: ruby remote_lat.rb " 24 | exit 25 | end 26 | 27 | def assert(rc) 28 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 29 | end 30 | 31 | connect_to = ARGV[0] 32 | message_size = ARGV[1].to_i 33 | roundtrip_count = ARGV[2].to_i 34 | 35 | begin 36 | ctx = ZMQ::Context.new 37 | s = ctx.socket(ZMQ::REQ) 38 | rescue ContextError => e 39 | STDERR.puts "Failed to allocate context or socket!" 40 | raise 41 | end 42 | 43 | assert(s.setsockopt(ZMQ::LINGER, 100)) 44 | assert(s.connect(connect_to)) 45 | 46 | msg = "#{ '3' * message_size }" 47 | 48 | start_time = Time.now 49 | 50 | roundtrip_count.times do 51 | assert(s.send_string(msg, 0)) 52 | 53 | msg = '' 54 | assert(s.recv_string(msg, 0)) 55 | 56 | raise "Message size doesn't match, expected [#{message_size}] but received [#{msg.size}]" if message_size != msg.size 57 | end 58 | 59 | end_time = Time.now 60 | elapsed_secs = (end_time.to_f - start_time.to_f) 61 | elapsed_usecs = elapsed_secs * 1000000 62 | latency = elapsed_usecs / roundtrip_count / 2 63 | 64 | puts "message size: %i [B]" % message_size 65 | puts "roundtrip count: %i" % roundtrip_count 66 | puts "throughput (msgs/s): %i" % (roundtrip_count / elapsed_secs) 67 | puts "mean latency: %.3f [us]" % latency 68 | 69 | assert(s.close) 70 | 71 | ctx.terminate 72 | -------------------------------------------------------------------------------- /examples/v3api/remote_lat.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2007-2010 iMatix Corporation 3 | # 4 | # This file is part of 0MQ. 5 | # 6 | # 0MQ is free software; you can redistribute it and/or modify it under 7 | # the terms of the Lesser GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # 0MQ is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # Lesser GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the Lesser GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | 20 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 21 | 22 | if ARGV.length < 3 23 | puts "usage: ruby remote_lat.rb " 24 | exit 25 | end 26 | 27 | def assert(rc) 28 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 29 | end 30 | 31 | connect_to = ARGV[0] 32 | message_size = ARGV[1].to_i 33 | roundtrip_count = ARGV[2].to_i 34 | 35 | begin 36 | ctx = ZMQ::Context.new 37 | s = ctx.socket(ZMQ::REQ) 38 | rescue ContextError => e 39 | STDERR.puts "Failed to allocate context or socket!" 40 | raise 41 | end 42 | 43 | assert(s.setsockopt(ZMQ::LINGER, 100)) 44 | assert(s.connect(connect_to)) 45 | 46 | msg = "#{ '3' * message_size }" 47 | 48 | start_time = Time.now 49 | 50 | roundtrip_count.times do 51 | assert(s.send_string(msg, 0)) 52 | 53 | msg = '' 54 | assert(s.recv_string(msg, 0)) 55 | 56 | raise "Message size doesn't match, expected [#{message_size}] but received [#{msg.size}]" if message_size != msg.size 57 | end 58 | 59 | end_time = Time.now 60 | elapsed_secs = (end_time.to_f - start_time.to_f) 61 | elapsed_usecs = elapsed_secs * 1000000 62 | latency = elapsed_usecs / roundtrip_count / 2 63 | 64 | puts "message size: %i [B]" % message_size 65 | puts "roundtrip count: %i" % roundtrip_count 66 | puts "throughput (msgs/s): %i" % (roundtrip_count / elapsed_secs) 67 | puts "mean latency: %.3f [us]" % latency 68 | 69 | assert(s.close) 70 | 71 | ctx.terminate 72 | -------------------------------------------------------------------------------- /lib/ffi-rzmq.rb: -------------------------------------------------------------------------------- 1 | module ZMQ 2 | 3 | # :stopdoc: 4 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR 5 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR 6 | # :startdoc: 7 | 8 | # Returns the version string for the library. 9 | # 10 | def self.version 11 | @version ||= ZMQ::VERSION 12 | end 13 | 14 | # Returns the library path for the module. If any arguments are given, 15 | # they will be joined to the end of the libray path using 16 | # File.join. 17 | # 18 | def self.libpath( *args, &block ) 19 | rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten) 20 | if block 21 | begin 22 | $LOAD_PATH.unshift LIBPATH 23 | rv = block.call 24 | ensure 25 | $LOAD_PATH.shift 26 | end 27 | end 28 | return rv 29 | end 30 | 31 | # Returns the lpath for the module. If any arguments are given, 32 | # they will be joined to the end of the path using 33 | # File.join. 34 | # 35 | def self.path( *args, &block ) 36 | rv = args.empty? ? PATH : ::File.join(PATH, args.flatten) 37 | if block 38 | begin 39 | $LOAD_PATH.unshift PATH 40 | rv = block.call 41 | ensure 42 | $LOAD_PATH.shift 43 | end 44 | end 45 | return rv 46 | end 47 | 48 | # Utility method used to require all files ending in .rb that lie in the 49 | # directory below this file that has the same name as the filename passed 50 | # in. Optionally, a specific _directory_ name can be passed in such that 51 | # the _filename_ does not have to be equivalent to the directory. 52 | # 53 | def self.require_all_libs_relative_to( fname, dir = nil ) 54 | dir ||= ::File.basename(fname, '.*') 55 | search_me = ::File.expand_path( 56 | ::File.join(::File.dirname(fname), dir, '**', '*.rb')) 57 | 58 | Dir.glob(search_me).sort.each {|rb| require rb} 59 | end 60 | 61 | end # module ZMQ 62 | 63 | # some code is conditionalized based upon what ruby engine we are 64 | # executing 65 | 66 | require 'ffi' 67 | 68 | # the order of files is important 69 | #%w(wrapper zmq exceptions context message socket poll_items poll device).each do |file| 70 | %w(libc libzmq constants util exceptions context message socket poll_items poll_item poll device).each do |file| 71 | require ZMQ.libpath(['ffi-rzmq', file]) 72 | end 73 | -------------------------------------------------------------------------------- /examples/v2api/publish_subscribe.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 2 | 3 | 4 | def assert(rc) 5 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 6 | end 7 | 8 | link = "tcp://127.0.0.1:5555" 9 | 10 | begin 11 | ctx = ZMQ::Context.new 12 | s1 = ctx.socket(ZMQ::PUB) 13 | s2 = ctx.socket(ZMQ::SUB) 14 | s3 = ctx.socket(ZMQ::SUB) 15 | s4 = ctx.socket(ZMQ::SUB) 16 | s5 = ctx.socket(ZMQ::SUB) 17 | rescue ContextError => e 18 | STDERR.puts "Failed to allocate context or socket!" 19 | raise 20 | end 21 | 22 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 23 | assert(s2.setsockopt(ZMQ::SUBSCRIBE, '')) # receive all 24 | assert(s3.setsockopt(ZMQ::SUBSCRIBE, 'animals')) # receive any starting with this string 25 | assert(s4.setsockopt(ZMQ::SUBSCRIBE, 'animals.dog')) 26 | assert(s5.setsockopt(ZMQ::SUBSCRIBE, 'animals.cat')) 27 | 28 | assert(s1.bind(link)) 29 | assert(s2.connect(link)) 30 | assert(s3.connect(link)) 31 | assert(s4.connect(link)) 32 | assert(s5.connect(link)) 33 | 34 | sleep 1 35 | 36 | topic = "animals.dog" 37 | payload = "Animal crackers!" 38 | 39 | s1.identity = "publisher-A" 40 | puts "sending" 41 | # use the new multi-part messaging support to 42 | # automatically separate the topic from the body 43 | assert(s1.send_string(topic, ZMQ::SNDMORE)) 44 | assert(s1.send_string(payload, ZMQ::SNDMORE)) 45 | assert(s1.send_string(s1.identity)) 46 | 47 | topic = '' 48 | assert(s2.recv_string(topic)) 49 | 50 | body = '' 51 | assert(s2.recv_string(body)) if s2.more_parts? 52 | 53 | identity = '' 54 | assert(s2.recv_string(identity)) if s2.more_parts? 55 | puts "s2 received topic [#{topic}], body [#{body}], identity [#{identity}]" 56 | 57 | 58 | 59 | topic = '' 60 | assert(s3.recv_string(topic)) 61 | 62 | body = '' 63 | assert(s3.recv_string(body)) if s3.more_parts? 64 | puts "s3 received topic [#{topic}], body [#{body}]" 65 | 66 | topic = '' 67 | assert(s4.recv_string(topic)) 68 | 69 | body = '' 70 | assert(s4.recv_string(body)) if s4.more_parts? 71 | puts "s4 received topic [#{topic}], body [#{body}]" 72 | 73 | s5_string = '' 74 | rc = s5.recv_string(s5_string, ZMQ::NonBlocking) 75 | eagain = (rc == -1 && ZMQ::Util.errno == ZMQ::EAGAIN) 76 | puts(eagain ? "s5 received no messages" : "s5 FAILED") 77 | 78 | [s1, s2, s3, s4, s5].each do |socket| 79 | assert(socket.close) 80 | end 81 | 82 | ctx.terminate 83 | -------------------------------------------------------------------------------- /examples/v3api/publish_subscribe.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 2 | 3 | 4 | def assert(rc) 5 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 6 | end 7 | 8 | link = "tcp://127.0.0.1:5555" 9 | 10 | begin 11 | ctx = ZMQ::Context.new 12 | s1 = ctx.socket(ZMQ::PUB) 13 | s2 = ctx.socket(ZMQ::SUB) 14 | s3 = ctx.socket(ZMQ::SUB) 15 | s4 = ctx.socket(ZMQ::SUB) 16 | s5 = ctx.socket(ZMQ::SUB) 17 | rescue ContextError => e 18 | STDERR.puts "Failed to allocate context or socket!" 19 | raise 20 | end 21 | 22 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 23 | assert(s2.setsockopt(ZMQ::SUBSCRIBE, '')) # receive all 24 | assert(s3.setsockopt(ZMQ::SUBSCRIBE, 'animals')) # receive any starting with this string 25 | assert(s4.setsockopt(ZMQ::SUBSCRIBE, 'animals.dog')) 26 | assert(s5.setsockopt(ZMQ::SUBSCRIBE, 'animals.cat')) 27 | 28 | assert(s1.bind(link)) 29 | assert(s2.connect(link)) 30 | assert(s3.connect(link)) 31 | assert(s4.connect(link)) 32 | assert(s5.connect(link)) 33 | 34 | sleep 1 35 | 36 | topic = "animals.dog" 37 | payload = "Animal crackers!" 38 | 39 | s1.identity = "publisher-A" 40 | puts "sending" 41 | # use the new multi-part messaging support to 42 | # automatically separate the topic from the body 43 | assert(s1.send_string(topic, ZMQ::SNDMORE)) 44 | assert(s1.send_string(payload, ZMQ::SNDMORE)) 45 | assert(s1.send_string(s1.identity)) 46 | 47 | topic = '' 48 | assert(s2.recv_string(topic)) 49 | 50 | body = '' 51 | assert(s2.recv_string(body)) if s2.more_parts? 52 | 53 | identity = '' 54 | assert(s2.recv_string(identity)) if s2.more_parts? 55 | puts "s2 received topic [#{topic}], body [#{body}], identity [#{identity}]" 56 | 57 | 58 | 59 | topic = '' 60 | assert(s3.recv_string(topic)) 61 | 62 | body = '' 63 | assert(s3.recv_string(body)) if s3.more_parts? 64 | puts "s3 received topic [#{topic}], body [#{body}]" 65 | 66 | topic = '' 67 | assert(s4.recv_string(topic)) 68 | 69 | body = '' 70 | assert(s4.recv_string(body)) if s4.more_parts? 71 | puts "s4 received topic [#{topic}], body [#{body}]" 72 | 73 | s5_string = '' 74 | rc = s5.recv_string(s5_string, ZMQ::NonBlocking) 75 | eagain = (rc == -1 && ZMQ::Util.errno == ZMQ::EAGAIN) 76 | puts(eagain ? "s5 received no messages" : "s5 FAILED") 77 | 78 | [s1, s2, s3, s4, s5].each do |socket| 79 | assert(socket.close) 80 | end 81 | 82 | ctx.terminate 83 | -------------------------------------------------------------------------------- /examples/v2api/xreqxrep_poll.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | def assert(rc) 6 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 7 | end 8 | 9 | link = "tcp://127.0.0.1:5555" 10 | 11 | 12 | begin 13 | ctx = ZMQ::Context.new 14 | s1 = ctx.socket(ZMQ::XREQ) 15 | s2 = ctx.socket(ZMQ::XREP) 16 | rescue ContextError => e 17 | STDERR.puts "Failed to allocate context or socket" 18 | raise 19 | end 20 | 21 | s1.identity = 'socket1.xreq' 22 | s2.identity = 'socket2.xrep' 23 | 24 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 25 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 26 | 27 | assert(s1.bind(link)) 28 | assert(s2.connect(link)) 29 | 30 | poller = ZMQ::Poller.new 31 | poller.register_readable(s2) 32 | poller.register_writable(s1) 33 | 34 | start_time = Time.now 35 | @unsent = true 36 | 37 | until @done do 38 | assert(poller.poll_nonblock) 39 | 40 | # send the message after 5 seconds 41 | if Time.now - start_time > 5 && @unsent 42 | puts "sending payload nonblocking" 43 | 44 | 5.times do |i| 45 | payload = "#{ i.to_s * 40 }" 46 | assert(s1.send_string(payload, ZMQ::NonBlocking)) 47 | end 48 | @unsent = false 49 | end 50 | 51 | # check for messages after 1 second 52 | if Time.now - start_time > 1 53 | poller.readables.each do |sock| 54 | 55 | if sock.identity =~ /xrep/ 56 | routing_info = '' 57 | assert(sock.recv_string(routing_info, ZMQ::NonBlocking)) 58 | puts "routing_info received [#{routing_info}] on socket.identity [#{sock.identity}]" 59 | else 60 | routing_info = nil 61 | received_msg = '' 62 | assert(sock.recv_string(received_msg, ZMQ::NonBlocking)) 63 | 64 | # skip to the next iteration if received_msg is nil; that means we got an EAGAIN 65 | next unless received_msg 66 | puts "message received [#{received_msg}] on socket.identity [#{sock.identity}]" 67 | end 68 | 69 | while sock.more_parts? do 70 | received_msg = '' 71 | assert(sock.recv_string(received_msg, ZMQ::NonBlocking)) 72 | 73 | puts "message received [#{received_msg}]" 74 | end 75 | 76 | puts "kick back a reply" 77 | assert(sock.send_string(routing_info, ZMQ::SNDMORE | ZMQ::NonBlocking)) if routing_info 78 | time = Time.now.strftime "%Y-%m-%dT%H:%M:%S.#{Time.now.usec}" 79 | reply = "reply " + sock.identity.upcase + " #{time}" 80 | puts "sent reply [#{reply}], #{time}" 81 | assert(sock.send_string(reply)) 82 | @done = true 83 | poller.register_readable(s1) 84 | end 85 | end 86 | end 87 | 88 | puts "executed in [#{Time.now - start_time}] seconds" 89 | 90 | assert(s1.close) 91 | assert(s2.close) 92 | 93 | ctx.terminate 94 | -------------------------------------------------------------------------------- /examples/v3api/xreqxrep_poll.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | def assert(rc) 6 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 7 | end 8 | 9 | link = "tcp://127.0.0.1:5555" 10 | 11 | 12 | begin 13 | ctx = ZMQ::Context.new 14 | s1 = ctx.socket(ZMQ::XREQ) 15 | s2 = ctx.socket(ZMQ::XREP) 16 | rescue ContextError => e 17 | STDERR.puts "Failed to allocate context or socket" 18 | raise 19 | end 20 | 21 | s1.identity = 'socket1.xreq' 22 | s2.identity = 'socket2.xrep' 23 | 24 | assert(s1.setsockopt(ZMQ::LINGER, 100)) 25 | assert(s2.setsockopt(ZMQ::LINGER, 100)) 26 | 27 | assert(s1.bind(link)) 28 | assert(s2.connect(link)) 29 | 30 | poller = ZMQ::Poller.new 31 | poller.register_readable(s2) 32 | poller.register_writable(s1) 33 | 34 | start_time = Time.now 35 | @unsent = true 36 | 37 | until @done do 38 | assert(poller.poll_nonblock) 39 | 40 | # send the message after 5 seconds 41 | if Time.now - start_time > 5 && @unsent 42 | puts "sending payload nonblocking" 43 | 44 | 5.times do |i| 45 | payload = "#{ i.to_s * 40 }" 46 | assert(s1.send_string(payload, ZMQ::NonBlocking)) 47 | end 48 | @unsent = false 49 | end 50 | 51 | # check for messages after 1 second 52 | if Time.now - start_time > 1 53 | poller.readables.each do |sock| 54 | 55 | if sock.identity =~ /xrep/ 56 | routing_info = '' 57 | assert(sock.recv_string(routing_info, ZMQ::NonBlocking)) 58 | puts "routing_info received [#{routing_info}] on socket.identity [#{sock.identity}]" 59 | else 60 | routing_info = nil 61 | received_msg = '' 62 | assert(sock.recv_string(received_msg, ZMQ::NonBlocking)) 63 | 64 | # skip to the next iteration if received_msg is nil; that means we got an EAGAIN 65 | next unless received_msg 66 | puts "message received [#{received_msg}] on socket.identity [#{sock.identity}]" 67 | end 68 | 69 | while sock.more_parts? do 70 | received_msg = '' 71 | assert(sock.recv_string(received_msg, ZMQ::NonBlocking)) 72 | 73 | puts "message received [#{received_msg}]" 74 | end 75 | 76 | puts "kick back a reply" 77 | assert(sock.send_string(routing_info, ZMQ::SNDMORE | ZMQ::NonBlocking)) if routing_info 78 | time = Time.now.strftime "%Y-%m-%dT%H:%M:%S.#{Time.now.usec}" 79 | reply = "reply " + sock.identity.upcase + " #{time}" 80 | puts "sent reply [#{reply}], #{time}" 81 | assert(sock.send_string(reply)) 82 | @done = true 83 | poller.register_readable(s1) 84 | end 85 | end 86 | end 87 | 88 | puts "executed in [#{Time.now - start_time}] seconds" 89 | 90 | assert(s1.close) 91 | assert(s2.close) 92 | 93 | ctx.terminate 94 | -------------------------------------------------------------------------------- /spec/reqrep_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 3 | 4 | module ZMQ 5 | 6 | 7 | describe Socket do 8 | 9 | context "when running ping pong" do 10 | include APIHelper 11 | 12 | let(:string) { "booga-booga" } 13 | 14 | # reset sockets each time because we only send 1 message which leaves 15 | # the REQ socket in a bad state. It cannot send again unless we were to 16 | # send a reply with the REP and read it. 17 | before(:each) do 18 | @context = ZMQ::Context.new 19 | poller_setup 20 | 21 | endpoint = "inproc://reqrep_test" 22 | @ping = @context.socket ZMQ::REQ 23 | @pong = @context.socket ZMQ::REP 24 | @pong.bind(endpoint) 25 | connect_to_inproc(@ping, endpoint) 26 | end 27 | 28 | after(:each) do 29 | @ping.close 30 | @pong.close 31 | @context.terminate 32 | end 33 | 34 | def send_ping(string) 35 | @ping.send_string string 36 | received_message = '' 37 | rc = @pong.recv_string received_message 38 | [rc, received_message] 39 | end 40 | 41 | it "should receive an exact string copy of the string message sent" do 42 | rc, received_message = send_ping(string) 43 | received_message.should == string 44 | end 45 | 46 | it "should generate a EFSM error when sending via the REQ socket twice in a row without an intervening receive operation" do 47 | send_ping(string) 48 | rc = @ping.send_string(string) 49 | rc.should == -1 50 | Util.errno.should == ZMQ::EFSM 51 | end 52 | 53 | it "should receive an exact copy of the sent message using Message objects directly" do 54 | received_message = Message.new 55 | 56 | rc = @ping.sendmsg(Message.new(string)) 57 | LibZMQ.version2? ? rc.should == 0 : rc.should == string.size 58 | rc = @pong.recvmsg received_message 59 | LibZMQ.version2? ? rc.should == 0 : rc.should == string.size 60 | 61 | received_message.copy_out_string.should == string 62 | end 63 | 64 | it "should receive an exact copy of the sent message using Message objects directly in non-blocking mode" do 65 | sent_message = Message.new string 66 | received_message = Message.new 67 | 68 | poll_it_for_read(@pong) do 69 | rc = @ping.sendmsg(Message.new(string), ZMQ::NonBlocking) 70 | LibZMQ.version2? ? rc.should == 0 : rc.should == string.size 71 | end 72 | 73 | rc = @pong.recvmsg received_message, ZMQ::NonBlocking 74 | LibZMQ.version2? ? rc.should == 0 : rc.should == string.size 75 | 76 | received_message.copy_out_string.should == string 77 | end 78 | 79 | end # context ping-pong 80 | 81 | 82 | end # describe 83 | 84 | 85 | end # module ZMQ 86 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # To run these specs using rake, make sure the 'bones' and 'bones-extras' 2 | # gems are installed. Then execute 'rake spec' from the main directory 3 | # to run all specs. 4 | 5 | require File.expand_path( 6 | File.join(File.dirname(__FILE__), %w[.. lib ffi-rzmq])) 7 | 8 | require 'thread' # necessary when testing in MRI 1.8 mode 9 | Thread.abort_on_exception = true 10 | 11 | require 'openssl' 12 | require 'socket' 13 | require 'securerandom' 14 | 15 | # define some version guards so we can turn on/off specs based upon 16 | # the version of the 0mq library that is loaded 17 | def version2? 18 | ZMQ::LibZMQ.version2? 19 | end 20 | 21 | def version3? 22 | ZMQ::LibZMQ.version3? 23 | end 24 | 25 | 26 | def connect_to_inproc(socket, endpoint) 27 | begin 28 | rc = socket.connect(endpoint) 29 | end until ZMQ::Util.resultcode_ok?(rc) 30 | end 31 | 32 | module APIHelper 33 | def stub_libzmq 34 | @err_str_mock = mock("error string") 35 | 36 | LibZMQ.stub!( 37 | :zmq_init => 0, 38 | :zmq_errno => 0, 39 | :zmq_sterror => @err_str_mock 40 | ) 41 | end 42 | 43 | def poller_setup 44 | @helper_poller ||= ZMQ::Poller.new 45 | end 46 | 47 | def poller_register_socket(socket) 48 | @helper_poller.register(socket, ZMQ::POLLIN) 49 | end 50 | 51 | def poller_deregister_socket(socket) 52 | @helper_poller.deregister(socket, ZMQ::POLLIN) 53 | end 54 | 55 | def poll_delivery 56 | # timeout after 1 second 57 | @helper_poller.poll(1000) 58 | end 59 | 60 | def poll_it_for_read(socket, &blk) 61 | poller_register_socket(socket) 62 | blk.call 63 | poll_delivery 64 | poller_deregister_socket(socket) 65 | end 66 | 67 | # generate a random port between 10_000 and 65534 68 | def random_port 69 | rand(55534) + 10_000 70 | end 71 | 72 | def bind_to_random_tcp_port(socket, max_tries = 500) 73 | tries = 0 74 | rc = -1 75 | 76 | while !ZMQ::Util.resultcode_ok?(rc) && tries < max_tries 77 | tries += 1 78 | random = random_port 79 | rc = socket.bind(local_transport_string(random)) 80 | end 81 | 82 | unless ZMQ::Util.resultcode_ok?(rc) 83 | raise "Could not bind to random port successfully; retries all failed!" 84 | end 85 | 86 | random 87 | end 88 | 89 | def connect_to_random_tcp_port socket, max_tries = 500 90 | tries = 0 91 | rc = -1 92 | 93 | while !ZMQ::Util.resultcode_ok?(rc) && tries < max_tries 94 | tries += 1 95 | random = random_port 96 | rc = socket.connect(local_transport_string(random)) 97 | end 98 | 99 | unless ZMQ::Util.resultcode_ok?(rc) 100 | raise "Could not connect to random port successfully; retries all failed!" 101 | end 102 | 103 | random 104 | end 105 | 106 | def local_transport_string(port) 107 | "tcp://127.0.0.1:#{port}" 108 | end 109 | 110 | def assert_ok(rc) 111 | raise "Failed with rc [#{rc}] and errno [#{ZMQ::Util.errno}], msg [#{ZMQ::Util.error_string}]! #{caller(0)}" unless rc >= 0 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/message_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 3 | 4 | module ZMQ 5 | 6 | 7 | describe Message do 8 | 9 | context "when initializing with an argument" do 10 | 11 | it "calls zmq_msg_init_data()" do 12 | LibZMQ.should_receive(:zmq_msg_init_data) 13 | message = Message.new "text" 14 | end 15 | 16 | it "should *not* define a finalizer on this object" do 17 | ObjectSpace.should_not_receive(:define_finalizer) 18 | Message.new "text" 19 | end 20 | end # context initializing with arg 21 | 22 | context "when initializing *without* an argument" do 23 | 24 | it "calls zmq_msg_init()" do 25 | LibZMQ.should_receive(:zmq_msg_init).and_return(0) 26 | message = Message.new 27 | end 28 | 29 | it "should *not* define a finalizer on this object" do 30 | ObjectSpace.should_not_receive(:define_finalizer) 31 | Message.new "text" 32 | end 33 | end # context initializing with arg 34 | 35 | 36 | context "#copy_in_string" do 37 | it "calls zmq_msg_init_data()" do 38 | message = Message.new "text" 39 | 40 | LibZMQ.should_receive(:zmq_msg_init_data) 41 | message.copy_in_string("new text") 42 | end 43 | 44 | it "correctly finds the length of binary data by ignoring encoding" do 45 | message = Message.new 46 | message.copy_in_string("\x83\x6e\x04\x00\x00\x44\xd1\x81") 47 | message.size.should == 8 48 | end 49 | end 50 | 51 | 52 | context "#copy" do 53 | it "calls zmq_msg_copy()" do 54 | message = Message.new "text" 55 | copy = Message.new 56 | 57 | LibZMQ.should_receive(:zmq_msg_copy) 58 | copy.copy(message) 59 | end 60 | end # context copy 61 | 62 | 63 | context "#move" do 64 | it "calls zmq_msg_move()" do 65 | message = Message.new "text" 66 | copy = Message.new 67 | 68 | LibZMQ.should_receive(:zmq_msg_move) 69 | copy.move(message) 70 | end 71 | end # context move 72 | 73 | 74 | context "#size" do 75 | it "calls zmq_msg_size()" do 76 | message = Message.new "text" 77 | 78 | LibZMQ.should_receive(:zmq_msg_size) 79 | message.size 80 | end 81 | end # context size 82 | 83 | 84 | context "#data" do 85 | it "calls zmq_msg_data()" do 86 | message = Message.new "text" 87 | 88 | LibZMQ.should_receive(:zmq_msg_data) 89 | message.data 90 | end 91 | end # context data 92 | 93 | 94 | context "#close" do 95 | it "calls zmq_msg_close() the first time" do 96 | message = Message.new "text" 97 | 98 | LibZMQ.should_receive(:zmq_msg_close) 99 | message.close 100 | end 101 | 102 | it "*does not* call zmq_msg_close() on subsequent invocations" do 103 | message = Message.new "text" 104 | message.close 105 | 106 | LibZMQ.should_not_receive(:zmq_msg_close) 107 | message.close 108 | end 109 | end # context close 110 | 111 | end # describe Message 112 | 113 | 114 | describe ManagedMessage do 115 | 116 | context "when initializing with an argument" do 117 | 118 | it "should define a finalizer on this object" do 119 | ObjectSpace.should_receive(:define_finalizer) 120 | ManagedMessage.new "text" 121 | end 122 | end # context initializing 123 | 124 | 125 | end # describe ManagedMessage 126 | 127 | 128 | end # module ZMQ 129 | -------------------------------------------------------------------------------- /spec/context_spec.rb: -------------------------------------------------------------------------------- 1 | $: << "." # added for ruby 1.9.2 compatibilty; it doesn't include the current directory on the load path anymore 2 | 3 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 4 | 5 | module ZMQ 6 | 7 | 8 | describe Context do 9 | 10 | context "when initializing with factory method #create" do 11 | include APIHelper 12 | 13 | it "should return nil for negative io threads" do 14 | Context.create(-1).should be_nil 15 | end 16 | 17 | it "should default to requesting 1 i/o thread when no argument is passed" do 18 | ctx = Context.create 19 | ctx.io_threads.should == 1 20 | end 21 | 22 | it "should set the :pointer accessor to non-nil" do 23 | ctx = Context.create 24 | ctx.pointer.should_not be_nil 25 | end 26 | 27 | it "should set the :context accessor to non-nil" do 28 | ctx = Context.create 29 | ctx.context.should_not be_nil 30 | end 31 | 32 | it "should set the :pointer and :context accessors to the same value" do 33 | ctx = Context.create 34 | ctx.pointer.should == ctx.context 35 | end 36 | 37 | it "should define a finalizer on this object" do 38 | ObjectSpace.should_receive(:define_finalizer) 39 | ctx = Context.create 40 | end 41 | end # context initializing 42 | 43 | 44 | context "when initializing with #new" do 45 | include APIHelper 46 | 47 | it "should raise a ContextError exception for negative io threads" do 48 | lambda { Context.new(-1) }.should raise_exception(ZMQ::ContextError) 49 | end 50 | 51 | it "should default to requesting 1 i/o thread when no argument is passed" do 52 | ctx = Context.new 53 | ctx.io_threads.should == 1 54 | end 55 | 56 | it "should set the :pointer accessor to non-nil" do 57 | ctx = Context.new 58 | ctx.pointer.should_not be_nil 59 | end 60 | 61 | it "should set the :context accessor to non-nil" do 62 | ctx = Context.new 63 | ctx.context.should_not be_nil 64 | end 65 | 66 | it "should set the :pointer and :context accessors to the same value" do 67 | ctx = Context.new 68 | ctx.pointer.should == ctx.context 69 | end 70 | 71 | it "should define a finalizer on this object" do 72 | ObjectSpace.should_receive(:define_finalizer) 73 | Context.new 1 74 | end 75 | end # context initializing 76 | 77 | 78 | context "when terminating" do 79 | it "should set the context to nil when terminating the library's context" do 80 | ctx = Context.new # can't use a shared context here because we are terminating it! 81 | ctx.terminate 82 | ctx.pointer.should be_nil 83 | end 84 | 85 | it "should call the correct library function to terminate the context" do 86 | ctx = Context.new 87 | 88 | if LibZMQ.version2? 89 | LibZMQ.should_receive(:zmq_term).and_return(0) 90 | ctx.terminate 91 | else 92 | LibZMQ.should_receive(:zmq_ctx_destroy).with(ctx.pointer).and_return(0) 93 | ctx.terminate 94 | end 95 | end 96 | end # context terminate 97 | 98 | 99 | context "when allocating a socket" do 100 | it "should return nil when allocation fails" do 101 | ctx = Context.new 102 | LibZMQ.stub!(:zmq_socket => nil) 103 | ctx.socket(ZMQ::REQ).should be_nil 104 | end 105 | end # context socket 106 | 107 | end # describe Context 108 | 109 | 110 | end # module ZMQ 111 | -------------------------------------------------------------------------------- /examples/v2api/throughput_measurement.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 2 | 3 | 4 | # Within a single process, we start up five threads. Main thread has a PUB (publisher) 5 | # socket and the secondary threads have SUB (subscription) sockets. We measure the 6 | # *throughput* between these sockets. A high-water mark (HWM) is *not* set, so the 7 | # publisher queue is free to grow to the size of memory without dropping packets. 8 | # 9 | # This example also illustrates how a single context can be shared amongst several 10 | # threads. Sharing a single context also allows a user to specify the "inproc" 11 | # transport in addition to "tcp" and "ipc". 12 | # 13 | # % ruby throughput_measurement.rb tcp://127.0.0.1:5555 1024 1_000_000 14 | # 15 | # % ruby throughput_measurement.rb inproc://lm_sock 1024 1_000_000 16 | # 17 | 18 | if ARGV.length < 3 19 | puts "usage: ruby throughput_measurement.rb " 20 | exit 21 | end 22 | 23 | link = ARGV[0] 24 | message_size = ARGV[1].to_i 25 | count = ARGV[2].to_i 26 | 27 | def assert(rc) 28 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 29 | end 30 | 31 | begin 32 | master_context = ZMQ::Context.new 33 | rescue ContextError => e 34 | STDERR.puts "Failed to allocate context or socket!" 35 | raise 36 | end 37 | 38 | 39 | class Receiver 40 | def initialize context, link, size, count 41 | @context = context 42 | @link = link 43 | @size = size 44 | @count = count 45 | 46 | begin 47 | @socket = @context.socket(ZMQ::SUB) 48 | rescue ContextError => e 49 | STDERR.puts "Failed to allocate SUB socket!" 50 | raise 51 | end 52 | 53 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 54 | assert(@socket.setsockopt(ZMQ::SUBSCRIBE, "")) 55 | 56 | assert(@socket.connect(@link)) 57 | end 58 | 59 | def run 60 | msg = ZMQ::Message.new 61 | assert(@socket.recvmsg(msg)) 62 | 63 | elapsed = elapsed_microseconds do 64 | (@count -1).times do 65 | assert(@socket.recvmsg(msg)) 66 | end 67 | end 68 | 69 | throughput = @count * 1000000 / elapsed 70 | megabits = throughput * @size * 8 / 1000000 71 | 72 | puts "message size: %i [B]" % @size 73 | puts "message count: %i" % @count 74 | puts "mean throughput: %i [msg/s]" % throughput 75 | puts "mean throughput: %.3f [Mb/s]" % megabits 76 | 77 | assert(@socket.close) 78 | end 79 | 80 | def elapsed_microseconds(&blk) 81 | start = Time.now 82 | yield 83 | ((Time.now - start) * 1_000_000) 84 | end 85 | end 86 | 87 | class Transmitter 88 | def initialize context, link, size, count 89 | @context = context 90 | @link = link 91 | @size = size 92 | @count = count 93 | 94 | begin 95 | @socket = @context.socket(ZMQ::PUB) 96 | rescue ContextError => e 97 | STDERR.puts "Failed to allocate PUB socket!" 98 | raise 99 | end 100 | 101 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 102 | 103 | assert(@socket.bind(@link)) 104 | end 105 | 106 | def run 107 | sleep 1 108 | contents = "#{'0' * @size}" 109 | 110 | i = 0 111 | while i < @count 112 | msg = ZMQ::Message.new(contents) 113 | assert(@socket.sendmsg(msg)) 114 | i += 1 115 | end 116 | 117 | assert(@socket.close) 118 | end 119 | end 120 | 121 | threads = [] 122 | 123 | threads << Thread.new do 124 | transmitter = Transmitter.new(master_context, link, message_size, count) 125 | transmitter.run 126 | end 127 | 128 | 1.times do 129 | threads << Thread.new do 130 | receiver = Receiver.new(master_context, link, message_size, count) 131 | receiver.run 132 | end 133 | end 134 | 135 | 136 | threads.each {|t| t.join} 137 | 138 | master_context.terminate 139 | -------------------------------------------------------------------------------- /examples/v2api/latency_measurement.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | # Within a single process, we start up two threads. One thread has a REQ (request) 6 | # socket and the second thread has a REP (reply) socket. We measure the 7 | # *round-trip* latency between these sockets. Only *one* message is in flight at 8 | # any given moment. 9 | # 10 | # This example also illustrates how a single context can be shared amongst several 11 | # threads. Sharing a single context also allows a user to specify the "inproc" 12 | # transport in addition to "tcp" and "ipc". 13 | # 14 | # % ruby latency_measurement.rb tcp://127.0.0.1:5555 1024 1_000_000 15 | # 16 | # % ruby latency_measurement.rb inproc://lm_sock 1024 1_000_000 17 | # 18 | 19 | if ARGV.length < 3 20 | puts "usage: ruby latency_measurement.rb " 21 | exit 22 | end 23 | 24 | link = ARGV[0] 25 | message_size = ARGV[1].to_i 26 | roundtrip_count = ARGV[2].to_i 27 | 28 | def assert(rc) 29 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 30 | end 31 | 32 | begin 33 | master_context = ZMQ::Context.new 34 | rescue ContextError => e 35 | STDERR.puts "Failed to allocate context or socket!" 36 | raise 37 | end 38 | 39 | 40 | class Receiver 41 | def initialize context, link, size, count 42 | @context = context 43 | @link = link 44 | @size = size 45 | @count = count 46 | 47 | begin 48 | @socket = @context.socket(ZMQ::REP) 49 | rescue ContextError => e 50 | STDERR.puts "Failed to allocate REP socket!" 51 | raise 52 | end 53 | 54 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 55 | 56 | assert(@socket.setsockopt(ZMQ::HWM, 100)) 57 | 58 | assert(@socket.bind(@link)) 59 | end 60 | 61 | def run 62 | @count.times do 63 | string = '' 64 | assert(@socket.recv_string(string, 0)) 65 | 66 | raise "Message size doesn't match, expected [#{@size}] but received [#{string.size}]" if @size != string.size 67 | 68 | assert(@socket.send_string(string, 0)) 69 | end 70 | 71 | assert(@socket.close) 72 | end 73 | end 74 | 75 | class Transmitter 76 | def initialize context, link, size, count 77 | @context = context 78 | @link = link 79 | @size = size 80 | @count = count 81 | 82 | begin 83 | @socket = @context.socket(ZMQ::REQ) 84 | rescue ContextError => e 85 | STDERR.puts "Failed to allocate REP socket!" 86 | raise 87 | end 88 | 89 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 90 | 91 | assert(@socket.setsockopt(ZMQ::HWM, 100)) 92 | 93 | assert(@socket.connect(@link)) 94 | end 95 | 96 | def run 97 | msg = "#{ '3' * @size }" 98 | 99 | elapsed = elapsed_microseconds do 100 | @count.times do 101 | assert(@socket.send_string(msg, 0)) 102 | assert(@socket.recv_string(msg, 0)) 103 | 104 | raise "Message size doesn't match, expected [#{@size}] but received [#{msg.size}]" if @size != msg.size 105 | end 106 | end 107 | 108 | latency = elapsed / @count / 2 109 | 110 | puts "message size: %i [B]" % @size 111 | puts "roundtrip count: %i" % @count 112 | puts "throughput (msgs/s): %i" % (@count / (elapsed / 1_000_000)) 113 | puts "mean latency: %.3f [us]" % latency 114 | assert(@socket.close) 115 | end 116 | 117 | def elapsed_microseconds(&blk) 118 | start = Time.now 119 | yield 120 | value = ((Time.now - start) * 1_000_000) 121 | end 122 | end 123 | 124 | threads = [] 125 | threads << Thread.new do 126 | receiver = Receiver.new(master_context, link, message_size, roundtrip_count) 127 | receiver.run 128 | end 129 | 130 | sleep 1 131 | 132 | threads << Thread.new do 133 | transmitter = Transmitter.new(master_context, link, message_size, roundtrip_count) 134 | transmitter.run 135 | end 136 | 137 | threads.each {|t| t.join} 138 | 139 | master_context.terminate 140 | -------------------------------------------------------------------------------- /spec/pushpull_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 3 | 4 | module ZMQ 5 | describe Socket do 6 | context "when running basic push pull" do 7 | include APIHelper 8 | 9 | let(:string) { "booga-booga" } 10 | 11 | before(:each) do 12 | # Use new context for each iteration to avoid inproc race. See 13 | # poll_spec.rb for more details. 14 | @context = Context.new 15 | poller_setup 16 | 17 | @push = @context.socket ZMQ::PUSH 18 | @pull = @context.socket ZMQ::PULL 19 | @push.setsockopt ZMQ::LINGER, 0 20 | @pull.setsockopt ZMQ::LINGER, 0 21 | 22 | @link = "inproc://push_pull_test" 23 | @push.bind @link 24 | connect_to_inproc(@pull, @link) 25 | end 26 | 27 | after(:each) do 28 | @push.close 29 | @pull.close 30 | @context.terminate 31 | end 32 | 33 | it "should receive an exact copy of the sent message using Message objects directly on one pull socket" do 34 | @push.send_string string 35 | received = '' 36 | rc = @pull.recv_string received 37 | assert_ok(rc) 38 | received.should == string 39 | end 40 | 41 | it "should receive an exact string copy of the message sent when receiving in non-blocking mode and using Message objects directly" do 42 | sent_message = Message.new string 43 | received_message = Message.new 44 | 45 | poll_it_for_read(@pull) do 46 | rc = @push.sendmsg sent_message 47 | LibZMQ.version2? ? rc.should == 0 : rc.should == string.size 48 | end 49 | 50 | rc = @pull.recvmsg received_message, ZMQ::NonBlocking 51 | LibZMQ.version2? ? rc.should == 0 : rc.should == string.size 52 | received_message.copy_out_string.should == string 53 | end 54 | 55 | 56 | 57 | it "should receive a single message for each message sent on each socket listening, when an equal number of sockets pulls messages and where each socket is unique per thread" do 58 | received = [] 59 | threads = [] 60 | sockets = [] 61 | count = 4 62 | mutex = Mutex.new 63 | 64 | # make sure all sockets are connected before we do our load-balancing test 65 | (count - 1).times do 66 | socket = @context.socket ZMQ::PULL 67 | socket.setsockopt ZMQ::LINGER, 0 68 | connect_to_inproc(socket, @link) 69 | sockets << socket 70 | end 71 | sockets << @pull 72 | 73 | sockets.each do |socket| 74 | threads << Thread.new do 75 | buffer = '' 76 | rc = socket.recv_string buffer 77 | version2? ? (rc.should == 0) : (rc.should == buffer.size) 78 | mutex.synchronize { received << buffer } 79 | socket.close 80 | end 81 | end 82 | 83 | count.times { @push.send_string(string) } 84 | 85 | threads.each {|t| t.join} 86 | 87 | received.find_all {|r| r == string}.length.should == count 88 | end 89 | 90 | it "should receive a single message for each message sent when using a single shared socket protected by a mutex" do 91 | received = [] 92 | threads = [] 93 | count = 4 94 | mutex = Mutex.new 95 | 96 | count.times do |i| 97 | threads << Thread.new do 98 | buffer = '' 99 | rc = 0 100 | mutex.synchronize { rc = @pull.recv_string buffer } 101 | version2? ? (rc.should == 0) : (rc.should == buffer.size) 102 | mutex.synchronize { received << buffer } 103 | end 104 | end 105 | 106 | count.times { @push.send_string(string) } 107 | 108 | threads.each {|t| t.join} 109 | 110 | received.find_all {|r| r == string}.length.should == count 111 | end 112 | 113 | end # @context ping-pong 114 | end # describe 115 | end # module ZMQ 116 | -------------------------------------------------------------------------------- /examples/v3api/latency_measurement.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 3 | 4 | 5 | # Within a single process, we start up two threads. One thread has a REQ (request) 6 | # socket and the second thread has a REP (reply) socket. We measure the 7 | # *round-trip* latency between these sockets. Only *one* message is in flight at 8 | # any given moment. 9 | # 10 | # This example also illustrates how a single context can be shared amongst several 11 | # threads. Sharing a single context also allows a user to specify the "inproc" 12 | # transport in addition to "tcp" and "ipc". 13 | # 14 | # % ruby latency_measurement.rb tcp://127.0.0.1:5555 1024 1_000_000 15 | # 16 | # % ruby latency_measurement.rb inproc://lm_sock 1024 1_000_000 17 | # 18 | 19 | if ARGV.length < 3 20 | puts "usage: ruby latency_measurement.rb " 21 | exit 22 | end 23 | 24 | link = ARGV[0] 25 | message_size = ARGV[1].to_i 26 | roundtrip_count = ARGV[2].to_i 27 | 28 | def assert(rc) 29 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 30 | end 31 | 32 | begin 33 | master_context = ZMQ::Context.new 34 | rescue ContextError => e 35 | STDERR.puts "Failed to allocate context or socket!" 36 | raise 37 | end 38 | 39 | 40 | class Receiver 41 | def initialize context, link, size, count 42 | @context = context 43 | @link = link 44 | @size = size 45 | @count = count 46 | 47 | begin 48 | @socket = @context.socket(ZMQ::REP) 49 | rescue ContextError => e 50 | STDERR.puts "Failed to allocate REP socket!" 51 | raise 52 | end 53 | 54 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 55 | assert(@socket.setsockopt(ZMQ::RCVHWM, 100)) 56 | assert(@socket.setsockopt(ZMQ::SNDHWM, 100)) 57 | 58 | assert(@socket.bind(@link)) 59 | end 60 | 61 | def run 62 | @count.times do 63 | string = '' 64 | assert(@socket.recv_string(string, 0)) 65 | 66 | raise "Message size doesn't match, expected [#{@size}] but received [#{string.size}]" if @size != string.size 67 | 68 | assert(@socket.send_string(string, 0)) 69 | end 70 | 71 | assert(@socket.close) 72 | end 73 | end 74 | 75 | class Transmitter 76 | def initialize context, link, size, count 77 | @context = context 78 | @link = link 79 | @size = size 80 | @count = count 81 | 82 | begin 83 | @socket = @context.socket(ZMQ::REQ) 84 | rescue ContextError => e 85 | STDERR.puts "Failed to allocate REP socket!" 86 | raise 87 | end 88 | 89 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 90 | assert(@socket.setsockopt(ZMQ::RCVHWM, 100)) 91 | assert(@socket.setsockopt(ZMQ::SNDHWM, 100)) 92 | 93 | assert(@socket.connect(@link)) 94 | end 95 | 96 | def run 97 | msg = "#{ '3' * @size }" 98 | 99 | elapsed = elapsed_microseconds do 100 | @count.times do 101 | assert(@socket.send_string(msg, 0)) 102 | assert(@socket.recv_string(msg, 0)) 103 | 104 | raise "Message size doesn't match, expected [#{@size}] but received [#{msg.size}]" if @size != msg.size 105 | end 106 | end 107 | 108 | latency = elapsed / @count / 2 109 | 110 | puts "message size: %i [B]" % @size 111 | puts "roundtrip count: %i" % @count 112 | puts "throughput (msgs/s): %i" % (@count / (elapsed / 1_000_000)) 113 | puts "mean latency: %.3f [us]" % latency 114 | assert(@socket.close) 115 | end 116 | 117 | def elapsed_microseconds(&blk) 118 | start = Time.now 119 | yield 120 | value = ((Time.now - start) * 1_000_000) 121 | end 122 | end 123 | 124 | threads = [] 125 | threads << Thread.new do 126 | receiver = Receiver.new(master_context, link, message_size, roundtrip_count) 127 | receiver.run 128 | end 129 | 130 | sleep 1 131 | 132 | threads << Thread.new do 133 | transmitter = Transmitter.new(master_context, link, message_size, roundtrip_count) 134 | transmitter.run 135 | end 136 | 137 | threads.each {|t| t.join} 138 | 139 | master_context.terminate 140 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/util.rb: -------------------------------------------------------------------------------- 1 | 2 | module ZMQ 3 | 4 | # General utility methods. 5 | # 6 | class Util 7 | 8 | # Returns true when +rc+ is greater than or equal to 0, false otherwise. 9 | # 10 | # We use the >= test because zmq_poll() returns the number of sockets 11 | # that had a read or write event triggered. So, a >= 0 result means 12 | # it succeeded. 13 | # 14 | def self.resultcode_ok? rc 15 | rc >= 0 16 | end 17 | 18 | # Returns the +errno+ as set by the libzmq library. 19 | # 20 | def self.errno 21 | LibZMQ.zmq_errno 22 | end 23 | 24 | # Returns a string corresponding to the currently set #errno. These 25 | # error strings are defined by libzmq. 26 | # 27 | def self.error_string 28 | LibZMQ.zmq_strerror(errno).read_string 29 | end 30 | 31 | # Returns an array of the form [major, minor, patch] to represent the 32 | # version of libzmq. 33 | # 34 | # Class method! Invoke as: ZMQ::Util.version 35 | # 36 | def self.version 37 | major = FFI::MemoryPointer.new :int 38 | minor = FFI::MemoryPointer.new :int 39 | patch = FFI::MemoryPointer.new :int 40 | LibZMQ.zmq_version major, minor, patch 41 | [major.read_int, minor.read_int, patch.read_int] 42 | end 43 | 44 | # Attempts to bind to a random tcp port on +host+ up to +max_tries+ 45 | # times. Returns the port number upon success or nil upon failure. 46 | # 47 | def self.bind_to_random_tcp_port host = '127.0.0.1', max_tries = 500 48 | tries = 0 49 | rc = -1 50 | 51 | while !resultcode_ok?(rc) && tries < max_tries 52 | tries += 1 53 | random = random_port 54 | rc = socket.bind "tcp://#{host}:#{random}" 55 | end 56 | 57 | resultcode_ok?(rc) ? random : nil 58 | end 59 | 60 | # :doc: 61 | # Called to verify whether there were any errors during 62 | # operation. If any are found, raise the appropriate #ZeroMQError. 63 | # 64 | # When no error is found, this method returns +true+ which is behavior 65 | # used internally by #send and #recv. 66 | # 67 | def self.error_check source, result_code 68 | if -1 == result_code 69 | raise_error source, result_code 70 | end 71 | 72 | # used by Socket::send/recv, ignored by others 73 | true 74 | end 75 | 76 | 77 | private 78 | 79 | # generate a random port between 10_000 and 65534 80 | def self.random_port 81 | rand(55534) + 10_000 82 | end 83 | 84 | def self.raise_error source, result_code 85 | if context_error?(source) 86 | raise ContextError.new source, result_code, ZMQ::Util.errno, ZMQ::Util.error_string 87 | 88 | elsif message_error?(source) 89 | raise MessageError.new source, result_code, ZMQ::Util.errno, ZMQ::Util.error_string 90 | 91 | else 92 | raise ZeroMQError.new source, result_code, -1, 93 | "Source [#{source}] does not match any zmq_* strings, rc [#{result_code}], errno [#{ZMQ::Util.errno}], error_string [#{ZMQ::Util.error_string}]" 94 | end 95 | end 96 | 97 | def self.eagain? 98 | EAGAIN == ZMQ::Util.errno 99 | end 100 | 101 | if LibZMQ.version2? 102 | def self.context_error?(source) 103 | 'zmq_init' == source || 104 | 'zmq_socket' == source 105 | end 106 | 107 | def self.message_error?(source) 108 | ['zmq_msg_init', 'zmq_msg_init_data', 'zmq_msg_copy', 'zmq_msg_move'].include?(source) 109 | end 110 | 111 | elsif LibZMQ.version3? 112 | def self.context_error?(source) 113 | 'zmq_ctx_new' == source || 114 | 'zmq_ctx_set' == source || 115 | 'zmq_ctx_get' == source || 116 | 'zmq_ctx_destory' == source || 117 | 'zmq_ctx_set_monitor' == source 118 | end 119 | 120 | def self.message_error?(source) 121 | ['zmq_msg_init', 'zmq_msg_init_data', 'zmq_msg_copy', 'zmq_msg_move', 'zmq_msg_close', 'zmq_msg_get', 122 | 'zmq_msg_more', 'zmq_msg_recv', 'zmq_msg_send', 'zmq_msg_set'].include?(source) 123 | end 124 | end # if LibZMQ.version...? 125 | 126 | end # module Util 127 | 128 | end # module ZMQ 129 | -------------------------------------------------------------------------------- /examples/v3api/throughput_measurement.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'ffi-rzmq') 2 | require 'thread' 3 | 4 | # Within a single process, we start up five threads. Main thread has a PUB (publisher) 5 | # socket and the secondary threads have SUB (subscription) sockets. We measure the 6 | # *throughput* between these sockets. A high-water mark (HWM) is *not* set, so the 7 | # publisher queue is free to grow to the size of memory without dropping packets. 8 | # 9 | # This example also illustrates how a single context can be shared amongst several 10 | # threads. Sharing a single context also allows a user to specify the "inproc" 11 | # transport in addition to "tcp" and "ipc". 12 | # 13 | # % ruby throughput_measurement.rb tcp://127.0.0.1:5555 1024 1_000_000 14 | # 15 | # % ruby throughput_measurement.rb inproc://lm_sock 1024 1_000_000 16 | # 17 | 18 | if ARGV.length < 3 19 | puts "usage: ruby throughput_measurement.rb " 20 | exit 21 | end 22 | 23 | link = ARGV[0] 24 | message_size = ARGV[1].to_i 25 | count = ARGV[2].to_i 26 | 27 | def assert(rc) 28 | raise "Last API call failed at #{caller(1)}" unless rc >= 0 29 | end 30 | 31 | begin 32 | master_context = ZMQ::Context.new 33 | rescue ContextError => e 34 | STDERR.puts "Failed to allocate context or socket!" 35 | raise 36 | end 37 | 38 | 39 | class Receiver 40 | def initialize context, link, size, count, stats 41 | @context = context 42 | @link = link 43 | @size = size 44 | @count = count 45 | @stats = stats 46 | 47 | begin 48 | @socket = @context.socket(ZMQ::SUB) 49 | rescue ContextError => e 50 | STDERR.puts "Failed to allocate SUB socket!" 51 | raise 52 | end 53 | 54 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 55 | assert(@socket.setsockopt(ZMQ::SUBSCRIBE, "")) 56 | 57 | assert(@socket.connect(@link)) 58 | end 59 | 60 | def run 61 | msg = ZMQ::Message.new 62 | assert(@socket.recvmsg(msg)) 63 | 64 | elapsed = elapsed_microseconds do 65 | (@count - 1).times do 66 | assert(@socket.recvmsg(msg)) 67 | end 68 | end 69 | 70 | @stats.record_elapsed(elapsed) 71 | assert(@socket.close) 72 | end 73 | 74 | def elapsed_microseconds(&blk) 75 | start = Time.now 76 | yield 77 | ((Time.now - start) * 1_000_000) 78 | end 79 | end 80 | 81 | class Transmitter 82 | def initialize context, link, size, count 83 | @context = context 84 | @link = link 85 | @size = size 86 | @count = count 87 | 88 | begin 89 | @socket = @context.socket(ZMQ::PUB) 90 | rescue ContextError => e 91 | STDERR.puts "Failed to allocate PUB socket!" 92 | raise 93 | end 94 | 95 | assert(@socket.setsockopt(ZMQ::LINGER, 100)) 96 | assert(@socket.bind(@link)) 97 | end 98 | 99 | def run 100 | sleep 1 101 | contents = "#{'0' * @size}" 102 | 103 | i = 0 104 | while i < @count 105 | msg = ZMQ::Message.new(contents) 106 | assert(@socket.sendmsg(msg)) 107 | i += 1 108 | end 109 | 110 | end 111 | 112 | def close 113 | assert(@socket.close) 114 | end 115 | end 116 | 117 | class Stats 118 | def initialize size, count 119 | @size = size 120 | @count = count 121 | 122 | @mutex = Mutex.new 123 | @elapsed = [] 124 | end 125 | 126 | def record_elapsed(elapsed) 127 | @mutex.synchronize do 128 | @elapsed << elapsed 129 | end 130 | end 131 | 132 | def output 133 | @elapsed.each do |elapsed| 134 | throughput = @count * 1000000 / elapsed 135 | megabits = throughput * @size * 8 / 1000000 136 | 137 | puts "message size: %i [B]" % @size 138 | puts "message count: %i" % @count 139 | puts "mean throughput: %i [msg/s]" % throughput 140 | puts "mean throughput: %.3f [Mb/s]" % megabits 141 | puts 142 | end 143 | end 144 | end 145 | 146 | threads = [] 147 | stats = Stats.new message_size, count 148 | transmitter = Transmitter.new(master_context, link, message_size, count) 149 | 150 | threads << Thread.new do 151 | transmitter.run 152 | end 153 | 154 | 1.times do 155 | threads << Thread.new do 156 | receiver = Receiver.new(master_context, link, message_size, count, stats) 157 | receiver.run 158 | end 159 | end 160 | 161 | 162 | threads.each {|t| t.join} 163 | transmitter.close 164 | stats.output 165 | 166 | master_context.terminate 167 | -------------------------------------------------------------------------------- /spec/multipart_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | module ZMQ 4 | describe Socket do 5 | context "multipart messages" do 6 | before(:all) { @ctx = Context.new } 7 | after(:all) { @ctx.terminate } 8 | 9 | context "using #send_strings" do 10 | include APIHelper 11 | 12 | before(:all) do 13 | @receiver = Socket.new(@ctx.pointer, ZMQ::REP) 14 | port = bind_to_random_tcp_port(@receiver) 15 | 16 | @sender = Socket.new(@ctx.pointer, ZMQ::REQ) 17 | rc = @sender.connect("tcp://127.0.0.1:#{port}") 18 | end 19 | 20 | after(:all) do 21 | @sender.close 22 | @receiver.close 23 | end 24 | 25 | it "correctly handles a multipart message array with 1 element" do 26 | data = [ "1" ] 27 | 28 | @sender.send_strings(data) 29 | sleep 1 30 | strings = [] 31 | rc = @receiver.recv_strings(strings) 32 | strings.should == data 33 | end 34 | end 35 | 36 | 37 | context "without identity" do 38 | include APIHelper 39 | 40 | before(:all) do 41 | @rep = Socket.new(@ctx.pointer, ZMQ::REP) 42 | port = bind_to_random_tcp_port(@rep) 43 | 44 | @req = Socket.new(@ctx.pointer, ZMQ::REQ) 45 | @req.connect("tcp://127.0.0.1:#{port}") 46 | end 47 | 48 | after(:all) do 49 | @req.close 50 | @rep.close 51 | end 52 | 53 | it "should be delivered between REQ and REP returning an array of strings" do 54 | req_data, rep_data = [ "1", "2" ], [ "2", "3" ] 55 | 56 | @req.send_strings(req_data) 57 | strings = [] 58 | rc = @rep.recv_strings(strings) 59 | strings.should == req_data 60 | 61 | @rep.send_strings(rep_data) 62 | strings = [] 63 | rc = @req.recv_strings(strings) 64 | strings.should == rep_data 65 | end 66 | 67 | it "should be delivered between REQ and REP returning an array of messages" do 68 | req_data, rep_data = [ "1", "2" ], [ "2", "3" ] 69 | 70 | @req.send_strings(req_data) 71 | messages = [] 72 | rc = @rep.recvmsgs(messages) 73 | messages.each_with_index do |message, index| 74 | message.copy_out_string.should == req_data[index] 75 | end 76 | 77 | @rep.send_strings(rep_data) 78 | messages = [] 79 | rc = @req.recvmsgs(messages) 80 | messages.each_with_index do |message, index| 81 | message.copy_out_string.should == rep_data[index] 82 | end 83 | end 84 | end 85 | 86 | context "with identity" do 87 | include APIHelper 88 | 89 | before(:each) do # was :all 90 | @rep = Socket.new(@ctx.pointer, ZMQ::XREP) 91 | port = bind_to_random_tcp_port(@rep) 92 | 93 | @req = Socket.new(@ctx.pointer, ZMQ::REQ) 94 | @req.identity = 'foo' 95 | @req.connect("tcp://127.0.0.1:#{port}") 96 | end 97 | 98 | after(:each) do # was :all 99 | @req.close 100 | @rep.close 101 | end 102 | 103 | it "should be delivered between REQ and REP returning an array of strings with an empty string as the envelope delimiter" do 104 | req_data, rep_data = "hello", [ @req.identity, "", "ok" ] 105 | 106 | @req.send_string(req_data) 107 | strings = [] 108 | rc = @rep.recv_strings(strings) 109 | strings.should == [ @req.identity, "", "hello" ] 110 | 111 | @rep.send_strings(rep_data) 112 | string = '' 113 | rc = @req.recv_string(string) 114 | string.should == rep_data.last 115 | end 116 | 117 | it "should be delivered between REQ and REP returning an array of messages with an empty string as the envelope delimiter" do 118 | req_data, rep_data = "hello", [ @req.identity, "", "ok" ] 119 | 120 | @req.send_string(req_data) 121 | msgs = [] 122 | rc = @rep.recvmsgs(msgs) 123 | msgs[0].copy_out_string.should == @req.identity 124 | msgs[1].copy_out_string.should == "" 125 | msgs[2].copy_out_string.should == "hello" 126 | 127 | @rep.send_strings(rep_data) 128 | msgs = [] 129 | rc = @req.recvmsgs(msgs) 130 | msgs[0].copy_out_string.should == rep_data.last 131 | end 132 | end 133 | 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/context.rb: -------------------------------------------------------------------------------- 1 | 2 | module ZMQ 3 | 4 | 5 | # Recommended to use the default for +io_threads+ 6 | # since most programs will not saturate I/O. 7 | # 8 | # The rule of thumb is to make +io_threads+ equal to the number 9 | # gigabits per second that the application will produce. 10 | # 11 | # The +io_threads+ number specifies the size of the thread pool 12 | # allocated by 0mq for processing incoming/outgoing messages. 13 | # 14 | # Returns a context object when allocation succeeds. It's necessary 15 | # for passing to the 16 | # #Socket constructor when allocating new sockets. All sockets 17 | # live within a context. 18 | # 19 | # Also, Sockets should *only* be accessed from the thread where they 20 | # were first created. Do *not* pass sockets between threads; pass 21 | # in the context and allocate a new socket per thread. If you must 22 | # use threads, then make sure to execute a full memory barrier (e.g. 23 | # mutex) as you pass a socket from one thread to the next. 24 | # 25 | # To connect sockets between contexts, use +inproc+ or +ipc+ 26 | # transport and set up a 0mq socket between them. This is also the 27 | # recommended technique for allowing sockets to communicate between 28 | # threads. 29 | # 30 | # context = ZMQ::Context.create 31 | # if context 32 | # socket = context.socket(ZMQ::REQ) 33 | # if socket 34 | # ... 35 | # else 36 | # STDERR.puts "Socket allocation failed" 37 | # end 38 | # else 39 | # STDERR.puts "Context allocation failed" 40 | # end 41 | # 42 | # 43 | class Context 44 | 45 | attr_reader :context, :io_threads, :max_sockets 46 | alias :pointer :context 47 | 48 | # Use the factory method Context#create to make contexts. 49 | # 50 | if LibZMQ.version2? 51 | def self.create io_threads = 1 52 | new(io_threads) rescue nil 53 | end 54 | 55 | def initialize io_threads = 1 56 | @io_threads = io_threads 57 | @context = LibZMQ.zmq_init io_threads 58 | ZMQ::Util.error_check 'zmq_init', (@context.nil? || @context.null?) ? -1 : 0 59 | 60 | define_finalizer 61 | end 62 | elsif LibZMQ.version3? 63 | 64 | def self.create(opts = {}) 65 | new(opts) rescue nil 66 | end 67 | 68 | def initialize(opts = {}) 69 | if opts.respond_to?(:empty?) 70 | @io_threads = opts[:io_threads] || IO_THREADS_DFLT 71 | @max_sockets = opts[:max_sockets] || MAX_SOCKETS_DFLT 72 | else 73 | @io_threads = opts || 1 74 | @max_sockets = MAX_SOCKETS_DFLT 75 | end 76 | 77 | @context = LibZMQ.zmq_ctx_new 78 | ZMQ::Util.error_check 'zmq_ctx_new', (@context.nil? || @context.null?) ? -1 : 0 79 | 80 | rc = LibZMQ.zmq_ctx_set(@context, ZMQ::IO_THREADS, @io_threads) 81 | ZMQ::Util.error_check 'zmq_ctx_set', rc 82 | 83 | rc = LibZMQ.zmq_ctx_set(@context, ZMQ::MAX_SOCKETS, @max_sockets) 84 | ZMQ::Util.error_check 'zmq_ctx_set', rc 85 | 86 | define_finalizer 87 | end 88 | end 89 | 90 | # Call to release the context and any remaining data associated 91 | # with past sockets. This will close any sockets that remain 92 | # open; further calls to those sockets will return -1 to indicate 93 | # the operation failed. 94 | # 95 | # Returns 0 for success, -1 for failure. 96 | # 97 | if LibZMQ.version2? 98 | def terminate 99 | unless @context.nil? || @context.null? 100 | remove_finalizer 101 | rc = LibZMQ.zmq_term @context 102 | @context = nil 103 | rc 104 | else 105 | 0 106 | end 107 | end 108 | elsif LibZMQ.version3? 109 | def terminate 110 | unless @context.nil? || @context.null? 111 | remove_finalizer 112 | rc = LibZMQ.zmq_ctx_destroy(@context) 113 | @context = nil 114 | rc 115 | else 116 | 0 117 | end 118 | end 119 | end 120 | 121 | # Short-cut to allocate a socket for a specific context. 122 | # 123 | # Takes several +type+ values: 124 | # #ZMQ::REQ 125 | # #ZMQ::REP 126 | # #ZMQ::PUB 127 | # #ZMQ::SUB 128 | # #ZMQ::PAIR 129 | # #ZMQ::PULL 130 | # #ZMQ::PUSH 131 | # #ZMQ::DEALER 132 | # #ZMQ::ROUTER 133 | # 134 | # Returns a #ZMQ::Socket when the allocation succeeds, nil 135 | # if it fails. 136 | # 137 | def socket type 138 | sock = nil 139 | begin 140 | sock = Socket.new @context, type 141 | rescue ContextError => e 142 | sock = nil 143 | end 144 | 145 | sock 146 | end 147 | 148 | 149 | private 150 | 151 | def define_finalizer 152 | ObjectSpace.define_finalizer(self, self.class.close(@context)) 153 | end 154 | 155 | def remove_finalizer 156 | ObjectSpace.undefine_finalizer self 157 | end 158 | 159 | def self.close context 160 | Proc.new { LibZMQ.zmq_term context unless context.null? } 161 | end 162 | end 163 | 164 | end # module ZMQ 165 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/constants.rb: -------------------------------------------------------------------------------- 1 | module ZMQ 2 | # Set up all of the constants that are *common* to all API 3 | # versions 4 | 5 | # Socket types 6 | PAIR = 0 7 | PUB = 1 8 | SUB = 2 9 | REQ = 3 10 | REP = 4 11 | XREQ = 5 12 | XREP = 6 13 | PULL = 7 14 | PUSH = 8 15 | 16 | SocketTypeNameMap = { 17 | PAIR => "PAIR", 18 | PUB => "PUB", 19 | SUB => "SUB", 20 | REQ => "REQ", 21 | REP => "REP", 22 | PULL => "PULL", 23 | PUSH => "PUSH", 24 | XREQ => "XREQ", 25 | XREP => "XREP" 26 | } 27 | 28 | # Socket options 29 | AFFINITY = 4 30 | SUBSCRIBE = 6 31 | UNSUBSCRIBE = 7 32 | RATE = 8 33 | RECOVERY_IVL = 9 34 | SNDBUF = 11 35 | RCVBUF = 12 36 | RCVMORE = 13 37 | FD = 14 38 | EVENTS = 15 39 | TYPE = 16 40 | LINGER = 17 41 | RECONNECT_IVL = 18 42 | BACKLOG = 19 43 | RECONNECT_IVL_MAX = 21 44 | RCVTIMEO = 27 45 | SNDTIMEO = 28 46 | 47 | # Send/recv options 48 | SNDMORE = 2 49 | 50 | # I/O multiplexing 51 | 52 | POLL = 1 53 | POLLIN = 1 54 | POLLOUT = 2 55 | POLLERR = 4 56 | 57 | # Socket errors 58 | EAGAIN = Errno::EAGAIN::Errno 59 | EINVAL = Errno::EINVAL::Errno 60 | ENOMEM = Errno::ENOMEM::Errno 61 | ENODEV = Errno::ENODEV::Errno 62 | EFAULT = Errno::EFAULT::Errno 63 | EINTR = Errno::EINTR::Errno 64 | 65 | # ZMQ errors 66 | HAUSNUMERO = 156384712 67 | EFSM = (HAUSNUMERO + 51) 68 | ENOCOMPATPROTO = (HAUSNUMERO + 52) 69 | ETERM = (HAUSNUMERO + 53) 70 | EMTHREAD = (HAUSNUMERO + 54) 71 | 72 | # Rescue unknown constants and use the ZeroMQ defined values 73 | # Usually only happens on Windows though some don't resolve on 74 | # OSX too (ENOTSUP) 75 | ENOTSUP = Errno::ENOTSUP::Errno rescue (HAUSNUMERO + 1) 76 | EPROTONOSUPPORT = Errno::EPROTONOSUPPORT::Errno rescue (HAUSNUMERO + 2) 77 | ENOBUFS = Errno::ENOBUFS::Errno rescue (HAUSNUMERO + 3) 78 | ENETDOWN = Errno::ENETDOWN::Errno rescue (HAUSNUMERO + 4) 79 | EADDRINUSE = Errno::EADDRINUSE::Errno rescue (HAUSNUMERO + 5) 80 | EADDRNOTAVAIL = Errno::EADDRNOTAVAIL::Errno rescue (HAUSNUMERO + 6) 81 | ECONNREFUSED = Errno::ECONNREFUSED::Errno rescue (HAUSNUMERO + 7) 82 | EINPROGRESS = Errno::EINPROGRESS::Errno rescue (HAUSNUMERO + 8) 83 | ENOTSOCK = Errno::ENOTSOCK::Errno rescue (HAUSNUMERO + 9) 84 | EMSGSIZE = Errno::EMSGSIZE::Errno rescue (HAUSNUMERO + 10) 85 | EAFNOSUPPORT = Errno::EAFNOSUPPORT::Errno rescue (HAUSNUMERO + 11) 86 | ENETUNREACH = Errno::ENETUNREACH::Errno rescue (HAUSNUMERO + 12) 87 | ECONNABORTED = Errno::ECONNABORTED::Errno rescue (HAUSNUMERO + 13) 88 | ECONNRESET = Errno::ECONNRESET::Errno rescue (HAUSNUMERO + 14) 89 | ENOTCONN = Errno::ENOTCONN::Errno rescue (HAUSNUMERO + 15) 90 | ETIMEDOUT = Errno::ETIMEDOUT::Errno rescue (HAUSNUMERO + 16) 91 | EHOSTUNREACH = Errno::EHOSTUNREACH::Errno rescue (HAUSNUMERO + 17) 92 | ENETRESET = Errno::ENETRESET::Errno rescue (HAUSNUMERO + 18) 93 | 94 | # Device Types 95 | STREAMER = 1 96 | FORWARDER = 2 97 | QUEUE = 3 98 | end # module ZMQ 99 | 100 | 101 | if ZMQ::LibZMQ.version2? 102 | module ZMQ 103 | # Socket types 104 | UPSTREAM = PULL 105 | DOWNSTREAM = PUSH 106 | DEALER = XREQ 107 | ROUTER = XREP 108 | 109 | SocketTypeNameMap[ROUTER] = 'ROUTER' 110 | SocketTypeNameMap[DEALER] = 'DEALER' 111 | 112 | # Socket options 113 | HWM = 1 114 | IDENTITY = 5 115 | MCAST_LOOP = 10 116 | SWAP = 3 117 | RECOVERY_IVL_MSEC = 20 118 | 119 | # Send/recv options 120 | NOBLOCK = 1 121 | NonBlocking = NOBLOCK 122 | end 123 | end # version2? 124 | 125 | 126 | if ZMQ::LibZMQ.version3? 127 | module ZMQ 128 | # Socket types 129 | XPUB = 9 130 | XSUB = 10 131 | DEALER = XREQ 132 | ROUTER = XREP 133 | 134 | SocketTypeNameMap[ROUTER] = 'ROUTER' 135 | SocketTypeNameMap[DEALER] = 'DEALER' 136 | SocketTypeNameMap[XPUB] = 'XPUB' 137 | SocketTypeNameMap[XSUB] = 'XSUB' 138 | 139 | # Context options 140 | IO_THREADS = 1 141 | MAX_SOCKETS = 2 142 | IO_THREADS_DFLT = 1 143 | MAX_SOCKETS_DFLT = 1024 144 | 145 | # Socket options 146 | IDENTITY = 5 147 | MAXMSGSIZE = 22 148 | SNDHWM = 23 149 | RCVHWM = 24 150 | MULTICAST_HOPS = 25 151 | IPV4ONLY = 31 152 | LAST_ENDPOINT = 32 153 | ROUTER_BEHAVIOR = 33 154 | TCP_KEEPALIVE = 34 155 | TCP_KEEPALIVE_CNT = 35 156 | TCP_KEEPALIVE_IDLE = 36 157 | TCP_KEEPALIVE_INTVL = 37 158 | TCP_ACCEPT_FILTER = 38 159 | 160 | # Message options 161 | MORE = 1 162 | 163 | # Send/recv options 164 | DONTWAIT = 1 165 | SNDLABEL = 4 166 | NonBlocking = DONTWAIT 167 | 168 | # Socket events and monitoring 169 | EVENT_CONNECTED = 1 170 | EVENT_CONNECT_DELAYED = 2 171 | EVENT_CONNECT_RETRIED = 4 172 | EVENT_LISTENING = 8 173 | EVENT_BIND_FAILED = 16 174 | EVENT_ACCEPTED = 32 175 | EVENT_ACCEPT_FAILED = 64 176 | EVENT_CLOSED = 128 177 | EVENT_CLOSE_FAILED = 256 178 | EVENT_DISCONNECTED = 512 179 | EVENT_ALL = EVENT_CONNECTED | EVENT_CONNECT_DELAYED | EVENT_CONNECT_RETRIED | 180 | EVENT_LISTENING | EVENT_BIND_FAILED | EVENT_ACCEPTED | 181 | EVENT_ACCEPT_FAILED | EVENT_CLOSED | EVENT_CLOSE_FAILED | 182 | EVENT_DISCONNECTED 183 | 184 | # Socket & other errors 185 | EMFILE = Errno::EMFILE::Errno 186 | end 187 | end # version3? 188 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/poll.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module ZMQ 4 | class Poller 5 | extend Forwardable 6 | 7 | def_delegators :@poll_items, :size, :inspect 8 | attr_reader :readables, :writables 9 | 10 | def initialize 11 | @poll_items = ZMQ::PollItems.new 12 | @readables = [] 13 | @writables = [] 14 | end 15 | 16 | # Checks each registered socket for selectability based on the poll items' 17 | # registered +events+. Will block for up to +timeout+ milliseconds. 18 | # A millisecond is 1/1000 of a second, so to block for 1 second 19 | # pass the value "1000" to #poll. 20 | # 21 | # Pass "-1" or +:blocking+ for +timeout+ for this call to block 22 | # indefinitely. 23 | # 24 | # This method will return *immediately* when there are no registered 25 | # sockets. In that case, the +timeout+ parameter is not honored. To 26 | # prevent a CPU busy-loop, the caller of this method should detect 27 | # this possible condition (via #size) and throttle the call 28 | # frequency. 29 | # 30 | # Returns 0 when there are no registered sockets that are readable 31 | # or writable. 32 | # 33 | # Return 1 (or greater) to indicate the number of readable or writable 34 | # sockets. These sockets should be processed using the #readables and 35 | # #writables accessors. 36 | # 37 | # Returns -1 when there is an error. Use ZMQ::Util.errno to get the related 38 | # error number. 39 | # 40 | def poll timeout = :blocking 41 | unless @poll_items.empty? 42 | timeout = adjust timeout 43 | items_triggered = LibZMQ.zmq_poll @poll_items.address, @poll_items.size, timeout 44 | 45 | update_selectables if Util.resultcode_ok?(items_triggered) 46 | items_triggered 47 | else 48 | 0 49 | end 50 | end 51 | 52 | # The non-blocking version of #poll. See the #poll description for 53 | # potential exceptions. 54 | # 55 | # May return -1 when an error is encounted. Check ZMQ::Util.errno 56 | # to determine the underlying cause. 57 | # 58 | def poll_nonblock 59 | poll 0 60 | end 61 | 62 | # Register the +pollable+ for +events+. This method is idempotent meaning 63 | # it can be called multiple times with the same data and the socket 64 | # will only get registered at most once. Calling multiple times with 65 | # different values for +events+ will OR the event information together. 66 | # 67 | def register pollable, events = ZMQ::POLLIN | ZMQ::POLLOUT 68 | return if pollable.nil? || events.zero? 69 | 70 | unless item = @poll_items[pollable] 71 | item = PollItem.from_pollable(pollable) 72 | @poll_items << item 73 | end 74 | 75 | item.events |= events 76 | end 77 | 78 | # Deregister the +pollable+ for +events+. When there are no events left 79 | # or socket has been closed this also deletes the socket from the poll items. 80 | # 81 | def deregister pollable, events 82 | return unless pollable 83 | 84 | item = @poll_items[pollable] 85 | if item && (item.events & events) > 0 86 | item.events ^= events 87 | delete(pollable) if item.events.zero? || item.closed? 88 | true 89 | else 90 | false 91 | end 92 | end 93 | 94 | # A helper method to register a +pollable+ as readable events only. 95 | # 96 | def register_readable pollable 97 | register pollable, ZMQ::POLLIN 98 | end 99 | 100 | # A helper method to register a +pollable+ for writable events only. 101 | # 102 | def register_writable pollable 103 | register pollable, ZMQ::POLLOUT 104 | end 105 | 106 | # A helper method to deregister a +pollable+ for readable events. 107 | # 108 | def deregister_readable pollable 109 | deregister pollable, ZMQ::POLLIN 110 | end 111 | 112 | # A helper method to deregister a +pollable+ for writable events. 113 | # 114 | def deregister_writable pollable 115 | deregister pollable, ZMQ::POLLOUT 116 | end 117 | 118 | # Deletes the +pollable+ for all subscribed events. Called internally 119 | # when a socket has been deregistered and has no more events 120 | # registered anywhere. 121 | # 122 | # Can also be called directly to remove the socket from the polling 123 | # array. 124 | # 125 | def delete pollable 126 | return false if @poll_items.empty? 127 | @poll_items.delete(pollable) 128 | end 129 | 130 | def to_s; inspect; end 131 | 132 | private 133 | 134 | def update_selectables 135 | @readables.clear 136 | @writables.clear 137 | 138 | @poll_items.each do |poll_item| 139 | @readables << poll_item.pollable if poll_item.readable? 140 | @writables << poll_item.pollable if poll_item.writable? 141 | end 142 | end 143 | 144 | # Convert the timeout value to something usable by 145 | # the library. 146 | # 147 | # -1 or :blocking should be converted to -1. 148 | # 149 | # Users will pass in values measured as 150 | # milliseconds, so we need to convert that value to 151 | # microseconds for the library. 152 | # 153 | if LibZMQ.version2? 154 | def adjust timeout 155 | if :blocking == timeout || -1 == timeout 156 | -1 157 | else 158 | (timeout * 1000).to_i 159 | end 160 | end 161 | else 162 | # version3 changed units from microseconds to milliseconds 163 | def adjust timeout 164 | if :blocking == timeout || -1 == timeout 165 | -1 166 | else 167 | timeout.to_i 168 | end 169 | end 170 | end 171 | 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /examples/README.rdoc: -------------------------------------------------------------------------------- 1 | = Examples 2 | 3 | == Requirements 4 | 5 | 1. lib dir 6 | 7 | All of the examples assume the lib directory containing the gem sources is two directories up from the location of the example. 8 | 9 | 2. Installed libzmq library 10 | 11 | The ZeroMQ C libraries need to be downloaded, compiled and installed separately from the gem. Please see http://www.zeromq.org/area:download for links to the downloadable files along with some simple installation instructions. Also, be sure to check the FAQ if you run into problems with compiling. 12 | 13 | This gem auto-configures itself to conform to the API for 0mq 2.1.x and 3.1.x. The 0mq project started making backward-incompatible changes with the 3.x branch. Rather than create separate gems, this one handles all of them. 14 | 15 | It is possible to install the libzmq* files directly into the gem in the ext/ directory. This directory is checked for loadable libraries first before it falls back to checking the system paths. 16 | 17 | 3. One terminal window 18 | 19 | ZeroMQ is used to build network applications. At minimum, there is a "client" application and a "server" application that talk to each other over the network, IPC or an internal thread queue. Several of the examples start the client and server components within separate threads or use the polling mechanism to interleave I/O operations amongst several sockets. A few examples need two terminal windows because the client and server code is in separate files (local_lat.rb/remote_lat.rb, local_throughput.rb/remote_throughput.rb). 20 | 21 | == Latency Test 22 | 23 | The examples include a latency performance test. The example sets up a pair of REQ/REP sockets and send a message back and forth as fast as possible. There is only a single message in flight at any given moment. The time required to send the message the requested number of times determines overall single-message latency for this type of socket. 24 | 25 | ==== Files 26 | 27 | * latency_measurement.rb 28 | 29 | ==== Arguments 30 | 31 | The remote_lat.rb program takes 3 arguments: 32 | 33 | [link_address] Requires a transport string of the format "transport"://"endpoint"<:>. For example, tcp://127.0.0.1:5555 34 | 35 | [message size] Size of each message measured in bytes. Allowable range is 1 to 2^(64-1). 36 | 37 | [message count] The number of round-trips used for the latency measurements. Allowable range is 1 to 2^(64-1). 38 | 39 | 40 | ==== Execution 41 | 42 | In an open terminal window, execute the latency_measurement.rb file. 43 | 44 | % ruby latency_measurement.rb tcp://127.0.0.1:5555 1024 100_000 45 | 46 | On a relatively new system, it can run 100k messages in under 30 seconds. When complete, the program prints out a few statistics and exits. 47 | 48 | Running with a larger "message count" will yield a more accurate latency measurement since nearly all Ruby runtimes require a little warm up time to hit their stride. I recommend 100k as a minimum while 10 million is better for determining a true measure. 49 | 50 | On a desktop computer purchased in 2007, all of the Ruby runtimes report a latency of approximately 110 microseconds per message. For comparison, the pure C latency test reports approximately 88 microseconds of latency. 51 | 52 | 53 | 54 | == Throughput Test 55 | 56 | The examples include a throughput performance test. The example sets up a pair of PUB/SUB sockets and publish messages as fast as possible to a subscriber listening for every message. The publisher can send much faster than the subscriber can retrieve messages. 57 | 58 | Since the publisher completes first, the program waits for all subscribers to exit before closing the PUB socket. This is necessary because all enqueued messages are discarded when the socket is closed. 59 | 60 | The subscriber prints some statistics when it exits. 61 | 62 | ==== Files 63 | 64 | * throughput_measurement.rb 65 | 66 | ==== Arguments 67 | 68 | The throughput_measurement.rb program takes 3 arguments: 69 | 70 | [link_address] Requires a transport string of the format "transport"://"endpoint"<:>. For example, tcp://127.0.0.1:5555 71 | 72 | [message size] Size of each message measured in bytes. Allowable range is 1 to 2^(64-1). 73 | 74 | [message count] The number of round-trips used for the latency measurements. Allowable range is 1 to 2^(64-1). 75 | 76 | 77 | ==== Execution 78 | 79 | In an open terminal, execute the throughput_measurement.rb script. 80 | 81 | % ruby throughput_measurement.rb tcp://127.0.0.1:5555 1024 100_000 82 | 83 | On a relatively new system, it can run 100k messages in under 10 seconds. When complete, the program prints out a few statistics and exits. 84 | 85 | Running with a larger "message count" will yield a more accurate latency measurement since nearly all Ruby runtimes require a little warm up time to hit their stride. I recommend 100k as a minimum while 1 million is better for determining a true measure. NOTE! The publisher can send much faster than the subscriber so the publisher's queue will grow very rapidly in RAM. For 1 million messages (or more) this can consume hundreds of megabytes or gigabytes of RAM. On my system, sending 10 million messages requires 10 GB of RAM before the subscriber can catch up. 86 | 87 | On a desktop computer purchased in 2007, all of the Ruby runtimes report a throughput of approximately 150k messages per second. For comparison, the pure C throughput test reports approximately 260k messages per second. 88 | 89 | 90 | 91 | == Poll 92 | 93 | For a reasonable example of using zmq_poll(), take a look at the reqrep_poll.rb program. It illustrates the use of zmq_poll(), as wrapped by the Ruby library, for detecting and responding to read and write events recorded on sockets. It also shows how to use ZMQ::NO_BLOCK/ZMQ::DONTWAIT for non-blocking send and receive. 94 | 95 | ==== Files 96 | 97 | * reqrep_poll.rb 98 | 99 | ==== Arguments 100 | 101 | None. 102 | 103 | ==== Execution 104 | 105 | This program is completely self-contained, so it only requires a single terminal window for execution. 106 | 107 | % ruby reqrep_poll.rb 108 | 109 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | ffi-rzmq 2 | by Chuck Remes 3 | http://www.zeromq.org/bindings:ruby-ffi 4 | 5 | == DESCRIPTION: 6 | 7 | This gem wraps the ZeroMQ networking library using the ruby FFI (foreign 8 | function interface). It's a pure ruby wrapper so this gem can be loaded 9 | and run by any ruby runtime that supports FFI. That's all of them: 10 | MRI 1.9.x, Rubinius and JRuby. 11 | 12 | This single gem supports 0mq 2.2.x and 3.2.x 0mq APIs. The 0mq project started 13 | making backward-incompatible changes to the API with the 3.1.x release. 14 | The gem auto-configures itself to expose the API conforming to the loaded 15 | C library. 0mq API 3.0 is *not* supported; the 0mq community voted to 16 | abandon it. 17 | 18 | The impetus behind this library was to provide support for ZeroMQ in 19 | JRuby which has native threads. Unlike MRI, which has a GIL, JRuby and 20 | Rubinius allow for threaded access to Ruby code from outside extensions. 21 | ZeroMQ is heavily threaded, so until the MRI runtime removes its GIL, 22 | JRuby and Rubinius will likely be the best environments to run this library. 23 | 24 | Please read the History.txt file for a description of all changes, including 25 | API changes, since the last release! 26 | 27 | == PERFORMANCE: 28 | 29 | Check out the latest performance results: 30 | 31 | http://www.zeromq.org/bindings:ruby-ffi 32 | 33 | == FEATURES/PROBLEMS: 34 | 35 | This gem needs more tests. This gem has been battle tested by myself 36 | and others for over a year, so I am fairly confident that it is solid. 37 | However, it is inevitable that there will be bugs, so please open 38 | issues for them here or fork this project, fix them, and send me a pull 39 | request. 40 | 41 | The 'ffi' gem has dropped support for MRI 1.8.x. Since this project relies 42 | on that gem to load and run this code, then this project also no longer 43 | supports MRI 1.8.x. I recommend JRuby for the best performance and 44 | stability. 45 | 46 | All features are implemented. 47 | 48 | == BUILD STATUS: 49 | 50 | {Build Status}[http://travis-ci.org/chuckremes/ffi-rzmq] 51 | 52 | {}[https://codeclimate.com/github/chuckremes/ffi-rzmq] 53 | 54 | == SYNOPSIS: 55 | 56 | 0mq API v2 client code: 57 | 58 | require 'rubygems' 59 | require 'ffi-rzmq' 60 | 61 | if ARGV.length < 3 62 | puts "usage: local_lat " 63 | exit 64 | end 65 | 66 | bind_to = ARGV[0] 67 | message_size = ARGV[1].to_i 68 | roundtrip_count = ARGV[2].to_i 69 | 70 | ctx = ZMQ::Context.new 71 | s = ctx.socket ZMQ::REP 72 | rc = s.setsockopt(ZMQ::HWM, 100) 73 | rc = s.bind(bind_to) 74 | 75 | msg = "" 76 | roundtrip_count.times do 77 | rc = s.recv_string msg 78 | raise "Message size doesn't match, expected [#{message_size}] but received [#{msg.size}]" if message_size != msg.size 79 | rc = s.send_string msg, 0 80 | end 81 | 82 | 83 | 0mq API v2 server code: 84 | 85 | require 'rubygems' 86 | require 'ffi-rzmq' 87 | 88 | if ARGV.length < 3 89 | puts "usage: remote_lat " 90 | exit 91 | end 92 | 93 | connect_to = ARGV[0] 94 | message_size = ARGV[1].to_i 95 | roundtrip_count = ARGV[2].to_i 96 | 97 | ctx = ZMQ::Context.new 98 | s = ctx.socket ZMQ::REQ 99 | rc = s.connect(connect_to) 100 | 101 | msg = "#{ '3' * message_size }" 102 | 103 | start_time = Time.now 104 | 105 | msg = "" 106 | roundtrip_count.times do 107 | rc = s.send_string msg, 0 108 | rc = s.recv_string msg 109 | raise "Message size doesn't match, expected [#{message_size}] but received [#{msg.size}]" if message_size != msg.size 110 | end 111 | 112 | == Better Examples 113 | 114 | I highly recommend visiting the Learn Ruby 0mq project for a bunch of good code examples. 115 | 116 | http://github.com/andrewvc/learn-ruby-zeromq 117 | 118 | 119 | 120 | == REQUIREMENTS: 121 | 122 | * 0mq 2.2.x, 3.2.x or later; 2.0.x, 3.0.x and 3.1.x are no longer supported 123 | 124 | The ZeroMQ library must be installed on your system in a well-known location 125 | like /usr/local/lib. This is the default for new ZeroMQ installs. 126 | 127 | If you have installed ZeroMQ using brew, you need to `brew link zeromq` before installing this gem. 128 | 129 | Future releases may include the library as a C extension built at 130 | time of installation. 131 | 132 | * ffi (>= 1.0.0) 133 | 134 | This is a requirement for MRI only. Both Rubinius and JRuby have FFI support built 135 | in as a standard component. Do *not* run this gem under MRI with an old 'ffi' gem. 136 | It will crash randomly and you will be sad. 137 | 138 | 139 | == INSTALL: 140 | 141 | A full gem has been released to rubygems.org as of release 0.5.0. 142 | Make sure the ZeroMQ library is already installed on your system. 143 | 144 | % gem install ffi-rzmq # should grab the latest release 145 | 146 | 147 | To build from git master: 148 | 149 | % git clone git://github.com/chuckremes/ffi-rzmq 150 | % cd ffi-rzmq 151 | % gem build ffi-rzmq.gemspec 152 | % gem install ffi-rzmq-*.gem 153 | 154 | 155 | NOTE for Windows users! 156 | In order for this gem to find the libzmq.dll, it *must* be on the Windows PATH. Google 157 | for "modify windows path" for instructions on how to do that if you are unfamiliar with 158 | that activity. That DLL also requires that you copy libstdc++-6.dll and libgcc_s_sjlj-1.dll from DevKit MinGW into the same folder that you copied libzmq.dll. 159 | 160 | == LICENSE: 161 | 162 | (The MIT License) 163 | 164 | Copyright (c) 2013 Chuck Remes 165 | 166 | Permission is hereby granted, free of charge, to any person obtaining 167 | a copy of this software and associated documentation files (the 168 | 'Software'), to deal in the Software without restriction, including 169 | without limitation the rights to use, copy, modify, merge, publish, 170 | distribute, sublicense, and/or sell copies of the Software, and to 171 | permit persons to whom the Software is furnished to do so, subject to 172 | the following conditions: 173 | 174 | The above copyright notice and this permission notice shall be 175 | included in all copies or substantial portions of the Software. 176 | 177 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 178 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 179 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 180 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 181 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 182 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 183 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 184 | -------------------------------------------------------------------------------- /spec/poll_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module ZMQ 4 | 5 | describe Poller do 6 | 7 | context "when initializing" do 8 | include APIHelper 9 | 10 | it "should allocate a PollItems instance" do 11 | PollItems.should_receive(:new) 12 | Poller.new 13 | end 14 | 15 | end 16 | 17 | context "#register" do 18 | 19 | let(:pollable) { mock('pollable') } 20 | let(:poller) { Poller.new } 21 | let(:socket) { FFI::MemoryPointer.new(4) } 22 | let(:io) { stub(:posix_fileno => fd) } 23 | let(:fd) { 1 } 24 | 25 | it "returns false when given a nil pollable" do 26 | poller.register(nil, ZMQ::POLLIN).should be_false 27 | end 28 | 29 | it "returns false when given 0 for +events+ (e.g. no registration)" do 30 | poller.register(pollable, 0).should be_false 31 | end 32 | 33 | it "returns the default registered event value when given a valid pollable" do 34 | poller.register(pollable).should == (ZMQ::POLLIN | ZMQ::POLLOUT) 35 | end 36 | 37 | it "returns the registered event value when given a pollable responding to socket (ZMQ::Socket)" do 38 | pollable.should_receive(:socket).and_return(socket) 39 | poller.register(pollable, ZMQ::POLLIN).should == ZMQ::POLLIN 40 | end 41 | 42 | it "returns the registered event value when given a pollable responding to file descriptor (IO, BasicSocket)" do 43 | pollable.should_receive(:posix_fileno).and_return(fd) 44 | poller.register(pollable, ZMQ::POLLIN).should == ZMQ::POLLIN 45 | end 46 | 47 | it "returns the registered event value when given a pollable responding to io (SSLSocket)" do 48 | pollable.should_receive(:io).and_return(io) 49 | poller.register(pollable, ZMQ::POLLIN).should == ZMQ::POLLIN 50 | end 51 | 52 | end 53 | 54 | context "#deregister" do 55 | 56 | let(:pollable) { mock('pollable') } 57 | let(:poller) { Poller.new } 58 | let(:socket) { FFI::MemoryPointer.new(4) } 59 | let(:io) { stub(:posix_fileno => fd) } 60 | let(:fd) { 1 } 61 | 62 | it "returns true when deregistered pollable from event" do 63 | pollable.should_receive(:socket).at_least(:once).and_return(socket) 64 | poller.register(pollable) 65 | poller.deregister(pollable, ZMQ::POLLIN).should be_true 66 | end 67 | 68 | it "returns false when pollable not registered" do 69 | poller.deregister(pollable, ZMQ::POLLIN).should be_false 70 | end 71 | 72 | it "returns false when pollable not registered for deregistered event" do 73 | pollable.should_receive(:socket).at_least(:once).and_return(socket) 74 | poller.register(pollable, ZMQ::POLLOUT) 75 | poller.deregister(pollable, ZMQ::POLLIN).should be_false 76 | end 77 | 78 | it "deletes pollable when no events left" do 79 | poller.register(pollable, ZMQ::POLLIN) 80 | poller.deregister(pollable, ZMQ::POLLIN).should be_true 81 | poller.size.should == 0 82 | end 83 | 84 | it "deletes closed pollable responding to socket (ZMQ::Socket)" do 85 | pollable.should_receive(:socket).and_return(socket) 86 | poller.register(pollable) 87 | pollable.should_receive(:socket).and_return(nil) 88 | poller.deregister(pollable, ZMQ::POLLIN).should be_true 89 | poller.size.should == 0 90 | end 91 | 92 | it "deletes closed pollable responding to fileno (IO, BasicSocket)" do 93 | pollable.should_receive(:posix_fileno).and_return(fd) 94 | poller.register(pollable) 95 | pollable.should_receive(:closed?).and_return(true) 96 | poller.deregister(pollable, ZMQ::POLLIN).should be_true 97 | poller.size.should == 0 98 | end 99 | 100 | it "deletes closed pollable responding to io (SSLSocket)" do 101 | pollable.should_receive(:io).at_least(:once).and_return(io) 102 | poller.register(pollable) 103 | io.should_receive(:closed?).and_return(true) 104 | poller.deregister(pollable, ZMQ::POLLIN).should be_true 105 | poller.size.should == 0 106 | end 107 | 108 | end 109 | 110 | context "#delete" do 111 | 112 | before(:all) { @context = Context.new } 113 | after(:all) { @context.terminate } 114 | 115 | before(:each) do 116 | @socket = @context.socket(XREQ) 117 | @socket.setsockopt(LINGER, 0) 118 | @poller = Poller.new 119 | end 120 | 121 | after(:each) do 122 | @socket.close 123 | end 124 | 125 | it "should return false for an unregistered socket (i.e. not found)" do 126 | @poller.delete(@socket).should be_false 127 | end 128 | 129 | it "returns true for a sucessfully deleted socket when only 1 is registered" do 130 | socket1 = @context.socket(REP) 131 | socket1.setsockopt(LINGER, 0) 132 | 133 | @poller.register socket1 134 | @poller.delete(socket1).should be_true 135 | socket1.close 136 | end 137 | 138 | it "returns true for a sucessfully deleted socket when more than 1 is registered" do 139 | socket1 = @context.socket(REP) 140 | socket2 = @context.socket(REP) 141 | socket1.setsockopt(LINGER, 0) 142 | socket2.setsockopt(LINGER, 0) 143 | 144 | @poller.register socket1 145 | @poller.register socket2 146 | @poller.delete(socket2).should be_true 147 | socket1.close 148 | socket2.close 149 | end 150 | 151 | it "returns true for a successfully deleted socket when the socket has been previously closed" do 152 | socket1 = @context.socket(REP) 153 | socket1.setsockopt(LINGER, 0) 154 | 155 | @poller.register socket1 156 | socket1.close 157 | @poller.delete(socket1).should be_true 158 | end 159 | 160 | end 161 | 162 | context "poll" do 163 | include APIHelper 164 | 165 | before(:all) { @context = Context.new } 166 | after(:all) { @context.terminate } 167 | 168 | before(:each) do 169 | endpoint = "inproc://poll_test_#{SecureRandom.hex}" 170 | @sockets = [@context.socket(DEALER), @context.socket(ROUTER)] 171 | @sockets.each { |s| s.setsockopt(LINGER, 0) } 172 | @sockets.first.bind(endpoint) 173 | connect_to_inproc(@sockets.last, endpoint) 174 | @poller = Poller.new 175 | end 176 | 177 | after(:each) { @sockets.each(&:close) } 178 | 179 | it "returns 0 when there are no sockets to poll" do 180 | @poller.poll(100).should be_zero 181 | end 182 | 183 | it "returns 0 when there is a single socket to poll and no events" do 184 | @poller.register(@sockets.first, 0) 185 | @poller.poll(100).should be_zero 186 | end 187 | 188 | it "returns 1 when there is a read event on a socket" do 189 | first, last = @sockets 190 | @poller.register_readable(last) 191 | 192 | first.send_string('test') 193 | @poller.poll(1000).should == 1 194 | end 195 | 196 | it "returns 1 when there is a read event on one socket and the second socket has been removed from polling" do 197 | first, last = @sockets 198 | @poller.register_readable(last) 199 | @poller.register_writable(first) 200 | 201 | first.send_string('test') 202 | @poller.deregister_writable(first) 203 | @poller.poll(1000).should == 1 204 | end 205 | 206 | it "works with BasiSocket" do 207 | server = TCPServer.new("127.0.0.1", 0) 208 | f, port, host, addr = server.addr 209 | client = TCPSocket.new("127.0.0.1", port) 210 | s = server.accept 211 | 212 | @poller.register(s, ZMQ::POLLIN) 213 | @poller.register(client, ZMQ::POLLOUT) 214 | 215 | client.send("message", 0) 216 | 217 | @poller.poll.should == 2 218 | @poller.readables.should == [s] 219 | @poller.writables.should == [client] 220 | 221 | msg = s.read_nonblock(7) 222 | msg.should == "message" 223 | end 224 | 225 | it "works with IO objects" do 226 | r, w = IO.pipe 227 | @poller.register(r, ZMQ::POLLIN) 228 | @poller.register(w, ZMQ::POLLOUT) 229 | 230 | w.write("message") 231 | 232 | @poller.poll.should == 2 233 | @poller.readables.should == [r] 234 | @poller.writables.should == [w] 235 | 236 | msg = r.read(7) 237 | msg.should == "message" 238 | end 239 | 240 | it "works with SSLSocket" do 241 | crt, key = %w[crt key].map { |ext| File.read(File.join(File.dirname(__FILE__), "support", "test." << ext)) } 242 | 243 | ctx = OpenSSL::SSL::SSLContext.new 244 | ctx.key = OpenSSL::PKey::RSA.new(key) 245 | ctx.cert = OpenSSL::X509::Certificate.new(crt) 246 | 247 | server = TCPServer.new("127.0.0.1", 0) 248 | f, port, host, addr = server.addr 249 | client = TCPSocket.new("127.0.0.1", port) 250 | s = server.accept 251 | 252 | client = OpenSSL::SSL::SSLSocket.new(client) 253 | 254 | server = OpenSSL::SSL::SSLSocket.new(s, ctx) 255 | 256 | t = Thread.new { client.connect } 257 | s = server.accept 258 | t.join 259 | 260 | @poller.register_readable(s) 261 | @poller.register_writable(client) 262 | 263 | client.write("message") 264 | 265 | @poller.poll.should == 2 266 | @poller.readables.should == [s] 267 | @poller.writables.should == [client] 268 | 269 | msg = s.read(7) 270 | msg.should == "message" 271 | end 272 | end 273 | 274 | end 275 | 276 | end 277 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/libzmq.rb: -------------------------------------------------------------------------------- 1 | module ZMQ 2 | 3 | # Wraps the libzmq library and attaches to the functions that are 4 | # common across the 2.x and 3.x APIs. 5 | # 6 | module LibZMQ 7 | extend FFI::Library 8 | 9 | begin 10 | # bias the library discovery to a path inside the gem first, then 11 | # to the usual system paths 12 | inside_gem = File.join(File.dirname(__FILE__), '..', '..', 'ext') 13 | if FFI::Platform::IS_WINDOWS 14 | local_path=ENV['PATH'].split(';') 15 | else 16 | local_path=ENV['PATH'].split(':') 17 | end 18 | ZMQ_LIB_PATHS = [ 19 | inside_gem, '/usr/local/lib', '/opt/local/lib', '/usr/local/homebrew/lib', '/usr/lib64' 20 | ].map{|path| "#{path}/libzmq.#{FFI::Platform::LIBSUFFIX}"} 21 | ffi_lib(ZMQ_LIB_PATHS + %w{libzmq}) 22 | rescue LoadError 23 | if ZMQ_LIB_PATHS.push(*local_path).any? {|path| 24 | File.file? File.join(path, "libzmq.#{FFI::Platform::LIBSUFFIX}")} 25 | warn "Unable to load this gem. The libzmq library exists, but cannot be loaded." 26 | warn "If this is Windows:" 27 | warn "- Check that you have MSVC runtime installed or statically linked" 28 | warn "- Check that your DLL is compiled for #{FFI::Platform::ADDRESS_SIZE} bit" 29 | else 30 | warn "Unable to load this gem. The libzmq library (or DLL) could not be found." 31 | warn "If this is a Windows platform, make sure libzmq.dll is on the PATH." 32 | warn "If the DLL was built with mingw, make sure the other two dependent DLLs," 33 | warn "libgcc_s_sjlj-1.dll and libstdc++6.dll, are also on the PATH." 34 | warn "For non-Windows platforms, make sure libzmq is located in this search path:" 35 | warn ZMQ_LIB_PATHS.inspect 36 | end 37 | raise LoadError, "The libzmq library (or DLL) could not be loaded" 38 | end 39 | # Size_t not working properly on Windows 40 | find_type(:size_t) rescue typedef(:ulong, :size_t) 41 | 42 | # Context and misc api 43 | # 44 | # @blocking = true is a hint to FFI that the following (and only the following) 45 | # function may block, therefore it should release the GIL before calling it. 46 | # This can aid in situations where the function call will/may block and another 47 | # thread within the lib may try to call back into the ruby runtime. Failure to 48 | # release the GIL will result in a hang; the hint *may* allow things to run 49 | # smoothly for Ruby runtimes hampered by a GIL. 50 | # 51 | # This is really only honored by the MRI implementation but it *is* necessary 52 | # otherwise the runtime hangs (and requires a kill -9 to terminate) 53 | # 54 | @blocking = true 55 | attach_function :zmq_version, [:pointer, :pointer, :pointer], :void 56 | @blocking = true 57 | attach_function :zmq_errno, [], :int 58 | @blocking = true 59 | attach_function :zmq_strerror, [:int], :pointer 60 | 61 | def self.version 62 | if @version.nil? 63 | major = FFI::MemoryPointer.new :int 64 | minor = FFI::MemoryPointer.new :int 65 | patch = FFI::MemoryPointer.new :int 66 | LibZMQ.zmq_version major, minor, patch 67 | @version = {:major => major.read_int, :minor => minor.read_int, :patch => patch.read_int} 68 | end 69 | 70 | @version 71 | end 72 | 73 | def self.version2?() version[:major] == 2 && version[:minor] >= 1 end 74 | 75 | def self.version3?() version[:major] == 3 && version[:minor] >= 2 end 76 | 77 | # Context initialization and destruction 78 | @blocking = true 79 | attach_function :zmq_init, [:int], :pointer 80 | @blocking = true 81 | attach_function :zmq_term, [:pointer], :int 82 | 83 | # Message API 84 | @blocking = true 85 | attach_function :zmq_msg_init, [:pointer], :int 86 | @blocking = true 87 | attach_function :zmq_msg_init_size, [:pointer, :size_t], :int 88 | @blocking = true 89 | attach_function :zmq_msg_init_data, [:pointer, :pointer, :size_t, :pointer, :pointer], :int 90 | @blocking = true 91 | attach_function :zmq_msg_close, [:pointer], :int 92 | @blocking = true 93 | attach_function :zmq_msg_data, [:pointer], :pointer 94 | @blocking = true 95 | attach_function :zmq_msg_size, [:pointer], :size_t 96 | @blocking = true 97 | attach_function :zmq_msg_copy, [:pointer, :pointer], :int 98 | @blocking = true 99 | attach_function :zmq_msg_move, [:pointer, :pointer], :int 100 | 101 | # Used for casting pointers back to the struct 102 | # 103 | class Msg < FFI::Struct 104 | layout :content, :pointer, 105 | :flags, :uint8, 106 | :vsm_size, :uint8, 107 | :vsm_data, [:uint8, 30] 108 | end # class Msg 109 | 110 | # Socket API 111 | @blocking = true 112 | attach_function :zmq_socket, [:pointer, :int], :pointer 113 | @blocking = true 114 | attach_function :zmq_setsockopt, [:pointer, :int, :pointer, :int], :int 115 | @blocking = true 116 | attach_function :zmq_getsockopt, [:pointer, :int, :pointer, :pointer], :int 117 | @blocking = true 118 | attach_function :zmq_bind, [:pointer, :string], :int 119 | @blocking = true 120 | attach_function :zmq_connect, [:pointer, :string], :int 121 | @blocking = true 122 | attach_function :zmq_close, [:pointer], :int 123 | 124 | # Device API 125 | @blocking = true 126 | attach_function :zmq_device, [:int, :pointer, :pointer], :int 127 | 128 | # Poll API 129 | @blocking = true 130 | attach_function :zmq_poll, [:pointer, :int, :long], :int 131 | 132 | module PollItemLayout 133 | def self.included(base) 134 | if FFI::Platform::IS_WINDOWS && FFI::Platform::ADDRESS_SIZE==64 135 | # On Windows, zmq.h defines fd as a SOCKET, which is 64 bits on x64. 136 | fd_type=:uint64 137 | else 138 | fd_type=:int 139 | end 140 | base.class_eval do 141 | layout :socket, :pointer, 142 | :fd, fd_type, 143 | :events, :short, 144 | :revents, :short 145 | end 146 | end 147 | end # module PollItemLayout 148 | 149 | class PollItem < FFI::Struct 150 | include PollItemLayout 151 | 152 | def socket() self[:socket]; end 153 | 154 | def fd() self[:fd]; end 155 | 156 | def readable? 157 | (self[:revents] & ZMQ::POLLIN) > 0 158 | end 159 | 160 | def writable? 161 | (self[:revents] & ZMQ::POLLOUT) > 0 162 | end 163 | 164 | def both_accessible? 165 | readable? && writable? 166 | end 167 | 168 | def inspect 169 | "socket [#{socket}], fd [#{fd}], events [#{self[:events]}], revents [#{self[:revents]}]" 170 | end 171 | 172 | def to_s; inspect; end 173 | end # class PollItem 174 | 175 | end 176 | 177 | 178 | # Attaches to those functions specific to the 2.x API 179 | # 180 | if LibZMQ.version2? 181 | 182 | module LibZMQ 183 | # Socket api 184 | @blocking = true 185 | attach_function :zmq_recv, [:pointer, :pointer, :int], :int 186 | @blocking = true 187 | attach_function :zmq_send, [:pointer, :pointer, :int], :int 188 | end 189 | end 190 | 191 | 192 | # Attaches to those functions specific to the 3.x API 193 | # 194 | if LibZMQ.version3? 195 | 196 | module LibZMQ 197 | # New Context API 198 | @blocking = true 199 | attach_function :zmq_ctx_new, [], :pointer 200 | @blocking = true 201 | attach_function :zmq_ctx_destroy, [:pointer], :int 202 | @blocking = true 203 | attach_function :zmq_ctx_set, [:pointer, :int, :int], :int 204 | @blocking = true 205 | attach_function :zmq_ctx_get, [:pointer, :int], :int 206 | 207 | # Message API 208 | @blocking = true 209 | attach_function :zmq_msg_send, [:pointer, :pointer, :int], :int 210 | @blocking = true 211 | attach_function :zmq_msg_recv, [:pointer, :pointer, :int], :int 212 | @blocking = true 213 | attach_function :zmq_msg_more, [:pointer], :int 214 | @blocking = true 215 | attach_function :zmq_msg_get, [:pointer, :int], :int 216 | @blocking = true 217 | attach_function :zmq_msg_set, [:pointer, :int, :int], :int 218 | 219 | # Monitoring API 220 | # zmq_ctx_set_monitor is no longer supported as of version >= 3.2.1 221 | # replaced by zmq_socket_monitor 222 | if LibZMQ.version[:minor] > 2 || (LibZMQ.version[:minor] == 2 && LibZMQ.version[:patch] >= 1) 223 | @blocking = true 224 | attach_function :zmq_socket_monitor, [:pointer, :pointer, :int], :int 225 | else 226 | @blocking = true 227 | attach_function :zmq_ctx_set_monitor, [:pointer, :pointer], :int 228 | end 229 | 230 | # Socket API 231 | @blocking = true 232 | attach_function :zmq_unbind, [:pointer, :string], :int 233 | @blocking = true 234 | attach_function :zmq_disconnect, [:pointer, :string], :int 235 | @blocking = true 236 | attach_function :zmq_recvmsg, [:pointer, :pointer, :int], :int 237 | @blocking = true 238 | attach_function :zmq_recv, [:pointer, :pointer, :size_t, :int], :int 239 | @blocking = true 240 | attach_function :zmq_sendmsg, [:pointer, :pointer, :int], :int 241 | @blocking = true 242 | attach_function :zmq_send, [:pointer, :pointer, :size_t, :int], :int 243 | 244 | module EventDataLayout 245 | def self.included(base) 246 | base.class_eval do 247 | layout :event, :int, 248 | :addr, :string, 249 | :field2, :int 250 | end 251 | end 252 | end # module EventDataLayout 253 | 254 | class EventData < FFI::Struct 255 | include EventDataLayout 256 | 257 | def event() self[:event]; end 258 | 259 | def addr() self[:addr]; end 260 | alias :address :addr 261 | 262 | def fd() self[:field2]; end 263 | alias :err :fd 264 | alias :interval :fd 265 | 266 | def inspect 267 | "event [#{event}], addr [#{addr}], fd [#{fd}], field2 [#{fd}]" 268 | end 269 | 270 | def to_s; inspect; end 271 | end # class EventData 272 | 273 | end 274 | end 275 | 276 | 277 | # Sanity check; print an error and exit if we are trying to load an unsupported 278 | # version of libzmq. 279 | # 280 | unless LibZMQ.version2? || LibZMQ.version3? 281 | hash = LibZMQ.version 282 | version = "#{hash[:major]}.#{hash[:minor]}.#{hash[:patch]}" 283 | raise LoadError, "The libzmq version #{version} is incompatible with ffi-rzmq." 284 | end 285 | 286 | end # module ZMQ 287 | -------------------------------------------------------------------------------- /lib/ffi-rzmq/message.rb: -------------------------------------------------------------------------------- 1 | 2 | module ZMQ 3 | 4 | # The factory constructor optionally takes a string as an argument. It will 5 | # copy this string to native memory in preparation for transmission. 6 | # So, don't pass a string unless you intend to send it. Internally it 7 | # calls #copy_in_string. 8 | # 9 | # Call #close to release buffers when you are done with the data. 10 | # 11 | # (This class is not really zero-copy. Ruby makes this near impossible 12 | # since Ruby objects can be relocated in memory by the GC at any 13 | # time. There is no way to peg them to native memory or have them 14 | # use non-movable native memory as backing store.) 15 | # 16 | # Message represents ruby equivalent of the +zmq_msg_t+ C struct. 17 | # Access the underlying memory buffer and the buffer size using the 18 | # #data and #size methods respectively. 19 | # 20 | # It is recommended that this class be composed inside another class for 21 | # access to the underlying buffer. The outer wrapper class can provide 22 | # nice accessors for the information in the data buffer; a clever 23 | # implementation can probably lazily encode/decode the data buffer 24 | # on demand. Lots of protocols send more information than is strictly 25 | # necessary, so only decode (copy from the 0mq buffer to Ruby) that 26 | # which is necessary. 27 | # 28 | # When you are done using a *received* message object, call #close to 29 | # release the associated buffers. 30 | # 31 | # received_message = Message.create 32 | # if received_message 33 | # rc = socket.recvmsg(received_message) 34 | # if ZMQ::Util.resultcode_ok?(rc) 35 | # puts "Message contained: #{received_message.copy_out_string}" 36 | # else 37 | # STDERR.puts "Error when receiving message: #{ZMQ::Util.error_string}" 38 | # end 39 | # 40 | # 41 | # Define a custom layout for the data sent between 0mq peers. 42 | # 43 | # class MyMessage 44 | # class Layout < FFI::Struct 45 | # layout :value1, :uint8, 46 | # :value2, :uint64, 47 | # :value3, :uint32, 48 | # :value4, [:char, 30] 49 | # end 50 | # 51 | # def initialize msg_struct = nil 52 | # if msg_struct 53 | # @msg_t = msg_struct 54 | # @data = Layout.new(@msg_t.data) 55 | # else 56 | # @pointer = FFI::MemoryPointer.new :byte, Layout.size, true 57 | # @data = Layout.new @pointer 58 | # end 59 | # end 60 | # 61 | # def size() @size = @msg_t.size; end 62 | # 63 | # def value1 64 | # @data[:value1] 65 | # end 66 | # 67 | # def value4 68 | # @data[:value4].to_ptr.read_string 69 | # end 70 | # 71 | # def value1=(val) 72 | # @data[:value1] = val 73 | # end 74 | # 75 | # def create_sendable_message 76 | # msg = Message.new 77 | # msg.copy_in_bytes @pointer, Layout.size 78 | # end 79 | # 80 | # 81 | # message = Message.new 82 | # successful_read = socket.recv message 83 | # message = MyMessage.new message if successful_read 84 | # puts "value1 is #{message.value1}" 85 | # 86 | class Message 87 | 88 | # Recommended way to create a standard message. A Message object is 89 | # returned upon success, nil when allocation fails. 90 | # 91 | def self.create message = nil 92 | new(message) rescue nil 93 | end 94 | 95 | def initialize message = nil 96 | # allocate our own pointer so that we can tell it to *not* zero out 97 | # the memory; it's pointless work since the library is going to 98 | # overwrite it anyway. 99 | @pointer = FFI::MemoryPointer.new Message.msg_size, 1, false 100 | 101 | if message 102 | copy_in_string message 103 | else 104 | # initialize an empty message structure to receive a message 105 | result_code = LibZMQ.zmq_msg_init @pointer 106 | raise unless Util.resultcode_ok?(result_code) 107 | end 108 | end 109 | 110 | # Makes a copy of the ruby +string+ into a native memory buffer so 111 | # that libzmq can send it. The underlying library will handle 112 | # deallocation of the native memory buffer. 113 | # 114 | # Can only be initialized via #copy_in_string or #copy_in_bytes once. 115 | # 116 | def copy_in_string string 117 | string_size = string.respond_to?(:bytesize) ? string.bytesize : string.size 118 | copy_in_bytes string, string_size if string 119 | end 120 | 121 | # Makes a copy of +len+ bytes from the ruby string +bytes+. Library 122 | # handles deallocation of the native memory buffer. 123 | # 124 | # Can only be initialized via #copy_in_string or #copy_in_bytes once. 125 | # 126 | def copy_in_bytes bytes, len 127 | data_buffer = LibC.malloc len 128 | # writes the exact number of bytes, no null byte to terminate string 129 | data_buffer.write_string bytes, len 130 | 131 | # use libC to call free on the data buffer; earlier versions used an 132 | # FFI::Function here that called back into Ruby, but Rubinius won't 133 | # support that and there are issues with the other runtimes too 134 | LibZMQ.zmq_msg_init_data @pointer, data_buffer, len, LibC::Free, nil 135 | end 136 | 137 | # Provides the memory address of the +zmq_msg_t+ struct. Used mostly for 138 | # passing to other methods accessing the underlying library that 139 | # require a real data address. 140 | # 141 | def address 142 | @pointer 143 | end 144 | alias :pointer :address 145 | 146 | def copy source 147 | LibZMQ.zmq_msg_copy @pointer, source 148 | end 149 | 150 | def move source 151 | LibZMQ.zmq_msg_move @pointer, source 152 | end 153 | 154 | # Provides the size of the data buffer for this +zmq_msg_t+ C struct. 155 | # 156 | def size 157 | LibZMQ.zmq_msg_size @pointer 158 | end 159 | 160 | # Returns a pointer to the data buffer. 161 | # This pointer should *never* be freed. It will automatically be freed 162 | # when the +message+ object goes out of scope and gets garbage 163 | # collected. 164 | # 165 | def data 166 | LibZMQ.zmq_msg_data @pointer 167 | end 168 | 169 | # Returns the data buffer as a string. 170 | # 171 | # Note: If this is binary data, it won't print very prettily. 172 | # 173 | def copy_out_string 174 | data.read_string(size) 175 | end 176 | 177 | # Manually release the message struct and its associated data 178 | # buffer. 179 | # 180 | # Only releases the buffer a single time. Subsequent calls are 181 | # no ops. 182 | # 183 | def close 184 | rc = 0 185 | 186 | if @pointer 187 | rc = LibZMQ.zmq_msg_close @pointer 188 | @pointer = nil 189 | end 190 | 191 | rc 192 | end 193 | 194 | # cache the msg size so we don't have to recalculate it when creating 195 | # each new instance 196 | @msg_size = LibZMQ::Msg.size 197 | 198 | def self.msg_size() @msg_size; end 199 | 200 | end # class Message 201 | 202 | if LibZMQ.version3? 203 | class Message 204 | # Version3 only 205 | # 206 | def get(property) 207 | LibZMQ.zmq_msg_get(@pointer, property) 208 | end 209 | 210 | # Version3 only 211 | # 212 | # Returns true if this message has additional parts coming. 213 | # 214 | def more? 215 | Util.resultcode_ok?(get(MORE)) 216 | end 217 | 218 | def set(property, value) 219 | LibZMQ.zmq_msg_set(@pointer, property, value) 220 | end 221 | end 222 | end 223 | 224 | 225 | 226 | # A subclass of #Message that includes finalizers for deallocating 227 | # native memory when this object is garbage collected. Note that on 228 | # certain Ruby runtimes the use of finalizers can add 10s of 229 | # microseconds of overhead for each message. The convenience comes 230 | # at a price. 231 | # 232 | # The constructor optionally takes a string as an argument. It will 233 | # copy this string to native memory in preparation for transmission. 234 | # So, don't pass a string unless you intend to send it. Internally it 235 | # calls #copy_in_string. 236 | # 237 | # Call #close to release buffers when you have *not* passed this on 238 | # to Socket#send. That method calls #close on your behalf. 239 | # 240 | # When you are done using a *received* message object, just let it go out of 241 | # scope to release the memory. During the next garbage collection run 242 | # it will call the equivalent of #LibZMQ.zmq_msg_close to release 243 | # all buffers. Obviously, this automatic collection of message objects 244 | # comes at the price of a larger memory footprint (for the 245 | # finalizer proc object) and lower performance. If you wanted blistering 246 | # performance, Ruby isn't there just yet. 247 | # 248 | # As noted above, for sent objects the underlying library will call close 249 | # for you. 250 | # 251 | class ManagedMessage < Message 252 | # Makes a copy of +len+ bytes from the ruby string +bytes+. Library 253 | # handles deallocation of the native memory buffer. 254 | # 255 | def copy_in_bytes bytes, len 256 | rc = super(bytes, len) 257 | 258 | # make sure we have a way to deallocate this memory if the object goes 259 | # out of scope 260 | define_finalizer 261 | rc 262 | end 263 | 264 | # Manually release the message struct and its associated data 265 | # buffer. 266 | # 267 | def close 268 | rc = super() 269 | remove_finalizer 270 | rc 271 | end 272 | 273 | 274 | private 275 | 276 | def define_finalizer 277 | ObjectSpace.define_finalizer(self, self.class.close(@pointer)) 278 | end 279 | 280 | def remove_finalizer 281 | ObjectSpace.undefine_finalizer self 282 | end 283 | 284 | # Message finalizer 285 | # Note that there is no error checking for the call to #zmq_msg_close. 286 | # This is intentional. Since this code runs as a finalizer, there is no 287 | # way to catch a raised exception anywhere near where the error actually 288 | # occurred in the code, so we just ignore deallocation failures here. 289 | def self.close ptr 290 | Proc.new do 291 | # release the data buffer 292 | LibZMQ.zmq_msg_close ptr 293 | end 294 | end 295 | 296 | # cache the msg size so we don't have to recalculate it when creating 297 | # each new instance 298 | # need to do this again because ivars are not inheritable 299 | @msg_size = LibZMQ::Msg.size 300 | 301 | end # class ManagedMessage 302 | 303 | end # module ZMQ 304 | -------------------------------------------------------------------------------- /spec/nonblocking_recv_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 3 | 4 | module ZMQ 5 | 6 | 7 | describe Socket do 8 | include APIHelper 9 | 10 | 11 | shared_examples_for "any socket" do 12 | 13 | it "returns -1 when there are no messages to read" do 14 | array = [] 15 | rc = @receiver.recvmsgs(array, ZMQ::NonBlocking) 16 | Util.resultcode_ok?(rc).should be_false 17 | end 18 | 19 | it "gets EAGAIN when there are no messages to read" do 20 | array = [] 21 | rc = @receiver.recvmsgs(array, ZMQ::NonBlocking) 22 | ZMQ::Util.errno.should == ZMQ::EAGAIN 23 | end 24 | 25 | it "returns the given array unmodified when there are no messages to read" do 26 | array = [] 27 | rc = @receiver.recvmsgs(array, ZMQ::NonBlocking) 28 | array.size.should be_zero 29 | end 30 | 31 | end 32 | 33 | shared_examples_for "sockets without exposed envelopes" do 34 | 35 | it "read the single message and returns a successful result code" do 36 | poll_it_for_read(@receiver) do 37 | rc = @sender.send_string('test') 38 | Util.resultcode_ok?(rc).should be_true 39 | end 40 | 41 | array = [] 42 | rc = @receiver.recvmsgs(array, ZMQ::NonBlocking) 43 | Util.resultcode_ok?(rc).should be_true 44 | array.size.should == 1 45 | end 46 | 47 | it "read all message parts transmitted and returns a successful result code" do 48 | poll_it_for_read(@receiver) do 49 | strings = Array.new(10, 'test') 50 | rc = @sender.send_strings(strings) 51 | Util.resultcode_ok?(rc).should be_true 52 | end 53 | 54 | array = [] 55 | rc = @receiver.recvmsgs(array, ZMQ::NonBlocking) 56 | Util.resultcode_ok?(rc).should be_true 57 | array.size.should == 10 58 | end 59 | 60 | end 61 | 62 | shared_examples_for "sockets with exposed envelopes" do 63 | 64 | it "read the single message and returns a successful result code" do 65 | poll_it_for_read(@receiver) do 66 | rc = @sender.send_string('test') 67 | Util.resultcode_ok?(rc).should be_true 68 | end 69 | 70 | array = [] 71 | rc = @receiver.recvmsgs(array, ZMQ::NonBlocking) 72 | Util.resultcode_ok?(rc).should be_true 73 | array.size.should == 1 + 1 # extra 1 for envelope 74 | end 75 | 76 | it "read all message parts transmitted and returns a successful result code" do 77 | poll_it_for_read(@receiver) do 78 | strings = Array.new(10, 'test') 79 | rc = @sender.send_strings(strings) 80 | Util.resultcode_ok?(rc).should be_true 81 | end 82 | 83 | array = [] 84 | rc = @receiver.recvmsgs(array, ZMQ::NonBlocking) 85 | Util.resultcode_ok?(rc).should be_true 86 | array.size.should == 10 + 1 # add 1 for the envelope 87 | end 88 | 89 | end 90 | 91 | context "PUB" do 92 | 93 | describe "non-blocking #recvmsgs where sender connects & receiver binds" do 94 | include APIHelper 95 | 96 | before(:each) do 97 | @context = Context.new 98 | poller_setup 99 | 100 | endpoint = "inproc://nonblocking_test" 101 | @receiver = @context.socket ZMQ::SUB 102 | assert_ok(@receiver.setsockopt(ZMQ::SUBSCRIBE, '')) 103 | @sender = @context.socket ZMQ::PUB 104 | @receiver.bind(endpoint) 105 | connect_to_inproc(@sender, endpoint) 106 | end 107 | 108 | after(:each) do 109 | @receiver.close 110 | @sender.close 111 | @context.terminate 112 | end 113 | 114 | it_behaves_like "any socket" 115 | #it_behaves_like "sockets without exposed envelopes" # see Jira LIBZMQ-270; fails with tcp transport 116 | 117 | end # describe 'non-blocking recvmsgs' 118 | 119 | describe "non-blocking #recvmsgs where sender binds & receiver connects" do 120 | include APIHelper 121 | 122 | before(:each) do 123 | @context = Context.new 124 | poller_setup 125 | 126 | endpoint = "inproc://nonblocking_test" 127 | @receiver = @context.socket ZMQ::SUB 128 | port = connect_to_random_tcp_port(@receiver) 129 | assert_ok(@receiver.setsockopt(ZMQ::SUBSCRIBE, '')) 130 | @sender = @context.socket ZMQ::PUB 131 | @sender.bind(endpoint) 132 | connect_to_inproc(@receiver, endpoint) 133 | end 134 | 135 | after(:each) do 136 | @receiver.close 137 | @sender.close 138 | @context.terminate 139 | end 140 | 141 | it_behaves_like "any socket" 142 | it_behaves_like "sockets without exposed envelopes" # see Jira LIBZMQ-270; fails with tcp transport 143 | 144 | end # describe 'non-blocking recvmsgs' 145 | 146 | end # Pub 147 | 148 | context "REQ" do 149 | 150 | describe "non-blocking #recvmsgs where sender connects & receiver binds" do 151 | include APIHelper 152 | 153 | before(:each) do 154 | @context = Context.new 155 | poller_setup 156 | 157 | endpoint = "inproc://nonblocking_test" 158 | @receiver = @context.socket ZMQ::REP 159 | @sender = @context.socket ZMQ::REQ 160 | @receiver.bind(endpoint) 161 | connect_to_inproc(@sender, endpoint) 162 | end 163 | 164 | after(:each) do 165 | @receiver.close 166 | @sender.close 167 | @context.terminate 168 | end 169 | 170 | it_behaves_like "any socket" 171 | it_behaves_like "sockets without exposed envelopes" 172 | 173 | end # describe 'non-blocking recvmsgs' 174 | 175 | describe "non-blocking #recvmsgs where sender binds & receiver connects" do 176 | include APIHelper 177 | 178 | before(:each) do 179 | @context = Context.new 180 | poller_setup 181 | 182 | endpoint = "inproc://nonblocking_test" 183 | @receiver = @context.socket ZMQ::REP 184 | @sender = @context.socket ZMQ::REQ 185 | @sender.bind(endpoint) 186 | connect_to_inproc(@receiver, endpoint) 187 | end 188 | 189 | after(:each) do 190 | @receiver.close 191 | @sender.close 192 | @context.terminate 193 | end 194 | 195 | it_behaves_like "any socket" 196 | it_behaves_like "sockets without exposed envelopes" 197 | 198 | end # describe 'non-blocking recvmsgs' 199 | 200 | end # REQ 201 | 202 | 203 | context "PUSH" do 204 | 205 | describe "non-blocking #recvmsgs where sender connects & receiver binds" do 206 | include APIHelper 207 | 208 | before(:each) do 209 | @context = Context.new 210 | poller_setup 211 | 212 | endpoint = "inproc://nonblocking_test" 213 | @receiver = @context.socket ZMQ::PULL 214 | @sender = @context.socket ZMQ::PUSH 215 | @receiver.bind(endpoint) 216 | connect_to_inproc(@sender, endpoint) 217 | end 218 | 219 | after(:each) do 220 | @receiver.close 221 | @sender.close 222 | @context.terminate 223 | end 224 | 225 | it_behaves_like "any socket" 226 | it_behaves_like "sockets without exposed envelopes" 227 | 228 | end # describe 'non-blocking recvmsgs' 229 | 230 | describe "non-blocking #recvmsgs where sender binds & receiver connects" do 231 | include APIHelper 232 | 233 | before(:each) do 234 | @context = Context.new 235 | poller_setup 236 | 237 | endpoint = "inproc://nonblocking_test" 238 | @receiver = @context.socket ZMQ::PULL 239 | @sender = @context.socket ZMQ::PUSH 240 | @sender.bind(endpoint) 241 | connect_to_inproc(@receiver, endpoint) 242 | end 243 | 244 | after(:each) do 245 | @receiver.close 246 | @sender.close 247 | @context.terminate 248 | end 249 | 250 | it_behaves_like "any socket" 251 | it_behaves_like "sockets without exposed envelopes" 252 | 253 | end # describe 'non-blocking recvmsgs' 254 | 255 | end # PUSH 256 | 257 | 258 | context "DEALER" do 259 | 260 | describe "non-blocking #recvmsgs where sender connects & receiver binds" do 261 | include APIHelper 262 | 263 | before(:each) do 264 | @context = Context.new 265 | poller_setup 266 | 267 | endpoint = "inproc://nonblocking_test" 268 | @receiver = @context.socket ZMQ::ROUTER 269 | @sender = @context.socket ZMQ::DEALER 270 | @receiver.bind(endpoint) 271 | connect_to_inproc(@sender, endpoint) 272 | end 273 | 274 | after(:each) do 275 | @receiver.close 276 | @sender.close 277 | @context.terminate 278 | end 279 | 280 | it_behaves_like "any socket" 281 | it_behaves_like "sockets with exposed envelopes" 282 | 283 | end # describe 'non-blocking recvmsgs' 284 | 285 | describe "non-blocking #recvmsgs where sender binds & receiver connects" do 286 | include APIHelper 287 | 288 | before(:each) do 289 | @context = Context.new 290 | poller_setup 291 | 292 | endpoint = "inproc://nonblocking_test" 293 | @receiver = @context.socket ZMQ::ROUTER 294 | @sender = @context.socket ZMQ::DEALER 295 | @sender.bind(endpoint) 296 | connect_to_inproc(@receiver, endpoint) 297 | end 298 | 299 | after(:each) do 300 | @receiver.close 301 | @sender.close 302 | @context.terminate 303 | end 304 | 305 | it_behaves_like "any socket" 306 | it_behaves_like "sockets with exposed envelopes" 307 | 308 | end # describe 'non-blocking recvmsgs' 309 | 310 | end # DEALER 311 | 312 | 313 | context "XREQ" do 314 | 315 | describe "non-blocking #recvmsgs where sender connects & receiver binds" do 316 | include APIHelper 317 | 318 | before(:each) do 319 | @context = Context.new 320 | poller_setup 321 | 322 | endpoint = "inproc://nonblocking_test" 323 | @receiver = @context.socket ZMQ::XREP 324 | @sender = @context.socket ZMQ::XREQ 325 | @receiver.bind(endpoint) 326 | connect_to_inproc(@sender, endpoint) 327 | end 328 | 329 | after(:each) do 330 | @receiver.close 331 | @sender.close 332 | @context.terminate 333 | end 334 | 335 | it_behaves_like "any socket" 336 | it_behaves_like "sockets with exposed envelopes" 337 | 338 | end # describe 'non-blocking recvmsgs' 339 | 340 | describe "non-blocking #recvmsgs where sender binds & receiver connects" do 341 | include APIHelper 342 | 343 | before(:each) do 344 | @context = Context.new 345 | poller_setup 346 | 347 | endpoint = "inproc://nonblocking_test" 348 | @receiver = @context.socket ZMQ::XREP 349 | @sender = @context.socket ZMQ::XREQ 350 | @sender.bind(endpoint) 351 | connect_to_inproc(@receiver, endpoint) 352 | end 353 | 354 | after(:each) do 355 | @receiver.close 356 | @sender.close 357 | @context.terminate 358 | end 359 | 360 | it_behaves_like "any socket" 361 | it_behaves_like "sockets with exposed envelopes" 362 | 363 | end # describe 'non-blocking recvmsgs' 364 | 365 | end # XREQ 366 | 367 | end # describe Socket 368 | 369 | 370 | end # module ZMQ 371 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 1.0.1 / 20130318 2 | * Fix for issue #77 was not included in 1.0.0 release by mistake. 3 | 4 | * Add MIR 2.0.0 to travis runs. 5 | 6 | * Add support for LAST_ENDPOINT and MULTICAST_HOPS to Socket#getsockopt. 7 | 8 | == 1.0.0 / 20130109 9 | * Fix for issue #74 (send_multiple improperly handled single part messages). 10 | 11 | * Fix for issue #77 (unbind and disconnect). 12 | 13 | * Fix for issue #75 (socket monitor events). 14 | 15 | * The API is stable. Releasing 1.0. 16 | 17 | == 0.9.7 / 20121221 18 | * BROKE THE API. 19 | ZMQ::Poller#register and ZMQ::Poller#deregister don't take fd argument 20 | anymore. ZMQ::Poller#readables and ZMQ::Poller#writables return pollables 21 | instead of just fd when pollable is other than ZMQ socket. 22 | ZMQ::Poller#register now returns nil instead of false when no pollable 23 | or events to register to are given, which is consistent with rest of api. 24 | Thanks to Pawel Pacana for this code contribution. 25 | 26 | * Added support in ZMQ::Poller for pollables responding to fileno and socket. 27 | Standard Ruby Sockets and IOs can be now registered in poller to listen for 28 | events. Thanks to Pawel Pacana for this code contribution. 29 | 30 | * Fixed a bug in ZMQ::Poller#deregister where it would raise exception 31 | when trying to deregister already closed ZMQ socket. Issue 59. 32 | 33 | * Improved specs to use random inproc socket address to avoid race conditions 34 | between tests under same context. 35 | 36 | * Added continous integration for all supported platforms on Travis-CI. 37 | Thanks to Pawel Pacana for this code contribution. 38 | 39 | * Signed up for codeclimate.com and made some code changes to get a better 40 | "grade" from it. 41 | 42 | * Modified the library to *always* load the 'ffi' library upon startup. It 43 | used to be conditional for Rubinius. Thanks to brixen for the change. 44 | 45 | * There was a little bit of churn on the zmq "monitor" api. Thanks to 46 | Nilson Santos F. Jr. for some code to conditionally attach to the 47 | appropriate api depending on library version. 48 | 49 | 50 | 51 | == 0.9.6 / 20120808 52 | * Never released 0.9.5 as a gem. It was available via github only. 53 | 54 | * Improved error message when DLL loading fails on Windows. 55 | 56 | * Added support for 0mq 2.2. Support for 2.1 might be getting shakey... 57 | patches to make it fully support 2.1 (assuming it's even broken at 58 | all) are welcome. 59 | 60 | * Added support for 0mq 3.2 (no support for 3.0 or 3.1). Not all methods 61 | are exposed yet. For example, setting values on the context after it 62 | has been created is not supported; instead, pass the correct keys 63 | (:io_threads and :max_sockets) to the call to Context.create or 64 | Context#new. 65 | 66 | * Reduced spec running time from 30+ seconds to under 1 by eliminating 67 | most uses of "sleep." It now polls sockets to wait for message 68 | delivery. It also uses a technique of binding an inproc transport and 69 | busy-looping on the connect side until it succeeds. These techniques 70 | both allowed me to eliminate most uses of sleep. 71 | 72 | * Some changes to support usage on Win7x64 with a 64-bit libzmq DLL. 73 | 74 | == 0.9.5 / 20120119 75 | * BROKE THE API. 76 | In 0mq 2.x, there were two functions zmq_send() and zmq_recv(). 77 | As of 3.x, those functions were renamed zmq_sendmsg() and 78 | zmq_recvmsg(). As everyone starts moving to 0mq 3.x, it doesn't 79 | make sense to make the code break with 2.x. So, I'm breaking this 80 | binding so that it always uses sendmsg/recvmsg and eliminates the 81 | original send/recv methods. 82 | Sorry! 83 | This is likely to be the last non-backward-compatible API breakage. 84 | Release 1.0 is around the corner and the API will be stable (I follow 85 | semantic versioning). 86 | 87 | * Introduced ZMQ::NonBlocking. This flag returns the correct value to set 88 | a socket in non-blocking mode when sending/receiving. This hides the 89 | differences between 0mq 2.x and 3.x since the constant names have 90 | changed. 91 | 92 | == 0.9.4 / 20120102 93 | * Fixed bug in Poller#delete. Added specs to catch a regression. 94 | In short, a socket that was deleted from the Poller set wasn't 95 | always actually *removed* from the array. This led to a closed 96 | socket being part of the pollset which would return errno 38. 97 | This took about 4 days to find. 98 | 99 | 100 | == 0.9.3 / 20111214 101 | * Performance optimizations for #getsockopt. 102 | 103 | * Fixed Message#copy and Message#move. They didn't work before. 104 | 105 | * Cache LibZM::Msg.size in the ZMQ::Message class so that 106 | initialization can skip recalculating what is effectively a 107 | constant value. This speeds up ZMQ::Message instantiation by 108 | 5 to 10%. Wow. 109 | 110 | * Modified calls to #super to use explicit arguments (e.g. #super()) 111 | because otherwise the Ruby runtime has to (at runtime) dig out 112 | the arguments that are expected to be passed up the chain. By 113 | explicitly listing the args and using parentheses, the runtime 114 | can avoid that work and dispatch directly. This effects all 115 | Ruby runtimes, but it was through the work of Evan Phoenix that 116 | I figured this out. Results in a 2-5% speedup on method dispatch. 117 | 118 | 119 | 120 | == 0.9.2 / 20111115 121 | * Removed all references to the version4 API. 122 | 123 | * Dropped support for 3.0.x and added support for 3.1.x. The 0mq 124 | community has pretty much voted to abandon the path taken in 3.0 125 | so the 3.1 branch is the API that will be supported. 126 | 127 | * Fixed a bug in Poller#delete where it would erroneously return 128 | false even when it successfully deleted a socket. Issue 46. 129 | 130 | * All specs pass for 2.1.x API. 131 | 132 | * 3 specs fail when run with 3.1 API; these are due to bugs in the 133 | 0mq library and are *not* ffi-rzmq bugs. 134 | 135 | * Rescue LoadErrors when loading libzmq. Print a warning about 136 | adding libzmq.dll to the Windows PATH for that platform. Print 137 | the search paths where the gem looks for libzmq. 138 | 139 | == 0.9.1 / 20111027 140 | * Moved LibC and LibZMQ into the ZMQ module namespace. Necessary to 141 | avoid namespace collisions with other libraries that also use 142 | the constants LibC and/or LibZMQ. 143 | 144 | * Fixed a bug where file descriptors registered on Poll were never 145 | returned as readable or writable. 146 | 147 | * Added Socket#recv_multipart. This returns the message body and 148 | return address envelope as separate arrays. Only to be used with 149 | XREQ/XREP/DEALER/ROUTER sockets. 150 | 151 | == 0.9.0 / 20110930 152 | * Changed the behavior of every method that used to produce exceptions. 153 | The methods now behave more like the C API functions. They return 154 | result codes instead of raising exceptions. Further, the "receive" 155 | methods on Socket now all take an empty string as a buffer to read 156 | the message into. 157 | This is a BREAKING CHANGE and is NOT backward compatible with earlier 158 | releases. I apologize for the inconvenience, but this API will be 159 | much easier to test/spec and maintain. It will also allow for the 160 | production of more logical code. 161 | 162 | * Major refactoring of Socket internals so that a single gem can 163 | support libzmq 2.x, 3.x and 4.x APIs without any user intervention. 164 | The correct libzmq version is detected at runtime and used to 165 | configure the Ruby classes to conform to the proper API. 166 | 167 | * Added Socket#recvmsgs as a convenience method for receiving a 168 | multipart message into an array of Messages. 169 | 170 | * Added support for new 0mq API introduced in the 3.0 branch. 171 | API mostly changed for sending and receiving messages with new 172 | POSIX-compliant send() and recv() functions. The original 173 | functions were renamed sendmsg() and recvmsg(). 174 | Additionally, most getsockopt() and setsockopt() calls now use 175 | an int (4 bytes) instead of a mish-mash of 32-bit and 64-bit 176 | values. 177 | For a full list of differences, visit the 0mq wiki page at: 178 | http://www.zeromq.org/docs:3-0-upgrade 179 | 180 | * Created a new ext/ directory so that users can copy the libzmq* 181 | library files directly into the gem for easier distribution. This 182 | path is checked *before* the usual system paths. 183 | 184 | * Rewrote all examples to use the revised API. 185 | 186 | == 0.8.2 / 20110728 187 | * Fixed major bug with Socket#setsockopt when writing 8-byte longs. 188 | 189 | * Clarified a bit of logic for non-blocking sends. 190 | 191 | * Improved readability of exceptions. 192 | 193 | == 0.8.1 / 20110504 194 | * Fixed bug where Socket#setsockopt was using a size from the current 195 | runtime to determine how many bytes to use for HWM, et al. This was 196 | incorrect. All of those socket options require 8 bytes. Discovered 197 | this while getting the code running under mingw on Windows using a 198 | 32-bit Ruby runtime. 199 | 200 | == 0.8.0 / 20110307 201 | * API change! 202 | Socket#send_message no longer automatically calls 203 | Message#close on behalf of the user. The user is completely 204 | responsible for the lifecycle management of all buffers associated 205 | with the ZMQ::Message objects. 206 | This is a breaking change. 207 | If you want the old behavior (auto-close messages on send) then 208 | use the new Socket#send_and_close method which does as its name 209 | implies. 210 | * Fixed bug with type :size_t on Windows (thank you to arvicco) 211 | 212 | == 0.7.3 / 20110304 213 | * Fixed a bug where we had a small memory leak. When closing a socket 214 | I forgot to release a small amount of native memory used as a cache 215 | for doing #getsockopt calls. 216 | * Util.minimum_api? didn't work. Fixed. 217 | * Added ROUTER/DEALER constants to reflect new naming for XREQ/XREP. 218 | XREQ and XREP remain aliased for backward compatibility. 219 | 220 | == 0.7.2 / 20110224 221 | * Several minor refactorings to make the code intent clearer and to allow 222 | for better testing. In particular, the error condition checking for 223 | a non-blocking send/recv is much clearer. 224 | 225 | == 0.7.1 / 20110130 226 | * Fixed 1.9.1 Binary Encoding bug when UTF8 set as default (Thanks schmurfy) 227 | * Improved rubinius compat for specs 228 | * Improved spec compatibility on linux 229 | 230 | == 0.7.0 / 20101222 231 | * Improved performance of calls to Socket#getsockopt. There are usually 232 | a lot of calls passing RCVMORE, so we now cache those buffers instead 233 | of reallocating them every time. 234 | 235 | * Updated the docs on Poller#poll to warn about a possible busy-loop 236 | condition. 237 | 238 | * Fixed some more specs to conform with the 0mq 2.1 requirement that 239 | all sockets must be closed explicitly otherwise the program may 240 | hang on exit. 241 | 242 | == 0.6.1 / 20101127 243 | * API Change! 244 | Moved the #version method from the Util module and made it a class 245 | method instead. Invoke as ZMQ::Util.version. Used for conditionally 246 | enabling certain features based upon the 0mq version that is loaded. 247 | 248 | * Preliminary support for the Windows platform. Patches supplied 249 | by arvicco. 250 | 251 | * Added support for FD and EVENTS socket options. These were added 252 | in 0mq 2.1.0. Patches + specs supplied by andrewvc. 253 | 254 | * Added support for LINGER, RECONNECT_IVL, BACKLOG and 255 | RECOVERY_IVL_MSEC socket options. 256 | 257 | * Conditionally re-enable the socket finalizer when we are running 258 | with 0mq 2.1.0 or later. 259 | 260 | * Drop support for MRI 1.8.x since the 'ffi' gem has dropped it as a 261 | supported Ruby runtime with its 1.0 release. No action is taken to 262 | prevent running with MRI 1.8.x but it won't be supported. 263 | 264 | * Misc. spec fixes. Need more specs! 265 | 266 | == 0.6.0 / 20100911 267 | * API Change! Modified ZMQ::Message by removing automatic memory 268 | management. While doing some performance tests I saw that 269 | defining/undefining the finalizer added 15-30% processing 270 | overhead on the latency test. So, I split this functionality 271 | out to a subclass called ZMQ::ManagedMemory. Any existing code 272 | that relies on the default Message class to clean up after itself 273 | will now have a memory leak. Explicitly call #close on these 274 | received messages *unless* they are sent out again. The #send 275 | method automatically closes call on your behalf. 276 | 277 | * Rubinius/rbx compatibility! Requires an rbx code pull from git 278 | from 20100911 or later to get the necessary code fixes. 279 | 280 | * Modify Message to use the @pointer directly rather than indirectly 281 | via the @struct object. Provides better compatibility for rbx 282 | since rbx does not yet support the FFI pointer protocol for structs 283 | like the FFI gem. 284 | 285 | * Modify Message to pass libC's free function for disposing of message 286 | data buffers rather than trying to callback into ruby code to 287 | do the same thing. External thread callbacks into ruby code will 288 | never be supported in rbx; this also improves compatibility and 289 | performance with MRI and JRuby. (In particular, MRI enqueues these 290 | kinds of callbacks and spawns a *new* thread to execute each one. 291 | Avoiding the ruby callback entirely eliminates this extra work 292 | for MRI.) 293 | 294 | * Modify FFI wrapper to capture the libC dynamic library to fetch 295 | a pointer to the free function. 296 | 297 | * Modify FFI wrapper to remove the FFI::Function callback used 298 | by Message. It's no longer necessary since we now use free 299 | directly. 300 | 301 | == 0.5.1 / 20100830 302 | * Works with 0mq 2.0.8 release. 303 | 304 | * Removed the socket finalizer. The current 0mq framework cannot 305 | handle the case where zmq_close is called on a socket that was 306 | created from another thread. Therefore, the garbage collection 307 | thread causes the framework to break. Version 2.1 (or later) 308 | should fix this 0mq limitation. 309 | 310 | * Misc fixes. See commits. 311 | 312 | == 0.5.0 / 20100606 313 | * Updated the bindings to conform to the 0mq 2.0.7 release. 314 | Several parts of the API changed. 315 | 316 | * Updated all examples to use the new Context api. 317 | 318 | * Added Socket#getsockopt. 319 | 320 | * Added a Socket#identity and Socket#identity= method pair to 321 | allow for easy get/put on socket identities. Useful for async 322 | request/reply using XREQ/XREP sockets. 323 | 324 | * Added more specs (slowly but surely). 325 | 326 | * Support multi-part messages (new as of 2.0.7). I am unsure how 327 | to best support multi-part messages so the Message (and related) 328 | API may change in the future. Added Socket#more_parts?. 329 | 330 | * Lots of fixes. Many classes use finalizers to deallocate native 331 | memory when they go out of scope; be sure to use JRuby 1.5.1 or 332 | later to get important finalizer fixes. 333 | 334 | == 0.4.1 / 20100511 335 | * I was misusing all of the FFI memory allocator classes. I now 336 | wrap libc and use malloc/free directly for creating buffers 337 | used by libzmq. 338 | 339 | == 0.4.0 / 20100510 340 | * Changed the Socket#recv method signature to take an optional 341 | message object as its first argument. This allows the library 342 | user to allocate and pass in their own message object for the 343 | purposes of zero-copy. Original behavior was for the library to 344 | *always* allocate a new message object to receive a message into. 345 | Hopefully this is the last change required. 346 | 347 | * Modified the Socket constructor to take an optional hash as its 348 | final argument. It honors two keys; :receiver_klass and 349 | :sender_klass. Passing in a new constant for either (or both) keys 350 | will override the class used by Socket for allocating new 351 | Message objects. 352 | 353 | == 0.3.1 / 20100509 354 | * Modified ZMQ::Message so we have both an UnmanagedMessage where 355 | memory management is manual via the #close method, and Message where 356 | memory management is automated via a finalizer method run during 357 | garbage collection. 358 | 359 | * Updated ZMQ::Message docs to make it clearer how to use a subclass 360 | and FFI::Struct to lazily access the message buffer. This gets us as 361 | close to zero-copy as possible for performance. 362 | 363 | * Fixed a memory leak in Message where the FFI::Struct backing the 364 | C struct was not being freed. 365 | 366 | * Tested the FFI code against MRI 1.8.x and 1.9.x. It works! 367 | 368 | * Patched a potential problem in LibZMQ::MessageDeallocator. It was 369 | crashing under MRI because it complained that FFI::Pointer did not 370 | have a free method. It now checks for :free before calling it. 371 | Need to investigate this further because it never happened under 372 | JRuby. 373 | 374 | * Modified the Socket constructor slightly to allow for using 375 | unmanaged or managed messages. 376 | 377 | * Changed the /examples to print a throughput (msgs/s) number upon 378 | completion. 379 | 380 | == 0.3.0 / 20100507 381 | * ZMQ::Socket#send and ZMQ::Socket#recv semantics changed 382 | * The official 0mq ruby bindings utilize strings for #send and #recv. 383 | However, to do so requires lots of copying to and from buffers which 384 | greatly impacts performance. These methods now return a ZMQ::Message 385 | object which can be subclassed to do lazy evaluation of the buffer. 386 | 387 | * Added ZMQ::Socket#send_string and ZMQ::Socket#recv_string. They 388 | automatically convert the messages to strings just like the official 389 | 0mq ruby bindings. 390 | 391 | * Fixed bug in ZMQ::Util#error_string 392 | 393 | * Split the ZMQ::Message class into two classes. The base class called 394 | UnmanagedMessage requires manual memory management. The Message 395 | class (used by default by Socket) has a finalizer defined to 396 | automatically release memory when the message object gets garbage 397 | collected. 398 | 399 | 400 | == 0.2.0 / 20100505 401 | 402 | * 1 major enhancement 403 | * Birthday! 404 | -------------------------------------------------------------------------------- /spec/socket_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 3 | 4 | module ZMQ 5 | 6 | 7 | describe Socket do 8 | include APIHelper 9 | 10 | socket_types = if LibZMQ.version2? 11 | [ZMQ::REQ, ZMQ::REP, ZMQ::DEALER, ZMQ::ROUTER, ZMQ::PUB, ZMQ::SUB, ZMQ::PUSH, ZMQ::PULL, ZMQ::PAIR] 12 | elsif LibZMQ.version3? 13 | [ZMQ::REQ, ZMQ::REP, ZMQ::DEALER, ZMQ::ROUTER, ZMQ::PUB, ZMQ::SUB, ZMQ::PUSH, ZMQ::PULL, ZMQ::PAIR, ZMQ::XPUB, ZMQ::XSUB] 14 | end 15 | 16 | context "when initializing" do 17 | before(:all) { @ctx = Context.new } 18 | after(:all) { @ctx.terminate } 19 | 20 | 21 | it "should raise an error for a nil context" do 22 | lambda { Socket.new(FFI::Pointer.new(0), ZMQ::REQ) }.should raise_exception(ZMQ::ContextError) 23 | end 24 | 25 | it "works with a Context#pointer as the context_ptr" do 26 | lambda do 27 | s = Socket.new(@ctx.pointer, ZMQ::REQ) 28 | s.close 29 | end.should_not raise_exception(ZMQ::ContextError) 30 | end 31 | 32 | it "works with a Context instance as the context_ptr" do 33 | lambda do 34 | s = Socket.new(@ctx, ZMQ::SUB) 35 | s.close 36 | end.should_not raise_exception(ZMQ::ContextError) 37 | end 38 | 39 | 40 | socket_types.each do |socket_type| 41 | 42 | it "should not raise an error for a [#{ZMQ::SocketTypeNameMap[socket_type]}] socket type" do 43 | sock = nil 44 | lambda { sock = Socket.new(@ctx.pointer, socket_type) }.should_not raise_error 45 | sock.close 46 | end 47 | end # each socket_type 48 | 49 | it "should set the :socket accessor to the raw socket allocated by libzmq" do 50 | socket = mock('socket') 51 | socket.stub!(:null? => false) 52 | LibZMQ.should_receive(:zmq_socket).and_return(socket) 53 | 54 | sock = Socket.new(@ctx.pointer, ZMQ::REQ) 55 | sock.socket.should == socket 56 | end 57 | 58 | it "should define a finalizer on this object" do 59 | ObjectSpace.should_receive(:define_finalizer).at_least(1) 60 | sock = Socket.new(@ctx.pointer, ZMQ::REQ) 61 | sock.close 62 | end 63 | end # context initializing 64 | 65 | 66 | context "calling close" do 67 | before(:all) { @ctx = Context.new } 68 | after(:all) { @ctx.terminate } 69 | 70 | it "should call LibZMQ.close only once" do 71 | sock = Socket.new @ctx.pointer, ZMQ::REQ 72 | raw_socket = sock.socket 73 | 74 | LibZMQ.should_receive(:close).with(raw_socket) 75 | sock.close 76 | sock.close 77 | LibZMQ.close raw_socket # *really close it otherwise the context will block indefinitely 78 | end 79 | end # context calling close 80 | 81 | 82 | 83 | context "identity=" do 84 | before(:all) { @ctx = Context.new } 85 | after(:all) { @ctx.terminate } 86 | 87 | it "fails to set identity for identities in excess of 255 bytes" do 88 | sock = Socket.new @ctx.pointer, ZMQ::REQ 89 | 90 | sock.identity = ('a' * 256) 91 | sock.identity.should == '' 92 | sock.close 93 | end 94 | 95 | it "fails to set identity for identities of length 0" do 96 | sock = Socket.new @ctx.pointer, ZMQ::REQ 97 | 98 | sock.identity = '' 99 | sock.identity.should == '' 100 | sock.close 101 | end 102 | 103 | it "sets the identity for identities of 1 byte" do 104 | sock = Socket.new @ctx.pointer, ZMQ::REQ 105 | 106 | sock.identity = 'a' 107 | sock.identity.should == 'a' 108 | sock.close 109 | end 110 | 111 | it "set the identity identities of 255 bytes" do 112 | sock = Socket.new @ctx.pointer, ZMQ::REQ 113 | 114 | sock.identity = ('a' * 255) 115 | sock.identity.should == ('a' * 255) 116 | sock.close 117 | end 118 | 119 | it "should convert numeric identities to strings" do 120 | sock = Socket.new @ctx.pointer, ZMQ::REQ 121 | 122 | sock.identity = 7 123 | sock.identity.should == '7' 124 | sock.close 125 | end 126 | end # context identity= 127 | 128 | 129 | 130 | socket_types.each do |socket_type| 131 | 132 | context "#setsockopt for a #{ZMQ::SocketTypeNameMap[socket_type]} socket" do 133 | before(:all) { @ctx = Context.new } 134 | after(:all) { @ctx.terminate } 135 | 136 | let(:socket) do 137 | Socket.new @ctx.pointer, socket_type 138 | end 139 | 140 | after(:each) do 141 | socket.close 142 | end 143 | 144 | 145 | context "using option ZMQ::IDENTITY" do 146 | it "should set the identity given any string under 255 characters" do 147 | length = 4 148 | (1..255).each do |length| 149 | identity = 'a' * length 150 | socket.setsockopt ZMQ::IDENTITY, identity 151 | 152 | array = [] 153 | rc = socket.getsockopt(ZMQ::IDENTITY, array) 154 | rc.should == 0 155 | array[0].should == identity 156 | end 157 | end 158 | 159 | it "returns -1 given a string 256 characters or longer" do 160 | identity = 'a' * 256 161 | array = [] 162 | rc = socket.setsockopt(ZMQ::IDENTITY, identity) 163 | rc.should == -1 164 | end 165 | end # context using option ZMQ::IDENTITY 166 | 167 | 168 | if version2? 169 | 170 | context "using option ZMQ::HWM" do 171 | it "should set the high water mark given a positive value" do 172 | hwm = 4 173 | socket.setsockopt ZMQ::HWM, hwm 174 | array = [] 175 | rc = socket.getsockopt(ZMQ::HWM, array) 176 | rc.should == 0 177 | array[0].should == hwm 178 | end 179 | end # context using option ZMQ::HWM 180 | 181 | 182 | context "using option ZMQ::SWAP" do 183 | it "should set the swap value given a positive value" do 184 | swap = 10_000 185 | socket.setsockopt ZMQ::SWAP, swap 186 | array = [] 187 | rc = socket.getsockopt(ZMQ::SWAP, array) 188 | rc.should == 0 189 | array[0].should == swap 190 | end 191 | 192 | it "returns -1 given a negative value" do 193 | swap = -10_000 194 | rc = socket.setsockopt(ZMQ::SWAP, swap) 195 | rc.should == -1 196 | end 197 | end # context using option ZMQ::SWP 198 | 199 | 200 | context "using option ZMQ::MCAST_LOOP" do 201 | it "should enable the multicast loopback given a 1 (true) value" do 202 | socket.setsockopt ZMQ::MCAST_LOOP, 1 203 | array = [] 204 | rc = socket.getsockopt(ZMQ::MCAST_LOOP, array) 205 | rc.should == 0 206 | array[0].should be_true 207 | end 208 | 209 | it "should disable the multicast loopback given a 0 (false) value" do 210 | socket.setsockopt ZMQ::MCAST_LOOP, 0 211 | array = [] 212 | rc = socket.getsockopt(ZMQ::MCAST_LOOP, array) 213 | rc.should == 0 214 | array[0].should be_false 215 | end 216 | end # context using option ZMQ::MCAST_LOOP 217 | 218 | 219 | context "using option ZMQ::RECOVERY_IVL_MSEC" do 220 | it "should set the time interval for saving messages measured in milliseconds given a positive value" do 221 | value = 200 222 | socket.setsockopt ZMQ::RECOVERY_IVL_MSEC, value 223 | array = [] 224 | rc = socket.getsockopt(ZMQ::RECOVERY_IVL_MSEC, array) 225 | rc.should == 0 226 | array[0].should == value 227 | end 228 | 229 | it "should default to a value of -1" do 230 | value = -1 231 | array = [] 232 | rc = socket.getsockopt(ZMQ::RECOVERY_IVL_MSEC, array) 233 | rc.should == 0 234 | array[0].should == value 235 | end 236 | end # context using option ZMQ::RECOVERY_IVL_MSEC 237 | 238 | else # version3 or higher 239 | 240 | context "using option ZMQ::IPV4ONLY" do 241 | it "should enable use of IPV6 sockets when set to 0" do 242 | value = 0 243 | socket.setsockopt ZMQ::IPV4ONLY, value 244 | array = [] 245 | rc = socket.getsockopt(ZMQ::IPV4ONLY, array) 246 | rc.should == 0 247 | array[0].should == value 248 | end 249 | 250 | it "should default to a value of 1" do 251 | value = 1 252 | array = [] 253 | rc = socket.getsockopt(ZMQ::IPV4ONLY, array) 254 | rc.should == 0 255 | array[0].should == value 256 | end 257 | 258 | it "returns -1 given a negative value" do 259 | value = -1 260 | rc = socket.setsockopt ZMQ::IPV4ONLY, value 261 | rc.should == -1 262 | end 263 | 264 | it "returns -1 given a value > 1" do 265 | value = 2 266 | rc = socket.setsockopt ZMQ::IPV4ONLY, value 267 | rc.should == -1 268 | end 269 | end # context using option ZMQ::IPV4ONLY 270 | 271 | context "using option ZMQ::LAST_ENDPOINT" do 272 | it "should return last enpoint" do 273 | random_port = bind_to_random_tcp_port(socket, max_tries = 500) 274 | array = [] 275 | rc = socket.getsockopt(ZMQ::LAST_ENDPOINT, array) 276 | ZMQ::Util.resultcode_ok?(rc).should == true 277 | endpoint_regex = %r{\Atcp://(.*):(\d+)\0\z} 278 | array[0].should =~ endpoint_regex 279 | Integer(array[0][endpoint_regex, 2]).should == random_port 280 | end 281 | end 282 | end # version2? if/else block 283 | 284 | 285 | context "using option ZMQ::SUBSCRIBE" do 286 | if ZMQ::SUB == socket_type 287 | it "returns 0 for a SUB socket" do 288 | rc = socket.setsockopt(ZMQ::SUBSCRIBE, "topic.string") 289 | rc.should == 0 290 | end 291 | else 292 | it "returns -1 for non-SUB sockets" do 293 | rc = socket.setsockopt(ZMQ::SUBSCRIBE, "topic.string") 294 | rc.should == -1 295 | end 296 | end 297 | end # context using option ZMQ::SUBSCRIBE 298 | 299 | 300 | context "using option ZMQ::UNSUBSCRIBE" do 301 | if ZMQ::SUB == socket_type 302 | it "returns 0 given a topic string that was previously subscribed" do 303 | socket.setsockopt ZMQ::SUBSCRIBE, "topic.string" 304 | rc = socket.setsockopt(ZMQ::UNSUBSCRIBE, "topic.string") 305 | rc.should == 0 306 | end 307 | 308 | else 309 | it "returns -1 for non-SUB sockets" do 310 | rc = socket.setsockopt(ZMQ::UNSUBSCRIBE, "topic.string") 311 | rc.should == -1 312 | end 313 | end 314 | end # context using option ZMQ::UNSUBSCRIBE 315 | 316 | 317 | context "using option ZMQ::AFFINITY" do 318 | it "should set the affinity value given a positive value" do 319 | affinity = 3 320 | socket.setsockopt ZMQ::AFFINITY, affinity 321 | array = [] 322 | rc = socket.getsockopt(ZMQ::AFFINITY, array) 323 | rc.should == 0 324 | array[0].should == affinity 325 | end 326 | end # context using option ZMQ::AFFINITY 327 | 328 | 329 | context "using option ZMQ::RATE" do 330 | it "should set the multicast send rate given a positive value" do 331 | rate = 200 332 | socket.setsockopt ZMQ::RATE, rate 333 | array = [] 334 | rc = socket.getsockopt(ZMQ::RATE, array) 335 | rc.should == 0 336 | array[0].should == rate 337 | end 338 | 339 | it "returns -1 given a negative value" do 340 | rate = -200 341 | rc = socket.setsockopt ZMQ::RATE, rate 342 | rc.should == -1 343 | end 344 | end # context using option ZMQ::RATE 345 | 346 | 347 | context "using option ZMQ::RECOVERY_IVL" do 348 | it "should set the multicast recovery buffer measured in seconds given a positive value" do 349 | rate = 200 350 | socket.setsockopt ZMQ::RECOVERY_IVL, rate 351 | array = [] 352 | rc = socket.getsockopt(ZMQ::RECOVERY_IVL, array) 353 | rc.should == 0 354 | array[0].should == rate 355 | end 356 | 357 | it "returns -1 given a negative value" do 358 | rate = -200 359 | rc = socket.setsockopt ZMQ::RECOVERY_IVL, rate 360 | rc.should == -1 361 | end 362 | end # context using option ZMQ::RECOVERY_IVL 363 | 364 | 365 | context "using option ZMQ::SNDBUF" do 366 | it "should set the OS send buffer given a positive value" do 367 | size = 100 368 | socket.setsockopt ZMQ::SNDBUF, size 369 | array = [] 370 | rc = socket.getsockopt(ZMQ::SNDBUF, array) 371 | rc.should == 0 372 | array[0].should == size 373 | end 374 | end # context using option ZMQ::SNDBUF 375 | 376 | 377 | context "using option ZMQ::RCVBUF" do 378 | it "should set the OS receive buffer given a positive value" do 379 | size = 100 380 | socket.setsockopt ZMQ::RCVBUF, size 381 | array = [] 382 | rc = socket.getsockopt(ZMQ::RCVBUF, array) 383 | rc.should == 0 384 | array[0].should == size 385 | end 386 | end # context using option ZMQ::RCVBUF 387 | 388 | 389 | context "using option ZMQ::LINGER" do 390 | it "should set the socket message linger option measured in milliseconds given a positive value" do 391 | value = 200 392 | socket.setsockopt ZMQ::LINGER, value 393 | array = [] 394 | rc = socket.getsockopt(ZMQ::LINGER, array) 395 | rc.should == 0 396 | array[0].should == value 397 | end 398 | 399 | it "should set the socket message linger option to 0 for dropping packets" do 400 | value = 0 401 | socket.setsockopt ZMQ::LINGER, value 402 | array = [] 403 | rc = socket.getsockopt(ZMQ::LINGER, array) 404 | rc.should == 0 405 | array[0].should == value 406 | end 407 | 408 | if (ZMQ::SUB == socket_type) && version3? || (defined?(ZMQ::XSUB) && ZMQ::XSUB == socket_type) 409 | it "should default to a value of 0" do 410 | value = 0 411 | array = [] 412 | rc = socket.getsockopt(ZMQ::LINGER, array) 413 | rc.should == 0 414 | array[0].should == value 415 | end 416 | else 417 | it "should default to a value of -1" do 418 | value = -1 419 | array = [] 420 | rc = socket.getsockopt(ZMQ::LINGER, array) 421 | rc.should == 0 422 | array[0].should == value 423 | end 424 | end 425 | end # context using option ZMQ::LINGER 426 | 427 | 428 | context "using option ZMQ::RECONNECT_IVL" do 429 | it "should set the time interval for reconnecting disconnected sockets measured in milliseconds given a positive value" do 430 | value = 200 431 | socket.setsockopt ZMQ::RECONNECT_IVL, value 432 | array = [] 433 | rc = socket.getsockopt(ZMQ::RECONNECT_IVL, array) 434 | rc.should == 0 435 | array[0].should == value 436 | end 437 | 438 | it "should default to a value of 100" do 439 | value = 100 440 | array = [] 441 | rc = socket.getsockopt(ZMQ::RECONNECT_IVL, array) 442 | rc.should == 0 443 | array[0].should == value 444 | end 445 | end # context using option ZMQ::RECONNECT_IVL 446 | 447 | 448 | context "using option ZMQ::BACKLOG" do 449 | it "should set the maximum number of pending socket connections given a positive value" do 450 | value = 200 451 | socket.setsockopt ZMQ::BACKLOG, value 452 | array = [] 453 | rc = socket.getsockopt(ZMQ::BACKLOG, array) 454 | rc.should == 0 455 | array[0].should == value 456 | end 457 | 458 | it "should default to a value of 100" do 459 | value = 100 460 | array = [] 461 | rc = socket.getsockopt(ZMQ::BACKLOG, array) 462 | rc.should == 0 463 | array[0].should == value 464 | end 465 | end # context using option ZMQ::BACKLOG 466 | 467 | end # context #setsockopt 468 | 469 | 470 | context "#getsockopt for a #{ZMQ::SocketTypeNameMap[socket_type]} socket" do 471 | before(:all) { @ctx = Context.new } 472 | after(:all) { @ctx.terminate } 473 | 474 | let(:socket) do 475 | Socket.new @ctx.pointer, socket_type 476 | end 477 | 478 | after(:each) do 479 | socket.close 480 | end 481 | 482 | if RUBY_PLATFORM =~ /linux|darwin/ 483 | # this spec doesn't work on Windows; hints welcome 484 | 485 | context "using option ZMQ::FD" do 486 | it "should return an FD as a positive integer" do 487 | array = [] 488 | rc = socket.getsockopt(ZMQ::FD, array) 489 | rc.should == 0 490 | array[0].should > 0 491 | end 492 | 493 | it "returns a valid FD that is accepted by the system poll() function" do 494 | # Use FFI to wrap the C library function +poll+ so that we can execute it 495 | # on the 0mq file descriptor. If it returns 0, then it succeeded and the FD 496 | # is valid! 497 | module LibSocket 498 | extend FFI::Library 499 | # figures out the correct libc for each platform including Windows 500 | library = ffi_lib(FFI::Library::LIBC).first 501 | 502 | find_type(:nfds_t) rescue typedef(:uint32, :nfds_t) 503 | 504 | attach_function :poll, [:pointer, :nfds_t, :int], :int 505 | 506 | class PollFD < FFI::Struct 507 | layout :fd, :int, 508 | :events, :short, 509 | :revents, :short 510 | end 511 | end # module LibSocket 512 | 513 | array = [] 514 | rc = socket.getsockopt(ZMQ::FD, array) 515 | rc.should be_zero 516 | fd = array[0] 517 | 518 | # setup the BSD poll_fd struct 519 | pollfd = LibSocket::PollFD.new 520 | pollfd[:fd] = fd 521 | pollfd[:events] = 0 522 | pollfd[:revents] = 0 523 | 524 | rc = LibSocket.poll(pollfd, 1, 0) 525 | rc.should be_zero 526 | end 527 | end 528 | 529 | end # posix platform 530 | 531 | context "using option ZMQ::EVENTS" do 532 | it "should return a mask of events as a Fixnum" do 533 | array = [] 534 | rc = socket.getsockopt(ZMQ::EVENTS, array) 535 | rc.should == 0 536 | array[0].should be_a(Fixnum) 537 | end 538 | end 539 | 540 | context "using option ZMQ::TYPE" do 541 | it "should return the socket type" do 542 | array = [] 543 | rc = socket.getsockopt(ZMQ::TYPE, array) 544 | rc.should == 0 545 | array[0].should == socket_type 546 | end 547 | end 548 | end # context #getsockopt 549 | 550 | end # each socket_type 551 | 552 | 553 | describe "Mapping socket EVENTS to POLLIN and POLLOUT" do 554 | include APIHelper 555 | 556 | shared_examples_for "pubsub sockets where" do 557 | it "SUB socket that received a message always has POLLIN set" do 558 | events = [] 559 | rc = @sub.getsockopt(ZMQ::EVENTS, events) 560 | rc.should == 0 561 | events[0].should == ZMQ::POLLIN 562 | end 563 | 564 | it "PUB socket always has POLLOUT set" do 565 | events = [] 566 | rc = @pub.getsockopt(ZMQ::EVENTS, events) 567 | rc.should == 0 568 | events[0].should == ZMQ::POLLOUT 569 | end 570 | 571 | it "PUB socket never has POLLIN set" do 572 | events = [] 573 | rc = @pub.getsockopt(ZMQ::EVENTS, events) 574 | rc.should == 0 575 | events[0].should_not == ZMQ::POLLIN 576 | end 577 | 578 | it "SUB socket never has POLLOUT set" do 579 | events = [] 580 | rc = @sub.getsockopt(ZMQ::EVENTS, events) 581 | rc.should == 0 582 | events[0].should_not == ZMQ::POLLOUT 583 | end 584 | end # shared example for pubsub 585 | 586 | context "when SUB binds and PUB connects" do 587 | 588 | before(:each) do 589 | @ctx = Context.new 590 | poller_setup 591 | 592 | endpoint = "inproc://socket_test" 593 | @sub = @ctx.socket ZMQ::SUB 594 | rc = @sub.setsockopt ZMQ::SUBSCRIBE, '' 595 | rc.should == 0 596 | 597 | @pub = @ctx.socket ZMQ::PUB 598 | @sub.bind(endpoint) 599 | connect_to_inproc(@pub, endpoint) 600 | 601 | @pub.send_string('test') 602 | end 603 | 604 | #it_behaves_like "pubsub sockets where" # see Jira LIBZMQ-270 605 | end # context SUB binds PUB connects 606 | 607 | context "when SUB connects and PUB binds" do 608 | 609 | before(:each) do 610 | @ctx = Context.new 611 | poller_setup 612 | 613 | endpoint = "inproc://socket_test" 614 | @sub = @ctx.socket ZMQ::SUB 615 | rc = @sub.setsockopt ZMQ::SUBSCRIBE, '' 616 | 617 | @pub = @ctx.socket ZMQ::PUB 618 | @pub.bind(endpoint) 619 | connect_to_inproc(@sub, endpoint) 620 | 621 | poll_it_for_read(@sub) do 622 | rc = @pub.send_string('test') 623 | end 624 | end 625 | 626 | it_behaves_like "pubsub sockets where" 627 | end # context SUB binds PUB connects 628 | 629 | 630 | after(:each) do 631 | @sub.close 632 | @pub.close 633 | # must call close on *every* socket before calling terminate otherwise it blocks indefinitely 634 | @ctx.terminate 635 | end 636 | 637 | end # describe 'events mapping to pollin and pollout' 638 | 639 | end # describe Socket 640 | 641 | 642 | end # module ZMQ 643 | --------------------------------------------------------------------------------