Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
S
smart_hotel_app
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张宏
smart_hotel_app
Commits
9d09840c
Commit
9d09840c
authored
Jun 06, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
tcp
parent
a1177508
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1452 additions
and
0 deletions
+1452
-0
index.dart
lib/utils/tcp/index.dart
+3
-0
index.dart
lib/utils/tcp/service/index.dart
+6
-0
tcp_client_cubit.dart
lib/utils/tcp/service/tcp_client_cubit.dart
+186
-0
tcp_client_state.dart
lib/utils/tcp/service/tcp_client_state.dart
+37
-0
tcp_connection_status.dart
lib/utils/tcp/service/tcp_connection_status.dart
+18
-0
tcp_server_cubit.dart
lib/utils/tcp/service/tcp_server_cubit.dart
+107
-0
tcp_server_state.dart
lib/utils/tcp/service/tcp_server_state.dart
+34
-0
tcp使用手册.md
lib/utils/tcp/tcp使用手册.md
+1061
-0
No files found.
lib/utils/tcp/index.dart
0 → 100644
View file @
9d09840c
library
tcp
;
export
'service/index.dart'
;
\ No newline at end of file
lib/utils/tcp/service/index.dart
0 → 100644
View file @
9d09840c
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
lib/utils/tcp/service/tcp_client_cubit.dart
0 → 100644
View file @
9d09840c
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
lib/utils/tcp/service/tcp_client_state.dart
0 → 100644
View file @
9d09840c
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
lib/utils/tcp/service/tcp_connection_status.dart
0 → 100644
View file @
9d09840c
/// TCP 连接状态枚举
enum
TcpConnectionStatus
{
/// 未连接
disconnected
,
/// 连接中
connecting
,
/// 已连接
connected
,
/// 重连中
reconnecting
,
/// 错误
error
,
}
\ No newline at end of file
lib/utils/tcp/service/tcp_server_cubit.dart
0 → 100644
View file @
9d09840c
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
lib/utils/tcp/service/tcp_server_state.dart
0 → 100644
View file @
9d09840c
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
lib/utils/tcp/tcp使用手册.md
0 → 100644
View file @
9d09840c
# 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`
| 枚举类名变更 |
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment