Java编程思想第4版[中文版](PDF格式)-第138部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。
(2) 修改Counter2。java ,使线程成为一个内部类,而且不需要明确保存指向Counter2 的一个。
(3) 修改Sharing2。java,在TwoCounter 的run()方法内部添加一个synchronized (同步)块,而不是同步
整个run()方法。
535
…………………………………………………………Page 537……………………………………………………………
(4) 创建两个Thread 子类,第一个的run()方法用于最开始的启动,并捕获第二个Thread 对象的句柄,然
后调用wait()。第二个类的run()应在过几秒后为第一个线程调用modifyAll(),使第一个线程能打印出一
条消息。
(5) 在Ticker2 内的Counter5。java 中,删除yield(),并解释一下结果。用一个sleep()换掉yield(),再
解释一下结果。
(6) 在ThreadGroup1。java 中,将对sys。suspend()的调用换成对线程组的一个wait()调用,令其等候2 秒
钟。为了保证获得正确的结果,必须在一个同步块内取得 sys 的对象锁。
(7) 修改Daemons。java,使main()有一个 sleep(),而不是一个readLine()。实验不同的睡眠时间,看看会
有什么发生。
(8) 到第7 章(中间部分)找到那个GreenhouseControls。java 例子,它应该由三个文件构成。在
Event。java 中,Event 类建立在对时间的监视基础上。修改这个 Event,使其成为一个线程。然后修改其余
的设计,使它们能与新的、以线程为基础的Event 正常协作。
536
…………………………………………………………Page 538……………………………………………………………
第 15 章 网络编程
历史上的网络编程都倾向于困难、复杂,而且极易出错。
程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识。一般地,我们需要理解连网协议
中不同的“层”(Layer)。而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接、
打包和拆包;这些块的来回运输;以及握手等等。这是一项令人痛苦的工作。
但是,连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息,并把它们移到这儿;或者
相反。这与读写文件非常相似,只是文件存在于远程机器上,而且远程机器有权决定如何处理我们请求或者
发送的数据。
Java 最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏
在JVM 以及Java 的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接
(一个“套接字”)已被封装到系统对象里,所以可象对其他数据流那样采用同样的方法调用。除此以外,
在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java 内建的多线程机制也是十分方便
的。
本章将用一系列易懂的例子解释Java 的连网支持。
15。1 机器的标识
当然,为了分辨来自别处的一台机器,以及为了保证自己连接的是希望的那台机器,必须有一种机制能独一
无二地标识出网络内的每台机器。早期网络只解决了如何在本地网络环境中为机器提供唯一的名字。但 Java
面向的是整个因特网,这要求用一种机制对来自世界各地的机器进行标识。为达到这个目的,我们采用了 IP
(互联网地址)的概念。IP 以两种形式存在着:
(1) 大家最熟悉的DNS (域名服务)形式。我自己的域名是bruceeckel。。所以假定我在自己的域内有一
台名为Opus 的计算机,它的域名就可以是 Opus。bruceeckel。。这正是大家向其他人发送电子函件时采用
的名字,而且通常集成到一个万维网(WWW)地址里。
(2) 此外,亦可采用“四点”格式,亦即由点号(。)分隔的四组数字,比如202。98。32。111 。
不管哪种情况,IP地址在内部都表达成一个由32 个二进制位(bit)构成的数字(注释①),所以IP地址
的每一组数字都不能超过255。利用由java 提供的 static InetAddress。getByName(),我们可以让一个
特定的 Java 对象表达上述任何一种形式的数字。结果是类型为InetAddress 的一个对象,可用它构成一个
“套接字”(Socket),大家在后面会见到这一点。
①:这意味着最多只能得到40 亿左右的数字组合,全世界的人很快就会把它用光。但根据目前正在研究的新
IP编址方案,它将采用 128 bit 的数字,这样得到的唯一性IP地址也许在几百年的时间里都不会用完。
作为运用 InetAddress。getByName()一个简单的例子,请考虑假设自己有一家拨号连接因特网服务提供者
(ISP),那么会发生什么情况。每次拨号连接的时候,都会分配得到一个临时 IP地址。但在连接期间,那
个 IP 地址拥有与因特网上其他 IP 地址一样的有效性。如果有人按照你的 IP地址连接你的机器,他们就有可
能使用在你机器上运行的Web 或者 FTP 服务器程序。当然这有个前提,对方必须准确地知道你目前分配到的
IP。由于每次拨号连接获得的IP 都是随机的,怎样才能准确地掌握你的IP 呢?
下面这个程序利用 InetAddress。getByName()来产生你的 IP 地址。为了让它运行起来,事先必须知道计算机
的名字。该程序只在Windows 95 中进行了测试,但大家可以依次进入自己的 “开始”、“设置”、“控制面
板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。
//: WhoAmI。java
// Finds out your network address when you're
// connected to the Internet。
package c15;
import java。*;
public class WhoAmI {
public static void main(String'' args)
537
…………………………………………………………Page 539……………………………………………………………
throws Exception {
if(args。length != 1) {
System。err。println(
〃Usage: WhoAmI MachineName〃);
System。exit(1);
}
InetAddress a =
InetAddress。getByName(args'0');
System。out。println(a);
}
} ///:~
就我自己的情况来说,机器的名字叫作“Colossus”(来自同名电影,“巨人”的意思。我在这台机器上有
一个很大的硬盘)。所以一旦连通我的ISP,就象下面这样执行程序:
java whoAmI Colossus
得到的结果象下面这个样子(当然,这个地址可能每次都是不同的):
Colossus/202。98。41。151
假如我把这个地址告诉一位朋友,他就可以立即登录到我的个人Web 服务器,只需指定目标地址
http://202。98。41。151 即可(当然,我此时不能断线)。有些时候,这是向其他人发送信息或者在自己的
Web 站点正式出台以前进行测试的一种方便手段。
15。1。1 服务器和客户机
网络最基本的精神就是让两台机器连接到一起,并相互“交谈”或者“沟通”。一旦两台机器都发现了对
方,就可以展开一次令人愉快的双向对话。但它们怎样才能“发现”对方呢?这就象在游乐园里那样:一台
机器不得不停留在一个地方,侦听其他机器说:“嘿,你在哪里呢?”
“停留在一个地方”的机器叫作“服务器”(Server);到处“找人”的机器则叫作“客户机”(Client)
或者“客户”。它们之间的区别只有在客户机试图同服务器连接的时候才显得非常明显。一旦连通,就变成
了一种双向通信,谁来扮演服务器或者客户机便显得不那么重要了。
所以服务器的主要任务是侦听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户机的任务
是试着与一台服务器建立连接,这是由我们创建的特定客户机对象完成的。一旦连接建好,那么无论在服务
器端还是客户机端,连接只是魔术般地变成了一个 IO数据流对象。从这时开始,我们可以象读写一个普通的
文件那样对待连接。所以一旦建好连接,我们只需象第 10章那样使用自己熟悉的 IO 命令即可。这正是 Java
连网最方便的一个地方。
1。 在没有网络的前提下测试程序
由于多种潜在的原因,我们可能没有一台客户机、服务器以及一个网络来测试自己做好的程序。我们也许是
在一个课堂环境中进行练习,或者写出的是一个不十分可靠的网络应用,还能拿到网络上去。IP 的设计者注
意到了这个问题,并建立了一个特殊的地址——localhost——来满足非网络环境中的测试要求。在 Java 中
产生这个地址最一般的做法是:
InetAddress addr = InetAddress。getByName(null);
如果向 getByName()传递一个null (空)值,就默认为使用localhost。我们用InetAddress 对特定的机器
进行索引,而且必须在进行进一步的操作之前得到这个 InetAddress (互联网地址)。我们不可以操纵一个
InetAddress 的内容(但可把它打印出来,就象下一个例子要演示的那样)。创建 InetAddress 的唯一途径
就是那个类的static (静态)成员方法getByName() (这是最常用的)、getAllByName()或者
getLocalHost()。
为得到本地主机地址,亦可向其直接传递字串〃localhost〃:
InetAddress。getByName(〃localhost〃);
或者使用它的保留 IP 地址(四点形式),就象下面这样:
InetAddress。getByName(〃127。0。0。1〃);
这三种方法得到的结果是一样的。
538
…………………………………………………………Page 540……………………………………………………………
15。1。2 端口:机器内独一无二的场所
有些时候,一个 IP地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中,往往运行着多个服
务器(程序)。由 IP 表达的每台机器也包含了“端口”(Port )。我们设置一个客户机或者服务器的时候,
必须选择一个无论客户机还是服务器都认可连接的端口。就象我们去拜会某人时,IP 地址是他居住的房子,
而端口是他在的那个房间。
注意端口并不是机器上一个物理上存在的场所,而是一种软件抽象(主要是为了表述的方便)。客户程序知
道如何通过机器的 IP 地址同它连接,但怎样才能同自己真正需要的那种服务连接呢(一般每个端口都运行着
一种服务,一台机器可能提供了多种服务,比如HTTP 和 FTP 等等)?端口编号在这里扮演了重要的角色,它
是必需的一种二级定址措施。也就是说,我们请求一个特定的端口,便相当于请求与那个端口编号关联的服
务。“报时”便是服务的一个典型例子。通常,每个服务都同一台特定服务器机器上的一个独一无二的端口
编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。
系统服务保留了使用端口 1 到端口 1024 的权力,所以不应让自己设计的服务占用这些以及其他任何已知正在
使用的端口。本书的第一个例子将使用端口8080 (为追忆我的第一台机器使用的老式8 位 Intel 8080芯
片,那是一部使用CP/M 操作系统的机子)。
15。2 套接字
“套接字”或者“插座”(Socket)也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。
针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的
每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知
的。抽象的基本宗旨是让我们尽可能不必知道那些细节。
在Java 中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个 InputStream 以
及OutputStream (若使用恰当的转换器,则分别是Reader 和 Writer),以便将连接作为一个IO流对象对
待。有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;以及Socket,客户用
它初始一次连接。一旦客户(程序)申请建立一个套接字连接,ServerSocket 就会返回(通过accept()方
法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字”
连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用 getInputStream()以及
getOutputStream()从每个套接字产生对应的 InputStream 和OutputStream 对象。这些数据流必须封装到缓
冲区内。可按第 10章介绍的方法对类进行格式化,就象对待其他任何流对象那样。
对于Java 库的命名机制,ServerSocket (服务