Skip to content

8. TCP-IP 编程

8.1. 概述

8.1.1. 互联网协议

本文将介绍互联网通信协议,即通常所说的TCP/IP传输控制协议/互联网协议)套件,其名称源自其中的两个主要协议。建议读者在着手开发分布式应用程序之前,先对网络的工作原理,特别是TCP/IP协议,有一个大致的了解。

下文节选自NOVELL公司1990年代初出版的《LAN Workplace for DOS——管理员指南》一书,并进行了部分翻译。


构建异构计算机网络这一基本概念源于美国国防高级研究计划局(DARPA)的研究。DARPA开发了一套名为TCP/IP的协议套件,该套件使异构机器能够相互通信。这些协议曾在名为ARPAnet的网络上进行测试,该网络后来演变为互联网。 TCP/IP协议定义了传输和接收的格式及规则,这些定义独立于网络架构和所使用的硬件。

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

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

8.1.2. OSI模型

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

Image

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

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

在每一层,负责发送信息的发送方进程都会将其发送给另一台机器上与其处于同一层的接收方进程。这一过程遵循被称为“层协议”的特定规则。因此,我们得到如下最终通信示意图:

Image

各层的作用如下:

物理层
确保通过物理介质传输比特。该层包括数据处理终端设备(DPTE),如终端或计算机,以及数据电路终端设备(DCTE),如调制解调器、复用器和集中器。该层的关键点包括:
. 信息编码方式的选择(模拟或数字)
. 传输模式的选择(同步或异步)。
数据链路层
隐藏物理层的物理特性。检测并纠正传输错误。
网络
管理通过网络传输的信息必须遵循的路径。这被称为路由:确定信息到达目的地必须经过的路径。
传输
实现两个应用程序之间的通信,而前几层仅允许机器之间的通信。该层提供的一项服务是多路复用:传输层可利用单一网络连接(机器间连接)来传输属于多个应用程序的数据。
会话
该层提供服务,允许应用程序在远程机器上建立并维持一个工作会话。
表示层
旨在标准化不同机器间数据的表示形式。因此,源自机器A的数据在通过网络发送前,将由机器A表示层根据标准格式进行“格式化”。当数据到达目标机器B的表示层时,该层将凭借其标准格式识别数据,并将其重新格式化,以便机器B上的应用程序能够识别。
应用层
在此层级,我们看到的是通常与用户密切相关的应用程序,例如电子邮件或文件传输。

8.1.3. TCP/IP模型

OSI模型是一个理想模型,至今尚未完全实现。TCP/IP协议套件通过以下方式对其进行了近似实现:

Image

物理层

在局域网中,通常采用以太网令牌环技术。本文将仅聚焦于以太网技术。

以太网

这是对一种分组交换局域网技术的称谓,该技术于20世纪70年代初在施乐帕克研究中心(Xerox PARC)发明,并于1978年由施乐、英特尔和数字设备公司共同制定了标准。 该网络在物理层由直径约1.27厘米、长度最长可达500米的同轴电缆构成。可通过中继器延长网络,但任意两台设备之间最多只能使用两个中继器。该电缆属于无源设备:所有有源组件均位于连接到电缆的设备上。每台设备通过网络接口卡连接到电缆,该卡包含:

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

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

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

Image

  • 广播网络——发送设备在电缆上发送信息时会附带接收设备的地址。所有连接的设备都会接收到该信息,但只有预定的接收方才会保留该信息。
  • 访问方式如下:希望发送信息的发送器监听电缆——随后检测是否存在载波,载波的存在表明正在进行传输。这就是 CSMA载波侦听多路访问)技术。若未检测到载波,发送器可决定依次进行传输。 可能有多个发送器做出此决定。这些传输信号会相互混杂,这被称为“碰撞”。发送器会检测这种情况:在电缆上传输的同时,它也会监听实际通过电缆的数据。如果检测到电缆上传输的信息并非自己发送的,它便会判定发生了碰撞并停止传输。其他正在传输的发送器也会采取同样的措施。 每个发射器将在经过随机延迟后恢复传输,延迟时间取决于各发射器自身。该技术称为 CD碰撞检测)。因此,这种访问方式被称为 CSMA/CD
  • 48位地址。每台设备都有一个地址,此处称为物理地址,该地址写在连接设备与电缆的网卡上。该地址被称为设备的以太网地址。

网络层

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

IP(互联网协议)
在两个网络节点之间传输数据包
ICMP
(互联网控制消息协议)
ICMP 促进了不同机器上 IP 协议程序之间的通信。因此,它是一种在 IP 协议内部进行消息交换的协议。
ARP
(地址解析协议)
将机器的互联网地址映射到其物理地址
RARP
(反向地址解析协议)
将计算机的物理地址映射到其互联网地址

传输层/会话层

该层包括以下协议:

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

应用层/表示层/会话层

此处包含多种协议:

TELNET
一种终端仿真器,允许机器A以终端身份连接到机器B
FTP(文件传输协议)
支持文件传输
TFTP(简易文件传输协议)
支持文件传输
SMTP(简单邮件传输协议)
支持网络用户之间的消息交换
DNS(域名系统)
将主机名转换为该主机的互联网地址
XDR(外部数据表示法)
由 Sun Microsystems 创建,它定义了一种标准的、与机器无关的数据表示法
RPC(远程过程调用)
同样由 Sun 定义,是一种独立于传输层的远程应用程序间通信协议。该协议至关重要:它使程序员无需了解传输层的细节,并确保应用程序的可移植性。该协议基于 XDR 协议
NFS(网络文件系统)
同样由 Sun 定义,该协议允许一台机器“看到”另一台机器的文件系统。它基于上述的 RPC 协议

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

在 TCP/IP 环境中开发的应用程序通常会使用该环境中的多种协议。 应用程序与协议的最高层进行通信。该层将信息传递给其下层,依此类推,直至到达物理介质。在那里,信息被物理传输至目标机器,随后在目标机器上再次经过相同的协议层,但方向相反,直至到达预定接收该信息的应用程序。下图展示了信息的传输路径:

Image

让我们举个例子:定义在应用层的FTP应用程序,它支持机器之间的文件传输。

  • 应用程序将待传输的一系列字节传递给传输
  • 传输层将这串字节划分为TCP分段,并在每个分段开头添加序号。这些分段被传递给网络层,该层由IP协议管理。
  • IP 层创建一个封装了接收到的 TCP 分段的数据包。在该数据包的头中,它放置了源机和目标机的互联网地址。它还确定了目标机的物理地址。整个数据包被传递到数据链路物理层,即连接机器与物理网络的网卡。
  • 在那里,IP数据包会被进一步封装到物理帧中,并通过电缆发送给接收方。
  • 在目标机器上,数据链路层与物理层执行相反的操作:它将 IP 数据包从物理帧中解封装,并将其传递给 IP 层。
  • IP层会验证数据包是否正确:它根据接收到的位计算校验和,并将其与数据包头中的校验和进行比对。如果不匹配,则拒绝该数据包。
  • 如果数据包被认为是有效的,IP 层会将其中的 TCP 数据段解封装,并将其传递给传输层。
  • 传输层(在本例中即TCP层)会检查分段号,以确保分段的顺序正确。
  • 它还会为 TCP 分段计算校验和。如果校验和正确,TCP 层会向源主机发送确认;否则,该 TCP 分段将被拒绝。
  • TCP 层剩下的工作就是将分段的数据部分传输给上层中预定的接收应用程序。

