Rails 5.1.5环境下IoT设备TCP请求转Puma处理报错咨询
嘿,这个问题我之前帮朋友踩过坑!核心原因很直白:Puma是纯HTTP服务器,它只能解析符合HTTP协议格式的请求,而IoT设备发的是原始TCP数据流,这俩完全不在一个频道上,直接转发肯定会报HTTP解析错误。给你几个实用的解决方案,按推荐程度排序:
方案1:编写独立的TCP服务进程(最推荐)
我最推荐这个思路——单独搞个轻量的TCP服务,专门对接IoT设备的连接和数据接收,然后把数据转存到Rails的数据库里。这样既不干扰Rails主应用的运行,又能高效处理TCP长/短连接。
给你写个简单的Ruby示例(用原生socket库,直接复用Rails的数据库模型):
# tcp_server.rb require 'socket' require 'active_record' # 加载Rails生产环境的数据库配置,直接操作模型 ActiveRecord::Base.establish_connection(YAML.load_file('config/database.yml')['production']) require './app/models/iot_data.rb' # 监听本地1234端口,接收IoT设备连接 server = TCPServer.new('0.0.0.0', 1234) loop do client = server.accept # 读取设备发送的数据,按需调整读取大小(比如1024字节) raw_data = client.readpartial(1024) # 假设你的IoTData模型有data字段,直接存原始数据 IoTData.create(data: raw_data.strip) client.close end
然后你可以用systemd或者foreman把这个TCP进程和Puma一起管理,Nginx的stream配置直接把TCP流量转发到这个服务的端口就行:
stream { server { listen 8888; # IoT设备要连接的端口 proxy_pass 127.0.0.1:1234; # 转发到我们的TCP服务 } }
方案2:把TCP请求包装成HTTP(适合简单数据场景)
如果你的IoT设备发的是简单文本数据,不想额外维护进程,可以试试在Nginx里把原始TCP数据转换成HTTP请求,再转发给Puma。不过这个需要Nginx编译时开启ngx_stream_js_module模块。
举个Nginx配置示例:
stream { # 导入自定义JS脚本,用来转换TCP数据为HTTP js_import tcp_to_http.js; server { listen 8888; # 提前读取TCP数据,转换成HTTP格式 js_preread tcp_to_http.convert; # 转发给Puma的端口 proxy_pass 127.0.0.1:3000; } }
对应的tcp_to_http.js脚本:
function convert(s) { let rawData = s.buffer; // 把原始数据包装成POST请求的格式 let httpRequest = `POST /iot/save_data HTTP/1.1\r\nHost: your-domain.com\r\nContent-Length: ${rawData.length}\r\n\r\n${rawData}`; s.send(httpRequest); }
然后在Rails里写个对应的控制器动作接收:
# app/controllers/iot_controller.rb class IotController < ApplicationController # IoT设备不需要CSRF验证,跳过这个检查 skip_before_action :verify_authenticity_token def save_data # 读取HTTP请求体里的原始数据 raw_data = request.body.read IoTData.create(data: raw_data) # 返回200状态码给设备 head :ok end end
别忘了在routes.rb里加路由:
post '/iot/save_data', to: 'iot#save_data'
这个方案的缺点是如果TCP数据是二进制或者需要长连接,Nginx的JS模块处理起来会很麻烦,不如方案1灵活。
方案3:用Action Cable(适合双向通信场景)
如果你的IoT设备需要和服务器双向通信(比如服务器给设备发指令),可以试试Rails的Action Cable——它基于WebSocket,而WebSocket本身是建立在TCP之上的,Puma可以直接处理。不过这个需要修改IoT设备的通信协议,改成WebSocket。
先在Rails里创建一个Channel:
# app/channels/iot_channel.rb class IotChannel < ApplicationCable::Channel def subscribed # 给每个设备开个专属的数据流 stream_from "iot_device_#{params[:device_id]}" end def receive(data) # 接收设备发过来的数据,存到数据库 IoTData.create(device_id: params[:device_id], data: data['payload']) # 如果需要给设备发响应,直接用transmit就行 transmit status: 'success' end end
然后设备端需要实现WebSocket客户端,连接到ws://your-domain.com/cable,发送符合Action Cable格式的数据。这个方案适合有双向交互需求的场景,但得改设备的通信逻辑。
总结一下:如果只是单纯接收IoT数据,方案1是最稳定、最灵活的;如果数据格式简单且不想额外维护进程,可以试试方案2;如果需要双向通信,方案3更合适。
内容的提问来源于stack exchange,提问作者darshi sheth

