5.4 WebSocket 与实时通信

实时功能(聊天、推送通知、行情更新)是现代 App 的核心需求。Flutter 支持原生 WebSocket 和 Socket.IO 两种实时通信方案。


一、原生 WebSocket(dart:io)

1.1 基本连接

dart 复制代码
import 'dart:io';
import 'dart:convert';

class WebSocketService {
  WebSocket? _socket;
  final _messageController = StreamController<Map<String, dynamic>>.broadcast();

  Stream<Map<String, dynamic>> get messages => _messageController.stream;

  Future<void> connect(String url) async {
    try {
      _socket = await WebSocket.connect(
        url,
        headers: {
          'Authorization': 'Bearer ${AuthService.token}',
        },
      );

      _socket!.listen(
        (data) {
          final json = jsonDecode(data as String);
          _messageController.add(json);
        },
        onDone: () {
          debugPrint('WebSocket closed: ${_socket?.closeCode}');
          _reconnect(url);
        },
        onError: (error) {
          debugPrint('WebSocket error: $error');
          _reconnect(url);
        },
      );

      debugPrint('WebSocket connected to $url');
    } catch (e) {
      debugPrint('WebSocket connect failed: $e');
      await Future.delayed(const Duration(seconds: 3));
      _reconnect(url);
    }
  }

  void send(Map<String, dynamic> data) {
    if (_socket?.readyState == WebSocket.open) {
      _socket!.add(jsonEncode(data));
    }
  }

  Future<void> _reconnect(String url) async {
    await Future.delayed(const Duration(seconds: 3));
    await connect(url);
  }

  Future<void> disconnect() async {
    await _socket?.close();
    _socket = null;
  }

  void dispose() {
    disconnect();
    _messageController.close();
  }
}

1.2 Flutter 端使用(web_socket_channel)

web_socket_channel 兼容所有平台(包括 Web):

yaml 复制代码
dependencies:
  web_socket_channel: ^2.4.5
dart 复制代码
import 'package:web_socket_channel/web_socket_channel.dart';

class ChatService {
  WebSocketChannel? _channel;
  final _messages = StreamController<ChatMessage>.broadcast();

  Stream<ChatMessage> get messages => _messages.stream;
  bool get isConnected => _channel != null;

  void connect() {
    _channel = WebSocketChannel.connect(
      Uri.parse('wss://chat.example.com/ws'),
    );

    _channel!.stream.listen(
      (data) {
        final json = jsonDecode(data as String);
        _messages.add(ChatMessage.fromJson(json));
      },
      onDone: () => _handleDisconnect(),
      onError: (e) => _handleDisconnect(),
    );
  }

  void sendMessage(String content, String roomId) {
    _channel?.sink.add(jsonEncode({
      'type': 'message',
      'content': content,
      'roomId': roomId,
      'timestamp': DateTime.now().toIso8601String(),
    }));
  }

  void _handleDisconnect() {
    _channel = null;
    // 自动重连
    Future.delayed(const Duration(seconds: 3), connect);
  }

  void dispose() {
    _channel?.sink.close();
    _messages.close();
  }
}

二、Socket.IO

yaml 复制代码
dependencies:
  socket_io_client: ^2.0.3+1
dart 复制代码
import 'package:socket_io_client/socket_io_client.dart' as IO;

class SocketIOService {
  late IO.Socket _socket;
  final _messageController = StreamController<ChatMessage>.broadcast();

  Stream<ChatMessage> get messages => _messageController.stream;

  void connect() {
    _socket = IO.io(
      'https://chat.example.com',
      IO.OptionBuilder()
          .setTransports(['websocket']) // 仅 WebSocket(跳过轮询)
          .enableAutoConnect()
          .enableReconnection()
          .setAuth({'token': AuthService.token})
          .setReconnectionDelay(3000)
          .setReconnectionAttempts(10)
          .build(),
    );

    _socket.onConnect((_) {
      debugPrint('Socket.IO connected: ${_socket.id}');
    });

    _socket.onDisconnect((_) {
      debugPrint('Socket.IO disconnected');
    });

    _socket.onConnectError((error) {
      debugPrint('Socket.IO connect error: $error');
    });

    // 监听事件
    _socket.on('new_message', (data) {
      _messageController.add(ChatMessage.fromJson(data));
    });

    _socket.on('user_joined', (data) {
      debugPrint('用户 ${data['username']} 加入了聊天室');
    });

    _socket.on('typing', (data) {
      // 对方正在输入...
    });
  }

  // 加入房间
  void joinRoom(String roomId) {
    _socket.emit('join_room', {'roomId': roomId});
  }

  // 发送消息
  void sendMessage(String content, String roomId) {
    _socket.emit('send_message', {
      'content': content,
      'roomId': roomId,
    });
  }

  // 正在输入
  void sendTyping(String roomId) {
    _socket.emit('typing', {'roomId': roomId});
  }

  // ACK 回调
  void sendWithAck(Map<String, dynamic> data) {
    _socket.emitWithAck('send_message', data, ack: (response) {
      debugPrint('Server ACK: $response');
    });
  }

