Flutter WebSocket 文本回显应用完整实践

Flutter WebSocket 文本回显应用完整实践

普通 HTTP 请求通常以一次请求和一次响应为单位。WebSocket 则在连接建立后保持双向通信,客户端和服务器都能继续发送消息,无需靠定时轮询取得新内容。本篇按 Flutter 官方文档的四个步骤,建立一个最小的文本回显应用:输入一段文字,将它送到测试服务器,再把服务器发回的消息显示在界面上。

原文:Google / Flutter & Dart team 维护的 Communicate with WebSockets。本篇为中文翻译整理,加入版本说明和静态审查注释。核验日期:2026-10-08。原页说明对应 Flutter 3.47,最后更新于 2026-09-29;不是原文发表日期。

Flutter 输入框经 WebSocket sink 发送文本,测试服务器回显后经 stream 和 StreamBuilder 更新 Text,页面销毁时关闭连接。
回显消息的完整流向。未完纪自绘技术示意图,不是应用运行截图。

准备依赖与测试边界

web_socket_channel 提供跨平台的 WebSocketChannel 接口,把接收消息的流与发送消息的入口放在同一个通道对象中。核验时包页面为 3.0.3。当前官方完整示例采用 package:material_ui/material_ui.dart;material_ui 1.5.0 的说明确认它是从 Flutter 框架拆分出来的官方 Material UI 库。旧文章中的 package:flutter/material.dart 属于另一版本上下文,不应在不核对 SDK 的情况下混用。

已有 Flutter 工程需要在依赖中声明这两个包,例如按上述核验版本使用以下片段。它是本篇补充的依赖说明,不是声称已执行过依赖解析。应用工程仍需保留自己的 SDK 约束和其他配置,并锁定实际解析版本。

dependencies:
  flutter:
    sdk: flutter
  material_ui: ^1.5.0
  web_socket_channel: ^3.0.3

官方示例连接 wss://echo.websocket.org。这是第三方公共测试服务,发出的文字会离开设备并到达该服务;使用无敏感信息的测试文字。wss 表示通过 TLS 建立 WebSocket 连接,但不等于服务器为你的业务提供身份认证、存储或隐私保障。本次只读取文档与代码,没有发起 WebSocket 握手或向服务器发送消息。

第一步:建立通道

final WebSocketChannel channel = WebSocketChannel.connect(
  Uri.parse('wss://echo.websocket.org'),
);

connect 根据 URI 创建通道,之后通过 channel.stream 接收数据,通过 channel.sink 发送数据。返回通道对象不能证明远端握手已经成功。包的 README 还演示了 await channel.ready;需要明确连接状态的应用应在自己的异步生命周期逻辑中等待它并处理失败。

第二步:监听服务器消息

StreamBuilder(
  stream: channel.stream,
  builder: (context, snapshot) {
    return Text(snapshot.hasData ? '${snapshot.data}' : '');
  },
),

Stream 是 Dart 异步模型的一部分,可以随着时间产生多次事件;Future 通常对应一次异步结果。StreamBuilder 订阅传入的流,每收到事件便使用新的快照调用 builder 重建对应界面。这里有数据时显示它,没有数据时显示空字符串。该写法只保留当前快照的内容,并没有实现消息历史列表。

这也是示例的一个边界:它没有为 snapshot.hasError 或连接关闭状态显示提示。网络断开时的空白界面不能被解释为“服务器成功收到消息”。需要用户反馈的产品应分别显示连接中、失败和已断开状态。

第三步:把文本加入发送入口

channel.sink.add('Hello!');

sink 提供 StreamSink 风格的写入接口。调用 add 后,通道负责把消息发送出去;这不是业务层送达确认。完整例子从 TextEditingController 取出输入框中的文字,仅在字符串非空时发送。空格仍然是非空字符串,源例没有自动清理空格,也没有消息长度限制。

第四步:释放页面资源

@override
void dispose() {
  _channel.sink.close();
  _controller.dispose();
  super.dispose();
}

页面不再需要连接时,应关闭 sink,同时释放文本控制器。源例把两者放在状态对象的 dispose 中,避免在页面离开后继续持有这些资源。不要在每次 build 时重新创建连接;重建界面不应意味着反复建立新的网络会话。

完整应用

以下保留官方示例的应用结构和行为,仅合并少量排版、删除解释性尾逗号注释。它包含入口、MaterialApp、StatefulWidget、输入框、接收流、发送按钮和资源释放。没有添加未经测试的重连逻辑。

import 'package:material_ui/material_ui.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    const title = 'WebSocket Demo';
    return const MaterialApp(title: title, home: MyHomePage(title: title));
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final TextEditingController _controller = TextEditingController();
  final WebSocketChannel _channel = WebSocketChannel.connect(
    Uri.parse('wss://echo.websocket.org'),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(labelText: 'Send a message'),
              ),
            ),
            const SizedBox(height: 24),
            StreamBuilder(
              stream: _channel.stream,
              builder: (context, snapshot) {
                return Text(snapshot.hasData ? '${snapshot.data}' : '');
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _sendMessage,
        tooltip: 'Send message',
        child: const Icon(Icons.send),
      ),
    );
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      _channel.sink.add(_controller.text);
    }
  }

  @override
  void dispose() {
    _channel.sink.close();
    _controller.dispose();
    super.dispose();
  }
}

从演示到实际应用需要补上的内容

源例适合观察一次文本发送和回显,不是完整聊天后端。实际业务需要按协议增加身份认证、消息格式验证、消息大小上限、服务器限流、错误和关闭提示、受控的重连退避,以及是否重发消息的明确规则。自动重发会造成重复业务动作,不能仅凭连接恢复就重发所有历史消息。

静态审查未在这段客户端代码中发现硬编码秘密、动态执行代码或 HTML 注入入口;收到的消息由 Text 作为文本显示。这个结论只覆盖所示代码,不证明依赖、服务器或应用整体没有漏洞。公共服务器可以返回非预期内容,生产应用仍须按业务类型约束输入与输出。

在你自己的测试环境中,可按“等待连接成功—发送普通测试文字—核对回显—主动离开页面—断网观察错误提示”的顺序验收。该顺序是建议的验证方法,本篇没有实际运行 Flutter、安装依赖、验证目标平台权限或测量网络表现。

来源与许可

正文原作者归属 Google / Flutter & Dart team,原页未列个人署名。Flutter 文档按 CC BY 4.0 提供,代码示例按 BSD 3-Clause 提供。本文已翻译并补充版本、安全及验证边界说明;自绘图归未完纪。

代码示例 BSD 3-Clause 许可声明

保留自 Flutter 文档仓库 LICENSE 的代码版权与许可声明。在线页面 footer 对文本列明 CC BY 4.0,但随稿保存的 Flutter website 仓库 LICENSE 文本将仓库内容列为 CC BY 3.0;这两处许可信息不一致,本文保留差异并按在线页面标示正文许可,代码示例为 BSD 3-Clause。

Copyright 2012, the project authors. All rights reserved. Redistribution and use
in source and binary forms, with or without modification, are permitted provided
that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.
    * Neither the name of Google Inc. nor the names of its
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容