Skip to content

10. PHP 服务器

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

第9.2节中描述的Web客户端让我们得以了解HTTP协议的一部分。

Image

在最简单的版本中,客户端与服务器的交互如下:

  • 客户端与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
2
3
GET /exemples-web/web01.php HTTP/1.1
Host: localhost
Connection: close
  • 第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
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Date: Wed, 17 Aug 2011 13:35:00 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Content-Length: 17
Connection: close
Content-Type: text/html

17/08/11 13:35:00

注释

  • 第 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;

结果

HTTP/1.1 200 OK
Date: Wed, 17 Aug 2011 14:14:51 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Content-Length: 17
Connection: close
Content-Type: text/html

17/08/11 14:14:51
dateheure=[17,08,11,14,14,51]

10.2. 服务器接收客户端发送的参数

在 HTTP 协议中,客户端有两种方法向 Web 服务器传递参数:


GET url?param1=val1&param2=val2&param3=val3… HTTP/1.0

其中,vali中的值必须事先进行编码,以便将某些保留字符替换为相应的十六进制值。

  • 以以下形式向服务请求 URL:

POST url HTTP/1.0

随后,在发送给服务器的HTTP头部中插入以下头部:

Content-length=N

客户端发送的后续头部以空行结束。随后,它可以将数据以以下形式发送:

val1&param2=val2&param3=val3…

其中,vali 的值必须像 GET 方法那样,事先进行编码。发送给服务器的字符数必须为 N,其中 N 是标头中声明的值

Content-length=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 服务器发送的标头为
Content-Type: text/html

,表示响应为 HTML 格式的文本。在此处,响应将是以 UTF-8 编码的无格式文本:

Content-Type: text/plain; charset=utf-8

HTTP 头部必须在服务器响应之前发送。因此,在上文中,对 header 函数的调用将位于任何 print 指令之前。

  • 第16-18行:从数组$_GET中获取3个参数。
  • 第21行:构建将作为响应发送给客户端的字符串。某些字符在HTML中具有特殊含义,必须替换为HTML中的实体才能显示。 htmlspecialchars($string) 将所有这些字符替换为字符串 $string 中的等效字符。 例如,字符 $ 将变为 &amp。随后,正如我们在第 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]。收到以下响应:

infos envoyées au serveur (GET)=prenom=jean-paul&nom=de+la+h%FBche&age=45
URL demandée=[/exemples-web/web_02.php?prenom=jean-paul&nom=de+la+h%FBche&age=45]

HTTP/1.1 200 OK
Date: Wed, 17 Aug 2011 14:31:01 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Content-Length: 59
Connection: close
Content-Type: text/plain; charset=utf-8

informations reçues du client [jean-paul,de la hûche,45]
  • 第 1 行:3 个参数的编码。可以看到字符 û 已变为 %FB。
  • 第 12 行:服务器的响应

10.2.3. 客户端 POST (client2_web_03)

一个HTTP客户端向Web服务器发送了以下文本序列:HTTP头部、空行、文档。在之前的客户端中,该序列如下:

GET /url?paramètres HTTP/1.1
… autres entêtes HTTP
ligne vide

当时没有文档。还有另一种传递参数的方法,即所谓的 POST 方法。在这种情况下,发送给 Web 服务器的文本序列如下:

POST /url HTTP/1.1
… autres entêtes HTTP
ligne vide
paramètres

这次,对于客户端 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标头
Content-type : text/html

如果发送的是未格式化的文本,则使用标头 HTTP

Content-type : text/plain

我们的客户端 POST 发送的文档是一个文本,其格式为 param1=val1&param2=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 段)。启动客户端向服务器发送请求后,将收到以下响应:

client : infos envoyées au serveur (POST) : prenom=jean-paul&nom=de+la+h%FBche&age=45
HTTP/1.1 200 OK
Date: Sat, 20 Aug 2011 12:55:19 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Content-Length: 59
Connection: close
Content-Type: text/plain; charset=utf-8

informations reçues du client [jean-paul,de la hûche,45]
  • 第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 中的 (键, 值) 对被发送给客户端。

当客户端为网页浏览器时,显示结果如下:

Image

