理解 HTTP 協議細節、排查網絡問題、或在無 cURL/流擴展環境下發送請求,掌握原生 Socket 發送 HTTP 請求至關重要。本文提供兩個健壯的封裝類(介面風格與單例風格),支援 GET/POST、自定義 Header、超時控制、錯誤處理。
請求報文:
<請求行> GET /path?query HTTP/1.1
<請求頭> Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
<空行>
<請求體> username=test&age=25
響應報文同理:狀態行、響應頭、空行、響應體
interface HttpProto {
public function conn(string $url): void;
public function get(): string;
public function post(array $data = []): string;
public function close(): void;
}
class HttpClient implements HttpProto {
const CRLF = "\r\n";
private $url, $fp, $response = '';
private $line = [], $header = [], $body = [];
private $version = 'HTTP/1.1';
public function conn(string $url) {
$this->url = parse_url($url);
$this->url['port'] = $this->url['port'] ?? 80;
$this->url['path'] = $this->url['path'] ?? '/';
$this->url['query'] = $this->url['query'] ?? '';
$this->fp = fsockopen($this->url['host'], $this->url['port'], $errno, $errstr, 5);
if (!$this->fp) throw new RuntimeException("連接失敗: $errstr ($errno)");
$this->header[] = 'Host: ' . $this->url['host'];
}
private function setLine(string $method) {
$this->line = ["{$method} {$this->url['path']}?{$this->url['query']} {$this->version}"];
}
public function setHeader(string $line) { $this->header[] = $line; }
private function setBody(array $data) { $this->body = [http_build_query($data)]; }
private function request(): string {
$req = array_merge($this->line, $this->header, [''], $this->body, ['']);
$reqStr = implode(self::CRLF, $req);
fwrite($this->fp, $reqStr);
while (!feof($this->fp)) $this->response .= fread($this->fp, 4096);
$this->close();
return $this->response;
}
public function get(): string {
$this->setLine('GET');
return $this->request();
}
public function post(array $data = []): string {
$this->setLine('POST');
$this->setHeader('Content-Type: application/x-www-form-urlencoded');
$this->setBody($data);
$this->setHeader('Content-Length: ' . strlen($this->body[0]));
return $this->request();
}
public function close() { if ($this->fp) fclose($this->fp); }
}
// 使用
$client = new HttpClient();
$client->conn('https://httpbin.org/get');
echo $client->get();
$client->conn('https://httpbin.org/post');
echo $client->post(['username' => '測試', 'age' => 25]);
class HttpSocket {
private $fp, $host, $port, $path, $query;
private $headers = [], $body = '';
private static $instance;
private function __construct() {}
public static function create(): self {
return self::$instance ??= new self();
}
public function init(string $url): self {
$u = parse_url($url);
$this->host = $u['host'];
$this->port = $u['port'] ?? 80;
$this->path = $u['path'] ?? '/';
$this->query = $u['query'] ?? '';
$this->headers['Host'] = $this->host;
return $this;
}
public function get(array $headers = []): string {
$this->headers = array_merge($this->headers, $headers);
return $this->request('GET');
}
public function post(array $headers = [], array $data = []): string {
$this->headers = array_merge($this->headers, $headers);
if ($data) {
$this->body = http_build_query($data);
$this->headers['Content-Type'] = 'application/x-www-form-urlencoded';
$this->headers['Content-Length'] = strlen($this->body);
}
return $this->request('POST');
}
private function request(string $method): string {
$reqLine = "$method {$this->path}?{$this->query} HTTP/1.1";
$headerStr = implode("\r\n", array_map(fn($k,$v)=>"$k: $v", array_keys($this->headers), $this->headers));
$request = "$reqLine\r\n$headerStr\r\n\r\n$this->body";
$this->fp = fsockopen($this->host, $this->port, $errno, $errstr, 5);
if (!$this->fp) throw new RuntimeException("Socket 連接失敗: $errstr ($errno)");
fwrite($this->fp, $request);
$resp = '';
while (!feof($this->fp)) $resp .= fread($this->fp, 4096);
fclose($this->fp);
return $resp;
}
}
// 使用
$http = HttpSocket::create()->init('https://httpbin.org/post');
echo $http->post(
['User-Agent' => 'MyBot/1.0'],
['username' => '中文測試', 'age' => 22]
);
| 功能 | 實現要點 |
|---|---|
| HTTPS 支援 | fsockopen('ssl://host', 443) 或 stream_socket_client('ssl://host:443'),需驗證證書(stream_context_set_option) |
| Chunked 編碼響應解析 | 解析 Transfer-Encoding: chunked,按塊長度讀取拼接 |
| Gzip/Deflate 解壓 | 檢測 Content-Encoding,用 gzinflate()/gzdecode() |
| Cookie 管理 | 解析 Set-Cookie,後續請求帶上 Cookie 頭 |
| 重定向跟隨 | 解析 Location,遞歸請求(限制次數防循環) |
| 多部分表單 | 構造 multipart/form-data 請求體,支援文件上傳 |