前言
在当今的移动应用开发中,实时数据推送已成为标配需求。无论是 AI 对话的逐字输出、股票行情的实时更新,还是新闻推送的即时通知,Server-Sent Events(SSE)都提供了一种比 WebSocket 更轻量、更易实现的解决方案。
本文将带领你从零开始,全面掌握 Flutter 中 SSE 的开发技巧。我们将从 SSE 协议原理讲起,逐步深入到实战代码,并最终实现一个完整的 AI 流式对话应用。
一、SSE 基础:什么是 Server-Sent Events?
1.1 SSE 的核心概念
Server-Sent Events 是一种基于 HTTP 的服务端推送技术,允许服务端主动向客户端推送数据。与传统的请求-响应模式不同,SSE 会维持一个长连接,服务端可以持续不断地发送数据。
核心特点:
· 基于 HTTP/HTTPS 协议,无需特殊协议
· 单向通信(服务端 → 客户端)
· 自动重连机制
· 轻量级,实现简单
1.2 SSE vs WebSocket:如何选择?
特性 SSE WebSocket
通信方向 单向(服务端→客户端) 双向
协议 HTTP/HTTPS WS/WSS
自动重连 ✅ 内置支持 ❌ 需手动实现
数据格式 文本(纯文本/JSON) 文本/二进制
实现复杂度 简单 较复杂
适用场景 实时通知、流式输出 聊天、游戏、双向交互
浏览器支持 所有现代浏览器 所有现代浏览器
选择建议:
· 只需服务端推送 → 选择 SSE
· 需要双向通信 → 选择 WebSocket
· 需要发送二进制数据 → 选择 WebSocket
1.3 SSE 数据格式详解
SSE 的数据格式非常简洁,每个事件由以下字段组成:
event: message
data: {"text": "Hello"}
event: done
data: [DONE]
字段说明:
· event: - 事件类型(可选,默认 "message")
· data: - 事件数据(必需)
· id: - 事件 ID,用于断点续传(可选)
· retry: - 重连时间(毫秒,可选)
· 空行 - 表示一个事件结束
多行数据示例:
data: {"id": 1,
data: "name": "张三",
data: "age": 25}
二、环境搭建与依赖配置
2.1 创建 Flutter 项目
bash
flutter create flutter_sse_demo
cd flutter_sse_demo
2.2 添加必要依赖
在 pubspec.yaml 中添加依赖:
yaml
dependencies:
flutter:
sdk: flutter
# 网络请求
dio: ^5.4.0
# 图片缓存(可选)
cached_network_image: ^3.3.0
# 状态管理
provider: ^6.1.1
# 日志打印
logger: ^2.0.1
安装依赖:
bash
flutter pub get
2.3 添加网络权限
对于 Android,在 android/app/src/main/AndroidManifest.xml 中添加:
xml
<uses-permission android:name="android.permission.INTERNET" />
对于 iOS,默认已支持网络访问。
三、手写实现:深入理解 SSE 底层
在引入框架之前,让我们先手写一个 SSE 客户端。这不仅能帮助我们理解协议细节,还能在特殊场景下进行定制化开发。
3.1 基础版本:使用 Dio 流式解析
dart
// services/sse_manual_service.dart
import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';
class SseManualService {
final Dio _dio = Dio();
CancelToken? _cancelToken;
/// 手动实现 SSE 流式接收
Stream<SseEvent> connect({
required String url,
required Map<String, String> headers,
Map<String, dynamic>? body,
}) async* {
_cancelToken = CancelToken();
try {
// 发起流式请求
final response = await _dio.post<ResponseBody>(
url,
data: body,
options: Options(
responseType: ResponseType.stream,
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
...headers,
},
),
cancelToken: _cancelToken,
);
final stream = response.data?.stream;
if (stream == null) return;
String buffer = ''; // 数据缓冲区
// 逐块读取数据
await for (final bytes in stream) {
buffer += utf8.decode(bytes);
// 按行处理,确保完整解析
while (buffer.contains('\n')) {
final idx = buffer.indexOf('\n');
final line = buffer.substring(0, idx).trim();
buffer = buffer.substring(idx + 1);
if (line.isEmpty) continue;
// 解析 SSE 数据行
if (line.startsWith('data:')) {
final dataStr = line.substring(5).trim();
// 检查结束标志
if (dataStr == '[DONE]') {
return;
}
try {
// 尝试解析 JSON
final json = jsonDecode(dataStr);
final event = SseEvent.fromJson(json);
yield event;
} catch (e) {
// 非 JSON 数据,直接返回文本
yield SseEvent(
event: 'message',
data: dataStr,
);
}
}
}
}
} catch (e) {
if (e is DioError && CancelToken.isCancel(e)) {
print('SSE 连接已取消');
} else {
print('SSE 错误: $e');
rethrow;
}
}
}
/// 取消连接
void cancel() {
_cancelToken?.cancel('用户取消');
}
}
/// SSE 事件模型
class SseEvent {
final String event;
final dynamic data;
final String? id;
SseEvent({
required this.event,
required this.data,
this.id,
});
factory SseEvent.fromJson(Map<String, dynamic> json) {
return SseEvent(
event: json['event'] ?? 'message',
data: json,
id: json['id'],
);
}
}
3.2 核心原理解析
为什么需要 Buffer?
网络传输中,一个数据块可能不是完整的一行,甚至可能把 data: 的 JSON 从中间截断:
dart
// 错误示例:直接解析每个 chunk
await for (final bytes in stream) {
final line = utf8.decode(bytes);
if (line.startsWith('data:')) { ... } // 可能解析失败!
}
// 正确做法:使用 buffer 累积数据
String buffer = '';
await for (final bytes in stream) {
buffer += utf8.decode(bytes);
while (buffer.contains('\n')) {
// 按行处理完整数据
}
}
处理多种 SSE 字段:
dart
String? eventType;
String? eventId;
String? eventData;
while (buffer.contains('\n')) {
final line = ...;
if (line.startsWith('event:')) {
eventType = line.substring(6).trim();
}
else if (line.startsWith('id:')) {
eventId = line.substring(3).trim();
}
else if (line.startsWith('data:')) {
eventData = line.substring(5).trim();
}
else if (line.isEmpty) {
// 空行表示事件结束,触发回调
if (eventData != null) {
yield SseEvent(
event: eventType ?? 'message',
data: eventData,
id: eventId,
);
eventData = null;
eventType = null;
}
}
}
四、使用成熟框架:简化开发
手写实现虽然灵活,但需要考虑各种边界情况。使用成熟的框架可以大大提高开发效率。
4.1 方案一:dart_http_sse
这是一个轻量级的 SSE 客户端库,支持自动重连。
安装依赖:
yaml
dependencies:
flutter_http_sse:
git:
url: https://github.com/ElshiatyTube/flutter_http_sse.git
基础用法:
dart
import 'package:flutter_http_sse/model/sse_request.dart';
import 'package:flutter_http_sse/service/sse_client.dart';
class SseSimpleService {
final sseClient = SSEClient();
Stream<SseEvent> connect(String url, Map<String, String> headers) {
final request = SSERequest(
requestType: SSERequestType.GET,
url: Uri.parse(url),
headers: headers,
onData: (data) => print("Received: $data"),
onError: (error) => print("Error: $error"),
onDone: () => print("Stream closed"),
retry: true, // 启用自动重连
);
final Stream<SSEResponse> stream = sseClient.connect("connectionId", request);
return stream.map((response) => SseEvent(
event: response.event ?? 'message',
data: response.data,
));
}
void disconnect() {
sseClient.close(connectionId: "connectionId");
}
}
4.2 方案二:sse_processor(推荐)
sse_processor 是一个功能强大的 SSE 处理库,特别适合处理 AI 流式响应等复杂场景。
安装依赖:
yaml
dependencies:
sse_processor: ^1.0.0
dio: ^5.0.0
初始化配置:
dart
// main.dart
import 'package:sse_processor/sse.dart';
void main() {
final dio = Dio();
SSEProcessor.init(
SSEProcessorConfig(
version: '1.0.0',
debug: true,
idleTimeout: 30.0, // 空闲超时
exceptionTimeout: 60, // 异常超时
sseBufferExtractInterval: 100, // 缓冲区提取间隔(毫秒)
),
dio,
);
runApp(MyApp());
}
完整使用示例:
dart
// services/sse_advanced_service.dart
import 'package:sse_processor/sse.dart';
class SseAdvancedService {
final Map<String, StreamController<SseEvent>> _controllers = {};
/// 创建 SSE 连接
Stream<SseEvent> connect(String connectionId, String url, Map<String, String> headers) {
final controller = StreamController<SseEvent>.broadcast();
_controllers[connectionId] = controller;
// 添加连接状态监听
SSEProcessor.addConnectStateObserver(
SSEConnectObserver(
connectionId,
NotifyPriority.high,
(ConnectState state) {
print('连接状态: $state');
return false;
},
),
);
// 添加事件拦截器
final interceptor = SSEInterceptor(
watchEvents: {'message', 'text', 'done'},
autoClean: true,
intercept: (ServerSentEvent sse, SSEChain chain) {
final event = SseEvent(
event: sse.elementType ?? 'message',
data: sse.result,
id: sse.id,
);
controller.add(event);
return chain.proceed(sse);
},
);
SSEProcessor.addSSEInterceptor(interceptor);
return controller.stream;
}
/// 关闭连接
void disconnect(String connectionId) {
_controllers[connectionId]?.close();
_controllers.remove(connectionId);
SSEProcessor.removeSSEInterceptorById(connectionId);
}
/// 暂停/恢复数据分发
void setDeliverState(String connectionId, DelivererState state) {
SSEProcessor.setDeliverState(state);
}
}
五、实战:完整的 AI 流式对话应用
结合前面的知识,我们来实现一个完整的 AI 流式对话应用。这个应用将支持:
· 实时流式文本输出
· 图片生成与展示
· 消息历史记录
· 优雅的 UI 反馈
5.1 项目结构
lib/
├── models/
│ └── chat_message.dart # 消息模型
├── services/
│ └── openai_sse_service.dart # SSE 服务
├── viewmodels/
│ └── chat_viewmodel.dart # 视图模型
├── widgets/
│ ├── message_bubble.dart # 消息气泡组件
│ └── input_area.dart # 输入区域组件
└── screens/
└── chat_screen.dart # 聊天界面
5.2 数据模型定义
dart
// models/chat_message.dart
class ChatMessage {
final String id;
final String content;
final bool isUser;
final DateTime timestamp;
final String? imageUrl;
final bool isError;
final bool isStreaming;
ChatMessage({
String? id,
required this.content,
required this.isUser,
DateTime? timestamp,
this.imageUrl,
this.isError = false,
this.isStreaming = false,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
timestamp = timestamp ?? DateTime.now();
ChatMessage copyWith({
String? content,
bool? isStreaming,
String? imageUrl,
}) {
return ChatMessage(
id: id,
content: content ?? this.content,
isUser: isUser,
timestamp: timestamp,
imageUrl: imageUrl ?? this.imageUrl,
isError: isError,
isStreaming: isStreaming ?? this.isStreaming,
);
}
}
// models/sse_event.dart
class SseEvent {
final String event;
final Map<String, dynamic>? data;
final String? content;
final Map<String, dynamic>? imageData;
SseEvent({
required this.event,
this.data,
this.content,
this.imageData,
});
factory SseEvent.fromJson(Map<String, dynamic> json) {
// 解析 AI 消息内容
String? content;
Map<String, dynamic>? imageData;
if (json['aiMessage'] != null) {
final aiMessage = json['aiMessage'];
content = aiMessage['content'];
// 提取图片数据
if (aiMessage['image'] != null && aiMessage['image'] is Map) {
imageData = Map<String, dynamic>.from(aiMessage['image']);
}
}
return SseEvent(
event: json['event'] ?? 'message',
data: json,
content: content,
imageData: imageData,
);
}
}
5.3 SSE 服务实现
dart
// services/openai_sse_service.dart
import 'dart:async';
import 'dart:convert';
import 'package:dio/dio.dart';
class OpenAISseService {
final Dio _dio = Dio();
CancelToken? _cancelToken;
/// 发送消息并获取流式响应
Stream<SseEvent> streamChat({
required String apiKey,
required int conversationId,
required String message,
List<Map<String, String>>? history,
}) async* {
_cancelToken = CancelToken();
try {
final response = await _dio.post<ResponseBody>(
'https://your-api.com/chat/stream',
data: {
'conversationId': conversationId,
'content': message,
'history': history ?? [],
'stream': true,
},
options: Options(
responseType: ResponseType.stream,
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
),
cancelToken: _cancelToken,
);
final stream = response.data?.stream;
if (stream == null) return;
String buffer = '';
await for (final bytes in stream) {
buffer += utf8.decode(bytes);
// 按行处理
while (buffer.contains('\n')) {
final idx = buffer.indexOf('\n');
final line = buffer.substring(0, idx).trim();
buffer = buffer.substring(idx + 1);
if (line.isEmpty) continue;
// 解析 SSE 数据
if (line.startsWith('data:')) {
final dataStr = line.substring(5).trim();
if (dataStr == '[DONE]') {
return;
}
try {
final json = jsonDecode(dataStr);
final event = SseEvent.fromJson(json);
yield event;
// 如果是 done 事件,结束流
if (event.event == 'done') {
return;
}
} catch (e) {
print('JSON 解析错误: $e');
}
}
}
}
} catch (e) {
if (e is DioError && CancelToken.isCancel(e)) {
print('请求已取消');
} else {
print('SSE 错误: $e');
rethrow;
}
}
}
/// 取消当前请求
void cancel() {
_cancelToken?.cancel('用户取消');
}
}
5.4 ViewModel 实现
dart
// viewmodels/chat_viewmodel.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/chat_message.dart';
import '../models/sse_event.dart';
import '../services/openai_sse_service.dart';
class ChatViewModel extends ChangeNotifier {
final OpenAISseService _service = OpenAISseService();
final String apiKey;
final int conversationId;
List<ChatMessage> _messages = [];
String _currentStreamingText = '';
String? _currentImageUrl;
bool _isStreaming = false;
bool _isLoading = false;
String? _error;
ChatViewModel({
required this.apiKey,
required this.conversationId,
});
// Getters
List<ChatMessage> get messages => _messages;
String get currentStreamingText => _currentStreamingText;
String? get currentImageUrl => _currentImageUrl;
bool get isStreaming => _isStreaming;
bool get isLoading => _isLoading;
String? get error => _error;
/// 发送消息
Future<void> sendMessage(String content) async {
if (content.isEmpty) return;
// 重置状态
_error = null;
_isLoading = true;
_currentStreamingText = '';
_currentImageUrl = null;
_isStreaming = true;
notifyListeners();
// 添加用户消息
final userMessage = ChatMessage(
content: content,
isUser: true,
);
_messages.add(userMessage);
notifyListeners();
// 准备消息历史
final history = _messages
.where((msg) => !msg.isStreaming)
.map((msg) => {
'role': msg.isUser ? 'user' : 'assistant',
'content': msg.content,
})
.toList();
try {
// 开始流式接收
await for (final event in _service.streamChat(
apiKey: apiKey,
conversationId: conversationId,
message: content,
history: history,
)) {
if (event.event == 'message' && event.content != null) {
// 更新流式文本
_currentStreamingText = event.content!;
notifyListeners();
}
else if (event.event == 'done') {
// 最终消息
if (event.content != null) {
_currentStreamingText = event.content!;
}
// 提取图片
if (event.imageData != null && event.imageData!['url'] != null) {
_currentImageUrl = event.imageData!['url'];
}
// 保存完整消息
_messages.add(ChatMessage(
content: _currentStreamingText,
isUser: false,
imageUrl: _currentImageUrl,
));
// 清空临时状态
_currentStreamingText = '';
_currentImageUrl = null;
_isStreaming = false;
notifyListeners();
break;
}
}
} catch (e) {
_error = '发送失败: $e';
_messages.add(ChatMessage(
content: '发送失败,请重试',
isUser: false,
isError: true,
));
} finally {
_isLoading = false;
_isStreaming = false;
notifyListeners();
}
}
/// 取消当前流式响应
void cancelStream() {
_service.cancel();
_isStreaming = false;
_isLoading = false;
_currentStreamingText = '';
notifyListeners();
}
/// 清空所有消息
void clearMessages() {
_messages.clear();
_currentStreamingText = '';
_currentImageUrl = null;
_error = null;
notifyListeners();
}
@override
void dispose() {
_service.cancel();
super.dispose();
}
}
5.5 UI 组件实现
dart
// widgets/message_bubble.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../models/chat_message.dart';
class MessageBubble extends StatelessWidget {
final ChatMessage message;
const MessageBubble({Key? key, required this.message}) : super(key: key);
@override
Widget build(BuildContext context) {
return Align(
alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: const BoxConstraints(maxWidth: 280),
decoration: BoxDecoration(
color: _getBackgroundColor(),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message.content,
style: TextStyle(
color: _getTextColor(),
),
),
if (message.imageUrl != null) ...[
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: message.imageUrl!,
placeholder: (context, url) => const SizedBox(
height: 100,
child: Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => const Icon(
Icons.broken_image,
size: 50,
),
fit: BoxFit.cover,
),
),
],
const SizedBox(height: 4),
Text(
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 10,
color: _getTimeColor(),
),
),
],
),
),
);
}
Color _getBackgroundColor() {
if (message.isError) return Colors.red.shade100;
if (message.isUser) return Colors.blue;
return Colors.grey.shade300;
}
Color _getTextColor() {
if (message.isUser) return Colors.white;
return Colors.black87;
}
Color _getTimeColor() {
if (message.isUser) return Colors.white70;
return Colors.grey.shade600;
}
String _formatTime(DateTime time) {
final now = DateTime.now();
final difference = now.difference(time);
if (difference.inMinutes < 1) {
return '刚刚';
} else if (difference.inHours < 1) {
return '${difference.inMinutes}分钟前';
} else if (difference.inDays < 1) {
return '${difference.inHours}小时前';
} else {
return '${time.month}/${time.day} ${time.hour}:${time.minute.toString().padLeft(2, '0')}';
}
}
}
dart
// widgets/input_area.dart
import 'package:flutter/material.dart';
class InputArea extends StatelessWidget {
final TextEditingController controller;
final bool isLoading;
final bool isStreaming;
final VoidCallback onSend;
final VoidCallback onCancel;
const InputArea({
Key? key,
required this.controller,
required this.isLoading,
required this.isStreaming,
required this.onSend,
required this.onCancel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
offset: const Offset(0, -2),
blurRadius: 4,
color: Colors.black.withOpacity(0.05),
),
],
),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
enabled: !isLoading,
maxLines: null,
decoration: InputDecoration(
hintText: '输入消息...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
onSubmitted: (_) => onSend(),
),
),
const SizedBox(width: 8),
if (isStreaming)
IconButton(
icon: const Icon(Icons.stop, color: Colors.red),
onPressed: onCancel,
)
else
IconButton(
icon: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.send),
onPressed: isLoading ? null : onSend,
),
],
),
);
}
}
5.6 主界面实现
dart
// screens/chat_screen.dart
import 'package:flutter/material.dart';
import '../viewmodels/chat_viewmodel.dart';
import '../widgets/message_bubble.dart';
import '../widgets/input_area.dart';
class ChatScreen extends StatefulWidget {
final String apiKey;
final int conversationId;
const ChatScreen({
Key? key,
required this.apiKey,
required this.conversationId,
}) : super(key: key);
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
late ChatViewModel _viewModel;
final TextEditingController _controller = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_viewModel = ChatViewModel(
apiKey: widget.apiKey,
conversationId: widget.conversationId,
);
}
@override
void dispose() {
_controller.dispose();
_scrollController.dispose();
_viewModel.dispose();
super.dispose();
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AI 助手'),
actions: [
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _viewModel.clearMessages(),
),
],
),
body: Column(
children: [
// 消息列表
Expanded(
child: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
_scrollToBottom();
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _viewModel.messages.length +
(_viewModel.isStreaming ? 1 : 0),
itemBuilder: (context, index) {
// 正在流式输出的消息
if (index == _viewModel.messages.length &&
_viewModel.isStreaming) {
return _buildStreamingMessage();
}
final message = _viewModel.messages[index];
return MessageBubble(message: message);
},
);
},
),
),
// 输入区域
InputArea(
controller: _controller,
isLoading: _viewModel.isLoading,
isStreaming: _viewModel.isStreaming,
onSend: _handleSend,
onCancel: _viewModel.cancelStream,
),
],
),
);
}
Widget _buildStreamingMessage() {
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: const BoxConstraints(maxWidth: 280),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_viewModel.currentStreamingText,
style: const TextStyle(color: Colors.black87),
),
if (_viewModel.currentImageUrl != null) ...[
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
_viewModel.currentImageUrl!,
height: 150,
fit: BoxFit.cover,
),
),
],
const SizedBox(height: 8),
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
),
);
}
void _handleSend() {
if (_controller.text.isNotEmpty) {
_viewModel.sendMessage(_controller.text);
_controller.clear();
}
}
}
5.7 应用入口
dart
// main.dart
import 'package:flutter/material.dart';
import 'screens/chat_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter SSE Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: ChatScreen(
apiKey: 'your-api-key-here',
conversationId: 1,
),
);
}
}
六、高级优化技巧
6.1 自动重连机制
dart
class AutoReconnectSseService {
int _retryCount = 0;
static const int maxRetries = 5;
Timer? _reconnectTimer;
Stream<SseEvent> connectWithRetry(String url, Map<String, String> headers) async* {
while (_retryCount < maxRetries) {
try {
await for (final event in _connect(url, headers)) {
// 成功连接,重置重试计数
_retryCount = 0;
yield event;
}
break;
} catch (e) {
_retryCount++;
if (_retryCount >= maxRetries) {
rethrow;
}
// 指数退避
final delay = Duration(seconds: _retryCount * _retryCount);
await Future.delayed(delay);
}
}
}
}
6.2 心跳检测
dart
class HeartbeatAwareService {
Timer? _heartbeatTimer;
DateTime _lastHeartbeat = DateTime.now();
Stream<SseEvent> connectWithHeartbeat(String url) async* {
// 启动心跳检测
_heartbeatTimer = Timer.periodic(Duration(seconds: 30), (_) {
final timeSinceLastHeartbeat = DateTime.now().difference(_lastHeartbeat);
if (timeSinceLastHeartbeat > Duration(seconds: 60)) {
print('心跳超时,断开连接');
_heartbeatTimer?.cancel();
// 触发重连逻辑
}
});
await for (final event in _connect(url)) {
if (event.event == 'heartbeat') {
_lastHeartbeat = DateTime.now();
}
yield event;
}
}
}
6.3 断点续传
dart
class ResumeableSseService {
String? _lastEventId;
final SharedPreferences _prefs;
ResumeableSseService(this._prefs);
Stream<SseEvent> connectWithResume(String url) async* {
// 加载上次的 event ID
_lastEventId = _prefs.getString('last_event_id');
final headers = {
if (_lastEventId != null) 'Last-Event-ID': _lastEventId!,
};
await for (final event in _connect(url, headers)) {
// 保存最后收到的 ID
if (event.id != null) {
_lastEventId = event.id;
await _prefs.setString('last_event_id', event.id!);
}
yield event;
}
}
}
6.4 节流与防抖
dart
class ThrottledSseService {
Timer? _throttleTimer;
String _pendingData = '';
Stream<String> connectWithThrottle(String url) async* {
await for (final event in _connect(url)) {
_pendingData += event;
if (_throttleTimer == null) {
_throttleTimer = Timer(Duration(milliseconds: 16), () {
// 每 16ms 更新一次 UI(约 60fps)
yield _pendingData;
_pendingData = '';
_throttleTimer = null;
});
}
}
}
}
七、常见问题与解决方案
7.1 连接频繁断开
问题现象:SSE 连接经常意外断开。
解决方案:
· 确保服务端有心跳机制
· 设置合理的超时时间
· 实现客户端重连逻辑
dart
// 服务端需要定期发送心跳
// 格式:data: heartbeat\n\n
7.2 数据解析失败
问题现象:JSON 解析偶尔出错。
解决方案:使用 buffer 累积数据,确保按行处理
dart
String buffer = '';
await for (final bytes in stream) {
buffer += utf8.decode(bytes);
while (buffer.contains('\n')) {
// 确保完整的一行
final line = buffer.substring(0, buffer.indexOf('\n'));
buffer = buffer.substring(buffer.indexOf('\n') + 1);
// 处理 line
}
}
7.3 UI 卡顿
问题现象:高频数据更新导致界面卡顿。
解决方案:实现节流,控制 UI 更新频率
dart
// 每 16ms 更新一次 UI
Timer? _updateTimer;
String _buffer = '';
void onData(String chunk) {
_buffer += chunk;
_updateTimer ??= Timer(Duration(milliseconds: 16), () {
setState(() {
text += _buffer;
_buffer = '';
});
_updateTimer = null;
});
}
7.4 内存泄漏
问题现象:页面退出后连接仍然存在。
解决方案:正确管理生命周期
dart
class _ChatScreenState extends State<ChatScreen> {
late ChatViewModel _viewModel;
@override
void dispose() {
_viewModel.dispose(); // 取消连接
super.dispose();
}
}
7.5 网络切换问题
问题现象:WiFi 切换到 4G 时连接中断。
解决方案:监听网络状态变化并重连
dart
import 'package:connectivity_plus/connectivity_plus.dart';
class NetworkAwareService {
StreamSubscription? _connectivitySubscription;
void init() {
_connectivitySubscription = Connectivity().onConnectivityChanged.listen((result) {
if (result != ConnectivityResult.none) {
// 网络恢复,触发重连
_reconnect();
}
});
}
void dispose() {
_connectivitySubscription?.cancel();
}
}
八、性能优化建议
8.1 使用连接池
dart
class SseConnectionPool {
static final Map<String, StreamController<SseEvent>> _connections = {};
static Stream<SseEvent> getOrCreate(String url, Map<String, String> headers) {
if (_connections.containsKey(url)) {
return _connections[url]!.stream;
}
final controller = StreamController<SseEvent>.broadcast();
_connections[url] = controller;
// 启动连接...
return controller.stream;
}
}
8.2 数据压缩
对于大量数据传输,可以考虑使用 gzip 压缩:
dart
options: Options(
headers: {
'Accept-Encoding': 'gzip, deflate',
},
)
8.3 智能缓冲
dart
class AdaptiveBuffer {
static const int _maxBufferSize = 1024 * 1024; // 1MB
String _buffer = '';
void add(String data) {
_buffer += data;
// 防止缓冲区过大
if (_buffer.length > _maxBufferSize) {
_buffer = _buffer.substring(_buffer.length - _maxBufferSize);
}
}
}
九、总结与展望
9.1 核心要点回顾
- SSE 基础:理解 SSE 协议格式,知道何时使用 SSE
- 手写实现:掌握底层原理,学会处理数据缓冲和协议解析
- 框架选择:根据项目需求选择合适的 SSE 库
- 实战应用:实现了完整的 AI 流式对话应用
- 优化技巧:掌握了重连、心跳、节流等高级特性
9.2 最佳实践总结
场景 推荐方案
学习研究 手写实现,深入理解原理
简单推送 dart_http_sse
AI 流式对话 sse_processor
生产环境 手写 + 完善的重连机制
低代码开发 FlutterFlow 内置支持
9.3 未来展望
随着 AI 应用的普及,SSE 在移动端的应用会越来越广泛:
· 实时翻译:边说话边翻译
· 语音转文字:实时转写
· 视频字幕:实时生成字幕
· IoT 监控:实时传感器数据推送
掌握 SSE 技术,将为你的 Flutter 开发之路增添一项重要技能。