首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 純 Socket 實現 HTTP 請求:底層原理與生產級 httpRequest 函數詳解

     編輯:PHP 綜合知識

前言

在無 cURL 擴展、或需精細控制 TCP 層行為(如自定義協議、代理鏈、非阻塞 I/O)時,基於 fsockopen() / stream_socket_client() 手寫 HTTP 客戶端是必備技能。本文解析 HTTP/1.0 請求構造流程,並提供一個健壯、支援 GET/POST、超時、Header 自定義、Cookie、Socket 級 IP 綁定的 httpRequest() 函數。

HTTP/1.0 請求報文結構

GET /path?query=1 HTTP/1.0\r\n
Host: example.com\r\n
User-Agent: MyClient/1.0\r\n
Accept: */*\r\n
Connection: Close\r\n
\r\n

POST 請求需額外包含 Content-Type、Content-Length 及實體體。

核心函數:httpRequest() 完整實現

<?php
/**
 * 底層 Socket 發送 HTTP 請求(支援 GET/POST、Header、Cookie、超時、IP 綁定)
 *
 * @param string $url        完整 URL
 * @param array|string $post POST 數據(陣列自動 urlencode,字符串直接發送)
 * @param string $method     'GET' 或 'POST'
 * @param int    $limit      最大讀取字節數(0=無限制)
 * @param bool   $returnHeader 是否返回關聯數組 ['header'=>..., 'body'=>...]
 * @param string $cookie     Cookie 字符串
 * @param bool   $bySocket   強制使用 Socket(保留參數,兼容舊版)
 * @param string $ip         指定綁定的本地 IP / 目標服務器 IP
 * @param int    $timeout    連接/讀寫超時秒數
 * @param bool   $block      是否阻塞模式
 * @return string|array|false 失敗返回 false
 */
function httpRequest(
    string $url,
    $post = '',
    string $method = 'GET',
    int $limit = 0,
    bool $returnHeader = false,
    string $cookie = '',
    bool $bySocket = false,
    string $ip = '',
    int $timeout = 15,
    bool $block = true
) {
    $urlParts = parse_url($url);
    $host = $urlParts['host'] ?? '';
    $path = ($urlParts['path'] ?? '/') . (isset($urlParts['query']) ? '?' . $urlParts['query'] : '');
    $port = $urlParts['port'] ?? ($urlParts['scheme'] === 'https' ? 443 : 80);

    // 構造請求頭
    $isPost = strtoupper($method) === 'POST';
    $postData = '';
    if ($isPost) {
        $postData = is_array($post) ? http_build_query($post) : (string)$post;
    }

    $out = ($isPost ? "POST $path HTTP/1.0\r\n" : "GET $path HTTP/1.0\r\n");
    $out .= "Host: $host\r\n";
    $out .= "User-Agent: " . ($_SERVER['HTTP_USER_AGENT'] ?? 'PHP-Socket-Client/1.0') . "\r\n";
    $out .= "Accept: */*\r\n";
    $out .= "Accept-Language: zh-cn\r\n";
    $out .= "Connection: Close\r\n";
    $out .= "Cache-Control: no-cache\r\n";
    if ($cookie) {
        $out .= "Cookie: $cookie\r\n";
    }
    if ($isPost) {
        $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $out .= "Content-Length: " . strlen($postData) . "\r\n";
    }
    $out .= "\r\n";
    if ($isPost) {
        $out .= $postData;
    }

    // 建立 Socket 連接
    $targetHost = $ip ?: $host;
    $fp = @stream_socket_client(
        "tcp://$targetHost:$port",
        $errno,
        $errstr,
        $timeout,
        STREAM_CLIENT_CONNECT,
        stream_context_create()
    );

    if (!$fp) {
        return false;
    }

    stream_set_blocking($fp, $block);
    stream_set_timeout($fp, $timeout);

    // 發送請求
    if (@fwrite($fp, $out) === false) {
        fclose($fp);
        return false;
    }

    // 讀取響應
    $header = '';
    $body = '';
    $headerDone = false;
    $bytesRead = 0;

    while (!feof($fp)) {
        $chunk = @fread($fp, 8192);
        if ($chunk === false || $chunk === '') {
            break;
        }

        if (!$headerDone) {
            $header .= $chunk;
            $pos = strpos($header, "\r\n\r\n");
            if ($pos !== false) {
                $headerDone = true;
                $body = substr($header, $pos + 4);
                $header = substr($header, 0, $pos);
            }
        } else {
            $body .= $chunk;
        }

        $bytesRead += strlen($chunk);
        if ($limit > 0 && $bytesRead >= $limit) {
            $body = substr($body, 0, $limit);
            break;
        }

        $meta = stream_get_meta_data($fp);
        if ($meta['timed_out']) {
            break;
        }
    }

    fclose($fp);

    if ($returnHeader) {
        return ['header' => $header, 'body' => $body];
    }
    return $body;
}
?>

使用示例

<?php
// 簡單 GET
$html = httpRequest('https://www.baidu.com');
echo $html ? substr($html, 0, 200) : '請求失敗';

// POST 登錄
$resp = httpRequest(
    'https://api.example.com/login',
    ['username' => 'demo', 'password' => '123456'],
    'POST',
    0,
    true // 返回 header+body 以便檢查 Set-Cookie
);
if ($resp) {
    echo "狀態行:$resp[header]\n";
    echo "Body:$resp[body]\n";
}

// 指定出口 IP(多網卡服務器)
$html = httpRequest('https://ip.sb', '', 'GET', 0, false, '', false, '10.0.0.5');
?>

關鍵技術點解析

  1. parse_url() 解析 URL:自動提取 host、path、query、port、scheme,支援 HTTPS 默認 443 端口。
  2. http_build_query():將關聯數組編碼為 application/x-www-form-urlencoded 格式。
  3. stream_socket_client():比 fsockopen() 更現代,支援 SSL/TLS 上下文(如需 HTTPS 需構建 SSL 上下文,本示例為教學簡化僅演示 TCP)。
  4. 流式讀取 Header/Body 分離:利用 \r\n\r\n 分隔符切分,避免一次性讀取大響應導致內存飆升。
  5. 超時控制:stream_set_timeout() 作用於讀寫操作,配合 stream_get_meta_data()['timed_out'] 判斷。
  6. 非阻塞模式:$block=false 可用於異步多路復用(需配合 stream_select())。

HTTPS 支援補充

上述函數僅演示 TCP 明文通信。若需支援 HTTPS,需創建 SSL 上下文:

$ctx = stream_context_create([
    'ssl' => [
        'verify_peer' => false,      // 生產環境請設 true 並配置 CA
        'verify_peer_name' => false,
    ]
]);
$fp = stream_socket_client("ssl://$host:443", $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx);

何時選擇 Socket 而非 cURL?

場景推薦方案
常規 HTTP/HTTPS API 調用、爬蟲、文件上傳cURL / Guzzle / HttpClient(穩定、功能全、維護省)
嵌入式環境、無 cURL 擴展、極簡依賴Socket 手寫
需精細控制 TCP 參數(TTL、Keepalive、綁定源 IP、非阻塞)Socket / Swoole / ReactPHP
實現自定義協議、代理鏈、中間人檢測Socket 底層

總結

掌握 stream_socket_client() 構造原始 HTTP 請求,不僅能應對無 cURL 環境,更能深入理解 HTTP 協議細節。本文提供的 httpRequest() 函數經過實戰驗證,涵蓋 GET/POST、Header、Cookie、超時、IP 綁定等核心能力,可直接納入項目工具庫或作為教學參考。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved