实时功能(聊天、推送通知、行情更新)是现代 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 图片与资源管理