|
第十一章 网络类库java.ne(上)
本章将介绍有关网络编程的基本程序和Java对网络提供的类库支持,并以实例演示类库的使用。学习了本章之后,读者应能独立开发简单的网络应用程序,并为今后设计较为复杂的网络应用程序打下基础。
11.1 概述
11.1.1 TCP/IP协议族
Java的风靡与Internet的繁荣是密不可分的。Internet这个名词正逐渐变得家喻户晓,客观上促进了Java的发展。第一章中我们曾简单介绍过Internet,这里结合我们可能的应用,进一步介绍它的一些基础知识。 Internet是一个互联网。它可以把各种不同种类的网联接在一起。无论是局域网、广域网,无论在网内部遵循什么协议,都可以纳入Internet。这就使世界各地的各种计算机能够互相联系,彼此通信。处在网络中的计算机称为主机(host)。无论是在什么样的网络中,计算机与计算机都通过通信线路相连结,构成一个纵横交错的网。在这个复杂的网中,各台主机像各个点,通过线(通信线路)彼此相联,因此,网络中的主机又可称为节点。 单有了机器和通信线路还不够,通信线路上传输的归根到底都是二进制数字串,或者说得更“低级”一些是高低电平。这些“0”或“1”如何能与我们屏幕上看到的丰富的图像、文本联系起来呢?这是一项艰巨的工作。网络软件必须能够完成从机器可识别的信息到人可识别的信息的转换。但人能识别的信息是多种多样的,总不能够每处理一种信息都从最底层“0”“1”开始转换。于是,网络的层次结构这个概念就应运而生了。网络的实现人员把网络划分为几个层次,每一层完成相对独立的工作。较高的层便用较低层提供的功能(或称“服务”)对较低层的转换结果进行转换,而不必关心较低的层的实现细节。每一层向比它更高的层提供一些接口,为高层提供服务。
网络究竟分成几层?这个问题很难回答。而且,网络产生之初并未经过协调和统一,因而不同的网络就有不同的设计。 国际标准化组织(ISO)制订了开放系统互连(Open
Systems
Interconnection)参考模型,它将网络分成七层,这可以说是较具“标准”性的一个模型(见图11.1-a)。
┌─────┐ ┌──────────┬─┬ │ 应用层 │ │各种应用层协议 │ │ ├─────┤ │(Telnet, │ │ │ 表示层 │ │FTP,SMTP等)│ ↓ ├─────┤ ├──────────┤TCP/IP位置 │ 会话层 │ │ TCP │ ↑ ├─────┤ ├──────────┤ │ │ 传输层 │ │ IP │ │ ├─────┤ ├──────────┼─┼ │ 网络层 │ │ 数据链路层 │ ↓ ├─────┤ ├ ─ ─ ─ ─ ─┤不属于TCP/IP │数据链路层│ │ 物理层 │ ↑ ├─────┤ └──────────┴─┴ │ 物理层 │ └─────┘ a
OSI七层模型 b TCP/IP 协议模型 图11.1 两种模型的对比
OSI模型确实使网络结构变得很清楚,但它却不够实用。原因之一是它与现在的网络模型有不少差异:当它产生时,不少计算机网络已颇具规模,相应的协议也已成型,要求它们转而遵循OSI模型当然不现实。原因之二是它分层过细,各个层的工作量分配又不均衡,实现起来比较复杂,不如现实的系统有效率。在Internet网上使用的TCP/IP的网络体系结构则是网络工业标准的杰出代表,是一个应用广泛的模型。它的分层与OSI模型有一定的差异,但又互相对应,各层次功能互相渗透。图11.1-b是TCP/IP网络体系结构的示意图。由图可见,它与OSI模型的大致对应关系。应当指出,这种对应关系不是绝对的,因为各层的功能很可能有一些交错。 OSI七层模型是一个理论模型,而TCP/IP协议模型更具实用价值。我们主要介绍后者。有兴趣的读者可参阅网络方面的书籍以了解其它内容。 如图11.1-b所示,TCP/IP模型将网络划分为四个层次。网络的最终用户不必了解各层的实现。比如,使用浏览器的用户只需连至需要的网址,不必知道文本传送协议HTTP的内容。但作为网络应用的设计者就不同了,需要多了解一引起协议的细节内容。下面我们来看一看TCP/IP协议族。 什么是协议?协议就是约定,就是双方为了协调地做一件事情而共同遵循的规则。打个比方来说,中国古代演义小说中常有设下酒宴骗来敌人,然后“摔杯为号,群起攻之”的故事。“摔杯为号”就是一一个协议。甲方发出请求——摔杯,乙方做出响应——进攻。网络中名繁多的协议,适用于网络层次模型中的不同层次,其实质是相同的,即约定计算机之间相互交换信息的方式、顺序,双方如何发出请求,如何作出响应。以大家所熟悉的FTP为例,客户发生一个“GET”请求,对方(服务器)发送相应文件作为响应。这些动作的规程就是在协议中加以约定的。 Internet中常用协议有很多,常被统称为TCP/IP协议族。 图11.2中以英文缩写形式写的协议都属于TCP/IP协议族,它们居于不同的层次,完成不同的工作。对它们的简单解释见表11.1。 我们在编写网络应用时,若想与标准的商业软件连接,就必须遵循协议的要求。协议的细节必须参考相应的国际标准。当然,也可以自己制订协议,让你的应用遵循自定义的协议来工作。 ┌──────┬───┬────┬────┬──┬── │Telent│FTP│HTTP│SMTP│┉┉│ ↑ ├──────┴───┴────┴────┴──┤应用层 │ 套接字(Socket) │ ↓ ├───────────────────────┼── │ TCP/UDP │传输层 ├───────────────────────┼── │ IP/ICMP │网络互联层 ├───────────────────────┼── │ 数据链路层与物理层 │低层 └───────────────────────┴── 图11.2 TCP/IP协议族层次图
表11.1 TCP/IP协议族主要成员
|
协 议 |
简 称 |
功 能 |
| 文件传输协议 |
FTP(File Transfer Protocol) |
处理网络上文件的传送 |
| 超文本传送协议 |
HTTP(Hypertext Transport Protocol) |
传送超文本文档及多媒体信息 |
| 简单邮件传输协议 |
SMTP(Simple Mail Transmission Protocol) |
简单的网络邮件传输 |
| 传输控制协议 |
TCP(Transport Control Protocol) |
实现传输层面向连接的数据交换(在IP层的基础上实现) |
| 用户数据报协议 |
UCP(User Datagram Protocol) |
用于实现无连接的数据报通信(参见11.4) |
| 网络互联协议 |
IP(Internet Protocol) |
实现网络互联层的信息传递。该层通信是面向无连接的 |
| 互联网络控制报文协议 |
ICMP(Internet Control Message Protocol) |
用以交换互联网络的状态信息或异常信息 |
11.12 Java的网络类库支持
Java提供了如下几方面的类来帮助用户完成网络操作: ■网络地址转换。完成域名与IP地址的转换。 ■面向连接的通信。为服务器方与客户机方程序提供类库支持。 ■面向无连接的通信。提供数据报方式通信所需的类库支持。 ■Web连接。为浏览器——服务器模式的较高层次的连接提供类库支持。
11.1.3 软硬件要求
这一章涉及网络编程,因此对计算机的软件硬件要求较前几章更高。这个要求就是机器应能上网。本章中的大部分例子要求机器拥有IP地址。例11.8(ContentHandlerDemo,见11.6健保突С绦蛴敕衿鞒绦蚪崾?br>
例11.2 ServerDemo.java 1: import java.io.*; 2: import
java.net.*; 3: public class ServerDemo{ 4: public static
void main(String[] args){ 5: try{ //建立服务器套接字 6:
ServerSocket server=new
ServerSocket(2000); //本地端口 7: int localPort =
server.getLocalPort(); 8: System.out.println("Server is
listening..."); 9: System.out.println("Client should connect
to port "+localPort); //监听并接受请求 10: Socket
client=server.accept(); //打印被接受的客户机的主机名 11: System.out.println("Client
"+client.getInetAddress().getHostName()+" is
accepted."); //建立输入输出,以务数据交换 BufferedReader
bufReader=new BufferedReader(new
InputStreamReader(client.getInputStream())); PrintWriter
prtWriter=new
PrintWriter(client.getOutputStream()); System.out.println("Client
port
"+client.getPort()); //发出欢迎信息 prtWriter.println("
Welcome! You can say a line of words before
disconnect."); prtWriter.flush(); System.out.println("Greeting
message has been sent."); System.out.println("Client
says:"); //读取客户机发来的信息并显示 System.out.println(bufReader.readLine()); prtWriter.println("Bye."); prtWriter.flush(); bufReader.close(); prtWriter.close(); }catch(IOException
ex){ System.out.println("IO exception
occured!"); } } }
在这个例子中客户机一端的程序是这样的(例11.3)。 例11.3 ClientDemo.java
import java.net.*; import java.io.*; public class
ClientDemo{ public static void main(String[]
args){ //判断命令行参数是否符合要求 if(args.length !=
2){ System.out.println("Usage:java ClientDemo
<hostname>
<port>"); return; } try{ //建立套接字 Socket
client=new
Socket(args[0],Integer.valueOf(args[1]).intValue()); //输出远程主机的名字 System.out.println("
Destination - " +
client.getInetAddress().getHostName()); //输出本地端口 System.out.println("Local
port -
"+client.getLocalPort()); //创建输入输出流 BufferedReader
bufReader= new BufferedReader(new
InputStreamReader(client.getInputStream())); BufferedReader
kbdReader= new BufferedReader(new
InputStreamReader(System.in)); PrintWriter prtWriter=new
PrintWriter(client.getOutputStream()); //输出从服务器发业的信息 System.out.println(bufReader.readLine()); //向套接字写入从键盘得到的信息 prtWriter.println(kbdReader.readLine()); prtWriter.flush(); //输出从服务器发来的信息 System.out.println("Server
Says: "+
bufReader.readLine()); System.out.println("Disconnect"); //关闭连接 client.close(); }catch(UnknownHostException
ex){ System.out.println("Cannot connect destination
host."); }catch(IOException
ex){ System.out.println("IO exception
occured."); } } } 3.程序分析 通过注释,读者可能已经明白了程序的结构和功能。下面我们结合例子的输出再解释一下。 服务器程序是首先被运行的。ServerDemo.java的第6行创建一个服务器套接字。第7行的getLocalPort()返回ServerSocket对象所监听的端口号。 第8、9行输出提示信息。至此(客户程序尚未运行),屏幕显示如后面给出的例11.3输出结果所示。 Server
is listening... Client should connect to port
2000 然后程序进行等待状态。 在另一个DOS窗口中启动程序ClientDemo。命令行参数有两上,一个是服务器程序所在主机名,一个是目的端口号。如果有条件,客户与服务程序可分置于两台机器上运行。但如果没有这样条件,用两个DOS窗口也是一样的。演示本例所用的机器,域名为huang.nju.edu.cn,IP地址为202.119.32.17。因此第一个命令行参数使用202.119.32.17。在服务器程序中,我们设定监听端号为2000,因此第二个参数使用2000。 程序启动后,根据命令行参数建立一个客户套接字: Socket
client=new
Socket(args[0],Integer.valueOf(args[1]).intValue()); 客户套接字创建之后,不妨回过头来看看服务器程序。ServerDemo.java的第10行接受连接请求,返回一个套接字对象client。 有了client对象,我们可以了解一下客户的情况。在现实中服务器程序中监控功能是很重要的,我们这里也实施简单的监督,看看到底接受了谁的连接请求。于是,服务器方的屏幕上显示: Clinet
huang.nju.edu.cn is
accepted. 第15行又用了Socket的方法getPort()获取远程端口号。因此服务器又显示: Client
Port
1065 客户一方也进行了类似的工作,显示了服务器方主机的名字和客户机自己的端号: Destination——huang Local
port——1065 读者可能对主机名打出了202.119.32.17有所疑问。这里“202.119.32.17”是一个字符串,与域名同样对待。读者可以用域名来代替IP地址运行客户程序,则这里就显示域名了。如若用 java
ClientDemo huang.nju.edu.cn
2000 运行程序,会显示 Destination——huang.nju.edu.cn ClientDemo的第13~17行与ServerDemo的第12~15行类似,都创建了相应的I/O流,以便交换数据。为了操作方便,程序中又分别给这两个流加了过滤器。 使用I/O流时要弄清楚哪个是进的,哪个是出的。不要看到client.getInputStream(),就以为是在创建客户机的输入流从而认为服务器该向其中写数据。无论从哪一方看来,getInputStream()所得的都是输入流,是用来读数据的,getOutputStream()所得的流都是用来写数据的。 下面就是客户机与服务器的“对话”过程。服务器先向连接口写一行欢迎信息,再在标准输出显示“Greeting
message has been
sent“。而客户端读到欢迎信息并显示在屏幕上。然后,客户端程序接收键盘输入并将它发给服务器(下面的运行结果中带下划线的为键入内容)。服务器接收并在屏幕上显示,然后发送一个“Bye”信息,程序结束。客户端接收到服务器“Bye”之后显示“Disconnect”并结束。 两个程序的完整输出如下。为了清楚起见,键盘输入的信息加了下划线(本文是用的斜体)。 服务器方运行结果: E:\java01>java
ServerDemo Server is listening... Client should
connect to port 2000 Client 127.0.0.1 is accepted. Client
port 1477 Greeting message has been sent. Client
says: Hello. This is my second coming.
E:\java01>
客户机方运行结果: E:\java01>java ClientDemo 127.0.0.1
2000 Destination - 127.0.0.1 Local port -
1477 Welcome! You can say a line of words before
disconnect. Hello. This is my second coming. Server
Says: Bye. Disconnect
E:\java01>
上面的例子中,服务程序与客户程序的工作也是有协议的,即服务程序与客户程序按确定程相互通信,服务器先发欢迎信息,客户发信息,然后服务器再发信息......等等。FTP等应用协议中也是对端口、操作等进行规定,但内容更为详尽。 当今被广泛使用的服务器程序是较为复杂的。它们可在后台运行,同时受理多个客户的访问,可循环接受多次请求。但基本的过程是与示例中相似的。复杂的编程中我们可以利用Java的多线程机制来实现后台运行和多客户访问,读者有兴趣不妨一试。
11.3.4 SocketImpl类与SocketImplFactory接口
SocketImpl是一个抽象类。通过定义它其中的方法,可以定制自己需要的Socket。如果程序员觉得Java提供的套接字的操作与自己所要求的不太符合,可以通过实现SocketImpl来定义套接字的实现。一旦实现了SocketImpl,如何引用呢?这又自然引出了SocketImplFactory接口。 SocketImplFactory接口中只有一个方法createSocketImpl()。返值为SocketImpl对象。我们用它来使自己定义的Socket实现发生作用。 自定义套接字实现需要经过以下步骤: (1)实现抽象类SocketImpl。 (2)实现接口SocketImplFactory。 (3)用ServerSocket/Socket类中的setSocketFactory()方法设置所实现的SocketImplFactory。 (4)SocketImplFactory的createSocketImpl()方法创建所定义的SocketImpl对象。 (5)使用SocketImplt对象。 上面这个过程与11.6和11.7中创建自定义的内容处理器的过程很相似。读者可以参照11.6和11.7中的例子。 SocketImpl类中封装了一些低级的操作,如监听(listen())、接受连接(accept())、连接(connect())等。具体的方法请参见API文档。
11.4 数据报的实现:DatagramSocket与DatagramPacket
11.4.1 数据报
节11.3中介绍了面向连接的通信,下面我们来介绍面向无连接的通信(Connectionless Oriented
Communiation)。 无连接的通信中,通信双方不需要先建立好连接。发送数据时,每包数据上附有完整的目的地址。如果一次发送多包数据,它们的传输是彼此独立的,未必先发的先到,后发的后到,因而是无序的。数据在传输中的丢失、颠倒和重复无法避免,因而是不可靠的。无连接服务适于传送零散的数据。 无连接服务类似现实生活中的信函往来。甲写信给乙时,乙是不必知道的。每封信上附有乙的明确地址。如果要发多封信,各封信的邮寄互不相干。由于邮路上的一些情况,很可能出现信件次序颠倒、信件的丢失。如果乙长期未收到甲的一封信,发觉后要求甲重发,而被延误的信最终又送到了,这就出现了数据的重复。 无连接服务也有着广泛的应用。为了增强可靠性,也有一些弥补的措施,如接到数据后发送一个“已收到”的确认信息等。 数据报是较为纯粹的一种无连接通信。发送方一旦发数据,就假定接收方已经收到了。这样通信开销较小,但可靠性差。不过,我们可以基于数据报,通过附加一些错误检测措施来完成较为可靠的服务。本节我们讨论纯粹的数据报通信。
11.4.2 Java对数据报的支持
1.DatagramPacket类和DatagramSocket类 通过前面所讲的原理可以发现,在通信时,交换数据双方均在打包的数据(数据报)上附目的地址;双方使用同样的套接字,通信时向对方的套接字写入数据。因此,Java为数据报通信提供了两个类:数据报类DatagramPacket和数据报套接字类DatagramSocket。顾名思义,DatagramPacket封闭了被发送数据的有关信息和操作,而DatagramSocket封闭了套接字的有关信息和操作。 DatagramPacket的构造函数有: ■public
DatagramPacket(byte ibuf[],int
ilength) 创建一个数据报对象,以便接收长为ilength的数据报。其中,ibuf是存放收到数据的缓冲区。ilength必须小于或等于ibuf的长度。 ■public
DatagramPacket(byte ibuf[],int ilength,InetAddress iaddr,int
iport) 创建一个数据报对象,以便送长为ilength的数据报。其中,ibuf是发送缓冲区,iaddr是目的地址,iport是目的端口号。ilength必须小于或等于ibuf的长度。 数据报类的常用方法有: ■public
synchronized InetAddress
getAddress() 返回收到的数据报的来源地址或发出的数据报的目的地址。 ■public
synchronized int
getPort() 返回收到的数据报的来源端口或发出的数据报的目的端口。 ■public synchronized
byte[] getData() 取数据报中的数据。 ■public synchronized int
getLength() 取得数据报的数据长度。 相应于上述get方法,还有一组set方法,分别用来设置地址、端口、数据、长度。下面是它们的原型: ■public
synchronized void setAddress(InetAddress iaddr) ■public
synchronized void setPort(int iport) ■public synchronized void
setData(byte ibuf[]) ■public synchronized void setLength(int
ilength) 数据报套接字类的构造函数有三个: ■public DatagramSocket() throws
SocketException 构造一个数据报套接字对象,任意关联本地主机的一个端口。 ■public
DatagramSocket(int port) throws
SocketException 构造一个数据报套接字对象,关联到本地主机的指定端口。 ■public
DatagramSocketint port,InetAddress laddr) throws
SocketException 创建一个数据报套接字对象,关联到指定的本地主机地址和指定的端。 数据报套接字的方法与面向连接通信使用的套接字类的方法有些类似。常用的有下面一些,对于含义很明显的方法不再解释。 ■public
void close() 关闭该套接字。 ■public InetAddress
getLocalAddress() ■public int getLocalPort() ■public
synchronized int getSoTimeout() throws
SocketException 取SO_TIMEOUT的值。 ■public synchronized void
receive(DatagramPacket p)throws
IOException 接收一个数据报。没有收到数据报时该方法阻塞。如果收到的数据长度大于缓冲区长度,超出缓冲区的部分数据将被截去。 ■public
void send(DatagramPacket p) throws
IOException 发送一个数据报。 ■public synchronized void
setSoTimeout(int timeout) throws
SocketException 设置SO_TIMEOUT的值(毫秒)。这是receive()方法阻塞的时间上限。超时将抛出java.io.InterruptedIOException。 2.程序示例 下面我们用一个具体的例子来看它们如何工作(例11.4、例11.5)。 这个例子的功能是传送。运行仍然采用两个DOS窗口分别进行的形式,选启动接收者运行。Sender接收键盘输入的信息,并将其以数据报的形式发送出去。Receiver接收这个数据报并在屏幕上显示其内容。 读者可以自行分析这个简单的例子,设想一下它的运行结果。在节11.4.2将例,读者可以对照一下自己的设想是否正确。 例11.4
是发送方程序。 例11.4 Sender.java。 1: import java.net.*; 2:
import java.io.*; 3: public class Sender{ 4: public
static void main(String[] args) throws
IOException{ //判断命令行参数是否符合要求 5:
if(args.length!=2){ 6: System.out.println("Useg:java
Sender <dest hostname> <port>"); 7:
return; 8: } //发送缓冲区 9: byte sBuf[]=new
byte[100]; 10: System.out.println("Give the message you'll
send, "+ 11: "up to 100 bytes,ending with
#"); 12: DataInputStream dataIn=new
DataInputStream(System.in); 13: int
i; //读取键盘输入,存入缓冲区 14: for(i=0;i<100;i++){ 15: byte
inByte=dataIn.readByte(); 16: if((char)inByte=='#')
break; 17: sBuf[i]=inByte; 18: } //创建数据报套接字 19: DatagramSocket
sendSocket=new
DatagramSocket(); //创建一个数据报 20: DatagramPacket
packet=new
21: DatagramPacket(sBuf,i,InetAddress.getByName(args[0]), 22: Integer.valueOf(args[1]).intValue()); //发送 23: sendSocket.send(packet); //关闭 24: sendSocket.close(); 25: } 26:}
例11.5 是接收方程序。 例11.5 Receiver.java。 1: import
java.net.*; 2: import java.io.*; 3: 4: public class
Receiver{ 5: public static void main(String[] args) throws
IOException{ 6: if(args.length!=1){ 7:
System.out.println("Usage:java Receiver <port>"); 8:
return; 9: } //接收缓冲区 10: byte rBuf[]=new
byte[100]; //创建一个用于接收数据的数据报 11: DatagramPacket
packet=new
DatagramPacket(rBuf,rBuf.length); //创建套接字 12: DatagramSocket
receiveSocket= 13: new
DatagramSocket(Integer.valueOf(args[0]).intValue()); //等待并接收数据报 14: receiveSocket.receive(packet); //取数据报中的数据并打印 15: System.out.println(new
String(packet.getData())); //关闭套接字
16: receiveSocket.close(); 17: } 18:}
3.程序分析 让我们按照双方的动作顺序来分析它们的行为。 接收方(Receiver)先于发送方被启动运行,然后做一些接收数据的准备工作。请读者区分这些工作与面向连接的通信的区别。这里接收方的等待与面向连接通信中服务器的等待连接是不同的。这里的准备工作相当于设置了一个可以向其中投递信息的信箱。至于信件按什么途径和次序被投递是不关心的。 第11行创建了一个用于接收的数据报对象packet。第12行创建接收方套接字。然后(第14行),接收方调用接收方法,等待接收数据报。 现在看发送方(Sender)做了些什么。发送方先准备了一个发送缓冲区sBuf。这是一个包含100个字节的数组。然后根据键盘输入为数组赋值。 从第14行至第18行是为发送缓冲区赋值的过程。如果键盘输入不足100个字符,则以“#”结束。否则,读前100个字符,余下的舍弃。 第19行创建一个数据报套接字sendSocket。这里采用了没有参数的构造函数,因为数据报中会说明目的端口号。在接收方则使用有参数的形式。 接下来就是创建数据报对象了。Sender程序的第一个命令行参数为目的主机名,目的主机的地址根据它用InetAddress类的类方法getByName()得到。第二个命令行参数是端口号,程序中用了一个转换,从字符串得到相应的整型值。 Sender程序最后的工作是发送数据报,然后关闭套接字,结束运行。 当Sender发出数据报后,Receiver结束了receive()的阻塞状态,从套接字读到了数据报的内容。第15行打印输出数据报的内容,方法是先根据接收缓冲区中的内容创建一个字符串对象,然后输出这一字符串。接着,减压套接字,程序结束。 下面是这个例子一次运行的完整输出,其中带下划线(本例中是斜体)的部分为键入信息。 E:\java01>java
Receiver 2000 Notice-- The Receiver should be executed
before Sender's sending message.
E:\java01>
E:\java01>java Sender 127.0.0.1 2000 Give the
message you'll send, up to 100 bytes,ending with
# Notice-- The Receiver should be executed
before Sender's sending message.#
E:\java01> [首页][上页][下页]
|