跳转到正文
YGJoon 技术博客

Flutter WebSocket 实现详解

发布于:
·1.2k 字 · 约 3 分钟·作者:
封装 WebSocketClient 单例:心跳保活、断线重连、StreamController 广播消息,做出可用的实时通信客户端
Flutter WebSocket 实现详解文章封面
目录

简介

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。Flutter 提供了 WebSocket 支持,可以实现实时通信功能。

基本使用

创建连接

WebSocket? socket;

Future<void> connect() async {
  try {
    socket = await WebSocket.connect('ws://example.com/ws');
    print('Connected to WebSocket server');
    
    // 监听消息
    socket?.listen(
      (data) {
        print('Received: $data');
      },
      onError: (error) {
        print('Error: $error');
      },
      onDone: () {
        print('Connection closed');
      },
    );
  } catch (e) {
    print('Error connecting to WebSocket server: $e');
  }
}

发送消息

void sendMessage(String message) {
  if (socket != null && socket!.readyState == WebSocket.open) {
    socket!.add(message);
  } else {
    print('WebSocket is not connected');
  }
}

完整实现

class WebSocketClient {
  static final WebSocketClient _instance = WebSocketClient._internal();
  factory WebSocketClient() => _instance;
  
  WebSocket? _socket;
  bool _isConnected = false;
  final _messageController = StreamController<dynamic>.broadcast();
  
  WebSocketClient._internal();
  
  bool get isConnected => _isConnected;
  Stream get messageStream => _messageController.stream;
  
  Future<void> connect(String url) async {
    if (_isConnected) return;
    
    try {
      _socket = await WebSocket.connect(url);
      _isConnected = true;
      
      _socket!.listen(
        (data) {
          _messageController.add(data);
        },
        onError: (error) {
          print('WebSocket error: $error');
          _handleError(error);
        },
        onDone: () {
          print('WebSocket connection closed');
          _handleDisconnect();
        },
        cancelOnError: true,
      );
      
      print('Connected to WebSocket server');
    } catch (e) {
      print('Error connecting to WebSocket server: $e');
      _handleError(e);
    }
  }
  
  void send(dynamic data) {
    if (!_isConnected) {
      print('WebSocket is not connected');
      return;
    }
    
    try {
      _socket!.add(data);
    } catch (e) {
      print('Error sending message: $e');
      _handleError(e);
    }
  }
  
  Future<void> disconnect() async {
    if (!_isConnected) return;
    
    try {
      await _socket!.close();
      _handleDisconnect();
    } catch (e) {
      print('Error closing WebSocket connection: $e');
    }
  }
  
  void _handleDisconnect() {
    _isConnected = false;
    _socket = null;
  }
  
  void _handleError(dynamic error) {
    _isConnected = false;
    _socket = null;
  }
  
  void dispose() {
    disconnect();
    _messageController.close();
  }
}

// 使用示例
class ChatPage extends StatefulWidget {
  @override
  _ChatPageState createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  final _client = WebSocketClient();
  final _messageController = TextEditingController();
  final List<String> _messages = [];
  
  @override
  void initState() {
    super.initState();
    _connectToServer();
    
    // 监听消息
    _client.messageStream.listen((message) {
      setState(() {
        _messages.add('Received: $message');
      });
    });
  }
  
  Future<void> _connectToServer() async {
    await _client.connect('ws://example.com/ws');
  }
  
  void _sendMessage() {
    if (_messageController.text.isNotEmpty) {
      _client.send(_messageController.text);
      setState(() {
        _messages.add('Sent: ${_messageController.text}');
      });
      _messageController.clear();
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('WebSocket Chat'),
        actions: [
          IconButton(
            icon: Icon(
              _client.isConnected ? Icons.cloud_done : Icons.cloud_off,
            ),
            onPressed: _client.isConnected
                ? _client.disconnect
                : _connectToServer,
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_messages[index]),
                );
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _messageController,
                    decoration: InputDecoration(
                      hintText: 'Enter message',
                    ),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.send),
                  onPressed: _sendMessage,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
  
  @override
  void dispose() {
    _client.dispose();
    _messageController.dispose();
    super.dispose();
  }
}

心跳机制

class HeartbeatWebSocket {
  final Duration pingInterval;
  final WebSocketClient client;
  Timer? _heartbeatTimer;
  
  HeartbeatWebSocket({
    this.pingInterval = const Duration(seconds: 30),
    required this.client,
  });
  
  void startHeartbeat() {
    _heartbeatTimer?.cancel();
    _heartbeatTimer = Timer.periodic(pingInterval, (timer) {
      if (client.isConnected) {
        client.send('ping');
      } else {
        stopHeartbeat();
      }
    });
  }
  
  void stopHeartbeat() {
    _heartbeatTimer?.cancel();
    _heartbeatTimer = null;
  }
  
  void dispose() {
    stopHeartbeat();
  }
}

最佳实践

  1. 心跳 30 秒一次:Timer.periodic(Duration(seconds: 30), (_) => sink.add('ping')),服务端 60~90 秒没收到心跳就判死——这是常见值;别设 5 秒(费电),也别不设(NAT 会把闲置连接掐掉)
  2. 断线重连用指数退避:1s → 2s → 4s → 8s……上限 60s;别写 while(true) 立刻重连——服务端抖一下,所有客户端同时砸重连就是自杀式 DDoS
  3. App 切后台断开、回前台重连:iOS 后台长连接被系统杀掉是预期行为,别试图保活;用 WidgetsBindingObserver 监听 AppLifecycleState
  4. 维护连接状态机:connecting / connected / disconnected 三态,UI 按状态显示(“重连中…”);别让用户对着一个假在线的界面发消息

注意事项

  1. 断线期间的消息先入本地队列,重连成功后按序补发——直接丢掉,用户会以为消息发出去了(IM 场景这是 P0 级 bug)
  2. stream 的 subscription 记得 cancel:页面 dispose 时 subscription.cancel() + sink.close(),否则后台还在收消息、白白耗电
  3. 别在 WebSocket 上跑”请求-响应”:需要可靠应答的业务(如下单)走 HTTP;WebSocket 只做服务端→客户端的推送(实战案例:没强实时需求就别碰,先轮询)
  4. 消息先判格式再解析:jsonDecode 包 try-catch——服务端推个畸形包,不能把整个 App 炸掉

实战案例

WebSocket(长连接、实时推送)这个场景我还没在项目里遇到过——我的 App(电商、货运、音乐)全是”请求-响应”模式,轮询+下拉刷新就够了。货运项目里”抢单”状态更新,我用的是短轮询(15 秒一次),虽然土,但实现简单、服务端无改造成本。WebSocket 真正的坑在断线重连、心跳保活、后台存活,App 切后台后系统会杀连接,iOS 尤其严。我的判断是:日活没到一定量级、没有强实时需求(聊天、协同编辑、实时竞价),别碰 WebSocket,轮询先顶着。真要做,参考 Flutter 官方文档的 WebSocket 章节(web_socket_channel 包)。

总结

WebSocket 为 Flutter 应用提供了实时通信能力。通过合理实现连接管理、心跳机制和错误处理,可以构建稳定可靠的实时通信功能。


分享这篇文章:


相关文章

上一篇
Flutter Container 组件详解
下一篇
Flutter SQLite 数据库详解