You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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", "测试短信")

关键注意事项

  1. 绝对不要使用类变量:所有网关状态(比如队列、配置)都要用实例变量(@xxx),避免实例间共享数据。
  2. 只启动一次EventMachine:ruby-smpp基于EM运行,多个网关必须在同一个EM循环中管理,否则会导致阻塞或冲突。
  3. 线程安全:如果使用多线程服务器(比如Puma),确保队列操作是线程安全的(Ruby的Queue本身是线程安全的)。

内容的提问来源于stack exchange,提问作者Pledges

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:26:35