# 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` | 枚举类名变更 |