  void disconnect() {
    _socket.disconnect();
    _socket.dispose();
  }

  void dispose() {
    disconnect();
    _messageController.close();
  }
}

三、聊天 UI 实践

dart 复制代码
class ChatPage extends StatefulWidget {
  final String roomId;
  const ChatPage({super.key, required this.roomId});

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  final _chatService = ChatService();
  final _inputController = TextEditingController();
  final _scrollController = ScrollController();
  final _messages = <ChatMessage>[];
  StreamSubscription? _subscription;

  @override
  void initState() {
    super.initState();
    _chatService.connect();

    _subscription = _chatService.messages.listen((msg) {
      setState(() => _messages.add(msg));
      // 自动滚动到底部
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (_scrollController.hasClients) {
          _scrollController.animateTo(
            _scrollController.position.maxScrollExtent,
            duration: const Duration(milliseconds: 200),
            curve: Curves.easeOut,
          );
        }
      });
    });
  }

  void _send() {
    final text = _inputController.text.trim();
    if (text.isEmpty) return;

    _chatService.sendMessage(text, widget.roomId);
    _inputController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('聊天')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              controller: _scrollController,
              itemCount: _messages.length,
              padding: const EdgeInsets.all(12),
              itemBuilder: (_, index) {
                final msg = _messages[index];
                final isMe = msg.senderId == currentUser.id;
                return MessageBubble(message: msg, isMe: isMe);
              },
            ),
          ),
          // 输入栏
          SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Row(children: [
                Expanded(
                  child: TextField(
                    controller: _inputController,
                    decoration: const InputDecoration(
                      hintText: '输入消息...',
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                    ),
                    textInputAction: TextInputAction.send,
                    onSubmitted: (_) => _send(),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton.filled(
                  onPressed: _send,
                  icon: const Icon(Icons.send),
                ),
              ]),
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _subscription?.cancel();
    _chatService.dispose();
    _inputController.dispose();
    _scrollController.dispose();
    super.dispose();
  }
}

四、心跳与断线重连

dart 复制代码
class RobustWebSocket {
  WebSocketChannel? _channel;
  Timer? _heartbeatTimer;
  Timer? _reconnectTimer;
  int _reconnectAttempts = 0;
  static const _maxReconnectAttempts = 10;

  void connect(String url) {
    _channel = WebSocketChannel.connect(Uri.parse(url));

    _channel!.stream.listen(
      _onMessage,
      onDone: () => _scheduleReconnect(url),
      onError: (_) => _scheduleReconnect(url),
    );

    _startHeartbeat();
    _reconnectAttempts = 0;
  }

  void _startHeartbeat() {
    _heartbeatTimer?.cancel();
    _heartbeatTimer = Timer.periodic(
      const Duration(seconds: 30),
      (_) => _channel?.sink.add(jsonEncode({'type': 'ping'})),
    );
  }

  void _scheduleReconnect(String url) {
    if (_reconnectAttempts >= _maxReconnectAttempts) return;
    _heartbeatTimer?.cancel();

    // 指数退避:3s, 6s, 12s, 24s...
    final delay = Duration(seconds: 3 * (1 << _reconnectAttempts));
    _reconnectTimer = Timer(delay, () {
      _reconnectAttempts++;
      connect(url);
    });
  }

  void dispose() {
    _heartbeatTimer?.cancel();
    _reconnectTimer?.cancel();
    _channel?.sink.close();
  }
}

小结

方案 特点 适用场景
web_socket_channel 轻量、跨平台、原生协议 简单实时通信
Socket.IO 自动重连、房间机制、ACK 聊天室、多人协作
心跳 + 指数退避 保活连接、健壮重连 生产环境必备

👉 下一节:5.5 图片与资源管理

相关推荐
@insist1232 小时前
网络工程师-网络规划与设计(一):网络开发过程与逻辑网络设计
网络·网络工程师·软考·软件水平考试
李长渊哦2 小时前
家用宽带动态公网 IP 下 Node + PostgreSQL 服务的 DDNS 全流程部署实践
网络协议·tcp/ip·postgresql
见山是山-见水是水2 小时前
鸿蒙flutter第三方库适配 - 收藏管理应用
flutter·华为·harmonyos
麒麟ZHAO2 小时前
鸿蒙flutter第三方库适配 - 路由导航应用
flutter·华为·harmonyos
2401_839633912 小时前
鸿蒙flutter第三方库适配 - 时间线应用
flutter·华为·harmonyos
见山是山-见水是水2 小时前
鸿蒙flutter第三方库适配 - 在线文档阅读器
flutter·华为·harmonyos
麒麟ZHAO2 小时前
鸿蒙flutter第三方库适配 - 文件压缩工具
flutter·华为·harmonyos
QH139292318802 小时前
是德科技KEYSIGHT N5183B 9 kHz~40 GHz微波模拟信号发生器
网络·数据库·科技·嵌入式硬件·集成测试
灰子学技术2 小时前
Envoy 中 UDP 网络通信实现分析
网络·单片机·嵌入式硬件·网络协议·udp