HTTP 协议概览
HTTP(HyperText Transfer Protocol,超文本传输协议)是 Web 应用通信的基础协议。PHP 作为服务端语言,理解 HTTP 协议的工作原理对于构建高效、安全的 Web 应用至关重要。本节将深入讲解 HTTP 协议的核心概念及其在 PHP 中的体现。
前置知识
阅读本节前,建议先了解:PHP Web 运行模式、PHP 内置服务器
基础概念
HTTP 工作模型
HTTP 采用 请求-响应(Request-Response) 模型:客户端发起请求,服务端返回响应。整个过程经历以下几个步骤:
客户端(浏览器) 服务端(PHP + Web 服务器)
| |
|--- 1. TCP 连接 ---------->|
| |
|--- 2. HTTP 请求 --------->|
| |--- 3. 处理请求 (PHP)
| |
|<-- 4. HTTP 响应 ----------|
| |
|--- 5. 渲染页面 |
| |
|--- 6. 连接关闭/保持 -----|HTTP 版本演进
| 版本 | 发布年份 | 特性 | PHP 支持 |
|---|---|---|---|
| HTTP/1.0 | 1996 | 短连接、无 Host 头 | 全版本 |
| HTTP/1.1 | 1997 | 长连接、管线化、分块传输 | 全版本 |
| HTTP/2 | 2015 | 多路复用、头部压缩、服务器推送 | 7.4+(需扩展) |
| HTTP/3 | 2022 | 基于 QUIC、0-RTT 连接 | 8.3+(需扩展) |
HTTP/2 与 PHP
PHP-FPM 本身不直接处理 HTTP/2,通常由 Nginx/Apache 终止 TLS 并代理到 PHP-FPM。PHP 8.3+ 的 cURL 扩展支持 HTTP/3。
HTTP 请求结构
一个完整的 HTTP 请求由以下三部分组成:
请求行(Request Line)
请求头(Headers)
空行
请求体(Body,可选)示例:
GET /index.php?id=1 HTTP/1.1
Host: www.example.com
Accept: text/html,application/xhtml+xml
Accept-Language: zh-CN,zh;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Cookie: session_id=abc123
Connection: keep-aliveHTTP 响应结构
状态行(Status Line)
响应头(Headers)
空行
响应体(Body)示例:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Set-Cookie: session_id=abc123; Path=/; HttpOnly; Secure
Cache-Control: max-age=3600
X-Powered-By: PHP/8.3
<!DOCTYPE html>
<html>...</html>PHP 中的 HTTP 超全局变量
PHP 通过超全局变量自动解析 HTTP 请求,开发者无需手动解析原始 HTTP 报文。
$_SERVER 中的 HTTP 相关键
php
<?php
declare(strict_types=1);
// 请求方法和协议
echo $_SERVER['REQUEST_METHOD'] // GET, POST, PUT, DELETE...
echo $_SERVER['SERVER_PROTOCOL'] // HTTP/1.1, HTTP/2
echo $_SERVER['REQUEST_URI'] // /index.php?id=1
echo $_SERVER['QUERY_STRING'] // id=1
echo $_SERVER['HTTP_HOST'] // www.example.com
echo $_SERVER['SERVER_NAME'] // www.example.com(配置值)
echo $_SERVER['SERVER_PORT'] // 80 或 443
echo $_SERVER['HTTPS'] // on(HTTPS 时)
echo $_SERVER['REQUEST_SCHEME'] // http 或 https(PHP 7.0+)
echo $_SERVER['REMOTE_ADDR'] // 客户端 IP
echo $_SERVER['REMOTE_PORT'] // 客户端端口
echo $_SERVER['DOCUMENT_ROOT'] // /var/www/html
// HTTP 请求头(以 HTTP_ 为前缀)
echo $_SERVER['HTTP_ACCEPT']; // Accept 头
echo $_SERVER['HTTP_ACCEPT_LANGUAGE']; // Accept-Language 头
echo $_SERVER['HTTP_USER_AGENT']; // User-Agent 头
echo $_SERVER['HTTP_REFERER']; // 来源页面
echo $_SERVER['HTTP_COOKIE']; // Cookie 头
echo $_SERVER['HTTP_AUTHORIZATION']; // Authorization 头
// 文件路径
echo $_SERVER['SCRIPT_FILENAME']; // /var/www/html/index.php
echo $_SERVER['SCRIPT_NAME']; // /index.php
echo $_SERVER['PHP_SELF']; // /index.php
// 时间
echo $_SERVER['REQUEST_TIME']; // 请求开始时间戳
echo $_SERVER['REQUEST_TIME_FLOAT']; // 请求开始时间(微秒精度,PHP 5.4+)判断 HTTPS 连接
php
<?php
declare(strict_types=1);
/**
* 安全地判断当前是否为 HTTPS 连接
*/
function isHttps(): bool
{
// 标准 HTTPS 检测
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
return true;
}
// 反向代理场景(Nginx -> Apache/PHP-FPM)
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
return true;
}
// 非标准 HTTPS 端口
if (!empty($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
return true;
}
return false;
}
if (isHttps()) {
echo "当前为安全连接";
} else {
echo "当前为非安全连接";
}HTTP 状态码
状态码分类
| 范围 | 类别 | 含义 |
|---|---|---|
| 1xx | Informational | 信息性状态码,表示请求已接收 |
| 2xx | Success | 请求成功 |
| 3xx | Redirection | 重定向 |
| 4xx | Client Error | 客户端错误 |
| 5xx | Server Error | 服务端错误 |
常用状态码详解
php
<?php
declare(strict_types=1);
// === 2xx 成功 ===
// 200 OK - 请求成功(默认状态码)
http_response_code(200);
// 201 Created - 资源创建成功(RESTful API 常用)
http_response_code(201);
// 204 No Content - 成功但无返回内容(DELETE 操作常用)
http_response_code(204);
// === 3xx 重定向 ===
// 301 Moved Permanently - 永久重定向
http_response_code(301);
header('Location: https://www.example.com/new-url');
// 302 Found - 临时重定向(默认)
http_response_code(302);
header('Location: /login');
// 304 Not Modified - 缓存有效,无需重新传输
http_response_code(304);
// 307 Temporary Redirect - 保持请求方法的临时重定向
http_response_code(307);
header('Location: https://example.com/new');
// 308 Permanent Redirect - 保持请求方法的永久重定向
http_response_code(308);
header('Location: https://example.com/new');
// === 4xx 客户端错误 ===
// 400 Bad Request - 请求格式错误
http_response_code(400);
echo json_encode(['error' => '无效的请求参数']);
// 401 Unauthorized - 未认证
http_response_code(401);
header('WWW-Authenticate: Basic realm="My Site"');
// 403 Forbidden - 无权限
http_response_code(403);
echo json_encode(['error' => '您没有访问权限']);
// 404 Not Found - 资源不存在
http_response_code(404);
echo json_encode(['error' => '页面不存在']);
// 405 Method Not Allowed - 请求方法不被允许
http_response_code(405);
header('Allow: GET, POST'); // 告知客户端允许的方法
// 413 Payload Too Large - 请求体过大
http_response_code(413);
// 415 Unsupported Media Type - 不支持的媒体类型
http_response_code(415);
// 422 Unprocessable Entity - 语义错误(WebDAV/REST 常用)
http_response_code(422);
echo json_encode(['error' => '参数验证失败', 'details' => $errors]);
// 429 Too Many Requests - 请求过于频繁
http_response_code(429);
header('Retry-After: 60');
// === 5xx 服务端错误 ===
// 500 Internal Server Error - 服务器内部错误
http_response_code(500);
// 502 Bad Gateway - 网关错误
http_response_code(502);
// 503 Service Unavailable - 服务不可用
http_response_code(503);
header('Retry-After: 120');
// 504 Gateway Timeout - 网关超时
http_response_code(504);PHP 设置状态码的方式
php
<?php
declare(strict_types=1);
// 方式一:http_response_code()(推荐,PHP 5.4+)
http_response_code(404);
echo "Page not found";
// 方式二:header() 第一参数包含状态行
header('HTTP/1.1 404 Not Found');
echo "Page not found";
// 方式三:header() 配合 http_response_code()
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['error' => 'Not Found']);
// 获取当前状态码
$currentCode = http_response_code();
echo "当前状态码: {$currentCode}"; // 200注意事项
header() 必须在向客户端输出任何内容之前调用,包括空格、BOM 和 PHP 标签前的空行。启用 输出缓冲 可以避免此问题。
HTTP 请求方法
常用请求方法
| 方法 | 用途 | 请求体 | 幂等 | 安全 |
|---|---|---|---|---|
| GET | 获取资源 | 无 | 是 | 是 |
| POST | 创建资源 | 有 | 否 | 否 |
| PUT | 全量更新资源 | 有 | 是 | 否 |
| PATCH | 部分更新资源 | 有 | 否 | 否 |
| DELETE | 删除资源 | 可选 | 是 | 否 |
| HEAD | 获取响应头 | 无 | 是 | 是 |
| OPTIONS | 获取允许的方法 | 无 | 是 | 是 |
在 PHP 中处理不同请求方法
php
<?php
declare(strict_types=1);
// 获取当前请求方法
$method = $_SERVER['REQUEST_METHOD'];
// 方式一:switch 匹配
match ($method) {
'GET' => handleGet(),
'POST' => handlePost(),
'PUT', 'PATCH' => handleUpdate(),
'DELETE' => handleDelete(),
'OPTIONS' => handleOptions(),
default => http_response_code(405),
};
// 方式二:类封装
class Request
{
public function __construct(
public readonly string $method,
public readonly string $uri,
public readonly array $headers,
public readonly array $query,
public readonly string $body,
public readonly array $server,
) {
}
public static function fromGlobals(): self
{
return new self(
method: $_SERVER['REQUEST_METHOD'],
uri: $_SERVER['REQUEST_URI'],
headers: getallheaders(),
query: $_GET,
body: file_get_contents('php://input'),
server: $_SERVER,
);
}
public function isMethod(string $method): bool
{
return strtoupper($method) === $this->method;
}
public function json(): array
{
return json_decode($this->body, true, 512, JSON_THROW_ON_ERROR);
}
}
$request = Request::fromGlobals();
if ($request->isMethod('GET')) {
$id = $request->query['id'] ?? null;
// 处理 GET 请求
} elseif ($request->isMethod('POST')) {
$data = $request->json();
// 处理 POST 请求
}HTTP 请求头与响应头
常用请求头
| 头部字段 | 说明 | PHP 获取方式 |
|---|---|---|
Host | 目标主机 | $_SERVER['HTTP_HOST'] |
User-Agent | 客户端标识 | $_SERVER['HTTP_USER_AGENT'] |
Accept | 可接受的内容类型 | $_SERVER['HTTP_ACCEPT'] |
Accept-Language | 偏好语言 | $_SERVER['HTTP_ACCEPT_LANGUAGE'] |
Accept-Encoding | 可接受的内容编码 | $_SERVER['HTTP_ACCEPT_ENCODING'] |
Content-Type | 请求体的媒体类型 | $_SERVER['CONTENT_TYPE'] |
Content-Length | 请求体长度 | $_SERVER['CONTENT_LENGTH'] |
Authorization | 认证信息 | $_SERVER['HTTP_AUTHORIZATION'] |
Cookie | Cookie 数据 | $_SERVER['HTTP_COOKIE'] |
Referer | 来源页面 | $_SERVER['HTTP_REFERER'] |
If-None-Match | 缓存验证(ETag) | $_SERVER['HTTP_IF_NONE_MATCH'] |
If-Modified-Since | 缓存验证(时间) | $_SERVER['HTTP_IF_MODIFIED_SINCE'] |
X-Requested-With | AJAX 标识 | $_SERVER['HTTP_X_REQUESTED_WITH'] |
常用响应头
| 头部字段 | 说明 | PHP 设置方式 |
|---|---|---|
Content-Type | 响应内容的媒体类型 | header('Content-Type: ...') |
Content-Length | 响应体长度 | header('Content-Length: ...') |
Location | 重定向目标 | header('Location: ...') |
Set-Cookie | 设置 Cookie | setcookie() 或 setrawcookie() |
Cache-Control | 缓存策略 | header('Cache-Control: ...') |
Expires | 过期时间 | header('Expires: ...') |
ETag | 资源标识 | header('ETag: ...') |
Last-Modified | 最后修改时间 | header('Last-Modified: ...') |
Access-Control-Allow-Origin | CORS 允许源 | header('Access-Control-Allow-Origin: ...') |
X-Frame-Options | 防止点击劫持 | header('X-Frame-Options: ...') |
X-Content-Type-Options | 防止 MIME 嗅探 | header('X-Content-Type-Options: nosniff') |
Strict-Transport-Security | 强制 HTTPS | header('Strict-Transport-Security: ...') |
X-Powered-By | 服务器技术栈 | 建议移除(安全考虑) |
HTTP Content-Type
常见 Content-Type 值
php
<?php
declare(strict_types=1);
// HTML
header('Content-Type: text/html; charset=utf-8');
// 纯文本
header('Content-Type: text/plain; charset=utf-8');
// JSON(API 最常用)
header('Content-Type: application/json; charset=utf-8');
// XML
header('Content-Type: application/xml; charset=utf-8');
// 表单数据(multipart)
header('Content-Type: multipart/form-data; boundary=----WebKitFormBoundary');
// URL 编码表单
header('Content-Type: application/x-www-form-urlencoded');
// 文件下载
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="report.pdf"');
// 图片
header('Content-Type: image/png');
header('Content-Type: image/jpeg');
header('Content-Type: image/svg+xml');
// PDF
header('Content-Type: application/pdf');
// 流媒体
header('Content-Type: video/mp4');
header('Content-Type: audio/mpeg');
// CSS / JavaScript
header('Content-Type: text/css');
header('Content-Type: application/javascript');实战示例:完整的 HTTP 请求信息调试页面
php
<?php
declare(strict_types=1);
/**
* HTTP 请求信息调试工具
* 仅在开发环境使用,生产环境必须禁用
*/
if (!defined('APP_DEBUG') || !APP_DEBUG) {
http_response_code(403);
exit('Access denied');
}
header('Content-Type: text/html; charset=utf-8');
$protocol = $_SERVER['SERVER_PROTOCOL'];
$method = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
$timestamp = $_SERVER['REQUEST_TIME'];
$timestampFloat = $_SERVER['REQUEST_TIME_FLOAT'];
$sections = [
'请求基本信息' => [
'请求方法' => $method,
'请求 URI' => $uri,
'协议版本' => $protocol,
'查询字符串' => $_SERVER['QUERY_STRING'] ?? '',
'请求时间' => date('Y-m-d H:i:s', $timestamp),
'请求时间(微秒)' => $timestampFloat,
'HTTPS' => isHttps() ? '是' : '否',
],
'服务器信息' => [
'服务器名称' => $_SERVER['SERVER_NAME'],
'服务器地址' => $_SERVER['SERVER_ADDR'],
'服务器端口' => $_SERVER['SERVER_PORT'],
'文档根目录' => $_SERVER['DOCUMENT_ROOT'],
'PHP 文件路径' => $_SERVER['SCRIPT_FILENAME'],
],
'客户端信息' => [
'客户端 IP' => $_SERVER['REMOTE_ADDR'],
'客户端端口' => $_SERVER['REMOTE_PORT'],
'User-Agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'Referer' => $_SERVER['HTTP_REFERER'] ?? '(无)',
'Accept' => $_SERVER['HTTP_ACCEPT'] ?? '',
'Accept-Language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
'Accept-Encoding' => $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '',
],
'请求体' => [
'原始请求体' => file_get_contents('php://input') ?: '(空)',
'POST 数据' => $_POST,
'GET 数据' => $_GET,
'Cookie' => $_COOKIE,
'请求头' => getallheaders(),
],
];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>HTTP 请求调试</title>
<style>
body { font-family: monospace; max-width: 960px; margin: 20px auto; background: #1e1e1e; color: #d4d4d4; }
h1 { color: #569cd6; }
h2 { color: #dcdcaa; margin-top: 2em; border-bottom: 1px solid #444; padding-bottom: 5px; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th { text-align: left; background: #2d2d2d; padding: 8px; color: #9cdcfe; }
td { padding: 8px; border-bottom: 1px solid #333; word-break: break-all; }
pre { background: #2d2d2d; padding: 12px; overflow-x: auto; border-radius: 4px; }
</style>
</head>
<body>
<h1>HTTP 请求调试信息</h1>
<?php foreach ($sections as $title => $items): ?>
<h2><?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?></h2>
<table>
<?php foreach ($items as $key => $value): ?>
<tr>
<th><?= htmlspecialchars((string) $key, ENT_QUOTES, 'UTF-8') ?></th>
<td>
<?php if (is_array($value)): ?>
<pre><?= htmlspecialchars(print_r($value, true), ENT_QUOTES, 'UTF-8') ?></pre>
<?php else: ?>
<?= htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endforeach; ?>
</body>
</html>注意事项
1. header() 必须在输出前调用
php
<?php
// 错误:echo 之后再设置 header
echo "Hello";
header('Location: /login'); // 触发 "headers already sent" 警告
// 正确:先设置 header
header('Location: /login');
echo "Hello"; // 不会执行(Location 会重定向)
exit;2. $_SERVER 中的键可能不存在
php
<?php
// 不安全:直接访问可能不存在的键
$host = $_SERVER['HTTP_HOST']; // E_WARNING
// 安全:使用 null 合并运算符
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
// 更安全:使用 filter_input
$host = filter_input(INPUT_SERVER, 'HTTP_HOST', FILTER_DEFAULT, FILTER_FLAG_REQUIRED);3. 信任代理头需谨慎
php
<?php
// 危险:直接信任 X-Forwarded-For
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']; // 可被伪造
// 安全:仅在已知可信代理时才使用
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && isTrustedProxy()) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}4. 隐藏 PHP 版本信息
php
<?php
// 在 php.ini 中设置
// expose_php = Off
// 或在运行时(需在所有输出之前)
header_remove('X-Powered-By');
ini_set('expose_php', '0');最佳实践
1. 始终设置 Content-Type
php
<?php
declare(strict_types=1);
// HTML 页面
header('Content-Type: text/html; charset=utf-8');
// API 响应
header('Content-Type: application/json; charset=utf-8');
// 文件下载
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file('/path/to/file');
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="download.pdf"');2. 设置安全响应头
php
<?php
declare(strict_types=1);
// 防止 MIME 类型嗅探
header('X-Content-Type-Options: nosniff');
// 防止点击劫持
header('X-Frame-Options: DENY'); // 或 SAMEORIGIN
// XSS 防护(旧浏览器)
header('X-XSS-Protection: 1; mode=block');
// HSTS(仅 HTTPS)
if (isHttps()) {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
// Content Security Policy
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");3. 正确使用 HTTP 缓存
php
<?php
declare(strict_types=1);
// API 响应通常禁用缓存
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
// 静态资源设置缓存
$ttl = 3600; // 1 小时
header('Cache-Control: public, max-age=' . $ttl);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $ttl) . ' GMT');
// ETag 缓存验证
$etag = md5_file('/path/to/resource');
$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
if ($ifNoneMatch === '"' . $etag . '"') {
http_response_code(304);
exit;
}
header('ETag: "' . $etag . '"');下一节
继续学习:GET 与 POST 请求