联网
Java几乎是Internet编程的同义词。这有许多原因,不仅仅是因为Java能够产生安全、跨平台、可移植的代码。Java称为首选网络编程余姚最重要的原因之一,反而是java.net包中定义的类,它们为我们访问网络资源提供了易于使用的方法。
本篇讨论java.net包中的几个核心类和接口。
1 联网的基础知识
支持Java联网功能的核心是套接字的概念。套接字用于识别网络上的断点。套接字规范在20世纪80年代早期发布的4.2BSD UNIX的一部分。由于这个原因,也使用"伯克利套接字"这一术语。套接字是现代联网功能的基础,因为通过套接字,一台计算机可以同时为许多不同的客户端提供服务,也能为许多不同类型的信息提供服务。这是通过使用端口完成的,端口是特定计算机上具有编号的套接字。服务器进程"监听"端口,直到客户端连接到端口。服务器允许同一个端口号接受多个客户端连接,不过每次回话都是唯一的。为了管理多个客户端连接,服务器进程必须是多线程的,或具有一些其他多路复用同步I/O的方法。
套接字通信是通过协议进行的。IP(Internet Protocol)协议是低级的路由协议,可以将数据分隔到许多小的数据包中,并通过网络将它们发送到某个地址,但不能保证所有包都发送到目的地。传输控制协议(Transmission Control Protocol,TCP)是一个更高层的一些,该协议负责将这些数据包健壮地串联到一起,并且为了可靠地传输数据,该协议根据需要对数据包进行排序和重新传输。第3个协议是用户数据报协议(User Datagram Protocol,UDP),位于TCP协议的上层,用于直接支持更快速的、无连接的、不可靠的数据包传输。
一旦建立连接,一个更高层的协议就会随之而来,这取决于使用的是哪个端口。TCP/IP保留较低的1024个端口用于特定协议。端口号21用于FTP;23用于Telnet;25用于e-mail;43用于whois;80用于HTTP;119用于netnews;等等。客户端和端口之间的交互方式是由每个协议决定的。
例如,HTTP是Web浏览器和服务器用于传输超文本页面和图像的协议。这是一个相当简单的协议,用于基本的页面浏览Web服务器。下面是其工作原理:当客户端从HTTP服务器请求文件时,这个动作就是所谓的点击,它简单地以特定的格式将文件名发送到预先定义的端口,读取并返回文件的内容。服务器还通过状态进行响应,告诉客户端是否能够满足请求及其原因。
Internet的关键部分时地址。在Internet上,每台计算机都有一个地址。Internet地址是用于表示网络上每台计算机的数字。最初,所有Internet地址都是由32位数值构成的,并组织为4个8位数值。这种类型的地址通过IPv4指定。然后,一种新的称为IPv6的地址模式已经开始使用。IPv6使用128位数值表示一个地址,并被组织成8个16位块。尽管使用IPv6有许多原因和优点,但是与IPv4相比,主要的优点还是在于支持更大的地址空间。幸运的是,当使用Java时,通常不需要担心使用IPv4还是IPv6,因为java会处理这些细节。
就像IP地址的数字描述网络的层次结构一样,Internet地址的名称会被称为域名(domain name),描述了名称空间中机器的位置。例如,www.HerSchildt.com位于Com顶级域中(这是为美国商业站点保留的域);名为HerSchildt,并且www表示用于Web请求的服务器。Internet域名通过域名服务(Domain Naming Service,DNS)映射到IP地址,从而使用户可以使用域名,而Internet操作IP地址。
2 联网类和接口
Java通过扩展已经建立的流I/O接口,并通过添加在网络上构建I/O对象所需要的特性来支持TCP/IP。Java支持TCP和UDP协议族。TCP用于网络上可靠的基于流的I/O,UDP支持更简单,并且更快、点对点的面向数据报的模型。java.net包中包含的类如下所示:
Authenticator | InetAddress | SocketAddress |
CacheRequest | InetSocketAddress | SocketImpl |
CacheResponse | InterfaceAddress | SocketPermission |
ContentHandler | JarURLConnection | StandardSocketOption |
CookieHandler | MulticastSocket | URI |
CookieManager | NetPermission | URL |
DatagramPacket | NetworkInterface | URLClassLoader |
DatagramSocket | PasswordAuthentication | URLConnection |
DatagramSocketImpl | Proxy | URLDecoder |
HttpCookie | ProxySelector | URLEncoder |
HttpURLConnection | ResponseCache | URLPermission(JDK8新增) |
IDN | SecureCacheResponse | URLStreamHandler |
Inet4Address | ServerSocket | |
Inet6Address | Socket |
下面列出了java.net包中包含的接口:
ContentHandlerFactory | FileNameMap | SocketOptions |
CookiePolicy | ProtocolFamily | URLStreamHandlerFactory |
CookieStore | SocketImplFactory | |
DatagramSocketImplFactory | SocketOption |
在接下来的内容中,将分析主要的联网类并展示一些应用它们的示例。
3 InetAddress 类
InetAddress类用于封装数字IP地址及对应的域名。使用IP主机的名称与这个类进行交互,与使用IP地址相比,使用IP主机名称更方便、也更容易理解。InetAddress类在内部隐藏了数字。InetAddress类可以同时处理IPv4和IPv6地址。
3.1 工厂方法
InetAddress类没有可见的构造函数。为了创建InetAddress对象,必须使用某个可用的工厂方法。工厂方法只不过是一种约定,根据约定,类中的静态方法返回类的一个实例。这代替了使用不同的参数列表重载构造函数,因为使用独特的方法名称使结果更加清晰。3个常用的InetAddress工厂方法如下所示:
static InetAddress getLocalHost() throws UnknownHostException
static InetAddress getByName(String hostName) throws UnknowHostException
static InetAddress[] getAllByName(String hostName) throws UnknowHostException
getLocalHost()方法简单地返回表示本地主机的InetAddress对象。getByName()方法根据传递过啦的主机名返回InetAddress对象。如果这些方法无法解析主机名,就会抛出UnknownHostException异常。
在Internet上,使用单个名称表示多台机器是很常见的。在Web服务器世界,这种方式可以提供某种程度的伸缩功能。getAllByName()工程方法返回一个InetAddress对象数组,该数组表示解析特定名称后得到的所有地址。如果不能解析出至少一个地址,该方法会抛出UnknownHostException异常。
InetAddress类还提供了工厂方法getByAddress(),该方法返回IP地址返回InetAddress对象。不管是IPv4还是IPv6都可以使用。
下面的示例程序打印本地机器以及两个Internet Web站点的地址和名称:
//Demonstrate InetAddress
import java.net.InetAddress;
import java.net.UnknownHostException;
class InetAddressTest {
public static void main(String[] args) throws UnknownHostException {
InetAddress address=InetAddress.getLocalHost();
System.out.println(address);
address = InetAddress.getByName("www.baidu.com");
System.out.println(address);
InetAddress sw[] = InetAddress.getAllByName("www.nba.com");
for(int i=0;i<sw.length;i++){
System.out.println(sw[i]);
}
}
/**
* 输出:
* LAPTOP-LFCMFUVE/192.168.0.10
* www.baidu.com/39.156.66.14
* www.nba.com/23.194.27.74
*/
}
3.2 实例方法
InetAddress类还有其他一些方法,可以对刚才讨论过的方法所返回的对象使用这些方法。表1中是一些相对更常用的方法。
方 法 | 描 述 |
---|---|
boolean equals(Object other) | 如果该对象与other对象具有相同的Internet地址,就返回true |
byte[] getAddress() | 以网络字节顺序返回一个字节数组,表示该对象的IP地址 |
String getHostAddress() | 返回一个字符串,表示与InetAddress对象关联的主机地址 |
String getHostName() | 返回一个字符串,表示与InetAddress对象关联的主机名 |
boolean isMulticastAddress() | 如果该地址是一个多播地址,就返回true;否则返回false |
String toString() | 为方便起见,返回一个列出主机名和IP地址的字符串 |
Internet地址是在一系列层次化的缓存服务器中进行查找的,这以为着本地计算机可能自动地知道特定的从名称到IP地址的映射,例如对于本地计算机自身以及附近的服务器。对于其他名称,可能需要向本地DNS服务器资讯IP地址信息。如果那台服务器没有这个特定的地址,那么还能够进行入到远程站点并进行资讯。这个过程可以一直持续下去,知道到达根服务器。这个过程可能需要比较长的时间,因此需要聪明地编写代码,从而在本地缓存IP地址,而不是重复地进行查找。
4 Inet4Address 类和 Inet6Address类
Java对IPv4和IPv6地址提供了支持。因此,Java创建了InetAddress的两个子类:Inet4Address和Inet6Address。Inet4Address表示传统风格的IPv4地址,Inet6Address封装了新风格的IPv6地址。因为它们是InetAddress的子类,所以InetAddress引用可以指向它们中的任何一个。这是Java能添加IPv6功能而不会破坏已有代码或添加许多其他类的一种方式。对于使用IP地址的大多数情况,可以简单地使用InetAddress,因为它能够适应这两种风格。
5 TCP/IP 客户端套接字
TCP/IP套接字用于在Internet主机之间实现可靠的。双向的、持续的、点对点的、基于流的连接。可以使用套接字将Java的I/O系统连接到其他程序,这些程序可能位于本地主机或Internet的任何其他机器上。
注意
作为通用规则,applet只可以建立到主机(从该主机可以下载applet)的套接字连接,之所以存在这一限制,是因为让通过防火墙加载的applet访问任意一台机器是危险的。
在Java中,有两种类型的TCP套接字。一种用于服务器,另一种用于客户端。ServerSocket类被设计成"监听者",等待客户端进行连接,在这之前什么也不做。因为ServerSocket用于服务器。Socket类用户客户端。它被设计成用于连接到服务器套接字并发起协议交换。因为客户端套接字是Java应用程序最常用的,所以在此对其进行分析。
创建Socket对象会隐式地建立客户端和服务器之间的连接。没有显式提供建立连接细节的方式或构造函数。表2中是用于创建客户端套接字的两个构造函数。
构 造 函 数 | 描 述 |
---|---|
Socket(String hostName,int port) throws UnknownHostException,IOException | 创建连接到命名主机和端口的套接字 |
Socket(InetAddress inAddress,int port) throws IOException | 使用已存在的InetAddress对象和端口创建套接字 |
Socket定义了几个实例方法。例如,在任何时间都可以使用表3中的方法检查与Socket对象关联的地址和端口信息。
方 法 | 描 述 |
---|---|
InetAddress getInetAddress() | 返回与调用Socket对象关联的InetAddress对象。如果套接字没有连接,就返回null |
int getPort() | 返回调用Socket对象连接到的远程端口。如果套接字没有连接,就返回0 |
int getLocalHost() | 返回绑定到调用Socket对象的本地端口。如果套接字没有连接,就返回-1 |
通过使用表4中的getInputStream()和getOutputStream()方法,可以获得对与Socket关联的输入流和输出流的访问。如果套接字因为丢失连接而变得无效,这两个方法会抛出IOException异常。完全可以像使用I/O流那样,使用这些流发送和就收数据。
方 法 | 描 述 |
---|---|
InputStream getInputStream() throws IOException | 返回与调用套接字关联的InputStream对象 |
OutputStream getOutputStream() throws IOException | 返回与调用套接字关联的OutputStream对象 |
可以使用的其他一些方法包括:connect(),可以通过该方法指定新的连接:isConnected(),如果套接字连接到了服务器,该方法将返回true;isBound(),如果套接字被绑定到某个地址,该方法就返回true;isClosed(),如果套接字已经关闭,该方法就返回true;为了关闭套接字,可以调用close()方法。关闭套接字也会关闭与之关联的I/O流。从JDK7开始,Socket还实现了AutoCloseable接口,这意味着可以使用带资源的try语句块管理套接字。
下面的程序提供了一个简单的Socket示例。该例打开一个InterNIC服务器上"whois"端口(端口43)的连接,向套接字发送命令行参数,然后打印返回的数据。InterNIC将尝试作为已注册的Internet域名来查找参数,然后返回IP地址以及站点的联系信息。
//Demonstrate Sockets.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
class Whois {
public static void main(String[] args) throws IOException {
int c;
//Create a socket connected to internic.net,port 43.
Socket s = new Socket("Whois.internic.net", 43);
//Obtain input and output streams.
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
//Construct a request string.
String str = (args.length == 0 ? "MHProfessional.com" : args[0]) + "\n";
//Convert to bytes.
byte buf[] = str.getBytes();
//Send request.
out.write(buf);
//Read and display response.
while ((c = in.read()) != -1) {
System.out.print((char) c);
}
s.close();
}
}
举例来说,如果获取到关于MHProfessional.com站点的信息,就会得到类似下面的一些内容:
whois Server Version 2.0
Domain names in the .com and .net domains can now be registered
with many different competing registrars.Go to http://www.internic.net for detailed information.
Domain Name:MHPROFESSIONAL.COM
Registrar:CSC CORPORATE DOMAINS,INC.
Referral URL:http://www.cscglobal.com
Name Server:NS1.MHEDU.com
Name Server:NS2.MHEDU.com
下面是该程序的工作原理。首先,构造一个指定主机名"whois.intermic.net"和端口号43的Socket。Intermic.net是InterNIC Web站点,该站点处理whois请求。端口号43是whois端口。接下来,在该套接字上打开输入流和输出流。然后构造一个字符串,其中包含希望获取其相关信息的Web站点的名称。在这个示例中,如果再命令行上没有指定Web站点,那么将使用"MHProfessional.com"。将字符串转换成字节数组,然后发送到套接字。通过来自套接字的输入读取相应并显示结果。最后关闭套接字,也会关闭I/O流。
在前面的程序中,套接字是通过调用close()方法手动关闭的。如果使用JDK7或更新版本,就可以使用带资源的try语句自动关闭套接字。例如,下面是编写前面程序中main()方法的另外一种方式:
//Use try-with-resources to close a socket.
public static void main(String args[]) throws Exception{
int c;
//Create a socket connected to internic.net,port 43. Manage this
//socket with a try-with-resources block.
try(Socket s=new Socket("whois.internic.net",43)){
//Obtain input and output stream.
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
//Construct a request string.
String str = (args.length == 0 ? "MHProfessional.com" : args[0])+"\n";
//Convert to bytes.
byte buf[] = str.getBytes();
//Read and display response.
while((c=in.read())!=-1){
System.out.print((char)c)
}
}
在这个版本中,当try代码块结束时会自动关闭套接字。
6 URL 类
前面的例子相等晦涩,因为现代Internet上流行的不是老式的协议,例如whois、finger和FTP,而是www(World wide Web)。Web时高层协议和文件格式的松散集合,全部统一于Web浏览器中。关于Web最重要的一个方面是Tim Berners-Lee设计的一种用于定位于定位网络上所有资源的扩展方式。一旦可以可靠得命名任何东西,这种方式就成为一种非常强大的模式。统一资源定位器(Uniform Resource Locator,URL)用于完成该工作。
URL为唯一标识或寻址Internet上的信息,提供了一种相当容易理解的形式。URL是普遍存在的,每个浏览器都使用它们来标识Web上的信息。在Java的网络类库中,URL类提供了一套简单、简明的API,用于使用URL访问Internet上的信息。
所有URL都使用相同的基本格式,尽管也可以存在某些变化。下面是两个例子:http://www.MHProfessional.com/与http://www.MHProfessional.com:80/index.htm。URL规范基于4个部分。第1个部分是使用的协议,使用冒号(:)与定位器的其他部分分开。常用的协议有HTTP、FTP、gopher以及file,监管现在几乎所有事情都是通过HTTP完成的(实际上,即便URL中省略"http://",大多数浏览器也仍然会正确地执行)。第2部分时所用主机的主机名和IP地址;这是在左侧使用双斜线(//),并且在右侧使用单斜线(/)或可选的冒号(:)界定,在右侧由单斜线(/)界定(默认端口是80,即预定义的HTTP端口;因此,":80"是多余的)。第4部分是实际的文件路径。大多数HTTP服务器会为直接引用目录资源的URL追加名为index.html或index.htm的文件。因此,http://www.MHProfessional.com/与http://www.MHProfessional.com/index.htm是相同的。
Java的URL类具有几个构造函数,每个都可能抛出MalformedURLException异常。常用的一种形式使用与在浏览器中显示的相同字符串指定URL:
URL(String urlSpecifier) throws MalformedURLException
另外两种形式的构造函数允许将URL分隔成组成部分:
URL(String protocolName,String hostName,int port,String path)
URL(String protocolName,String hostName,String path) throws MalformedURLException
另外一种经常使用的构造函数允许使用已有的URL作为参考上下文,然后根据该上下文创建新的URL。尽管这听起来有些费解,但实际上很容易并且很有帮助。
URL(URL urlObj,String urlSpecifier) throws MalformedURLException
下面的例子创建一个到HerbSchildt.com站点中文章页面的URL,然后检查这个URL的属性:
//Demonstrate URL.
import java.net.MalformedURLException;
import java.net.URL;
class URLDemo {
public static void main(String[] args) throws MalformedURLException {
URL hp = new URL("http://www.HerbSchildt.com/WhatsNew");
System.out.println("Protocol: " + hp.getProtocol());
System.out.println("Port: " + hp.getPort());
System.out.println("Host: " + hp.getHost());
System.out.println("File: " + hp.getFile());
System.out.println("Ext: " + hp.toExternalForm());
}
/**
* 输出:
* Protocol: http
* Port: -1
* Host: www.HerbSchildt.com
* File: /WhatsNew
* Ext: http://www.HerbSchildt.com/WhatsNew
*/
}
注意端口号-1:这意味着没有显式地设置端口。给定一个URL对象,可以检索与之关联的数据。为了访问URL对象的实际位或内容信息,可以使用openConnection()方法创建URLConnection对象,如下所示:
urlc = url.openConnection()
openConnection()方法的一般形式如下所示:
URLConnection openConnection() throws IOException
该方法返回一个与调用URL对象关联的URLConnection对象。该方法可能会抛出IOException异常。
7 URLConnection 类
URLConnection是用于访问远程资源属性的通用类。一旦构造一个到远程服务器的连接,就可以使用URLConnection对象在实际传送远程对象到本地之前,检查远程对象的属性。这些属性是由HTTP协议规范提供的,并且只对使用HTTP一些的URL对象有意义。
URLConnection类定义了一些方法,如表5所示。
方 法 | 描 述 |
---|---|
int getContentLength() | 返回与资源关联的内容的字节大小。如果长度不可得,就返回-1 |
long getContentLengthLong() | 返回与资源关联的内容的字节大小。如果长度不可得,就返回-1 |
String getContentType() | 返回在资源中找到的内容的类型,也就是content-type标题字段的值。如果内容类型不可得 ,就返回null |
long getDate() | 返回响应的时间和日期,使用从1970年1月1日(GMT时间)到现在经历的毫秒数表示。如果有效日期不可得,就返回0 |
String getHeaderField(String fieldName) | 返回标题字段的值,名称由fileName指定。如果没有找到指定的名称,就返回null |
String getHeaderFieldKey(int idx) | 返回idx索引位置标题字段的键(标题字段的索引从0开始)。如果idx的值超过字段的数量,就返回null |
Map<String,List<\String>> getHeaderFields() | 返回包含所有标题字段和值的映射 |
long getLastModified() | 返回最后一次修改资源的时间和日期,使用从1970年1月1日(GMT时间)到现在经历的毫秒数表示。 |
InputStream getInputStream() throws IOException | 返回链接到资源的InputStream对象。可以使用这个流获取资源的内容 |
注意,URLConnection定义了一些处理标题信息的方法。标题包含使用字符串表示的键值对。通过getHeaderField()方法可以获取与标题键关联的值。通过getHeaderFields()方法可以获取包含所有标题的映射。一些标准标题字段可以直接通过getDate()和getConnectType()这类方法获取。
下面的例子使用URL对象的openConnection()方法创建URLConnection对象,然后使用该对象检查文件的属性和内容:
//Demonstrate URLConnection.
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
class UCDemo {
public static void main(String[] args) throws IOException {
int c;
URL hp = new URL("http://www.internic.net");
URLConnection hpCon = hp.openConnection();
//get date
long d = hpCon.getDate();
if (d == 0) {
System.out.println("No date information.");
} else {
System.out.println("Date: " + new Date(d));
}
//get content type
System.out.println("Content-type: " + hpCon.getContentType());
//get expiration data
d = hpCon.getExpiration();
if (d == 0) {
System.out.println("No expiration information.");
} else {
System.out.println("Expires: " + new Date(d));
}
//get last-modified date
d = hpCon.getLastModified();
if (d == 0) {
System.out.println("No last-modified information");
} else {
System.out.println("Last-Modified: " + new Date(d));
}
//get content length
long len = hpCon.getContentLengthLong();
if (len == -1) {
System.out.println("Content length unavailable");
} else {
System.out.println("Content-length:" + len);
}
if (len != 0) {
System.out.println("=== Content ===");
InputStream input = hpCon.getInputStream();
while ((c = input.read()) != -1) {
System.out.print((char) c);
}
input.close();
} else {
System.out.println("No content available.");
}
}
}
该程序在端口80建立一个到www.internic.net的HTTP连接,然后显示一些标题值并检索内容。
8 HttpURLConnection 类
Java提供了URLConnection的一个子类,用来支持HTTP连接,这个子类是HttpURLConnection。可以使用前面显示的相同方式,通过对URL对象调用openConnection()方法来获取HttpURLConnection对象,但是必须将结果强制转换成HttpURLConnection对象的引用,就可以使用继承自URLConnection的任何方法了。还可以使用HttpURLConnection定义的任何方法。表6中是HttpURLConnection类定义的一些方法。
方 法 | 描 述 |
---|---|
static boolean getFollowRedirects() | 如果重定向是自动进行的,就返回true;否则返回false,这个特性是默认打开的 |
String getRequestMethod() | 返回一个字符串,表示生成URL请求的方式。默认为GET,也可以是其他方式,例如POST |
int getResponseCode() throws IOException | 返回HTTP响应代码。如果不能获得响应代码,就返回-1,如果连接失败,那么会抛出IOException异常 |
String getResponseMessage() throws IOException | 返回与响应代码关联的响应消息。如果消息不可得,就返回null。如果连接失败,那么会抛出IOException异常 |
static void setFollowRedirects(boolean how) | 如果how为true,那么重定向是自动进行的。如果how为false,那么重定向不是自动进行的。默认情况下,重定向是自定进行的 |
void setRequestMethod(String how) throws ProtocolException | 将HTTP请求的生成方式设置为how指定的方式。默认方法是GET,但也可以使其他方式,例如POST。如果how无效,将会抛出ProtocolException异常 |
下面的程序演示了HttpURLConnection。该程序首先建立一个到www.google.com的连接,然后显示请求方法、响应代码以及响应消息。最后,显示响应标题中的键和值。
//Demonstrate HttpURLConnection.
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Set;
class HttpURLDemo {
public static void main(String[] args) throws IOException {
URL hp = new URL("http://www.baidu.com");
HttpURLConnection hpCon = (HttpURLConnection) hp.openConnection();
//Display request method.
System.out.println("Request method is " + hpCon.getRequestMethod());
//Display response code.
System.out.println("Response code is " + hpCon.getResponseCode());
//Display response message.
System.out.println("Response Message is " + hpCon.getResponseMessage());
//Get a list of header fields and a set
//of the header keys.
Map<String, List<String>> hdrMap = hpCon.getHeaderFields();
Set<String> hdrField = hdrMap.keySet();
System.out.println("\nHere is the header:");
//Display all header keys and values.
for (String k : hdrField) {
System.out.println("Key: " + k + " Value: " + hdrMap.get(k));
}
}
/**
* 输出:
* Request method is GET
* Response code is 200
* Response Message is OK
*
* Here is the header:
* Key: null Value: [HTTP/1.1 200 OK]
* Key: Server Value: [bfe]
* Key: Content-Length Value: [2381]
* Key: Date Value: [Fri, 21 Feb 2020 03:58:19 GMT]
* Key: Content-Type Value: [text/html]
*/
}
注意标题中键和值的显示方式。首先,调用getHeaderFields()方法(该方法继承自URLConnection)来获取标题中键和值的映射。接下来,通过映射调用keySet()方法来获取标题中键的集合。然后使用for-each风格的for循环遍历键的集合。通过对映射调用get()方法还可以获取与每个键关联的值。
9 URI 类
URI类封装了统一资源标识符(Uniform Resource Identifier,URI)。URI与URL类似。实际上,URL是URI的一个子集。URI代表定位资源的一种标准方式。URL还描述了如何访问资源。
10 Cookie
在java.net包中包含了帮助管理cookie的类和接口,并且可以用于出创建有状态的(与之对应的是无状态的)HTTP会话。这些类有CookieHandler、CookieManager和HttpCookie,接口有CookiePolicy和CookieStore。
11 TCP/IP 服务器套接字
如前所述,Java具有不同类型的套接字类,用于创建服务器应用程序。ServerSocket类用于创建服务器,在发布的端口上监听与之连接的本地或远程客户端程序。ServerSocket与常规的Socket区别很大。当创建ServerSocket对象时,它会在系统中注册自身,表明对客户端连接有兴趣。ServerSocket类的构造函数反映了希望接受连接的端口号,并且(可选)反映了希望端口号使用的队列长度。队列长度告诉系统:在简单地拒绝连接之前,可以保留多少个等待连接的客户端连接。默认队列长度是50.在不利条件下,构造函数可能会抛出IOException异常。表7中是ServerSocket类的3个构造函数:
构 造 函 数 | 描 述 |
---|---|
ServerSocket(int port) throws IOException | 在指定的端口创建服务器套接字,队列长度为50 |
ServerScoket(int port,int maxQueue) throws IOException | 在指定的端口上创建服务器套接字,最大队列长度为maxQueue |
ServerSocket(int port,int maxQueue,InetAddress localAddress) throws IOException | 在指定的端口上创建服务器套接字,最大队列长度为maxQueue。在多宿主主机上,localAddress指定了套接字绑定的IP地址 |
ServeSocket还有一个名为accept()的方法,这是一个等等客户端发起通信的阻塞调用,然后返回一个常规的Socket对象,该Socket对象用于与客户端进行通信。
12 数据报
TCP/IP风格的联网适合于大多数数据网络需求,提供了序列化、可预测的、可靠的、包形式的数据量。但是,这些优点也是有代价的。TCP为处理拥挤网络上的拥塞控制以及数据丢失的悲观预期提供了许多复杂的算法,这在一定程度上降低了数据的传输效率。数据报提供了传输数据的另外一种方式。
数据报是在两台机器之间传递的信息包。在某种程度上,它们类似于从训练有素,但被蒙住双眼的投手头像第2名垒手的使劲传杀(hard throw)。一旦将数据报释放给期望的目标,就既不能保证到达,也不能保证有人会在目的地捕获到。同样,当接收到数据报时,既不能保证数据在传输过程中没有被穗槐,也不能保证发送者仍然在等待接收响应。
Java通过两个类在UDP协议之上实现数据报:DatagramPacket对象是数据封装器,而DatagramSocket对象是用于发送和接收DatagramPacket对象的机制。下面分别介绍这两个类。
12.1 DatagramSocket 类
DatagramSocket类定义了4个公有构造函数,它们如下所示:
DatagramSocket() throws SocketException
DatagramSocket(int port) throws SocketException
DatagramSocket(int port,InetAddress ipAddress) throws SocketException
DatagramSocket(SocketAddress address) throws SocketException
第1个构造函数创建绑定到本机计算机上任意使用端口的DatagramSocket对象。第2个构造函数创建绑定到port指定端口的DatagramScoket对象。第3个构造函数创建绑定到指定端口和InetAddress对象的DatagramSocket对象。第4个构造函数创建绑定到指定SocketAddress对象的DatagramSocket对象。SocketAddress是抽象类,InetSocketAddress这个具体类实现了该抽象类。InetSocketAddress将IP地址与端口号封装到一起。在创建套接字时,如果发生错误,这些构造函数都会抛出SocketException异常。
DatagramSocket类定义了许多方法,其中最重要的两个方法是send()和receive(),如下所示:
void send(DatagramPacket packet) throws IOException
void receive(DatagramPacket packet) throws IOException
send()方法向packet指定的端口发送数据包,receive()方法等待从packet指定的端口接受数据包并返回结果。
DatagramSocket类还定义了close()方法,该方法关闭套接字。自JDK 7开始,DatagramSocket类实现了AutoCloseable接口,这意味着可以通过带资源的try语句管理DatagramSocket对象。
通过其他方法可以访问与DatagramSocket对象关联的各种特性。表8中是这些方法的一些例子。
方 法 | 描 述 |
---|---|
InetAddress getInetAddress() | 如果套接字已经连接,就返回地址;否则返回null |
int getLocalPort() | 返回本地端口号 |
int getPort() | 返回套接字连接的端口号。如果套接字没有连接到端口,就返回-1 |
boolean isBound() | 如果套接字被绑定到某个地址,就返回true;否则返回false |
boolean isConnected() | 如果套接字已经连接到某太服务器,就返回true;否则返回false |
void setSoTimeout(int millis) throws SocketException | 将超时时间设置成millis传递的毫秒数 |
12.2 DatagramPacket 类
DatagramPacket类定义了几个构造函数,其中的4个如下所示:
DatagramPacket(byte data[],int size)
DatagramPacket(byte data[],int offset,int size)
DatagramPacket(byte data[],int size,InetAddress ipAddress,int port)
DatagramPacket(byte data[],int offset,InetAddress inAddress,int port)
第1种形式指定了接收数据的缓冲区和包的大小,用于通过DatagramSocket对象接受数据。第2种形式允许指定缓冲区的偏移量,从此偏移位置保存数据。第3中形式指定了目标地址和端口,DatagramSocket对象使用该地址和端口确定将数据包发送到何处。第4种形式从指定的数据偏移位置传递数据包。可以将前两种形式想象成搭建"邮箱",将后两种形式想象成在信封中装入东西并填写地址。
DatagramPacket类定义了一些方法,包括表9中显示的那些方法,通过这些方法可以访问数据包的地址和端口号,以及原始数据及其长度。
方 法 | 描 述 |
---|---|
InetAddress getAddress() | (为将要接受的数据报) 返回源的地址,或(将要发送的数据报)返回目的地的地址 |
byte[] getData() | 返回在数据报中包含的数据的字节数组。通常用于在接受到数据报之后,从数据报中接受数据 |
int getLength() | 返回字节数组(由getData()方法返回)中包含的有效数据的长度,可能不等于整个字节数组的长度 |
int getOffset() | 返回数据的起始索引 |
int getPost() | 返回端口号 |
void setAddress(InetAddress ipAddress) | 设置包的目的地址,目的地址是由ipAddress指定的 |
void getPort() | 返回端口号 |
void setAddress(InetAddress ipAddress) | 设置包的目的地址,目的地址是由ipAddress指定的 |
void setData(byte[] data) | 将数据设置为data,将偏移量设置为0,将长度设置为data中字节的数量 |
void setData(byte[] data) | 将数据设置为data,将偏移量设置为idx,将长度设置为size |
void setLength(int size) | 将包的长度设置为size |
void setPort(int port) | 将端口设置为port |
12.3 数据报示例
下面的例子实现了一个非常简单的网络通信客户端和服务器。在服务器的窗口中键入消息,通过网络将消息写入客户端,在客户端显示这些消息。
//Demonstrate datagrams.
import com.sun.org.apache.xpath.internal.operations.String;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
class WriteServer {
public static int serverPort = 998;
public static int clientPort = 999;
public static int buffer_size = 1024;
public static DatagramSocket ds;
public static byte buffer[] = new byte[buffer_size];
public static void TheServer() throws IOException {
int pos = 0;
while (true) {
int c = System.in.read();
switch (c) {
case -1:
System.out.println("Server Quits.");
ds.close();
return;
case '\r':
break;
case '\n':
ds.send(new DatagramPacket(buffer, pos, InetAddress.getLocalHost(), clientPort));
pos = 0;
break;
default:
buffer[pos++] = (byte) c;
}
}
}
public static void TheClient() throws Exception {
while (true) {
DatagramPacket p = new DatagramPacket(buffer, buffer.length);
ds.receive(p);
System.out.println(new String(p.getData(), 0, p.getLength()));
}
}
public static void main(String[] args) throws Exception {
if (args.length == 1) {
ds = new DatagramSocket(serverPort);
TheServer();
} else {
ds = new DatagramSocket(clientPort);
TheClient();
}
}
}
这个示例程序通过DatagramSocekt类的构造函数进行限制,使其在本地机器上的两个端口之间运行。为了使用该程序,在一个窗口执行以下命名:
java WriteServer
这将是客户端。然后执行以下命令:
java WriteServer 1
这将是服务器。在服务器窗口中键入的任何内容,在接受到换行符之后都将被发送到客户端窗口。
注意
计算机上有可能不允许使用数据报,例如防火墙可能阻止使用它们。如果这种情况,就无法运行上面的例子。另外,程序中使用的端口号,也可能需要针对个人的环境进行调整。