feat: 实现TCP Server
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace app\net;
|
||||
|
||||
use app\Exceptions\UnsupportedPacketException;
|
||||
|
||||
|
||||
class Packet
|
||||
{
|
||||
/**
|
||||
* @var string 原始报文字节数据
|
||||
*/
|
||||
public string $rawBytes {
|
||||
get {
|
||||
return $this->rawBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string 原始报文的十六进制字符串
|
||||
*/
|
||||
public string $hexString {
|
||||
get {
|
||||
return $this->hexString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var int 十六进制字符串长度
|
||||
*/
|
||||
public int $hexLength {
|
||||
get {
|
||||
return $this->hexLength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var PacketType 报文类型 0:未知 1:有线读卡器 2:无线读卡器 3:电流采集器 4:新版电流采集器
|
||||
*/
|
||||
public PacketType $hexType = PacketType::UNKNOWN {
|
||||
get {
|
||||
return $this->hexType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var bool 是否匹配成功
|
||||
*/
|
||||
public bool $isMatched = false {
|
||||
get {
|
||||
return $this->isMatched;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 卡号
|
||||
*/
|
||||
public string $card = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->card;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 读卡器编号
|
||||
*/
|
||||
public string $reader = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->reader;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 检测设备/站号
|
||||
*/
|
||||
public string $detectionDevice = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->detectionDevice;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 网关编号
|
||||
*/
|
||||
public string $gateway = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->gateway;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 通道1数据
|
||||
*/
|
||||
public string $channel1 = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->channel1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 通道2数据
|
||||
*/
|
||||
public string $channel2 = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->channel2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 通道3数据
|
||||
*/
|
||||
public string $channel3 = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->channel3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 通道4数据
|
||||
*/
|
||||
public string $channel4 = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->channel4;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 通道5数据
|
||||
*/
|
||||
public string $channel5 = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->channel5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var string 通道6数据
|
||||
*/
|
||||
public string $channel6 = '' {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->channel6;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnsupportedPacketException 当前报文未解析成功时抛出异常
|
||||
* @var float 电池电量
|
||||
*/
|
||||
public float $batteryLevel = 0.0 {
|
||||
get {
|
||||
if (!$this->isMatched) throw new UnsupportedPacketException();
|
||||
return $this->batteryLevel;
|
||||
}
|
||||
}
|
||||
|
||||
public string $rawText {
|
||||
get => $this->rawText;
|
||||
}
|
||||
|
||||
public int $length{
|
||||
get => strlen($this->rawBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param string $rawBytes 原始报文字节数据
|
||||
* @param array $parsedData 解析后的属性数据(可选)
|
||||
*/
|
||||
public function __construct(string $rawBytes, array $parsedData = [])
|
||||
{
|
||||
// 初始化基础属性
|
||||
$this->rawBytes = $rawBytes;
|
||||
$this->hexString = strtoupper(bin2hex($rawBytes));
|
||||
$this->hexLength = strlen($this->hexString);
|
||||
|
||||
// 合并解析后的属性
|
||||
if (!empty($parsedData)) {
|
||||
foreach ($parsedData as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
// 处理报文类型枚举
|
||||
if ($key === 'hexType' && is_int($value)) {
|
||||
$this->hexType = PacketType::fromValue($value);
|
||||
continue;
|
||||
}
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 对外暴露的读取方法 ==========
|
||||
|
||||
public function getRawText(): string
|
||||
{
|
||||
return $this->hexString;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有解析结果(数组形式)
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'hex' => $this->hexString,
|
||||
'len' => $this->hexLength,
|
||||
'hextype' => $this->hexType,
|
||||
'match' => $this->isMatched ? 1 : 0,
|
||||
'card' => $this->card,
|
||||
'reader' => $this->reader,
|
||||
'detection_device' => $this->detectionDevice,
|
||||
'gateway' => $this->gateway,
|
||||
'channel_1' => $this->channel1,
|
||||
'channel_2' => $this->channel2,
|
||||
'channel_3' => $this->channel3,
|
||||
'channel_4' => $this->channel4,
|
||||
'channel_5' => $this->channel5,
|
||||
'channel_6' => $this->channel6,
|
||||
'battery_level' => $this->batteryLevel
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace app\net;
|
||||
|
||||
use Workerman\Connection\TcpConnection;
|
||||
|
||||
class PacketContext
|
||||
{
|
||||
public $connections {
|
||||
get => $this->connections;
|
||||
}
|
||||
public TcpConnection $connection {
|
||||
get => $this->connection;
|
||||
}
|
||||
public Packet $packet {
|
||||
get => $this->packet;
|
||||
}
|
||||
|
||||
public string $ip {
|
||||
get => $this->connection->getRemoteIp();
|
||||
}
|
||||
|
||||
public int $port {
|
||||
get => $this->connection->getRemotePort();
|
||||
}
|
||||
/**
|
||||
* @var int 上下文创建时的时间
|
||||
*/
|
||||
public int $createTime {
|
||||
get {
|
||||
return time();
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(array $connections,TcpConnection $connection, Packet $packet)
|
||||
{
|
||||
$this->connections = $connections;
|
||||
$this->connection = $connection;
|
||||
$this->packet = $packet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace app\net;
|
||||
|
||||
/**
|
||||
* 报文类型枚举
|
||||
* 对应原hexType的数字值:0-未知,1-有线读卡器,2-无线读卡器,3-电流采集器,4-新版电流采集器
|
||||
*/
|
||||
enum PacketType: int
|
||||
{
|
||||
// 未知类型(默认)
|
||||
case UNKNOWN = 0;
|
||||
// 有线读卡器
|
||||
case WIRED_READER = 1;
|
||||
// 无线读卡器
|
||||
case WIRELESS_READER = 2;
|
||||
// 电流采集器
|
||||
case CURRENT_COLLECTOR = 3;
|
||||
// 新版电流采集器
|
||||
case NEW_CURRENT_COLLECTOR = 4;
|
||||
|
||||
/**
|
||||
* 从整数值获取枚举实例(兼容原有数字逻辑)
|
||||
* @param int $value
|
||||
* @return self
|
||||
*/
|
||||
public static function fromValue(int $value): self
|
||||
{
|
||||
return match ($value) {
|
||||
1 => self::WIRED_READER,
|
||||
2 => self::WIRELESS_READER,
|
||||
3 => self::CURRENT_COLLECTOR,
|
||||
4 => self::NEW_CURRENT_COLLECTOR,
|
||||
default => self::UNKNOWN,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取友好的类型名称(用于日志/展示)
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::UNKNOWN => '未知报文',
|
||||
self::WIRED_READER => '有线读卡器报文',
|
||||
self::WIRELESS_READER => '无线读卡器报文',
|
||||
self::CURRENT_COLLECTOR => '电流采集器报文',
|
||||
self::NEW_CURRENT_COLLECTOR => '新版电流采集器报文',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace app\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 不支持的报文异常:当报文没有匹配到任何解析器时抛出
|
||||
*/
|
||||
class UnsupportedPacketException extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* 构造函数(可选:添加自定义上下文)
|
||||
* @param string $message 异常消息
|
||||
* @param int $code 异常码
|
||||
* @param \Throwable|null $previous 前一个异常
|
||||
*/
|
||||
public function __construct(
|
||||
string $message = 'Unsupported packet type: no parser matched',
|
||||
int $code = 400,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
// 可选:添加获取报文相关信息的方法,方便调试
|
||||
public static function fromHexString(string $hexString): self
|
||||
{
|
||||
$len = strlen($hexString);
|
||||
return new self(
|
||||
sprintf('Unsupported packet (hex length: %d, hex string: %s)', $len, $hexString),
|
||||
400
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace app\net\parsers;
|
||||
|
||||
|
||||
/**
|
||||
* 电流采集器报文解析器(长度78)
|
||||
*/
|
||||
class CurrentCollectorParser implements PacketParserInterface
|
||||
{
|
||||
public function supports(string $hexString): bool
|
||||
{
|
||||
return strlen($hexString) === 78;
|
||||
}
|
||||
|
||||
public function parse(string $hexString): array
|
||||
{
|
||||
$pattern = '/^BBBB([0-9A-F]{12})([0-9A-F]{10})([0-9A-F]{10})(24[0-9A-F]{8})(01[0-9A-F]{2})(02[0-9A-F]{2})(03[0-9A-F]{2})(04[0-9A-F]{2})(05[0-9A-F]{2})(06[0-9A-F]{2})([0-9A-F]{8})$/';
|
||||
if (preg_match($pattern, $hexString, $matches)) {
|
||||
return [
|
||||
'hexType' => 3,
|
||||
'isMatched' => true,
|
||||
'card' => $matches[3],
|
||||
'reader' => $matches[2],
|
||||
'detectionDevice' => $matches[4],
|
||||
'channel1' => $matches[5],
|
||||
'channel2' => $matches[6],
|
||||
'channel3' => $matches[7],
|
||||
'channel4' => $matches[8],
|
||||
'channel5' => $matches[9],
|
||||
'channel6' => $matches[10],
|
||||
];
|
||||
}
|
||||
return ['isMatched' => false];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace app\net\parsers;
|
||||
|
||||
|
||||
/**
|
||||
* 新版电流采集器报文解析器(长度38)
|
||||
*/
|
||||
class NewCurrentCollectorParser implements PacketParserInterface
|
||||
{
|
||||
public function supports(string $hexString): bool
|
||||
{
|
||||
return strlen($hexString) === 38;
|
||||
}
|
||||
|
||||
public function parse(string $hexString): array
|
||||
{
|
||||
return [
|
||||
'hexType' => 4,
|
||||
'isMatched' => true,
|
||||
'detectionDevice' => substr($hexString, 16, 2),
|
||||
'channel1' => substr($hexString, 24, 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace app\net\parsers;
|
||||
|
||||
use app\net\Packet;
|
||||
|
||||
/**
|
||||
* 报文解析器工厂(单例模式)
|
||||
*/
|
||||
class PacketParserFactory
|
||||
{
|
||||
/**
|
||||
* @var self|null 单例实例
|
||||
*/
|
||||
private static ?self $instance = null;
|
||||
|
||||
/**
|
||||
* @var PacketParserInterface[] 解析器列表
|
||||
*/
|
||||
private array $parsers = [];
|
||||
|
||||
/**
|
||||
* 私有化构造函数(禁止外部 new)
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
$this->registerDefaultParsers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 防止克隆(保障单例唯一性)
|
||||
*/
|
||||
private function __clone() {}
|
||||
|
||||
/**
|
||||
* 防止反序列化(保障单例唯一性)
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \RuntimeException('Cannot unserialize singleton ' . self::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例(懒汉式:首次调用才初始化)
|
||||
* @return self
|
||||
*/
|
||||
public static function getInstance(): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认解析器(抽离出来,便于扩展)
|
||||
*/
|
||||
private function registerDefaultParsers(): void
|
||||
{
|
||||
$this->registerParser(new WiredReaderParser());
|
||||
$this->registerParser(new WirelessReaderParser());
|
||||
$this->registerParser(new CurrentCollectorParser());
|
||||
$this->registerParser(new NewCurrentCollectorParser());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册解析器(支持动态扩展)
|
||||
* @param PacketParserInterface $parser
|
||||
* @return void
|
||||
*/
|
||||
public function registerParser(PacketParserInterface $parser): void
|
||||
{
|
||||
$this->parsers[] = $parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并解析Packet对象
|
||||
* @param string $rawBytes 原始报文字节数据
|
||||
* @return Packet
|
||||
*/
|
||||
public function create(string $rawBytes): Packet
|
||||
{
|
||||
$hexString = strtoupper(bin2hex($rawBytes));
|
||||
$parsedData = null;
|
||||
|
||||
// 遍历解析器找匹配的
|
||||
foreach ($this->parsers as $parser) {
|
||||
if ($parser->supports($hexString)) {
|
||||
$parsedData = $parser->parse($hexString);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Packet($rawBytes, $parsedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并解析Packet对象
|
||||
* @param string $rawBytes 原始报文字节数据
|
||||
* @return Packet
|
||||
*/
|
||||
public static function new(string $rawBytes): Packet
|
||||
{
|
||||
return self::getInstance()->create($rawBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace app\net\parsers;
|
||||
|
||||
/**
|
||||
* 报文解析器接口
|
||||
*/
|
||||
interface PacketParserInterface
|
||||
{
|
||||
/**
|
||||
* 判断当前解析器是否支持该报文
|
||||
* @param string $hexString 十六进制字符串
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(string $hexString): bool;
|
||||
|
||||
/**
|
||||
* 解析报文并返回填充好的属性数组
|
||||
* @param string $hexString 十六进制字符串
|
||||
* @return array 解析后的属性数组(key为属性名,value为属性值)
|
||||
*/
|
||||
public function parse(string $hexString): array;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace app\net\parsers;
|
||||
|
||||
use app\net\parsers\PacketParserInterface;
|
||||
|
||||
/**
|
||||
* 有线读卡器报文解析器(长度20)
|
||||
*/
|
||||
class WiredReaderParser implements PacketParserInterface
|
||||
{
|
||||
public function supports(string $hexString): bool
|
||||
{
|
||||
return strlen($hexString) === 20;
|
||||
}
|
||||
|
||||
public function parse(string $hexString): array
|
||||
{
|
||||
$pattern = '/(09[a-fA-F0-9]{8})([a-fA-F0-9]{10})/';
|
||||
if (preg_match($pattern, $hexString, $matches)) {
|
||||
return [
|
||||
'hexType' => 1,
|
||||
'isMatched' => true,
|
||||
'reader' => $matches[1],
|
||||
'card' => $matches[2],
|
||||
];
|
||||
}
|
||||
return ['isMatched' => false];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\net\parsers;
|
||||
|
||||
|
||||
/**
|
||||
* 无线读卡器报文解析器(长度56)
|
||||
*/
|
||||
class WirelessReaderParser implements PacketParserInterface
|
||||
{
|
||||
public function supports(string $hexString): bool
|
||||
{
|
||||
return strlen($hexString) === 56;
|
||||
}
|
||||
|
||||
public function parse(string $hexString): array
|
||||
{
|
||||
$pattern = '/^5A(.{12})(.{26})(.{12})(.{2})A5$/';
|
||||
if (preg_match($pattern, $hexString, $matches)) {
|
||||
return [
|
||||
'hexType' => 2,
|
||||
'isMatched' => true,
|
||||
'gateway' => $matches[1],
|
||||
'card' => $matches[2],
|
||||
'reader' => $matches[3],
|
||||
'batteryLevel' => (hexdec($matches[4]) + 200) / 100,
|
||||
];
|
||||
}
|
||||
return ['isMatched' => false];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user