本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
连接生产环境
托管的 MQTT 服务,例如 AWS IoT Core、HiveMQ Cloud 或 EMQX Cloud,要求凭据和 TLS,往往还要求客户端证书。 本指南从部署环境中读取设置,为这样的服务配置 Broker。
凭据与 TLS
/// Where the server is and who the service is, as the deployment provides them.
struct Deployment {
url: String,
client_id: String,
username: String,
password: String,
ca: Vec<u8>,
cert: Vec<u8>,
key: Vec<u8>,
}
impl Deployment {
fn from_env() -> Self {
let read = |name: &str| env::var(name).unwrap_or_default();
let file = |name: &str| fs::read(read(name)).unwrap_or_default();
Self {
url: read("MQTT_URL"),
client_id: read("MQTT_CLIENT_ID"),
username: read("MQTT_USERNAME"),
password: read("MQTT_PASSWORD"),
ca: file("MQTT_CA"),
cert: file("MQTT_CERT"),
key: file("MQTT_KEY"),
}
}
}
const LARGEST_PACKET: NonZeroU32 = NonZeroU32::new(256 * 1024).unwrap();
const IN_FLIGHT: NonZeroU16 = NonZeroU16::new(200).unwrap();
fn broker(deployment: Deployment) -> MqttBroker {
MqttBroker::new(deployment.url, deployment.client_id)
.credentials(deployment.username, deployment.password)
.tls_ca(deployment.ca)
.tls_client_auth(deployment.cert, deployment.key)
.keep_alive(Duration::from_secs(30))
.max_packet_size(LARGEST_PACKET)
.receive_maximum(IN_FLIGHT)
}
部署环境通过环境变量提供每一项设置,通过文件提供证书。
credentials 设置 CONNECT 报文中的用户名和密码。
URL 中的登录信息(例如 mqtt://user:p%40ss@host)同样有效,并按百分号编码解码;credentials 优先于它。
它们不会进入生成的 AsyncAPI 文档,文档只报告服务连接的主机和端口。
mqtts:// 选择 TLS,URL 没有指定端口时使用 8883。
tls_ca 是 PEM 格式的证书颁发机构,客户端用它验证服务器。
客户端只信任这个颁发机构,不使用任何其他证书库,所以没有 tls_ca 的 mqtts:// URL 会让启动失败,错误信息会指出 tls_ca。
tls_client_auth 加入客户端的证书和密钥,用于按证书认证客户端的服务。
每个进程一个客户端 ID
客户端 ID 是 MqttBroker::new 的第二个参数,服务器按它索引会话。
使用同一个 ID 的第二个连接会接管会话,并关闭第一个连接。
因此,服务的两个实例绝不共用一个 ID:这里每个实例从 MQTT_CLIENT_ID 取得自己的 ID,例如 pod 的名字。
需要分摊工作的实例改为加入共享组。
限制
keep_alive是客户端在空闲连接上证明自己存活的频率;连接静默达到该间隔的一倍半时,服务器关闭连接。rumqttc客户端的最小值是 5 秒,默认值是 60 秒。max_packet_size是服务接受的最大报文;服务器不会向它发送更大的报文。Broker 的默认值是 1 MiB,高于客户端自己的默认值 10 KiB。receive_maximum限制传输中的QoS1 和 2 投递数量(投递保证)。
服务
- 宏
- 手写
#[subscriber(MqttFilter::new("devices/+/telemetry").qos(Qos::AtLeastOnce).shared("ingest"))]
async fn record(telemetry: &Telemetry) -> HandlerOutcome {
tracing::info!(telemetry.device, telemetry.temperature, "reading recorded");
HandlerOutcome::ack()
}
fn service(broker: MqttBroker) -> RustStream {
RustStream::new(AppInfo::new("telemetry", "0.1.0")).with_broker(broker, |b| {
b.include(record)
.out_retry(Publish::default())
.to("devices/retry/telemetry");
})
}
#[ruststream::app]
fn app() -> impl App<State = ()> {
service(broker(Deployment::from_env()))
}
struct Record;
impl Handle<Telemetry> for Record {
fn handle(
&self,
telemetry: &Telemetry,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
tracing::info!(telemetry.device, telemetry.temperature, "reading recorded");
ready(Ok(()))
}
}
fn service(broker: MqttBroker) -> RustStream {
RustStream::new(AppInfo::new("telemetry", "0.1.0")).with_broker(broker, |b| {
let fleet = MqttFilter::new("devices/+/telemetry")
.qos(Qos::AtLeastOnce)
.shared("ingest");
b.include(
subscriber("devices/+/telemetry", Record)
.map_source(|_| fleet)
.build(),
)
.out_retry(Publish::default())
.to("devices/retry/telemetry");
})
}
fn app() -> impl App<State = ()> {
service(broker(Deployment::from_env()))
}
fn main() -> ExitCode {
run_main(app)
}
服务启动后,Broker 才建立连接。 连接丢失后会重连,退避时间从 100 ms 开始,每次加倍,最长 5 秒;服务器报告会话已不存在时,Broker 重新订阅。 重试也无法消除的拒绝则会结束连接任务:凭据错误、服务器不接受的客户端 ID,或者证书无法完成的 TLS 握手。 此后每个订阅都返回这个错误。
测试配置
测试在 TestApp::start 下启动生产环境的 service,配上生产环境的 broker 和测试设置。
进程内服务器读取 Broker 的配置,像客户端那样拒绝不合规的配置,但不会去连接服务器:
- keep-alive 小于 5 秒会让启动失败,错误消息中带有客户端的最小值;
- 没有
tls_ca的mqtts://URL 会让启动失败,错误信息会指出tls_ca。
凭据和 TLS 握手只能由真实的服务器检查。 安全页面的线上测试在要求这两者的 Mosquitto 监听端口上运行它们。