8.1.5. 解决互联网上的地址问题

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

节点的互联网地址逻辑地址:它与所使用的硬件和网络无关。这是一个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。 该网络的管理员随后可以根据需要分配 193.49.144.1 到 193.49.144.254 之间的 IP 地址。该地址通常存储在连接到该网络的每台机器上的特定文件中。

8.1.5.1. IP地址类

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

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

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

A类

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

R1 是网络地址

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

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

Image

网络地址长7位,主机地址长24位。因此,可以有127个A类网络,每个网络最多包含2²⁴个主机。

B类

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

R1.R2 是网络地址

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

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

Image

网络地址为 2 字节(确切地说,是 14 位),节点地址也是 2 字节。这意味着可以有 2¹⁴ 个 B 类网络,每个网络最多包含 2¹⁶ 个节点。

C类

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

R1.R2.R3 是网络地址

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

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

Image

网络地址占用3字节(减去3位),主机地址占用1字节。因此,C类网络共有2²¹个,每个网络最多可包含256个主机。

由于昂热大学理学院的Lagaffe机器的地址为193.49.144.1,我们可以看到最高字节为193,二进制表示为11000001。由此可推断该网络属于C类网络。

保留地址

. 某些 IP 地址是网络地址,而非网络内的节点地址。这些地址的节点地址被设置为 0。因此,地址 193.49.144.0 是昂热科学学院网络的 IP 地址。因此,网络中没有任何节点可以使用地址 0。

. 当 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数据包拆分为多个片段,每个片段随后通过物理网络发送。这些片段仅会在最终目的地被重新组装。

8.1.6.1. 路由

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

直接路由

直接路由是指IP数据包在同一网络内直接从发送方传输到接收方:

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

间接路由

间接路由是指将 IP 数据包传输到位于发送方所属网络之外的网络上的目的地。在这种情况下,源机和目标机的 IP 地址中的网络地址部分是不同的。 源计算机识别到这一点。然后,它将数据包发送到一个称为路由器的特殊节点,该节点将本地网络连接到其他网络,并在其表中查找到该节点的 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 协议在经过一定时间后仍未收到确认,它将重传该数据段,从而确保信息传输服务的质量。
  • 两个通信进程之间建立的虚拟电路是全双工的:这意味着信息可以双向流动。因此,即使源进程仍在发送信息,目标进程也可以发送确认。这使得源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机器

位于昂热ISERPA的Sun机器

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

昂热大学的DPX2/320计算机*被*命名为Lagaffe,而一台486DX50个人电脑则*被命名为liny*。如何从外部引用这些计算机?方法是指定它们所属的域名层级。因此,Lagaffe计算机的全称将是:
        Lagaffe.univ-Angers.fr
在域内,可以使用相对名称。因此,在 **fr** 域内且位于 **univ-Angers** 域之外时,可以这样引用 Lagaffe 机器:
        Lagaffe.univ-Angers
最后,在 *univ-Angers* 域内,只需简单地引用为
        Lagaffe
因此,应用程序可以通过其名称来引用一台机器。但归根结底,你仍然需要获取该机器的 IP 地址。该如何操作呢?假设我们希望从机器 A 与机器 B 进行通信。
  • 如果机器 B 与机器 A 属于同一个域,那么机器 A 上的某个文件中很可能就包含机器 B 的 IP 地址。
  • 否则,机器A会在另一个文件或之前提到的同一个文件中,找到一份包含多个域名服务器及其IP地址的列表。域名服务器负责将机器名称映射到其IP地址。机器A会向列表中的第一个域名服务器发送一个特殊请求,即所谓的DNS查询,其中包含要查找的机器名称。 如果被查询的服务器在其记录中拥有该名称,它将向计算机A发送相应的IP地址。否则,该服务器也会在其文件中查找可查询的域名服务器列表,并进行查询。因此,将查询多个域名服务器,但并非随机进行,而是以尽量减少请求次数的方式进行。如果最终找到了该计算机,响应将被发回给计算机A。

XDR:(外部数据表示法)

该协议由 Sun Microsystems 创建,规定了一种标准的、与机器无关的数据表示法。

RPC:(远程过程调用)

同样由Sun定义,这是一种独立于传输层的远程应用程序间通信协议。该协议至关重要:它使程序员无需了解传输层的细节,并确保应用程序的可移植性。该协议基于XDR协议

NFS:网络文件系统

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

8.1.9. 结论

在本篇简介中,我们介绍了互联网协议的一些主要特征。若想进一步探索这一领域,读者可参考道格拉斯·科默(Douglas Comer)的优秀著作:

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

作者:道格拉斯·科默

出版社:InterEditions

8.2. Java中的网络地址管理

8.2.1. 定义

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

byte [] getAddress()
返回当前 InetAddress 实例的 4 字节 IP 地址
String getHostAddress()
返回当前 InetAddress 实例的 IP 地址
String getHostName()
返回当前 InetAddress 实例的互联网名称
String toString()
返回当前 InetAddress 实例的 IP 地址/互联网名称
InetAddress getByName(String Host)
为由 Host 指定的机器创建 InetAddress 实例。如果 Host 未知,则抛出异常。Host 可以是机器的互联网名称,也可以是 I1.I2.I3.I4 格式的 IP 地址
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);
    }// fin try
  }// fine hand
}// fin class

执行结果如下:

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;
    // we retrieve the argument
    if(arg.length==0) 
      nomMachine="localhost";
    else nomMachine=arg[0];
    // we try to obtain the machine address
    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);
    }// fin try
  }// fine hand
}// fin class

调用 JavagetByName 方法后,我们得到以下结果:

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

使用 Javagetbyname 调用 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 协议仍未收到确认,它将重传该分段,从而确保信息传输服务的质量。
  • 两个通信进程之间建立的虚拟电路是全双工的:这意味着信息可以双向流动。因此,即使源进程仍在发送信息,目标进程也可以发送确认。这使得源 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. 定义

程序在互联网上进行通信时所使用的基本工具是套接字socket)。这个英语单词原意为“电源插座”,在此被引申为“网络接口”。 应用程序若要在互联网上发送和接收信息,就需要一个网络接口,即套接字。该工具最初诞生于伯克利版的Unix系统中。此后,它已被移植到所有Unix系统以及Windows环境中。在Java虚拟机中,它也以两种形式存在:用于客户端应用程序的Socket类,以及用于服务器应用程序的ServerSocket类。下面我们将介绍Socket类的一些构造函数和方法:

public Socket(String host, int port)
在主机上打开与端口 port 的远程连接
public int getLocalPort()
返回套接字使用的本地端口号
  
public int getPort()
返回套接字所连接的远程端口号
  
public InetAddress getLocalAddress()
返回套接字绑定的本地 InetAddress
  
public InetAddress getInetAddress()
返回套接字所绑定的远程 InetAddress
  
public InputStream getInputStream()
返回一个用于读取远程方发送的数据的输入流
  
public OutputStream getOutputStream()
返回一个用于向远程伙伴发送数据的输出流
  
public void shutdownInput()
关闭套接字的输入流
  
public void shutdownOutput()
关闭套接字的输出流
  
public void close()
关闭套接字及其 I/O 流
  
public String toString()
返回一个“表示”该套接字的字符串
 

8.3.6.2. 与服务器建立连接

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

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

构造函数

    public Socket(String  host, int  port);

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

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

