基于ruby-smpp的Rails应用多SMSC绑定异常问题求助
解决Ruby-SMPP多SMSC连接的两个核心问题
首先可以明确说:完全不需要用两个独立的Rails应用,问题出在你对ruby-smpp网关实例的隔离性处理上。我来拆解你的两个问题,给出具体的修复方案:
问题1:一个连接断开导致所有连接丢失
这个问题的核心原因几乎可以肯定是:你的SampleGateway使用了类级别的事件回调(比如self.unbound),或者共享了全局的EventMachine(EM)停止逻辑。当其中一个网关触发unbound事件时,错误地调用了EM.stop,直接终止了整个事件循环,导致所有SMSC连接都被断开。
修复方案:改用实例级别的事件回调
修改你的SampleGateway,确保所有事件处理都是实例专属的,并且断开时只重连当前实例,不影响其他网关:
class SampleGateway < RubySmpp::BaseGateway def initialize super @config = nil setup_instance_callbacks end def start(config) @config = config # 确保在现有EM上下文启动连接,不要每个实例单独调用EM.run EM.next_tick { bind_transceiver(config) } end private def setup_instance_callbacks # 实例专属的断开回调:只处理当前网关的重连 self.on_unbound do Rails.logger.warn("SMSC #{@config[:system_id]} 断开连接,10秒后尝试重连") EM.add_timer(10) do Rails.logger.info("开始重连 #{@config[:system_id]}") bind_transceiver(@config) end end # 实例专属的绑定成功回调 self.on_bound do Rails.logger.info("成功绑定到SMSC #{@config[:system_id]}") end end end
同时,在Rails初始化时只启动一次EventMachine,然后在同一个EM循环中启动两个网关:
# config/initializers/smpp_gateways.rb config_1 = { host: "isp1.example.com", port: 2775, system_id: "your_isp1_account", # 其他配置项... } config_2 = { host: "isp2.example.com", port: 2775, system_id: "your_isp2_account", # 其他配置项... } # 初始化两个独立的网关实例 $gw_1 = SampleGateway.new $gw_2 = SampleGateway.new # Rails启动完成后,在EM上下文启动网关 Rails.application.config.after_initialize do EM.next_tick do $gw_1.start(config_1) $gw_2.start(config_2) end end
问题2:短信发送数量翻倍
这个问题的根源是消息发送逻辑没有隔离:要么两个网关共享了同一个发送队列,要么你调用了全局的发送方法,导致两个实例都处理了同一条消息。
修复方案:给每个网关分配独立的发送通道
方式1:使用实例专属队列
修改SampleGateway,给每个实例添加独立的消息队列,发送时明确指定目标网关:
class SampleGateway < RubySmpp::BaseGateway attr_reader :send_queue def initialize super @send_queue = Queue.new @config = nil setup_instance_callbacks start_queue_processor end # ... 保留之前的start和回调逻辑 ... private def start_queue_processor # 启动独立线程处理当前实例的队列 Thread.new do loop do msg = @send_queue.pop begin submit_sm( source_addr: msg[:from], destination_addr: msg[:to], short_message: msg[:body] ) Rails.logger.info("通过 #{@config[:system_id]} 发送短信到 #{msg[:to]}") rescue => e Rails.logger.error("发送失败:#{e.message},5秒后重试") @send_queue << msg sleep(5) end end end end end
发送短信时,直接把消息推入对应网关的队列:
# 发送到ISP1 $gw_1.send_queue << { from: "10086", to: "138xxxxxxx", body: "测试短信" } # 发送到ISP2 $gw_2.send_queue << { from: "10010", to: "139xxxxxxx", body: "测试短信" }
方式2:结合ActiveJob隔离任务(更符合Rails最佳实践)
创建两个独立的Job类,分别对应两个网关:
# app/jobs/send_via_isp1_job.rb class SendViaIsp1Job < ApplicationJob queue_as :isp1_sms def perform(from, to, body) $gw_1.send_queue << { from: from, to: to, body: body } end end # app/jobs/send_via_isp2_job.rb class SendViaIsp2Job < ApplicationJob queue_as :isp2_sms def perform(from, to, body) $gw_2.send_queue << { from: from, to: to, body: body } end end
发送时调用对应Job:
SendViaIsp1Job.perform_later("10086", "138xxxxxxx", "测试短信") SendViaIsp2Job.perform_later("10010", "139xxxxxxx", "测试短信")
关键注意事项
- 绝对不要使用类变量:所有网关状态(比如队列、配置)都要用实例变量(
@xxx),避免实例间共享数据。 - 只启动一次EventMachine:
ruby-smpp基于EM运行,多个网关必须在同一个EM循环中管理,否则会导致阻塞或冲突。 - 线程安全:如果使用多线程服务器(比如Puma),确保队列操作是线程安全的(Ruby的
Queue本身是线程安全的)。
内容的提问来源于stack exchange,提问作者Pledges
相关产品推荐
相关产品推荐

