Commit 9d09840c authored by 张宏's avatar 张宏

tcp

parent a1177508
library tcp;
export 'service/index.dart';
\ No newline at end of file
export 'tcp_client_cubit.dart';
export 'tcp_client_state.dart';
export 'tcp_server_cubit.dart';
export 'tcp_server_state.dart';
export 'tcp_connection_status.dart';
\ No newline at end of file
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'tcp_client_state.dart';
import 'tcp_connection_status.dart';
class TcpClientCubit extends Cubit<TcpClientState> {
Socket? _socket;
String? _address;
int? _port;
int _timeout = 5000;
int _maxRetryCount = 3;
int _retryInterval = 2000;
int _currentRetry = 0;
Timer? _reconnectTimer;
Timer? _heartbeatTimer;
bool _isManualDisconnect = false;
bool _isConnecting = false;
final StreamController<Uint8List> _dataController = StreamController.broadcast();
/// 接收到的数据流
Stream<Uint8List> get onData => _dataController.stream;
TcpClientCubit() : super(const TcpClientState());
/// 配置连接参数
void config({
required String address,
required int port,
int timeout = 5000,
int maxRetry = 3,
int retryInterval = 2000,
}) {
_address = address;
_port = port;
_timeout = timeout;
_maxRetryCount = maxRetry;
_retryInterval = retryInterval;
emit(state.copyWith(address: address, port: port));
}
/// 连接服务器
Future<bool> connect() async {
if (_address == null || _port == null) {
emit(state.copyWith(error: '未配置地址或端口'));
return false;
}
if (state.isConnected) return true;
if (_isConnecting) return false;
_isConnecting = true;
_isManualDisconnect = false;
emit(state.copyWith(connectionStatus: TcpConnectionStatus.connecting, clearError: true));
try {
_socket = await Socket.connect(
_address!,
_port!,
timeout: Duration(milliseconds: _timeout),
);
_socket!.setOption(SocketOption.tcpNoDelay, true);
_listenSocket();
_currentRetry = 0;
emit(state.copyWith(connectionStatus: TcpConnectionStatus.connected));
_startHeartbeat();
return true;
} catch (e) {
emit(state.copyWith(
connectionStatus: TcpConnectionStatus.error,
error: e.toString(),
));
_tryReconnect();
return false;
} finally {
_isConnecting = false;
}
}
/// 监听 Socket 数据
void _listenSocket() {
_socket!.listen(
(data) {
final uint8List = Uint8List.fromList(data);
if (!_dataController.isClosed) {
_dataController.add(uint8List);
}
},
onError: (e) {
emit(state.copyWith(
connectionStatus: TcpConnectionStatus.error,
error: e.toString(),
));
_tryReconnect();
},
onDone: () {
if (!_isManualDisconnect) {
_tryReconnect();
}
},
);
}
/// 自动重连
void _tryReconnect() {
if (_isManualDisconnect) return;
if (_currentRetry >= _maxRetryCount) {
emit(state.copyWith(connectionStatus: TcpConnectionStatus.disconnected));
return;
}
_currentRetry++;
emit(state.copyWith(connectionStatus: TcpConnectionStatus.reconnecting));
_reconnectTimer?.cancel();
_reconnectTimer = Timer(Duration(milliseconds: _retryInterval), () {
connect();
});
}
/// 发送 Uint8List
Future<bool> send(Uint8List data) async {
if (_socket == null || !state.isConnected) return false;
try {
_socket!.add(data);
await _socket!.flush();
return true;
} catch (e) {
return false;
}
}
/// 发送十六进制字符串
Future<bool> sendHex(String hex) async {
hex = hex.replaceAll(RegExp(r'\s+'), '');
if (hex.length % 2 != 0) return false;
try {
List<int> bytes = [];
for (int i = 0; i < hex.length; i += 2) {
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
}
return await send(Uint8List.fromList(bytes));
} catch (e) {
return false;
}
}
/// 发送字符串 UTF8
Future<bool> sendString(String msg) async {
return await send(Uint8List.fromList(msg.codeUnits));
}
/// 心跳保活(每 15 秒发送一次)
void _startHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
if (state.isConnected) {
sendHex('00 00 00 00');
}
});
}
/// 手动断开
void disconnect() {
_isManualDisconnect = true;
_reconnectTimer?.cancel();
_heartbeatTimer?.cancel();
_socket?.close();
_socket = null;
emit(state.copyWith(connectionStatus: TcpConnectionStatus.disconnected));
}
@override
Future<void> close() {
disconnect();
_dataController.close();
return super.close();
}
}
\ No newline at end of file
import 'package:equatable/equatable.dart';
import 'tcp_connection_status.dart';
class TcpClientState extends Equatable {
final TcpConnectionStatus connectionStatus;
final String? address;
final int? port;
final String? error;
const TcpClientState({
this.connectionStatus = TcpConnectionStatus.disconnected,
this.address,
this.port,
this.error,
});
TcpClientState copyWith({
TcpConnectionStatus? connectionStatus,
String? address,
int? port,
String? error,
bool clearError = false,
}) {
return TcpClientState(
connectionStatus: connectionStatus ?? this.connectionStatus,
address: address ?? this.address,
port: port ?? this.port,
error: clearError ? null : (error ?? this.error),
);
}
bool get isConnected => connectionStatus == TcpConnectionStatus.connected;
@override
List<Object?> get props => [connectionStatus, address, port, error];
}
\ No newline at end of file
/// TCP 连接状态枚举
enum TcpConnectionStatus {
/// 未连接
disconnected,
/// 连接中
connecting,
/// 已连接
connected,
/// 重连中
reconnecting,
/// 错误
error,
}
\ No newline at end of file
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'tcp_server_state.dart';
class TcpServerCubit extends Cubit<TcpServerState> {
ServerSocket? _serverSocket;
final List<Socket> _clients = [];
final StreamController<Map<String, dynamic>> _dataController = StreamController.broadcast();
/// 接收到的客户端数据流
Stream<Map<String, dynamic>> get onClientData => _dataController.stream;
TcpServerCubit() : super(const TcpServerState());
/// 设置端口
void setPort(int port) {
emit(state.copyWith(port: port));
}
/// 启动服务端
Future<void> start() async {
try {
_serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, state.port);
emit(state.copyWith(isRunning: true, port: _serverSocket!.port));
_listenClients();
} catch (e) {
if (e.toString().contains('already in use')) {
emit(state.copyWith(port: state.port + 1));
start();
} else {
emit(state.copyWith(error: e.toString()));
}
}
}
/// 监听客户端连接
void _listenClients() {
_serverSocket!.listen((client) {
_clients.add(client);
emit(state.copyWith(connectedClients: _clients.length));
_handleClient(client);
});
}
/// 处理单个客户端数据
void _handleClient(Socket client) {
client.listen(
(data) {
_dataController.add({
'client': client,
'address': client.remoteAddress.address,
'port': client.remotePort,
'data': Uint8List.fromList(data),
});
},
onDone: () => _removeClient(client),
onError: (e) => _removeClient(client),
);
}
/// 群发字节数据给所有客户端
void sendToAll(Uint8List data) {
for (var c in _clients) {
try {
c.add(data);
} catch (_) {}
}
}
/// 群发十六进制字符串给所有客户端
void sendHexToAll(String hex) {
hex = hex.replaceAll(RegExp(r'\s+'), '');
List<int> bytes = [];
for (int i = 0; i < hex.length; i += 2) {
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
}
sendToAll(Uint8List.fromList(bytes));
}
/// 移除客户端
void _removeClient(Socket client) {
_clients.remove(client);
emit(state.copyWith(connectedClients: _clients.length));
client.destroy();
}
/// 停止服务端
Future<void> stop() async {
for (var c in _clients) {
await c.close();
}
_clients.clear();
await _serverSocket?.close();
emit(state.copyWith(isRunning: false, connectedClients: 0));
}
@override
Future<void> close() async {
await stop();
_dataController.close();
return super.close();
}
}
\ No newline at end of file
import 'package:equatable/equatable.dart';
class TcpServerState extends Equatable {
final bool isRunning;
final int port;
final int connectedClients;
final String? error;
const TcpServerState({
this.isRunning = false,
this.port = 0,
this.connectedClients = 0,
this.error,
});
TcpServerState copyWith({
bool? isRunning,
int? port,
int? connectedClients,
String? error,
bool clearError = false,
}) {
return TcpServerState(
isRunning: isRunning ?? this.isRunning,
port: port ?? this.port,
connectedClients: connectedClients ?? this.connectedClients,
error: clearError ? null : (error ?? this.error),
);
}
@override
List<Object?> get props => [isRunning, port, connectedClients, error];
}
\ No newline at end of file
# TCP 模块使用手册
# TCP 模块使用手册
## 概述
本模块基于 `flutter_bloc` + `Cubit` 架构,提供 TCP 客户端和服务端通信能力,与项目整体技术栈保持一致。
### 模块结构
```
lib/utils/tcp/
├── index.dart
└── service/
├── index.dart # 统一导出
├── tcp_connection_status.dart # 连接状态枚举
├── tcp_client_state.dart # 客户端状态
├── tcp_client_cubit.dart # 客户端 Cubit
├── tcp_server_state.dart # 服务端状态
└── tcp_server_cubit.dart # 服务端 Cubit
```
### 依赖
本模块依赖 `flutter_bloc``equatable`,已在项目 `pubspec.yaml` 中配置,无需额外引入。
---
## 一、TCP 客户端(TcpClientCubit)
### 1.1 State 说明
`TcpClientState` 包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `connectionStatus` | `TcpConnectionStatus` | 当前连接状态 |
| `address` | `String?` | 目标服务器地址 |
| `port` | `int?` | 目标服务器端口 |
| `error` | `String?` | 最近一次错误信息 |
| `isConnected` | `bool` | 便捷属性:是否已连接(仅 `connected` 状态返回 true) |
### 1.2 TcpConnectionStatus 枚举
| 值 | 说明 |
|---|---|
| `disconnected` | 未连接 / 已断开 |
| `connecting` | 正在建立连接 |
| `connected` | 已连接,可正常通信 |
| `reconnecting` | 自动重连中 |
| `error` | 连接出错 |
**状态转换流程:**
```
disconnected → connecting → connected
↓ ↓
error ←───────┘ (连接异常/Socket 错误)
reconnecting → ... (最多重试 maxRetry 次)
disconnected (重试耗尽)
```
手动调用 `disconnect()` 后状态直接回到 `disconnected`,且不会触发自动重连。
### 1.3 公共 API 参考
| 方法/属性 | 签名 | 返回值 | 说明 |
|-----------|------|--------|------|
| `config()` | `config({required address, required port, timeout, maxRetry, retryInterval})` | `void` | 配置连接参数。必须先调用此方法再 connect |
| `connect()` | `Future<bool>` | `bool` | 发起连接。成功返回 `true`;已连接返回 `true`;正在连接中返回 `false`;失败返回 `false` 并触发自动重连 |
| `disconnect()` | `void` | — | 手动断开连接,停止心跳和重连 |
| `send()` | `Future<bool>(Uint8List data)` | `bool` | 发送字节数组。未连接或发送失败返回 `false` |
| `sendHex()` | `Future<bool>(String hex)` | `bool` | 发送十六进制字符串(支持空格分隔)。奇数长度或非法字符返回 `false` |
| `sendString()` | `Future<bool>(String msg)` | `bool` | 发送字符串(使用 UTF-8 编码)。未连接返回 `false` |
| `onData` | `Stream<Uint8List>` (getter) | Stream | 接收到的原始数据流,广播流,可多次订阅 |
| `close()` | `Future<void>` | — | 资源清理:断开连接 + 关闭数据流 |
#### config() 参数详情
| 参数 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| `address` | `String` | — | 是 | 服务器 IP 或域名 |
| `port` | `int` | — | 是 | 服务器端口 |
| `timeout` | `int` | `5000` | 否 | 连接超时时间(毫秒) |
| `maxRetry` | `int` | `3` | 否 | 断线后最大自动重试次数 |
| `retryInterval` | `int` | `2000` | 否 | 每次重试的间隔(毫秒) |
### 1.4 并发安全机制
`connect()` 方法内置了**并发防护**
- 如果当前已有连接正在进行中(`connecting` 状态),再次调用 `connect()` 会立即返回 `false`,不会创建重复连接
- 使用内部标志位 `_isConnecting` 配合 `try/finally` 保证状态正确释放
- **使用建议**:无需在外层加锁,多次快速点击"连接"按钮是安全的
```dart
// 安全示例:连续多次调用不会产生问题
cubit.connect(); // 第1次:正常发起连接
cubit.connect(); // 第2次:立即返回 false,忽略
cubit.connect(); // 第3次:立即返回 false,忽略
// 最终只会有一个 Socket 连接
```
### 1.5 心跳机制
`TcpClientCubit` 内置心跳保活功能:
- **触发时机**:连接成功后自动启动
- **间隔**:每 **15 秒** 发送一次
- **内容**:十六进制 `00 00 00 00`(4 字节全零)
- **停止时机**:调用 `disconnect()` 时自动取消;连接断开时也会因 `state.isConnected == false` 而不再发送
- **自定义**:如需修改心跳内容或间隔,可直接编辑 `_startHeartbeat()` 方法
> 注意:心跳包通过 `sendHex()` 发送,如果发送失败不会影响连接状态(静默失败)。
### 1.6 自动重连机制
当以下情况发生时,客户端会尝试自动重连:
- `Socket.connect()` 抛出异常(网络不通、服务器未启动等)
- Socket 的 `onError` 回调被触发
- Socket 的 `onDone` 回调被触发(对端关闭连接)
**重连规则:**
| 条件 | 行为 |
|------|------|
| 当前重试次数 < `maxRetry` | 等待 `retryInterval` 毫秒后重新调用 `connect()` |
| 当前重试次数 >= `maxRetry` | 放弃重连,状态变为 `disconnected` |
| 手动调用了 `disconnect()` | 不触发重连(`_isManualDisconnect = true` 阻止) |
每次重连成功后,`_currentRetry` 计数器归零。
### 1.7 基本使用
#### 第一步:在 Widget 树中注入
```dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/utils/tcp/service/tcp_client_cubit.dart';
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TcpClientCubit(),
child: MyPageBody(),
);
}
}
```
#### 第二步:配置并连接
```dart
class MyPageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cubit = context.read<TcpClientCubit>();
// 先配置,后连接
cubit.config(
address: '192.168.1.100',
port: 8888,
timeout: 5000,
maxRetry: 3,
retryInterval: 2000,
);
// 发起连接(返回 Future<bool>)
cubit.connect();
return /* ... UI */;
}
}
```
#### 第三步:监听连接状态变化
```dart
// 方式一:BlocBuilder —— 根据状态构建 UI
BlocBuilder<TcpClientCubit, TcpClientState>(
builder: (context, state) {
switch (state.connectionStatus) {
case TcpConnectionStatus.disconnected:
return Text('未连接');
case TcpConnectionStatus.connecting:
return Row(
children: [
CircularProgressIndicator(strokeWidth: 2),
SizedBox(width: 8),
Text('连接中...'),
],
);
case TcpConnectionStatus.connected:
return Text('已连接 ${state.address}:${state.port}');
case TcpConnectionStatus.reconnecting:
return Text('重连中...');
case TcpConnectionStatus.error:
return Text('错误: ${state.error}', style: TextStyle(color: Colors.red));
}
},
)
// 方式二:BlocListener —— 监听状态变化执行副作用(不重建 UI)
BlocListener<TcpClientCubit, TcpClientState>(
listener: (context, state) {
if (state.connectionStatus == TcpConnectionStatus.connected) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('连接成功')),
);
}
if (state.connectionStatus == TcpConnectionStatus.disconnected &&
state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('连接失败: ${state.error}')),
);
}
},
child: /* ... UI */,
)
```
#### 第四步:监听接收到的数据
```dart
class _MyPageBodyState extends State<MyPageBody> {
StreamSubscription<Uint8List>? _dataSubscription;
@override
void initState() {
super.initState();
final cubit = context.read<TcpClientCubit>();
// 订阅数据流(onData 是广播流,可多次订阅)
_dataSubscription = cubit.onData.listen((data) {
// 将字节转为十六进制字符串显示
String hex = data
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(' ');
print('收到数据 (${data.length} bytes): $hex');
// 在此处解析业务协议...
});
}
@override
void dispose() {
_dataSubscription?.cancel(); // 务必取消订阅防止内存泄漏
super.dispose();
}
}
```
#### 第五步:发送数据
```dart
final cubit = context.read<TcpClientCubit>();
// 方式一:发送十六进制字符串(最常用,支持空格分隔)
bool ok = await cubit.sendHex('AA BB CC DD');
if (!ok) print('发送失败');
// 方式二:发送原始字节数组
await cubit.send(Uint8List.fromList([0xAA, 0xBB, 0xCC, 0xDD]));
// 方式三:发送文本字符串(UTF-8 编码)
await cubit.sendString('Hello World');
```
**发送方法对比:**
| 方法 | 适用场景 | 输入格式 | 返回值含义 |
|------|---------|----------|-----------|
| `sendHex()` | 发送协议指令 | 十六进制字符串(如 `"AA BB"`) | `false` = 格式错误/未连接/发送异常 |
| `send()` | 发送原始字节 | `Uint8List` | `false` = 未连接/发送异常 |
| `sendString()` | 发送文本消息 | `String` | `false` = 未连接/发送异常 |
> **注意**:所有发送方法在未连接状态下都会安全地返回 `false`,不会抛出异常。
#### 第六步:断开连接与资源清理
```dart
// 仅断开连接(保留 Cubit,可再次 connect)
context.read<TcpClientCubit>().disconnect();
// 完全销毁 Cubit(通常不需要手动调用,BlocProvider 会处理)
// context.read<TcpClientCubit>().close();
```
### 1.8 完整示例
```dart
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/utils/tcp/service/tcp_client_cubit.dart';
import 'package:smart_hotel_app/utils/tcp/service/tcp_client_state.dart';
import 'package:smart_hotel_app/utils/tcp/service/tcp_connection_status.dart';
class TcpClientDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TcpClientCubit(),
child: _TcpClientDemoView(),
);
}
}
class _TcpClientDemoView extends StatefulWidget {
@override
State<_TcpClientDemoView> createState() => _TcpClientDemoViewState();
}
class _TcpClientDemoViewState extends State<_TcpClientDemoView> {
StreamSubscription<Uint8List>? _dataSub;
final TextEditingController _ipController = TextEditingController(text: '192.168.1.100');
final TextEditingController _hexController = TextEditingController(text: 'AA BB CC DD');
List<String> _logMessages = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _init());
}
void _init() {
final cubit = context.read<TcpClientCubit>();
// 监听接收数据
_dataSub = cubit.onData.listen((data) {
String hex = data
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(' ');
_addLog('<< 接收 [${data.length} bytes] $hex');
});
// 自动配置并连接
cubit.config(address: _ipController.text, port: 8888);
cubit.connect();
}
void _addLog(String msg) {
setState(() => _logMessages.insert(0, '${DateTime.now().toString().substring(11, 19)} $msg'));
}
@override
void dispose() {
_dataSub?.cancel();
_ipController.dispose();
_hexController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TCP 客户端 Demo')),
body: BlocBuilder<TcpClientCubit, TcpClientState>(
builder: (context, state) {
final cubit = context.read<TcpClientCubit>();
final isConn = state.isConnected;
final statusText = {
TcpConnectionStatus.disconnected: '未连接',
TcpConnectionStatus.connecting: '连接中...',
TcpConnectionStatus.connected: '已连接',
TcpConnectionStatus.reconnecting: '重连中...',
TcpConnectionStatus.error: '错误',
}[state.connectionStatus]!;
return Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 状态栏
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: isConn ? Colors.green[50] : Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
isConn ? Icons.wifi : Icons.wifi_off,
color: isConn ? Colors.green : Colors.grey,
),
SizedBox(width: 8),
Text('$statusText ${state.address ?? ""}:${state.port ?? ""}'),
],
),
),
if (state.error != null)
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(state.error!, style: TextStyle(color: Colors.red, fontSize: 12)),
),
SizedBox(height: 16),
// 操作按钮行
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: isConn ? null : () {
cubit.config(address: _ipController.text, port: 8888);
cubit.connect();
},
icon: Icon(Icons.link),
label: Text('连接'),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: isConn ? () => cubit.disconnect() : null,
icon: Icon(Icons.link_off),
label: Text('断开'),
),
),
],
),
SizedBox(height: 12),
// 发送区域
TextField(
controller: _hexController,
decoration: InputDecoration(
labelText: '十六进制数据',
suffixIcon: IconButton(
icon: Icon(Icons.send),
onPressed: isConn ? () async {
bool ok = await cubit.sendHex(_hexController.text);
if (ok) {
_addLog('>> 发送 ${_hexController.text}');
} else {
_addLog('!! 发送失败');
}
} : null,
),
),
),
SizedBox(height: 12),
// 日志区域
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(4),
),
child: _logMessages.isEmpty
? Center(child: Text('暂无日志', style: TextStyle(color: Colors.grey)))
: ListView.builder(
itemCount: _logMessages.length,
itemBuilder: (_, i) => Text(
_logMessages[i],
style: TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
),
),
],
),
);
},
),
);
}
}
```
---
## 二、TCP 服务端(TcpServerCubit)
### 2.1 State 说明
`TcpServerState` 包含以下字段:
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `isRunning` | `bool` | `false` | 服务是否正在运行(监听中) |
| `port` | `int` | `0` | 当前实际监听的端口 |
| `connectedClients` | `int` | `0` | 当前在线的客户端数量 |
| `error` | `String?` | `null` | 最近一次错误信息 |
### 2.2 公共 API 参考
| 方法/属性 | 签名 | 返回值 | 说明 |
|-----------|------|--------|------|
| `setPort()` | `setPort(int port)` | `void` | 设置目标监听端口。必须在 `start()` 之前调用 |
| `start()` | `Future<void>` | — | 启动 TCP 服务端监听。端口被占用时自动递增重试 |
| `stop()` | `Future<void>` | — | 停止服务端,关闭所有客户端连接 |
| `sendToAll()` | `sendToAll(Uint8List data)` | `void` | 向所有已连接客户端群发字节数据 |
| `sendHexToAll()` | `sendHexToAll(String hex)` | `void` | 向所有已连接客户端群发十六进制字符串 |
| `onClientData` | `Stream<Map<String,dynamic>>` (getter) | Stream | 客户端数据流,每条消息包含 client/address/port/data |
| `close()` | `Future<void>` | — | 资源清理:停服 + 关闭数据流 |
### 2.3 onClientData 数据格式
`onClientData` 流中的每条消息为 `Map<String, dynamic>`,包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `client` | `Socket` | 发送数据的客户端 Socket 对象(可用于定向回复) |
| `address` | `String` | 客户端 IP 地址(如 `"192.168.1.50"`) |
| `port` | `int` | 客户端端口号 |
| `data` | `Uint8List` | 接收到的原始字节数据 |
### 2.4 端口占用自动递增
`start()` 方法内置了端口冲突自动处理:
- 绑定指定端口时如果抛出 `already in use` 异常,会自动将端口号 +1 后重试
- 例如:设置端口 `8888` 被占用 → 尝试 `8889` → 再被占用 → 尝试 `8890` ...
- 最终绑定成功的端口号会更新到 `state.port`
> **注意**:此递归没有上限保护。如果在密集端口环境下使用,建议提前确认目标端口可用性。
### 2.5 基本使用
#### 第一步:注入
```dart
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TcpServerCubit(),
child: MyPageBody(),
);
}
}
```
#### 第二步:设置端口并启动
```dart
final cubit = context.read<TcpServerCubit>();
// 设置监听端口
cubit.setPort(8888);
// 启动服务(异步)
await cubit.start();
print('服务运行在端口: ${cubit.state.port}'); // 可能因端口占用而自增
```
#### 第三步:监听客户端数据
```dart
final cubit = context.read<TcpServerCubit>();
// 订阅客户端数据流
cubit.onClientData.listen((msg) {
Socket client = msg['client']; // 可用于单独回复该客户端
String address = msg['address']; // 客户端 IP
int port = msg['port']; // 客户端端口
Uint8List data = msg['data']; // 收到的字节
print('[客户端 $address:$port] 收到 ${data.length} 字节');
// 示例:将收到数据转为 hex 显示
String hex = data
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(' ');
print(' 数据: $hex');
});
```
#### 第四步:向客户端发送数据
```dart
final cubit = context.read<TcpServerCubit>();
// 群发字节数据给所有客户端
cubit.sendToAll(Uint8List.fromList([0xAA, 0xBB, 0xCC]));
// 群发十六进制字符串给所有客户端
cubit.sendHexToAll('AA BB CC DD EE FF');
```
#### 第五步:停止服务
```dart
await context.read<TcpServerCubit>().stop();
```
### 2.6 完整示例
```dart
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/utils/tcp/service/tcp_server_cubit.dart';
import 'package:smart_hotel_app/utils/tcp/service/tcp_server_state.dart';
class TcpServerDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TcpServerCubit(),
child: _TcpServerDemoView(),
);
}
}
class _TcpServerDemoView extends StatefulWidget {
@override
State<_TcpServerDemoView> createState() => _TcpServerDemoViewState();
}
class _TcpServerDemoViewState extends State<_TcpServerDemoView> {
StreamSubscription? _dataSub;
final TextEditingController _portController = TextEditingController(text: '8888');
final TextEditingController _hexController = TextEditingController(text: 'AA BB CC DD');
List<String> _logMessages = [];
void _addLog(String msg) {
setState(() => _logMessages.insert(0, '${DateTime.now().toString().substring(11, 19)} $msg'));
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _init());
}
void _init() {
final cubit = context.read<TcpServerCubit>();
_dataSub = cubit.onClientData.listen((msg) {
final addr = msg['address'];
final data = msg['data'] as Uint8List;
final hex = data.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(' ');
_addLog('[$addr] << ${data.length}B $hex');
});
}
@override
void dispose() {
_dataSub?.cancel();
_portController.dispose();
_hexController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TCP 服务端 Demo')),
body: BlocBuilder<TcpServerCubit, TcpServerState>(
builder: (context, state) {
final cubit = context.read<TcpServerCubit>();
final running = state.isRunning;
return Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// 状态卡片
Container(
width: double.infinity,
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: running ? Colors.blue[50] : Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(running ? Icons.dns : Icons.dns_outlined,
color: running ? Colors.blue : Colors.grey),
SizedBox(width: 8),
Text(running ? '服务运行中' : '服务已停止',
style: TextStyle(fontWeight: FontWeight.bold)),
],
),
SizedBox(height: 4),
Text('监听端口: ${state.port} | 在线客户端: ${state.connectedClients}',
style: TextStyle(fontSize: 13)),
],
),
),
if (state.error != null)
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(state.error!, style: TextStyle(color: Colors.red, fontSize: 12)),
),
SizedBox(height: 16),
// 端口输入 + 启动/停止按钮
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller: _portController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: '端口', border: OutlineInputBorder()),
enabled: !running,
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: running ? null : () {
cubit.setPort(int.parse(_portController.text));
cubit.start();
},
icon: Icon(Icons.play_arrow),
label: Text('启动'),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: running ? () => cubit.stop() : null,
icon: Icon(Icons.stop),
label: Text('停止'),
),
),
],
),
SizedBox(height: 12),
// 群发输入
TextField(
controller: _hexController,
decoration: InputDecoration(
labelText: '群发十六进制数据',
suffixIcon: IconButton(
icon: Icon(Icons.send),
onPressed: running ? () {
cubit.sendHexToAll(_hexController.text);
_addLog('>> 群发 ${_hexController.text}');
} : null,
),
),
),
SizedBox(height: 12),
// 日志
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(4),
),
child: _logMessages.isEmpty
? Center(child: Text('等待客户端连接...', style: TextStyle(color: Colors.grey)))
: ListView.builder(
itemCount: _logMessages.length,
itemBuilder: (_, i) => Text(
_logMessages[i],
style: TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
),
),
],
),
);
},
),
);
}
}
```
---
## 三、高级用法:客户端+服务端同时使用
在需要同时作为客户端和服务端的场景(如 P2P 通信、设备桥接等),可以分别注入两个 Cubit:
```dart
MultiBlocProvider(
providers: [
BlocProvider<TcpClientCubit>(create: (_) => TcpClientCubit()),
BlocProvider<TcpServerCubit>(create: (_) => TcpServerCubit()),
],
child: MyPage(),
)
```
两个 Cubit 完全独立,各自管理自己的 Socket 和状态,互不干扰。
**典型场景:智能酒店中控**
- **作为服务端**:监听局域网内设备的连接请求(设备主动上报状态)
- **作为客户端**:主动向特定设备下发控制指令
```dart
class HotelControlPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
// 服务端:监听设备上报
BlocProvider<TcpServerCubit>(
create: (_) => TcpServerCubit()..setPort(9000)..start(),
),
// 客户端:向设备下发指令
BlocProvider<TcpClientCubit>(
create: (_) => TcpClientCubit(),
),
],
child: _HotelControlView(),
);
}
}
```
---
## 四、在业务 Cubit 中封装使用
推荐将 TCP 通信封装在业务 Cubit 中,而不是直接在 Widget 层操作。这样可以将协议解析、状态管理与 UI 解耦。
### 4.1 业务 Cubit 封装示例
```dart
/// 设备控制业务 Cubit
class DeviceControlCubit extends Cubit<DeviceControlState> {
final TcpClientCubit _tcpClient;
StreamSubscription<Uint8List>? _dataSub;
DeviceControlCubit(this._tcpClient) : super(DeviceControlState.initial()) {
// 订阅 TCP 数据流
_dataSub = _tcpClient.onData.listen(_handleDeviceResponse);
}
/// 连接设备
Future<void> connectDevice(String ip, int port) async {
emit(state.copyWith(status: DeviceStatus.connecting));
_tcpClient.config(address: ip, port: port, timeout: 3000);
bool success = await _tcpClient.connect();
if (success) {
emit(state.copyWith(status: DeviceStatus.connected, deviceIp: ip));
} else {
emit(state.copyWith(status: DeviceStatus.error, error: _tcpClient.state.error));
}
}
/// 发送控制指令
Future<void> sendCommand(DeviceCommand command) async {
if (!_tcpClient.state.isConnected) {
emit(state.copyWith(error: '设备未连接'));
return;
}
emit(state.copyWith(isSending: true));
bool ok = await _tcpClient.sendHex(command.hexCode);
if (!ok) {
emit(state.copyWith(error: '指令发送失败'));
}
emit(state.copyWith(isSending: false));
}
/// 处理设备响应数据
void _handleDeviceResponse(Uint8List data) {
// 在这里实现你的协议解析逻辑
// 示例:假设前两字节是命令类型,后续是数据
if (data.length >= 2) {
final cmdType = data[0];
switch (cmdType) {
case 0x01: // 设备状态上报
emit(state.copyWith(deviceOnline: data[1] == 0x01));
break;
case 0x02: // 温度数据
final temp = (data[1] << 8) | data[2];
emit(state.copyWith(temperature: temp / 10.0));
break;
// ... 其他命令类型
}
}
}
/// 断开连接
void disconnect() {
_tcpClient.disconnect();
emit(state.copyWith(status: DeviceStatus.disconnected));
}
@override
Future<void> close() {
_dataSub?.cancel();
_tcpClient.disconnect();
return super.close();
}
}
```
### 4.2 注入方式
```dart
// 方式一:RepositoryProvider 注入 TCP Cubit,再传给业务 Cubit
RepositoryProvider<TcpClientCubit>(
create: (_) => TcpClientCubit(),
child: BlocProvider(
create: (context) => DeviceControlCubit(context.read<TcpClientCubit>()),
child: DeviceControlPage(),
),
)
// 方式二:直接在 BlocProvider.create 中创建
BlocProvider(
create: (context) {
final tcp = TcpClientCubit();
return DeviceControlCubit(tcp); // 由 DeviceControlCubit 管理 tcp 生命周期
},
child: DeviceControlPage(),
)
```
---
## 五、生命周期与资源清理
### 5.1 清理时序图
```
Widget 销毁 (dispose)
BlocProvider 自动调用 cubit.close()
├── TcpClientCubit.close()
│ ├── disconnect() → 取消重连Timer + 心跳Timer + 关闭Socket
│ └── _dataController.close() → 关闭数据流
├── TcpServerCubit.close()
│ ├── stop() → 关闭所有客户端Socket + 关闭ServerSocket
│ └── _dataController.close() → 关闭数据流
└── super.close() → Cubit 自身销毁
```
### 5.2 各场景下的清理策略
| 场景 | 推荐做法 | 说明 |
|------|---------|------|
| 页面跳转后不再需要 | 不做任何处理 | `BlocProvider` 自动调用 `close()`,完整清理 |
| 页面重建但保持连接 | 使用全局/顶层 `BlocProvider` | 不要放在页面级 `BlocProvider` 中 |
| 手动临时断开 | 调用 `cubit.disconnect()` | Cubit 保留,可以再次 `connect()` |
| 应用退出 | 不需要特殊处理 | Flutter 会依次销毁 Widget 树 |
### 5.3 StreamSubscription 管理
**务必在 `dispose()` 中取消订阅**,否则会导致内存泄漏:
```dart
// ✅ 正确做法
class _MyWidgetState extends State<MyWidget> {
StreamSubscription<Uint8List>? _sub;
@override
void initState() {
super.initState();
_sub = context.read<TcpClientCubit>().onData.listen(/* ... */);
}
@override
void dispose() {
_sub?.cancel(); // 必须!
super.dispose();
}
}
// ❌ 错误做法:忘记 cancel
@override
void dispose() {
// _sub 未取消 → 内存泄漏
super.dispose();
}
```
---
## 六、常见问题排查(FAQ)
### Q1: connect() 返回 false 但没有 error 信息?
可能原因:
- 地址/端口未配置(需先调用 `config()`
- 已有一个连接正在进行中(并发防护机制拦截)
解决方法:
```dart
if (!await cubit.connect()) {
if (cubit.state.address == null) {
print('请先调用 config() 配置地址和端口');
} else if (cubit.state.connectionStatus == TcpConnectionStatus.connecting) {
print('连接进行中,请勿重复调用');
} else {
print('连接失败: ${cubit.state.error}');
}
}
```
### Q2: 发送数据对方没收到?
检查项:
1. `state.isConnected` 是否为 `true`
2. `sendHex()` / `sendString()` 返回值是否为 `true`
3. 对方是否还在连接状态
4. 网络防火墙是否放行了对应端口
### Q3: 如何知道当前连接的是哪台服务器?
```dart
// 从 state 中读取
final addr = cubit.state.address; // "192.168.1.100"
final port = cubit.state.port; // 8888
```
### Q4: 服务端如何识别不同客户端?
`onClientData` 流中每条消息都携带了 `client`(Socket 对象)、`address``port`,可用于区分:
```dart
cubit.onClientData.listen((msg) {
final key = '${msg['address']}:${msg['port']}'; // 唯一标识一个客户端
print('来自 $key 的消息');
});
```
### Q5: 心跳包内容可以自定义吗?
目前心跳固定发送 `00 00 00 00`。如需修改,直接编辑 `tcp_client_cubit.dart` 中的 `_startHeartbeat()` 方法即可。
---
## 七、从旧 GetX 代码迁移
| 旧 API(GetX) | 新 API(Cubit) | 备注 |
|---|---|---|
| `Get.put(TcpClientService())` | `BlocProvider(create: (_) => TcpClientCubit())` | 依赖注入方式变更 |
| `tcpClient.connectionState.value` | `state.connectionStatus` | 枚举名称略有调整 |
| `ever(tcpClient.connectionState, cb)` | `BlocListener<TcpClientCubit, TcpClientState>(listener: cb)` | 副作用监听 |
| `Obx(() => Text(...))` | `BlocBuilder<TcpClientCubit, TcpClientState>(builder: ...)` | 响应式 UI 构建 |
| `Get.put(TcpServerService())` | `BlocProvider(create: (_) => TcpServerCubit())` | 同上 |
| `tcpServer.connectedClients.value` | `state.connectedClients` | 直接读取 state |
| `TcpConnectionEnum.connected` | `TcpConnectionStatus.connected` | 枚举类名变更 |
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment