Skip to content

8. 课程编号 TCP-IP

8.1. 概述

8.1.1. 互联网协议

本文将介绍互联网通信协议,该协议套件也常被称为 TCP/IP传输控制协议/互联网协议),其名称源自其中的两个主要协议。 在着手构建分布式应用程序之前,读者最好对网络的运作原理,特别是对 TCP/IP 协议有整体的理解。

下文是 NOVELL 于 90 年代初发布的《Lan Workplace for Dos - Administrator's Guide》文档中部分内容的翻译。


构建异构计算机网络这一总体概念源于美国国防高级研究计划局(DARPA)的研究。 DARPA 开发了一套名为 TCP/IP 的协议套件,该套件允许异构机器之间进行通信。 这些协议曾在名为 ARPAnet 的网络上进行过测试,该网络后来演变为 INTERNET 网络。 TCP/IP协议定义了传输和接收的格式及规则,这些规则与网络架构和所用硬件无关。

由 DARPA 设计并由 TCP/IP 协议管理的网络是一个分组交换网络。 此类网络将信息以称为“数据包”的小块形式在网络中传输。因此,如果一台计算机传输一个大文件,该文件将被分割成小块,这些小块将被发送到网络中,并在目的地重新组合。TCP/IP定义了这些数据包的格式,即:

  • 数据包来源
  • 目的地
  • 长度
  • 类型

8.1.2. OSI 模型

TCP/IP协议大致遵循由ISO(国际标准化组织)定义的、名为OSI(开放系统互连参考模型)的开放网络模型。 该模型描述了一个理想网络,其中机器间的通信可通过七层模型来表示:

Image

每一层从下层接收服务,并向上层提供服务。假设位于不同机器 A 和 B 上的两个应用程序想要通信:它们在 Application 层进行通信。 它们无需了解网络运行的所有细节:每个应用程序将想要传输的信息交给下层——即 Présentation 层。因此,应用程序只需了解与 Présentation 层交互的规则即可。

信息进入Présentation层后,将根据其他规则传递至Session层,依此类推,直至信息到达物理介质并被物理传输至目标机器。 到达目的地后,数据将经历与发送端机器上处理过程相反的处理。

在每一层,负责发送信息的发送方进程将其发送给另一台机器上属于同一层的接收方进程。这一过程遵循某些规则,即所谓的“层协议”。因此,最终的通信图示如下:

Image

各层的作用如下:

Physique
负责在物理介质上传输比特。 该层包含数据处理终端设备(E.T.T.D.),如终端或计算机,以及数据电路终端设备(E.T.C.D.),如调制解调器、多路复用器和集线器。该层的主要关注点包括:
. 信息编码方式的选择(模拟或数字)
. 传输模式的选择(同步或异步)。
Liaison de données
隐藏物理层的物理特性。检测并纠正传输错误。
Réseau
管理网络中发送的信息应遵循的路径。这被称为 routage:确定信息应遵循的路线,以便其到达收件人。
Transport
允许两个应用程序之间进行通信,而前几层仅支持机器间的通信。该层提供的一项服务可能是多路复用:传输层可以利用同一条网络连接(机器到机器)来传输属于多个应用程序的信息。
Session
该层包含允许应用程序在远程机器上建立并维持工作会话的服务。
Présentation
该层旨在统一不同机器上的数据表示形式。因此,来自机器A的数据将在发送至网络前,由机器A的Présentation层按照标准格式进行“包装”。 数据到达目标机器B的Présentation层后,该层将通过标准格式识别数据,并对其进行重新包装,以便机器B上的应用程序能够识别。
Application
在此层级,通常存在与用户密切相关的应用程序,例如电子邮件或文件传输。

8.1.3. TCP/IP 模型

OSI 模型是一个理想但尚未实现的模型。TCP/IP 协议套件以如下形式接近该模型:

Image

物理层

在局域网中,通常采用以太网或令牌环技术。本文仅介绍以太网技术。

以太网

这是20世纪70年代初由施乐公司(Xerox)发明、并于1978年由施乐、英特尔和数字设备公司(Digital Equipment)共同制定的分组交换局域网技术的名称。 该网络在物理上由一根直径约1.27厘米、最长500米的同轴电缆构成。可通过répéteurs进行扩展,但两台设备之间最多只能使用两个中继器。 该电缆为无源设备:所有有源元件均位于连接至电缆的设备上。每台设备通过一张网络接入卡连接至电缆,该卡包含:

  • 一个发射器(transceiver),用于检测电缆上的信号,并将模拟信号转换为数字信号,反之亦然。
  • 一个耦合器,用于接收来自发射器的数字信号并将其传输至计算机进行处理,或反向传输。

以太网技术的主要特点如下:

  • 10兆比特/秒的传输速率。
  • 总线拓扑:所有设备均连接在同一根电缆上

Image

  • 广播网络——发送设备将信息连同接收设备的地址一起传输到电缆上。所有连接的设备都会收到这些信息,只有目标设备才会保留它们。
  • 其访问方式如下:欲发送数据的发送方监听电缆——从而检测到载波信号的存在与否,若存在则表示正在进行传输。这就是载波侦听多路访问Carrier Sense Multiple Access)技术。 若未检测到载波,发送器即可决定进行传输。可能有多个发送器同时做出此决定。此时,发出的信号会相互干扰,即发生“碰撞”。 发射器能检测到这种情况:在向电缆发送信号的同时,它也会监听电缆上实际传输的内容。如果检测到电缆上传输的信息并非自己发送的,它便会推断出发生了碰撞,并停止发送。其他正在发送的发射器也会采取同样的措施。 每个发送器将在一个随机时间间隔后恢复发送,该间隔取决于各自的发送器。该技术被称为CD碰撞检测)。因此,该访问方法被称为CSMA/CD
  • 48位地址。每台机器都有一个地址,此处称为物理地址,该地址被写在连接机器与网线的网卡上。该地址被称为机器的Ethernet地址

网络层

在此层中,我们发现 IP、ICMP、ARP 和 RARP 协议。

IP(互联网协议)
在网络的两个节点之间传输数据包
ICMP
(互联网控制消息协议)
ICMP 实现一台机器上的 IP 协议程序与另一台机器上的该协议程序之间的通信。因此,它是在 IP 协议内部进行消息交换的协议。
ARP
(地址解析协议)
负责将机器的互联网地址映射到机器的物理地址
RARP
(反向地址解析协议)
将机器物理地址映射到机器互联网地址

传输层/会话层

该层包含以下协议:

TCP (传输控制协议)
确保两个客户端之间信息可靠地传递
UDP(用户数据报协议)
确保两个客户端之间信息的不可靠传输

应用层/表示层/会话层

此处包含多种协议:

TELNET
终端仿真器,允许机器A作为终端连接到机器B
FTP (File Transfer Protocol)
支持文件传输
TFTP (Trivial File Transfer Protocol)
支持文件传输
SMTP (Simple Mail Transfer protocol)
支持网络用户之间的消息交换
DNS (Domain Name System)
将主机名转换为该主机的互联网地址
XDR (eXternal Data Representation)
由 sun 创建 MicroSystems,它规定了一种与机器无关的数据标准表示形式
RPC(Remote Procedures Call)
同样由 Sun 定义,是一种独立于传输层的远程应用程序间通信协议。该协议至关重要:它使程序员无需了解传输层的细节,并使应用程序具有可移植性。该协议基于 XDR 协议
NFS (Network File System)
(同样由Sun定义),该协议允许一台机器“查看”另一台机器的文件系统。它基于前面的RPC协议

8.1.4. 互联网协议的工作原理

在 TCP/IP 环境中开发的应用程序通常会使用该环境中的多种协议。应用程序与协议的最高层进行通信。该层将信息传递给下一层,以此类推,直至到达物理介质。 在那里,信息被物理传输至接收端设备,随后将按相反方向再次穿越相同的协议层,直至到达接收该信息的应用程序。下图展示了信息的传输路径:

Image

举个例子:应用程序 FTP 定义在 Application 层,用于在机器之间传输文件。

  • 该应用程序生成一串待传输的字节序列,并将其传递给 transport.
  • transport层将该字节序列分割为segments和TCP,并在每个分段的开头添加其序号。 这些分段被传递给由 IP 协议管理的网络层。
  • IP 层创建了一个数据包,将接收到的 TCP 段封装其中。 在该数据包的头部,它放置了源机和目的机的互联网地址。它还确定了目的机的物理地址。所有这些信息被传递到数据链路和物理层,即连接机器与物理网络的网卡。
  • 在那里,数据包 IP 又被封装进一个物理帧中,并通过电缆发送给接收方。
  • 在接收端机器上,数据链路与物理层执行相反的操作:它将数据包 IP 从物理帧中解封装,并将其传递给 IP 层。
  • IP层会验证数据包是否正确:它根据接收到的位计算出一个校验和(checksum),并检查该校验和是否与数据包头中的校验和一致。如果不一致,则该数据包将被丢弃。
  • 如果数据包被判定为正确,IP层将解封装其中的TCP分段,并将其向上传递给transport层。
  • transport 层(在本例中为 TCP 层)会检查分段编号,以确保分段的顺序正确。
  • 该层还会为 TCP 数据段计算校验和。如果校验通过,TCP 层将向源端发送确认;否则,TCP 数据段将被拒绝。
  • TCP层只需将分段的数据部分传输给上一层的接收应用程序即可。

8.1.5. 互联网中的寻址问题

网络中的一个 noeud 节点可以是计算机、智能打印机、文件服务器,实际上是任何能够使用 TCP/IP 协议进行通信的设备。 每个节点都有一个物理地址,其格式取决于网络类型。在以太网中,物理地址由 6 个字节编码。X25 网络的地址是一个 14 位数字。

节点的互联网地址是一个逻辑地址:它与所使用的硬件和网络无关。这是一个4字节的地址,既标识一个局域网,也标识该网络中的一个节点。互联网地址通常以4个数字的形式表示,即4个字节的值,用点分隔。 因此,昂热大学理学院的 Lagaffe 机器的地址记为 193.49.144.1,而 Liny 机器的地址记为 193.49.144.9。由此可推断,该局域网的互联网地址为 193.49.144.0。该网络上最多可容纳 254 个节点。

由于互联网地址或IP地址与网络无关,因此A网络中的计算机可以与B网络中的计算机进行通信,而无需关心自身所在的网络类型:只需知道对方的IP地址即可。 每个网络的 IP 协议负责双向转换 IP 地址与物理地址。

所有 IP 地址都必须是唯一的。由官方机构负责分配这些地址。实际上,这些机构会为局域网分配一个地址,例如为昂热大学理学院网络分配 193.49.144.0。 该网络的管理员随后可以按需分配 IP 193.49.144.1 至 193.49.144.254 之间的地址。该地址通常记录在每台联网机器的特定文件中。

8.1.5.1. IP 地址类

一个 IP 地址是由 4 个字节组成的序列,通常记为 I1.I2.I3.I4,实际上包含两个地址:

  • 网络地址
  • 该网络中某个节点的地址

根据这两个字段的大小,IP地址分为3类:A类、B类和C类。

A类

地址 IP:I1.I2.I3.I4 具有 R1.N1.N2.N3 的形式,其中

R1 是网络地址

N1.N2.N3 是该网络中某台机器的地址

更准确地说,A类地址 IP 的形式如下:

Image

其中网络地址占用7位,节点地址占用24位。因此,A类网络最多可有127个,每个网络最多包含2²⁴个节点。

B类

在此,地址 IP:I1.I2.I3.I4 的形式为 R1.R2.N1.N2,其中

R1.R2 是网络地址

N1.N2 是该网络中某台机器的地址

更准确地说,B类地址 IP 的形式如下:

Image

网络地址和节点地址各占2字节(确切地说,是14位)。因此,我们可以拥有2¹⁴个B类网络,每个网络最多包含2¹⁶个节点。

C类

在此类中,地址 IP:I1.I2.I3.I4 的形式为 R1.R2.R3.N1,其中

R1.R2.R3 是网络地址

N1 是该网络中某台机器的地址

更准确地说,C类地址IP的形式如下:

Image

其中网络地址占用3个字节(减去3位),节点地址占用1个字节。因此,我们可以拥有221个C类网络,每个网络最多包含256个节点。

安格斯大学理学院的计算机 Lagaffe 的地址为 193.49.144.1,可见其高字节值为 193,即二进制表示为 11000001。由此可推断该网络属于 C 类。

保留地址

. 某些地址(如 IP)属于网络地址,而非网络中的节点地址。 这些地址中,节点地址被设为0。因此,地址193.49.144.0是昂热大学理学院网络的地址IP。因此,网络中没有任何节点的地址可以是零。

. 当在地址 IP 中,节点地址仅包含数字 1 时,则该地址为广播地址:该地址指向网络中的所有节点

. 在理论上可容纳 2⁸=256 个节点的 C 类网络中,若剔除两个被禁止的地址,则仅剩 254 个有效地址。

8.1.5.2. 互联网地址 <--> 物理地址转换协议

我们已经看到,当一台机器向另一台机器发送信息时,这些信息在通过IP层时会被封装在数据包中。这些数据包的形式如下:

Image

因此,数据包 IP 包含源机和目标机的互联网地址。当该数据包被传递到负责将其发送至物理网络的层时,会添加其他信息以形成最终发送至网络的物理帧。例如,以太网上的帧格式如下:

Image

在最终的帧中,包含源主机和目标主机的物理地址。这些地址是如何获得的?

发送方主机已知其通信对象的主机地址为 IP,它会通过一种名为 ARP地址解析协议)的特殊协议来获取该主机的物理地址。

  • 它发送一种名为 ARP 的特殊数据包,其中包含目标机器的地址 IP。同时,它还特意在该数据包中添加了自己的地址 IP 及其物理地址。
  • 该数据包被发送至网络中的所有节点。
  • 这些节点识别出该数据包的特殊性质。识别出数据包中包含其地址 IP 的节点,会通过向数据包发送方发送其物理地址来响应。它是如何做到的?因为它在数据包中找到了发送方的地址 IP 及其物理地址。
  • 因此,发件人收到了其所需的物理地址。它将该地址存储在内存中,以便日后向同一收件人发送其他数据包时使用。

一台机器的 IP 地址通常记录在其某个文件中,因此它可以通过查阅该文件来获取该地址。该地址可以更改:只需编辑该文件即可。而物理地址则记录在网卡的内存中,无法更改。

当管理员希望重新规划网络结构时,可能需要更改所有节点的 IP 地址,从而需要编辑各个节点的配置文件。如果机器数量众多,这不仅费时费力,还容易出错。 一种方法是:不为机器分配 IP 地址,而是将一个特殊代码写入该文件中,机器将据此从该文件中获取其 IP 地址。 当机器发现自己没有 IP 地址时,会通过一种名为 RARP(反向地址解析协议)的协议进行请求 随后,它向网络发送一个名为 RARP 的特殊数据包(类似于之前的 ARP 数据包),并在其中写入其物理地址。该数据包被发送至所有节点,这些节点随后识别出一个 RARP 数据包。 其中一个节点,称为服务器RARP,拥有一个文件,其中记录了所有节点的物理地址与IP地址之间的对应关系。 于是,它向 RARP 数据包的发送方响应,将其 IP 地址发回给对方。因此,如果管理员想要重新配置网络,只需编辑 RARP 服务器的映射文件即可。 该服务器通常应拥有一个固定的 IP 地址,且无需自行使用 RARP 协议即可知晓该地址。

8.1.6. 互联网中的所谓 IP 网络层

IP协议(互联网协议)定义了数据包应采用的形式,以及在发送或接收时应如何处理它们。这种特殊类型的数据包被称为IP数据报。我们之前已经介绍过它:

Image

关键在于,除了待传输的数据外,IP数据报还包含源机和目的机的互联网地址。因此,接收方能够识别消息的发送者。

与网络帧的长度由其传输的网络的物理特性决定不同,数据报 IP 的长度由软件固定,因此在不同的物理网络上长度保持不变。 我们看到,从网络层向下到物理层时,数据报 IP 被封装在物理帧中。我们以以太网的物理帧为例:

Image

物理帧在节点间传输,最终到达目的地,而该目的地可能并不位于与发送端机器相同的物理网络上。 因此,数据包 IP 可能在连接不同类型网络的节点处,依次被封装到不同的物理帧中。此外,数据包 IP 也可能因体积过大而无法封装到物理帧中。 此时,发生此问题的节点上的 IP 软件将根据特定规则将数据包 IP 拆分为 fragments,随后将它们分别发送至物理网络。这些数据包仅会在最终目的地被重新组装。

8.1.6.1. 路由

路由是指将数据包 IP 传输至目的地的方法。主要有两种方式:直接路由和间接路由。

直接路由

直接路由是指在同一网络内,将数据包 IP 从发件人直接传输至收件人的过程:

  • 发送 IP 数据报的机器拥有收件人的地址 IP。
  • 它通过协议 ARP 获取收件人的物理地址,或者如果该地址已存在于其表中,则直接从表中获取。
  • 它将数据包发送到网络上的该物理地址。

间接路由

间接路由是指将数据包IP转发至位于与发件人所属网络不同的网络中的目的地。在此情况下,源主机和目标主机的IP地址中的网络地址部分不同。源主机识别到这一点。 于是,它将数据包发送到一个名为路由器router)的特殊节点。该节点连接本地网络与其他网络,源主机在其表中找到了该路由器的地址 IP——该地址最初是从文件、永久存储器中获取的,或是通过网络上流通的信息获得的。

路由器连接两个网络,并在这两个网络内部拥有地址 IP。

Image

在上述示例中:

  • 网络 1 的互联网地址为 193.49.144.0,网络 2 的地址为 193.49.145.0。
  • 在网络 1 内,路由器的地址为 193.49.144.6;在网络 2 内,其地址为 193.49.145.3。

路由器的作用是将收到的数据包 IP(该数据包包含在网络 1 的典型物理帧中)转换为可在网络 2 上传输的物理帧。 如果数据包收件人的地址 IP 位于第 2 号网络中,路由器将直接向其发送数据包;否则,它会将数据包发送给另一台路由器,该路由器连接第 2 号网络与第 3 号网络,以此类推。

8.1.6.2. 错误与控制消息

同样位于网络层(即与 IP 协议处于同一层级)的,还有 ICMP 协议(互联网控制消息协议)。该协议用于发送有关网络内部运行状况的消息:节点故障、路由器拥塞等…… ICMP消息被封装在IP数据包中,并通过网络发送。 各节点的 IP 层会根据收到的 ICMP 消息采取相应的行动。因此,应用程序本身永远不会察觉到这些网络特有的问题。

节点将利用 ICMP 信息来更新其路由表。

8.1.7. 传输层:UDP 和 TCP 协议

8.1.7.1. UDP 协议:用户数据报协议

UDP 协议支持两点间非可靠的数据交换,即无法保证数据包能正确路由至目的地。应用程序若需要,可自行处理此问题,例如在发送消息后等待确认收到,再发送下一条消息。

目前,在网络层面,我们讨论的是设备的 IP 地址。然而,在一台设备上,可能同时存在多个进程,且这些进程均可相互通信。 因此,在发送消息时,不仅需要指定接收机器的 IP 地址,还需指定接收进程的“名称”。该名称实际上是一个数字,称为端口号。 某些端口号被保留给标准应用程序使用:例如,端口 69 用于 tftp(简单文件传输协议)应用程序。

由 UDP 协议处理的数据包也被称为数据报。它们的格式如下:

Image

这些数据报将被封装在 IP 数据包中,随后封装在物理帧中。

8.1.7.2. TCP协议:传输控制协议

若要实现安全通信,仅靠 UDP 协议是不够的:应用程序开发人员必须自行设计一种协议,以便检测数据包是否被正确路由。

TCP协议(传输控制协议)可避免这些问题。其特征如下:

  • 希望发送数据的进程首先需与接收该信息的目标进程建立连接。该连接在发送端机器的某个端口与接收端机器的某个端口之间建立。两个端口之间由此形成一条虚拟路径,该路径仅供建立连接的两个进程专属使用。
  • 源进程发送的所有数据包均沿此虚拟路径传输,并按发送顺序到达接收端——而UDP协议无法保证这一点,因为数据包可能沿不同路径传输。
  • 发送的信息呈现连续性特征。发送进程按自身节奏发送信息。这些信息未必会立即发送:TCP协议会等待积累足够的数据后再进行发送。它们被存储在一个名为TCP段的结构中。 该分段填满后将传输至 IP 层,并在该层封装为 IP 数据包。
  • TCP协议发送的每个分段都有编号。接收方TCP协议会验证是否按顺序正确接收了这些分段。对于每个正确接收的分段,它都会向发送方发送一份确认。
  • 当发送方收到该确认时,会通知发送进程。因此,发送进程可以得知某个分段已成功送达,而这在 UDP 协议中是无法实现的。
  • 如果经过一段时间后,发送了数据段的 TCP 协议未收到确认,它将重发该数据段,从而保证信息传输服务的质量。
  • 在两个通信进程之间建立的虚拟电路是 full-duplex:这意味着信息可以双向传输。因此,即使源进程仍在发送信息,目标进程也可以发送确认。 例如,这使得源协议 TCP 能够发送多个数据段,而无需等待确认。如果经过一段时间后,它发现尚未收到编号为 n 的某个数据段的确认,它将从该点重新开始发送数据段。

8.1.8. 应用程序层

在 UDP 和 TCP 协议之上,还存在各种标准协议:

TELNET

该协议允许网络中机器 A 的用户连接到机器 B(通常称为主机)。TELNET 在机器 A 上模拟一个所谓的通用终端。 因此,用户操作起来就如同拥有了一台连接到机器 B 的终端。Telnet 基于 TCP 协议。

FTP:(文件传输协议)

该协议支持两台远程机器之间的文件交换,以及文件操作(例如创建目录)。它基于 TCP 协议。

TFTP:(简易文件传输控制)

该协议是 FTP 的变体。它基于 UDP 协议,且比 FTP 更为简单。

DNS:(域名系统)

当用户希望与远程机器交换文件时(例如通过 FTP),必须知道该机器的互联网地址。 例如,要在昂热大学的Lagaffe机器上进行FTP操作,需按以下方式启动FTP:FTP 193.49.144.1

这要求必须有一个将机器与 IP 地址相互映射的目录。该目录中,机器很可能使用符号名称来标识,例如:

昂热大学的 DPX2/320 机器

昂热大学的 Sun 机器 ISERPA

显然,用名称来指代一台机器,比使用其 IP 这样的地址要方便得多。这便引出了名称唯一性的问题:有数百万台机器相互连接。人们可能会设想由一个中央机构来分配名称。但这无疑会相当繁琐。实际上,名称的管理已被分配到各个域名中。 每个域名通常由一个运作非常灵活的机构管理,该机构在选择机器名称方面拥有完全的自由。因此,法国境内的机器属于fr域名,该域名由巴黎的Inria管理。为了进一步简化,管理权限被进一步下放:在fr域名内部创建了子域名。例如,昂热大学就属于univ-Angers域名。 管理该域的服务机构在为昂热大学网络中的计算机命名方面拥有完全的自由。目前该域尚未进一步细分。但在拥有大量联网计算机的大型大学中,可能会进行细分。

昂热大学的 DPX2/320 被命名为 Lagaffe,而 PC 和 486DX50 则被命名为 liny。 如何从外部引用这些机器?需明确它们所属的域名层级。因此,机器 Lagaffe 的完整名称为:

        Lagaffe.univ-Angers.fr

在域内部,可以使用相对名称。因此,在 fr 域内且位于 univ-Angers 域之外时,可以通过以下方式引用 Lagaffe 机器:

        Lagaffe.univ-Angers

最后,在 univ-Angers 域内,只需使用

        Lagaffe

因此,应用程序可以通过其名称来引用一台机器。但归根结底,仍需获取该机器的互联网地址。这是如何实现的?假设从机器 A 想要与机器 B 通信。

  • 如果机器 B 与机器 A 属于同一个域,那么很可能在机器 A 的某个文件中就能找到其地址 IP。
  • 否则,机器A将在另一个文件(或与前文相同的文件)中找到一份包含若干域名服务器及其地址IP的列表。域名服务器的职责就是将机器名称与其地址IP进行映射。 计算机A将向列表中的第一个域名服务器发送一个特殊请求,称为DNS请求,其中包含所查询的计算机名称。如果被查询的服务器在其记录中拥有该名称,它将向计算机A发送相应的地址IP。 否则,该服务器也会在其文件中查找可查询的域名服务器列表,并据此进行查询。因此,将查询一定数量的域名服务器,但并非无序进行,而是以尽量减少请求的方式进行。如果最终找到了该机器,响应将传回机器A。

XDR:(eXternal 数据表示)

该协议由 sun MicroSystems 创建,规定了一种与机器无关的数据标准表示形式。

RPC:(远程过程调用)

该协议同样由 sun 定义,是一种独立于传输层的远程应用程序通信协议。该协议非常重要:它使程序员无需了解传输层的细节,并使应用程序具有可移植性。该协议基于 XDR 协议

NFS:网络文件系统

该协议同样由Sun定义,它允许一台机器“看到”另一台机器的文件系统。它基于前面的RPC协议。

8.1.9. 结论

在本介绍中,我们概述了互联网协议的一些主要内容。若想深入了解这一领域,可阅读道格拉斯·科默(Douglas Comer)的优秀著作:

书名:《TCP/IP:架构、协议与应用》。

作者:道格拉斯·COMER

出版社:InterEditions

8.2. Java中的网络地址管理

8.2.1. 定义

互联网上的每台机器都通过一个唯一的地址或名称来标识。在 Java 中,这两者由 InetAddress 类进行管理,以下是该类的一些方法:

byte [] getAddress()
返回当前 InetAddress 实例的 IP 地址的 4 个字节
String getHostAddress()
返回当前实例 InetAddress 的地址 IP
String getHostName()
返回当前实例 InetAddress 的互联网名称
String toString()
返回当前实例 InetAddress 的地址标识 IP/互联网名称
InetAddress getByName(String Host)
创建由 Host 指定的机器的实例 InetAddress。如果 Host 未知,则抛出异常。Host 可以是机器的互联网名称,也可以是其地址 IP,格式为 I1.I2.I3.I4
InetAddress getLocalHost()
创建包含此指令的程序所运行机器的 InetAddress 实例。

8.2.2. 一些示例

8.2.2.1. 识别本地机器


import java.net.*;