我们需要处理此异常:


    Socket  sClient=null;
    try{
        sClient=new Socket(host,port);
    } catch(Exception e){
        // la connexion a échoué - on traite l'erreur
        ….
    }

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

public int getLocalPort();

如果连接成功,我们已经看到,在服务器端,会有另一个任务负责在所谓的“服务端口”上处理服务。可以通过以下方法获取该端口号:

public int getPort();

8.3.6.3. 通过网络发送信息

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

public OutputStream getOutputStream();

发送到该流中的所有内容都将在服务器机器的服务端口上接收。许多应用程序使用基于文本的接口,该接口由文本行后跟换行符组成。因此,println 方法在这些情况下非常有用。然后,我们将 OutputStream 输出流转换为 PrintWriter 流,该流提供了 println 方法。写入操作可能会引发异常。

8.3.6.4. 从网络读取信息

您可以使用以下方法获取用于读取套接字上接收到的数据的读取流:

public InputStream getInputStream();

从该流读取的所有内容均来自服务器机器的服务端口。对于对话框由换行符结尾的文本行组成的应用程序,我们建议使用 readLine 方法。为此,我们将 InputStream 转换为 BufferedReader,后者提供了 readLine() 方法。读取操作可能会抛出异常。

8.3.6.5. 关闭连接

可通过以下方法实现:

public void close();

此方法可能会抛出异常。所使用的资源(尤其是网络端口)将被释放。

8.3.6.6. 客户端架构

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


    Socket  sClient=null;
    try{
            // connect to the service running on port P of machine M
        sClient=new Socket(M,P);
 
        // create client socket I/O streams
        BufferedReader in=new BufferedReader(new InputStreamReader(sClient.getInputStream()));
        PrintWriter out=new PrintWriter(sClient.getOutputStream(),true);
 
        // request-response loop
        boolean  fini=false;
        String demande;
        String réponse;
        while (! fini){
            // preparing the application
            demande=…
            // we send it
            out.println(demande);
            // we read the answer
            réponse=in.readLine();
            // the answer is processed

        }
        // it's over
        sClient.close();
    } catch(Exception e){
        // we handle the exception
        ….
    }

为了保持示例的简洁性,我们没有尝试处理 Socket 构造函数以及 readLine、getInputStream、getOutputStream 和 close 方法生成的各种类型的异常。所有异常都被合并为一个单一的异常。

8.3.7. ServerSocket 类

8.3.7.1. 定义

本类旨在用于服务器端的套接字管理。下面我们将介绍该类的一些构造函数和方法:

public ServerSocket(int port)
在端口 port 上创建一个监听套接字
public ServerSocket(int port, int count)
与上述相同,但将队列大小设置为 count,即当客户端连接到达时服务器正忙,此时队列中最多可容纳的客户端连接数。
public int getLocalPort()
返回套接字使用的监听端口号
public InetAddress getInetAddress()
返回套接字绑定的本地 InetAddress
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{
    // ouverture du service
    int portEcoute=…
    int maxConnexions=…
    sEcoute=new ServerSocket(portEcoute,maxConnexions);
 
    // traitement des demandes de connexion
    boolean fini=false;
    Socket sService=null;
    while( ! fini){
        // attente et acceptation d'une demande
        sService=sEcoute.accept();
 
        // le service est rendu par une autre tâche à laquelle on passe la socket de service
        new Service(sService).start();
 
        // on se remet en attente des demandes de connexion
    }
    // c'est fini - on clôt le service
    sEcoute.close();
} catch (Exception e){
    // on traite l'exception

}

Service 类是一个线程,其代码可能如下所示:


public class Service extends Thread{
 
    Socket sService;        // service socket
 
    // manufacturer
    public Service(Socket S){
        sService=S;
    }
 
// run
public void run(){
    try{
        // create input-output flows
    BufferedReader in=new BufferedReader(new InputStreamReader(sService.getInputStream()));
    PrinttWriter out=new PrintWriter(sService.getOutputStream(),true);
 
    // request-response loop
    boolean  fini=false;
    String demande;
    String réponse;
    while (! fini){
        // we read the request
        demande=in.readLine();
 
        // we treat it 

 
        // we prepare the answer
        réponse=…

        // we send it
        out.println(réponse);
    }
    // it's over
    sService.close();
    } catch(Exception e){
    // we handle the exception
    ….
    }// try
} // run

8.4. 应用程序

8.4.1. 回显服务器

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

    java serveurEcho port

服务器在作为参数传入的端口上运行。它只是将客户端发送的请求原样发回给客户端,并附带客户端的身份信息(IP+名称)。它的队列中最多接受 2 个连接。这里包含了 TCP 服务器的所有组件。程序如下:

// call: serveurEcho port
// echo server
// returns the line sent to the customer


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

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

    // main program
    public static void main (String arg[]){

     // is there an argument
     if(arg.length != 1)
        erreur(syntaxe,1);

     // this argument must be integer >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);

     // create the listening socket
     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);
     }

     // follow-up
     System.out.println("Serveur d'écho lancé sur le port " + port);

     // service loop
     boolean serviceFini=false;
     Socket service=null;
     while (! serviceFini){
         // waiting for a customer
        try{
            service=ecoute.accept();
        } catch (IOException e){
                erreur("Erreur lors de l'acceptation d'une connexion ("+e+")",4);
        }

         // we identify the link
        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);
        }


         // the service is provided by another task
        new traiteClientEcho(service).start();
     }// end while
    }// fine hand

