底層 Socket 實作(教學用途)
<?php
/**
* 使用 fsockopen 發送原始 HTTP POST 請求
* @param string $url 目標 URL
* @param array $data POST 欄位陣列
* @param string $referer 可選 Referer
* @return array ['status'=>'ok'|'err', 'header'=>'', 'content'=>'', 'error'=>'']
*/
function socketPostRequest($url, array $data, $referer = '') {
// 1. 解析 URL
$parsed = parse_url($url);
if ($parsed['scheme'] !== 'http') {
return ['status' => 'err', 'error' => '僅支援 HTTP'];
}
$host = $parsed['host'];
$path = $parsed['path'] ?? '/';
if (!empty($parsed['query'])) $path .= '?' . $parsed['query'];
$port = $parsed['port'] ?? 80;
// 2. 建構 POST 資料
$postData = http_build_query($data);
// 3. 開啟 Socket 連線
$fp = fsockopen($host, $port, $errno, $errstr, 30);
if (!$fp) {
return ['status' => 'err', 'error' => "$errstr ($errno)"];
}
// 4. 發送 HTTP 請求
$request = "POST $path HTTP/1.1\r\n";
$request .= "Host: $host\r\n";
if ($referer) $request .= "Referer: $referer\r\n";
$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
$request .= "Content-Length: " . strlen($postData) . "\r\n";
$request .= "Connection: close\r\n\r\n";
$request .= $postData;
fwrite($fp, $request);
// 5. 讀取回應
$response = '';
while (!feof($fp)) {
$response .= fgets($fp, 1024);
}
fclose($fp);
// 6. 分離 Header 與 Body
list($header, $content) = explode("\r\n\r\n", $response, 2) + ['', ''];
return [
'status' => 'ok',
'header' => $header,
'content' => $content
];
}
// 使用範例
$result = socketPostRequest('https://httpbin.org/post', [
'username' => 'testuser',
'action' => 'login',
'timestamp' => time()
]);
if ($result['status'] === 'ok') {
echo "Header:\n{$result['header']}\n\n";
echo "Body:\n{$result['content']}\n";
} else {
echo "錯誤:{$result['error']}\n";
}
?>
現代最佳實作:cURL / Guzzle(生產環境)
<?php
// cURL 版本
function curlPostRequest($url, array $data, array $options = []) {
$ch = curl_init($url);
curl_setopt_array($ch, array_merge([
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_SSL_VERIFYPEER => true, // 生產環境必須驗證
CURLOPT_HTTPHEADER => [
'User-Agent: MyApp/1.0',
'Accept: application/json',
],
], $options));
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($errno) {
return ['status' => 'err', 'error' => $error, 'errno' => $errno];
}
return [
'status' => 'ok',
'http_code' => $info['http_code'],
'content' => $response,
'header_size' => $info['header_size'],
];
}
// Guzzle HTTP Client(推薦現代框架專案)
// composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client([
'timeout' => 30,
'headers' => ['User-Agent' => 'MyApp/1.0'],
]);
try {
$response = $client->post('https://api.example.com/endpoint', [
'form_params' => ['key' => 'value'],
'json' => ['nested' => ['data' => 123]], // 自動 JSON 編碼
]);
$data = json_decode($response->getBody()->getContents(), true);
echo "狀態碼: {$response->getStatusCode()}\n";
print_r($data);
} catch (RequestException $e) {
echo "請求失敗: {$e->getMessage()}\n";
if ($e->hasResponse()) {
echo "回應: " . $e->getResponse()->getBody() . "\n";
}
}
?>
關鍵差異比較
| 特性 | fsockopen | cURL | Guzzle |
| HTTPS 支援 | 需手動實作 SSL | 原生支援 | 原生支援 |
| 錯誤處理 | 手動解析 | 錯誤碼/訊息 | 例外物件 |
| 重導向 | 手動處理 | 自動跟隨 | 自動跟隨 |
| Cookie 管理 | 手動 | Cookie Jar | Cookie Jar |
| 並發請求 | 不支援 | curl_multi | Promise/Pool |
| Middleware/Plugin | 無 | 無 | 豐富生態 |
| 適用場景 | 教學、極簡環境 | 一般專案 | 現代框架專案 |
安全性檢查清單
- ✅ 生產環境強制
CURLOPT_SSL_VERIFYPEER => true
- ✅ 設定合理
CURLOPT_TIMEOUT 避免阻塞
- ✅ 驗證回應 HTTP 狀態碼(2xx 成功)
- ✅ 敏感資料(Token、密碼)不記錄日誌
- ✅ 使用
http_build_query 正確編碼 POST 資料
- ✅ 設定
User-Agent 識別應用身分