安全考量:防止 SSRF(服務端請求偽造)、惡意掃描內網、發送垃圾郵件、DDoS 反射攻擊。常見禁用方式:
; php.ini
disable_functions = fsockopen, pfsockopen, stream_socket_client, exec, shell_exec, ...
; 或
allow_url_fopen = Off
若僅禁用 fsockopen,pfsockopen 參數完全兼容:
<?php
// 直接替換函數名
$fp = pfsockopen($host, 80, $errno, $errstr, 30);
// 後續 fread/fwrite/fclose 操作完全一致
?>
注意:持久連接會複用進程級 Socket,高併發下需注意連接洩漏。
PHP 5.0+ 內建,參數形式不同:
<?php
// fsockopen($host, $port, $errno, $errstr, $timeout)
// stream_socket_client($remote_socket, $errno, $errstr, $timeout, $flags, $context)
$remote = "tcp://{$host}:80"; // 協議前綴 + host:port
$fp = stream_socket_client($remote, $errno, $errstr, 30, STREAM_CLIENT_CONNECT);
// 支持 SSL/TLS
$fp = stream_socket_client("ssl://{$host}:443", $errno, $errstr, 30);
// 支持上下文選項(超時、證書驗證等)
$context = stream_context_create([
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
'socket' => ['bindto' => '192.168.1.100:0'] // 指定源 IP
]);
$fp = stream_socket_client($remote, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
?>
極老環境(PHP 4)無 stream_socket_client,需使用 sockets 擴展:
<?php
function compat_fsockopen($host, $port, &$errno, &$errstr, $timeout = 30) {
$ip = gethostbyname($host);
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false) {
$errno = socket_last_error();
$errstr = socket_strerror($errno);
return false;
}
// 非阻塞連接 + select 等待
socket_set_nonblock($socket);
$result = @socket_connect($socket, $ip, $port);
if ($result === false) {
$errno = socket_last_error($socket);
if ($errno !== EINPROGRESS && $errno !== EWOULDBLOCK) {
$errstr = socket_strerror($errno);
socket_close($socket);
return false;
}
}
// 等待連接完成
$write = [$socket];
$null = null;
if (socket_select($null, $write, $null, $timeout) > 0) {
$errno = 0;
return $socket; // 返回 Socket 資源
}
$errno = ETIMEDOUT;
$errstr = 'Connection timed out';
socket_close($socket);
return false;
}
// 使用時需替換文件操作函數
$fp = compat_fsockopen($host, 80, $errno, $errstr, 30);
if ($fp) {
socket_write($fp, $request); // 替代 fwrite
$response = socket_read($fp, 8192); // 替代 fread
socket_close($fp); // 替代 fclose
}
?>
若業務僅需 HTTP 通信,cURL 更健壯且通常未被禁用:
<?php
function curl_http($url, $data = '', $method = 'GET') {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false, // 僅測試環境
CURLOPT_CUSTOMREQUEST => $method,
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
return $response ?: false;
}
?>
<?php
// composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
$client = new Client(['timeout' => 30]);
$response = $client->request('POST', $url, ['form_params' => $data]);
$body = $response->getBody()->getContents();
?>
| 原代碼 | 替換後 | 注意事項 |
|---|---|---|
| fsockopen(host, port, ...) | stream_socket_client("tcp://host:port", ...) | 參數順序變化,增加協議前綴 |
| fwrite($fp, $data) | fwrite($fp, $data) / socket_write($fp, $data) | Socket 資源需用 socket_* 系列 |
| fread($fp, $len) | fread($fp, $len) / socket_read($fp, $len) | 同上 |
| fclose($fp) | fclose($fp) / socket_close($fp) | 同上 |
| feof($fp) | feof($fp) / socket_last_error($fp) !== 0 | Socket 無 EOF 概念,需檢查錯誤碼 |
fsockopen → stream_socket_client,補全協議前綴compat_fsockopen() 兼容層遇到 disable_functions 限制,優先考慮業務架構調整(如引入消息隊列、服務網格),而非單純繞過限制。