// error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifies
    private static String identifie(InetAddress Host){
        // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// end class


// provides service to an echo server client

class traiteClientEcho extends Thread{

    private Socket service;            // service socket
    private BufferedReader in;        // iNPUTS
    private PrintWriter out;            // output flow

     // manufacturer
    public traiteClientEcho(Socket service){
        this.service=service;
    }

     // run method
    public void run(){

         // creation of input and output flows
        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);
        }// fin 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);
        }// fin try

         // link identification is sent to the customer
        try{
            out.println("Client ["+identifie(service.getInetAddress())+","+
            service.getPort()+"] connecté au serveur [" + identifie (InetAddress.getLocalHost())
            + "," + service.getLocalPort() + "]");
        } catch (Exception e) {
            erreur("identification liaison",1);
        }

         // loop read request/write response
        String demande,reponse;
        try{
             // the service stops when the client sends an end-of-file marker
            while ((demande=in.readLine())!=null){
                // echo of demand
                reponse="["+demande+"]";
                out.println(reponse);
                 // service stops when client sends "end
                if(demande.trim().toLowerCase().equals("fin")) break;
            }// end while
        } catch (IOException e){
                erreur("Erreur lors des échanges client/serveur ("+e+")",3);
        }// fin try

         // close the socket
        try{
            service.close();
        } catch (IOException e){
            erreur("Erreur lors de la fermeture de la socket de service ("+e+")",2);
        }// fin try
    }// end run

     // error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }// end error

     // identifies
    private String identifie(InetAddress Host){
         // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fin class

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

通过以下命令启动服务器

    java serveurEcho 187

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

Serveur d'écho lancé sur le port 187

要测试此服务器,我们使用 telnet 程序,该程序在 Unix 和 Windows 系统上均可使用。Telnet 是一个通用的 TCP 客户端,适用于所有在通信中接受以换行符结尾的文本行的服务器。我们的回显服务器正是如此。我们在 Windows(本例中为 Windows 2000)上启动第一个 telnet 客户端,方法是在 DOS 窗口中输入 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'escapement is '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'operation
open            ouvre une connexion à un site
quit            quitte telnet
set             définit les options (entrez 'set ?' to display the list)
status          affiche les informations d'status
unset           annule les options (entrez 'unset ?' to display the list)
? ou help       affiche des informations d'help
 
Microsoft Telnet> set ?
NTLM            Active l'authentication NTLM.
LOCAL_ECHO      Active l'local echo.
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 客户端测试了回显服务。现在我们将编写自己的客户端:

// call: clientEcho machine port
// echo server client
// sends lines to the server, which echoes them back to the server

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

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

    // main program    
    public static void main (String arg[]){

     // are there two arguments
     if(arg.length != 2)
        erreur(syntaxe,1);

     // the first argument must be the name of an existing machine
    String machine=arg[0];
    InetAddress serveurAddress=null;
    try{
        serveurAddress=InetAddress.getByName(machine);
    } catch (Exception e){
        erreur(syntaxe+"\nMachine "+machine+" inaccessible (" + e +")",2);
    }

     // port must be integer >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);

     // connect to the server
     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);
     }

     // we identify the link
    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);
    }

     // creation of a flow for reading lines typed on the keyboard
    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);
    }
     // creation of the input stream associated with the client socket
    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);
    }
     // creation of the output stream associated with the client socket
    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);
    }

     // request-response loops
    boolean serviceFini=false;
    String demande=null;
    String reponse=null;

    // we read the message sent by the server just after connection 
    try{
        reponse=in.readLine();
    } catch (IOException e){
            erreur("Lecture réponse ("+e+")",4);
    }        

     // response display
    System.out.println("Serveur : " +reponse);

    while (! serviceFini){
         // read a line typed on the keyboard
        System.out.print("Client : ");
        try{
            demande=IN.readLine();
        } catch (Exception e){
            erreur("Lecture ligne ("+e+")",9);
        }
         // sending demand on the network
        try{
            out.println(demande);
        } catch (Exception e){
            erreur("Envoi demande ("+e+")",10);
        }
         // wait/read answer
        try{
            reponse=in.readLine();
        } catch (IOException e){
                erreur("Lecture réponse ("+e+")",4);
        }
         // response display
        System.out.println("Serveur : " +reponse);
         // is it over?
        if(demande.trim().toLowerCase().equals("fin")) serviceFini=true;
    }
     // it's over
    try{
        sClient.close();
    } catch(Exception e){
        erreur("Fermeture socket ("+e+")",11);
    }
}// hand

// error display
    public static void erreur(String msg, int exitCode){
        System.err.println(msg);
        System.exit(exitCode);
    }

    // identifies
    private static String identifie(InetAddress Host){
        // host identification
        String ipHost=Host.getHostAddress();
        String nomHost=Host.getHostName();
        String idHost;
        if (nomHost == null) idHost=ipHost;
            else idHost=ipHost+","+nomHost;
        return idHost;
    }

}// fin class

该客户端的结构符合 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]

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

8.4.3. 一个通用的 TCP 客户端

互联网早期创建的许多服务都遵循前面讨论过的回显服务器模型:客户端与服务器的通信仅限于交换文本行。我们将编写一个通用的 TCP 客户端,其启动方式如下:java cltTCPgenerique server port

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

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

既然在之前的应用程序中并不需要,为什么这里要用两个线程呢?在那个应用程序中,通信协议是固定的:客户端发送一行,服务器回复一行。每个服务都有其特定的协议,我们还会遇到以下情况:

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

因此,那种向服务器发送一行并接收一行响应的循环并不总是适用。因此,我们将创建两个独立的循环:

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

要实现这两个独立的循环,我们需要两个独立的线程。让我们通过一个示例来了解执行过程:我们的通用 TCP 客户端连接到一个 SMTP(简单邮件传输协议)服务。该服务负责将电子邮件路由到收件人。它运行在 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: 发件人,用于指定发件人的电子邮件地址
    • rcpt to: 收件人,用于指定邮件收件人的电子邮件地址。如果有多个收件人,则需针对每位收件人重复执行 rcpt to: 命令。
    • data,该命令向 SMTP 服务器发出信号,表示邮件即将发送。如服务器响应所示,这由一系列行组成,最后一行仅包含一个句点。邮件可能包含通过空行与邮件正文分隔的标题。在本示例中,我们使用 Subject: 关键字添加了主题
  • 消息发送完成后,我们可以使用 quit 命令告知服务器操作已完成。随后服务器将关闭网络连接。读取线程可检测到此事件并停止运行。
  • 随后用户在键盘上输入“end”,以停止读取键盘输入命令的线程。

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

Image

请注意,SMTP 服务无法检测发件人是否有效。因此,您绝不能信任邮件中的“发件人”字段。在此案例中,发件人 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,在此处输入您在邮件服务器上的登录名
  • pass password,在此输入与前一个登录名关联的密码
  • list,用于获取邮件列表,格式为编号、字节大小
  • retr i,用于阅读编号为 i 的邮件
  • quit,用于结束本次会话。

现在,让我们来探讨客户端与 Web 服务器之间的通信协议,该服务器通常运行在 80 端口上:


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(超文本传输协议)头。我们在此不详细讨论这些头,但请记住,我们的通用客户端提供了访问这些头的途径,这有助于理解它们。例如,第一行:

<-- HTTP/1.1 200 OK

表明被访问的 Web 服务器支持 HTTP/1.1 协议,并且已成功找到请求的文件(200 OK),其中 200 是一个 HTTP 响应代码。这些行

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

告知客户端,它将接收 11,251 字节的 HTML(超文本标记语言)文本,且数据发送完毕后连接将被关闭。

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

// imported packages
import java.io.*;
import java.net.*;

public class clientTCPgenerique{

    // receives the characteristics of a service as a parameter in the form
     // server port
     // connects to the service
     // creates a thread to read keyboard commands
     // these will be sent to the
     // creates a thread to read server responses
     // these will be displayed on the screen
     // the whole thing ends with the command end typed on the keyboard

   // instance variable
  private static Socket client;

    public static void main(String[] args){

         // syntax
        final String syntaxe="pg serveur port";

         // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the server name
        String serveur=args[0];

         // port must be integer >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;
         // there may be problems
        try{
             // connect to the service
            client=new Socket(serveur,port);
        }catch(Exception ex){
             // error
            erreur("Impossible de se connecter au service ("+ serveur
                +","+port+"), erreur : "+ex.getMessage(),3);
             // end
            return;
        }//catch

         // create read/write threads
    new ClientSend(client).start();
    new ClientReceive(client).start();

        // end thread main
        return;
    }// hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class  

class ClientSend extends Thread {
    // class for reading keyboard commands
     // and send them to a server via a tcp client passed in parameter

    private Socket client;    // tcp client

     // manufacturer
    public ClientSend(Socket client){
         // we note the tcp client
        this.client=client;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        PrintWriter OUT=null;            // network write streams
    BufferedReader IN=null;        // keyboard flow
        String commande=null;            // command read from keyboard

         // error management
        try{
             // network write stream creation
            OUT=new PrintWriter(client.getOutputStream(),true);
      // keyboard input stream creation
      IN=new BufferedReader(new InputStreamReader(System.in));
            // order entry-send loop
            System.out.println("Commandes : ");
            while(true){
                 // read command typed on keyboard
                commande=IN.readLine().trim();
                // finished?
                if (commande.toLowerCase().equals("fin")) break;
                // send order to server
                OUT.println(commande);
                 // next order
            }//while
        }catch(Exception ex){
             // error
            System.err.println("Envoi : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[Envoi : fin du thread d'envoi des commandes au serveur]");
    }//run
}//class

class ClientReceive extends Thread{
    // class responsible for reading lines of text intended for a 
     // tcp client passed as parameter

