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
This diff is collapsed.
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