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 服务器、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/web-examples] 文件夹中。如果某个服务器脚本命名为 S.php,则将通过 URL [http://localhost/exemples-web/S.php] 向 Web 服务器请求该脚本。随后,系统将返回文档 [<WampServer>/www/web-examples/S.php]。
![]() |
要使用 NetBeans 创建服务器端脚本 ,请按照以下步骤操作:
![]() |
- 在 [1] 中,创建一个新项目
- 在 [2] 中,选择 [PHP] 类别和 [PHP 应用程序] 项目
![]() |
- 在 [3] 中,我们为项目命名
- 在 [4] 中,我们为项目选择一个文件夹
- 在 [5] 中,我们指定脚本必须由本地 Web 服务器执行(脚本的 URL 将采用 http://localhost/... 的形式)。该本地 Web 服务器将使用 WampServer 中的 Apache Web 服务器。
- 在 [6] 中,我们指定项目的 URL。这里,我们决定项目中名为 S.php 的脚本将通过 URL [http://localhost/exemples-web/S.php] 进行访问。 基于上述内容,这意味着文件系统中 S.php 脚本的路径将为 [<WampServer>/www/web-examples/S.php]。这就是 [7] 中所指定的内容。在此,我们指定项目中的任何 S.php 脚本都应复制到 Apache Web 服务器目录结构中。
- 在 [8] 中,新建项目。
让我们编写一个测试脚本:
![]() |
- 在 [1] 中,我们在 [web-examples] 项目中创建了第一个 PHP 脚本
- 在 [2] 中,我们为其命名
- 在 [3] 中,创建完成后,我们为其添加以下内容
接下来,WampServer 必须处于运行状态。
![]() |
- 在 [4] 中,我们运行 Web 脚本 [example1.php]。随后 NetBeans 将启动计算机的默认浏览器,并指示其显示 URL [http://localhost/exemples-web/exemple1.php] [5]
- 在 [6] 中,浏览器显示了服务器脚本发送给客户端的内容。
接下来,我们将遇到两种类型的 Web 客户端:
- 如上所述的浏览器。我们注意到,Web 服务器发送的响应格式为:HTTP 头部、空行、正文。浏览器仅显示正文。
- 一个PHP脚本,它将显示完整的响应:HTTP头部、空行、正文。
接下来,
- 服务器端脚本将像上文的 [example1.php] 那样编写
- 客户端脚本将像我们迄今为止编写的控制台脚本那样编写。
10.1. 客户端/服务器日期/时间应用程序
10.1.1. 服务器(web_01)
<?php
// time: number of milliseconds since 01/01/1970
// date-time display format
// d: 2-digit day
// m: 2-digit month
// y: 2-digit year
// H: hour 0.23
// i : minutes
// s: seconds
print date("d/m/y H:i:s",time());
基本上,上面的 PHP 脚本会在屏幕上显示当前时间。然而,当由 Web 服务器执行时,通常与屏幕关联的流 1 会被重定向到连接服务器与客户端的通道上。因此,在 Web 环境中,上面的脚本会将当前时间作为文本发送给客户端。
让我们在 NetBeans 中运行此脚本:
![]() |
- 在[1]处,我们运行该脚本。随后将启动一个网页浏览器。
- 在 [2] 中,网页浏览器请求的 URL
- 在[3]处,服务器脚本发送的文本
客户端浏览器使用HTTP协议与Web服务器进行通信。我们已经描述了该协议的结构。
客户端发送的文本行可分为三部分:HTTP 头部、空行以及文档。发送给 Web 服务器的文档通常为空,或者由一组形式为 parami=vali 的参数组成,其中 vali 是用户在 HTML 表单中输入的值。
服务器的响应具有相同的结构:HTTP 头部、空行以及文档,其中文档此时即为客户端浏览器所请求的文档。如果客户端发送了参数,返回的文档通常取决于这些参数。
使用 Firefox 浏览器,您可以查看客户端与 Web 服务器之间实际交换的数据。有一个名为 Firebug 的 Firefox 插件,可让您追踪这些数据交换。 Firebug 可通过网址 [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 头部。我们可以获取通信的源代码,即实际交换的文本内容 [1]。随后,我们得到以下源代码:
![]() |
要为 Web 服务器编写客户端脚本,我们只需模拟浏览器的行为即可。在与服务器建立连接后,客户端脚本可以发送上文所示的 8 行请求。实际上,并非所有内容都必不可少,我们只需发送以下三行:
- 第 1 行:指定请求的文档和使用的 HTTP 协议
- 第 2 行:提供客户端脚本的主机名
- 第 3 行:表示数据交换完成后,客户端将关闭与服务器的连接
现在让我们看看服务器的响应。我们知道它是通过 PHP 脚本 [web_01.php] 生成的。在上文中,我们看到了响应的 HTTP 头部。而 [web01.php] 脚本的代码显示,这些头部并非由它生成的。回顾一下服务器脚本的配置:
![]() |
实际上是 Web 服务器生成了响应的 HTTP 头。服务器脚本本身也可以生成这些头部。稍后我们将看到一个相关示例。
我们提到过,Web 服务器的响应采用以下格式:HTTP 头部、空行、文档。如果文档是文本文件,我们可以在 Firebug 的 [Response] 选项卡中查看它:
![]() |
此响应由 [web_01.php] 脚本生成。
10.1.2. 一个客户端(client1_web_01)
现在我们将为前面的服务编写一个客户端脚本。我们知道客户端必须:
- 与 Web 服务器建立连接
- 发送文本:HTTP 头部、空行
- 读取服务器的完整响应,直到服务器关闭与客户端的连接
- 关闭与服务器的连接
客户端脚本在 NetBeans 控制台环境中运行:
![]() |
- 在 [1] 中,客户端脚本 [client1_web_01.php] 被包含在 NetBeans 项目 [examples] 中
- 在 [2] 中,NetBeans 项目 [examples] 的属性
- 在 [3] 中,NetBeans 项目 [examples] 以“命令行”模式运行,我们也将其称为“控制台”模式。
客户端脚本代码如下:
<?php
// data
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_01.php";
// open a connection on port 80 of $HOTE
$connexion = fsockopen($HOTE, $PORT);
// mistake?
if (!$connexion) {
print "Erreur : $erreur\n";
exit;
}
// protocol HTTP headers must end with an empty line
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// Host
fputs($connexion, "Host: localhost\n");
// Connection
fputs($connexion,"Connection: close\n");
// blank line
fputs($connexion,"\n");
// the server will now respond on channel $connexion. It will send all
// then close the channel. The client therefore reads everything that arrives from $connexion
// until the channel closes
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
}//while
// the customer in turn closes the connection
fclose($connexion);
// end
exit;
注释
- 第 8 行:建立与服务器的连接
- 第 16 行:HTTP GET 命令
- 第 18 行:HTTP Host 命令
- 第 20 行:HTTP Connection 命令
- 第 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
// retrieve information sent by a web server
// data
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_01.php";
// open a connection on port 80 of $HOTE
$connexion = fsockopen($HOTE, $PORT);
// mistake?
if (!$connexion) {
print "Erreur : $erreur\n";
exit;
}
// protocol HTTP headers must end with an empty line
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// Host
fputs($connexion, "Host: localhost\n");
// Connection
fputs($connexion,"Connection: close\n");
// blank line
fputs($connexion,"\n");
// the server will now respond on channel $connexion. It will send all
// then close the channel. The client therefore reads everything that arrives from $connexion
// until it finds the line it's looking for in the form dd/mm/yy 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)) {
// we retrieve the # fields
array_shift($champs); // e// removes the 1st element from the array fields
// we retrieve the 6 fields in 6 variables
list($j, $m, $a, $h, $i, $s) = $champs;
// result display
print "\ndateheure=[$j,$m,$a,$h,$i,$s]\n";
}////if
}//while
// the customer in turn closes the connection
fclose($connexion);
// end
exit;
10.2. 服务器检索客户端发送的参数
在 HTTP 协议中,客户端有两种方法向 Web 服务器传递参数:
- 它以表单形式请求服务 URL
GET url?param1=val1¶m2=val2¶m3=val3… HTTP/1.0
其中有效值必须先进行编码,以便将某些保留字符替换为相应的十六进制值。
- 它以以下形式请求服务 URL:
POST url HTTP/1.0
随后,在发送给服务器的 HTTP 头部中,包含以下头部:
客户端发送的其余标头以空行结尾。随后,它可以以以下形式发送其数据:
其中有效值必须像 GET 方法一样预先进行编码。发送给服务器的字符数必须为 N,其中 N 是标头中声明的值
用于检索客户端发送的上述参数的 PHP 脚本从数组中获取其值:
- 对于 GET 请求,使用 $_GET["parami"]
- 对于 POST 请求,使用 $_POST["parami"]
10.2.1. GET 客户端 (client1_web_02)
下面的 PHP 脚本向服务器发送三个参数 [last_name, first_name, age]。
<?php
// client: sends firstname,lastname,age to the server using the GET method
// data
$HOTE = "localhost";
$PORT = 80;
$URL = "/exemples-web/web_02.php";
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// web server connection
$connexion = fsockopen($HOTE, $PORT);
// return if error
if (!$connexion) {
print "Echec de la connexion au site ($HOTE,$PORT) : $erreur";
exit;
}//if
// information sent to server PHP
// information is encoded
$infos = "prenom=" . urlencode(utf8_decode($prenom)) . "&nom=" . urlencode(utf8_decode($nom)) . "&age=" . urlencode("$age");
// console monitoring
print "infos envoyées au serveur (GET)=$infos\n";
print "URL demandée=[$URL?$infos]\n\n";
// protocol HTTP headers must end with an empty line
// GET
fputs($connexion, "GET $URL?$infos HTTP/1.1\n");
// Host
fputs($connexion, "Host: localhost\n");
// Connection
fputs($connexion,"Connection: close\n");
// blank line
fputs($connexion,"\n");
// the server will now respond on channel $connexion. It will send all
// then close the channel. The client reads everything from $connexion until the channel is closed
while ($ligne = fgets($connexion, 1000))
print "$ligne";
// the customer in turn closes the connection
fclose($connexion);
注释
- 第 7 行:服务器脚本的 URL
- 第 8 行:3 个参数的值
- 第 10 行:建立与 Web 服务器的连接
- 第 18 行:对 3 个参数进行编码。我们正在使用一个采用 UTF-8 字符编码的 NetBeans 脚本进行操作。因此,第 8 行中的 3 个参数值采用 UTF-8 编码。utf8_decode 函数将它们的编码转换为 ISO-8859-1。 完成此操作后,即可对这些参数进行 URL 编码。所有非字母字符均被替换为 %xx,其中 xx 代表该字符的十六进制值。空格则被替换为 + 号。
- 第 24 行:请求的 URL 为 $URL?$infos,其中 $infos 的格式为 last_name=val1&first_name=val2&age=val3。
10.2.2. 服务器(web_02)
服务器仅负责显示接收到的内容。
<?php
// error management
ini_set("display_errors", "off");
// server retrieves information sent by the client
// here firstname=P&lastname=N&age=A
// this information is automatically available in the
// $_GET['prenom'], $_GET['nom'], $_GET['age']
// we send them back to the customer
// uTF-8 header
header("Content-Type: text/plain; charset=utf-8");
// parameters sent to server
$prenom = isset($_GET['prenom']) ? $_GET['prenom'] : "";
$nom = isset($_GET['nom']) ? $_GET['nom'] : "";
$age = isset($_GET['age']) ? $_GET['age'] : "";
// customer response
$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 数组中获取三个参数。
- 第 21 行:我们构建将作为响应发送给客户端的字符串。 某些字符在 HTML 中具有特殊含义,必须替换为 HTML 实体才能显示。htmlspecialchars($string) 会将 $string 中的所有此类字符替换为对应的等效字符。例如,$ 字符会被替换为 &。然后,由于我们在第 13 行指定响应为 UTF-8 文本,因此我们将检索到的值编码为 UTF-8。
- 第 25 行:将响应发送给客户端
现在让我们在 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
// client: sends firstname,lastname,age to the server using the POST method
// data
$HOTE = "localhost";
$PORT = 80;
$URL = "/exemples-web/web_03.php";
list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// web server connection
$connexion = fsockopen($HOTE, $PORT);
// return if error
if (!$connexion) {
print "Echec de la connexion au site ($HOTE,$PORT) : $erreur";
exit;
}//if
// information sent to server PHP
// information is encoded
$infos = "prenom=" . urlencode(utf8_decode($prenom)) . "&nom=" . urlencode(utf8_decode($nom)) . "&age=" . urlencode("$age");
print "client : infos envoyées au serveur (POST) : $infos\n";
// connect to the URL $URL by posting (POST) parameters to it
// protocol HTTP headers must end with an empty line
// POST
fputs($connexion, "POST $URL HTTP/1.1\n");
// Host
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
// send the size (number of characters) of the information to be sent
fputs($connexion, "Content-length: " . strlen($infos) . "\n");
// send an empty line
fputs($connexion, "\n");
// we send the news
fputs($connexion, $infos);
// the server will now respond on channel $connexion. It will send all
// then close the channel. The client reads everything that arrives from $connexion
// until the channel closes
while ($ligne = fgets($connexion, 1000))
print "$ligne";
// the customer in turn closes the connection
fclose($connexion);
注释
- 第 7 行:POST 客户端将连接到的 Web 服务的 URL。该 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。我们在此不作详细解释,因为这需要先说明什么是 Web 表单。
- 第 32 行:Content-length 指令。我们之前已经遇到过这个 HTTP 头。每次发送文档时都会包含它。它表示文档中的字节数。
- 第 34 行:表示 HTTP 头部结束的空行
- 第 36 行:发送参数
- 第 40–41 行:从服务器读取完整的响应
- 第 43 行:关闭连接
10.2.4. 服务器 (web_03)
Web 服务 [web_03] 的功能与 Web 服务 [web_02] 相同。它读取 POST 客户端发送的参数,并将它们发回给客户端。其代码如下:
<?php
// error management
ini_set("display_errors", "off");
// uTF-8 header
header("Content-Type: text/plain; charset=utf-8");
// server retrieves information sent by the client
// here firstname=P&lastname=N&age=A
// this information is automatically available in the
// $_POST['prenom'], $_POST['nom'], $_POST['age']
// we send them back to the customer
// parameters sent to server
$prenom = isset($_POST['prenom']) ? $_POST['prenom'] : "";
$nom = isset($_POST['nom']) ? $_POST['nom'] : "";
$age = isset($_POST['age']) ? $_POST['age'] : "";
// customer response
$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 客户端发送的参数将存储在接收这些参数的 Web 服务的 $_POST 数组中。
- 第 6 行:HTTP Content-Type 标头。您可能会惊讶地发现 HTTP 标头中没有 HTTP Content-Length 标头,该标头用于指示发回给客户端的文档大小。我们之前看到,Web 服务器默认会发送 HTTP 标头,而 Content-Length 标头正是其中之一。
在 NetBeans 中编写完服务器脚本后,该脚本将立即通过 WampServer Apache 服务器提供服务。请记住,这是通过配置实现的(参见第 10 段)。我们启动客户端,向服务器发送请求,随后收到以下响应:
- 第2-10行:服务器的响应
- 第2-8行:HTTP头
- 第 10 行:文档
- 第 6 行:HTTP Content-Length 标头。由于该标头并非由服务器脚本生成,因此是由 Web 服务器生成的。
- 第 8 行:唯一由服务器脚本生成的标头
10.3. 获取 Web 服务器环境变量
服务器脚本运行在可访问的 Web 环境中。该环境存储在 $_SERVER 字典中。首先,我们编写一个服务器应用程序,将该字典的内容发送给客户端。
10.3.1. 服务器(web_04)
<?php
// error management
ini_set("display_errors", "off");
// uTF-8 header
header("Content-Type: text/plain; charset=utf-8");
// returns to the client the list of variables available in the server environment
foreach ($_SERVER as $clé => $valeur) {
print "[$clé,$valeur]\n";
}
- 来自 $_SERVER 字典的 (键, 值) 对会被发送给客户端。
当客户端是网页浏览器时,得到的结果如下:

以下是部分变量的含义(适用于 Windows 系统。在 Linux 系统中,这些变量会有所不同):
CMDE 代表客户端发送的 HTTP 头部。我们可以访问所有这些头部。 | |
服务器脚本运行所在机器上可执行文件的路径 | |
DOS命令解释器的路径 | |
可执行文件的扩展名 | |
Windows 安装文件夹 | |
Web 服务器签名。此处无内容。 | |
Web 服务器的类型 | |
Web 服务器主机的互联网名称 | |
Web 服务器的监听端口 | |
Web 服务器主机的 IP 地址 | |
客户端的 IP 地址。在此情况下,客户端与服务器位于同一台机器上。 | |
客户端的通信端口 | |
Web 服务器所提供文档的目录树根目录 | |
Web 服务器管理员的电子邮件地址 | |
服务器脚本的完整路径 | |
Web 服务器使用的 HTTP 协议版本 | |
客户端使用的 HTTP 方法。共有四种:GET、POST、PUT、DELETE | |
随 GET 请求发送的参数 /url?parameters | |
客户端请求的 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
// data
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_04.php";
// open a connection on port 80 of $HOTE
$connexion = fsockopen($HOTE, $PORT);
// mistake?
if (!$connexion) {
print "Erreur : $erreur\n";
exit;
}
// connect to the Web server on a URL
// protocol HTTP headers must end with an empty line
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// Host
fputs($connexion, "Host: localhost\n");
// Connection
fputs($connexion,"Connection: close\n");
// blank line
fputs($connexion,"\n");
// the server will now respond on channel $connexion. It will send all
// then close the channel. The client therefore reads everything that arrives from $connexion
// until the channel closes
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
}//while
// the customer in turn closes the connection
fclose($connexion);
// end
exit;
10.4. Web 会话管理
在之前的客户端/服务器示例中,流程如下:
- 客户端向Web服务器的80端口建立连接
- 它发送文本序列:HTTP 头部、空行、[文档]
- 作为响应,服务器发送同类型的序列
- 服务器关闭与客户端的连接
- 客户端关闭与服务器的连接
如果同一客户端随后不久向 Web 服务器发出新请求,客户端与服务器之间将建立新的连接。服务器无法判断连接的客户端是否曾访问过,还是这是首次请求。在两次连接之间,服务器会“忘记”其客户端。因此,HTTP 协议被称为无状态协议。然而,服务器记住其客户端是有用的。 例如,如果应用程序是安全的,客户端会向服务器发送用户名和密码以进行身份验证。如果服务器在连接之间“忘记”了该客户端,那么客户端就必须在每次建立新连接时都重新进行身份验证,这显然是不现实的。
为了追踪客户端,服务器采取以下做法:当客户端发出初始请求时,服务器会在响应中包含一个标识符,客户端随后必须在每次后续请求中将该标识符发回。通过这个标识符(每个客户端的标识符都是唯一的),服务器可以识别该客户端。然后,服务器可以以文件的形式为该客户端维护记录,该文件与客户端的标识符唯一关联。
从技术角度看,其工作原理如下:
- 在响应新客户端时,服务器会在响应中包含 HTTP 标头 Set-Cookie: Key=Identifier。此操作仅在首次请求时执行。
- 在后续请求中,客户端将通过 HTTP Cookie 头部发送其标识符:Key=Identifier,以便服务器能够识别它。
有人可能会疑惑,服务器如何分辨当前是新客户端还是回访客户端?正是客户端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:\DBServers\wamp21\bin\PHP\php5.3.5\PHP.ini。它允许您修改主配置文件中的某些配置选项。
在 Web 环境中,将得到以下结果:

此处的辅助配置文件与控制台环境中的不同。我们将重点探讨后者。该文件中包含一个 session 部分:
- 第 1 行:客户端会话数据将保存到文件中
- 第3行:保存会话数据的目录。如果该目录不存在,则不会报告错误,且会话管理功能将无法正常工作。
- 第 4-5 行:表示会话 ID 由 HTTP Set-Cookie 和 Cookie 头部管理
- 第 6 行:Set-Cookie 标头将采用以下格式:Set-Cookie: PHPSESSID=session_id
- 第 7 行:客户端会话不会自动启动。服务器脚本必须使用 session_start() 函数显式地启动它。
10.4.2. 服务器 1 (web_05)
对于 Web 服务而言,会话 ID 的管理是透明的。该标识符由 Web 服务器管理。Web 服务通过 session_start() 函数访问客户端的会话。从那时起,Web 服务即可通过 $_SESSION 数组读取和写入客户端的会话。以下代码演示了三个计数器的会话管理。
<?php
// error management
ini_set("display_errors", "off");
// uTF-8 header
header("Content-Type: text/plain; charset=utf-8");
// log in
session_start();
// on met 3 variables en session
if (!isset($_SESSION['N1'])) {
$_SESSION['N1'] = 0;
}
if (!isset($_SESSION['N2'])) {
$_SESSION['N2'] = 10;
}
if (!isset($_SESSION['N3'])) {
$_SESSION['N3'] = 100;
}
// incrementing the 3 variables
$_SESSION['N1']++;
$_SESSION['N2']++;
$_SESSION['N3']++;
// sending information to the customer
print "N1=".$_SESSION['N1']."\n";
print "N2=".$_SESSION['N2']."\n";
print "N3=".$_SESSION['N3']."\n";
// end of session
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] 发送请求:
![]() |
在上图中,我们可以看到服务器在响应客户端的首次请求时发送的会话 ID。它使用了 HTTP Set-Cookie 头部。
现在,让我们通过在网页浏览器中刷新(F5)页面来发起一个新的请求:
![]() |
在此,我们会注意到两点:
- Web 浏览器会通过 HTTP Cookie 头将会话 ID 发回。
- 在响应中,Web 服务不再包含此标识符。现在,客户端有责任在每次请求中发送它。
10.4.3. 客户端 1 (client1_web_05)
接下来,我们将基于之前的服务器端脚本编写一个客户端脚本。在会话管理方面,它必须像 Web 浏览器那样工作:
- 在服务器对首次请求的响应中,它必须找到服务器发送的会话 ID。它知道该 ID 位于 HTTP Set-Cookie 头中。
- 对于后续的每次请求,它必须将收到的标识符发回给服务器。它将使用 HTTP Cookie 头来完成此操作。
客户端代码如下:
<?php
// data
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_05.php";
// tests
$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";
}
// end
exit;
function connecte($HOTE, $PORT, $urlServeur, $cookie) {
// connects client to ($HOTE,$PORT,$urlServeur)
// sends the $cookie cookie if it is non-empty
// displays all lines received in response
// open a connection on port 80 of $HOTE
$connexion = fsockopen($HOTE, $PORT);
// mistake?
if (!$connexion)
return array("erreur lors de la connexion au serveur ($HOTE, $PORT)");
// connect to $urlserveur
// protocol HTTP headers must end with an empty line
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// Host
fputs($connexion, "Host: localhost\n");
// Connection
fputs($connexion, "Connection: close\n");
// send cookie if non-empty
if ($cookie) {
fputs($connexion, "Cookie: $cookie\n");
}////if
// send empty line
fputs($connexion, "\n");
// the web server response is displayed
// and take care to retrieve any cookies and Ni
$N = "";
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
// cookie - only on 1st response
if (!$cookie) {
if (preg_match("/^Set-Cookie: (.*?)\s*$/", $ligne, $champs)) {
$cookie = $champs[1];
}
}
// n1 value
if (preg_match("/^N1=(.*?)\s*$/", $ligne, $champs))
$N1 = $champs[1];
// n2 value
if (preg_match("/^N2=(.*?)\s*$/", $ligne, $champs))
$N2 = $champs[1];
// n3 value
if (preg_match("/^N3=(.*?)\s*$/", $ligne, $champs))
$N3 = $champs[1];
}////while
// close the connection
fclose($connexion);
// return
return array("", $cookie, $N1, $N2, $N3);
}
注释
- 第3–16行:主程序
- 第 18–67 行:connect 函数
- 第 9–14 行:客户端向服务器发起五次请求,并显示计数器 N1、N2 和 N3 的连续值。如果会话管理正确,这些计数器应在每次新请求时递增 1。
- 第 10 行:`connecte` 函数使用参数 `$HOTE`、`$PORT` 和 `$urlServeur` 将客户端连接到 Web 服务。参数 `$cookie` 代表会话 ID。首次调用时,它是一个空字符串。后续调用时,它是服务器在响应客户端首次调用时发送的会话 ID。 `connecte` 函数返回三个计数器 `$N1`、`$N2`、`$N3` 的值,会话 ID `$cookie`,以及 `$erreur` 中的任何错误。
- 第 18 行:`connecte` 函数具有标准 HTTP 客户端的特性。我们仅对新功能进行说明。
- 第 30–40 行:发送 HTTP 头部。
- 第 36–38 行:如果已知会话 ID,则将其发送给服务器
- 第 44–66 行:处理服务器发送的所有文本行
- 第 47–51 行:如果尚未获取会话 ID,则使用正则表达式从 HTTP Set-Cookie 头中获取。
- 第 53–54 行:同样使用正则表达式获取 N1 计数器
- 第 56–57 行、第 59–60 行:计数器 N2 和 N3 也采用相同方法
- 第 63 行:关闭与服务器的连接。
- 第 65 行:将结果作为数组返回。
执行客户端脚本后,NetBeans 控制台将显示以下内容:
- 第5行:在首次响应中,服务器发送了会话ID。在后续响应中,它不再发送该ID。
- 显然,Web 服务器会在客户端的多次请求中保留 (N1, N2, N3) 的值。这被称为会话跟踪。
以下两个示例表明,您还可以保存数组或对象的值。
10.4.4. 服务器 2 (web_06)
以下服务器脚本演示了数组或字典可以存储在会话中。
<?php
// error management
ini_set("display_errors", "off");
// uTF-8 header
header("Content-Type: text/plain; charset=utf-8");
// log in
session_start();
// save a table and a dictionary
// initialize or modify the table
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;
}
}
// initialize or modify the dictionary
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);
}
// sending information to the customer
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
// data
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_06.php";
// tests
$cookie = "";
for ($i = 0; $i < 5; $i++) {
connecte($HOTE, $PORT, $urlServeur, $cookie);
}
// end
exit;
function connecte($HOTE, $PORT, $urlServeur, &$cookie) {
// connects client to ($HOTE,$PORT,$urlServeur)
// sends the $cookie cookie if it is non-empty
// displays all lines received in response
// the cookie is passed by reference to be shared between
// the called program and the calling program
// open a connection on the $PORT port of $HOTE
$connexion = fsockopen($HOTE, $PORT);
// mistake?
if (!$connexion)
return array("erreur lors de la connexion au serveur ($HOTE, $PORT)");
// protocol HTTP headers must end with an empty line
// GET
fputs($connexion, "GET $urlServeur HTTP/1.1\n");
// Host
fputs($connexion, "Host: localhost\n");
// Connection
fputs($connexion, "Connection: close\n");
// send cookie if non-empty
if ($cookie) {
fputs($connexion, "Cookie: $cookie\n");
}
// send empty line
fputs($connexion, "\n");
// the web server response is displayed
// and we take care to recover any cookie
while ($ligne = fgets($connexion, 1000)) {
print "$ligne";
// cookie - only on 1st response
if (!$cookie) {
if (preg_match("/^Set-Cookie: (.*?)\s*$/", $ligne, $champs)) {
$cookie = $champs[1];
}
}
}
// close the connection
fclose($connexion);
// return
return "";
}
客户端代码与已注释掉的客户端代码类似。
10.4.6. 服务器 3 (web_07)
以下服务器脚本演示了如何将对象存储在会话中。
<?php
// error management
ini_set("display_errors", "off");
// uTF-8 header
header("Content-Type: text/plain; charset=utf-8");
// log in
session_start();
// initialize or modify a Personne object
if (isset($_SESSION['personne'])) {
$personne = $_SESSION['personne'];
// increment age
$personne->setAge($personne->getAge() + 1);
} else {
// we define the person
$_SESSION['personne'] = new Personne("paul", "langévin", 10);
}
// customer display
print "personne=".$_SESSION['personne']."\n";
// end
exit;
// ----------------------------------------------------------------
class Personne {
// class attributes
private $prénom;
private $nom;
private $âge;
// getters and setters
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;
}
// manufacturer
function __construct($prénom, $nom, $âge) {
// we go through sets
$this->setPrénom($prénom);
$this->setNom($nom);
$this->setAge($âge);
}
// method toString
function __toString() {
return "[$this->prénom,$this->nom,$this->âge]";
}
}
评论
- 第 17 行:如果会话中还不存在 Person 对象,则将其添加进去。
- 第 11-15 行:如果该对象已存在,则将其年龄增加 1
- 第 20 行:我们将 Person 对象发送给客户端。
10.4.7. 客户端 3 (client1_web_07)
<?php
// data
$HOTE = "localhost";
$PORT = 80;
$urlServeur = "/exemples-web/web_07.php";
// tests
$cookie = "";
for ($i = 0; $i < 5; $i++) {
connecte($HOTE, $PORT, $urlServeur, $cookie);
}//if
// end
exit;
function connecte($HOTE, $PORT, $urlServeur, &$cookie) {
...
}
注释
- 第 15 行:connect 函数与前一个客户端脚本中的完全相同




