public class localhost{
  public static void main (String arg[]){
    try{
      InetAddress adresse=InetAddress.getLocalHost();
    byte[] IP=adresse.getAddress();
    System.out.print("IP=");
    int i;
    for(i=0;i<IP.length-1;i++) System.out.print(IP[i]+".");
    System.out.println(IP[i]);
      System.out.println("adresse="+adresse.getHostAddress());
    System.out.println("nom="+adresse.getHostName());
    System.out.println("identité="+adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getLocalHost : "+e);
    }// try 结束
  }// main 结束
}// 类结束

执行结果如下:

IP=127.0.0.1
adresse=127.0.0.1
nom=tahe
identité=tahe/127.0.0.1

每台机器都有一个内部地址 IP,即 127.0.0.1。当程序使用该网络地址时,实际上是在使用其运行的这台机器。该地址的优势在于无需网卡,因此无需连接网络即可测试网络程序。 另一种指定本地计算机的方法是使用 localhost 这个名称。

8.2.2.2. 识别任意一台计算机


import java.net.*;

public class getbyname{
  public static void main (String arg[]){
    String nomMachine;
    // 获取参数
    if(arg.length==0) 
      nomMachine="localhost";
    else nomMachine=arg[0];
    // 尝试获取机器地址
    try{
      InetAddress adresse=InetAddress.getByName(nomMachine);
      System.out.println("IP : "+  adresse.getHostAddress());
      System.out.println("nom : "+ adresse.getHostName());
      System.out.println("identité : "+ adresse);
    } catch (UnknownHostException e){
      System.out.println ("Erreur getByName : "+e);
    }// try结束
  }// main结束
}// 类结束

使用 java getbyname 调用,得到以下结果:

IP : 127.0.0.1
nom : localhost
identité : localhost/127.0.0.1

调用 java getbyname shiva.istia.univ-angers.fr 时,结果如下:

IP : 193.52.43.5
nom : shiva.istia.univ-angers.fr
identité : shiva.istia.univ-angers.fr/193.52.43.5

调用 Java 的 getbyname 方法 www.ibm.com,结果如下:

IP : 204.146.18.33
nom : www.ibm.com
identité : www.ibm.com/204.146.18.33

8.3. 通信 TCP-IP

8.3.1. 概述

Image

当机器 A 上的应用程序 AppA 想要与互联网上的机器 B 上的应用程序 AppB 通信时,它必须知道以下几项信息:

  • 机器 B 的地址 IP 或名称
  • 应用程序 AppB 所使用的端口号。因为机器 B 可能支持许多在互联网上运行的应用程序。当它收到来自网络的信息时,必须知道这些信息是发给哪个应用程序的。 机器B上的应用程序通过称为通信端口的接口访问网络。机器B接收的数据包中包含这一信息,以便将其分发给正确的应用程序。
  • 机器B所支持的通信协议。在本研究中,我们将仅使用TCP-IP协议。
  • 应用程序AppB所接受的对话协议。实际上,机器A和B将进行“对话”。它们传递的内容将被封装在TCP-IP协议中。 然而,当链路另一端的AppB应用程序接收到AppA应用程序发送的信息时,它必须能够对其进行解析。 这类似于两个人A和B通过电话交流的情况:他们的对话由电话传输。语音将被电话A编码为信号,通过电话线传输,到达电话B后被解码。 此时,B才能听到对方的说话声。这正是对话协议概念的由来:如果A说的是法语,而B不懂这种语言,那么A和B就无法进行有效的对话。

因此,两个通信的应用程序必须就将采用的对话类型达成一致。例如,与服务ftp的对话与服务pop的对话并不相同:这两个服务接受的命令不同。 它们采用的是不同的对话协议。

8.3.2. TCP协议的特性

本文仅探讨使用 TCP 传输协议的网络通信。在此回顾该协议的特征:

  • 希望发送信息的进程首先会与接收该信息的目标进程建立连接。该连接建立在发送端机器的一个端口与接收端机器的一个端口之间。两个端口之间由此形成一条虚拟路径,该路径仅供建立连接的这两个进程专属使用。
  • 源进程发送的所有数据包均沿此虚拟路径传输,并按发送顺序到达
  • 发送的信息呈现连续性特征。发送进程按自身节奏发送信息。这些信息未必会立即发送:TCP协议会等待积累足够的数据后再进行发送。它们被存储在一个名为TCP分段的结构中。 该分段填满后,将传输至 IP 层,并在该层封装为 IP 数据包。
  • TCP协议发送的每个数据段都带有编号。接收方TCP协议会验证其是否按顺序接收到了数据段。对于每个正确接收到的数据段,它都会向发送方发送一份确认。
  • 当发送方收到该确认后,会通知发送进程。因此,发送进程可以得知该分段已成功送达。
  • 如果经过一段时间后,发送了该分段的 TCP 协议未收到确认,它将重发该分段,从而保证信息传输服务的质量。
  • 在两个通信进程之间建立的虚拟电路是 full-duplex:这意味着信息可以双向传输。因此,即使源进程仍在发送信息,目标进程也可以发送确认。 例如,这使得源协议 TCP 能够发送多个数据段,而无需等待确认。如果经过一段时间后,它发现尚未收到编号为 n 的某个数据段的确认,它将从该点重新开始发送数据段。

8.3.3. 客户端-服务器关系

通常,互联网通信是非对称的:主机 A 发起连接以向主机 B 请求服务:它明确表示希望与主机 B 的 SB1 服务建立连接。主机 B 接受或拒绝该请求。 若服务器接受,机器A即可向服务SB1发送请求。这些请求必须符合服务SB1所支持的交互协议。 由此,在称为客户端的机器A与称为服务器端的机器B之间建立起一种请求-响应对话。双方中的一方将关闭连接。

8.3.4. 客户端架构

请求服务器应用程序服务的网络程序架构如下:

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
        tant que ce n'est pas fini
            préparer une demande
            l'émettre vers la machine B
            attendre et récupérer la réponse
            la traiter
        fin tant que
finsi

8.3.5. 服务器架构

提供服务的程序架构如下:

ouvrir le service sur la machine locale
tant que le service est ouvert
        se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
        lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

服务器程序对客户端的初始连接请求与后续获取服务的请求进行区别处理。该程序本身并不提供服务。如果它提供服务,那么在服务期间就无法监听连接请求,客户端将无法获得服务。 因此,它采取了另一种方式:一旦监听端口接收到连接请求并予以接受,服务器就会创建一个任务,负责提供客户端请求的服务。该服务是在服务器机器上的另一个端口(称为服务端口)上提供的。这样就可以同时为多个客户端提供服务。

一个服务任务的结构如下:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

8.3.6. Socket 类

8.3.6.1. Définition

互联网通信程序所使用的基本工具是 socket。这个英语单词意为“电源插座”。在此,其含义被扩展为“网络插座”。 为了使应用程序能够在互联网上发送和接收信息,它需要一个网络插座,即 socket。该工具最初是在伯克利大学的 Unix 版本中创建的。此后,它已被移植到所有 Unix 系统以及 Windows 环境中。 在 Java 虚拟机中,它也以两种形式存在:用于客户端应用程序的 Socket 类,以及用于服务器应用程序的 ServerSocket 类。本文将详细说明 Socket 类中的部分构造函数和方法:

public Socket(String host, int port)
与机器 host 的端口 port 建立远程连接
public int getLocalPort()
返回套接字使用的本地端口号
  
public int getPort()
返回套接字连接到的远程端口号
  
public InetAddress getLocalAdress()
返回套接字关联的本地地址
  
public InetAddress getInetAdress()
返回套接字关联的远程地址
  
public InputStream getInputStream()
创建一个输入流,用于读取远程伙伴发送的数据
  
public OutputStream getOutputStream()
返回一个输出流,用于向远程伙伴发送数据
  
public void shutdownInput()
关闭套接字的输入流
  
public void shutdownOutput()
关闭套接字的输出流
  
public void close()
关闭套接字及其输入/输出流
  
public String toString()
返回一个“代表”该套接字的字符串
 

8.3.6.2. 与服务器建立连接

我们已经看到,为了让机器 A 与机器 B 上的某个服务建立连接,它需要两项信息:

  • 机器 B 的地址 IP 或机器名称
  • 目标服务运行的端口号

构造函数

    public Socket(String  host, int  port);

会创建一个套接字,并将其连接到机器 hostport 端口。该构造函数在以下情况下会抛出异常:

  • 地址错误
  • 端口错误
  • 请求被拒绝
  • ……

我们需要处理此异常:


    Socket  sClient=null;
    try{
        sClient=new Socket(host,port);
    } catch(Exception e){
        // 连接失败 - 处理错误
        ….
    }

如果连接请求成功,客户端将被分配一个本地端口用于与机器 B 通信。建立连接后,可通过以下方法获取该端口:

public int getLocalPort();

如果连接成功,我们看到服务器端会由另一个在所谓服务端口上运行的进程来提供服务。可以通过以下方法获取该端口号:

public int getPort();

8.3.6.3. 通过网络发送信息

可通过以下方法获取套接字(即网络)的写入流:

public OutputStream getOutputStream();

该流中发送的所有内容都将被服务器端的服务端口接收。许多应用程序采用以换行符结尾的文本行形式进行交互。因此,在这种情况下,方法 println 非常实用。 此时,我们将输出流 OutputStream 转换为包含方法 println 的流 PrintWriter。写入操作可能会引发异常。

8.3.6.4. 读取来自网络的信息

可以通过以下方法获取套接字上接收到的信息的读取流:

public InputStream getInputStream();

该流中读取的所有内容均来自服务器端的服务端口。对于采用以换行符结尾的文本行进行对话的应用程序,建议使用方法 readLine。 为此,需将输入流 InputStream 转换为包含方法 readLine() 的流 BufferedReader。读取操作可能会引发异常。

8.3.6.5. 关闭连接

使用以下方法进行:

public void close();

该方法可能引发异常。所占用的资源(特别是网络端口)将被释放。

8.3.6.6. 客户端架构

现在我们已具备描述互联网客户端基本架构的要素:


    Socket  sClient=null;
    try{
            // 连接到在机器 M 的端口 P 上运行的服务
        sClient=new Socket(M,P);

        // 创建客户端套接字的输入/输出流
        BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
        PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);

        // 请求-响应循环
        boolean  fini=false;
        String demande;
        String réponse;
        while (! fini){
            // 准备请求
            demande=…
            // 发送请求
            out.println(demande);
            // 读取响应
            réponse=in.readLine();
            // 处理响应

        }
        // 完成
        sClient.close();
    } catch(Exception e){
        // 处理异常
        ….
    }

为了避免示例过于复杂,我们没有尝试处理构造函数 Socket 或方法 readline、getInputStream、getOutputStream、close 所产生的不同类型的异常。所有异常都被合并为一个异常。

8.3.7. ServerSocket 类

8.3.7.1. Définition

该类用于管理服务器端的套接字。下面我们将详细说明该类中的部分构造函数和方法:

public ServerSocket(int port)
在端口上创建一个监听套接字port
public ServerSocket(int port, int count)
同上,但将队列大小固定为 count,c.a.d。即当客户端连接到达时服务器正忙,此时最多可排队的客户端连接数。
public int getLocalPort()
返回套接字使用的监听端口号
public InetAddress getInetAdress()
返回套接字关联的本地地址
public Socket accept()
使服务器进入等待连接状态(阻塞操作)。当收到客户端连接时,返回一个用于向客户端提供服务的套接字。
public void close()
关闭套接字及其 I/O 流
public String toString()
返回一个“代表”该套接字的字符串
public void close()
关闭服务套接字并释放与其关联的资源

8.3.7.2. 打开服务

可通过以下两个构造函数实现:

public ServerSocket(int  port);    
public ServerSocket(int  port, int  count);

port 是服务的监听端口:客户端向该端口发送连接请求。count 是服务队列的最大容量(默认值为 50),该队列用于存储服务器尚未响应的客户端连接请求。 当队列已满时,新到达的连接请求将被拒绝。这两个构造函数都会抛出异常。

8.3.7.3. 接受连接请求

当客户端向服务的监听端口发送连接请求时,服务通过以下方法接受该请求:

    public Socket accept();

该方法返回一个 Socket 实例:即服务套接字,服务将通过该套接字提供,通常由另一个任务处理。该方法可能抛出异常。

8.3.7.4. 通过服务套接字进行读写

由于服务套接字是 Socket 类的实例,请参阅前面讨论过该主题的章节。

8.3.7.5. 识别客户端

获取服务套接字后,可通过以下方法识别客户端

    public InetAddress getInetAddress()

方法来识别客户端。随后即可获取 IP 地址及客户名称。

8.3.7.6. 关闭服务

可通过方法

    public void close();

方法实现。这将释放已占用的资源,特别是监听端口。该方法可能会引发异常。

8.3.7.7. 服务器的基本架构

根据上述内容,我们可以描述服务器的基本结构:


SocketServer sEcoute=null;
try{
    // 服务已启动
    int portEcoute=…
    int maxConnexions=…
    sEcoute=new ServerSocket(portEcoute,maxConnexions);

    // 处理连接请求
    boolean fini=false;
    Socket sService=null;
    while( ! fini){
        // 等待并接受请求
        sService=sEcoute.accept();

        // 服务由另一个任务提供,并将服务套接字传递给该任务
        new Service(sService).start();

        // 重新进入连接请求等待状态
    }
    // 处理完毕——关闭服务
    sEcoute.close();
} catch (Exception e){
    // 处理异常

}

Service 是一个 thread,其外观可能如下所示:


public class Service extends Thread{

    Socket sService;        // 服务套接字

    // 构造函数
    public Service(Socket S){
        sService=S;
    }

// 运行
public void run(){
    try{
        // 创建输入/输出流
    BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
    PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);

    // 请求-响应循环
    boolean  fini=false;
    String demande;
    String réponse;
    while (! fini){
        // 读取请求
        demande=in.readLine();

        // 处理请求 


        // 准备响应
        réponse=…

        // 发送响应
        out.println(réponse);
    }
    // 完成
    sService.close();
    } catch(Exception e){
    // 处理异常
    ….
    }// try
} // 运行

8.4. 应用

8.4.1. 回显服务器

我们将编写一个回显服务器,该服务器将通过以下命令从窗口 DOS 启动:

    java serveurEcho port

该服务器在作为参数传入的端口上运行。它仅将客户端发送的请求连同其身份(IP+名称)一并返回给客户端。它的等待队列中最多可接受2个连接。这里包含了TCP服务器的所有组成部分。程序如下:

// 调用:serveurEcho 端口
// 回显服务器
// 将客户端发送的行返回给客户端


import java.net.*;
import java.io.*;

public class serveurEcho{
    public final static String syntaxe="Syntaxe : serveurEcho port";
    public final static int nbConnexions=2;

