基於飛信 (Fetion) 通訊協議的逆向工程實作,純 PHP Socket 客戶端,支援登入、好友列表、發送/接收訊息、狀態維持。適用於伺服器端自動化通知、機器人開發、整合現有系統。
<?php
namespace Fetion;
class Client {
private $socket;
private $config = [
'host' => 'fetu.fetion.com.cn',
'port' => 5222,
'timeout' => 30,
'heartbeat_interval' => 60
];
private $state = 'DISCONNECTED'; // DISCONNECTED, CONNECTING, CHALLENGE, AUTH, READY
private $sid = 1; // 序號生成器
private $buffer = '';
private $callbacks = [];
private $user = [];
public function __construct(array $config = []) {
$this->config = array_merge($this->config, $config);
}
// 連線並啟動事件循環
public function connect($mobile, $password) {
$this->socket = stream_socket_client(
"tcp://{$this->config['host']}:{$this->config['port']}",
$errno, $errstr, $this->config['timeout'],
STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT
);
if (!$this->socket) throw new \Exception("連線失敗: $errstr");
stream_set_blocking($this->socket, 0); // 非阻塞
stream_set_timeout($this->socket, $this->config['timeout']);
$this->state = 'CONNECTING';
$this->sendPacket('SIP', ['M' => 'REGISTER', 'U' => $mobile]);
$this->eventLoop();
}
// 非阻塞事件循環
private function eventLoop() {
$read = [$this->socket];
$write = $this->hasPendingPackets() ? [$this->socket] : [];
$except = [$this->socket];
while ($this->state !== 'DISCONNECTED') {
$r = $read; $w = $write; $e = $except;
$n = @stream_select($r, $w, $e, 1, 0); // 1秒輪詢
if ($n === false) break; // 錯誤
if (in_array($this->socket, $r)) $this->handleRead();
if (in_array($this->socket, $w)) $this->handleWrite();
if (in_array($this->socket, $e)) $this->handleError();
$this->checkHeartbeat();
}
}
// 封包發送 (長度前綴 + XML)
private function sendPacket($cmd, $data) {
$seq = $this->sid++;
$xml = $this->buildXml($cmd, $data, $seq);
$len = strlen($xml);
$packet = pack('N', $len) . $xml; // 4字節大端長度
// 非阻塞寫入佇列
$this->writeBuffer .= $packet;
}
// 讀取處理 (處理粘包/拆包)
private function handleRead() {
$data = fread($this->socket, 8192);
if ($data === '' || $data === false) {
$this->disconnect('連線關閉');
return;
}
$this->buffer .= $data;
$this->parsePackets();
}
// 封包解析
private function parsePackets() {
while (strlen($this->buffer) >= 4) {
$len = unpack('N', substr($this->buffer, 0, 4))[1];
if (strlen($this->buffer) < 4 + $len) break; // 不完整封包
$xml = substr($this->buffer, 4, $len);
$this->buffer = substr($this->buffer, 4 + $len);
$this->handlePacket($xml);
}
}
// 狀態機處理
private function handlePacket($xml) {
$packet = simplexml_load_string($xml);
$cmd = (string)$packet['M'];
$seq = (int)$packet['S'];
switch ($this->state) {
case 'CONNECTING':
if ($cmd === 'SIP/2.0 401 Unauthorized') {
$this->state = 'CHALLENGE';
$this->handleChallenge($packet);
}
break;
case 'CHALLENGE':
if ($cmd === 'SIP/2.0 200 OK') {
$this->state = 'AUTH';
$this->doAuth();
}
break;
case 'AUTH':
if ($cmd === 'SIP/2.0 200 OK') {
$this->state = 'READY';
$this->onReady();
}
break;
case 'READY':
$this->dispatchEvent($cmd, $packet);
break;
}
}
// 事件回調註冊
public function on($event, callable $callback) {
$this->callbacks[$event][] = $callback;
}
private function dispatchEvent($cmd, $packet) {
foreach ($this->callbacks[$cmd] ?? [] as $cb) {
$cb($packet, $this);
}
}
// 公開 API
public function sendMessage($to, $content) {
if ($this->state !== 'READY') throw new \Exception('未就緒');
$this->sendPacket('SIP', [
'M' => 'MESSAGE',
'T' => $to,
'C' => base64_encode($content)
]);
}
}
// 使用範例
$client = new Fetion\Client();
$client->on('MESSAGE', function($packet, $client) {
$from = (string)$packet['F'];
$content = base64_decode((string)$packet['C']);
echo "收到來自 $from: $content\n";
// 自動回覆
if (strpos($content, '你好') !== false) {
$client->sendMessage($from, '你好!我是 PHP 飛信機器人。');
}
});
try {
$client->connect('13800138000', 'password123');
} catch (Exception $e) {
echo "錯誤: " . $e->getMessage();
}
| 難點 | 解決方案 |
|---|---|
| 非阻塞 I/O 處理 | stream_select() 多路複用 + 讀寫緩衝區管理 |
| 粘包/拆包 | 長度前綴協議 + 緩衝區累積解析 |
| 長連線維持 | 定時心跳封包 + 讀取逾時檢測 + 自動重連機制 |
| 加密握手 | RSA 公鑰加密密碼 + AES 會話金鑰衍生 (需 OpenSSL 擴展) |
| 協議變更適應 | 模組化封包建構/解析,版本適配器模式 |
本實作僅供學習協議設計、Socket 程式設計、狀態機模式等技術研究。實際使用飛信服務請遵守騰訊服務條款,勿用於垃圾訊息、騷擾、帳號盜取等違法行為。官方 API 應優先選擇。