├── History.txt ├── lib ├── em-zeromq │ ├── version.rb │ ├── event_emitter.rb │ ├── context.rb │ └── socket.rb └── em-zeromq.rb ├── Gemfile ├── .gitignore ├── Rakefile ├── spec ├── context_spec.rb ├── socket_spec.rb ├── spec_helper.rb ├── push_pull_spec.rb ├── pub_sub_spec.rb └── router_dealer_spec.rb ├── .bnsignore ├── example ├── simpler.rb ├── terminating.rb ├── multi-part.rb └── simple.rb ├── em-zeromq.gemspec └── README.md /History.txt: -------------------------------------------------------------------------------- 1 | == 1.0.0 / 2011-01-29 2 | 3 | * 1 major enhancement 4 | * Birthday! 5 | -------------------------------------------------------------------------------- /lib/em-zeromq/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module EmZeromq 3 | VERSION = "0.4.2" 4 | end 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in em-zeromq.gemspec 4 | gemspec 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.swn 4 | 5 | *.rbc 6 | *~ 7 | 8 | pkg 9 | 10 | *.gem 11 | .bundle 12 | Gemfile.lock 13 | pkg/* 14 | .rvmrc 15 | coverage/ 16 | *test*/ 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) do |t| 6 | ENV['COVERAGE'] = "1" 7 | t.rspec_opts = ['--color'] 8 | end 9 | task :default => :spec 10 | -------------------------------------------------------------------------------- /lib/em-zeromq.rb: -------------------------------------------------------------------------------- 1 | require 'eventmachine' 2 | require 'ffi-rzmq' 3 | 4 | # compatibilty hacks for zmq 2.x/3.x 5 | module ZMQ 6 | if LibZMQ.version3? 7 | NOBLOCK = DONTWAIT 8 | end 9 | end 10 | 11 | module EmZeromq 12 | 13 | end 14 | 15 | require 'em-zeromq/context' 16 | require 'em-zeromq/event_emitter' 17 | require 'em-zeromq/socket' 18 | -------------------------------------------------------------------------------- /spec/context_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | describe 'Context' do 4 | before do 5 | @ctx = EM::ZeroMQ::Context.new(1) 6 | end 7 | 8 | it 'can be created with a context' do 9 | zmq_ctx = ZMQ::Context.new(1) 10 | ctx = EM::ZeroMQ::Context.new( zmq_ctx ) 11 | ctx.instance_variable_get('@context').should == zmq_ctx 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /example/simpler.rb: -------------------------------------------------------------------------------- 1 | # Simpler than simple.rb ;) 2 | 3 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require 'em-zeromq' 6 | 7 | zmq = EM::ZeroMQ::Context.new(1) 8 | 9 | EM.run { 10 | push = zmq.socket(ZMQ::PUSH) 11 | push.connect("tcp://127.0.0.1:2091") 12 | 13 | pull = zmq.socket(ZMQ::PULL) 14 | pull.bind("tcp://127.0.0.1:2091") 15 | 16 | pull.on(:message) { |part| 17 | puts part.copy_out_string 18 | part.close 19 | } 20 | 21 | EM.add_periodic_timer(1) { 22 | push.send_msg("Hello") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/terminating.rb: -------------------------------------------------------------------------------- 1 | # This example shows how to use setsockopt to set the linger period for socket 2 | # shutdown. This is useful since by default pending meesages will block the 3 | # termination of the ZMQ context. 4 | 5 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 6 | 7 | require 'em-zeromq' 8 | 9 | zmq = EM::ZeroMQ::Context.new(1) 10 | 11 | EM.run { 12 | push = zmq.socket(ZMQ::PUSH) 13 | push.setsockopt(ZMQ::LINGER, 0) 14 | 15 | push.connect("ipc:///tmp/foo") 16 | 17 | push.send_msg('hello') 18 | 19 | Signal.trap('INT') { 20 | puts 'Trapped INT signal. Stopping eventmachine' 21 | EM.stop 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /spec/socket_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | describe 'Socket' do 4 | before do 5 | @ctx = EM::ZeroMQ::Context.new(1) 6 | end 7 | 8 | it 'can create a socket' do 9 | EM.run do 10 | s = @ctx.socket(ZMQ::ROUTER) 11 | s.instance_variable_get('@socket').name.should == 'ROUTER' 12 | EM.stop 13 | end 14 | end 15 | 16 | it 'can set hwm' do 17 | EM.run do 18 | s = @ctx.socket(ZMQ::PUSH) 19 | s.hwm = 100 20 | if defined?(ZMQ::HWM) 21 | s.hwm.should == 100 22 | else 23 | s.rcvhwm.should == 100 24 | s.sndhwm.should == 100 25 | end 26 | EM.stop 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/em-zeromq/event_emitter.rb: -------------------------------------------------------------------------------- 1 | module EventMachine 2 | module ZeroMQ 3 | module EventEmitter 4 | def on(event, &listener) 5 | _listeners[event] << listener 6 | end 7 | 8 | def emit(event, *args) 9 | _listeners[event].each { |l| l.call(*args) } 10 | end 11 | 12 | def remove_listener(event, &listener) 13 | _listeners[event].delete(listener) 14 | end 15 | 16 | def remove_all_listeners(event) 17 | _listeners.delete(event) 18 | end 19 | 20 | def listeners(event) 21 | _listeners[event] 22 | end 23 | 24 | private 25 | 26 | def _listeners 27 | @_listeners ||= Hash.new { |h,k| h[k] = [] } 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /example/multi-part.rb: -------------------------------------------------------------------------------- 1 | # This example shows how one might deal with sending and recieving multi-part 2 | # messages 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | 6 | require 'em-zeromq' 7 | 8 | zmq = EM::ZeroMQ::Context.new(1) 9 | 10 | EM.run { 11 | pull = zmq.socket(ZMQ::PULL) 12 | pull.bind("ipc:///tmp/test") 13 | 14 | pull.on(:message) { |part1, part2| 15 | p [:part1, part1.copy_out_string, :part2, part2.copy_out_string] 16 | part1.close 17 | part2.close 18 | } 19 | 20 | pull.on(:message) { |*parts| 21 | p [:parts, parts.map(&:copy_out_string)] 22 | parts.each(&:close) 23 | } 24 | 25 | push = zmq.socket(ZMQ::PUSH) 26 | push.connect("ipc:///tmp/test") 27 | 28 | i = 0 29 | EM.add_periodic_timer(1) { 30 | puts "Sending 2-part message" 31 | i += 1 32 | push.send_msg("hello #{i}", "second part") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'set' 3 | Thread.abort_on_exception = true 4 | 5 | if ENV['COVERAGE'] 6 | require 'simplecov' 7 | SimpleCov.start 8 | end 9 | 10 | require File.expand_path( 11 | File.join(File.dirname(__FILE__), %w[.. lib em-zeromq])) 12 | 13 | def run_reactor(time=0.1,&block) 14 | EM.run do 15 | yield 16 | EM.add_timer(time){EM.stop} 17 | end 18 | end 19 | 20 | USED_RAND_ADDRS = Set.new 21 | def rand_addr(scheme='tcp') 22 | addr = nil 23 | loop do 24 | case scheme 25 | when 'tcp' 26 | addr = "tcp://127.0.0.1:#{rand(10_000) + 20_000}" 27 | when 'inproc' 28 | addr = "inproc://testinp-#{rand(10_000) + 20_000}" 29 | end 30 | 31 | if USED_RAND_ADDRS.include? addr 32 | next 33 | else 34 | USED_RAND_ADDRS << addr 35 | break 36 | end 37 | end 38 | addr 39 | end 40 | 41 | SPEC_CTX = EM::ZeroMQ::Context.new(1) 42 | def spec_ctx 43 | SPEC_CTX 44 | end 45 | -------------------------------------------------------------------------------- /lib/em-zeromq/context.rb: -------------------------------------------------------------------------------- 1 | # 2 | # different ways to create a socket: 3 | # ctx.bind(:xreq, 'tcp://127.0.0.1:6666') 4 | # ctx.bind('xreq', 'tcp://127.0.0.1:6666') 5 | # ctx.bind(ZMQ::XREQ, 'tcp://127.0.0.1:6666') 6 | # 7 | module EventMachine 8 | module ZeroMQ 9 | class Context 10 | 11 | 12 | def initialize(threads_or_context) 13 | if threads_or_context.is_a?(ZMQ::Context) 14 | @context = threads_or_context 15 | else 16 | @context = ZMQ::Context.new(threads_or_context) 17 | end 18 | end 19 | 20 | ## 21 | # Create a socket in this context. 22 | # 23 | # @param [Integer] socket_type One of ZMQ::REQ, ZMQ::REP, ZMQ::PULL, ZMQ::PUSH, 24 | # ZMQ::ROUTER, ZMQ::DEALER 25 | # 26 | # 27 | def socket(socket_type) 28 | zmq_socket = @context.socket(socket_type) 29 | 30 | fd = [] 31 | if zmq_socket.getsockopt(ZMQ::FD, fd) < 0 32 | raise "Unable to get socket FD: #{ZMQ::Util.error_string}" 33 | end 34 | 35 | EM.watch(fd[0], EventMachine::ZeroMQ::Socket, zmq_socket, socket_type) 36 | end 37 | 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /em-zeromq.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "em-zeromq/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "em-zeromq" 7 | s.version = EmZeromq::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Andrew Cholakian", "Julien Ammous"] 10 | s.email = ["schmurfy@gmail.com"] 11 | s.homepage = "https://github.com/andrewvc/em-zeromq" 12 | s.summary = %q{Low level event machine support for ZeroMQ} 13 | s.description = %q{Low level event machine support for ZeroMQ} 14 | s.rdoc_options = ["--main", "README.md"] 15 | 16 | s.rubyforge_project = "em-zeromq" 17 | 18 | s.add_dependency 'eventmachine', '>= 1.0.0' 19 | s.add_dependency 'ffi', '>= 1.0.0' 20 | s.add_dependency 'ffi-rzmq', '~> 1.0.1' 21 | 22 | s.add_development_dependency 'rspec', '>= 2.5.0' 23 | s.add_development_dependency 'simplecov' 24 | s.add_development_dependency 'rake' 25 | 26 | s.files = `git ls-files`.split("\n") 27 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 28 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 29 | s.require_paths = ["lib"] 30 | end 31 | -------------------------------------------------------------------------------- /example/simple.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 3 | require 'em-zeromq' 4 | 5 | Thread.abort_on_exception = true 6 | 7 | trap('INT') do 8 | EM::stop() 9 | end 10 | 11 | puts "Started (with zmq #{ZMQ::Util.version.join('.')})." 12 | 13 | ctx = EM::ZeroMQ::Context.new(1) 14 | 15 | EM.run do 16 | # setup push sockets 17 | push_socket1 = ctx.socket(ZMQ::PUSH) 18 | 19 | push_socket1.hwm = 40 20 | puts "HWM: #{push_socket1.hwm}" 21 | 22 | push_socket1.bind('tcp://127.0.0.1:2091') 23 | 24 | push_socket2 = ctx.socket(ZMQ::PUSH) 25 | push_socket2.bind('ipc:///tmp/a') 26 | 27 | push_socket3 = ctx.socket(ZMQ::PUSH) 28 | push_socket3.bind('inproc://simple_test') 29 | 30 | # setup one pull sockets listening to all push sockets 31 | pull_socket = ctx.socket(ZMQ::PULL) 32 | pull_socket.connect('tcp://127.0.0.1:2091') 33 | pull_socket.connect('ipc:///tmp/a') 34 | pull_socket.connect('inproc://simple_test') 35 | 36 | pull_socket.on(:message) { |*parts| 37 | parts.each do |m| 38 | puts m.copy_out_string 39 | m.close 40 | end 41 | } 42 | 43 | n = 0 44 | 45 | EM::PeriodicTimer.new(0.1) do 46 | puts '.' 47 | push_socket1.send_msg("t#{n += 1}_") 48 | push_socket2.send_msg("i#{n += 1}_") 49 | push_socket3.send_msg("p#{n += 1}_") 50 | end 51 | end 52 | 53 | puts "Completed." 54 | -------------------------------------------------------------------------------- /spec/push_pull_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | describe EventMachine::ZeroMQ do 4 | it "Should instantiate a connection given valid opts" do 5 | pull_conn = nil 6 | run_reactor do 7 | pull_conn = SPEC_CTX.socket(ZMQ::PULL) 8 | pull_conn.bind(rand_addr) 9 | end 10 | pull_conn.should be_a(EventMachine::ZeroMQ::Socket) 11 | end 12 | 13 | describe "sending/receiving a single message via PUB/SUB" do 14 | before(:all) do 15 | @results = {} 16 | @received = [] 17 | @test_message = test_message = "TMsg#{rand(999)}" 18 | 19 | run_reactor(0.2) do 20 | address = rand_addr 21 | 22 | pull_conn = SPEC_CTX.socket(ZMQ::PULL) 23 | pull_conn.bind(address) 24 | pull_conn.on(:message) { |m| 25 | @received << m 26 | } 27 | 28 | push_conn = SPEC_CTX.socket(ZMQ::PUSH) 29 | push_conn.connect(address) 30 | 31 | push_conn.socket.send_string test_message, ZMQ::NOBLOCK 32 | 33 | EM::Timer.new(0.1) { @results[:specs_ran] = true } 34 | end 35 | end 36 | 37 | it "should run completely" do 38 | @results[:specs_ran].should be_true 39 | end 40 | 41 | it "should receive the message intact" do 42 | @received.should_not be_empty 43 | @received.first.should be_a(ZMQ::Message) 44 | @received.first.copy_out_string.should == @test_message 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/pub_sub_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | describe EventMachine::ZeroMQ do 4 | it "Should instantiate a connection given valid opts" do 5 | sub_conn = nil 6 | address = rand_addr 7 | 8 | run_reactor do 9 | sub_conn = SPEC_CTX.socket(ZMQ::PUB) 10 | sub_conn.bind(address) 11 | end 12 | sub_conn.should be_a(EventMachine::ZeroMQ::Socket) 13 | end 14 | 15 | describe "sending/receiving a single message via PUB/SUB" do 16 | before(:all) do 17 | @results = {} 18 | @received = [] 19 | @test_message = test_message = "TMsg#{rand(999)}" 20 | 21 | run_reactor(0.2) do 22 | address = rand_addr 23 | 24 | sub_conn = SPEC_CTX.socket(ZMQ::SUB) 25 | sub_conn.bind(address) 26 | sub_conn.subscribe('') 27 | sub_conn.on(:message) { |m| 28 | @received << m 29 | } 30 | 31 | pub_conn = SPEC_CTX.socket(ZMQ::PUB) 32 | pub_conn.connect(address) 33 | 34 | EM::Timer.new(0.1) do 35 | pub_conn.socket.send_string test_message, ZMQ::NOBLOCK 36 | end 37 | end 38 | end 39 | 40 | it "should run completely" do 41 | @received.should be_true 42 | end 43 | 44 | it "should receive one message" do 45 | @received.length.should == 1 46 | end 47 | 48 | it "should receive the message as a ZMQ::Message" do 49 | @received.first.should be_a(ZMQ::Message) 50 | end 51 | 52 | it "should receive the message intact" do 53 | @received.first.copy_out_string.should == @test_message 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/router_dealer_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), %w[spec_helper]) 2 | 3 | describe EventMachine::ZeroMQ do 4 | it "Should instantiate a connection given valid opts for Router/Dealer" do 5 | router_conn = nil 6 | run_reactor do 7 | router_conn = SPEC_CTX.socket(ZMQ::ROUTER) 8 | router_conn.bind(rand_addr) 9 | end 10 | router_conn.should be_a(EventMachine::ZeroMQ::Socket) 11 | end 12 | 13 | describe "sending/receiving a single message via Router/Dealer" do 14 | before(:all) do 15 | @results = {} 16 | @dealer_received, @router_received = [], [] 17 | @test_message = test_message = "M#{rand(999)}" 18 | 19 | run_reactor(0.3) do 20 | addr = rand_addr 21 | dealer_conn = SPEC_CTX.socket(ZMQ::DEALER) 22 | dealer_conn.identity = "dealer1" 23 | dealer_conn.bind(addr) 24 | dealer_conn.on(:message) { |message| 25 | # 2. Dealer receives messages, sends reply back to router 26 | @dealer_received << message 27 | dealer_conn.send_msg("re:#{message.copy_out_string}") 28 | } 29 | 30 | router_conn = SPEC_CTX.socket(ZMQ::ROUTER) 31 | router_conn.identity = "router1" 32 | router_conn.connect(addr) 33 | router_conn.on(:message) { |*parts| 34 | # 3. Message received in router identifies the sending dealer 35 | @router_received << parts 36 | } 37 | 38 | EM::add_timer(0.1) do 39 | # 1. Send message to the dealer 40 | router_conn.send_msg('dealer1', test_message) 41 | end 42 | 43 | EM::Timer.new(0.2) do 44 | @results[:specs_ran] = true 45 | end 46 | end 47 | end 48 | 49 | it "should run completely" do 50 | @results[:specs_ran].should be_true 51 | end 52 | 53 | it "should receive the message intact on the dealer" do 54 | @dealer_received.should_not be_empty 55 | @dealer_received.last.should be_a(ZMQ::Message) 56 | @dealer_received.last.copy_out_string.should == @test_message 57 | end 58 | 59 | it "the router should be echoed its original message with the dealer identity" do 60 | @router_received.size.should == 1 61 | parts = @router_received[0] 62 | parts[0].copy_out_string.should == "dealer1" 63 | parts[1].copy_out_string.should == "re:#{@test_message}" 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # em-zeromq # 2 | 3 | ## Current maintainer ## 4 | 5 | This gem is currently maintained by schmurfy, it will stay here for now 6 | but may be moved to my account later. 7 | 8 | ## Description: ## 9 | 10 | EventMachine support for ZeroMQ 11 | 12 | ## Usage: ## 13 | 14 | Supported on: 15 | 16 | - MRI 1.9+ 17 | - Rubinius 18 | - JRuby 19 | 20 | While the gem should work on Rubinius and JRuby I mainly use it with MRI 1.9+ so 21 | there may be some glitchs. 22 | 23 | Want to help out? Ask! 24 | 25 | ## Usage Warning ## 26 | 27 | To ensure your zeromq context won't be reclaimed by the garbage collector you need 28 | to keep a reference to it in scope, this is what you don't want to do (that's how the example used to be written): 29 | 30 | ```ruby 31 | EM.run do 32 | context = EM::ZeroMQ::Context.new(1) 33 | dealer_socket = context.socket(...) 34 | dealer_socket.connect(...) 35 | dealer_socket.send_msg('', "ping") 36 | end 37 | ``` 38 | 39 | If you do this everything will appear to work fine at first but as soon as the garbage collector 40 | is triggered your context will get destroyed and your application will hang. 41 | 42 | The same may be true for references to socket but I have no evidence of that. 43 | It should not be a major problem anyway since code like above is only written for examples/tests 44 | but I just pulled my hair trying to figure out why my test code was not working so now you 45 | have been warned ! 46 | 47 | ## Breaking changes: 0.2.x => 0.3.x ## 48 | 49 | Until the gem hit the 1.0 mark you should not use the "~>" operator in your Gemfile, 50 | lock yourself to the exact version you want. That said I will use the second digit to 51 | flag api changes but be aware that small changes can still occur between releases. 52 | 53 | 54 | ## Example ## 55 | ```ruby 56 | require 'em-zeromq' 57 | 58 | zmq = EM::ZeroMQ::Context.new(1) 59 | 60 | EM.run { 61 | push = zmq.socket(ZMQ::PUSH) 62 | push.connect("tcp://127.0.0.1:2091") 63 | 64 | pull = zmq.socket(ZMQ::PULL) 65 | pull.bind("tcp://127.0.0.1:2091") 66 | 67 | pull.on(:message) { |part| 68 | puts part.copy_out_string 69 | part.close 70 | } 71 | 72 | EM.add_periodic_timer(1) { 73 | push.send_msg("Hello") 74 | } 75 | } 76 | ``` 77 | Note that it's important to close message parts to avoid memory leaks. 78 | 79 | ## License: ## 80 | 81 | (The MIT License) 82 | 83 | Copyright (c) 2011 - 2012 84 | 85 | Permission is hereby granted, free of charge, to any person obtaining 86 | a copy of this software and associated documentation files (the 87 | 'Software'), to deal in the Software without restriction, including 88 | without limitation the rights to use, copy, modify, merge, publish, 89 | distribute, sublicense, and/or sell copies of the Software, and to 90 | permit persons to whom the Software is furnished to do so, subject to 91 | the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be 94 | included in all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 97 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 98 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 99 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 100 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 101 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 102 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 103 | -------------------------------------------------------------------------------- /lib/em-zeromq/socket.rb: -------------------------------------------------------------------------------- 1 | module EventMachine 2 | module ZeroMQ 3 | class Socket < EventMachine::Connection 4 | READABLES = [ ZMQ::SUB, ZMQ::PULL, ZMQ::ROUTER, ZMQ::DEALER, ZMQ::REP, ZMQ::REQ, ZMQ::PAIR ] 5 | WRITABLES = [ ZMQ::PUB, ZMQ::PUSH, ZMQ::ROUTER, ZMQ::DEALER, ZMQ::REP, ZMQ::REQ, ZMQ::PAIR ] 6 | 7 | include EventEmitter 8 | 9 | attr_reader :socket, :socket_type 10 | 11 | def initialize(socket, socket_type) 12 | @socket = socket 13 | @socket_type = socket_type 14 | 15 | self.notify_readable = true if READABLES.include?(socket_type) 16 | self.notify_writable = true if WRITABLES.include?(socket_type) 17 | end 18 | 19 | def self.map_sockopt(opt, name) 20 | define_method(name){ getsockopt(opt) } 21 | define_method("#{name}="){|val| setsockopt(opt, val) } 22 | end 23 | 24 | if defined?(ZMQ::HWM) 25 | map_sockopt(ZMQ::HWM, :hwm) 26 | else 27 | map_sockopt(ZMQ::SNDHWM, :sndhwm) 28 | map_sockopt(ZMQ::RCVHWM, :rcvhwm) 29 | def hwm=(val) 30 | self.sndhwm = val 31 | self.rcvhwm = val 32 | end 33 | end 34 | 35 | map_sockopt(ZMQ::SWAP, :swap) if defined?(ZMQ::SWAP) 36 | map_sockopt(ZMQ::IDENTITY, :identity) 37 | map_sockopt(ZMQ::AFFINITY, :affinity) 38 | map_sockopt(ZMQ::SNDBUF, :sndbuf) 39 | map_sockopt(ZMQ::RCVBUF, :rcvbuf) 40 | 41 | # pgm 42 | map_sockopt(ZMQ::RATE, :rate) 43 | map_sockopt(ZMQ::RECOVERY_IVL, :recovery_ivl) 44 | map_sockopt(ZMQ::MCAST_LOOP, :mcast_loop) if defined?(ZMQ::MCAST_LOOP) 45 | 46 | # User method 47 | def bind(address) 48 | @socket.bind(address) 49 | end 50 | 51 | def connect(address) 52 | @socket.connect(address) 53 | end 54 | 55 | def disconnect(address) 56 | @socket.disconnect(address) 57 | end 58 | 59 | def subscribe(what = '') 60 | raise "only valid on sub socket type (was #{@socket.name})" unless @socket.name == 'SUB' 61 | @socket.setsockopt(ZMQ::SUBSCRIBE, what) 62 | end 63 | 64 | def unsubscribe(what) 65 | raise "only valid on sub socket type (was #{@socket.name})" unless @socket.name == 'SUB' 66 | @socket.setsockopt(ZMQ::UNSUBSCRIBE, what) 67 | end 68 | 69 | # send a non blocking message 70 | # parts: if only one argument is given a signle part message is sent 71 | # if more than one arguments is given a multipart message is sent 72 | # 73 | # return: true is message was queued, false otherwise 74 | # 75 | def send_msg(*parts) 76 | parts = Array(parts[0]) if parts.size == 0 77 | sent = true 78 | 79 | # multipart 80 | parts[0...-1].each do |msg| 81 | ret = @socket.send_string(msg, ZMQ::NOBLOCK | ZMQ::SNDMORE) 82 | if ret < 0 83 | sent = false 84 | break 85 | end 86 | end 87 | 88 | if sent 89 | # all the previous parts were queued, send 90 | # the last one 91 | ret = @socket.send_string(parts[-1], ZMQ::NOBLOCK) 92 | if ret < 0 93 | sent = false 94 | end 95 | else 96 | # error while sending the previous parts 97 | # register the socket for writability 98 | self.notify_writable = true 99 | end 100 | 101 | EM::next_tick{ notify_readable() } 102 | 103 | sent 104 | end 105 | 106 | def getsockopt(opt) 107 | ret = [] 108 | rc = @socket.getsockopt(opt, ret) 109 | unless ZMQ::Util.resultcode_ok?(rc) 110 | raise ZMQOperationFailed, "getsockopt: #{ZMQ::Util.error_string}" 111 | end 112 | 113 | (ret.size == 1) ? ret[0] : ret 114 | end 115 | 116 | def setsockopt(opt, value) 117 | @socket.setsockopt(opt, value) 118 | end 119 | 120 | def unbind 121 | detach 122 | @socket.close 123 | end 124 | 125 | def notify_readable 126 | # Not sure if this is actually necessary. I suppose it prevents us 127 | # from having to to instantiate a ZMQ::Message unnecessarily. 128 | # I'm leaving this is because its in the docs, but it could probably 129 | # be taken out. 130 | return unless readable? 131 | 132 | while (message = get_message) 133 | emit(:message, *message) 134 | end 135 | end 136 | 137 | def notify_writable 138 | return unless writable? 139 | 140 | # one a writable event is successfully received the socket 141 | # should be accepting messages again so stop triggering 142 | # write events 143 | self.notify_writable = false 144 | 145 | emit(:writable) 146 | end 147 | def readable? 148 | (getsockopt(ZMQ::EVENTS) & ZMQ::POLLIN) == ZMQ::POLLIN 149 | end 150 | 151 | def writable? 152 | return true 153 | # ZMQ::EVENTS has issues in ZMQ HEAD, we'll ignore this till they're fixed 154 | # (getsockopt(ZMQ::EVENTS) & ZMQ::POLLOUT) == ZMQ::POLLOUT 155 | end 156 | 157 | private 158 | 159 | def get_message 160 | parts = [] 161 | rc = @socket.recvmsgs(parts, ZMQ::NOBLOCK) 162 | rc >= 0 ? parts : nil 163 | end 164 | end 165 | end 166 | end 167 | --------------------------------------------------------------------------------