    // 主程序
    public static void main (String arg[]){

     // 是否有参数
     if(arg.length != 1)
        erreur(syntaxe,1);

     // 该参数必须是大于0的整数
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[0]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // 创建监听套接字
     ServerSocket ecoute=null;
     try{
        ecoute=new ServerSocket(port,nbConnexions);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket d'écoute ("+e+")",3);
     }

     // 后续
     System.out.println("Serveur d'écho lancé sur le port " + port);

     // 服务循环
     boolean serviceFini=false;
     Socket service=null;
     while (! serviceFini){
         // 等待客户端
        try{
            service=ecoute.accept();
        } catch (IOException e){
                erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
        }

         // 识别连接
        try{
            System.out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }


         // 服务由另一个任务处理
        new traiteClientEcho(service).start();
     }// while循环结束
    }// main结束

// 显示错误
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // 识别
    private static String identifie(InetAddress Host){
        // 主机识别
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// 类结束


// 为回显服务器的客户端提供服务

class traiteClientEcho extends Thread{

    private Socket service;            // 服务套接字
    private BufferedReader in;        // 输入流
    private PrintWriter out;            // 输出流

     // 构造函数
    public traiteClientEcho(Socket service){
        this.service=service;
    }

     // run 方法
    public void run(){

         // 创建输入和输出流
        try{
            in=new BufferedReader(new InputStreamReader(service.getInputStream()));
        } catch (IOException e){
                erreur("Erreur lors de la création du flux déentrée de la socket de service ("+e+")",1);
        }// try 结束
        try{
            out=new PrintWriter(service.getOutputStream(),true);
        } catch (IOException e){
                erreur("Erreur lors de la création du flux de sortie de la socket de service ("+e+")",1);
        }// try 结束

         // 将连接标识发送给客户端
        try{
            out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }

         // 请求/写入响应读取循环
        String demande,reponse;
        try{
             // 当客户端发送文件结束标记时,服务停止
            while ((demande=in.readLine())!=null){
                // 请求回显
                reponse="["+demande+"]";
                out.println(reponse);
                 // 当客户端发送“结束”时,服务停止
                if(demande.trim().toLowerCase().equals("fin")) break;
            }// while 循环结束
        } catch (IOException e){
                erreur("Erreur lors des échanges client/serveur ("+e+")",3);
        }// try 结束

         // 关闭套接字
        try{
            service.close();
        } catch (IOException e){
            erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
        }// try结束
    }// 运行结束

     // 显示错误
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }// 结束错误

     // 识别
    private String identifie(InetAddress Host){
         // 主机识别
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// 类结束

该服务所需的两个类已合并到同一个源文件中。其中只有一个类(即包含函数 main 的类)具有属性 public。 服务器的结构符合 TCP 服务器的通用架构。其中添加了一个方法(identifie),用于识别服务器与客户端之间的连接。以下是部分运行结果:

服务器通过以下命令启动

    java serveurEcho 187

随后,控制窗口中会显示以下消息:

Serveur d'écho lancé sur le port 187

要测试该服务器,我们使用名为 telnet 的程序,该程序在 Unix 和 Windows 系统上均可运行。Telnet 是一款通用的 TCP 客户端,适用于所有在通信中接受以换行符结尾的文本行的服务器。 我们的回显服务器正是如此。在 Windows 系统(本例中为 Windows 2000)下,通过在 DOS 窗口中输入 telnet,启动第一个 telnet 客户端:


DOS>telnet
Microsoft (R) Windows 2000 (TM) version 5.00 (numéro 2195)
Client Telnet Microsoft
Client Telnet numéro 5.00.99203.1

Le caractère d'échappement est 'CTRL+$'

Microsoft Telnet> help

Les commandes peuvent être abrégées. Les commandes prises en charge sont :

close           ferme la connexion en cours
display         affiche les paramètres d'opération
open            ouvre une connexion à un site
quit            quitte telnet
set             définit les options (entrez 'set ?' pour afficher la liste)
status          affiche les informations d'état
unset           annule les options (entrez 'unset ?' pour afficher la liste)
? ou help       affiche des informations d'aide

Microsoft Telnet> set ?
NTLM            Active l'authentification NTLM.
LOCAL_ECHO      Active l'écho local.
TERM x          (où x est ANSI, VT100, VT52 ou VTNT))
CRLF            Envoi de CR et de LF

Microsoft Telnet> set local_echo

Microsoft Telnet> open localhost 187

默认情况下,程序 telnet 不会显示键盘输入的命令。若要显示这些命令,请执行以下命令:

Microsoft Telnet> set local_echo

要与服务器建立连接,并指定回显服务的端口(187)以及服务器所在机器的地址(localhost),请执行以下命令:

Microsoft Telnet> open localhost 187

在客户端的 DOS 窗口中,将收到以下消息:

Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

在服务器窗口中,显示以下消息:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]

这里 tahelocalhost 指代同一台机器。在客户端窗口 telnet 中,可以输入文本行。服务器会将它们回显出来:

Client [127.0.0.1,tahe,1059] connectÚ au serveur [127.0.0.1,tahe,187]
je suis là
[je suis là]
au revoir
[au revoir]

需要注意的是,客户端端口(1059)已被正确检测到,但服务端口(187)与监听端口(187)完全相同,这出乎意料。实际上,我们本应得到服务套接字的端口,而非监听端口。 需要验证在 Unix 系统下是否会得到相同的结果。现在,启动第二个客户端 telnet。服务器窗口显示为:

Serveur d'écho lancé sur le port 187
Client [127.0.0.1,tahe,1059] connecté au serveur [127.0.0.1,tahe,187]
Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]

在第二个客户端的窗口中,我们也可以输入文本行:

Client [127.0.0.1,tahe,1060] connecté au serveur [127.0.0.1,tahe,187]
ligne1
[ligne1]
ligne2
[ligne2]

由此可见,回显服务器可以同时为多个客户端提供服务。telnet客户端可以通过关闭其运行的DOS窗口来终止。

8.4.2. 回显服务器的Java客户端

在前一部分中,我们使用了一个 telnet 客户端来测试回显服务。现在,我们将编写自己的客户端:

// 调用:clientEcho 机器端口
// 回显服务器的客户端
// 向服务器发送行,服务器将其回显

import java.net.*;
import java.io.*;

public class clientEcho{
    public final static String syntaxe="Syntaxe : clientEcho machine port";

