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 图片与资源管理

相关推荐
其实防守也摸鱼8 小时前
CTF密码学综合教学指南--第五章
开发语言·网络·笔记·python·安全·网络安全·密码学
S1998_1997111609•X9 小时前
论mysql国盾shell-sfa犯罪行为集团下的分项工程及反向注入原理尐深度纳米算法下的鐌檵鄐鉎行为
网络·数据库·网络协议·百度·开闭原则
AI精钢10 小时前
AI Agent 从上线到删库跑路始末
网络·人工智能·云原生·aigc
笨笨饿11 小时前
69_如何给自己手搓一个串口
linux·c语言·网络·单片机·嵌入式硬件·算法·个人开发
geneculture12 小时前
《智能通信速分多次传输技术(VDMT)》专利文件的全文汉英双语对照版本
服务器·网络·人工智能·融智学的重要应用·哲学与科学统一性·融智时代(杂志)·人机间性
liulian091612 小时前
Flutter for OpenHarmony 跨平台开发:单位转换功能实战指南
flutter
千码君201613 小时前
Trae:一些关于flutter和 go前后端开发构建的分享
android·flutter·gradle·android-studio·trae·vibe code
xrui5813 小时前
2026实战:深度解析 Gemini 3.1 镜像站函数调用在自动化运维工单中的应用
linux·服务器·网络
Python私教13 小时前
GenericAgent记忆系统深度解析:四层架构如何让AI拥有永不遗忘的大脑
网络·人工智能·架构
时空系14 小时前
第9篇:成员功能——为结构体添加能力 Rust中文编程
开发语言·网络·rust