Skip to content

17. Web 服务

:此处所说的“Web 服务”,是指任何向客户端(在后续示例中为控制台脚本)提供原始数据的 Web 应用程序。我们并不关注任何特定的技术——例如 REST(表征状态转移)或 SOAP(简单对象访问协议)——这些技术以明确定义的格式提供或多或少的原始数据。 REST 返回 JSON,而 SOAP 返回 XML。这些技术各自精确定义了客户端必须如何向服务器发起请求,以及服务器响应必须采用的格式。在本课程中,我们将对客户端请求和服务器响应的性质采取更灵活的态度。不过,所编写的脚本和使用的工具与 REST 技术类似。

17.1. 简介

由于 PHP 程序可由 Web 服务器执行,此类程序便成为能够为多个客户端提供服务的服务器端程序。从客户端的角度来看,调用 Web 服务即相当于请求该服务的 URL。客户端可以使用任何语言编写,包括 PHP。在后一种情况下,我们将使用刚才介绍过的网络函数。 我们还需要了解如何与 Web 服务“通信”,即理解 Web 服务器与其客户端之间通信所使用的 HTTP 协议。这正是“链接”一节的宗旨。

“链接”一节中描述的 Web 客户端让我们得以探索 HTTP 协议的一部分。

Image

在最简单的形式下,客户端与服务端之间的交互过程如下:

  • 客户端向 Web 服务器的 80 端口建立连接;
  • 它请求一个文档;
  • Web 服务器发送所请求的文档并关闭连接;
  • 随后客户端关闭连接;

文档可以是多种类型:HTML格式的文本、图片、视频……它可以是现成的文档(静态文档),也可以是由脚本即时生成的文档(动态文档)。在后一种情况下,我们称之为Web编程。用于动态生成文档的脚本可以使用多种语言编写:PHP、Python、Perl、Java、Ruby、C#、VB.NET……

下文中,我们将使用 PHP 脚本动态生成文本文档。

Image

  • [1]中,客户端与服务器建立连接,请求一个PHP脚本,并可能向该脚本发送参数,也可能不发送;
  • [2]中,Web服务器使用PHP解释器执行该PHP脚本。脚本生成一个文档并将其发送给客户端[3];
  • 服务器关闭连接。客户端也执行同样的操作;

Web 服务器可以同时处理多个客户端。