    // 主程序    
    public static void main (String arg[]){

     // 是否有两个参数
     if(arg.length != 2)
        erreur(syntaxe,1);

     // 第一个参数必须是现有机器的名称
    String machine=arg[0];
    InetAddress serveurAddress=null;
    try{
        serveurAddress=InetAddress.getByName(machine);
    } catch (Exception e){
        erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
    }

     // 端口必须是大于0的整数
     int port=0;
     boolean erreurPort=false;
     Exception E=null;
     try{
        port=Integer.parseInt(arg[1]);
     }catch(Exception e){
            E=e;
            erreurPort=true;
     }
     erreurPort=erreurPort || port <=0;
     if(erreurPort)
        erreur(syntaxe+"\nPort incorrect ("+E+")",3);

     // 连接到服务器
     Socket sClient=null;
     try{
        sClient=new Socket(machine,port);
     } catch (Exception e){
        erreur("Erreur lors de la création de la socket de communication ("+e+")",4);
     }

     // 识别连接
    try{
        System.out.println("Client : Client ["+identifie(InetAddress.getLocalHost())+","+
        sClient.getLocalPort()+"] connecté au serveur [" + identifie (sClient.getInetAddress())
        + "," + sClient.getPort() + "]");
    } catch (Exception e) {
        erreur("identification liaison ("+e+")",5);
    }

     // 创建用于读取键盘输入行的数据流
    BufferedReader IN=null;
    try{
        IN=new BufferedReader(new InputStreamReader(System.in));
    } catch (Exception e){
        erreur("Création du flux d'entrée clavier ("+e+")",6);
    }
     // 创建与客户端套接字关联的输入流
    BufferedReader in=null;
    try{
        in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
    } catch (Exception e){
        erreur("Création du flux d'entrée de la socket client("+e+")",7);
    }
     // 创建与客户端套接字关联的输出流
    PrintWriter out=null;
    try{
        out=new PrintWriter(sClient.getOutputStream(),true);
    } catch (Exception e){
        erreur("Création du flux de sortie de la socket ("+e+")",8);
    }

     // 请求-响应循环
    boolean serviceFini=false;
    String demande=null;
    String reponse=null;

    // 连接后立即读取服务器发送的消息 
    try{
        reponse=in.readLine();
    } catch (IOException e){
            erreur("Lecture réponse ("+e+")",4);
    }        

     // 显示响应
    System.out.println("Serveur : " +reponse);

    while (! serviceFini){
         // 读取键盘输入的一行内容
        System.out.print("Client : ");
        try{
            demande=IN.readLine();
        } catch (Exception e){
            erreur("Lecture ligne ("+e+")",9);
        }
         // 向网络发送请求
        try{
            out.println(demande);
        } catch (Exception e){
            erreur("Envoi demande ("+e+")",10);
        }
         // 等待/读取响应
        try{
            reponse=in.readLine();
        } catch (IOException e){
                erreur("Lecture réponse ("+e+")",4);
        }
         // 显示响应
        System.out.println("Serveur : " +reponse);
         // 结束了吗?
        if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
    }
     // 已经结束
    try{
        sClient.close();
    } catch(Exception e){
        erreur("Fermeture socket ("+e+")",11);
    }
}// 主

// 显示错误
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // 识别
    private static String identifie(InetAddress Host){
        // 主机标识
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// 类结束

该客户端的结构符合 tcp 客户端的总体架构。在此,我们逐一处理了各种可能的异常,这使得程序变得臃肿。以下是测试该客户端时获得的结果:

Client : Client [127.0.0.1,tahe,1045] connecté au serveur [127.0.0.1,localhost,187]
Serveur : Client [127.0.0.1,localhost,1045] connectÚ au serveur [127.0.0.1,tahe,187]
Client : 123
Serveur : [123]
Client : abcd
Serveur : [abcd]
Client : je suis là
Serveur : [je suis là]
Client : fin
Serveur : [fin]

Client 开头的行是客户端发送的,以 Serveur 开头的行是服务器返回的。

8.4.3. 一个通用的 TCP 客户端

许多互联网早期创建的服务都遵循前文探讨的回显服务器模式:客户端与服务器之间的交互通过交换文本行来实现。我们将编写一个通用 TCP 客户端,其启动方式如下:java cltTCPgenerique 服务器端口

该客户端 TCP 将连接到服务器 serveur 的端口 port。连接成功后,它将创建两个线程:

  1. 一个线程负责读取键盘输入的命令并将其发送至服务器
  2. 另一个线程负责读取服务器的响应并将其显示在屏幕上

既然在之前的应用程序中并未出现这种需求,为何现在需要两个线程?在之前的应用程序中,对话协议是已知的:客户端发送一行,服务器回复一行。每个服务都有其特定的协议,还可能出现以下情况:

  • 客户端必须发送多行文本才能获得响应
  • 服务器的响应可能包含多行文本

因此,仅包含“向服务器发送单行文本——接收服务器发送的单行文本”这一循环的方案并不总是适用。因此,我们将创建两个独立的循环:

  • 一个用于读取键盘输入命令并发送至服务器的循环。用户将通过关键字 fin 来标记命令结束。
  • 一个用于接收并显示服务器响应的循环。这是一个无限循环,仅在服务器关闭网络连接或用户通过键盘输入命令 fin 时才会终止。

为了使这两个循环相互独立,我们需要两个独立的线程。 下面展示一个执行示例:我们的通用 TCP 客户端连接到 SMTP 服务(SendMail 传输协议)。该服务负责将电子邮件转发给收件人。它运行在 25 端口上,采用基于文本行交换的交互协议。


Dos>java clientTCPgenerique istia.univ-angers.fr 25
Commandes :
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
help
<-- 502 5.3.0 Sendmail 8.11.6 -- HELP not implemented
mail from: machin@univ-angers.fr
<-- 250 2.1.0 machin@univ-angers.fr... Sender ok
rcpt to: serge.tahe@istia.univ-angers.fr
<-- 250 2.1.5 serge.tahe@istia.univ-angers.fr... Recipient ok
data
<-- 354 Enter mail, end with "." on a line by itself
Subject: test

ligne1
ligne2
ligne3
.
<-- 250 2.0.0 g4D6bks25951 Message accepted for delivery
quit
<-- 221 2.0.0 istia.univ-angers.fr closing connection
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

让我们来分析一下这些客户端与服务端之间的交互:

  • 当客户端连接到 SMTP 服务时,该服务会发送一条欢迎消息:
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
  • 某些服务提供 help 命令,用于说明该服务支持的可用命令。但此处并非如此。示例中使用的 SMTP 命令如下:
    • mail from: expéditeur,用于指定消息发件人的电子邮箱地址
    • rcpt to: destinataire,用于指定邮件收件人的电子邮箱地址。如果有多个收件人,则需针对每位收件人重复发送 rcpt to: 命令。
    • data 用于通知服务器 SMTP 即将发送邮件。如服务器响应所示,邮件内容是一系列行,最后以仅包含一个点字符的行结尾。邮件的标题行通常通过空行与正文分隔。在本例中,我们使用 Subject: 关键字设置了主题
  • 消息发送完成后,可通过 quit 命令告知服务器操作已完成。此时服务器将关闭网络连接。读取线程可检测到此事件并停止运行。
  • 用户随后在键盘上输入“fin”以停止读取键盘输入命令的线程。

若检查收到的邮件,我们看到如下内容(Outlook):

Image

需要注意的是,SMTP服务无法判断发件人是否有效。因此,绝不能信任邮件中的from字段。 此处的发件人 machin@univ-angers.fr 并不存在。

这个通用 TCP 客户端可帮助我们发现互联网服务的通信协议,并据此为这些服务的客户端构建专用类。让我们来探索服务 POP(邮局协议)的通信协议,该协议允许检索存储在服务器上的邮件。它运行在 110 端口上。


Dos> java clientTCPgenerique istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<--     by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<--     Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<--     by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
<--     Mon, 13 May 2002 08:58:28 +0200 (CEST)
...
<-- ------------------------------------------------------------------------
<-- NOC-RENATER2                  Tl.  : 0800 77 47 95
<-- Fax : (+33) 01 40 78 64 00 ,  Email : noc-r2@cssi.renater.fr
<-- ------------------------------------------------------------------------
<--
<-- .
quit
<-- +OK Pop server at istia.univ-angers.fr signing off.
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

主要命令如下:

  • user login,其中需输入在存储我们邮件的机器上的登录名
  • 输入密码 password,其中需提供与前一个登录名关联的密码
  • list,以编号和字节大小格式获取消息列表
  • retr i,用于阅读第 i 号消息
  • quit,用于结束对话。

现在让我们来了解客户端与通常运行在 80 端口的 Web 服务器之间的交互协议:


Dos> java clientTCPgenerique istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>
<--
<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise  jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<--
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Web客户端按照以下模式向服务器发送请求:

commande1
commande2
...
commanden
[ligne vide]

只有在收到空行后,Web 服务器才会做出响应。在示例中,我们只使用了一个命令:

GET /index.html HTTP/1.0

该命令向服务器请求URL /index.html,并表明其使用的是HTTP协议的1.0版本。该协议的最新版本为1.1。 该示例显示,服务器响应时返回了文件 index.html 的内容,随后关闭了连接,因为可以看到响应读取线程已结束。在发送文件 index.html 的内容之前,Web 服务器发送了一系列以空行结尾的头部信息:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

<html> 这一行是文件 /index.html 的第一行。上述内容被称为 HTTP 头(HyperText 传输协议)。 我们在此不详细说明这些头部信息,但需注意我们的通用客户端提供了访问这些头部信息的途径,这有助于理解它们。例如第一行:

<-- HTTP/1.1 200 OK

表明所联系的 Web 服务器支持 HTTP/1.1 协议,并且已成功找到请求的文件(200 OK),其中 200 是 HTTP 响应代码。以下几行

<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html

告知客户端,它将接收 11251 个字节,这些数据代表文本 HTML(HyperText 标记语言),并且在发送完成后,连接将被关闭。

因此,这是一个非常实用的 TCP 客户端。虽然它的功能可能不如我们之前使用的 telnet 程序,但亲自编写它还是很有趣的。通用 TCP 客户端的程序如下:

// 导入的软件包
import java.io.*;
import java.net.*;

public class clientTCPgenerique{

    // 接收服务特征作为参数,格式为
     // 服务器端口
     // 连接到服务
     // 创建一个线程以读取键盘输入的命令
     // 将这些命令发送至服务器
     // 创建一个线程以读取服务器的响应
     // 将响应显示在屏幕上
     // 整个过程以键盘输入的“fin”命令结束

   // 实例变量
  private static Socket client;

    public static void main(String[] args){

         // 语法
        final String syntaxe="pg serveur port";

         // 参数个数
        if(args.length != 2)
            erreur(syntaxe,1);

         // 记录服务器名称
        String serveur=args[0];

         // 端口必须是大于0的整数
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[1]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

        client=null;
         // 可能会出现问题
        try{
             // 连接到服务
            client=new Socket(serveur,port);
        }catch(Exception ex){
             // 错误
            erreur("Impossible de se connecter au service ("+ serveur
                +","+port+"), erreur : "+ex.getMessage(),3);
             // 结束
            return;
        }//捕获

         // 创建读写线程
    new ClientSend(client).start();
    new ClientReceive(client).start();

        // 主线程结束
        return;
    }// 主线程

     // 显示错误
    public static void erreur(String msg, int exitCode){
         // 显示错误
        System.err.println(msg);
         // 因错误停止
        System.exit(exitCode);
    }//错误
}//类  

class ClientSend extends Thread {
    // 负责读取键盘输入命令的类
     // 并通过作为参数传递的 TCP 客户端将其发送至服务器

    private Socket client;    // 该 TCP 客户端

     // 构造函数
    public ClientSend(Socket client){
         // 注:此处为 TCP 客户端
        this.client=client;
    }//构造函数

     // 线程的 Run 方法
    public void run(){

        // 本地数据
        PrintWriter OUT=null;            // 网络写入流
    BufferedReader IN=null;        // 键盘流
        String commande=null;            // 键盘读取的命令

         // 错误处理
        try{
             // 创建网络写入流
            OUT=new PrintWriter(client.getOutputStream(),true);
      // 创建键盘输入流
      IN=new BufferedReader(new InputStreamReader(System.in));
            // 命令输入与发送循环
            System.out.println("Commandes : ");
            while(true){
                 // 读取键盘输入的命令
                commande=IN.readLine().trim();
                // 结束了吗?
                if (commande.toLowerCase().equals("fin")) break;
                // 将命令发送至服务器
                OUT.println(commande);
                 // 下一条命令
            }//while
        }catch(Exception ex){
             // 错误
            System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // 结束 - 关闭数据流
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // 报告线程结束
        System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
    }//运行
}//类

class ClientReceive extends Thread{
    // 负责读取发往 
     // 作为参数传递的 TCP 客户端

    private Socket client;    // TCP 客户端

     // 构造函数
    public ClientReceive(Socket client){
         // 记录 TCP 客户端
        this.client=client;
    }//构造函数

     // 线程的 Run 方法
    public void run(){

        // 本地数据
        BufferedReader IN=null;        // 网络读取流
        String réponse=null;        // 服务器响应

         // 错误处理
        try{
             // 创建网络读取流
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // 读取 IN 流中文本行的循环
            while(true){
                 // 读取网络流
                réponse=IN.readLine();
                 // 数据流已关闭?
                if(réponse==null) break;
                // 显示
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // 错误
            System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // 结束 - 关闭数据流
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // 报告线程结束
        System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
    }//运行
}//类

8.4.4. 一个通用的TCP服务器

现在我们关注一个

  • ,它将客户端发送的命令显示在屏幕上
  • 并将用户在键盘上输入的文本行作为响应发送给客户端。因此,用户本身充当了服务器。

程序通过以下命令启动:java serveurTCPgenerique portEcoute,其中 portEcoute 是客户端需要连接的端口。对客户端的服务将由两个线程负责:

  • 一个线程专门用于读取客户端发送的文本行
  • 另一个线程专门用于读取用户通过键盘输入的响应。该线程将通过 fin 命令通知服务器,表示其已结束与客户端的连接。

服务器为每个客户端创建两个线程。如果有 n 个客户端,则将有 2n 个线程同时处于活动状态。除非用户通过键盘输入 Ctrl-C,否则服务器永远不会停止运行。让我们来看几个示例。

服务器在100端口上运行,我们使用通用客户端与其通信。客户端窗口如下:

E:\data\serge\MSNET\c#\网络\通用 TCP 客户端> java clientTCPgenerique localhost 100
Commandes :
commande 1 du client 1
<-- réponse 1 au client 1
commande 2 du client 1
<-- réponse 2 au client 1
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

以 <-- 开头的行是服务器发送给客户端的,其余的是客户端发送给服务器的。服务器窗口如下:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]

以 <-- 开头的行是从客户端发送到服务器的。以 N: 开头的行是从服务器发送给第 N 号客户端的。上文中的服务器仍然处于活动状态,而客户端 1 已结束。现在为同一服务器启动第二个客户端:


Dos> java clientTCPgenerique localhost 100
Commandes :
commande 3 du client 2
<-- réponse 3 au client 2
fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du thread de lecture des réponses du serveur]
[fin du thread d'envoi des commandes au serveur]

此时服务器窗口如下所示:


Dos> java serveurTCPgenerique 100
Serveur générique lancé sur le port 100
Thread de lecture des réponses du serveur au client 1 lancé
1 : Thread de lecture des demandes du client 1 lancé
<-- commande 1 du client 1
réponse 1 au client 1
1 : <-- commande 2 du client 1
réponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des réponses du serveur au client 1]
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- commande 3 du client 2
réponse 3 au client 2
2 : [fin du Thread de lecture des demandes du client 2]
fin
[fin du Thread de lecture des réponses du serveur au client 2]
^C

现在,让我们通过在 88 端口上启动我们的通用服务器来模拟一个 Web 服务器:


Dos> java serveurTCPgenerique 88
Serveur générique lancé sur le port 88

现在打开浏览器,访问 http://localhost:88/exemple.html。 浏览器将连接到 localhost 机器的 88 端口,然后请求 /exemple.html 页面:

Image

现在让我们看看服务器的窗口:

Dos>java serveurTCPgenerique 88
Serveur générique lancé sur le port 88
Thread de lecture des réponses du serveur au client 2 lancé
2 : Thread de lecture des demandes du client 2 lancé
<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

由此,我们发现了浏览器发送的 HTTP 头部信息。这让我们能够逐步了解 HTTP 协议。在之前的示例中,我们创建了一个仅发送 GET 这一条命令的 Web 客户端。 当时仅此就已足够。这里可以看到,浏览器向服务器发送了其他信息。这些信息的目的是告知服务器,它面对的是何种类型的客户端。我们还注意到,HTTP 头部以空行结尾。

现在我们来为客户端编写一个响应。此时,键盘前的用户才是真正的服务器,可以手动编写响应。让我们回顾一下前一个示例中Web服务器生成的响应:

<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix)  (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<--
<-- <html>

让我们尝试给出一个类似的答案:

...
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--
2 : HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>
2 : fin
L'erreur suivante s'est produite : Impossible de lire les données de la connexion de transport.
[fin du Thread de lecture des demandes du client 2]
[fin du Thread de lecture des réponses du serveur au client 2]

2: 开头的行是从服务器发送给第 2 号客户端的。命令 fin 结束了服务器与客户端的连接。我们在响应中仅列出了以下 HTTP 头部信息:

HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :

我们不提供即将发送的文件(Content-Length)的大小,而是仅告知在发送该文件后将关闭连接(Connection: close)。这对浏览器来说已经足够。 当浏览器检测到连接已关闭时,便会知道服务器的响应已完成,并显示收到的页面 HTML。该页面内容如下:

2 : <html>
2 :   <head><title>Serveur generique</title></head>
2 :   <body>
2 :     <center>
2 :       <h2>Reponse du serveur generique</h2>
2 :     </center>
2 :    </body>
2 : </html>

随后,用户通过输入命令 fin 关闭与客户端的连接。此时,浏览器会知道服务器的响应已经结束,并可以显示该响应:

Image

如果在此处输入 View/Source 以查看浏览器接收到的内容,将得到:

Image

即与从通用服务器发送的内容完全一致。

通用 TCP 服务器的代码如下:

// 包
import java.io.*;
import java.net.*;

public class serveurTCPgenerique{

    // 主程序
    public static void main (String[] args){

    // 监听端口以接收客户端请求
     // 创建一个线程来读取客户端请求
     // 这些请求将显示在屏幕上
     // 创建一个线程以读取键盘输入的命令
     // 这些内容将作为回复发送给客户
     // 整个过程以键盘输入的结束命令结束

    final String syntaxe="Syntaxe : pg port";
   // 实例变量
         // 是否有参数
     if(args.length != 1)
        erreur(syntaxe,1);

         // 端口必须是大于0的整数
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

     // 创建监听服务
    ServerSocket ecoute=null;
    int nbClients=0;    // 处理的客户端数量
        try{
             // 创建服务
            ecoute=new ServerSocket(port);
             // 跟踪
            System.out.println("Serveur générique lancé sur le port " + port);

             // 客户端服务循环
            Socket client=null;
            while (true){ // 无限循环 - 将通过 Ctrl-C 终止
                 // 等待客户端
                client=ecoute.accept();

                 // 该服务由独立线程处理
                nbClients++;

                 // 创建读写线程
        new ServeurSend(client,nbClients).start();
        new ServeurReceive(client,nbClients).start();

                // 返回监听请求
            }// while循环结束
        }catch(Exception ex){
             // 报告错误
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }// main 结束

     // 显示错误
    public static void erreur(String msg, int exitCode){
         // 显示错误
        System.err.println(msg);
         // 带错误停止
        System.exit(exitCode);
    }//错误
}//类

class ServeurSend extends Thread{
    // 负责读取键盘输入的类
     // 并通过传递给构造函数的 TCP 客户端将其发送给客户端

    Socket client;    // TCP 客户端
    int numClient;        // 客户端编号

     // 构造函数
    public ServeurSend(Socket client, int numClient){
         // 这里记录的是 TCP 客户端
        this.client=client;
         // 及其编号
        this.numClient=numClient;
    }//构造函数

     // 线程的 Run 方法
    public void run(){

        // 本地数据
        PrintWriter OUT=null;        // 网络写入流
        String réponse=null;        // 键盘读取响应
    BufferedReader IN=null;    // 键盘流

        // 跟踪
        System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
         // 错误处理
        try{
             // 创建网络写入流
            OUT=new PrintWriter(client.getOutputStream(),true);
      // 创建键盘流
      IN=new BufferedReader(new InputStreamReader(System.in));
            // 命令输入-发送循环
            while(true){
                 // 用户身份验证
                System.out.print("--> " + numClient + " : ");
                 // 读取键盘输入的响应
                réponse=IN.readLine().trim();
                // 结束了吗?
                if (réponse.toLowerCase().equals("fin")) break;
                // 将响应发送至服务器
                OUT.println(réponse);
                 // 下一个响应
            }//while
        }catch(Exception ex){
             // 错误
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // 结束 - 关闭数据流
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // 报告线程结束
        System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
    }//运行
}//类

class ServeurReceive extends Thread{
    // 负责读取发送到服务器的文本行的类 
     // 通过传递给构造函数的 TCP 客户端

    Socket client;    // TCP客户端
    int numClient;        // 客户端编号

     // 构造函数
    public ServeurReceive(Socket client, int numClient){
         // 记录 TCP 客户端
        this.client=client;
         // 及其编号
        this.numClient=numClient;
    }//构造函数

     // 线程的 Run 方法
    public void run(){

        // 本地数据
        BufferedReader IN=null;        // 网络读取流
        String réponse=null;        // 服务器响应

         // 跟踪
        System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
         // 错误处理
        try{
             // 创建网络读取流
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // 从 IN 流中读取文本行的循环
            while(true){
                 // 读取网络流
                réponse=IN.readLine();
                 // 数据流已关闭?
                if(réponse==null) break;
                // 显示
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // 错误
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // 结束 - 关闭数据流
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // 报告线程结束
        System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
    }//运行
}//类

8.4.5. 一个Web客户端

我们在前面的示例中看到,浏览器发送了一些 HTTP 头部:

<-- GET /exemple.html HTTP/1.1
<-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, */*
<-- Accept-Language: fr
<-- Accept-Encoding: gzip, deflate
<-- User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705; .NET CLR 1.0.2
914)
<-- Host: localhost:88
<-- Connection: Keep-Alive
<--

我们将编写一个Web客户端,向其传递参数URL,并将其内容显示在屏幕上。 我们假设为 URL 请求的 Web 服务器支持 HTTP 1.1 协议。在上述标头中,我们仅使用以下内容:

<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
  • 第一个标头指定我们要访问的页面
  • 第二个标头指定我们查询的服务器
  • 第三个标头表示希望服务器在响应后关闭连接。

如果将上文中的 GET 替换为 HEAD,服务器将只发送 HTTP 头部,而不会发送 HTML 页面。

我们的Web客户端将按以下方式调用:java clientweb URL cmd,其中 URL 是目标 URL,而 cmd 则是 GET 或 HEAD 其中一个关键字,用于指定是否仅获取头部信息(HEAD),还是同时获取页面内容 (GET)。让我们来看一个示例。我们在同一台机器上启动 IIS 服务器,然后启动 Web 客户端:

dos>java clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private

答案

HTTP/1.1 302 Object moved

表示所请求的页面已更改位置(即从URL)。新的URL由Location:标头给出

Location: /IISSamples/Default/welcome.htm

如果我们在调用 Web 客户端时使用 GET 代替 HEAD:

dos>java clientweb http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:33:36 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=IMFNCCMDAKPNNGMGMFIHENFE; path=/
Cache-control: private

<head><title>L'objet a changé d'emplacement</title></head>
<body><h1>L'objet a changé d'emplacement</h1>Cet objet peut être trouvé <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

我们得到了与 HEAD 相同的结果,此外还获得了页面主体 HTML。程序如下:

// 导入的软件包
import java.io.*;
import java.net.*;

public class clientweb{

    // 请求 URL
     // 在屏幕上显示其内容

    public static void main(String[] args){
        // 语法
        final String syntaxe="pg URI GET/HEAD";

        // 参数个数
        if(args.length != 2)
            erreur(syntaxe,1);

         // 请注意所要求的URI
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // 验证 URI 的有效性
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI 错误
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//捕获
         // 验证订单
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // 订单错误
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // 从URL中提取有用信息
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // 可以处理
        Socket  client=null;                        // 客户
        BufferedReader IN=null;                    // 客户端的读取流
        PrintWriter OUT=null;                        // 客户端的写入流
        String réponse=null;                        // 服务器响应
        try{
             // 连接到服务器
            client=new Socket(host,port);

            // 创建客户端的输入/输出流 TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // 请求 URL - 发送头信息 HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // 读取响应
            while((réponse=IN.readLine())!=null){
                 // 处理响应
                System.out.println(réponse);
            }//while
             // 处理完毕
            client.close();
        } catch(Exception e){
            // 处理异常
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // 显示错误
    public static void erreur(String msg, int exitCode){
         // 显示错误
        System.err.println(msg);
         // 带错误终止
        System.exit(exitCode);
    }//错误
}//类别

该程序的唯一新功能是使用了类 URL。 该程序接收一个 URL(统一资源定位符)或 URI(统一资源标识符),其格式为 http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... URL类允许我们将URL字符串分解为各个组成部分。通过将作为参数接收的URLstring字符串作为基础,构建出一个URL对象:

         // 验证URL的有效性
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI 错误
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//捕获

如果作为参数接收的字符串 URL 不是有效的 URL(缺少协议、服务器等),则会抛出异常。这使我们能够验证接收到的参数是否有效。 一旦构建了 URL 对象,即可访问其各个元素。因此,如果前文代码中的 url 对象是基于字符串

http://服务器:端口/cheminPageHTML?param1=val1;param2=val2;... 

构建的,则将得到:

url.getHost()=serveur

url.getPort()=port,若未指定端口则返回 -1

url.getPath()=cheminPageHTML,若无路径则为空字符串

url.getQuery()=param1=val1;param2=val2;...,若无请求则返回 null

uri.getProtocol()=http

8.4.6. 支持重定向的 Web 客户端

前一个 Web 客户端无法处理其请求的 URL 可能发生的重定向。下一个客户端可以处理。

  1. 它读取服务器发送的 HTTP 响应头的第一行,以检查是否包含表示重定向的字符串 302 Object moved
  2. 它读取后续的头部信息。如果存在重定向,它会查找 Location: url 这一行,该行提供了所请求页面的新 URL,并记录下该 URL。
  3. 它显示服务器响应的其余部分。如果存在重定向,则使用新的 URL 重复步骤 1 至 3。该程序不接受超过一次的重定向。此限制由一个常量定义,该常量可以修改。

以下是一个示例:

Dos>java clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private

<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>

<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->

HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781

<html>

<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
</html>

日程安排如下:

// 导入的软件包
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientweb2{

    // 请求 URL
     // 在屏幕上显示其内容

    public static void main(String[] args){
        // 语法
        final String syntaxe="pg URL GET/HEAD";

        // 参数个数
        if(args.length != 2)
            erreur(syntaxe,1);

         // 记录所请求的URI
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // 验证 URI 的有效性
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI 错误
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//捕获
         // 订单验证
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // 订单错误
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // 可以处理
        Socket  client=null;                        // 客户
        BufferedReader IN=null;                    // 客户端读取流
        PrintWriter OUT=null;                        // 客户端写入流
        String réponse=null;                        // 服务器响应
        final int nbRedirsMax=1;                // 最多只允许一次重定向
        int nbRedirs=0;                                    // 当前重定向次数
        String premièreLigne;                        // 响应的第一行
        boolean redir=false;                        // 指示是否存在重定向
        String locationString="";                // 可能的重定向的字符串 URL

         // 用于查找重定向字符串 URL 的正则表达式
        Pattern location=Pattern.compile("^Location: (.+?)$");

         // 错误处理
        try{
             // 如果存在重定向,可能需要查询多个 URL
            while(nbRedirs<=nbRedirsMax){

                // 从URL中提取有用信息
            String protocol=url.getProtocol();
            String path=url.getPath();
            if(path.equals("")) path="/";
            String query=url.getQuery();
            if(query!=null) query="?"+query; else query="";
            String host=url.getHost();
            int port=url.getPort();
            if(port==-1) port=url.getDefaultPort();

                 // 连接到服务器
                client=new Socket(host,port);

                // 创建客户端的输入-输出流 TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // 请求 URL - 发送头信息 HTTP
                OUT.println(commande + " " + path + query + " HTTP/1.1");    
                OUT.println("Host: " + host + ":" + port);
                OUT.println("Connection: close");
                OUT.println();

                 // 读取响应的第一行
                premièreLigne=IN.readLine();
                 // 屏幕回显
                System.out.println(premièreLigne);

                // 重定向?
        if(premièreLigne.endsWith("302 Object moved")){     
                    // 存在重定向
                    redir=true;
                    nbRedirs++;
                }//if

                // 处理后续的 HTTP 头部,直到找到表示头部结束的空行
                boolean locationFound=false;
                while(!(réponse=IN.readLine()).equals("")){
                    // 显示响应
                    System.out.println(réponse);
                     // 如果存在重定向,则查找 Location 标头
                    if(redir && ! locationFound){
                        // 将该行与关系表达式 location 进行比较
                        Matcher résultat=location.matcher(réponse);
                        if(résultat.find()){
                            // 若找到,则记录重定向的URL
                            locationString=résultat.group(1);             
                             // 记录已找到
                            locationFound=true;
                        }//if
                    }//如果
                    // 下一个标头
                }//while

                 // 响应的后续行
                System.out.println(réponse);
                while((réponse=IN.readLine())!=null){
                     // 显示响应
                    System.out.println(réponse);
                }//while
                 // 关闭连接
                client.close();
                 // 是否已完成?
                if ( ! locationFound || nbRedirs>nbRedirsMax)
                    break;
                 // 需要进行重定向 - 构建新的 URL
                URLString=protocol +"://"+host+":"+port+locationString;
                url=new URL(URLString);
                // 跟踪
                System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
            }//同时
        } catch(Exception e){
            // 处理异常
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // 显示错误
    public static void erreur(String msg, int exitCode){
         // 显示错误
        System.err.println(msg);
         // 带错误终止
        System.exit(exitCode);
    }//错误
}//类别

8.4.7. 税款计算服务器

我们重新探讨练习题 IMPOTS,该题此前已以多种形式进行过讲解。回顾一下最新版本:

已创建了一个名为“taxes”的基类。其属性包含三个数字数组:

public class impots{

  // 计算税款所需的数据
   // 来自外部来源

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

   // 制造商字段为空
  protected impots(){}

   // 构造函数
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{

impots 类有两个构造函数:

  • 一个构造函数,用于传入计算税款所需的三个数据数组
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
  • 一个无参数的构造函数,仅供子类使用
  protected impots(){}

基于该类派生出了 impotsJDBC,该类可根据数据库内容填充三个数组 limitescoeffRcoeffN

public class impotsJDBC extends impots{
  // 添加一个用于构建的构造函数
   // 根据表
   // 税费表
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS:数据库名称
     // userIMPOTS, mdpIMPOTS:数据库登录名/密码

此前编写了一个图形应用程序。该应用程序使用了一个 impotsJDBC 类的对象。应用程序和该对象位于同一台机器上。我们计划将测试程序和 impotsJDBC 对象部署在不同的机器上。 我们将构建一个客户端-服务器应用程序,其中远程对象 impotsJDBC 将作为服务器。新类名为 ServeurImpots,并继承自类 impotsJDBC:

// 已导入的软件包
import java.net.*;
import java.io.*;
import java.sql.*;

public class ServeurImpots extends impotsJDBC {

    // 属性
    int portEcoute;                // 监听客户端请求的端口
    boolean actif;                // 服务器状态

     // 构造函数
    public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
      throws IOException, SQLException, ClassNotFoundException {
       // 父类
        super(DSNimpots, USERimpots, MDPimpots);
         // 记录监听端口
        this.portEcoute=portEcoute;
         // 目前处于非活动状态
        actif=false;
         // 创建并启动一个线程,用于读取键盘输入的命令
         // 服务器将根据这些命令进行管理
        Thread admin=new Thread(){
        public void run(){
          try{
            admin();
        }catch (Exception ignored){}
      }
    };
    admin.start();
    }//ServeurImpots

构造函数中唯一的新参数是监听客户端请求的端口。其他参数直接传递给基类 impotsJDBC。税务服务器通过键盘输入的命令进行控制。因此,我们需要创建一个线程来读取这些命令。 可能有两种命令:start用于启动服务,stop用于永久停止服务。管理这些命令的admin方法如下:

    public void admin() throws IOException{
        // 读取通过键盘输入的服务器管理命令
         // 在无限循环中
        String commande=null;
    BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
        while(true){
             // 提示
            System.out.print("Serveur d'impôts>");
             // 读取命令
            commande=IN.readLine().trim().toLowerCase();
             // 执行命令
            if(commande.equals("start")){
                // 是否处于活动状态?
                if(actif){
                     //错误
                    System.out.println("Le serveur est déjà actif");
                     // 继续
                    continue;
                }//if
                 // 创建并启动监听服务
                Thread ecoute=new Thread(){
                public void run(){
                  ecoute();
              }
            };
            ecoute.start();
            }//if
            else if(commande.equals("stop")){
                // 所有执行线程结束
                System.exit(0);
            }//如果
            else {
                // 错误
                System.out.println("Commande incorrecte. Utilisez (start,stop)");
            }//if
        }//while
    }//管理员

如果键盘输入的命令是 start,则会启动一个监听客户端请求的线程。如果输入的命令是 stop,则所有线程都会停止。监听线程执行方法 ecoute

    public void ecoute(){
         // 监听客户端请求的线程
         // 创建监听服务
        ServerSocket ecoute=null;
        try{
            // 创建服务
            ecoute=new ServerSocket(portEcoute);
             // 跟踪
            System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);

             // 服务循环
            Socket liaisonClient=null;
            while (true){ // 无限循环
                 // 等待客户端
                liaisonClient=ecoute.accept();

                 // 服务由另一个任务提供
                new traiteClientImpots(liaisonClient,this).start();

                 // 返回请求监听状态
            }// while循环结束
        }catch(Exception ex){
             // 报告错误
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }//监听线程

这里有一个经典的TCP服务器,监听端口portEcoute。客户端的请求由线程traiteCientImpots中的方法run处理,该方法的构造函数接收两个参数:

  1. Socket 对象 liaisonClient,它将用于连接客户端
  2. 对象 impotsJDBC this,该对象将提供对计算税款的方法 this.calculer 的访问权限。
// -------------------------------------------------------
// 为税务服务器的客户端提供服务

class traiteClientImpots extends Thread{

    private Socket liaisonClient;            // 与客户端建立连接
    private BufferedReader IN;                // 输入流
    private PrintWriter OUT;                    // 输出流
    private impotsJDBC objImpots;            // 税款对象

         // 构造函数
    public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
        this.liaisonClient=liaisonClient;
        this.objImpots=objImpots;
    }//构造函数

方法 run 处理客户请求。这些是文本行,可能有两种形式:

  1. 已婚计算(是/否) nbEnfants salaireAnnuel
  2. fincalculs

形式 1 用于计算税款,形式 2 用于结束客户端与服务器的连接。

     // 运行方法
    public void run(){
         // 向客户端提供服务
        try{
             // 输入流
            IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
             // 输出流
            OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
             // 向客户端发送欢迎消息
            OUT.println("Bienvenue sur le serveur d'impôts");

             // 请求/写入响应读取循环
            String demande=null;
            String[] champs=null;    // 请求的组成部分
            String commande=null;    // 客户端命令:计算或结束计算
            while ((demande=IN.readLine())!=null){
                 // 将请求拆分为字段
                champs=demande.trim().toLowerCase().split("\\s+");
                 // 两个请求被接受:计算和计算结束
                commande=champs[0];
                if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
                     // 客户错误
                    OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
                     // 下一条订单
                    continue;
                }//if
                if(commande.equals("calcul")) calculerImpôt(champs);
                if(commande.equals("fincalculs")){
                     // 向客户端发送告别消息
                    OUT.println("Au revoir...");
                     // 释放资源
                    try{ OUT.close();IN.close();liaisonClient.close();}
                    catch(Exception ex){}
                     // 结束
                    return;
                }//if
                 //下一个请求
            }//while
        }catch (Exception e){
            erreur("L'erreur suivante s'est produite ("+e+")",2);
        }// 结束 try
    }// 运行结束

税款计算通过方法 calculerImpôt 进行,该方法接收客户请求字段表作为参数。系统将验证请求的有效性,并计算税款后返回给客户。

     // 计算税款
    public void calculerImpôt(String[] champs){
         // 处理申请:已婚计算 nbEnfants salaireAnnuel
         // 拆分为字段表中的字段

        String marié=null;
        int nbEnfants=0;
        int salaireAnnuel=0;

         // 参数有效性
        try{
             // 至少需要 4 个字段
            if(champs.length!=4) throw new Exception();
            // 已婚
            marié=champs[1];
            if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
            // 子女
            nbEnfants=Integer.parseInt(champs[2]);
             // 工资
            salaireAnnuel=Integer.parseInt(champs[3]);
        }catch (Exception ignored){
            // 格式错误
            OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
             // 完成
            return;
        }//if
         // 可以计算税款
        long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
         // 向客户发送回复
        OUT.println(""+impot);
    }//计算

一个测试程序可以如下所示:

// 调用: serveurImpots 端口 dsnImpots userImpots mdpImpots

import java.io.*;

public class testServeurImpots{
    public static final String syntaxe="Syntaxe : pg port dsnImpots userImpots mdpImpots";

    // 主程序
    public static void main (String[] args){

        // 需要 4 个参数
        if(args.length != 4)
            erreur(syntaxe,1);

         // 端口必须是大于0的整数
        int port=0;
        boolean erreurPort=false;
        Exception E=null;
        try{
            port=Integer.parseInt(args[0]);
        }catch(Exception e){
            E=e;
            erreurPort=true;
        }
        erreurPort=erreurPort || port <=0;
        if(erreurPort)
            erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);

         // 创建税务服务器
        try{
            new ServeurImpots(port,args[1],args[2],args[3]);
        }catch(Exception ex){
             //错误
            System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
        }//捕获
    }//主函数

     // 显示错误
    public static void erreur(String msg, int exitCode){
         // 显示错误
        System.err.println(msg);
         // 带错误停止
        System.exit(exitCode);
    }//错误
}// 类结束

我们将构建 ServeurImpots 对象所需的数据传递给测试程序,程序据此创建该对象。

让我们尝试首次执行:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

该命令

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots

会创建一个名为 ServeurImpots 的对象,该对象此时尚未监听客户端的请求。只有通过键盘输入 start 命令,才会启动监听。stop 命令则会停止服务器。现在我们来使用一个客户端。我们将使用之前创建的通用客户端。服务器已启动:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124

通用客户端在另一个 DOS 窗口中启动:

Dos>java clientTCPgenerique localhost 124
Commandes :
<-- Bienvenue sur le serveur d'impôts

可以看到客户端已成功接收来自服务器的欢迎信息。我们发送其他命令:

x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<--  syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

返回服务器窗口将其停止:

dos>java testServeurImpots 124 mysql-dbimpots admimpots mdpimpots
Serveur d'impôts>start
Serveur d'impôts>Serveur d'écho lancé sur le port 124
stop

8.5. 练习

8.5.1. 练习 1 - TCP 通用图形客户端

8.5.1.1. 应用程序简介

我们计划创建一个能够通过互联网与主要服务进行交互的程序。我们将它称为通用TCP客户端。一旦理解了这个应用程序,就会发现所有的TCP客户端都大同小异。程序窗口如下所示:

Image

各控件的含义如下:

编号
名称
类型
作用
1
TxtRemoteHost
JTextField
提供所需服务的机器名称
2
TxtPort
JTextField
所需服务的端口
3
TxtSend
JTextField
客户端将发送至服务器的消息正文
4
OptRCLF
OptLF
JCheckBox
用于指定客户端/服务器对话中行结束符的按钮
RCLF:回车符 (#13) + 换行符 (#10)
LF:换行符 (#10)
5
LstSuivi
JList
显示客户端与服务器之间通信状态的消息
6
LstDialogue
JList
显示客户端(->)与服务器(<-)之间交换的消息
7
CmdAnnuler
JButton
隐藏 - 位于对话列表下方 - 当连接正在进行时显示,若服务器无响应可点击此处中断连接

可用的菜单选项如下:

选项
子选项
角色
连接
连接
将客户端连接到服务器
 
断开连接
关闭连接
 
退出
结束程序
消息
发送
将控件 TxtSend 的消息发送至服务器
 
RazSuivi
清除列表 LstSuivi
 
RazDialogue
清除列表 LstDialogue
作者
 
显示版权框

8.5.1.2. FONCTIONNEMENT DE APPLICATION

应用程序初始化

当应用程序的主页面加载时,将执行以下操作:

  • 页面居中显示在屏幕上
  • Connexion/Quitter Auteur 菜单选项处于活动状态
  • Annuler按钮被隐藏
  • 列表 LstSuivi 和 LstDialogue 为空
登录/连接菜单

仅当“远程主机”和“端口号”字段不为空,且当前没有活动连接时,此选项才可用。单击此选项将执行以下操作:

  • 验证端口是否有效:必须为大于0的整数
  • 启动一个线程以确保与服务器的连接
  • 显示“Annuler”按钮,以便用户中断当前连接
  • Quitter Auteur 外,所有菜单选项均被禁用

连接可通过多种方式终止:

  1. 用户点击了按钮 Annuler:停止连接线程,并将菜单恢复至初始状态。在跟踪记录中注明连接由用户关闭。
  2. 连接因错误终止:处理方式与前文相同,此外在跟踪记录中需注明错误原因。
  3. 连接正常结束: 移除按钮 Annuler,在跟踪记录中注明连接已建立,启用菜单 RazSuivi,禁用菜单 Connecter,启用菜单 Déconnecter
连接/断开菜单

此选项仅在与服务器建立连接时可用。启用后,将关闭与服务器的连接,并将菜单恢复至初始状态。跟踪记录中会显示连接已被客户端关闭。

连接/退出菜单

此选项将关闭与服务器的任何活动连接并退出应用程序。

消息/发送菜单

只有满足以下条件时,此选项才可用:

  • 已与服务器建立连接

  • 有待发送的消息

如果满足这些条件, 将向服务器发送TxtSend (3)字段中的文本,若已勾选RCLF选项,则以RCLF序列结尾;否则以LF序列结尾。 发送过程中出现的任何错误都会在跟踪列表中显示。

菜单 RazSuivi 和 RazDialogue

分别清空列表 LstSuiviLstDialogue。当相应的列表为空时,这些选项将被禁用。

“取消”按钮

该按钮位于表单底部,仅在客户端正在连接服务器时显示。由于服务器无响应或响应异常,连接可能无法建立。此时,Annuler按钮允许用户中断连接请求。

跟踪列表

列表 LstSuivi (5) 用于跟踪连接状态。它记录了连接过程中的关键时刻:

  • 客户端建立连接

  • 由服务器或客户端关闭连接

  • 连接处于活动状态期间可能发生的所有错误

列表 LstDialogue (6) 用于跟踪客户端与服务器之间建立的对话。一个线程在后台监视客户端通信套接字上的活动,并将相关信息显示在列表 6 中。

“作者”选项

此菜单将打开一个名为“版权”的窗口:

Image

错误管理

连接错误会在跟踪列表 6 中显示,而与客户端/服务器对话相关的错误则显示在对话列表 7 中。当发生连接错误时,客户端/服务器对话将被关闭,表单将恢复到初始状态,准备进行新的连接。

8.5.1.3. TRAVAIL 至 FAIRE

以两种形式实现上述功能:

  • 独立应用程序
  • 小程序

8.5.2. 练习 2 - 资源服务器

8.5.2.1. INTRODUCTION

某机构拥有多台可通过互联网访问的强大计算服务器。 任何希望使用这些计算服务的机器都会向其中一台服务器的756端口发送一个数据文件。该文件包含多种信息:登录名、密码、指定所需计算类型的命令,以及待计算的数据。如果数据文件正确,所选的计算服务器将使用该文件,并以文本文件的形式将结果返回给客户端。

这种架构具有多重优势:

  • 任何类型的客户端(PC、Mac、Unix 等)均可使用此服务
  • 客户端可以位于互联网的任何位置
  • 计算资源经过优化:仅需少量高性能机器。因此,缺乏计算资源的小型组织也可使用此服务,只需根据实际使用的计算时间支付相应费用。

尽管机器性能强大,但有时一次计算仍可能持续数小时:此时服务器将无法为其他客户端提供服务。这便给客户端带来了寻找可用计算服务器的难题。为此,我们使用了一个“计算资源管理器”,下文将称之为 GRC 服务器。 该服务部署在一台独立机器上,通过TCP协议864端口运行。当客户端需要访问计算服务器时,需向该服务发起请求。持有完整计算服务器列表的GRC服务器会响应请求,并返回当前处于空闲状态的某台服务器的名称。 此时,客户端只需将数据发送至指定的服务器即可。

我们计划编写服务器 GRC。

8.5.2.2. INTERFACE VISUELLE

视觉界面如下:

Image

该界面包含两份服务器列表:

  • 左侧为空闲服务器列表,这些服务器可用于计算
  • 右侧为正在处理客户计算任务的服务器列表。

菜单结构如下:

主菜单
子菜单
角色
服务
启动
启动端口 864 的 TCP 服务
 
停止
停止服务
 
退出
退出应用程序
作者
 
版权信息

表单中控件的结构如下:

姓名
类型
角色
listLibres
JList
可用服务器列表
listOccupés
JList
已占用服务器列表

8.5.2.3. FONCTIONNEMENT DE APPLICATION

应用程序加载

应用程序加载时,列表 listLibres 将填充由 GRC 管理的计算服务器名称列表。这些服务器在作为参数传递的 Servers 文件中定义。 该文件包含服务器名称列表,每行一个服务器名称,因此用于填充列表 listLibres。允许使用“启动”菜单,禁用“停止”菜单。

“服务/启动”选项

此选项

  • 将启动该机器的 864 端口监听服务
  • 禁用“启动”菜单
  • 启用“停止”菜单
服务/停止选项

此选项将终止服务:

  • 清空已占用服务器列表
  • 空闲服务器列表将填充为“服务器”文件中的内容
  • 启用“启动”菜单
  • 禁用“停止”菜单
“服务/退出”选项

应用程序将退出。

客户端/服务器对话

客户端/服务器对话通过交换以字符串 RCLF 结尾的文本行进行。服务器 GRC 识别两个命令:getserveurfinservice。我们详细说明这两个命令的作用:

  • 1-getserveur

客户端查询是否有可用的计算服务器。

此时,服务器 GRC 会从其空闲服务器列表中选取第一个服务器,并以以下格式将其名称返回给客户端:

        100-nom du serveur

此外,它会将分配给客户端的服务器以以下形式添加到已占用服务器列表中:

        serveur (IP du client)

如下例所示,服务器 calcul1.istia.univ-angers.fr 正忙于为地址为 IP 的客户端提供服务:

Image

如果客户端已被分配了计算服务器,则无法发送 getserveur 命令。 因此,在响应客户端之前,服务器 GRC 会检查客户端的地址 IP 是否已存在于“忙碌服务器”列表中。如果存在,服务器 GRC 将响应:

        501-Vous avez actuellement une demande en cours

最后,还存在计算服务器均不可用的情况:空闲服务器列表为空。此时,服务器 GRC 将返回:

        502- Il n’y a aucun serveur de calcul disponible

无论哪种情况,在向客户端响应后,服务器 GRC 都会关闭与该客户端的连接,以便为其他客户端提供服务。

  • 2-finservice

客户端表示不再需要其正在使用的计算服务器。

服务器 GRC 首先会验证该客户端是否确实是其曾服务的客户端。 为此,它会检查客户端的地址 IP 是否存在于“已占用服务器”列表中。若不存在,服务器 GRC 将响应:

        503-Aucun serveur ne vous a été attribué

如果识别出该客户端,服务器 GRC 将返回:

        101-Fin de service acceptée

并将分配给该客户端的计算服务器移至空闲服务器列表中。以之前的示例为例,如果客户端发送 finservice 命令,服务器 GRC 的显示将变为:

Image

无论响应内容为何,在发送响应后,服务器 GRC 都会关闭连接。

8.5.2.4. TRAVAIL 发给 FAIRE

将该应用程序编写为一个独立程序,例如可使用客户端 telnet 或上一练习中的通用 TCP 客户端进行测试。

8.5.3. 练习 3 - 一个 SMTP 客户端

8.5.3.1. INTRODUCTION

在此,我们希望构建一个用于 SMTP 服务(SendMail 传输协议)的客户端,该服务支持发送邮件。 在 Unix 或 Windows 系统中,程序 telnet 是一个基于 tcp 协议运行的客户端。 它可与任何接受以 RCLF 字符串结尾的文本格式命令的 tcp 服务进行“通信”,即接受代码为 ASCII 的字符 13 和 10。 以下是与邮件发送服务 smtp 进行交互的一个示例:

$ telnet istia.univ-angers.fr 25        // 调用 SMTP 服务

// SMTP 服务器的响应


Trying 193.52.43.2...
Connected to istia.univ-angers.fr.
Escape character is '^]'.
220-Istia.Istia.Univ-Angers.fr Sendmail 8.6.10/8.6.9 ready at Tue, 16 Jan 1996 07:53:12 +0100
220 ESMTP spoken here

// 注释 --------------

程序 telnet 可以通过以下语法调用任何服务

telnet machine_service port_service

客户端与服务器的交互通过以字符串 RCLF 结尾的文本行进行。

服务 smtp 的响应格式为:

消息编号 或

消息编号

SMTP 服务器可能发送多行响应。响应的最后一行以数字后跟空格表示,而响应的前几行中,数字后跟连字符 -。

编号大于或等于 500 表示错误消息。

// 注释结束

help                        // 键盘输入命令

// SMTP 服务器的响应

214-Commands:
214-    HELO    EHLO    MAIL    RCPT    DATA
214-    RSET    NOOP    QUIT    HELP    VRFY
214-    EXPN    VERB
214-For more info use "HELP <topic>".
214-To report bugs in the implementation send email to
214-    sendmail@CS.Berkeley.EDU.
214-For local information send email to Postmaster at your site.
214 End of HELP info
mail from: serge.tahe@istia.univ-angers.fr    // 键盘发出新命令

// 注释 ---------

mail 命令的语法如下:

mail from: 发件人的电子邮箱地址

// 注释结束

// SMTP 服务器的响应

250 serge.tahe@istia.univ-angers.fr... Sender ok

// 注释

服务器 smtp 不会对发件人地址的有效性进行任何验证:它直接采用所提供的地址

// 注释结束

rcpt to: user1@istia.univ-angers.fr        // 键盘发出新命令

// 注释 ---------

rcpt 命令的语法如下:

rcpt to: 邮件收件人的电子邮箱地址

如果该电子邮件地址属于运行 smtp 服务器的机器,则服务器会验证该地址是否存在;否则不会进行任何验证。如果进行了验证并检测到错误,将返回 >= 500 的错误代码。

可以发出任意数量的 rcpt to 命令:这使得向多人发送邮件成为可能。

// 注释结束

// SMTP 服务器的响应

250 user1@istia.univ-angers.fr... Recipient ok
data                        // 键盘输入的新命令

// 注释 ---------

data 命令的语法如下:

data

行1

行2

...

其后是构成消息的文本行,消息必须以仅包含“点”字符的一行结尾。

随后,该消息将发送给 rcpt 命令指定的收件人。

// 注释结束

// SMTP 服务器的响应

354 Enter mail, end with "." on a line by itself

// 通过键盘输入的消息正文


subject: essai smtp

essai smtp a partir de telnet
.

// 注释

在 data 命令的文本行中,可以添加一行 subject: 来指定邮件的主题。该行之后必须跟一行空行。

// SMTP 服务器的响应

250 HAA11627 Message accepted for delivery
quit                            // 键盘发出新命令

// 注释

quit 命令将关闭与 smtp 服务的连接

// 注释结束

// SMTP 服务器的响应

221 Istia.Istia.Univ-Angers.fr closing connection

8.5.3.2. INTERFACE VISUELLE

我们计划构建一个具有以下可视化界面的程序:

Image

各控件的功能如下:

编号
类型
功能
1
JTextField
用逗号分隔的一组电子邮件地址
2
JTextField
邮件主题文本
3
JTextField
用逗号分隔的一组电子邮件地址
4
JTextField
以逗号分隔的电子邮件地址序列
5
JTextArea
邮件正文
6
JList
跟踪清单
7
JList
对话列表
8
JButton
“取消”按钮未显示,当客户端请求连接服务器时出现(SMTP)。如果服务器未响应,用户可通过此按钮中断该请求。

8.5.3.3. LES MENUS

应用程序的菜单结构如下:

主菜单
二级菜单
角色
邮件
  
 
发送
发送控制台5的消息
 
退出
退出应用程序
选项
  
 
隐藏跟踪
将控件 6 设为不可见
 
清除跟踪
清空关注列表 6
 
隐藏对话框
隐藏对话列表 7
 
清空对话
清空对话列表 7
 
配置
允许用户指定
- 程序使用的 SMTP 服务器地址
- 其电子邮件地址
 
保存...
将之前的配置保存到 .ini 文件中
作者
 
版权信息

8.5.3.4. FONCTIONNEMENT DE APPLICATION

“选项/配置”菜单

此菜单将弹出以下窗口:

Image

必须填写这两个字段,才能使按钮 OK 处于活动状态。这两项信息应存储在全局变量中,以便其他模块使用。

邮件/发送菜单

仅当满足以下条件时,才可访问此选项:

  • 已完成配置
  • 有待发送的邮件
  • 存在邮件主题
  • 字段 1、3 和 4 中至少各有一个收件人

如果满足这些条件,事件序列如下:

  • 表单进入一种状态,其中所有可能干扰客户端/服务器对话的操作均被禁用
  • 连接到配置中指定的服务器25号端口
  • 随后,客户端根据上述协议与 SMTP 服务器进行通信
  • “mail from:”字段使用配置中指定的发件人电子邮箱地址
  • “rcpt to:”字段将用于字段1、3和4中找到的每个电子邮箱地址
  • 在命令 data 之后发送的行中,将包含以下文本:
    • 一行 Subject: 控制项 2 的主题文本
    • 一行 Cc: 检查地址 3
    • 一行 Bcc: 检查地址 4
    • 检查5的消息文本
    • 终端点
“取消”按钮

该按钮位于表单底部,仅在客户端正在连接 smtp 服务器时显示。由于 smtp 服务器未响应或响应异常,该连接可能无法建立。 此时,Annuler按钮允许用户中断连接请求。

跟踪列表

该列表(6)用于跟踪连接状态。它显示连接过程中的关键时刻:

  • 客户端建立连接
  • 由服务器或客户端关闭连接
  • 所有连接错误

列表 (7) 用于跟踪客户端与服务器之间建立的对话 smtp

这两个列表与菜单中的选项相关联:

隐藏关注列表
将跟踪列表 6 及其上方的标签设为不可见。 如果这两个控件占用的高度为 H,则其下方的所有控件将向上移动 H 的高度,表单的总高度将减少 H。 此外,“隐藏跟踪”会使下方的选项 RazSuivi 不可见。
清除跟踪
清空跟踪列表 6
隐藏对话框
将对话框列表 7、其上方的标签以及下方的菜单选项 RazDialogue 设为不可见。 与“隐藏跟踪”类似,下方控件(可能是按钮 Annuler)的位置将被重新计算,且窗口大小会缩小。
清空对话框
清空对话框列表 7
“作者”选项

此菜单将打开一个名为“版权”的窗口:

Image

错误管理

连接错误会在跟踪列表 6 中显示,而与客户端/服务器对话相关的错误则会在对话框列表 7 中显示。 发生错误时,系统会通过错误提示框通知用户,并显示包含错误原因的列表(如果该列表之前处于隐藏状态)。此外,客户端/服务器对话框将被关闭,表单将恢复到初始状态。

8.5.3.5. GESTION 来自 UN FICHIER DE CONFIGURATION

最好不要让用户每次使用软件时都需重新配置。 为此,如果勾选了选项/退出时保存配置”选项,则关闭程序时会将通过选项 Options/Configurer 获取的两项信息以及两个跟踪列表的状态保存到位于程序 .exe 文件同一目录下的 sendmail.ini 文件中。 该文件格式如下:

SmtpServer=shiva.istia.univ-angers.fr
ReplyAddress=serge.tahe@istia.univ-angers.fr
Suivi=0
Dialogue=1

SmtpServerReplyAddress 包含通过“选项/配置”选项获取的两项信息。 行 SuiviDialogue 显示“跟踪”和“对话”列表的状态:1(存在),0(不存在)。

程序加载时,若文件 sendmail.ini 存在,则读取该文件并据此配置表单。若文件 sendmail.ini 不存在,则按以下方式处理:

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

如果文件 sendmail.ini 存在但不完整(缺少行),则用上面的对应行替换缺失的行。因此,如果缺少行 Suivi=...,则将其视为 Suivi=1

所有不符合模板的行:

    mot clé= valeur

的行以及关键字无效的行均被忽略。关键字可大写或小写:这无关紧要。

在选项 Options/Configurer 中,将显示当前正在使用的值 SmtpServerReplyAddress。用户可根据需要进行修改。

8.5.3.6. TRAVAIL 至 FAIRE

执行上述所述的工作。建议将配置文件的管理留到最后处理。

8.5.4. 练习 4 - 客户端 POPPASS

8.5.4.1. Introduction

我们计划创建一个客户端 TCP,使其能够与运行在 106 端口的服务器 POPPASSD 进行通信。该服务允许用户在 UNIX 机器上更改密码。客户端/服务器通信协议如下:

1 - 通信通过交换以字符串 RCLF 结尾的消息进行

2 - 客户端向服务器发送命令

  • 服务器以三位数字开头的消息进行响应:XXX。若 XXX=200,则表示命令已正确执行;否则表示发生错误。

3 - 通信流程如下:

A    - le client se connecte
  • 服务器返回一条欢迎信息
B    - le client envoie USER login
  • 若登录成功,服务器将请求密码;否则返回错误信息
C    - le client envoie PASS mot_de_passe
  • 若密码被接受,服务器将要求输入新密码;否则返回错误
D    - le client envoie NEWPASS nouveau_mot_de_passe
  • 服务器会响应并确认新密码已被接受,否则返回错误
E    - le client envoie la commande QUIT
  • 服务器发送结束消息并关闭连接

8.5.4.2. 客户端表单

Image

各控件的含义如下:

编号
名称
类型
作用
1
txtRemoteHost
JTextField
服务器名称
2
txtLogin
JTextField
用户登录名
3
txtMdp
JTextField
用户密码
4
txtNewMdp
JTextField
用户新密码
5
txtConfirmation
JTextField
新密码确认
6
lstSuivi
JList
登录跟踪消息
7
lstDialogue
JList
客户端/服务器对话消息
10
cmdAnnuler
JButton
未显示 - 连接服务器时出现的按钮。用于终止连接。

8.5.4.3. 菜单

标题
控件名称
作用
连接
登录菜单
 
连接
mnuconnecter
启动与服务器的连接
退出
mnuQuitter
退出应用程序
消息
mnuMessages
 
RazSuivi
mnuRazSuivi
清除列表lstSuivi
RazDialogue
mnuRazDialogue
清除列表 lstDialogue
作者
mnuAuteur
显示版权框

8.5.4.4. 应用程序运行

应用程序初始化

当应用程序的主界面加载完成时,将执行以下操作:

  • 页面居中显示在屏幕上
  • Connexion/Quitter Auteur 菜单选项处于活动状态
  • 按钮 Annuler 被隐藏
  • 列表 LstSuivi 和 LstDialogue 为空
“登录/连接”菜单

仅当字段 1 至 5 已填写时,此选项才可用。单击此选项将执行以下操作:

  • 启动一个线程以确保与服务器的连接
  • 显示按钮 Annuler,以便用户中断当前连接
  • Quitter Auteur 外,所有菜单选项均被禁用

随后事件序列如下:

  1. 用户点击了按钮 Annuler:停止连接线程,并将菜单恢复至初始状态。在跟踪记录中标注用户已关闭连接。
  2. 服务器接受了连接请求。随后开始与服务器进行对话以更改密码。该对话的通信记录保存在列表 LstDialogue 中。对话结束后,与服务器的连接关闭,表单菜单恢复为初始状态。
  3. 在对话进行期间,“取消”按钮始终可见,以便用户在需要时终止连接。
  4. 若通信过程中发生任何错误,连接将被关闭,且错误原因将显示在跟踪列表 LstSuivi 中。
“连接/退出”菜单

此选项将关闭与服务器的任何活动连接并退出应用程序。

菜单 RazSuivi 和 RazDialogue

分别清空列表 LstSuiviLstDialogue。当相应的列表为空时,这些选项将被禁用。

“取消”按钮

该按钮位于表单底部,仅在客户端正在连接或已连接到服务器时显示。Annuler按钮允许用户中断与服务器的通信。

跟踪列表

列表 LstSuivi (5) 用于跟踪连接状态。它显示连接过程中的关键时刻:

  • 客户端建立连接

  • 由服务器或客户端关闭连接

  • 连接处于活动状态期间可能发生的任何错误

列表 LstDialogue (6) 用于跟踪客户端与服务器之间建立的对话。

“作者”选项

此菜单将打开一个名为“版权”的窗口:

Image

错误管理

通信错误会在跟踪列表 6 中显示,与客户端/服务器对话相关的错误则会在对话列表 7 中显示。发生连接错误时,客户端/服务器对话将被关闭,表单将恢复到初始状态,准备重新建立连接。

8.5.4.5. TRAVAIL 至 FAIRE

将上述工作以独立应用程序的形式实现,随后再以小程序的形式实现。