以下是部分变量的含义(适用于 Windows。在 Linux 系统中,这些变量会有所不同):

HTTP_CMDE
CMDE 代表客户端发送的 HTTP 头部。我们可以访问所有这些头部。
PATH
服务器脚本运行机器上的可执行文件路径
COMSPEC
DOS命令解释器的路径
PATHEXT
可执行文件的扩展名
WINDIR
Windows 安装目录
SERVER_SIGNATURE
Web 服务器的签名。此处无内容。
SERVER_SOFTWARE
Web 服务器的类型
SERVER_NAME 
Web 服务器的主机名
SERVER_PORT
Web 服务器的监听端口
SERVER_ADDR
Web 服务器的 IP 地址
REMOTE_ADDR
客户端的地址。此处客户端与服务器位于同一台机器上。
REMOTE_PORT
客户端的通信端口
DOCUMENT_ROOT
Web 服务器提供的文档树根目录
SERVER_ADMIN
Web 服务器管理员的电子邮件地址
SCRIPT_FILENAME
服务器脚本的完整路径
SERVER_PROTOCOL
Web 服务器使用的协议版本
REQUEST_METHOD
客户端使用的HTTP命令。共有四种:GET、POST、PUT、DELETE
QUERY_STRING
随 GET 指令发送的参数 /url?参数
REQUEST_URI
客户端请求的URL。如果浏览器请求URL http://machine[:port]/uri,则会得到REQUEST_URI=uri
SCRIPT_NAME
$_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;

结果

HTTP/1.1 200 OK
Date: Sat, 20 Aug 2011 14:11:58 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Content-Length: 1353
Connection: close
Content-Type: text/plain; charset=utf-8

[HTTP_HOST,localhost]
[HTTP_CONNECTION,close]
[PATH,C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files\Microsoft SQL Server\90\Tools\binn\;...;]
[SystemRoot,C:\Windows]
[COMSPEC,C:\Windows\system32\cmd.exe]
[PATHEXT,.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC]
[WINDIR,C:\Windows]
[SERVER_SIGNATURE,]
[SERVER_SOFTWARE,Apache/2.2.17 (Win32) PHP/5.3.5]
[SERVER_NAME,localhost]
[SERVER_ADDR,127.0.0.1]
[SERVER_PORT,80]
[REMOTE_ADDR,127.0.0.1]
[DOCUMENT_ROOT,C:/serveursSGBD/wamp21/www/]
[SERVER_ADMIN,admin@localhost]
[SCRIPT_FILENAME,C:/serveursSGBD/wamp21/www/exemples-web/web_04.php]
[REMOTE_PORT,54552]
[GATEWAY_INTERFACE,CGI/1.1]
[SERVER_PROTOCOL,HTTP/1.1]
[REQUEST_METHOD,GET]
[QUERY_STRING,]
[REQUEST_URI,/exemples-web/web_04.php]
[SCRIPT_NAME,/exemples-web/web_04.php]
[PHP_SELF,/exemples-web/web_04.php]
[REQUEST_TIME,1313849518]

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] 需在不同的文件夹中查找。要确定这些文件夹,请使用以下脚本:

1
2
3
4
<?php

// 信息 PHP
phpinfo();

第 4 行,函数 phpinfo 提供了关于执行该脚本的解释器 PHP 的信息。其中特别指出了所使用的配置文件 [PHP.ini] 的路径。

在控制台环境中,将获得类似以下的结果:

1
2
3
4
5
6
Configuration File (PHP.ini) Path => C:\Windows
Loaded Configuration File => C:\serveursSGBD\wamp21\bin\PHP\php5.3.5\PHP.ini
Scan this dir for additional .ini files => (none)
Additional .ini files parsed => (none)

第 2 行:主配置文件为 c:\windows\PHP.ini

第 3 行:辅助配置文件为 C:\serveursSGBD\wamp21\bin\PHP\php5.3.5\PHP.ini。 它允许修改主配置文件中的某些配置选项。

在 Web 环境中,将得到以下结果:

Image

此处的辅助配置文件与控制台环境中的不同。我们将参考后者。该文件中包含一个 session 部分:

