友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第94部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
…………………………………………………………Page 546……………………………………………………………
} ///:~
每次有新客户请求建立一个连接时,ServeOneJabber 线程都会取得由accept()在 main() 中生成的Socket 对
象。然后和往常一样,它创建一个 BufferedReader,并用Socket 自动刷新PrintWriter 对象。最后,它调
用Thread 的特殊方法 start(),令其进行线程的初始化,然后调用run()。这里采取的操作与前例是一样
的:从套扫字读入某些东西,然后把它原样反馈回去,直到遇到一个特殊的〃END〃结束标志为止。
同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在ServeOneJabber 外部创建
的,所以清除工作可以“共享”。若ServeOneJabber 构建器失败,那么只需向调用者“掷”出一个违例即
可,然后由调用者负责线程的清除。但假如构建器成功,那么必须由 ServeOneJabber 对象负责线程的清除,
这是在它的 run()里进行的。
请注意MultiJabberServer 有多么简单。和以前一样,我们创建一个 ServerSocket,并调用accept()允许一
个新连接的建立。但这一次,accept() 的返回值(一个套接字)将传递给用于ServeOneJabber 的构建器,由
它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。
如果ServerSocket 创建失败,则再一次通过 main()掷出违例。如果成功,则位于外层的 try…finally代码
块可以担保正确的清除。位于内层的try…catch 块只负责防范 ServeOneJabber 构建器的失败;若构建器成
功,则 ServeOneJabber 线程会将对应的套接字关掉。
为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的
服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许
创建的线程的最大数量是由final int maxthreads 决定的。大家会注意到这个值非常关键,因为假如把它设
得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。
//: MultiJabberClient。java
// Client that tests the MultiJabberServer
// by starting up multiple clients。
import java。*;
import java。io。*;
class JabberClientThread extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0;
private int id = counter++;
private static int threadcount = 0;
public static int threadCount() {
return threadcount;
}
public JabberClientThread(InetAddress addr) {
System。out。println(〃Making client 〃 + id);
threadcount++;
try {
socket =
new Socket(addr; MultiJabberServer。PORT);
} catch(IOException e) {
// If the creation of the socket fails;
// nothing needs to be cleaned up。
}
try {
in =
new BufferedReader(
new InputStreamReader(
socket。getInputStream()));
545
…………………………………………………………Page 547……………………………………………………………
// Enable auto…flush:
out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket。getOutputStream())); true);
start();
} catch(IOException e) {
// The socket should be closed on any
// failures other than the socket
// constructor:
try {
socket。close();
} catch(IOException e2) {}
}
// Otherwise the socket will be closed by
// the run() method of the thread。
}
public void run() {
try {
for(int i = 0; i 《 25; i++) {
out。println(〃Client 〃 + id + 〃: 〃 + i);
String str = in。readLine();
System。out。println(str);
}
out。println(〃END〃);
} catch(IOException e) {
} finally {
// Always close it:
try {
socket。close();
} catch(IOExcept ion e) {}
threadcount…; // Ending this thread
}
}
}
public class MultiJabberClient {
static final int MAX_THREADS = 40;
public static void main(String'' args)
throws IOException; InterruptedException {
InetAddress addr =
InetAddress。getByName(null);
while(true) {
if(JabberClientThread。threadCount()
《 MAX_THREADS)
new JabberClientThread(addr);
Thread。currentThread()。sleep(100);
}
}
} ///:~
546
…………………………………………………………Page 548……………………………………………………………
JabberClientThread 构建器获取一个 InetAddress,并用它打开一个套接字。大家可能已看出了这样的一个
套路:Socket 肯定用于创建某种 Reader 以及/或者Writer (或者InputStream和/或 OutputStream)对
象,这是运用Socket 的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重
复的代码编写工作)。同样地,start()执行线程的初始化,并调用run()。在这里,消息发送给服务器,而
来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接
字创建好以后,但在构建器完成之前,假若构建器失败,套接字会被清除。否则,为套接字调用 close()的
责任便落到了run()方法的头上。
threadcount跟踪计算目前存在的 JabberClientThread 对象的数量。它将作为构建器的一部分增值,并在
run()退出时减值(run()退出意味着线程中止)。在MultiJabberClient。main()中,大家可以看到线程的数
量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程
最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS,看看对于你使用的系
统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。
15。4 数据报
大家迄今看到的例子使用的都是“传输控制协议”(TCP),亦称作“基于数据流的套接字”。根据该协议的
设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地。换言之,它允许重传那些由于各种原因
半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然,这种控制与可靠性需要我们付
出一些代价:TCP 具有非常高的开销。
还有另一种协议,名为“用户数据报协议”(UDP),它并不刻意追求数据包会完全发送出去,也不能担保它
们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”(TCP 当然是“可靠协议”)。听起来
似乎很糟,但由于它的速度快得多,所以经常还是有用武之地的。对某些应用来说,比如声音信号的传输,
如果少量数据包在半路上丢失了,那么用不着太在意,因为传输的速度显得更重要一些。大多数互联网游
戏,如Diablo,采用的也是UDP 协议通信,因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想
一台报时服务器,如果某条消息丢失了,那么也真的不必过份紧张。另外,有些应用也许能向服务器传回一
条UDP 消息,以便以后能够恢复。如果在适当的时间里没有响应,消息就会丢失。
Java 对数据报的支持与它对 TCP 套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在
客户和服务器程序都可以放置一个 DatagramSocket (数据报套接字),但与ServerSocket 不同,前者不会
干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一
项本质的区别的是对TCP 套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过
会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味
着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。
DatagramSocket 用于收发数据包,而DatagramPacket 包含了具体的信息。准备接收一个数据报时,只需提
供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过 DatagramSocket,作为信息起源地的因特网地
址以及端口编号会自动得到初化。所以一个用于接收数据报的 DatagramPacket 构建器是:
DatagramPacket(buf; buf。length)
其中,buf 是一个字节数组。既然 buf 是个数组,大家可能会奇怪为什么构建器自己不能调查出数组的长度
呢?实际上我也有同感,唯一能猜到的原因就是C 风格的编程使然,那里的数组不能自己告诉我们它有多
大。
可以重复使用数据报的接收代码,不必每次都建一个新的。每次用它的时候(再生),缓冲区内的数据都会
被覆盖。
缓冲区的最大容量仅受限于允许的数据报包大小,这个限制位于比64KB 稍小的地方。但在许多应用程序中,
我们都宁愿它变得还要小一些,特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要
求。
发出一个数据报时,DatagramPacket 不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它
的目的地。所以用于输出DatagramPacket 的构建器是:
DatagramPacket(buf; length; inetAddress; port)
这一次,buf (一个字节数组)已经包含了我们想发出的数据。length可以是 buf 的长度,但也可以更短一
些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一
个目标端口(注释②)。
②:我们认为TCP 和 UDP 端口是相互独立的。也就是说,可以在端口8080 同时运行一个TCP 和 UDP 服务程
序,两者之间不会产生冲突。
547
…………………………………………………………Page 549……………………………………………………………
大家也许认为两个构建器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的
面向对象的设计方案,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决
于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,DatagramPacket 的使用相当简单,我
们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对 TCP 套接字的
MultiJabberServer 和MultiJabberClient 例子。多个客户都会将数据报发给服务器,后者会将其反馈回最
初发出消息的同样的客户。
为简化从一个String 里创建 DatagramPacket 的工作(或者从DatagramPacket 里创建 String),这个例子
首先用到了一个工具类,名为Dgram:
//: Dgram。java
// A utility class to convert back and forth
// Between Strings and DataGramPackets。
import java。*;
public class Dgram {
public static DatagramPacket toDatagram(
String s; InetAddress destIA; int destPort) {
// Deprecated in Java 1。1; but it works:
byte'' buf = new byte's。length() + 1';
s。getBytes(0; s。length(); buf; 0);
// The correct Java 1。1 approach; but it's
// Broken (it truncates the String):
// byte'' buf = s。getBytes();
return new DatagramPacket(buf; buf。length;
destIA; destPort);
}
public static String toString(DatagramPacket p){
// The Java 1。0 approach:
// return new String(p。getData();
// 0; 0; p。getLength());
// The Java 1。1 approach:
return
new String(p。getData(); 0; p。getLength());
}
} ///:~
Dgram 的第一个方法采用一个String、一个 InetAddress 以及一个端口号作为自己的参数,将String 的内容
复制到一个字节缓冲区,再将缓冲区传递进入 DatagramPacket 构建器,从而构建一个 DatagramPacket。注
意缓冲区分配时的〃+1〃——这对防止截尾现象是非常重要的。String 的getByte()方法属于一种特殊操作,
能将一个字串包含的char 复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1。1 有一个“更
好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉String 的部分内容。所以尽管我
们在Java 1。1 下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你
读到这里的时候修正了)。
Dgram。toString()方法同时展示了Java 1。0 的方法和Java 1。1 的方法(两者是不同的,因为有一种新类型
的String 构建器)。
下面是用于数据报演示的服务器代码:
//: ChatterServer。java
// A server that echoes datagrams
import java。*;
import java。io。*;
import java。util。*;
548
…………………………………………………………Page 550……………………………………………………………
public class ChatterServer {
static final int INPORT = 1711;
private byte'' buf = new byte'1000';
private DatagramPacket dp =
new DatagramPacket(buf; buf。length);
// Can listen & send on the same socket:
private DatagramSocket socket;
public ChatterServer() {
try {
socket = new DatagramSocket(INPORT);
System。out。println(〃Server started〃);
while(true) {
// Block until a datagram appears:
socket。receive(dp);
String rcvd = Dgram。toString(dp) +
〃; from address: 〃 + dp。getAddress() +
〃; port: 〃 + dp。getPort();
System。out。println(rcvd);
String echoString =
〃Echoed: 〃 + rcvd;
// Extract the address and port from the
// received datagram to find out where to
// send it back:
DatagramPacket echo =
Dgram。toDatagram(echoString;
dp。getAddress(); dp。getPort());
socket。send(echo);
}
} catch(SocketException e) {
System。err。println(〃Can't open socket〃);
System。exit(1);
} catch(IOException e) {
System。err。println(〃munication error〃);
e。printStackTrace();
}
}
public static void main(String'' args) {
new ChatterServer();
}
} ///:~
ChatterServer 创建了一个用来接收消息的DatagramSocket (数据报套接字),而不是在我们每次准备接收
一条新消息时都新建一个。这个单一的DatagramSocket 可以重复使用。它有一个端口号,因为这属于服务
器,客户必须确切知道自己把数据报发到哪个地址。尽管有一个端口号,但没有为它分配因特网地址,因为
它就驻留在“这”台机器内,所以知道自己的因特网地址是什么(目前是默认的 localhost)。在无限while
循环中,套接字被告知接收数据(receive())。然后暂时挂起,直到一个数据报出现,再把它反馈回我们希
望的接收人——DatagramPacket dp——里面。数据包(Packet)会被转换成一个字串,同时插入的还有数据
包的起源因特网地址及套接字。这些信息会显示出来,然后添加一个额外的字串,指出自己已从服务器反馈
回来了。
大家可能会觉得有点儿迷惑。正如大家会看到的那样,许多不同的因特网地址和端口号都可能是消息的起源
549
…………………………………………………………Page 551……………………………………………………………
地——换言之,客户程序可能驻留在任何一台机器里(就这一次演示来说,它们都驻留在 localhost 里,但
每个客户使用的端口编号是不同的)。为了将一条消息送回它真正的始发客户,需要知道那个客户的因特网
地址以及端口号。幸运的是,所有这些资料均已非常周到地封装到发出消息的DatagramPacket 内部,所以我
们要做的全部事情就是用getAddress()和getPort()把它们取出来。利用这些资料,可以构建
DatagramPacket echo——它通过与接收用的相同的套接字发送回来。除此以外,一旦套接字发出数据报,就
会添加“这”台机器的因特网地址及端口信息,所以当客户接收消息时,它可以利用 getAddress()和
getPort()了解数据报来自何处。事实上,getAddress()和 getPort()唯一不能告诉我们数据报来自何处的前
提是:我们创建一个待发送的数据报,并在正式发出之前调用了getAddress()和getPort()。到数据报正式
发送的时候,这台机器的地址以及端口才会写入数据报。所以我们得到了运用数据报时一项重要的原则:不
必跟踪一条消息的来源地!因为它肯定保存在数据报里。事实上,对程序来说,最可靠的做法是我们不要试
图跟踪,而是无论如何都从目标数据报里提取出地址以及端口信息(就象这里做的那样)。
为测试服务器的运转是否正常,下面这程序将创建大量客户(线程),它们都会将数据报包发给服务器,并
等候服务器把它们原样反馈回来。
//: ChatterClient。java
// Tests the ChatterServer by starting multiple
// clients; each of which sends datagrams。
import java。lang。Thread;
import java。*;
import java。io。*;
public class ChatterClient e
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!