    private Socket client;    // tcp client

     // manufacturer
    public ClientReceive(Socket client){
         // we note the tcp client
        this.client=client;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        BufferedReader IN=null;        // network read stream
        String réponse=null;        // server response

         // error management
        try{
             // create network read stream
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // loop read text lines from IN stream
            while(true){
                 // network streaming
                réponse=IN.readLine();
                 // closed flow?
                if(réponse==null) break;
                // display
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // error
            System.err.println("Réception : L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[Réception : fin du thread de lecture des réponses du serveur]");
    }//run
}//class

8.4.4. 一个通用的 TCP 服务器

现在我们将看一个服务器

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

该程序通过以下命令启动:java genericTCPserver listeningPort,其中 listeningPort 是客户端必须连接的端口。客户端服务将由两个线程处理:

  • 一个线程专门用于读取客户端发送的文本行
  • 另一个线程专门用于读取用户输入的响应。该线程将通过 `fin` 命令发出信号,表示其正在关闭与客户端的连接。

服务器为每个客户端创建两个线程。如果有 n 个客户端,则同时会有 2n 个活跃线程。除非用户按下键盘上的 Ctrl-C,否则服务器本身不会停止运行。让我们来看几个示例。

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


E:\data\serge\MSNET\c#\réseau\client tcp générique> 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

现在,让我们打开浏览器并访问 URL http://localhost:88/exemple.html。浏览器将连接到本地主机的 88 端口,并请求页面 /example.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 的。end 命令关闭了从服务器到客户端的连接。在我们的响应中,我们仅使用了以下 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

如果在上面的示例中,你选择“查看/源代码”来查看浏览器接收的内容,你会看到:

Image

也就是说,这正是通用服务器发送的内容。

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

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

public class serveurTCPgenerique{

    // main program
    public static void main (String[] args){

    // receives the port of listening to customer requests
     // creates a thread to read client requests
     // these will be displayed on the screen
     // creates a thread to read keyboard commands
     // these will be sent as a reply to the customer
     // the whole thing ends with the command end typed on the keyboard

    final String syntaxe="Syntaxe : pg port";
   // instance variable
         // is there an argument
     if(args.length != 1)
        erreur(syntaxe,1);

         // port must be integer >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);

     // we create the listening service
    ServerSocket ecoute=null;
    int nbClients=0;    // no. of customers handled
        try{
             // create the service
            ecoute=new ServerSocket(port);
             // follow-up
            System.out.println("Serveur générique lancé sur le port " + port);

             // customer service loop
            Socket client=null;
            while (true){ // infinite loop - will be stopped by Ctrl-C
                 // waiting for a customer
                client=ecoute.accept();

                 // the service is provided by separate threads
                nbClients++;

                 // create read/write threads
        new ServeurSend(client,nbClients).start();
        new ServeurReceive(client,nbClients).start();

                // back to listening to requests
            }// end while
        }catch(Exception ex){
             // we report the error
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }// fine hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

class ServeurSend extends Thread{
    // class responsible for reading typed responses
     // and send them to a client via a tcp client passed to the

    Socket client;    // tcp client
    int numClient;        // customer no

     // manufacturer
    public ServeurSend(Socket client, int numClient){
         // we note the tcp client
        this.client=client;
         // and its
        this.numClient=numClient;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        PrintWriter OUT=null;        // network write streams
        String réponse=null;        // answer read from keyboard
    BufferedReader IN=null;    // keyboard flow

        // follow-up
        System.out.println("Thread de lecture des réponses du serveur au client "+ numClient + " lancé");
         // error management
        try{
             // network write stream creation
            OUT=new PrintWriter(client.getOutputStream(),true);
      // keyboard flow creation
      IN=new BufferedReader(new InputStreamReader(System.in));
            // order entry-send loop
            while(true){
                 // customer identification
                System.out.print("--> " + numClient + " : ");
                 // read response typed on keyboard
                réponse=IN.readLine().trim();
                // finished?
                if (réponse.toLowerCase().equals("fin")) break;
                // send response to server
                OUT.println(réponse);
                 // following response
            }//while
        }catch(Exception ex){
             // error
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            OUT.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[fin du Thread de lecture des réponses du serveur au client "+ numClient+ "]");
    }//run
}//class

class ServeurReceive extends Thread{
    // class responsible for reading text lines sent to the server 
     // via a tcp client passed to the builder

    Socket client;    // tcp client
    int numClient;        // customer no

     // manufacturer
    public ServeurReceive(Socket client, int numClient){
         // we note the tcp client
        this.client=client;
         // and its
        this.numClient=numClient;
    }//manufacturer

     // thread Run method
    public void run(){

        // local data
        BufferedReader IN=null;        // network read stream
        String réponse=null;        // server response

         // follow-up
        System.out.println("Thread de lecture des demandes du client "+ numClient + " lancé");
         // error management
        try{
             // create network read stream
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            // loop read text lines from IN stream
            while(true){
                 // network streaming
                réponse=IN.readLine();
                 // closed flow?
                if(réponse==null) break;
                // display
                System.out.println("<-- "+réponse);
            }//while
        }catch(Exception ex){
            // error
            System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
        }//catch
         // end - we close the feeds
        try{
            IN.close();client.close();
        }catch(Exception ex){}
         // signals the end of the thread
        System.out.println("[fin du Thread de lecture des demandes du client "+ numClient+"]");
    }//run
}//class

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 页面的正文内容。程序如下:

// imported packages
import java.io.*;
import java.net.*;

public class clientweb{

    // requests a URL
     // displays its contents on the screen

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

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extract useful information from 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();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        try{
             // connect to the server
            client=new Socket(host,port);

            // create customer input/output flows TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // request URL - send HTTP headers
            OUT.println(commande + " " + path + query + " HTTP/1.1");
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // we read the answer
            while((réponse=IN.readLine())!=null){
                 // the answer is processed
                System.out.println(réponse);
            }//while
             // it's over
            client.close();
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

本程序的唯一新特性是使用了 URL 类。该程序接收以 http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... 形式呈现的 URL(统一资源定位符)或 URI(统一资源标识符)。 URL类允许我们将URL字符串分解为各个组成部分。URL对象是通过将作为参数接收的URL字符串进行构造而成的:

        // vérification validité de l'URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
            // URI incorrecte
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch

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

http://serveur:port/cheminPageHTML?param1=val1;param2=val2;... 

构建的,我们将得到:

url.getHost() = 服务器

url.getPort() = port-1(如果未指定端口)

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

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>

该程序如下:

// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientweb2{

    // requests a URL
     // displays its contents on the screen

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

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        final int nbRedirsMax=1;                // no more than one redirection accepted
        int nbRedirs=0;                                    // number of redirects in progress
        String premièreLigne;                        // 1st line of the answer
        boolean redir=false;                        // indicates redirection or not
        String locationString="";                // the URL string of a possible redirection

         // regular expression to find a URL redirect
        Pattern location=Pattern.compile("^Location: (.+?)$");

         // error management
        try{
             // you may have several URL to request if there are redirections
            while(nbRedirs<=nbRedirsMax){

                // extract useful information from 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();

                 // connect to the server
                client=new Socket(host,port);

                // create customer input/output flows TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // request URL - send HTTP headers
                OUT.println(commande + " " + path + query + " HTTP/1.1");    
                OUT.println("Host: " + host + ":" + port);
                OUT.println("Connection: close");
                OUT.println();

                 // read the first line of the answer
                premièreLigne=IN.readLine();
                 // screen echo
                System.out.println(premièreLigne);

                // redirection?
        if(premièreLigne.endsWith("302 Object moved")){     
                    // there is a redirection
                    redir=true;
                    nbRedirs++;
                }//if

                // next HTTP headers until you find the empty line signalling the end of the headers
                boolean locationFound=false;
                while(!(réponse=IN.readLine()).equals("")){
                    // the answer is displayed
                    System.out.println(réponse);
                     // if there is a redirection, we search for the Location header
                    if(redir && ! locationFound){
                        // compare the line with the relational expression location
                        Matcher résultat=location.matcher(réponse);
                        if(résultat.find()){
                            // if found, note the URL of redirection
                            locationString=résultat.group(1);             
                             // we note that we found
                            locationFound=true;
                        }//if
                    }//if
                    // next header
                }//while

                 // following lines of the answer
                System.out.println(réponse);
                while((réponse=IN.readLine())!=null){
                     // the answer is displayed
                    System.out.println(réponse);
                }//while
                 // close the connection
                client.close();
                 // are we done?
                if ( ! locationFound || nbRedirs>nbRedirsMax)
                    break;
                 // there is a redirection to be made - the new URL is built
                URLString=protocol +"://"+host+":"+port+locationString;
                url=new URL(URLString);
                // follow-up
                System.out.println("\n<--Redirection vers l'URL "+URLString+"-->\n");
            }//while
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

8.4.7. 税费计算服务器

我们将重新审视“TAXES”练习,该练习此前已以多种形式进行过讲解。让我们回顾一下最新版本:

已创建了一个名为 **impots** 的基类。其属性是三个数字数组:

public class impots{

  // data required for tax calculation
   // come from an external source

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

   // empty builder
  protected impots(){}

   // manufacturer
  public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{

impots 类有两个构造函数:

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

**impotsJDBC** 类继承自该类,允许从数据库内容中填充三个数组 limites**coeffR*coeffN*:

public class impotsJDBC extends impots{
  // addition of a constructor for building
   // limit tables, coeffr, coeffn from table
   // database taxes
  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

    // dsnIMPOTS: DSN database name
     // userIMPOTS, mdpIMPOTS: database login/password

此前编写了一个图形化应用程序。该应用程序使用了一个 impotsJDBC 类的实例。应用程序和该实例位于同一台机器上。我们建议将测试程序和 impotsJDBC 实例放置在不同的机器上。我们将构建一个客户端-服务器应用程序,其中远程的 impotsJDBC 实例将充当服务器。新类名为 TaxServer,并继承自 impotsJDBC

// imported packages
import java.net.*;
import java.io.*;
import java.sql.*;

public class ServeurImpots extends impotsJDBC {

    // attributes
    int portEcoute;                // the ability to listen to customer requests
    boolean actif;                // server status

     // manufacturer
    public ServeurImpots(int portEcoute,String DSNimpots, String USERimpots, String MDPimpots)
      throws IOException, SQLException, ClassNotFoundException {
       // parent construction
        super(DSNimpots, USERimpots, MDPimpots);
         // we note the listening port
        this.portEcoute=portEcoute;
         // currently inactive
        actif=false;
         // creates and launches a thread for reading keyboard commands
         // the server will be managed using these commands
        Thread admin=new Thread(){
        public void run(){
          try{
            admin();
        }catch (Exception ignored){}
      }
    };
    admin.start();
    }//ServeurImpots

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

    public void admin() throws IOException{
        // reads server administration commands typed from the keyboard
         // in an endless loop
        String commande=null;
    BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
        while(true){
             // invite
            System.out.print("Serveur d'impôts>");
             // read command
            commande=IN.readLine().trim().toLowerCase();
             // order execution
            if(commande.equals("start")){
                // active?
                if(actif){
                     //error
                    System.out.println("Le serveur est déjà actif");
                     // we continue
                    continue;
                }//if
                 // create and launch the listening service
                Thread ecoute=new Thread(){
                public void run(){
                  ecoute();
              }
            };
            ecoute.start();
            }//if
            else if(commande.equals("stop")){
                // end of all execution threads
                System.exit(0);
            }//if
            else {
                // error
                System.out.println("Commande incorrecte. Utilisez (start,stop)");
            }//if
        }//while
    }//admin

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

    public void ecoute(){
         // thread for listening to customer requests
         // we create the listening service
        ServerSocket ecoute=null;
        try{
            // create the service
            ecoute=new ServerSocket(portEcoute);
             // follow-up
            System.out.println("Serveur d'impôts lancé sur le port " + portEcoute);

             // service loop
            Socket liaisonClient=null;
            while (true){ // infinite loop
                 // waiting for a customer
                liaisonClient=ecoute.accept();

                 // the service is provided by another task
                new traiteClientImpots(liaisonClient,this).start();

                 // back to listening to requests
            }// end while
        }catch(Exception ex){
             // we report the error
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),3);
        }//catch
    }//listening thread

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

  1. Socket对象liaisonClient,用于访问客户端
  2. `impotsJDBC` 对象,该对象提供了 `this.calculate` 方法以计算税额。
// -------------------------------------------------------
// provides service to a tax server client

class traiteClientImpots extends Thread{

    private Socket liaisonClient;            // customer liaison
    private BufferedReader IN;                // iNPUTS
    private PrintWriter OUT;                    // output flow
    private impotsJDBC objImpots;            // object Tax

         // manufacturer
    public traiteClientImpots(Socket liaisonClient,impotsJDBC objImpots){
        this.liaisonClient=liaisonClient;
        this.objImpots=objImpots;
    }//manufacturer

run 方法处理客户端请求。这些请求以文本行形式呈现,可采用以下两种形式:

  1. 已婚(是/否) 子女数 年薪
  2. endCalculation

形式 1 用于计算税款,而形式 2 用于关闭客户端-服务器连接。

     // run method
    public void run(){
         // renders service to the customer
        try{
             // iNPUTS
            IN=new BufferedReader(new InputStreamReader(liaisonClient.getInputStream()));
             // output flow
            OUT=new PrintWriter(liaisonClient.getOutputStream(),true);
             // send a welcome message to the customer
            OUT.println("Bienvenue sur le serveur d'impôts");

             // loop read request/write response
            String demande=null;
            String[] champs=null;    // elements of the request
            String commande=null;    // customer order: calculation or fincalculs
            while ((demande=IN.readLine())!=null){
                 // demand is broken down into fields
                champs=demande.trim().toLowerCase().split("\\s+");
                 // two successful applications: calcul and fincalculs
                commande=champs[0];
                if(! commande.equals("calcul") && ! commande.equals("fincalculs")){
                     // customer error
                    OUT.println("Commande incorrecte. Utilisez (calcul,fincalculs).");
                     // next order
                    continue;
                }//if
                if(commande.equals("calcul")) calculerImpôt(champs);
                if(commande.equals("fincalculs")){
                     // good-bye message to customer
                    OUT.println("Au revoir...");
                     // freeing up resources
                    try{ OUT.close();IN.close();liaisonClient.close();}
                    catch(Exception ex){}
                     // end
                    return;
                }//if
                 //following request
            }//while
        }catch (Exception e){
            erreur("L'erreur suivante s'est produite ("+e+")",2);
        }// fin try
    }// end Run

税费计算由 calculateTax 方法执行,该方法将客户请求中的字段数组作为参数。系统会验证请求的有效性,若有效,则计算税费并返回给客户。

     // tax calculation
    public void calculerImpôt(String[] champs){
         // processing the application: calculation married nbEnfants salaireAnnuel
         // broken down into fields in the fields table

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

         // validity of arguments
        try{
             // at least 4 fields are required
            if(champs.length!=4) throw new Exception();
            // married
            marié=champs[1];
            if (! marié.equals("o") && ! marié.equals("n")) throw new Exception();
            // children
            nbEnfants=Integer.parseInt(champs[2]);
             // salary
            salaireAnnuel=Integer.parseInt(champs[3]);
        }catch (Exception ignored){
            // format error
            OUT.println(" syntaxe : calcul marié(O/N) nbEnfants salaireAnnuel");
             // finish
            return;
        }//if
         // tax can be calculated
        long impot=objImpots.calculer(marié.equals("o"),nbEnfants,salaireAnnuel);
         // we send the response to the customer
        OUT.println(""+impot);
    }//calculate

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

// call: serveurImpots port dsnImpots userImpots mdpImpots

import java.io.*;

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

    // main program
    public static void main (String[] args){

        // you need 4 arguments
        if(args.length != 4)
            erreur(syntaxe,1);

         // port must be integer >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);

         // we create the tax server
        try{
            new ServeurImpots(port,args[1],args[2],args[3]);
        }catch(Exception ex){
             //error
            System.out.println("L'erreur suivante s'est produite : "+ex.getMessage());
        }//catch
    }//Main

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}// end class

我们将构建 TaxServer 对象所需的数据传递给测试程序,然后由该程序创建该对象。

让我们试着第一次运行它:

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

会创建一个尚未监听客户端请求的 TaxServer 对象。只有在键盘上输入 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 客户端。一旦理解了这个应用程序,你就会发现所有 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
CmdCancel
JButton
隐藏 - 位于对话框列表下方 - 当连接正在建立时显示,若服务器无响应,可点击此按钮终止连接

可用的菜单选项如下:

选项
子选项
角色
连接
连接
连接
 
断开连接
关闭连接
 
退出
退出程序
消息
发送
将消息从 TxtSend 控件发送至服务器
 
清除跟踪
清除 LstSuivi 列表
 
ClearDialogue
清除 LstDialogue 列表
作者
 
显示版权框

8.5.1.2. 应用程序的工作原理

应用程序初始化

当应用程序的主窗口加载时,会执行以下操作:

  • 将窗口居中显示在屏幕上
  • “登录/注销”和“作者”菜单选项处于活动状态
  • “取消”按钮被隐藏
  • LstSuivi 和 LstDialogue 列表为空
连接/连接菜单

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

  • 验证端口:必须为大于 0 的整数
  • 启动一个线程以建立与服务器的连接
  • 显示“取消”按钮,允许用户中断正在进行的连接
  • “退出”和“授权”外,所有菜单选项均被禁用

连接可能以多种方式结束:

  1. 用户点击了“取消”按钮:连接线程被终止,菜单恢复到初始状态。日志中会显示用户关闭了连接。
  2. 连接因错误终止:处理方式与前文相同,此外,日志中会标注错误原因。
  3. 连接成功建立:移除“取消”按钮,日志中记录连接已建立,“重置日志”菜单启用,“连接”菜单禁用,“断开连接”菜单启用
连接/断开菜单

此选项仅在已连接到服务器时可用。启用后,它将关闭与服务器的连接,并将菜单重置为初始状态。日志中会显示连接是由客户端关闭的。

连接/退出菜单

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

消息/发送菜单

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

  • 已与服务器建立连接

  • 有待发送的消息

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

RazSuivi 和 RazDialogue 菜单

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

“取消”按钮

此按钮位于表单底部,仅在客户端尝试连接服务器时显示。连接失败可能是因为服务器未响应或响应异常。此时,“取消”按钮允许用户选择中止连接请求。

跟踪列表

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

  • 客户端建立连接

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

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

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

“作者”选项

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

Image

错误处理

连接错误会在跟踪列表 6 中报告,而与客户端/服务器对话相关的错误则会在对话列表 7 中报告。如果发生连接错误,客户端/服务器对话将被关闭,表单将重置为初始状态,准备建立新的连接。

8.5.1.3. 任务

将上述工作以两种形式实现:

  • 独立应用程序
  • 小程序

8.5.2. 练习 2 - 资源服务器

8.5.2.1. 简介

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

这种架构具有诸多优势:

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

尽管机器性能强大,但有时一次计算仍需数小时:此时服务器将无法为其他客户端提供服务。这给客户端带来了寻找可用计算服务器的难题。为解决此问题,引入了“计算资源管理器”(以下简称 GRC 服务器)。该服务运行于单台机器上,并通过 TCP 模式的 864 端口进行操作。 寻求访问计算服务器的客户端会联系该服务。GRC服务器维护着完整的计算服务器列表,会响应客户端并发送当前空闲服务器的名称。随后,客户端只需将数据发送至指定的服务器即可。

我们提议编写 GRC 服务器。

8.5.2.2. 可视化界面

视觉界面将如下所示:

Image

该界面显示两份服务器列表:

  • 左侧为非活动服务器列表,这些服务器因此可用于计算
  • 右侧是正在被客户端计算占用的服务器列表。

菜单结构如下:

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

表单上控件的结构如下:

名称
类型
角色
listLibres
JList
可用服务器列表
listBusy
JList
繁忙服务器列表

8.5.2.3. 应用工作原理

正在加载应用程序

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

服务/启动选项

此选项

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

此选项将停止服务:

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

应用程序退出。

客户端/服务器通信

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

  • 1-getserver

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

随后,GRC 服务器从其可用服务器列表中选取第一个服务器,并以以下格式将其名称返回给客户端:
        100-nom du serveur
此外,它会将分配给该客户端的服务器移至已占用服务器列表中,格式如下:
        serveur (IP du client)
如下例所示,其中服务器 *calcul1.istia.univ-angers.fr* 正忙于为 IP 地址为 *193.52.43.5* 的客户端提供服务:

Image

如果客户端已被分配了一台计算服务器,则无法发送 **getserver** 命令。因此,在响应客户端之前,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
并将分配给该客户端的计算服务器移至空闲服务器列表中。以之前的示例为例,如果客户端发送**服务**结束命令,GRC服务器的显示将变为:

Image

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

8.5.2.4. 任务

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

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

8.5.3.1. 简介

在此,我们将构建一个用于 SMTP(简单邮件传输协议)服务的客户端,该协议允许您发送电子邮件。 在 Unix 或 Windows 系统中,telnet 程序是一个基于 TCP 协议运行的客户端。它可以与任何接受以 RCLF 序列(即 ASCII 字符 13 和 10)结尾的文本命令的 TCP 服务进行“通信”。以下是一个与 SMTP 服务进行通信以发送电子邮件的示例:


$ telnet istia.univ-angers.fr 25        // appel du service 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*** **主机\_服务 端口\_服务**

客户端/服务器之间的通信使用以 RCLF 序列结尾的文本行。

来自 SMTP 服务的响应采用以下形式:

**消息编号 或**

**Message number**

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

大于或等于 500 的数字表示错误消息。

// 注释结束

help                        // commande émise au clavier

// 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    // nouvelle commande émise au clavier

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

mail 命令的语法如下:

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

// 注释结束

// SMTP 服务器的响应

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

// 注释

SMTP 服务器不会验证发件人地址的有效性:它会直接接受提供的地址

// 注释结束


rcpt to: user1@istia.univ-angers.fr        // nouvelle commande émise au clavier

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

rcpt 命令的语法如下:

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

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

你可以任意数量的命令发出 rcpt 指令:这允许你向多人发送消息。

// 注释结束

// SMTP 服务器响应

250 user1@istia.univ-angers.fr... Recipient ok
data                        // nouvelle commande émise au clavier

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

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                            // nouvelle commande émise au clavier

// 注释

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

// 注释结束

// 来自 SMTP 服务器的响应

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

8.5.3.2. 可视化界面

我们建议构建一个具有以下视觉界面的程序:

Image

这些控件具有以下功能:

数字
类型
角色
1
JTextField
以逗号分隔的电子邮件地址列表
2
JTextField
邮件主题文本
3
JTextField
用逗号分隔的电子邮件地址列表
4
JTextField
以逗号分隔的电子邮件地址列表
5
JTextArea
消息正文
6
JList
跟踪列表
7
JList
对话框列表
8
JButton
“取消”按钮未显示,当客户端请求连接到 SMTP 服务器时才会出现。如果服务器没有响应,用户可以取消此请求。

8.5.3.3. 菜单

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

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

8.5.3.4. 应用程序的工作原理

“选项/配置”菜单

此菜单将显示以下窗口:

Image

两个字段都必须填写,"确定"按钮才会生效。这两项信息必须存储在全局变量中,以便其他模块可以使用。

邮件/发送菜单

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

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

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

  • 表单将进入一种状态,其中所有可能干扰客户端/服务器对话的操作均被禁用
  • 在配置中指定的服务器 25 端口上建立连接
  • 随后,客户端将根据上述协议与 SMTP 服务器进行通信
  • “Mail From”字段使用配置中指定的发件人电子邮件地址
  • 对于字段 1、3 和 4 中找到的每个电子邮件地址,都会使用“rcpt to:”命令
  • data 命令之后发送的行中,将出现以下文本:
    • a Subject: 行:来自控制 2 的主题文本
    • a Cc:来自测试 3 的地址
    • a 密件副本 (Bcc) 行:来自测试 4 的地址
    • 控制组 5 的邮件正文
    • 结尾
“取消”按钮

表单底部的此按钮仅在客户端尝试连接到 SMTP 服务器时才会显示。连接失败可能是因为 SMTP 服务器未响应或响应异常。此时,“取消”按钮允许用户中止连接请求。

跟踪列表

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

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

列表 (7) 记录了客户端与服务器之间建立的 SMTP 对话。

这两个列表与以下菜单选项相关联:

隐藏跟踪
隐藏跟踪列表 6 及其上方的标签。如果这两个控件占用的高度为 H,则位于其下方的所有控件将向上移动 H 的高度,且表单的总尺寸将减少 H。此外,“隐藏跟踪”还会隐藏下方的“清除跟踪”选项。
清除跟踪
清除跟踪列表 6
隐藏对话框
隐藏对话框列表 7、其上方的标签以及下方的“清除对话框”菜单选项。与“隐藏跟踪”类似,下方控件(例如“取消”按钮)的位置将重新计算,且窗口大小会相应缩小。
清除对话框
清除对话框列表 7
“作者”选项

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

Image

错误处理

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

8.5.3.5. 管理配置文件

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

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

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

程序加载时,若 sendmail.ini 文件存在,则读取该文件并据此配置表单。若 sendmail.ini 文件不存在,程序将按其存在的情况进行处理:

SmtpServer=
ReplyAddress=
Suivi=1
Dialogue=1

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

所有不符合以下模式的行:

    mot clé= valeur

的行均会被忽略,关键词无效的行同样会被忽略。关键词可大写或小写:不分大小写。

“选项/配置”菜单中,会显示当前的 SmtpServerReplyAddress 值。用户可根据需要进行修改。

8.5.3.6. 待执行任务

完成上述任务。建议将配置文件管理留到最后处理。

8.5.4. 练习 4 - POPPASS 客户端

8.5.4.1. 简介

我们建议创建一个能够与运行在 106 端口的 POPPASSD 服务器进行通信的 TCP 客户端。该服务允许您在 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
登录文本框
JTextField
用户登录
3
txtPassword
JTextField
用户密码
4
txtNewPassword
JTextField
用户的新密码
5
txtConfirmation
JTextField
新密码确认
6
lstTracking
JList
连接跟踪消息
7
lstDialogue
JList
客户端/服务器对话消息
10
cmdCancel
JButton
未显示 - 连接服务器时出现的按钮。允许您停止连接。

8.5.4.3. 菜单

标题
控件名称
角色
连接
登录菜单
 
连接
mnuconnect
启动与服务器的连接
退出
mnuQuitter
退出应用程序
消息
mnuMessages
 
清除历史记录
mnuClearTracking
清除 lstSuivi 列表
ClearDialog
mnuClearDialog
清除 lstDialogue 列表
作者
mnuAuthor
显示版权框

8.5.4.4. 应用程序的工作原理

应用程序初始化

当应用程序的主视图加载时,会执行以下操作:

  • 将表单居中显示在屏幕上
  • 仅“登录/注销”和“授权”菜单选项处于活动状态
  • “取消”按钮被隐藏
  • “LstSuivi”和“LstDialogue”列表为空
登录/连接菜单

仅当第 1 至第 5 字段已填写时,此选项才可用。点击此选项将触发以下操作:

  • 将启动一个线程以建立与服务器的连接
  • 显示“取消”按钮,允许用户中断正在进行的连接
  • “退出”和“作者”外,所有菜单选项均被禁用

随后事件的顺序如下:

  1. 用户点击了“取消”按钮:连接线程被停止,菜单恢复到初始状态。日志显示用户已关闭连接。
  2. 服务器接受了连接请求。随后,我们与服务器建立对话以更改密码。该对话中的通信记录保存在 LstDialogue 列表中。对话完成后,与服务器的连接将关闭,表单菜单将恢复到初始状态。
  3. 只要对话处于活动状态,取消按钮就会保持可见,以便用户在需要时关闭连接。
  4. 若通信过程中发生任何错误,连接将被关闭,且错误原因将显示在 LstSuivi 跟踪列表中。
连接/退出菜单

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

“清除跟踪”和“清除对话框”菜单

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

“取消”按钮

此按钮位于表单底部,仅在客户端正在连接或已连接到服务器时显示。用户可通过“取消”按钮终止与服务器的通信。

跟踪列表

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

  • 客户端建立连接

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

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

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

“作者”选项

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

Image

错误处理

通信错误会在跟踪列表 6 中报告,而与客户端/服务器对话相关的错误则会在对话列表 7 中报告。如果发生连接错误,客户端/服务器对话将被关闭,表单将重置为初始状态,准备建立新的连接。

8.5.4.5. 任务

将上述工作实现为独立应用程序,然后实现为小程序。