10. PHP 服务器
由于 PHP 程序可由 WEB 服务器执行,此类程序便成为可服务于多个客户端的服务器程序。 从客户端的角度来看,调用一个 WEB 服务相当于请求该服务的 URL。客户端可以使用任何语言编写,特别是 PHP。 在后一种情况下,我们将使用刚才介绍过的网络函数。此外,我们还需要学会与 WEB 服务“对话”,即理解 Web 服务器与其客户端之间的 http 通信协议。这就是接下来这些程序的目标。
第9.2节中描述的Web客户端让我们得以了解HTTP协议的一部分。

在最简单的版本中,客户端与服务器的交互如下:
- 客户端与Web服务器的80端口建立连接
- 客户端发起文档请求
- Web 服务器发送所请求的文档并关闭连接
- 客户端随后关闭连接
客户端可以是多种形式:HTML格式的文本、图片、视频等。它可以是现成的文档(静态文档),也可以是由脚本即时生成的文档(动态文档)。在后一种情况下,我们称之为Web编程。 用于动态生成文档的脚本可以使用多种语言编写:PHP、Python、Perl、Java、Ruby、C#、VB.net等
此处我们使用 PHP 来动态生成文本文档。
![]() |
- 在 [1] 中,客户端与服务器建立连接,请求脚本 PHP,并可选择是否向该脚本发送参数
- 在 [2] 中,Web 服务器通过解释器 PHP 执行脚本 PHP。该脚本生成一个文档,并将其发送给客户端 [3]
- 服务器关闭连接。客户端也随之关闭连接。
Web 服务器可同时处理多个客户端。使用软件包 WampServer 时,Web 服务器为 Apache 服务器,这是 Apache 基金会(http://www.apache.org/)提供的一款开源服务器。 在接下来的应用中,需启动 WampServer。这将激活三个组件:Apache Web 服务器、SGBD MySQL 以及 PHP 解释器。
由Web服务器执行的脚本将使用NetBeans工具编写。到目前为止,我们编写了在控制台环境中运行的PHP脚本:
![]() |
用户通过控制台请求执行脚本 PHP 并接收其结果。
在接下来的客户端/服务器应用程序中,
- 客户端脚本在控制台环境中运行
- 服务器端脚本在 Web 环境中执行
![]() |
服务器端的 PHP 脚本不能位于文件系统中的任意位置。因为 Web 服务器会根据配置指定的位置,查找用户请求的静态和动态文档。 WampServer的默认配置规定,文档将在<WampServer>/www文件夹中查找,其中<WampServer>是WampServer的安装目录。 因此,当Web客户端请求路径为URL [http://localhost/D]的文档D时,Web服务器将返回路径为[<WampServer>/www/D]的文档D。
在接下来的示例中,我们将服务器脚本放在 [www/exemples-web] 文件夹中。如果一个服务器脚本名为 S.php,则会通过 URL [http://localhost/exemples-web/S.php] 向 Web 服务器请求该脚本。 此时,系统将返回文档 [<WampServer>/www/exemples-web/S.php]。
![]() |
要使用 NetBeans 为 创建服务器脚本,我们将按照以下步骤进行:
![]() |
- 在 [1] 中,我们创建一个新项目
- 在 [2] 中,我们选择类别 [PHP] 以及项目 [PHP Application]
![]() |
- 在 [3] 中,我们为项目命名
- 在 [4] 中,我们为该项目选择一个文件夹
- 在 [5] 中,我们指定脚本应由本地 Web 服务器执行(脚本的 URL 将采用 http://localhost/... 的形式)。本地 Web 服务器将是 WampServer 的 Apache Web 服务器。
- 在 [6] 中,我们指定了该项目的 URL。 在此,我们决定通过 URL 调用该项目的 S.php 脚本。 根据上述说明,这意味着脚本S.php在文件系统中的路径将是[<WampServer>/www/exemples-web/S.php]。这就是[7]中所示的内容。 这里要求将该项目中的所有 S.php 脚本复制到 Apache 网页服务器的目录树中。
- 在 [8] 中,即新项目。
编写一个测试脚本:
![]() |
- 在 [1] 中,我们在项目 [exemples-web] 中创建第一个脚本 PHP
- 在 [2] 中,为其命名
- 在 [3] 中,创建完成后,为其添加以下内容
接下来,需要运行 WampServer。
![]() |
- 在 [4] 中,我们执行 Web 脚本 [exemple1.php]。 NetBeans 随后将启动计算机的默认浏览器,并要求其显示 URL [http://localhost/exemples-web/exemple1.php] [5]
- 文件,浏览器将显示服务器脚本写给客户端的内容。
接下来,我们将遇到两种类型的Web客户端:
- 如上所述的浏览器。我们提到Web服务器发送的响应格式为:HTTP头部、空行、正文。该浏览器仅显示texte。
- 一个名为 PHP 的脚本,它将完整显示响应内容:HTTP 头部、空行、正文。
接下来,
- 服务器端脚本将按上述 [exemple1.php] 的格式编写
- 客户端脚本将采用我们迄今为止编写的控制台脚本形式。
10.1. 日期/时间客户端/服务器应用程序
10.1.1. 服务器(web_01)
<?php
// 时间:自 1970 年 1 月 1 日以来的毫秒数
// 日期和时间显示格式
// d:2位数的日期
// m:2位数的月份
// y:2位数的年份
// H:小时 0,23
// i:分钟
// s:秒
print date("d/m/y H:i:s",time());
基本上,上面的脚本 PHP 会将当前时间写入屏幕。然而,当它由 Web 服务器执行时,通常与屏幕关联的第 1 号数据流会被重定向到连接服务器与其客户端的连接上。 因此,在 Web 环境中,上述脚本会将当前时间以文本形式发送给客户端。
让我们在 NetBeans 中运行此脚本:
![]() |
- 在 [1] 中,我们执行该脚本。随后将启动一个 Web 浏览器。
- 在 [2] 中,处理由网页浏览器请求的 URL
- 在 [3] 中,服务器脚本发送的文本
客户端浏览器使用 HTTP 协议与 Web 服务器进行通信。我们之前已经描述过该协议的格式。
客户端发送的文本行可分解为三个部分:HTTP 头部、空行、文档。 发送至Web服务器的文档通常为空,或者是一组parami=vali参数,其中vali是用户在HTML表单中输入的值。
服务器的响应格式相同:HTTP 头部、空行、文档,其中 document 此时是客户端浏览器请求的文档。如果客户端传输了参数,返回的文档通常取决于这些参数。
使用 Firefox 浏览器,可以查看客户端与 Web 服务器之间的实际通信。有一个名为 Firebug 的插件,可用于追踪这些通信。Firebug 可在 URL [https://addons.mozilla.org/fr/firefox/addon/firebug/] 处获取。 若使用Firefox浏览器访问该网址,即可下载Firebug插件。下文将假设Firebug插件已下载并安装完毕。用户可通过Firefox菜单中的选项访问该插件:
![]() |
Firebug 窗口将在 Firefox 浏览器窗口中打开。该窗口本身也包含一个菜单:
![]() |
为了查看 HTTP 请求过程中的客户端/服务器交互,我们使用 Firefox 浏览器发送 URL [http://localhost/exemples-web/web_01.php] 请求。此时 Firebug 窗口将显示相关信息:
![]() |
上图展示了客户端与服务器之间通信的摘要:
- [1]:客户端发送了请求 HTTP: GET /exemples-web/web_01.php HTTP/1.1 以请求文档 [web01.php]
- [2]:服务器发送了响应:HTTP/1.1 200 OK,表明已找到所请求的文档。
Firebug 可用于查看完整的通信记录。只需“展开”URL:
![]() |
上文展示了客户端(请求)与服务器(响应)之间交换的HTTP头部。我们可以获取通信的源代码(c.a.d)以及实际交换的文本行([1])。由此得到以下源代码:
![]() |
要编写一个Web服务器的客户端脚本,我们只需模拟浏览器的行为即可。在与服务器建立连接后,客户端脚本可以发送上述请求中的8行内容。实际上并非所有内容都必不可少,我们只需发送以下三行:
- 第1行:指定请求的文档及所使用的协议 HTTP
- 第2行:提供客户端脚本的主机名
- 第3行:表明数据交换完成后,客户端将关闭与服务器的连接
现在让我们关注服务器的响应。我们知道该响应由脚本 PHP [web_01.php] 生成。 上文所示为响应中的 HTTP 头部信息。[web01.php] 脚本的代码表明,这些头部信息并非由该脚本生成。回顾一下服务器脚本的配置:
![]() |
响应中的 HTTP 头部是由 Web 服务器生成的。服务器脚本可以自行生成这些头部。稍后我们将看到一个示例。
我们提到过,Web 服务器的响应格式为:HTTP 头部、空行、文档。如果文档是文本文件,我们可以在 Firebug 的 [Réponse] 标签页中看到它:
![]() |
该响应由脚本 [web_01.php] 生成。
10.1.2. 客户端(client1_web_01)
现在,我们为上述服务编写一个客户端脚本。我们知道客户端必须:
- 与 Web 服务器建立连接
- 发送文本:HTTP 头部,空行
- 读取服务器的完整响应,直到服务器关闭与客户端的连接
- 关闭与服务器的连接
该客户端脚本在 NetBeans 的控制台环境中运行:
![]() |
- 在 [1] 中,客户端脚本 [client1_web_01.php] 被包含在 NetBeans 项目 [exemples] 中
- 在 [2] 中,NetBeans 项目 [exemples] 的属性
- 在 [3] 中,NetBeans 项目 [exemples] 以“命令行”模式运行,我们也称之为“控制台”模式。
客户端脚本代码如下:
<?php
// 数据
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_01.php";
// 在 $HOTE 的 80 端口上建立连接
$connexion = fsockopen($HOTE, $PORT);
// 错误?
if (!$connexion) {
print "Erreur : $erreur\n";
exit;
}
// HTTP 协议的头部(headers)必须以空行结尾
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// 主机
fputs($connexion, "Host: localhost\n");
// 连接
fputs($connexion,"Connection: close\n");
// 空行
fputs($connexion,"\n");
// 服务器现在将在 $connexion 通道上进行响应。它将发送所有
// 数据,然后关闭通道。因此客户端会读取来自 $connexion
// 直至通道关闭
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
}//期间
// 客户端也关闭连接
fclose($connexion);
// 结束
exit;
注释
- 第 8 行:建立与服务器的连接
- 第 16 行:命令 HTTP GET
- 第 18 行:命令 HTTP Host
- 第 20 行:命令 HTTP 连接
- 第 22 行:空行
- 第 26-28 行:读取服务器发送的所有文本行,直到服务器关闭连接。
- 第 30 行:客户端也关闭连接
结果
执行客户端脚本后,得到以下结果:
注释
- 第 1-7 行:Web 服务器的响应 HTTP。
- 第 8 行:空行,表示 HTTP 头部结束
- 第 9 行及之后:文档内容。此处为显示当前日期和时间的简单文本。这是脚本 PHP 写入第 1 号输出流的文本。
- 第 1 行:服务器响应称已找到所请求的文档。
- 第2行:服务器的当前日期和时间
- 第3行:Web服务器的标识
- 第 4 行:表明接下来的文档是由脚本 PHP 生成的
- 第 5 行:文档的字符数
- 第6行:服务器表示在发送文档后将关闭连接
- 第7行:表明服务器发送的文档是HTML格式的文本。此处信息不准确。 该文档是无特定格式的纯文本。当文档并非 HTML 格式文本时,应由脚本 PHP 进行标注。但此处未执行该操作。
10.1.3. 第二个客户端(client2_web_01)
前一个客户端会显示 Web 服务器发送的所有内容。实际上,通常会忽略响应中的 HTTP 头部信息,而只处理文档部分。这里我们试图提取由服务器脚本 PHP 发送的日期和时间。 我们将通过正则表达式来提取这些信息。
<?php
// 获取Web服务器发送的信息
// 数据
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_01.php";
// 在 $HOTE 的 80 端口上建立连接
$connexion = fsockopen($HOTE, $PORT);
// 错误?
if (!$connexion) {
print "Erreur : $erreur\n";
exit;
}
// HTTP 协议的头部(headers)必须以空行结尾
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// 主机
fputs($connexion, "Host: localhost\n");
// 连接
fputs($connexion,"Connection: close\n");
// 空行
fputs($connexion,"\n");
// 服务器现在将在 $connexion 通道上进行响应。它将发送所有
// 这些数据,然后关闭该通道。因此,客户端会读取来自 $connexion 的所有数据
// 直到找到其寻找的、格式为 jj/mm/aa hh:mm:ss 的数据行
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
if (preg_match("/(\d\d)\/(\d\d)\/(\d\d) (\d\d):(\d\d):(\d\d)/", $ligne, $champs)) {
// 从中提取 # 个字段
array_shift($champs); // 移除字段数组中的第一个元素
// 将 6 个字段分别存入 6 个变量
list($j, $m, $a, $h, $i, $s) = $champs;
// 显示结果
print "\ndateheure=[$j,$m,$a,$h,$i,$s]\n";
}//if
}//while
// 客户端也关闭连接
fclose($connexion);
// 结束
exit;
结果
10.2. 服务器接收客户端发送的参数
在 HTTP 协议中,客户端有两种方法向 Web 服务器传递参数:
- 以
GET url?param1=val1¶m2=val2¶m3=val3… HTTP/1.0
其中,vali中的值必须事先进行编码,以便将某些保留字符替换为相应的十六进制值。
- 以以下形式向服务请求 URL:
POST url HTTP/1.0
随后,在发送给服务器的HTTP头部中插入以下头部:
客户端发送的后续头部以空行结束。随后,它可以将数据以以下形式发送:
其中,vali 的值必须像 GET 方法那样,事先进行编码。发送给服务器的字符数必须为 N,其中 N 是标头中声明的值
脚本 PHP 用于获取客户端发送的上述 parami 参数,其值来自以下数组:
- 对于命令 GET,从数组 $_GET["parami"] 中获取参数值
- $_POST["parami"] 对应订单 POST
10.2.1. 客户端 GET (client1_web_02)
下面的脚本 PHP 向服务器发送三个参数 [nom, prenom, age]。
<?php
// 客户端:通过方法 GET 将名字、姓氏、年龄发送至服务器
// 数据
$HOTE = "localhost";
$PORT = 80;
$URL = "/exemples-web/web_02.php";
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// 连接到Web服务器
$connexion = fsockopen($HOTE, $PORT);
// 若出错则返回
if (!$connexion) {
print "Echec de la connexion au site ($HOTE,$PORT) : $erreur";
exit;
}//if
// 将信息发送至服务器 PHP
// 对信息进行编码
$infos = "prenom=" . urlencode(utf8_decode($prenom)) . "&nom=" . urlencode(utf8_decode($nom)) . "&age=" . urlencode("$age");
// 控制台跟踪
print "infos envoyées au serveur (GET)=$infos\n";
print "URL demandée=[$URL?$infos]\n\n";
// HTTP 协议的头部(headers)必须以空行结尾
// GET
fputs($connexion, "GET $URL?$infos HTTP/1.1\n");
// 主机
fputs($connexion, "Host: localhost\n");
// 连接
fputs($connexion,"Connection: close\n");
// 空行
fputs($connexion,"\n");
// 服务器现在将在 $connexion 通道上响应。它将发送所有
// 读取其数据后关闭通道。客户端会读取来自 $connexion 的所有数据,直到通道关闭
while ($ligne = fgets($connexion, 1000))
print "$ligne";
// 客户端随后关闭连接
fclose($connexion);
注释
- 第 7 行:服务器脚本的 URL
- 第 8 行:3 个参数的值
- 第 10 行:与 Web 服务器建立连接
- 第18行:对3个参数进行编码。我们正在使用Netbeans编写的脚本,字符编码格式为UTF-8。因此,第8行中3个参数的值被编码为UTF-8。 函数 utf8_decode 将其编码转换为 ISO-8859-1。完成此操作后,即可将其编码为 URL。 所有非字母字符均被替换为 %xx,其中 xx 代表该字符的十六进制值。空格则被替换为 + 符号。
- 第24行:请求的URL是$URL?$infos,其中$infos的形式为nom=val1&prenom=val2&age=val3。
10.2.2. 服务器(web_02)
服务器仅显示其接收到的内容。
<?php
// 错误处理
ini_set("display_errors", "off");
// 服务器接收客户端发送的信息
// 此处名=P&姓=N&年龄=A
// 这些信息会自动保存在变量中
// $_GET['prenom'], $_GET['nom'], $_GET['age']
// 将其发回给客户端
// UTF-8 头部
header("Content-Type: text/plain; charset=utf-8");
// 发送给服务器的参数
$prenom = isset($_GET['prenom']) ? $_GET['prenom'] : "";
$nom = isset($_GET['nom']) ? $_GET['nom'] : "";
$age = isset($_GET['age']) ? $_GET['age'] : "";
// 对客户端的响应
$réponse = "informations reçues du client [" .
utf8_encode(htmlspecialchars($prenom, ENT_QUOTES)) .
"," . utf8_encode(htmlspecialchars($nom, ENT_QUOTES)) .
"," . utf8_encode(htmlspecialchars($age, ENT_QUOTES)) . "]\n";
print $réponse;
注释
- 第 13 行:设置 HTTP 的 "Content-Type" 标头。默认情况下,Web 服务器发送的标头为
,表示响应为 HTML 格式的文本。在此处,响应将是以 UTF-8 编码的无格式文本:
HTTP 头部必须在服务器响应之前发送。因此,在上文中,对 header 函数的调用将位于任何 print 指令之前。
- 第16-18行:从数组$_GET中获取3个参数。
- 第21行:构建将作为响应发送给客户端的字符串。某些字符在HTML中具有特殊含义,必须替换为HTML中的实体才能显示。 htmlspecialchars($string) 将所有这些字符替换为字符串 $string 中的等效字符。 例如,字符 $ 将变为 &。随后,正如我们在第 13 行所述,响应内容为 UTF-8 字符串,因此我们将获取的值编码为 UTF-8。
- 第25行:将响应发送给客户端
测试 1
在 NetBeans 中运行脚本 [web_02]。随后将启动浏览器以显示 URL [http://localhost/exemples-web/web_02.php]:
![]() |
- 在 [1] 中,浏览器显示 URL [http://localhost/exemples-web/web_02.php]。由于我们未在该 URL 后附加参数,服务器返回了空参数。 请注意,服务器的响应内容是使用指令 print 生成的。
- 在 [2] 中,我们在 URL 后添加了参数。这次服务器脚本成功返回了这些参数。
需要注意的是,执行服务器脚本并不需要 NetBeans。只需在浏览器中输入服务器脚本的 URL,该脚本即可被执行。
测试 2
在 NetBeans 中运行客户端 [client1_web_02.php]。收到以下响应:
- 第 1 行:3 个参数的编码。可以看到字符 û 已变为 %FB。
- 第 12 行:服务器的响应
10.2.3. 客户端 POST (client2_web_03)
一个HTTP客户端向Web服务器发送了以下文本序列:HTTP头部、空行、文档。在之前的客户端中,该序列如下:
当时没有文档。还有另一种传递参数的方法,即所谓的 POST 方法。在这种情况下,发送给 Web 服务器的文本序列如下:
这次,对于客户端 GET 而言包含在 HTTP 头部中的参数,在客户端 POST 中则作为头部后发送的文档的一部分。
POST 客户端的脚本如下:
<?php
// 客户端:通过 POST 方法将名字、姓氏、年龄发送给服务器
// 数据
$HOTE = "localhost";
$PORT = 80;
$URL = "/exemples-web/web_03.php";
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// 连接到Web服务器
$connexion = fsockopen($HOTE, $PORT);
// 若出错则返回
if (!$connexion) {
print "Echec de la connexion au site ($HOTE,$PORT) : $erreur";
exit;
}//if
// 将信息发送至服务器 PHP
// 对信息进行编码
$infos = "prenom=" . urlencode(utf8_decode($prenom)) . "&nom=" . urlencode(utf8_decode($nom)) . "&age=" . urlencode("$age");
print "client : infos envoyées au serveur (POST) : $infos\n";
// 通过向 URL $URL 发送(POST)参数来连接
// HTTP协议的头部(headers)必须以空行结尾
// POST
fputs($connexion, "POST $URL HTTP/1.1\n");
// 主机
fputs($connexion, "Host: localhost\n");
// Connection
fputs($connexion,"Connection: close\n");
// Content-type
fputs($connexion, "Content-type: application/x-www-form-urlencoded\n");
// Content-length
// 发送即将发送的信息的大小(字符数)
fputs($connexion, "Content-length: " . strlen($infos) . "\n");
// 发送空行
fputs($connexion, "\n");
// 发送信息
fputs($connexion, $infos);
// 服务器现在将在 $connexion 通道上进行响应。它将发送所有
// 数据后关闭该通道。客户端会读取来自 $connexion
// 直到通道关闭
while ($ligne = fgets($connexion, 1000))
print "$ligne";
// 客户端随后关闭连接
fclose($connexion);
注释
- 第 7 行:Web 服务 URL,客户端 POST 将连接到该服务。该 Web 服务将在后续章节中进行描述。
- 第 8 行:要传递给 Web 服务的参数
- 第 10 行:连接到 Web 服务器
- 第18行:发送给Web服务的参数编码
- 第23行:HTTP命令 POST
- 第25行:HTTP Host命令
- 第27行:HTTP Connection命令
- 第29行:Http Content-type命令。我们之前已经遇到过这个HTTP标头。每次发送文档时都会出现它。发送HTML文档的Web服务器使用HTTP标头
如果发送的是未格式化的文本,则使用标头 HTTP
我们的客户端 POST 发送的文档是一个文本,其格式为 param1=val1¶m2=val2&...。此类文档的类型为 application/x-www-form-urlencoded。我们在此不作解释,因为这将迫使我们解释什么是网页表单。
- 第32行:Content-length指令。我们之前已经遇到过这个HTTP标头。每次发送文档时它都会出现。它表示文档的字节数。
- 第 34 行:空行,标志着 HTTP 头部结束
- 第36行:发送参数
- 第 40-41 行:读取服务器的完整响应
- 第 43 行:关闭连接
10.2.4. 服务器(web_03)
Web 服务 [web_03] 的功能与 Web 服务 [web_02] 相同。它读取客户端 POST 发送的参数,并将其发回给客户端。其代码如下:
<?php
// 错误处理
ini_set("display_errors", "off");
// UTF-8 报头
header("Content-Type: text/plain; charset=utf-8");
// 服务器接收客户端发送的信息
// 此处名=P&姓=N&年龄=A
// 这些信息会自动保存在变量中
// $_POST['prenom'], $_POST['nom'], $_POST['age']
// 将其发回给客户端
// 发送给服务器的参数
$prenom = isset($_POST['prenom']) ? $_POST['prenom'] : "";
$nom = isset($_POST['nom']) ? $_POST['nom'] : "";
$age = isset($_POST['age']) ? $_POST['age'] : "";
// 向客户端发送响应
$réponse = "informations reçues du client [" .
utf8_encode(htmlspecialchars($prenom, ENT_QUOTES)) .
"," . utf8_encode(htmlspecialchars($nom, ENT_QUOTES)) .
"," . utf8_encode(htmlspecialchars($age, ENT_QUOTES)) . "]\n";
print $réponse;
注释
- 第 14-16 行:由客户端 POST 发送的参数将存储在数组 $_POST 中,供接收这些参数的 Web 服务使用。
- 第 6 行:HTTP Content-Type 标头。 令人惊讶的是,在 HTTP 的头部中,并未发现 HTTP Content-Length 头部,该头部本应指示返回给客户端的文档大小。 我们已经看到,Web 服务器默认发送 HTTP 头部。Content-Length 头部正是其中之一。
结果
在 NetBeans 中编写完服务器脚本后,该脚本会立即通过 Apache 服务器以 WampServer 的形式提供。请注意,这是通过配置实现的(参见第 10 段)。启动客户端向服务器发送请求后,将收到以下响应:
- 第2-10行:服务器的响应
- 第2-8行:HTTP头部
- 第 10 行:文档
- 第 6 行:HTTP Content-Length 标头。由于该标头并非由服务器脚本生成,因此是由 Web 服务器生成的。
- 第 8 行:由服务器脚本生成的唯一标头
10.3. 检索服务器环境变量 WEB
服务器脚本在可识别的 Web 环境中运行。该环境存储在字典 $_SERVER 中。首先,我们编写一个服务器应用程序,将其向客户端发送该字典的内容。
10.3.1. 服务器(web_04)
<?php
// 错误处理
ini_set("display_errors", "off");
// UTF-8 头部
header("Content-Type: text/plain; charset=utf-8");
// 向客户端返回服务器环境中可用的变量列表
foreach ($_SERVER as $clé => $valeur) {
print "[$clé,$valeur]\n";
}
- 字典 $_SERVER 中的 (键, 值) 对被发送给客户端。
当客户端为网页浏览器时,显示结果如下:

以下是部分变量的含义(适用于 Windows。在 Linux 系统中,这些变量会有所不同):
CMDE 代表客户端发送的 HTTP 头部。我们可以访问所有这些头部。 | |
服务器脚本运行机器上的可执行文件路径 | |
DOS命令解释器的路径 | |
可执行文件的扩展名 | |
Windows 安装目录 | |
Web 服务器的签名。此处无内容。 | |
Web 服务器的类型 | |
Web 服务器的主机名 | |
Web 服务器的监听端口 | |
Web 服务器的 IP 地址 | |
客户端的地址。此处客户端与服务器位于同一台机器上。 | |
客户端的通信端口 | |
Web 服务器提供的文档树根目录 | |
Web 服务器管理员的电子邮件地址 | |
服务器脚本的完整路径 | |
Web 服务器使用的协议版本 | |
客户端使用的HTTP命令。共有四种:GET、POST、PUT、DELETE | |
随 GET 指令发送的参数 /url?参数 | |
客户端请求的URL。如果浏览器请求URL http://machine[:port]/uri,则会得到REQUEST_URI=uri | |
$_SERVER['SCRIPT_FILENAME']=$_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME'] |
10.3.2. 客户端 (client1_web_04)
客户端仅负责显示服务器发送的所有内容。
<?php
// 数据
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_04.php";
// 在 $HOTE 的 80 端口上建立连接
$connexion = fsockopen($HOTE, $PORT);
// 错误?
if (!$connexion) {
print "Erreur : $erreur\n";
exit;
}
// 连接到 Web 服务器 URL
// HTTP 协议的头部(headers)必须以空行结尾
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// 主机
fputs($connexion, "Host: localhost\n");
// 连接
fputs($connexion,"Connection: close\n");
// 空行
fputs($connexion,"\n");
// 服务器现在将在 $connexion 通道上进行响应。它将发送所有
// 数据,然后关闭通道。因此客户端会读取来自 $connexion
// 直到通道关闭
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
}//同时
// 客户端也关闭连接
fclose($connexion);
// 结束
exit;
结果
10.4. 会话管理 WEB
在之前的客户端/服务器示例中,其工作原理如下:
- 客户端向 Web 服务器的 80 端口建立连接
- 发送文本序列:HTTP 头部、空行、[document]
- 作为响应,服务器发送同类型的字符串
- 服务器关闭与客户端的连接
- 客户端关闭与服务器的连接
如果同一客户端随后向Web服务器发出新请求,客户端与服务器之间将建立新的连接。服务器无法判断连接的客户端是否曾访问过,还是首次请求。在两次连接之间,服务器会“忘记”该客户端。因此,HTTP协议被称为无状态协议。 然而,服务器记住其客户端是有益的。例如,如果应用程序是安全的,客户端会向服务器发送用户名和密码以进行身份验证。如果服务器在两次连接之间“忘记”了该客户端,那么客户端在每次新连接时都必须重新验证身份,这显然是不现实的。
为了追踪客户端,服务器采取以下方式:在收到客户端的首次请求时,服务器会在响应中包含一个标识符,客户端随后必须在每次新请求中将该标识符发回给服务器。借助这个针对每个客户端各不相同的标识符,服务器能够识别客户端。随后,服务器可以为该客户端管理一个存储空间,该空间以一个唯一关联于客户端标识符的文件形式存在。
技术实现过程如下:
- 在响应新客户端时,服务器会在响应中包含以下标头:HTTP Set-Cookie: MotClé=标识符。服务器仅在首次请求时执行此操作。
- 在后续请求中,客户端将通过HTTP标头发送其标识符:MotClé=标识符,以便服务器识别它。
人们可能会问,服务器如何知道它面对的是新客户,而不是曾经访问过的客户。这是通过客户端的HTTP头中是否存在HTTP和Cookie这两个头部来判断的。对于新客户,这些头部不存在。
某个客户端的所有连接统称为一个会话。
10.4.1. 配置文件
为确保 PHP 的会话管理正常运行,需确认其配置正确。在 Windows 系统中,其配置文件为 PHP.ini。 根据运行环境(控制台、Web),配置文件 [PHP.ini] 需在不同的文件夹中查找。要确定这些文件夹,请使用以下脚本:
第 4 行,函数 phpinfo 提供了关于执行该脚本的解释器 PHP 的信息。其中特别指出了所使用的配置文件 [PHP.ini] 的路径。
在控制台环境中,将获得类似以下的结果:
第 2 行:主配置文件为 c:\windows\PHP.ini
第 3 行:辅助配置文件为 C:\serveursSGBD\wamp21\bin\PHP\php5.3.5\PHP.ini。 它允许修改主配置文件中的某些配置选项。
在 Web 环境中,将得到以下结果:

此处的辅助配置文件与控制台环境中的不同。我们将参考后者。该文件中包含一个 session 部分:
- 第 1 行:客户端会话数据将保存到一个文件中
- 第3行:会话数据保存的文件夹。如果该文件夹不存在,系统不会报错,但会话管理将无法正常工作。
- 第4-5行:表明会话标识符由HTTP头Set-Cookie和Cookie管理
- 第6行:Set-Cookie标头将采用以下格式:Set-Cookie: PHPSESSID=identifiant_de_session
- 第 7 行:客户端会话不会自动启动。服务器脚本必须通过 session_start() 指令显式请求。
10.4.2. 服务器 1 (web_05)
对于 Web 服务而言,会话标识符的管理是透明的。该标识符由 Web 服务器管理。 Web 服务可通过 session_start() 指令访问客户端会话。此时,Web 服务可通过 $_SESSION 字典在客户端会话中读写数据。以下代码演示了三个计数器的会话管理。
<?php
// 错误处理
ini_set("display_errors", "off");
// 头信息 UTF-8
header("Content-Type: text/plain; charset=utf-8");
// 打开会话
session_start();
// 将 3 个变量放入会话
if (!isset($_SESSION['N1'])) {
$_SESSION['N1'] = 0;
}
if (!isset($_SESSION['N2'])) {
$_SESSION['N2'] = 10;
}
if (!isset($_SESSION['N3'])) {
$_SESSION['N3'] = 100;
}
// 递增这3个变量
$_SESSION['N1']++;
$_SESSION['N2']++;
$_SESSION['N3']++;
// 向客户端发送信息
print "N1=".$_SESSION['N1']."\n";
print "N2=".$_SESSION['N2']."\n";
print "N3=".$_SESSION['N3']."\n";
// 结束会话
session_close();
- 第 9 行:启动客户端会话
- 第 11-13 行:数组 $_SESSION 是一个键值对字典。放入该字典中的数据会在同一客户端的多次请求中保持持久化。这是服务器上的客户端内存。
- 第11-19行:如果三个计数器N1、N2、N3未处于会话中,则将其加入会话。
- 第21-23行:将它们的值加1
- 第25-27行:将计数器的值发送给客户端
在客户端/服务器关系中,服务器端对客户端会话的管理取决于两个参与方:客户端和服务器:
- 服务器负责在客户端首次请求时向其发送一个标识符
- 客户端负责在每次新请求时返回该标识符。若未返回,服务器将认为这是新客户端,并为新会话生成新的标识符。
结果
我们使用网页浏览器作为客户端。默认情况下(实际上是配置所致),浏览器会将服务器发送的会话标识符如实发回给服务器。随着请求的进行,浏览器将收到服务器发送的三个计数器,并观察到其值逐渐递增。
![]() |
- 变为 [1],首次向 Web 服务发送的请求为 [web_05]
- 变为 [2],第三次请求表明计数器确实已递增。在请求过程中,计数器的值确实被保存了下来。
我们使用Firebug查看服务器与客户端之间交换的HTTP头部。关闭Firefox以结束当前与服务器的会话,重新打开并启用Firebug。我们请求服务 |web_05]:
![]() |
上图显示了服务器在响应客户端首次请求时发送的会话标识符。它使用了 HTTP Set-Cookie 头部。
现在,我们通过在网页浏览器中刷新页面(F5)发起一个新的请求:
![]() |
从上文可以注意到两点:
- Web 浏览器通过 HTTP Cookie 标头返回了会话标识符。
- 而在其响应中,Web 服务不再包含该标识符。现在,客户端负责在每次请求中发送该标识符。
10.4.3. 客户端 1 (client1_web_05)
现在,我们编写一个基于前述服务器脚本的客户端脚本。在会话管理方面,它必须像网页浏览器一样工作:
- 在服务器对其首次请求的响应中,他必须找到服务器发送给他的会话标识符。他知道该标识符位于 HTTP Set-Cookie 头部中。
- 在随后的每次请求中,他都必须将收到的标识符发回给服务器。他将通过 HTTP Cookie 这个标头来完成这一操作。
客户端代码如下:
<?php
// 数据
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_05.php";
// 测试
$cookie = "";
for ($i = 0; $i < 5; $i++) {
list($erreur, $cookie, $N1, $N2, $N3) = connecte($HOTE, $PORT, $urlServeur, $cookie);
print "----------------------------\n";
print "client(erreur,cookie,N1,N2,N3)=[$erreur,$cookie,$N1,$N2,$N3]\n";
print "----------------------------\n";
}
// 结束
exit;
function connecte($HOTE, $PORT, $urlServeur, $cookie) {
// 将客户端连接至 ($HOTE,$PORT,$urlServeur)
// 如果 cookie 不为空,则发送 $cookie
// 显示响应中收到的所有行
// 在 $HOTE 的 80 端口上建立连接
$connexion = fsockopen($HOTE, $PORT);
// 错误?
if (!$connexion)
return array("erreur lors de la connexion au serveur ($HOTE, $PORT)");
// 连接到 $urlserveur
// HTTP 协议的头部(headers)必须以空行结尾
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// 主机
fputs($connexion, "Host: localhost\n");
// 连接
fputs($connexion, "Connection: close\n");
// 如果cookie不为空,则发送该cookie
if ($cookie) {
fputs($connexion, "Cookie: $cookie\n");
}//if
// 发送空行
fputs($connexion, "\n");
// 显示Web服务器的响应
// 并确保获取可能存在的cookie及Ni的值
$N = "";
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
// cookie - 仅在首次响应时
if (!$cookie) {
if (preg_match("/^Set-Cookie: (.*?)\s*$/", $ligne, $champs)) {
$cookie = $champs[1];
}
}
// N1的值
if (preg_match("/^N1=(.*?)\s*$/", $ligne, $champs))
$N1 = $champs[1];
// N2 的值
if (preg_match("/^N2=(.*?)\s*$/", $ligne, $champs))
$N2 = $champs[1];
// N3 的值
if (preg_match("/^N3=(.*?)\s*$/", $ligne, $champs))
$N3 = $champs[1];
}//while
// 关闭连接
fclose($connexion);
// 返回
return array("", $cookie, $N1, $N2, $N3);
}
注释
- 第3-16行:主程序
- 第18-67行:函数connecte
- 第 9-14 行:客户端向服务器发起 5 次请求,并显示计数器 N1、N2 和 N3 的连续值。 如果会话管理正确,这些计数器应在每次新请求时递增1。
- 第 10 行:函数 connecte 使用参数 $HOTE、$PORT、$urlServeur 将客户端连接到 Web 服务。 参数 $cookie 表示会话标识符。首次调用时,该字符串为空。后续调用时,该字符串为服务器在响应客户端首次调用时发送的会话标识符。 函数 connecte 返回三个计数器的值:$N1、 $N2、$N3 这三个计数器的值,会话标识符 $cookie 以及可能出现的错误 $erreur。
- 第18行:函数connecte具有经典HTTP客户端的特征。我们仅对新增内容进行说明。
- 第 30-40 行:发送 HTTP 头部。
- 第 36-38 行:如果已知会话标识符,则将其发送给服务器
- 第 44-66 行:处理服务器发送的所有文本行
- 第 47-51 行:如果尚未获取会话标识符,则通过正则表达式从 HTTP Set-Cookie 头部中获取。
- 第53-54行:计数器N1同样通过正则表达式获取
- 第 56-57 行、第 59-60 行:计数器 N2 和 N3 的获取方式同上
- 第 63 行:关闭与服务器的连接。
- 第 65 行:以数组形式返回结果。
结果
执行客户端脚本后,NetBeans 控制台将显示以下内容:
- 第 5 行:在首次响应中,服务器发送了会话标识符。在随后的响应中,它不再发送该标识符。
- 可以清楚地看到,Web 服务器在处理客户端的请求时,会保留 (N1,N2,N3) 的值。这就是所谓的会话跟踪。
以下两个示例表明,也可以保存数组或对象的值。
10.4.4. 服务器 2 (web_06)
以下服务器脚本展示了如何将数组或字典放入会话中。
<?php
// 错误处理
ini_set("display_errors", "off");
// 头部UTF-8
header("Content-Type: text/plain; charset=utf-8");
// 建立会话
session_start();
// 保存数组和字典
// 初始化或修改数组
if (isset($_SESSION['tableau'])) {
for ($i = 0; $i < count($_SESSION['tableau']); $i++) {
$_SESSION['tableau'][$i]++;
}
} else {
for ($i = 0; $i < 10; $i++) {
$_SESSION['tableau'][$i] = $i * 10;
}
}
// 初始化或修改字典
if (isset($_SESSION['dico'])) {
foreach (array_keys($_SESSION['dico']) as $clé) {
$_SESSION['dico'][$clé]++;
}
} else {
$_SESSION['dico'] = array("zéro" => 0, "dix" => 10, "vingt" => 20);
}
// 向客户端发送信息
print "tableau=" . join(",", $_SESSION['tableau']) . "\n";
print "dico=";
foreach ($_SESSION['dico'] as $clé => $valeur) {
print "($clé,$valeur) ";
}
print "\n";
注释
- 第 17-19 行:如果数组尚未存在于会话中,则将其初始化放入会话
- 第12-15行:如果已存在,则将其元素值加1
- 第27行:如果该数组尚未存入会话,则将其存入会话
- 第22-25行:如果该字典已存在,则将其数值元素加1
- 第 30-35 行:将数组和字典发送给客户端
10.4.5. 客户端 2 (client1_web_06)
<?php
// 数据
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_06.php";
// 测试
$cookie = "";
for ($i = 0; $i < 5; $i++) {
connecte($HOTE, $PORT, $urlServeur, $cookie);
}
// 结束
exit;
function connecte($HOTE, $PORT, $urlServeur, &$cookie) {
// 将客户端连接至 ($HOTE,$PORT,$urlServeur)
// 如果 cookie 不为空,则发送 $cookie
// 显示响应中收到的所有行
// Cookie 通过引用传递,以便在
// 被调用程序与调用程序之间共享
// 在 $HOTE 的 $PORT 端口上建立连接
$connexion = fsockopen($HOTE, $PORT);
// 错误?
if (!$connexion)
return array("erreur lors de la connexion au serveur ($HOTE, $PORT)");
// HTTP 协议的头部(headers)必须以空行结尾
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// 主机
fputs($connexion, "Host: localhost\n");
// 连接
fputs($connexion, "Connection: close\n");
// 如果 cookie 不为空,则发送
if ($cookie) {
fputs($connexion, "Cookie: $cookie\n");
}
// 发送空行
fputs($connexion, "\n");
// 显示Web服务器的响应
// 并确保获取可能存在的 Cookie
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
// Cookie - 仅在首次响应时
if (!$cookie) {
if (preg_match("/^Set-Cookie: (.*?)\s*$/", $ligne, $champs)) {
$cookie = $champs[1];
}
}
}
// 关闭连接
fclose($connexion);
// 返回
return "";
}
该客户端的代码与已注释的客户端代码类似。
结果
10.4.6. 服务器 3 (web_07)
以下服务器脚本展示了如何将一个对象放入会话中。
<?php
// 错误处理
ini_set("display_errors", "off");
// UTF-8 标头
header("Content-Type: text/plain; charset=utf-8");
// 打开会话
session_start();
// 初始化或修改 Personne 对象
if (isset($_SESSION['personne'])) {
$personne = $_SESSION['personne'];
// 增加年龄
$personne->setAge($personne->getAge() + 1);
} else {
// 定义人员
$_SESSION['personne'] = new Personne("paul", "langévin", 10);
}
// 向客户显示
print "personne=".$_SESSION['personne']."\n";
// 结束
exit;
// ----------------------------------------------------------------
class Personne {
// 类的属性
private $prénom;
private $nom;
private $âge;
// 获取器和设置器
public function getPrénom() {
return $this->prénom;
}
public function getNom() {
return $this->nom;
}
public function getAge() {
return $this->âge;
}
public function setPrénom($prénom) {
$this->prénom = $prénom;
}
public function setNom($nom) {
$this->nom = $nom;
}
public function setAge($age) {
$this->âge = $age;
}
// 构造函数
function __construct($prénom, $nom, $âge) {
// 通过 set 方法
$this->setPrénom($prénom);
$this->setNom($nom);
$this->setAge($âge);
}
// 方法toString
function __toString() {
return "[$this->prénom,$this->nom,$this->âge]";
}
}
注释
- 第 17 行:如果会话中尚未存在 Personne 类型的对象,则将其放入会话中。
- 第 11-15 行:如果该对象已存在,则将其年龄增加 1
- 第 20 行:将类型为 Personne 的对象发送给客户端。
10.4.7. 客户端 3 (client1_web_07)
<?php
// 数据
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_07.php";
// 测试
$cookie = "";
for ($i = 0; $i < 5; $i++) {
connecte($HOTE, $PORT, $urlServeur, $cookie);
}//if
// 结束
exit;
function connecte($HOTE, $PORT, $urlServeur, &$cookie) {
...
}
注释
- 第 15 行:函数 connecte 与前一个客户端脚本中的函数相同
结果




