[Laragon] 软件包中,Web 服务器采用 Apache 服务器,这是 Apache 基金会(http://www.apache.org/)提供的一款开源服务器。在以下应用场景中,必须启动 [Laragon]

Image

这将启动 Apache Web 服务器以及 MySQL 数据库管理系统。

Web 服务器执行的脚本将使用 NetBeans 工具编写。到目前为止,我们编写的是在控制台环境中执行的 PHP 脚本:

Image

用户通过控制台请求执行 PHP 脚本并接收结果。

在接下来的客户端/服务器应用程序中:

  • 客户端脚本在控制台环境中执行;
  • 服务器脚本在 Web 环境中执行;

Image

服务器端的 PHP 脚本不能随意放置在文件系统的任意位置。实际上,Web 服务器会根据配置中指定的位置来查找用户请求的静态和动态文档。 Laragon 的默认配置会将文档搜索范围限定在 <Laragon>/www 文件夹内,其中 <Laragon> 代表 Laragon 的安装目录( )。因此,如果 Web 客户端通过 URL [http://localhost/D] 请求文档 D,Web 服务器将返回位于路径 [<Laragon>/www/D] 下的文档 D。

在以下示例中,我们将服务器脚本放置在 [www/php7/scripts-web] 文件夹中。如果某个服务器脚本命名为 S.php,则将通过 URL [http://localhost/php7/scripts-web/S.php] 向 Web 服务器请求该脚本。随后,系统将返回文档 [<Laragon>/www/php7/scripts-web/S.php]

Image

  • [1] 中,是 [<laragon>/www] 文件夹;
  • [2] 中,是 [php7/scripts-web] 文件夹;

要使用 NetBeans 创建服务器脚本,我们将按以下步骤进行:

Image

  • [1-2] 中,我们创建一个新项目
  • [3-4] 中,选择 [PHP] 类别和 [PHP 应用程序] 项目

Image

  • [5] 中,输入项目名称;
  • [6] 中,指定文件系统中的项目文件夹。请注意,此处位于 [<laragon>/www] 文件夹中,位置正确;
  • [7-8] 中,接受默认值;
  • [9-10] 中,接受提供的默认值。在 [10] 中,请注意我们将放置在此项目中的脚本的 URL 将以路径 [http://localhost/php7/scripts-web/] 开头;

Image

  • [11] 中,系统向您提供了基于 PHP 编写的 Web 框架。一旦 Web 应用程序规模扩大,这些框架就必不可少;
  • [12] 中,您可以使用 [Composer] 工具添加 PHP 库。我们在 Laragon [终端] 窗口中使用过两次该工具:
    • 安装 [SwiftMailer] 库,该库支持发送电子邮件;
    • 安装 [php-mime-mail-parser] 库,该库可用于读取电子邮件;
  • [13]中,一旦项目创建向导确认完成,该项目便会出现在[13]的“项目”选项卡中;

17.2. 编写静态页面

注意:在本指南的后续部分中,[Laragon] 必须处于运行状态。

我们将演示如何使用 NetBeans 创建静态 HTML(超文本标记语言)页面:

Image

  • [1-5]中,我们创建了一个名为[01]的文件夹;

Image

Image

  • [6-12] 中,我们创建了一个名为 [example-01.html] 的 HTML 文件;

[example-01.html] 文件生成时包含以下预填充内容(2019年5月):


<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

让我们将其内容更新如下:


<!DOCTYPE html>
<html>
    <head>
        <title>PHP7 par l'exemple</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div><b>Ceci est un exemple de page statique</b></div>
    </body>
</html>

我们更改了页面标题(第 4 行)及其内容(第 9 行)。

现在让我们让 Laragon 的 Apache 服务器显示这个 HTML 页面:

Image

  • [1-2]中,我们让 Laragon Apache 服务器显示该页面;
  • [3] 中,显示页面的 URL;
  • [4] 中,是我们修改过的标题;
  • [5] 中,是我们修改后的内容;

显示的页面是一个静态页面:您可以在浏览器中随意刷新(按 F5 键),显示的内容始终保持不变。

大多数浏览器都提供了访问客户端与服务器之间交换数据的途径,具体方法如“链接”一节所述。在 Firefox 浏览器中(截至 2019 年 5 月),按 F12 键即可访问这些数据:

Image

[1]中所述,让我们刷新页面(F5):

Image

  • [2]中,浏览器加载的文档:我们选中它;

Image

  • [5]中,选中待分析的文档;
  • [3-4] 中,我们请求查看客户端/服务器交互;
  • [6] 中,显示这些交互;

Image

  • [7] 中,选择“标头”选项卡;
  • [8]中,浏览器请求的URL;
  • [9] 中,发送到服务器的命令是 [GET http://localhost/php7/scripts-web/01/exemple-01.html HTTP/1.1]
  • [10] 中,浏览器(客户端)随后发送的 HTTP 头部;
  • [11] 中,是服务器响应的 HTTP 头部;

Image

  • [12-14] 中,是 HTTP 头部之后发送的服务器响应;
  • [14] 中,我们可以看到客户端浏览器已收到我们构建的 HTML 页面。随后,它解析了该代码并显示如下内容:

Image

17.3. 使用 PHP 创建动态页面

现在我们将用 PHP 编写一个动态页面:

Image

Image

  • [1-8] 中,我们创建了一个页面 [example-01.php]

[example-01.php] 文件生成时已预填充如下内容(2019年5月):


<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <?php
        // put your code here
        ?>
    </body>
</html>

我们将上述代码修改如下:


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // time : nb de millisecondes entre le moment présent et le 01/01/1970
        // format affichage date-heure
        // d : jour sur 2 chiffres
        // m : mois sur 2 chiffres
        // y : année sur 2 chiffres
        // H : heure 0,23
        // I : minutes
        // s: secondes
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

评论

  • 第 5 行:我们更改了页面标题;
  • 第17行:打印当前日期和时间;

基本上,上面的 PHP 脚本会将当前时间打印到控制台。然而,当由 Web 服务器执行时,[print] 语句的输出——通常会导向脚本的执行控制台——会被重定向到连接服务器与客户端的通道中。因此,在 Web 环境中,上面的脚本会将当前时间作为文本发送给客户端,在本例中即浏览器。

让我们运行 [example-01.php] 脚本:

Image

  • [3] 中,是从 Apache Web 服务器请求的 URL;
  • [4] 中,是我们修改过的页面标题;
  • [5] 中,是 [print] 语句生成的内容;

这是一个动态页面,因为如果你在浏览器中多次刷新页面(按 F5 键),其内容会发生变化(时间会更新)。

浏览器已接收到了一个 HTML 数据流。要查看它,您需要在浏览器中显示该页面的源代码:

Image

  • 要访问菜单 [1],请在浏览器中右键单击页面;
  • [2] 中,页面的 URL [example-01.php] 需在前面添加前缀 [view-source:] [3]
  • [4] 中,浏览器显示的 HTML 内容;

因此,必须牢记:旨在由 Web 服务器执行的 PHP 脚本必须生成 HTML 数据流。

现在让我们通过 F12 查看服务器发送给客户端浏览器的 HTTP 头部:

Image

  • [3] 中,出现了一个在请求静态页面时不存在的 HTTP 头。该头表明服务器的响应是由 PHP 脚本生成的;

我们已经看到,服务器的响应(此处的HTML输出)可以由PHP脚本生成。该脚本还可以生成HTTP头以及服务器响应中的几乎所有元素。

17.4. HTML 基础

本章不会深入探讨 PHP 网页编程。相关章节中已介绍了 MVC Web 应用程序的开发。本章主要关注 Web 服务:即通过 Web 服务器向其他 PHP 客户端提供数据的 PHP 页面。尽管如此,我们认为向读者介绍一些 HTML 基础知识仍会有所帮助。

Web 浏览器可以显示各种文档,其中最常见的是 HTML(超文本标记语言)文档。这些文档由采用 <tag>text</tag> 形式的标签进行格式化的文本组成。因此,文本 <b>important</b> 将以粗体显示“important”一词。还有一些独立标签,例如 <hr/> 标签,它会显示一条水平线。我们不会逐一讲解 HTML 文本中可能出现的所有标签。 有许多所见即所得(WYSIWYG)软件,允许您无需编写任何 HTML 代码即可构建网页。这些工具会自动为使用鼠标和预定义控件创建的布局生成 HTML 代码。因此,您可以(使用鼠标)将表格插入页面,然后查看软件生成的 HTML 代码,从而了解在网页上定义表格应使用的标签。就是这么简单。 此外,掌握 HTML 知识至关重要,因为动态 Web 应用程序必须自行生成 HTML 代码并发送给 Web 客户端。该代码是通过编程方式生成的,而您当然必须清楚该生成什么内容,才能确保客户端收到他们想要的网页。

简而言之,您无需精通整个HTML语言即可开始进行网页编程。不过,这些知识是必要的,并且可以通过使用DreamWeaver等所见即所得(WYSIWYG)网页编辑器来掌握。探索HTML奥秘的另一种方法是浏览网页,查看那些包含您尚未接触过的有趣元素的页面源代码。

请看以下示例,其中列举了网页文档中常见的一些元素,例如:

  • 表格;
  • 图片;
  • 一个链接。

Image

HTML 文档通常具有以下形式:

<html> <head> <title>标题</title> ... </head> <body 属性> ... </body></html>

整个文档由 <html>…</html> 标签包围。它由两部分组成:

  1. <head>…</head>:这是文档中不可显示的部分。它向将显示该文档的浏览器提供信息。该部分通常包含 <title>…</title> 标签,用于设置浏览器标题栏中显示的文本。 该部分还可能包含其他标签,尤其是定义文档关键词的标签,这些关键词随后会被搜索引擎使用。该部分还可能包含脚本,通常采用 JavaScript 或 VBScript 编写,这些脚本将由浏览器执行。
  1. <body attributes>…</body>:这是浏览器将显示的部分。该部分包含的 HTML 标签向浏览器说明了文档的“预期”视觉布局。每个浏览器对这些标签的解释方式各不相同。因此,两个浏览器可能以不同的方式显示同一个网页文档。这通常是网页设计师面临的挑战之一。

本示例文档的 HTML 代码如下:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>Quelques balises HTML</title>
    </head>
 
    <body style="background-image: url(images/standard.jpg)">
        <h1 style="text-align: left">Quelques balises HTML</h1>
        <hr />
 
        <table border="1">
            <thead>
                <tr>
                    <th>Colonne 1</th>
                    <th>Colonne 2</th>
                    <th>Colonne 3</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>cellule(1,1)</td>
                    <td style="text-align: center;">cellule(1,2)</td>
                    <td>cellule(1,3)</td>
                </tr>
                <tr>
                    <td>cellule(2,1)</td>
                    <td>cellule(2,2)</td>
                    <td>cellule(2,3</td>
                </tr>
            </tbody>
        </table>
        <br/><br/>
        <table border="0">
            <tr>
                <td>Une image</td>
                <td>
                    <img border="0" src="images/cerisier.jpg"/></td>
            </tr>
            <tr>
                <td>Le site de Polytech'Angers</td>
                <td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
            </tr>
        </table>
    </body>
</html>
HTML
HTML 标签及示例
文档标题
<title>一些 HTML 标签</title> (第 5 行)
当文档显示时,文本 [一些 HTML 标签] 将出现在浏览器的标题栏中
水平线
<hr />:显示一条水平线(第 10 行)
表格
<table attributes>….</table>:用于定义表格(第 12、32 行)
<thead>…</thead>:定义列标题(第13、19行)
<tbody>…</tbody>:定义表格内容(第 20、31 行)
<tr attributes>…</tr>:定义行(第 21、25 行)
<td attributes>…</td>:用于定义单元格(第 22 行)
示例
<table border="1">…</table>:border 属性定义表格边框的粗细
<td style="text-align: center;">单元格(1,2)</td>(第23行):定义一个内容为单元格(1,2)的单元格。该内容将水平居中(text-align: center)。
图片
<img border="0" src="images/cherrytree.jpg"/>(第 38 行):定义了一张无边框(border="0")的图片,其源文件位于 Web 服务器上的 [images/cherrytree.jpg](src="images/cherrytree.jpg")。 该链接位于可通过 URL http://localhost/php7/scripts-web/01/balises.html 访问的网页文档中。因此,浏览器将请求 URL http://localhost/php7/scripts-web/01/images/cerisier.jpg 以获取此处引用的图片。
链接
<a href="http://www.polytech-angers.fr/fr/index.html">here</a>(第 42 行):使文本“here”作为指向 URL http://www.polytech-angers.fr/fr/index.html 的链接。
页面背景
<body style="background-image: url(images/standard.jpg)">(第8行):表示用作页面背景的图片位于Web服务器上的URL [images/standard.jpg]。在本示例中,浏览器将请求URL http://localhost/php7/scripts-web/01/images/standard.jpg 以获取此背景图片。

在这个简单的示例中,我们可以看到,为了构建整个文档,浏览器必须向服务器发出三次请求:

  1. http://localhost/php7/scripts-web/01/images/balises.html 以获取文档的 HTML 源代码
  2. http://localhost/php7/scripts-web/01/images/cerisier.jpg 用于获取图片 cerisier.jpg
  3. http://localhost/php7/scripts-web/01/images/standard.jpg 用于获取背景图片 standard.jpg

这可以通过客户端与服务器之间的网络流量(在浏览器中按 F12)来查看:

Image

  • [3-5]中,我们可以看到浏览器发出的三个请求;

17.5. 将静态页面变为动态

让我们演示如何将 HTML 页面 [example-01.html] 变为动态页面。复制内容

Image

我们将 [example-01.html] 的内容复制到了文件 [page-01.php] 中。如果运行 [2] 这个 Web 脚本,浏览器中会显示如下内容:

Image

  • [3] 中,请求的 URL;
  • [4] 中,页面标题;
  • [5] 中,页面内容;

如果查看浏览器接收到的代码,我们会发现如下内容:

Image

  • [7] 中,[example-01.php] 脚本中放置的 HTML 代码

PHP 解释器解析了 [page-01.php] 脚本,并生成了与静态页面 [example-01.html] 完全相同的 HTML 输出。在 [page-01.php] 脚本中,并没有 PHP 代码,只有 HTML。这告诉我们一个道理:当 PHP 解释器在 PHP 脚本中发现 HTML 时,它会保持原样,并将它原样发送给客户端。

现在,让我们在 [page-01.php] 脚本中添加一些 PHP 指令,以便 PHP 解释器有任务可执行:


<!DOCTYPE html>
<html>
    <head>
        <title><?php print $page->title ?></title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div><b><?php print $page->contents ?></b></div>
    </body>
</html>

在第 4 行和第 9 行,我们添加了 PHP 代码以动态生成页面标题和内容。这里,我们假设变量 [$page] 是一个包含待显示数据的对象。

如果运行这段新代码,浏览器中将显示以下结果:

Image

  • [1] 中,是请求的 URL;
  • [2] 中,由于变量 [$page] 未定义,因此无法显示页面标题;
  • [3] 中,内容同样无法显示;

现在,让我们编写以下 Web 脚本 [example-02.php]

Image

脚本 [example-02.php] 内容如下:


<?php
 
// define the page elements to be displayed
$page=new \stdclass();
$page->title="Un nouveau titre";
$page->contents="Un nouveau contenu généré dynamiquement";
// display [page-01]
require_once "page-01.php";
  • 第 4-6 行:我们定义了 [$page] 对象;
  • 第 8 行:包含脚本 [page-01.php]。随后将解析该脚本中的代码:
    • 变量 [$page] 现已定义,PHP 解释器将使用它;
    • 来自 [page-01.php] 的 HTML 代码将原样发送给客户端;
    • PHP [print] 操作的结果将被包含在发送给客户端的文本流中;

现在,如果我们运行 Web 脚本 [example-02.php],浏览器中将显示以下内容:

Image

如果查看浏览器接收到的文本内容:

Image

  • 原先位于 [2] [3] 中的 PHP 代码已被两个 [print] 命令的执行结果所替换;

从这个例子中,我们可以得出两个关键点:

  • 面向浏览器的 HTML 页面可以被封装在仅包含 HTML 代码和少量由 PHP 代码生成的动态部分的 PHP 脚本中。这些页面中的 PHP 代码应尽可能少;
  • 所有用于生成 HTML 页面中动态数据的逻辑,都必须封装在纯 PHP 脚本中,且这些脚本不应包含任何页面呈现代码(HTML、CSS、JavaScript 等);

这使得任务分离成为可能:

  • 创建待显示网页的任务(HTML、CSS、JavaScript 等);
  • 我们正在构建的 Web 应用程序逻辑的任务。该逻辑可以采用三层架构来实现,这与我们处理控制台脚本时完全一致;

接下来,我们将构建具体的 Web 脚本;

  • 这些脚本仅向客户端发送数据,而不包含任何呈现元素(HTML、CSS、JavaScript)。因此,它们将作为数据服务器而非网页存在;
  • 这些 Web 脚本的客户端将是控制台脚本,它们将检索服务器发送的数据并进行处理;

17.6. 客户端/服务器日期/时间应用程序

目前我们的配置如下:

Image

我们将编写:

  • 一个 Web 脚本 [1],用于将当前日期和时间发送给客户端;
  • 一个控制台脚本 [2],它将作为 Web 脚本的客户端:它将接收 Web 脚本发送的日期和时间,并在控制台显示出来;

Image

  • [1] 中,即 Web 脚本 [date-time-server.php]
  • [2] 中,控制台脚本 [date-time-client],它是 Web 脚本的客户端;

17.6.1. 服务器脚本

我们在前面的章节中已经编写了一个生成当前日期和时间的 Web 脚本。该脚本如下所示 [example-01.php]


<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Exemple de page dynamique</title>
    </head>
    <body>
        <?php
        // time : nb de millisecondes depuis 01/01/1970
        // format affichage date-heure
        // d: jour sur 2 chiffres
        // m: mois sur 2 chiffres
        // y : année sur 2 chiffres
        // H : heure 0,23
        // i : minutes
        // s: secondes
        print "<b>Date et heure du jour : </b>" . date("d/m/y H:i:s", time());
        ?>
    </body>
</html>

我们说过要编写数据服务器:不带 HTML 标记的原始数据。那么服务器脚本 [date-time-server.php] 将如下所示:


<?php
 
// set header HTP [Content-Type]
header('Content-Type: text/plain; charset=UTF-8');
//
// send date and time
// 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());
  • 第 4 行:我们设置了 HTTP 头部 [Content-Type],用于告知客户端将接收的文档类型。此前,[Content-Type] 的设置为:[Content-Type: text/html; charset=UTF-8]。 在此,我们告知客户端该文档是不含 HTML 标记的纯文本。这对我们的控制台客户端并不重要,因为它不会使用此标头。但对浏览器客户端而言则更为重要,因为它们确实会使用此标头;

让我们运行这个服务器端脚本:

Image

如果我们在浏览器中检查服务器的响应(F12),可以看到在[5]处是服务器脚本设置的HTTP头部,而在[8]处则是接收到的文本文档;

Image

17.6.2. 客户端脚本

在前一节中,我们开发了几个 HTTP 客户端。 我们可以利用它们来获取由服务器脚本 [date-time-server.php] 发送的文本文件。但我们不会这样做。正如我们处理 SMTP 和 IMAP 协议时所做的那样,我们将使用一个第三方库,即 Symfony 框架 [https://symfony.com/doc/master/components/http_client.html] 中的 [HttpClient] 组件。

与前两个库一样,我们使用 [Composer] 工具来安装 Symfony 的 [HttpClient] 组件。在 Laragon [终端] 窗口中(参见相关章节),输入以下命令:

Image

  • [3] 中,请确认您当前位于 [<laragon>/www/] 目录下,其中 <laragon> 是 Laragon 的安装目录;
  • [4] 中,执行 [composer] 命令以安装 Symfony [HttpClient] 库;
  • [5] 中,由于该机器上已安装 [HttpClient] 库,因此未进行安装;
  • [6-7] 中,[<laragon>/www/vendor/symfony] 目录下会出现新的文件夹;

[5] 不同,您应该看到类似以下内容:


C:\myprograms\laragon-lite\www
? composer require symfony/http-client
Using version ^4.3 for symfony/http-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-php73 (v1.11.0): Downloading (100%)
  - Installing symfony/http-client-contracts (v1.1.1): Downloading (100%)
  - Installing psr/log (1.1.0): Loading from cache
  - Installing symfony/http-client (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files

请确保 [<laragon>/www/vendor] 文件夹已包含在项目的 [Include Path] 中(参见相关章节):

Image

完成此操作后,我们可以编写控制台脚本 [date-time-client.php]

Image

该控制台脚本 [date-time-client.php] 将使用以下 JSON 文件 [config-date-time-client.json]

1
2
3
{
    "url": "http://localhost/php7/scripts-web/02/date-time-server.php"
}
  • 第 2 行:服务器脚本的 URL;

客户端脚本 [date-time-client.php] 将如下所示:


<?php
 
// service customer date / time
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-date-time-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
 
try {
  // query
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // retrieve the body of the reply
  $content = $response->getContent();
  // we display it
  print "---Réponse du serveur : [$content]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
  exit;
}

注释

  • 第 10 行:与之前处理库的方式一样,我们加载文件 [<laragon>/www/vendor/autoload.php]
  • 第 11 行:声明我们将要使用的 [HttpClient] 类;
  • 第 13–24 行:从 [$config] 字典中获取脚本的配置;
  • 第 27 行:创建一个 [HttpClient] 类型的对象;
  • 第 31 行:使用 GET 请求 [GET URL HTTP/1.1] 向服务器脚本发送请求。此操作为异步操作。程序执行将直接跳至第 33 行,无需等待响应;
  • 第 33 行:获取响应状态。该状态位于服务器返回的第一个 HTTP 头中。因此,如果该头为 [HTTP/1.1 200 OK],则响应状态为 200。此操作是阻塞的:只有当客户端从服务器接收到了完整的响应后,执行才会继续;
  • 第 37 行:请求响应的 HTTP 头部;
  • 第 42 行:获取服务器返回的文档:我们知道该文档是文本。
  • 第 45–49 行:若发生错误,则显示错误信息;

当客户端脚本执行时(必须运行 Laragon 才能访问服务器脚本),控制台将显示以下结果:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Thu, 30 May 2019 14:42:03 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
content-length: 17
content-type: text/plain; charset=UTF-8
---Réponse du serveur : [30/05/19 14:42:03]

我们在第 8 行成功获取了当前日期和时间。

您可能想知道客户端脚本向服务器发送了什么。为此,我们将使用我们的通用 TCP 服务器(参见“链接”部分):

Image

  • 位于 [1] 中的 utilities 文件夹内;
  • [2]中,TCP服务器正在100端口上运行;
  • [3]中,正在等待通过键盘输入的命令;

我们修改脚本的配置文件 [date-time-client.php]


{
    "url": "http://localhost:100/php7/scripts-web/02/date-time-server.php"
}

这次,客户端通过 100 端口联系服务器 [localhost]。因此,我们的通用 TCP 服务器将被调用。当我们运行控制台脚本 [date-time-client.php] 时,通用 TCP 服务器的控制台会发生如下变化:

Image

  • [3] 中,客户端脚本构建的 HTTP GET 请求;
  • [4] 中,是控制台脚本的签名;
  • [5] 中,服务器对客户端脚本的响应。请注意,这不是一个有效的 HTTP 响应:
    • 应包含 HTTP 头部;
    • 随后应有一行空行;
    • 然后是发送给客户端的文本文件;
  • [6] 中,我们关闭与客户端脚本的连接,以便它检测到已收到完整的响应;

在客户端脚本中,控制台显示如下内容:

Image

  • [7] 中,Symfony 客户端接收到的内容;

17.6.3. 服务器脚本 – 版本 2

默认情况下,用于编写 Web 脚本的 PHP 函数并非面向对象的。因此,在服务器端,我们不得不混合使用传统的 PHP 类和函数。为了实现更一致的编码风格,我们将使用 Symfony 框架中的 [HttpFoundation] 库。它将 Web 服务所需的所有传统 PHP 函数封装成一个由类和接口组成的系统。 该库的文档可访问 [https://symfony.com/doc/current/components/http_foundation.html](2019年5月)。

要在 Laragon 终端中安装该库,请按照以下步骤操作(参见相关章节):

Image

  • [2-3]:确保您位于 [<laragon>/www] 文件夹中;
  • [4]: 运行 [composer] 命令,这将安装 [HttpFoundation] 库;
  • [5]: 在此示例中,该库已安装;

首次安装时,您应看到类似以下的控制台日志:


C:\myprograms\laragon-lite\www
? composer require symfony/http-foundation
Using version ^4.3 for symfony/http-foundation
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing symfony/mime (v4.3.0): Downloading (100%)
  - Installing symfony/http-foundation (v4.3.0): Downloading (100%)
Writing lock file
Generating autoload files

Web 服务器的第二个版本 [date-time-server-2.php] 如下:


<?php
 
// using Symfony libraries
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Response;
 
// we set the Content-Type header
$response=new Response();
$response->headers->set("content-type","text/plain");
$response->setCharset("utf-8");
 
// set the content of the response
//
// send date and time
// 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
$response->setContent(date("d/m/y H:i:s", time()));
 
// we send the answer
$response->send();

评论

  • 第 7 行:来自 Symfony [HttpFoundation] 库的 [Response] 类负责处理对 Web 服务客户端的全部响应;
  • 第 10 行:创建 [Response] 类的实例;
  • 第 11 行:指定响应类型为 [text/plain]
  • 第 12 行:响应内容为 UTF-8 编码的文本;
  • 第 25 行:将响应正文设置为客户端请求的内容;
  • 第 28 行:将响应发送给客户端;

17.6.4. 客户端脚本 – 版本 2

客户端脚本保持不变。我们仅修改其配置文件 [config-date-time-client.json]

1
2
3
{
    "url": "http://localhost/php7/scripts-web/02/date-time-server-2.php"
}

结果与第 1 版相同。

17.7. JSON 数据服务器

Web脚本的响应可以包含多个数据点,这些数据点可以组织成数组和对象。然后,脚本可以将这些各种元素封装在一个JSON字符串中发送,客户端将对其进行解码。

Image

17.7.1. 服务器脚本

[json-server.php] 脚本使用了以下 [Person] 类:


<?php
 
namespace Modèles;
 
class Personne implements \JsonSerializable {
  // attributes
  private $nom;
  private $prénom;
  private $âge;
 
  // convert associative array to object [Person]
  public function setFromArray(array $assoc): Personne {
    // initialize the current object with the associative array
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // result
    return $this;
  }

  // getters and setters
  public function getNom() {
    return $this->nom;
  }
 
  public function getPrénom() {
    return $this->prénom;
  }
 
  public function setNom($nom) {
    $this->nom = $nom;
    return $this;
  }
 
  public function setPrénom($prénom) {
    $this->prénom = $prénom;
    return $this;
  }
 
  public function getÂge() {
    return $this->âge;
  }
 
  public function setÂge($âge) {
    $this->âge = $âge;
    return $this;
  }
 
  // toString
  public function __toString(): string {
    return "Personne [$this->prénom, $this->nom, $this->âge]";
  }
 
  // implements the JsonSerializable interface
  public function jsonSerialize(): array {
    // render an associative array with the object's attributes as keys
    // this table can then be encoded as jSON
    return get_object_vars($this);
  }
 
  // convert a jSON to a [Person] object
  public static function jsonUnserialize(string $json): Personne {
    // we create a person from the string jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }
 
}

评论

  • 第 5 行:该类实现了 PHP 的 [JsonSerializable] 接口。这要求它在第 55–59 行中实现 [jsonSerialize] 方法。 该方法必须返回一个关联数组,该数组将被序列化为 JSON。在使用表达式 [json_encode($person)] 时,[json_encode] 函数会检查 [Person] 类是否实现了 [JsonSerializable] 接口。如果是,则该表达式变为 [json_encode($person→serialize())]
  • 第 12–19 行:该类没有构造函数,但有一个初始化器。因此,可以使用表达式 [(new Person()) → setFromArray($array)] 来实例化 [Person] 类。初始化器可以有多种类型,而构造函数只能有一个。这些初始化器允许采用 [(new Person())→initializer(…)) 形式的各种实例化模式;
  • 第 62–65 行:静态函数 [jsonUnserialize] 允许您根据其 JSON 字符串创建 [Person] 对象;

[json-server.php] 脚本如下:


<?php
 
// dependencies
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
 
// set the Content-Type header and the character library used
$response = new Response();
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
 
// create a Person object
$personne = (new Personne())->setFromArray([
  "nom" => "de la Hûche",
  "prénom" => "jean-paul",
  "âge" => 27]);
// an associative table
$assoc = ["attr1" => "value1",
  "attr2" => [
    "prenom" => "Jean-Paul",
    "nom" => "de la Hûche"
  ]
];
// the content of the response is jSON
$response->setContent(json_encode([$personne, $assoc]));
 
// reply sent
$response->send();

评论

  • 第 4-5 行:导入 [Person] 类;
  • 第 11 行:我们指定文档类型为 [application/json]。浏览器接收到此标头后,将以格式化的 JSON 字符串形式显示内容,而非纯文本;
  • 第 12 行:JSON 字符串将包含 UTF-8 字符;
  • 第 15-18 行:创建一个 [Person] 对象;
  • 第 20–25 行:创建了一个两级关联数组;
  • 第 27 行:我们将数组的 JSON 字符串发送给客户端:
    • [$person] 元素将通过其 [jsonSerialize] 方法序列化为 JSON;
    • [$assoc] 元素将以原生方式序列化为 JSON;

如果运行此服务器端脚本(需确保 Laragon 正在运行),浏览器中将显示以下响应:

Image

Image

评论

  • [2] 中,格式化的 JSON 响应;
  • [4] 中,原始的 JSON 响应。请注意带重音字符的编码;
  • [6]中,正是服务器发送的内容类型[application/json]导致浏览器以这种方式格式化输出;

17.7.2. 客户端

Image

客户端 [json-client.php] 由以下 JSON 文件 [config-json-client.json] 进行配置:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/03/json-server.php"
}

脚本 [json-client.php] 如下:


<?php
 
// service customer jSON
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
 
// customer configuration
const CONFIG_FILE_NAME = "config-json-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
 
try {
  // query
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // retrieve the jSON body of the response
  list($personne, $assoc) = json_decode($response->getContent(), true);
  // a person is instantiated from an array of attributes
  $personne = (new Personne())->setFromArray($personne);
  // server response is displayed
  print "---Réponse du serveur\n";
  print "$personne\n";
  print "tableau=" . json_encode($assoc, JSON_UNESCAPED_UNICODE) . "\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
 
 

评论

  • 第 12-13 行:导入 [Person] 类;
  • 第 30 行:创建 HTTP 客户端;
  • 第 44 行:解码服务器发送的 JSON 字符串。我们知道,被编码的内容是一个包含两个关联数组的二元数组;
  • 第 46 行:创建一个 [Person] 对象,以便在第 49 行显示它;
  • 第 50 行:显示第二个关联数组。由于 [print] 语句无法直接显示数组,因此我们将该数组转换为 JSON 字符串。为正确显示带重音的字符,必须将第二个参数设置为 [JSON_UNESCAPED_UNICODE]。我们可以看到,带重音的字符确实已编码在 JSON 字符串中;

执行客户端脚本后,结果如下:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 09:56:29 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 143
connection: close
content-type: application/json
---Réponse du serveur
Personne [jean-paul, de la Hûche, 27]
tableau={"attr1":"value1","attr2":{"prenom":"Jean-Paul","nom":"de la Hûche"}}

第11行和第12行:带重音的字符已正确检索。

17.8. 检索 Web 服务环境变量

服务器脚本运行于其可访问的 Web 环境中。该环境存储在 $_SERVER 字典中,这是一个 PHP 全局变量。若使用 [HttpFoundation] 库,该环境位于 [Request→server] 字段中,其中 [Request] 指由 Web 脚本处理的 HTTP 请求。

17.8.1. 服务器脚本

我们正在编写一个服务器应用程序,该程序会将其执行环境发送给客户端。

Image

Web脚本 [env-server.php] 如下所示:


<?php
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
 
// we retrieve the request
$request = Request::createFromGlobals();
// we work out the answer
$response = new Response();
// response content is json utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// set the jSON content of the response
$response->setContent(json_encode($request->server->all()));
// reply sent
$response->send();
  • 第 9 行:我们获取 [Request] 对象,该对象封装了 Web 脚本接收到的 HTTP 请求的所有可用信息及其执行环境;
  • 第 13–14 行:我们将包含 UTF-8 字符的纯文本发送给客户端;
  • 第 16 行:发送给客户端的信息将是一个字符串,该字符串由 [$request→server→all()] 对象经 JSON 序列化后获得:[$request→server] 代表 Web 脚本的执行环境。它是一个 [ServerBag] 类型的对象,属于字典的一种。[$request→server→all()] 是一个真正的字典,包含 [ServerBag] 的内容;
  • 第 18 行:发送信息;

如果从 NetBeans 运行此脚本,浏览器将显示以下页面:

Image

  • [2] 中,环境字典的各个键;
  • [3] 中,这些键的值;

17.8.2. 客户端脚本

Image

客户端脚本 [env-client.php] 由以下 JSON 文件 [config-env-client.json] 进行配置:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/04/env-server.php"
}

客户端脚本 [env-client.php] 如下:


<?php
 
// server script environment
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-env-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // make a request to the server
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur\n";
  $env = json_decode($response->getContent());
  foreach ($env as $key => $value) {
    print "[$key]=>$value\n";
  }
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

注释

  • 第 42 行:反序列化来自服务器的 JSON 响应。这会返回一个哈希;
  • 第43–45行:显示该关联数组中的所有值;

得到以下控制台输出:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Sun, 02 Jun 2019 17:35:50 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 1505
connection: close
content-type: application/json
---Réponse du serveur
[HTTP_HOST]=>localhost
[HTTP_USER_AGENT]=>Symfony HttpClient/Curl
[HTTP_ACCEPT_ENCODING]=>deflate, gzip
[PATH]=>C:\Program Files (x86)\Mail Enable\BIN;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files (x86)\Mail Enable\BIN64;C:\Users\serge\AppData\Local\Microsoft\WindowsApps;;C:\myprograms\Microsoft VS Code\bin
[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.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
[SERVER_NAME]=>localhost
[SERVER_ADDR]=>::1
[SERVER_PORT]=>80
[REMOTE_ADDR]=>::1
[DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[REQUEST_SCHEME]=>http
[CONTEXT_PREFIX]=>
[CONTEXT_DOCUMENT_ROOT]=>C:/myprograms/laragon-lite/www
[SERVER_ADMIN]=>admin@example.com
[SCRIPT_FILENAME]=>C:/myprograms/laragon-lite/www/php7/scripts-web/04/env-server.php
[REMOTE_PORT]=>63744
[GATEWAY_INTERFACE]=>CGI/1.1
[SERVER_PROTOCOL]=>HTTP/1.1
[REQUEST_METHOD]=>GET
[QUERY_STRING]=>
[REQUEST_URI]=>/php7/scripts-web/04/env-server.php
[SCRIPT_NAME]=>/php7/scripts-web/04/env-server.php
[PHP_SELF]=>/php7/scripts-web/04/env-server.php
[REQUEST_TIME_FLOAT]=>1559496950.644
[REQUEST_TIME]=>1559496950

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

HTTP_HOST
客户端发送的 HTTP 头部 [Host: xxx] 中的 xxx 值
HTTP_USER_AGENT
客户端发送的 HTTP 头部 [User_Agent: xxx] 中的值 xxx
HTTP_ACCEPT_ENCODING
客户端发送的 HTTP 标头 [Accept-Encoding: xxx] 中的值 xxx
PATH
服务器脚本运行所在机器上可执行文件的路径
COMSPEC
DOS命令提示符路径
PATHEXT
可执行文件扩展名
WINDIR
Windows 安装文件夹
SERVER_SIGNATURE
Web 服务器签名。此处无内容。
SERVER_SOFTWARE
Web 服务器的类型
SERVER_NAME
Web 服务器主机的互联网名称
SERVER_PORT
Web 服务器的监听端口
SERVER_ADDR
Web 服务器主机的 IP 地址,此处为 127.0.0.1
REMOTE_ADDR
客户端的 IP 地址。在此示例中,客户端与服务器位于同一台机器上。
REMOTE_PORT
客户端的通信端口
DOCUMENT_ROOT
Web 服务器所提供文档的目录树根目录
REQUEST_SCHEME
URL请求的TCP协议(http://localhost/php7/等)
SERVER_ADMIN
Web 服务器管理员的电子邮件地址
SCRIPT_FILENAME
服务器脚本的完整路径
REMOTE_PORT
客户端发起请求所使用的端口
SERVER_PROTOCOL
Web 服务器使用的 HTTP 协议版本
REQUEST_METHOD
客户端使用的 HTTP 方法。共有四种:GET、POST、PUT、DELETE
QUERY_STRING
随 GET 请求发送的参数 /url?parameters
REQUEST_URI
客户端请求的 URL。如果浏览器请求 URL http://machine[:port]/uri,则 REQUEST_URI 将为 uri
SCRIPT_NAME
$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].$_SERVER['SCRIPT_NAME']

17.9. 服务器检索客户端发送的参数

17.9.1. 简介

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

  • 它以表单形式请求服务 URL

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

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

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

POST url HTTP/1.0

然后,在发送到服务器的 HTTP 头部中,包含以下头部:

Content-length=N

客户端发送的其余标头以空行结尾。随后,它可以以以下形式发送其数据:

val1&param2=val2&param3=val3…

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

Content-length=N

Web 服务中的 PHP 脚本用于检索客户端发送的先前 parami 参数,并从数组中获取其值:

  • 对于 GET 请求,使用 $_GET["parami"]
  • 对于 POST 请求,则为 $_POST["parami"]

这适用于基本的 PHP 函数。如果使用 [HttpFoundation] 库,这些参数将位于:

  • 对于 GET 请求,位于 [Request]->query->get('parami');
  • [Request]->request->get('parami') 用于 POST 请求;

其中 [Request] 代表 Web 脚本接收到的请求的所有信息;

17.9.2. GET 客户端 – 版本 1

Image

客户端脚本通过以下 JSON 文件 [config-parameters-client.json] 进行配置:

1
2
3
4
{
    "url-get": "http://localhost/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
  • 第 1 行:GET 客户端的目标 Web 脚本 URL;
  • 第 2 行:POST 客户端的目标 Web 脚本 URL;

GET 客户端向服务器发送三个参数 [last_name, first_name, age]。客户端 [parameters-get-client.php] 如下所示:


<?php
 
// client GET of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
 
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
// information is encoded
  $parameters = "prenom=" . urlencode($prenom) .
    "&nom=" . urlencode($nom) .
    "&age=$age”;
  // query
  $response = $httpClient->request('GET', $config['url-get'] . "?$parameters");
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

注释

  • 第 33-35 行:将参数发送至服务器的编码处理。参数 [$first_name, $last_name] 可能包含 UTF-8 字符,因此使用 [urlencode] 函数进行编码。所有非字母数字字符(由关系表达式定义)均被替换为 %xx,其中 xx 是该字符的十六进制值。空格被替换为 + 号;
  • 第 37 行:请求的 URL 为 $URL?$parameters,其中 $parameters 的格式为 name=val1&firstname=val2&age=val3
  • 第 48 行:客户端将直接显示服务器的响应;

您可能好奇在参数化 GET 请求过程中服务器会接收什么。为此,我们从 Laragon 终端(参见链接部分)在本地机器的 100 端口上启动通用服务器 [RawTcpServer]

Image

请确认在 [4] 中,您确实位于 utilities 文件夹内。

我们修改配置 GET 和 POST 客户端的 JSON 文件 [parameters-get-client.json]


{
    "url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost/php7/scripts-web/05/parameters-server.php"
}
  • 第 2 行:我们已更改了 Web 服务器的端口。因此,将联系 [RawTcpServer]

我们运行客户端。在 [RawTcpServer] 窗口中,我们看到以下信息:

Image

  • [1] 中,显示了客户端发送的 GET 请求。我们可以清楚地看到某些字符的编码;

17.9.3. GET / POST 服务器

Image

服务器脚本 [parameters-server.php] 如下所示:


<?php
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
 
// we retrieve the request
$request = Request::createFromGlobals();
// retrieve query parameters
$getParameters = $request->query->all();
$bodyParameters = $request->request->all();
 
// we work out the answer
$response = new Response();
// the content of the answer is utf-8 text
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// response content - an array encoded in jSON
$response->setContent(json_encode([
  "method" => $request->getMethod(),
  "uri" => $request->getRequestUri(),
  "getParameters" => $getParameters,
  "bodyParameters" => $bodyParameters
    ], JSON_UNESCAPED_UNICODE));
// reply sent
$response->send();

注释

  • 第 9 行:为 Web 脚本创建 [Request] 对象。该对象封装了 Web 脚本从客户端接收到的所有信息;
  • 第 11 行:[Request→query] 对象的类型为 [ParameterBag],用于收集客户端发起的任何 GET 操作的参数。表达式 [Request→query→get("X")] 从 GET 参数 [name=val1&firstname=val2&age=val3] 中提取名为 X 的参数。表达式 [Request→query→all()] 提取 GET 参数的字典;
  • 第 12 行:对象 [Request→request] 的类型为 [ParameterBag],包含客户端作为文档发送给服务器的参数。这些参数也被称为上传参数,因为它们属于客户端发送给服务器的文档。 表达式 [Request→request→get("X")] 从上传的参数 [last_name=val1&first_name=val2&age=val3] 中提取名为 X 的参数。表达式 [Request→request→all()] 获取上传参数的字典;
  • 第 17–18 行:通知客户端将接收 UTF-8 编码的 JSON;
  • 第 20–25 行:服务器向客户端返回其接收到的所有参数、客户端执行的操作类型 [GET / POST / …] 以及请求的 URI。该方法通过表达式 [$request→getMethod()] 获取。发送给客户端的文档是一个关联数组的 JSON 字符串,其中部分值的本身也是关联数组。 [JSON_UNESCAPED_UNICODE] 参数确保 Unicode 字符(例如带重音的字符)以原始形式发送,而不进行编码;
  • 第 27 行:将响应发送给客户端;

执行客户端脚本将产生以下结果:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 10:08:45 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 207
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom=jean-paul&nom=de+la+h%C3%BBche&age=45","getParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"},"bodyParameters":[]}]
  • 第 10 行:
    • [method]:方法为 GET;
    • [uri]:GET请求的URL编码参数显示在请求的URI中;
    • [getParameters]:GET 参数数组;
    • [bodyParameters]:上传参数的数组:该数组为空;

17.9.4. GET 客户端 – 版本 2

在客户端脚本的上一版本中,出于教学目的,我们手动对发送给服务器的参数进行了 URL 编码。而 [HttpClient] 对象可以自行处理此任务。以下是相应的 [parameters-get-client-2.php] 脚本:


<?php
 
// client GET of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('GET', $config['url-get'],
    ["query" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

注释

  • 第 33–37 行:向第 32 行的 GET 请求中添加参数。[HttpClient] 对象会自行处理 URL 编码;

17.9.5. POST 客户端

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

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

没有文档。还有另一种发送参数的方法,称为 POST 方法。在这种情况下,发送到 Web 服务器的文本序列如下:

1
2
3
4
POST /url HTTP/1.1
… autres entêtes HTTP
ligne vide
paramètres

这一次,GET 客户端中包含在 HTTP 头中的参数,在 POST 客户端中作为头部之后发送的文档的一部分。

POST客户端脚本 [parameters-postclient.php] 如下所示:


<?php
 
// client POST of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";

// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('POST', $config['url-post'],
    ["body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
  • 第 32 行:现在我们有一个 POST HTTP 请求;
  • 第 33–37 行:POST 参数被称为 POST 请求的请求体:这是客户端发送给服务器的文档。这里发送了三个参数 [last_name, first_name, age]
  • 第 48 行:我们显示服务器的 JSON 响应;

运行客户端脚本的结果如下:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 11:43:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 163
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php","getParameters":[],"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
  • 第 10 行:方法为 [Post],参数类型为 [bodyParameters]。如 [uri] 所示,不存在 [getParameters]

您可能好奇服务器在POST请求期间会接收什么数据。为此,我们从 Laragon 终端在本地机器的 100 端口上启动通用的 [RawTcpServer](参见相关段落):

Image

请确认在 [4] 中,您确实位于 utilities 文件夹内。

我们修改配置 POST 客户端的 JSON 文件 [config-parameters-client.json]


{
    "url-get": "http://localhost:100/php7/scripts-web/05/parameters-server.php",
    "url-post": "http://localhost:100/php7/scripts-web/05/parameters-server.php"
}
  • 第 3 行:我们已更改了 Web 服务器的端口。因此,将联系 [RawTcpServer]

我们运行客户端。在 [RawTcpServer] 窗口中,我们看到以下信息:

Image

  • [6] 中,POST 请求;
  • [7] 中:HTTP 头部 [Content-Length] 指定了客户端将发送给服务器的文档的字节数。HTTP 头部 [Content-Type] 指定了该文档的性质。类型 [application/x-www-form-urlencoded] 表示 URL 编码的文本;
  • [8] 中,空行标志着 HTTP 头部的结束以及 44 字节文档的开始。截图中未显示的是文档本身。它是由 URL 编码的参数字符串:[first_name=jean-paul&last_name=de+la+h%C3%BBche&age=45]。读者可以验证它确实有 44 个字符;

17.9.6. 混合 POST 客户端

在 POST 请求中,您可以将 URL 中编码的参数与客户端在 HTTP 头部之后发送的文档中编码的参数混合使用。以下是一个示例 [parameters-mixte-postclient.php]


<?php
 
// client POST of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('POST', $config['url-post'],
    [
      // document parameters (body)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // parameters of URL (query)
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

注释

  • 第 32 行:一个 POST 请求;
  • 第 40–45 行:URL 中的 URL 编码参数;
  • 第 35–39 行:请求正文(body、document)中的 URL 编码参数;

执行后,控制台输出如下:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:34:23 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 270
connection: close
content-type: application/json
---Réponse du serveur [{"method":"POST","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":{"prenom":"jean-paul","nom":"de la hûche","age":"45"}}]
  • 第 10 行:我们可以看到服务器成功检索到了这两种类型的参数;

17.9.7. 混合 GET 客户端

我们将尝试使用 GET 请求实现与之前相同的功能。脚本 [parameters-mixte-get-client.php] 如下:


<?php
 
// client POST of a web server
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-parameters-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // prepare the parameters
  list($prenom, $nom, $age) = array("jean-paul", "de la hûche", 45);
  // make a request to the server
  $response = $httpClient->request('GET', $config['url-post'],
    [
      // document parameters (body)
      "body" => [
        "prenom" => $prenom,
        "nom" => $nom,
        "age" => $age
      ],
      // parameters of URL (query)
      "query" => [
        "prenom2" => $prenom,
        "nom2" => $nom,
        "age2" => $age
  ]]);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // server response is displayed
  print "---Réponse du serveur [" . $response->getContent() . "]\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

注释

  • 第 32 行:一个 POST 请求;
  • 第 40–45 行:URL 中的 URL 编码参数;
  • 第 35–39 行:请求正文(body、document)中的 URL 编码参数;

执行后,控制台输出如下:

---Réponse avec statut : 200
---Entêtes de la réponse
date: Mon, 03 Jun 2019 12:41:19 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 217
connection: close
content-type: application/json
---Réponse du serveur [{"method":"GET","uri":"\/php7\/scripts-web\/05\/parameters-server.php?prenom2=jean-paul&nom2=de%20la%20h%C3%BBche&age2=45","getParameters":{"prenom2":"jean-paul","nom2":"de la hûche","age2":"45"},"bodyParameters":[]}]
  • 第 10 行:我们可以看到,服务器并未在客户端发送的文档中接收到任何 URL 编码的参数。查看客户端发送的 HTTP 头部时,我们发现它确实发送了一个 44 个字符的文档,但服务器并未对其进行处理;

那么,你应该选择哪种方法向服务器发送信息?

  • [GET URL?param1=val1&param2=val2&…] 方法使用带参数的 URL,该 URL 可以作为链接。这是其主要优势:用户可以将此类链接添加到书签中;
  • 在其他应用场景中,您可能不希望在 URL 中显示发送给服务器的参数。例如出于安全考虑。在这种情况下,您将使用 [POST] 方法,并将 URL 编码的参数包含在发送给服务器的文档中;

17.10. Web 会话管理

在之前的客户端/服务器示例中,流程如下:

  • 客户端向 Web 服务器机器的 80 端口建立连接;
  • 它发送文本序列:HTTP 头部、空行、[文档];
  • 作为响应,服务器发送同类型的序列;
  • 服务器关闭与客户端的连接;
  • 客户端关闭与服务器的连接;

如果同一客户端随后不久向 Web 服务器发出新请求,客户端与服务器之间将建立新的连接。服务器无法判断连接的客户端是否曾访问过,还是这是首次请求。在两次连接之间,服务器会“忘记”其客户端。因此,HTTP 协议被称为无状态协议。然而,服务器记住其客户端是有用的。 例如,如果应用程序是安全的,客户端会向服务器发送用户名和密码以进行身份验证。如果服务器在连接之间“忘记”了该客户端,那么客户端就必须在每次建立新连接时都重新进行身份验证,这显然是不切实际的。

为了追踪客户端,服务器采取以下做法:当客户端发出初始请求时,服务器会在响应中包含一个标识符,客户端随后必须在每次新请求中将该标识符发回。得益于这个对每个客户端都唯一的标识符,服务器能够识别客户端。随后,服务器可以为该客户端管理一个内存条目,该条目与客户端的标识符唯一关联。

从技术角度看,其工作原理如下:

  • 在响应新客户端时,服务器会在响应中包含 HTTP 头部 Set-Cookie: Key=Identifier。此操作仅在首次请求时执行;
  • 在后续请求中,客户端将通过 HTTP 头部 Cookie: Key=Identifier 发送其标识符,以便服务器识别它;

有人可能会疑惑,服务器是如何知道自己面对的是新客户端而非回访客户端的。这要归功于客户端HTTP头中是否存在HTTP Cookie头部。对于新客户端,该头部并不存在。

来自某个特定客户端的所有连接统称为一个会话。

17.10.1. 配置文件 [php.ini]

要使 PHP 的会话管理正常工作,必须确保其配置正确。在 Windows 系统中,其配置文件为 php.ini。根据执行环境(控制台、Web)的不同,[php.ini] 配置文件必须位于不同的目录中。要查找这些目录,请使用以下脚本:

1
2
3
4
<?php

// infos PHP
phpinfo();

第 4 行:phpinfo 函数提供有关正在运行脚本的 PHP 解释器的信息。特别是,它提供了正在使用的配置文件 [php.ini] 的路径。

我们已经在控制台环境中使用过此脚本(参见“链接”部分)。在 Web 环境中,我们会得到以下结果:

Image

  • [1-2] 中,[php.ini] 文件用于配置 Web 脚本解释器。该文件包含一个 session 部分:
[Session]
session.save_handler = files
session.save_path = "C:/myprograms/laragon-lite/tmp"
session.use_strict_mode = 0
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.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 36000
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
  • 第 2 行:客户端会话数据将保存到文件中;
  • 第 3 行:保存会话数据的目录。如果该目录不存在,则不会报告错误,且会话管理功能无法正常工作;
  • 第 4–6 行:表示会话 ID 由 Set-Cookie Cookie HTTP 头部管理
  • 第 7 行:Set-Cookie 标头将采用 Set-Cookie: PHPSESSID=session_id 的形式
  • 第 8 行:客户端会话不会自动启动。服务器脚本必须使用 session_start() 函数显式请求启动会话;
  • 第 9 行:会话 Cookie 有效期直至客户端浏览器关闭;
  • 第 10 行:必须发送会话 Cookie 的路径。如果 [session.cookie_path = /xxx],那么每次浏览器请求形式为 [/xxx/yyy/zzz] 的 URL 时,都必须发送该 Cookie。此处路径 [/] 表示必须为网站上的每个 URL 发送该 Cookie;
  • 第 13 行:某些会话对象必须经过序列化才能存储在文件中。PHP 使用 [serialize / unserialize] 函数处理此序列化/反序列化操作;
  • 第 16 行:超时时间,超过该时间后,存储在会话文件中的会话对象将被视为过期;
  • 第 19 行:会话生命周期。超过此时间后,将创建新的会话,而之前会话中保存的对象将被丢失;

17.10.2. 示例 1

17.10.2.1. 服务器

Image

对于 Web 服务而言,会话 ID 的管理是透明的。该 ID 由 Web 服务器进行管理。Web 服务通过 session_start() 函数访问客户端的会话。从那时起,Web 服务即可通过 $_SESSION 字典对客户端的会话进行读写操作。如果使用了 [HttpFoundation] 库,则可通过 [Request→getSession] 表达式访问会话。

以下代码 [session-server.php] 演示了基于会话的三计数器管理。每次收到新请求时,Web 脚本都会递增这些计数器并将结果存储在会话中,以便在下次请求时检索。


<?php
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
 
//
// we retrieve the request
$request = Request::createFromGlobals();
// session
$session = new Session();
$session->start();
// three counters are retrieved from the session
if ($session->has("N1")) {
  // n1 counter increment
  $session->set("N1", (int) $session->get("N1") + 1);
} else {
  // counter N1 is not in session - create it
  $session->set("N1", 0);
}
if ($session->has("N2")) {
  // n2 counter increment
  $session->set("N2", (int) $session->get("N2") + 1);
} else {
  // counter N2 is not in session - create it
  $session->set("N2", 10);
}
if ($session->has("N3")) {
  // n3 counter increment
  $session->set("N3", (int) $session->get("N3") + 1);
} else {
  // counter N3 is not in session - create it
  $session->set("N3", 100);
}
// we work out the answer
$response = new Response();
// the content of the answer is utf-8 text
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
// the answer will be the jSON of an array containing the three counters
$response->setContent(json_encode([
  "N1" => $session->get("N1"),
  "N2" => $session->get("N2"),
  "N3" => $session->get("N3")]));
 
// reply sent
$response->send();
  • 第 10 行:[$request] 对象封装了 Web 脚本接收到的请求的所有信息;
  • 第 12-13 行:创建并激活会话。 [Session] 对象封装了与客户端发送的会话 Cookie 相对应的会话数据。如果客户端未发送此类 Cookie,则 [Session] 中不会存储任何数据。 Web脚本将在其首次响应中包含HTTP头[Set-Cookie: PHPSESSID=xxx]。在后续请求中,客户端将发送HTTP头[Cookie: PHPSESSID=xxx],以指定其希望使用的会话内容。会话相当于客户端的内存;
  • 第 15 行:我们检查会话中是否存在名为 [N1] 的键。这将是我们第一个计数器的名称。如果不存在(第 20 行),我们将它的值设为 0 并将其添加到会话中。如果存在(第 23 行),我们将:
    • 从会话中检索该计数器;
    • 将其值加 1;
    • 将其放回会话中;
  • 第 22–35 行:对另外两个计数器 N2 和 N3 执行相同操作;
  • 第 36–40 行:准备一个类型为 [application/json] 的响应;
  • 第 42–45 行:响应将是一个包含这三个计数器的数组的 JSON 字符串;
  • 第 48 行:将响应发送给客户端;

在客户端/服务器关系中,服务器端对客户端会话的管理取决于双方——客户端和服务器:

  • 服务器负责在客户端首次请求时向其发送一个标识符
  • 客户端负责在每次新请求中将该标识符发回。若未发送,服务器将视其为新客户端,并为新会话生成新的标识符。

结果

我们使用网页浏览器作为客户端。默认情况下(实际上是根据配置),浏览器确实会将服务器发送给它的会话标识符发回给服务器。随着请求的发出,浏览器将接收服务器发送的三个计数器,并看到它们的值逐渐增加。

Image

  • [2]中,对Web服务的首次请求;
  • [4]中,第四次请求显示计数器确实已被递增。计数器值确实在请求过程中被保存了下来;

让我们使用开发者模式查看服务器与客户端之间交换的 HTTP 头部。我们关闭 Firefox 以结束与服务器的当前会话,重新打开它,并启用开发者模式(F12)。这将清除浏览器的当前会话,使其开始一个新的会话。我们请求服务 [session-server.php]

Image

[5] 中,我们可以看到服务器在响应客户端首次请求时发送的会话 ID。它使用了 Set-Cookie HTTP 头部。

让我们通过在网页浏览器中刷新(F5)页面来发起一个新请求:

Image

在此,我们会注意到两点:

  • [11]中,Web浏览器会通过HTTP Cookie标头将会话ID发回。
  • [12]中,Web服务不再在其响应中包含该标识符。现在,客户端有责任在每次请求中发送该标识符。

17.10.2.2. 客户端

接下来,我们将基于之前的服务器端脚本编写一个客户端脚本。在会话管理方面,它必须像 Web 浏览器那样工作:

  • 在服务器对其首次请求的响应中,它必须找到服务器发送的会话 ID。它知道该 ID 位于 HTTP Set-Cookie 头中。
  • 对于后续的每次请求,它必须将收到的标识符发回给服务器。它将通过 HTTP Cookie 头来完成这一操作。

Image

客户端 [session-client] 由以下 JSON 文件 [config-session-client.json] 进行配置:

1
2
3
{
    "url": "http://localhost/php7/scripts-web/06/session-server.php"
}

客户端代码 [session-client] 如下:


<?php
 
// session management
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-session-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create();
try {
  // we'll make 10 requests
  for ($i = 0; $i < 10; $i++) {
    // make a request to the server
    if (!isset($sessionCookie)) {
      // no session
      $response = $httpClient->request('GET', $config['url']);
    } else {
      // with session
      $response = $httpClient->request('GET', $config['url'],
        ["headers" => ["Cookie" => $sessionCookie]]);
    }
    // answer status
    $statusCode = $response->getStatusCode();
    print "---Réponse avec statut : $statusCode\n";
    // we retrieve the headers
    print "---Entêtes de la réponse\n";
    $headers = $response->getHeaders();
    foreach ($headers as $type => $value) {
      print "$type: " . $value[0] . "\n";
    }
    // retrieve the session cookie if it exists
    if (isset($headers["set-cookie"])) {
      // session cookie ?
      foreach ($headers["set-cookie"] as $cookie) {
        $match = [];
        $match = preg_match("/^PHPSESSID=(.+?);/", $cookie, $champs);
        if ($match) {
          $sessionCookie = "PHPSESSID=" . $champs[1];
        }
      }
    }
  }
  // the jSON server response is displayed
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}
 
 

评论

  • 第 27 行:创建 HTTP 客户端;
  • 第 30 行:我们将向服务器 [session-server.php] 发送相同的请求 10 次;
  • 第 32 行:变量 [$sessionCookie] 将被设置为客户端接收到的 [Set-Cookie] HTTP 头部的值;
  • 第 32–34 行:如果该变量不存在,则表示会话尚未开始。此时我们发送不带 [Cookie] 头部的 [GET] 请求;
  • 第 35–38 行:否则,表示会话已开始,我们将发送包含 [Cookie] 头部的 [GET] 请求。该头部的值将是 [$sessionCookie]
  • 第 50 行:如果接收到的 HTTP 头部中包含 [Set-Cookie] 头部,则查找会话 Cookie;
  • 第 52 行:Web 服务器可能会发送多个 [Set-Cookie] 头部。会话 Cookie 只是其中之一。在本例中,其格式为 [PHPSESSID=xxx;;]
  • 第 53–57 行:使用正则表达式查找会话 Cookie;
  • 第 62 行:当发送完 10 个请求后,显示来自服务器的最后一个 JSON 响应;

结果

执行客户端脚本后,NetBeans 控制台将显示以下内容:


"C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.exe" "C:\Data\st-2019\dev\php7\poly\scripts-console\clients web\06\session-client.php"
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=1cerjgsgdlc35e1mkenvtltmh8; path=/
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse avec statut : 200
…………………………………………………………
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 13:41:34 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 25
connection: close
content-type: application/json
---Réponse du serveur : {"N1":9,"N2":19,"N3":109}
  • 第 8 行:在首次响应中,服务器发送了会话 ID。在后续响应中,它不再发送该 ID;
  • 第41行:三个计数器[N1, N2, N3]确实已被递增了9次。在第1次请求期间,它们被重置为零;

以下示例表明,您还可以将数组或对象的值保存到会话中。

17.10.3. 示例 2

17.10.3.1. 服务器

Image

我们将把一个 [Person] 对象放入会话中。该类的定义如下:


<?php
 
namespace Modèles;
 
class Personne implements \JsonSerializable {
  // attributes
  private $nom;
  private $prénom;
  private $âge;
 
  // convert associative array to object [Person]
  public function setFromArray(array $assoc): Personne {
    // initialize the current object with the associative array
    foreach ($assoc as $attribute => $value) {
      $this->$attribute = $value;
    }
    // result
    return $this;
  }
 
  // getters and setters
  public function getNom() {
    return $this->nom;
  }
 
  public function getPrénom() {
    return $this->prénom;
  }
 
  public function setNom($nom) {
    $this->nom = $nom;
    return $this;
  }
 
  public function setPrénom($prénom) {
    $this->prénom = $prénom;
    return $this;
  }
 
  public function getÂge() {
    return $this->âge;
  }
 
  public function setÂge($âge) {
    $this->âge = $âge;
    return $this;
  }
 
  // toString
  public function __toString(): string {
    return "Personne [$this->prénom, $this->nom, $this->âge]";
  }
 
  // implements the JsonSerializable interface
  public function jsonSerialize(): array {
    // render an associative array with the object's attributes as keys
    // this table can then be encoded as jSON
    return get_object_vars($this);
  }
 
  // convert a jSON to a [Person] object
  public static function jsonUnserialize(string $json): Personne {
    // we create a person from the string jSON
    return (new Personne())->setFromArray(json_decode($json, true));
  }
 
}

服务器脚本如下:


<?php
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
require_once __DIR__ . "/Personne.php";
use \Modèles\Personne;
 
//
// retrieve the current query
$request = Request::createFromGlobals();
 
// session
$session = new Session();
$session->start();
 
// retrieve various data from the session
// table
if ($session->has("tableau")) {
  // the array is in the session - all its values are incremented
  $tableau = $session->get("tableau");
  for ($i = 0; $i < count($tableau); $i++) {
    $tableau[$i] += 1;
  }
  // put the table back in the session
  $session->set("tableau", $tableau);
} else {
  // the array is not in the session - we create it
  $tableau = [0, 10, 100];
  // we put it in the session
  $session->set("tableau", $tableau);
}
// dictionary
if ($session->has("assoc")) {
  // [assoc] is in the session - all its elements are incremented
  $assoc = $session->get("assoc");
  foreach ($assoc as $key => $value) {
    $assoc[$key] = $value + 1;
  }
  // put $assoc in the session
  $session->set("assoc", $assoc);
} else {
  // [assoc] is not in the session - we create it
  $assoc = ["un" => 0, "deux" => 10, "trois" => 100];
  // put $assoc in the session
  $session->set("assoc", $assoc);
}
// object Person
if ($session->has("personne")) {
  // [person] is in the session - his age is incremented
  $personne = $session->get("personne");
  $personne->setÂge($personne->getÂge() + 1);
} else {
  // [person] is not in the session - we create it
  $personne = (new Personne())->setFromArray(
    ["prénom" => "Léonard", "nom" => "Hûche", "âge" => 0]);
  // put $personne in the session
  $session->set("personne", $personne);
}
// we work out the answer
$response = new Response();
// the content of the response is jSON utf-8
$response->headers->set("content-type", "application/json");
$response->setCharset("utf-8");
$response->setContent(json_encode([
  "tableau" => $tableau,
  "assoc" => $assoc,
  "personne" => $personne], JSON_UNESCAPED_UNICODE));
 
// reply sent
$response->send();

评论

  • 第 16-17 行:获取当前会话并激活它;
  • 第 21-34 行:管理存储在会话中的数组 [array]。每次收到新请求时,其元素值都会增加 1;
  • 第 36-49 行:管理存储在会话中的关联数组 [assoc]。每次收到新请求时,其元素值都会增加 1;
  • 第 51–61 行:管理一个存储在会话中的对象 [Person]。每次收到新请求时,此人的年龄都会增加 1;
  • 第 62–73 行:向客户端发送 JSON 响应:一个关联数组的 JSON 字符串;

让我们在 NetBeans 中运行此脚本。前两次请求的结果如下(在浏览器中按 F5 执行第二次请求):

Image

  • 我们可以看到,在 [6-8] 中,所有计数器都已递增;

17.10.3.2. 客户端

Image

客户端与示例 1(链接部分)中的相同。我们仅修改其配置文件 [config-session-client]


{
    "url": "http://localhost/php7/scripts-web/07/session-server.php"
}

执行后得到以下结果:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
set-cookie: PHPSESSID=qbfrj8clr20mod3eriur71mao6; path=/
content-length: 119
connection: close
content-type: application/json
---Réponse avec statut : 200
………….……………………………………………………….
---Réponse avec statut : 200
---Entêtes de la réponse
date: Tue, 04 Jun 2019 14:25:24 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: max-age=0, private, must-revalidate
content-length: 119
connection: close
content-type: application/json
---Réponse du serveur : {"tableau":[9,19,109],"assoc":{"un":9,"deux":19,"trois":109},"personne":{"nom":"Hûche","prénom":"Léonard","âge":9}}
  • 第 22 行,我们可以看到所有计数器都已递增;

17.11. 身份验证

接下来我们将重点探讨仅面向特定用户的 Web 服务。因此,客户端在收到响应之前必须先通过 Web 服务进行身份验证。

17.11.1. 客户端

Image

客户端代码 [auth-client.php] 如下:


<?php
 
// session management
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
 
// customer configuration
const CONFIG_FILE_NAME = "config-auth-client.json";
 
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
  print "Le fichier de configuration [" . CONFIG_FILE_NAME . "] n'existe pas\n";
  exit;
}
if (!$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true)) {
  print "Erreur lors de l'exploitation du fichier de configuration jSON [" . CONFIG_FILE_NAME . "]\n";
  exit;
}
 
// create a HTTP customer
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    // "verify_peer" => false,
    // "verify_host" => false
  ]);
 
 
try {
  // make a request to the server
  $response = $httpClient->request('GET', $config['url']);
  // answer status
  $statusCode = $response->getStatusCode();
  print "---Réponse avec statut : $statusCode\n";
  // we retrieve the headers
  print "---Entêtes de la réponse\n";
  $headers = $response->getHeaders();
  foreach ($headers as $type => $value) {
    print "$type: " . $value[0] . "\n";
  }
  // the jSON server response is displayed
  print "---Réponse du serveur : {$response->getContent()}\n";
} catch (TypeError | RuntimeException $ex) {
  // error is displayed
  print "Erreur de communication avec le serveur : " . $ex->getMessage() . "\n";
}

评论

  • 第 27–31 行:我们向静态方法 [HttpClient::create] 传递了一个参数,即一个哈希;
  • 第 28 行:[auth_basic] 键的值为一个包含两个元素的数组 [user, password]。客户端将使用这些元素与 Web 服务进行身份验证。[auth_basic] 键指的是一种名为 [Basic Authorization] 的身份验证类型,其名称源自客户端将发送的 HTTP 头。还有其他类型的身份验证;
  • 除这段代码外,该客户端与之前的客户端完全相同;

要查看客户端发送的 HTTP 头部,我们将像之前多次做过的那样,将其连接到通用 TCP 服务器 [RawTcpServer]

Image

我们使用以下 [config-auth-client.json] 配置启动客户端:


{
    "url": "http://localhost:100/php7/scripts-web/08/auth-server.php"
}

随后,[RawTcpServer] 接收到了以下几行内容:

Image

  • [5] 中,我们可以看到客户端发送的 [Authorization: Basic XXX] 头部。XXX 字符串是经过 Base64 编码的 [用户名:密码] 字符串;

要验证这一点,您可以在网站 [https://www.base64decode.org/] 上对接收到的字符串进行解码:

Image

17.11.2. 服务器

Image

[auth-server.php] 服务器代码如下:


<?php
 
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
 
// authorized users
$users = ["admin" => "admin"];
//
// retrieve the current query
$request = Request::createFromGlobals();
// authentication
$requestUser = $request->headers->get('php-auth-user');
$requestPassword = $request->headers->get('php-auth-pw');
// does the user exist?
$trouvé = array_key_exists($requestUser, $users) && $users[$requestUser] === $requestPassword;
// answer preparation
$response = new Response();
// set the response status code
if (!$trouvé) {
  // not found - code 401
  $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
  $response->headers->add(["WWW-Authenticate"=> "Basic realm=".utf8_decode("\"PHP7 par l'exemple\"")]);
} else {
  // found - code 200
  $response->setStatusCode(Response::HTTP_OK);
}
// response has no content, only HTTP headers
$response->send();

注释

  • 第 9 行:授权用户,本例中为登录名 [admin] 和密码 [admin] 的单个用户;
  • 第 14 行:从 [PHP-AUTH-USER] 标头中获取用户的 ID。这不是客户端发送的标头,而是由服务器的 PHP 生成的;
  • 第 15 行:从 [PHP-AUTH-PW] 标头中获取用户的密码,该标头由 PHP 构建;
  • 第 17 行:在授权用户列表中搜索尝试登录的用户;
  • 第 23–24 行:如果未识别该用户,则向客户端发送以下内容
    • 第 23 行:[401 Unauthorized] 状态码;
    • 第 24 行:一个 [WWW-Authenticate: Basic realm="something"] 标头。大多数浏览器都能识别此标头,并会显示一个身份验证窗口,提示用户登录。HTTP 标头必须采用 ISO 8859-1 编码。然而,NetBeans 中的文本采用的是 UTF-8 编码。 [utf8_decode] 函数负责将 UTF-8 转换为 ISO 8859-1。在此处,该函数并非必需,因为字符串中的字符(本例中为 [PHP7])在 UTF-8 和 ISO 8859-1 编码下是相同的。包含该函数仅是为了提醒 HTTP 头所使用的编码;
  • 第 25 行:如果已识别用户,则向客户端发送 [200 OK] 状态码;

现在让我们使用浏览器请求 URL [auth-server.php]

Image

我们可以看到浏览器显示了一个身份验证窗口。在 [2] 中,我们可以看到服务器发送的 [WWW-Authenticate] 标头值。如果查看浏览器接收到的 HTTP 标头,我们会发现以下内容:

1
2
3
4
5
6
7
8
9
HTTP/1.0 401 Unauthorized
Date: Fri, 07 Jun 2019 09:11:23 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
WWW-Authenticate: Basic realm="PHP7 par l'exemple"
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
  • 第 1 行:响应状态码 [401 未授权]
  • 第 6 行:HTTP 头 [WWW-Authenticate]
  • 第 7 行:响应正文为空;

如果在 [3-4] 中,您输入两次 [admin],服务器的响应如下:

1
2
3
4
5
6
7
8
HTTP/1.0 200 OK
Date: Fri, 07 Jun 2019 09:21:00 GMT
Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
X-Powered-By: PHP/7.2.11
Cache-Control: no-cache, private
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
  • 第 1 行:200 OK 响应代码;
  • 第 6 行:响应正文为空;

如果在[3-4]中输入了错误的凭据,用于测试的浏览器[Firefox]会持续显示身份验证窗口,直到输入正确的凭据为止。每次与服务器进行往返通信时,都会收到相同的响应,从而触发浏览器的身份验证窗口。

让我们使用未授权用户运行客户端 [auth-client.php]。服务器的响应如下:


---Réponse avec statut : 401
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 401 Unauthorized returned for "https://localhost/php7/scripts-web/08/auth-server.php".
  • [1] 中,客户端确实收到了 401 状态码;
  • 而在[3]中,客户端抛出了异常。抛出异常的是Symfony的[HttpClient]:当HTTP响应状态码指示服务器端错误,且客户端试图读取服务器响应的头部或内容时,它会抛出异常。 第 3 行的信息显示,服务器返回了 [HTTP/1.0 401 Unauthorized] 状态码,表明用户未被识别;

现在让我们使用已授权用户 [‘admin’,’admin’] 运行客户端 [auth-client.php]。此时服务器的响应如下:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:11:02 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :
 
  • 第 1 行:服务器响应 [HTTP/1.2 200 OK]
  • 第7行:响应内容为空(0字节);

17.11.3. 保障客户端/服务器连接的安全

我们已经看到,为了向服务器进行身份验证,客户端会发送以下头部:

authorization: Basic YWRtaW46YWRtaW4=

如果这条数据被间谍软件截获,它便能轻松提取出字符串 [YWRtaW46YWRtaW4=] 中通过 Base64 编码的 [用户名、密码] 凭据。因此,客户端与服务器之间的身份验证必须通过 安全连接进行。安全 URL 使用 [HTTPS] 协议,而非 HTTP 协议。 [HTTPS] 协议是在安全客户端/服务器连接中运行的 HTTP 协议。安全 URL 的形式为 [https://chemin_document]

并非所有 Web 服务器都支持此格式的 URL。必须对其进行修改才能确保安全。Laragon 的 Apache 服务器是安全的,但默认情况下未启用 HTTPS 协议。您必须在 Laragon 菜单中启用它:

Image

  • [4] 中,为 Apache 服务器启用 SSL 加密;

完成此操作后,Apache 服务器将自动重启:

Image

  • [1] 中,会出现一个绿色挂锁图标:这表示 HTTPS 协议已启用;
  • [2] 处,会出现一个新的服务端口,此处为 443 端口。这是安全 HTTPS 协议的服务端口;

既然我们已经拥有了一个安全的服务器,接下来让我们按以下方式修改客户端的配置文件 [config-auth-client.json]


{
    "url": "https://localhost:443/php7/scripts-web/08/auth-server.php"
}

[2]中,协议已更改为[https],端口更改为[443]

现在,让我们使用授权用户 [admin, admin] 运行客户端 [auth-client.php]。控制台输出如下:

Erreur de communication avec le serveur : Peer certificate cannot be authenticated with given CA certificates for"https://localhost/php7/scripts-web/08/auth-server.php".

Symfony 的 [HttpClient] 抛出了异常,因为服务器发送的信任证书未被 [HttpClient] 接受。SSL 通信依赖于由官方机构签发的信任证书。当我们在 Laragon Apache 服务器上启用 HTTPS 协议时,系统为 Apache 服务器生成了一份自签名证书。自签名证书是指未经官方机构验证的证书。 Symfony 的 [HttpClient] 客户端拒绝了此自签名证书。

可以通过在 [HttpClient::create] 方法中使用选项,指示 [HttpClient] 不要验证服务器发送的证书的有效性:


// on crée un client HTTP
$httpClient = HttpClient::create([
    'auth_basic' => ['admin', 'admin'],
    "verify_peer" => false
  ]);

第 4 行指定不验证服务器证书。我们在链接部分的 [http-02.php] 脚本中已经遇到过这个问题。该脚本使用 [libcurl] 库连接到 HTTP 和 HTTPS 站点。我们当时为该库使用了以下配置:


// Initialisation d'a cURL session
  $curl = curl_init($url);
  if ($curl === FALSE) {
    // il y a eu une erreur
    return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
  }
  // options de curl
  $options = [
    // mode verbose
    CURLOPT_VERBOSE => true,
    // nouvelle connexion - pas de cache
    CURLOPT_FRESH_CONNECT => true,
    // timeout de la requête (en secondes)
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    // ne pas vérifier la validité des certificats SSL
    CURLOPT_SSL_VERIFYPEER => false,
    // suivre les redirections
    CURLOPT_FOLLOWLOCATION => true,
    // récupération du document demandé sous la forme d'a character string
    CURLOPT_RETURNTRANSFER => true
  ];
 
  // paramétrage de curl
curl_setopt_array($curl, $options);

第 17 行:常量 [CURLOPT_SSL_VERIFYPEER] 控制是否验证服务器发送的证书。当 PHP 配置中启用了 [curl] 扩展时(如本例所示),[HttpClient] 客户端实际上是一个 [curl] 客户端。 此时由 [HttpClient::create] 实例化的类即为 [CurlHttpClient] 类。该类中提供了 [curl] 常量,但名称有所不同:

$curlopts = [
            CURLOPT_URL => $url,
            CURLOPT_USERAGENT => 'Symfony HttpClient/Curl',
            CURLOPT_TCP_NODELAY => true,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
            CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
            CURLOPT_CONNECTTIMEOUT_MS => 1000 * $options['timeout'],
            CURLOPT_PROXY => $options['proxy'],
            CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
            CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
            CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
            CURLOPT_CAINFO => $options['cafile'],
            CURLOPT_CAPATH => $options['capath'],
            CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
            CURLOPT_SSLCERT => $options['local_cert'],
            CURLOPT_SSLKEY => $options['local_pk'],
            CURLOPT_KEYPASSWD => $options['passphrase'],
            CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
        ];

我们已将 [CurlHttpClient] 所使用的常量用黄色标出。

如果现在使用用户 [admin, admin] 运行 [auth-client] 客户端,我们将得到以下结果:


---Réponse avec statut : 200
---Entêtes de la réponse
date: Wed, 05 Jun 2019 10:44:37 GMT
server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
x-powered-by: PHP/7.2.11
cache-control: no-cache, private
content-length: 0
connection: close
content-type: text/html; charset=UTF-8
---Réponse du serveur :

用户身份验证成功。如果我们使用除 [admin, admin] 以外的用户运行 [auth-client] 客户端,将得到以下结果:

1
2
3
---Réponse avec statut : 403
---Entêtes de la réponse
Erreur de communication avec le serveur : HTTP/1.0 403 Forbidden returned for "https://localhost/php7/scripts-web/08/auth-server.php".

现在我们已经知道如何对安全服务器进行身份验证了。