[Session]
session.save_handler = files
session.save_path = "C:/serveursSGBD/wamp21/tmp"
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.serialize_handler = PHP
session.cache_expire = 180
session.hash_function = 0
  • 第 1 行:客户端会话数据将保存到一个文件中
  • 第3行:会话数据保存的文件夹。如果该文件夹不存在,系统不会报错,但会话管理将无法正常工作。
  • 第4-5行:表明会话标识符由HTTP头Set-CookieCookie管理
  • 第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 控制台将显示以下内容:

HTTP/1.1 200 OK
Date: Sun, 21 Aug 2011 13:59:17 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Set-Cookie: PHPSESSID=ohiqtkv7hu2b26kdshjtqms9p7; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 18
Connection: close
Content-Type: text/plain; charset=utf-8

N1=1
N2=11
N3=101
----------------------------
client(erreur,cookie,N1,N2,N3)=[,PHPSESSID=ohiqtkv7hu2b26kdshjtqms9p7; path=/,1,11,101]
----------------------------
HTTP/1.1 200 OK
Date: Sun, 21 Aug 2011 13:59:18 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 18
Connection: close
Content-Type: text/plain; charset=utf-8

N1=2
N2=12
N3=102
----------------------------
client(erreur,cookie,N1,N2,N3)=[,PHPSESSID=ohiqtkv7hu2b26kdshjtqms9p7; path=/,2,12,102]
----------------------------
HTTP/1.1 200 OK
Date: Sun, 21 Aug 2011 13:59:18 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 18
Connection: close
Content-Type: text/plain; charset=utf-8

N1=3
N2=13
N3=103
----------------------------
client(erreur,cookie,N1,N2,N3)=[,PHPSESSID=ohiqtkv7hu2b26kdshjtqms9p7; path=/,3,13,103]
----------------------------
HTTP/1.1 200 OK
Date: Sun, 21 Aug 2011 13:59:18 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 18
Connection: close
Content-Type: text/plain; charset=utf-8

N1=4
N2=14
N3=104
----------------------------
client(erreur,cookie,N1,N2,N3)=[,PHPSESSID=ohiqtkv7hu2b26kdshjtqms9p7; path=/,4,14,104]
----------------------------
HTTP/1.1 200 OK
Date: Sun, 21 Aug 2011 13:59:18 GMT
Server: Apache/2.2.17 (Win32) PHP/5.3.5
X-Powered-By: PHP/5.3.5
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 18
Connection: close
Content-Type: text/plain; charset=utf-8

N1=5
N2=15
N3=105
----------------------------
client(erreur,cookie,N1,N2,N3)=[,PHPSESSID=ohiqtkv7hu2b26kdshjtqms9p7; path=/,5,15,105]
  • 第 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 "";
}

该客户端的代码与已注释的客户端代码类似。

结果

HTTP/1.1 200 OK
...
Set-Cookie: PHPSESSID=6lvttr0uhpj5q3sl91h4h7p322; path=/
...

tableau=0,10,20,30,40,50,60,70,80,90
dico=(zéro,0) (dix,10) (vingt,20) 

HTTP/1.1 200 OK
...

tableau=1,11,21,31,41,51,61,71,81,91
dico=(zéro,1) (dix,11) (vingt,21) 

HTTP/1.1 200 OK
...

tableau=2,12,22,32,42,52,62,72,82,92
dico=(zéro,2) (dix,12) (vingt,22) 

HTTP/1.1 200 OK
...

tableau=3,13,23,33,43,53,63,73,83,93
dico=(zéro,3) (dix,13) (vingt,23) 

HTTP/1.1 200 OK
...

tableau=4,14,24,34,44,54,64,74,84,94
dico=(zéro,4) (dix,14) (vingt,24) 

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 与前一个客户端脚本中的函数相同

结果

HTTP/1.1 200 OK
...

personne=[paul,langévin,10]

HTTP/1.1 200 OK
...

personne=[paul,langévin,11]

HTTP/1.1 200 OK
...

personne=[paul,langévin,12]

HTTP/1.1 200 OK
...

personne=[paul,langévin,13]

HTTP/1.1 200 OK
...

personne=[paul,langévin,14]