Java8 API 入门手册(四)
四、Applet
在本章中,您将学习
- 什么是 applet
- 如何开发、部署和运行 Applet
- 如何使用
<applet>标签在 HTML 文档中嵌入 applet - 如何安装和配置运行 Applet 的 Java 插件
- 如何使用
appletviewer程序运行 Applet - Applet 的生命周期
- 如何将参数传递给 Applet
- 如何发布 Applet 的参数和描述
- 如何在 Applet 中使用图像和音频剪辑
- 如何定制 Java 策略文件以授予 Applet 权限
- 如何签署 Applet
什么是 Applet?
是一种嵌入在 HTML 文档中并在 web 浏览器中运行的 Java 程序。组成 applet 的编译后的 Java 代码存储在 web 服务器上。web 浏览器通过互联网从 web 服务器下载 applet 代码,并在浏览器的上下文中本地运行该代码。通常,applet 有一个图形用户界面(GUI)。一个 applet 有许多安全限制,包括它在客户端计算机上能访问什么和不能访问什么。对 Applet 的限制是必要的,因为 Applet 可能不是由同一个人开发和使用的。如果允许恶意编写的小应用完全访问客户端机器,它可能会对客户端机器造成有害影响。例如,安全限制不允许 applet 访问文件系统或在客户机上启动程序。假设你打开一个网页,里面有一个 applet 可以读取你机器上的文件。在你不知情的情况下,一个恶意的 Applet 会把你的私人信息发送到它的服务器上。为了保护 applet 用户免受这种伤害,有必要在运行 applet 时设置安全限制。使用策略文件可以配置许多安全限制。我将在本章后面讨论如何配置 Applet 安全策略。
虽然 a 和 Applet 没有关系,但是我还是来解释一下两者的区别。像 applet 一样,servlet 也是部署在 web 服务器上的 Java 程序。与 applet 不同,servlet 运行在 web 服务器本身上,它不包括 GUI。
开发小应用
开发 applet 有四个步骤:
- 为 applet 编写 Java 代码
- 打包和部署 applet 文件
- 安装和配置 Java 插件
- 查看子视图
为 applet 编写 Java 代码与为 Swing 应用编写代码没有太大区别。您只需要学习一些将在代码中使用的 Applet 的标准类和方法。
小应用部署在网络服务器上,并通过互联网/内联网使用网络浏览器在网页中查看。您还可以在开发和测试期间使用 applet 查看器查看 applet。JDK 发布了一个 appletviewer 程序。安装 JDK 时,appletviewer 程序会安装在您机器上的JAVA_HOME\bin目录中。我将在本章后面详细讨论如何使用 appletviewer。
要在网页中查看 applet,您需要在 HTML 文档中嵌入对 applet 的引用。您可以使用三种 HTML 标签中的任何一种,即<applet>、<object>或<embed>来将 applet 嵌入到 HTML 文档中。我将很快详细讨论这些标签的使用。
接下来的两节讨论如何为 applet 编写 Java 代码,以及如何查看 applet。
编写 Applet
您的 applet 类必须是 Java 提供的标准 applet 类的子类。有两个标准的 applet 类:
java.applet.Appletjavax.swing.JApplet
Applet类支持 AWT GUI 组件,而JApplet类支持 Swing GUI 组件。JApplet类继承自Applet类。在这一章中我将只讨论JApplet。清单 4-1 显示了你能拥有的最简单的 applet 的代码。
清单 4-1。最简单的 Applet
// SimplestApplet.java
package com.jdojo.applet;
import javax.swing.JApplet;
public class SimplestApplet extends JApplet {
// No extra code is needed for your simplest applet
}
SimplestApplet没有任何 GUI 部件或逻辑。从技术上来说,它是一个完整的 Applet。如果您在浏览器中测试这个 applet,您所看到的只是网页中的一个空白区域。
让我们创建另一个带有 GUI 的 applet,这样您可以在浏览器中看到一些东西。新的 applet 叫做HelloApplet,如清单 4-2 所示。
清单 4-2。使用 JLabel 显示消息的 HelloApplet Applet
// HelloApplet.java
package com.jdojo.applet;
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import static javax.swing.JOptionPane.INFORMATION_MESSAGE;
public class HelloApplet extends JApplet {
@Override
public void init() {
// Create Swing components
JLabel nameLabel = new JLabel("Your Name:");
JTextField nameFld = new JTextField(15);
JButton sayHelloBtn = new JButton("Say Hello");
// Add an action litener to the button to display the message
sayHelloBtn.addActionListener(e -> sayHello(nameFld.getText()));
// Add Swing components to the content pane of the applet
Container contentPane = this.getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.add(nameLabel);
contentPane.add(nameFld);
contentPane.add(sayHelloBtn);
}
private void sayHello(String name) {
String msg = "Hello there";
if (name.length() > 0) {
msg = "Hello " + name;
}
// Display the hello message
JOptionPane.showMessageDialog(null,
msg, "Hello", INFORMATION_MESSAGE);
}
}
这个类的代码看起来熟悉吗?这类似于使用自定义的JFrame。JApplet类包含一个init()方法。您需要重写该方法,并向 applet 添加 GUI 部件。我将很快详细讨论用 applet 的方法编写代码。像一个JFrame,JApplet有一个内容窗格,包含 applet 的组件。您在JApplet的内容窗格中添加了一个JLabel、一个JTextField和一个JButton。程序逻辑很简单。用户可以输入姓名并点击显示消息的Say Hello按钮。
与 Swing 应用不同,您不应该在 applet 的构造函数中添加任何 GUI,即使它在大多数情况下都可以工作。调用 applet 的构造函数来创建 applet 类的对象。applet 对象在创建时不会获得其“applet”状态。它还是一个普通的 Java 对象。如果在构造函数中使用 applet 的任何特性,这些特性将无法正常工作,因为 applet 对象只是一个简单的 Java 对象,而不是真正意义上的“applet”。在创建之后,它获得 applet 的状态,并且它的init()方法被显示它的环境(通常是浏览器)调用。这就是为什么您需要将所有 GUI 代码(或任何初始化代码)放在它的init()方法中的原因。Applet类提供了一些其他的标准方法,您可以覆盖这些方法并编写您的逻辑来在 applet 中执行不同种类的工作。
运行 applet 的方式与运行 Swing 应用的方式不同。注意,applet 类没有main()方法。然而,从技术上来说,可以在 applet 中添加一个方法,但这对运行 applet 没有任何帮助。要查看 applet 的运行情况,您需要一个 HTML 文件。你应该有 HTML 的基本知识来使用 applet,但是你不需要成为 HTML 的专家。我将在下一节讨论如何查看 applet。这时候你就要编译HelloApplet类了。您将拥有一个名为HelloApplet.class的类文件。
部署 Applet
Applet 是 Java 程序。但是,它们不能像其他 Java 程序一样直接运行。在运行 applet 之前,您需要做一些准备工作。Applet 需要部署后才能使用。applet 部署分为两个部分:
- 定义 applet GUI 和逻辑的 Java 代码
- 一个 HTML 文档,包含 applet 的详细信息,如类名、包含类文件的存档文件名、宽度、高度等。
在上一节中,您已经看到了如何为 applet 编写 Java 代码。
使用<applet>标签将 applet 细节嵌入到 HTML 文档中。applet 代码和 HTML 文档都被部署到 web 服务器上。客户机上的浏览器向 web 服务器请求 HTML 文档。当浏览器在 HTML 文档中找到<applet>标记时,它会读取 applet 的详细信息,从 web 服务器下载 applet 代码,并在浏览器中将代码作为 applet 运行。这是否意味着您需要一个 web 服务器来查看您的 Applet 的运行情况?答案是否定的。你可以不用网络服务器来测试你的 Applet。如果您想让用户可以使用您的 Applet,您需要一个 web 服务器来部署您的 Applet。以下部分描述了如何为 applet 创建 HTML 文档,以及如何将 applet 部署到不同的环境中。
创建 HTML 文档
一个<applet>标签被用来在一个 HTML 文档中嵌入一个 applet。下面是一个<applet>标签的例子:
<applet code="com.jdojo.applet.HelloApplet" width="300" height="100" archive="myapplets.jar">
This browser does not support Applets.
</applet>
您需要指定标签的以下强制属性:
codewidthheightarchive
属性指定了 applet 的全限定类名。可选地,您可以将.class附加到 applet 的完全限定名。例如,以下两个<applet>标签的工作原理相同:
<!-- Use fully qualified name of the applet class as code -->
<applet code="com.jdojo.applet.HelloApplet">
...
</applet>
<!-- Use fully-qualified name of the applet class followed by .class -->
<applet code="com.jdojo.applet.HelloApplet.class">
...
</applet>
您也可以使用正斜杠而不是点来分隔子包名称。例如,您也可以将 code 属性的值指定为"com/jdojo/applet/HelloApplet"和com/jdojo/applet/HelloApplet.class。
width和height属性分别指定网页中 applet 区域的初始宽度和高度。您可以用像素或百分比指定width和height属性。如果值是数字,则以像素为单位;例如,width="150"表示 150 像素的width。如果值后面有一个百分号(%),则表示显示 Applet 的容器的尺寸百分比;例如,width="50%"表示 applet 的宽度将是其容器的 50%。通常,容器是浏览器窗口。
如果您正在使用 Java 7 Update 51 或更高版本来查看 applet,archive属性是必需的。您需要将一个 applet 的所有文件——类文件和其他资源文件——捆绑到一个 JAR 文件中。将 applet 文件捆绑在一个 JAR 文件中会使文件变得更小,从而加快 applet 用户的下载速度。属性的值是包含 applet 文件的 JAR 文件的名称。
如果浏览器不支持<applet>标签,您可能希望在网页中显示一条消息。消息应该放在<applet>和</applet>标签之间,如下所示。如果浏览器支持 Applet,它将忽略该消息。
<applet>
Inform the user that the browser does not support applets.
</applet>
清单 4-3 显示了用于测试 applet 的helloapplet.html文件的内容。注意,<applet>标签不包含archive属性,该属性允许您测试 applet,而不必创建 JAR 文件。
清单 4-3。helloapplet.html 档案的内容
<html>
<head>
<title>Hello Applet</title>
</head>
<body>
<h3>Hello Applet in Action</h3>
<applet code="com.jdojo.applet.HelloApplet" width="200" height="100">
This browser does not support Applets.
</applet>
</body>
</html>
在生产中部署 Applet
在生产环境中,您必须使用 JAR 文件部署 applet,并使用可信机构颁发的证书对 JAR 文件进行签名。对 JAR 进行自签名是行不通的。在测试环境中,您可以忽略这个需求,并且您可以使用一个未签名的 JAR 文件或者简单地使用类文件。在本章中,我将向你展示如何忽略这个需求来测试 Applet。如果你是第一次学习 Applet,你可以跳到下一节。当您需要在生产环境中部署您的 applet 时,您可以重温这一节。
使用以下步骤来打包和部署 Applet。这些步骤指的是与创建的 JAR 文件相关的术语和命令。有关创建 JAR 文件的更多细节,请参考《Java 语言特性入门》( ISBN: 978-1-4302-6658-7)一书中的第八章。
Create a manifest file (say manifest.mf). It must contain a Permissions attribute. The following shows the contents of the manifest file:
Manifest-Version: 1.0
Permissions: sandbox
Permissions属性的另一个值是all-permissions。sandbox的值表示 applet 将在安全沙箱中运行,并且不需要访问客户端机器上的任何额外资源。all-permissions的值表示 applet 需要访问客户端的机器。
Create a JAR file that contains all class files for the applet and the manifest file created in the previous section. You can use the following command to create the JAR file named helloapplet.jar:
jar cvfm helloapplet.jar manifest.mf com\jdojo\applet\*.class
Sign the helloapplet.jar file with the certificate you obtained from a trusted authority. Obtaining a certificate costs money (approximately $100). If you are just learning applets, you can skip this step. The “Signing Applets” section later in this chapter explains in detail how to sign an applet. Deploy the signed helloapplet.jar file to the web server. You will need to consult the documentation of your web server on how to deploy applets. Some web servers provide deployment screens to let you deploy your JAR files and some let you drop the JAR file into a specific directory. The typical way of deploying files to a web server is to let the development IDE such as NetBeans and Eclipse package and deploy the necessary project files for you.
部署 Applet 进行测试
如果你需要按照上一节描述的步骤来进行测试,那么打包和部署一个 applet 就太麻烦了。您可以在文件系统中保存所有的类文件和 HTML 文件,并测试您的 applet。我假设您有 applet 文件,并且它们的完整路径类似于以下路径:
C:\myapplets\helloapplet.htmlC:\myapplets\com\jdojo\applet\HelloApplet.class
使用 Windows 上使用的文件路径语法显示路径。如果您使用的不是 Windows,请将它们更改为操作系统使用的路径语法。
您不需要将 applet 文件存储在特定的目录中,如C:\myapplets。您可以用任何目录的路径替换目录C:\myapplets。但是,您必须保留在C:\myapplets目录之后的文件路径。在您阅读更多章节后,您将能够使用存储 applet 文件的目录结构。
如果您已经创建了helloapplet.jar文件来测试 applet,我假设您已经将archive属性添加到了helloapplet.html文件中的<applet>标签中作为archive="helloapplet.jar",并且文件路径如下所示:
C:\myapplets\helloapplet.htmlC:\myapplets\helloapplet.jar
安装和配置 Java 插件
浏览器使用 Java 插件来运行 Applet。在运行 applet 之前,您必须安装和配置 Java 插件。
安装 Java 插件
Java 运行时环境(JRE)也称为 Java 插件或 Java 附加组件。当您安装 JDK 时,JRE(以及 Java 插件)已经为您安装好了。运行 Applet 的客户机不需要安装 JDK。它可以只安装 JRE。您可以从 www.oracle.com 下载 JRE 的最新版本,在撰写本文时是 8.0。JRE 可以免费下载、安装和使用。
在 Windows 8 上,使用 64 位 JRE 8.0,我只能在 Internet Explorer 中运行我的 Applet。我不得不卸载 64 位的 JRE 8.0,安装 32 位的 JRE 9.0,这样我的 Applet 才能在所有浏览器中运行,比如谷歌 Chrome、Mozilla Firefox 和 Internet Explorer。
在 Linux 上,您需要做一些手动设置来为 Firefox 浏览器安装 Java 插件。请按照 www.oracle.com/technetwork/java/javase/manual-plugin-install-linux-136395.html 中的说明在 Linux 上设置 Java 插件。
打开 Java 控制面板
您可以使用 Java 控制面板程序配置 Java 插件。Java 控制面板程序启动如图 4-1 所示的窗口。
图 4-1。
The Java Control Panel
在 Windows 8 上,您可以通过以下步骤访问 Java 控制面板。
Open Search by pressing the Windows logo key + W. Make sure to select “Everywhere” for the search location. By default “Settings” is selected. Enter “Java,” “Java Control Panel,” or “Configure Java” as the search term. Click the Java icon to open the Java Control Panel. If you could not find the Java Control Panel using Search, open the Control Panel by right-clicking the Start icon and selecting Control Panel from the menu. In the top-right corner in the Control Panel, you get a Search field. Enter “Java” in the Search field and you will see a program named Java. Click the program name to open the Java Control Panel.
在 Windows 7 上,您可以通过以下步骤访问 Java 控制面板。
Click the Start button, and then select the Control Panel option from the menu. Enter Java Control Panel in the Search field in Control Panel. Click the Java icon to open the Java Control Panel. Tip
在 Windows 上,您可以通过运行位于JRE_HOME\bin目录下的文件javacpl.exe直接启动 Java 控制面板。对于 JRE 8,默认路径是C:\Program Files\Java\jre8\bin\javacpl.exe。
在 Linux 上,您可以通过从终端窗口运行ControlPanel程序来访问 Java 控制面板。ControlPanel程序安装在JRE_HOME\bin目录中,其中 JRE_HOME 是您安装 JRE 的目录。假设您已经在/java8/jre目录下安装了 JRE。您需要从终端窗口运行以下命令:
[/java8/jre/bin]$ ./ControlPanel
在 Mac OS X (10.7.3 及更高版本)上,您可以使用以下步骤访问 Java 控制面板:
- 点击屏幕左上角的苹果图标,进入系统偏好设置。
- 单击 Java 图标访问 Java 控制面板。
配置 Java 插件
您可以使用 Java 控制面板为 Java 插件配置各种设置。在这一节中,我将描述如何绕过运行 applets 的签名 JAR 要求。打开 Java 控制面板,选择安全选项卡,如图 4-2 所示。
图 4-2。
Configuring the security settings in the Java Control Panel
标记为“在浏览器中启用 Java 内容”的复选框可让您启用/停用在浏览器中运行 Applet 的支持。默认情况下,此复选框处于选中状态,Applet 可以在浏览器中运行。如果未选中此复选框,您将无法在浏览器中运行 Applet。
第二个设置是安全级别,可以通过滑动垂直滑块控件的旋钮来设置。它可以设置为以下三个值:
- 非常高:这是最严格的安全级别设置。只有具有有效证书并且在主 JAR 文件的清单中包含了
Permissions属性的已签名的 Applet 才允许在有安全提示的情况下运行。所有其他 Applet 都被阻止。 - 高:这是推荐的最低默认安全级别设置。使用有效或过期证书签名的 Applet 以及在主 JAR 文件的清单中包含
Permissions属性的 Applet 允许在有安全提示的情况下运行。当无法检查证书的撤销状态时,Applet 也允许在安全提示下运行。所有其他 Applet 都被阻止。 - 中:仅阻止请求所有权限的未签名 Applet。所有其他 Applet 都允许在安全提示下运行。不建议选择此安全级别。如果你运行一个恶意的 Applet,它会使你的计算机更容易受到攻击。
出于测试目的,您可以将安全级别设置为中。这将允许您测试打包在未签名的 JAR 文件中的 Applet。您也不需要在清单文件中包含Permissions属性。它还允许您从文件系统测试您的 Applet,避免了 web 服务器部署您的 Applet 的需要。完成测试后,您应该将安全设置改回推荐的高或非常高。请注意,当您尝试运行任何不符合安全要求的 Applet 时,使用“中”安全级别设置会向您显示警告。当您收到警告时,您需要确认是否要继续运行 Applet,尽管存在安全风险。
“安全”选项卡上的第三项设置称为“例外站点列表”。这使您可以绕过指定站点的安全级别设置所需的安全要求。点击“编辑站点列表”按钮,打开异常站点列表对话框,如图 4-3 所示。
图 4-3。
The Exception Site List Dialog Box
点击Add按钮。您将看到为该位置添加了一个空行。输入位置的file:///(注意三个///)。再次点击Add按钮。第二次点击Add按钮会显示安全警告信息,说明添加file://(注二/ /)有安全风险。点击警告对话框上的Continue按钮。您会得到另一个空行位置。输入http://localhost:8080。重复此步骤再添加一个位置, http://www.jdojo.com 。异常站点列表对话框应如图 4-4 所示。现在,单击OK按钮返回到安全选项卡。
图 4-4。
The Exception Site List Dialog Box
从现在开始,无论“安全级别”设置如何,您都可以从以下三个站点运行所有 Applet:
file:///表示使用file协议的文件系统中的 Applet。http://localhost:8080是指使用http协议在您机器上的 8080 端口运行的任何 web 服务器。http://www.jdojo.com表示使用http协议从网站www.jdojo.com运行的 Applet。我维护jdojo.com。您可以使用 URLhttp://www.jdojo.com/myapplets/helloapplet.html访问 hello Applet。
一旦您完成测试您的 Applet,请从例外列表中删除这些网站,这样您的计算机就不会运行恶意 Applet。
查看 Applet
如果您已经遵循了前面几节中的步骤,查看 applet 就像在浏览器中输入hellapapplet.html文件的 URL 一样简单。按照以下步骤查看 Applet。
Open the browser of your choice, such as Google Chrome. Mozilla Firefox, or Internet Explorer. Press Ctrl + O or select the Open menu option from the File menu. You will get a browse/open dialog box. Navigate to the directory in which you have stored the helloapplet.html file and open it in the browser. Depending on the settings in the Java Control Panel, you may get security warnings, which you need to ignore. Alternatively, you can enter the URL for the HTML file directly. If you saved the helloapplet.html file in the C:\myapplets directory in windows, you can enter the URL as file:///C:/myapplets/helloapplet.html. If everything was set up correctly, you will see the applet running in your browser as shown in Figure 4-5. Enter your name and click the Say Hello button to display a greeting dialog box.
图 4-5。
The Hello Applet running from the file system in the Google Chrome browser
如果您无法使用这些步骤查看 Applet,请阅读下一节,该节将描述如何使用appletviewer在测试期间查看 Applet。
使用 appletviewer 测试 Applet
您可以使用appletviewer命令查看 Applet。它在JAVA_HOME\bin文件夹中作为appletviewer程序提供,其中JAVA_HOME是您机器上的 JDK 安装文件夹。以下是命令语法的一般形式:
appletviewer <options> <urls>
在<options>中,您可以指定命令的各种选项。您必须指定一个或多个由空格分隔的包含 Applet 文档的 URL。您可以使用以下任何命令来查看上一节中描述的 applet。在 Microsoft Windows 上,可以使用命令提示符输入命令。在 Linux 上,使用终端窗口。
appletviewerhttp://www.jdojo.com/myapplets/helloapplet.html
或者
appletviewer file:///C:/myapplets/helloapplet.html
当您运行上述命令时,可能会出现以下错误:
'appletviewer' is not recognized as an internal or external command, operable program or batch file.
如果您收到上述错误,您需要指定 appletviewer 命令的完整路径,例如C:\java8\bin\appletviewer,假设您已经在C:\java8目录中安装了 JDK。您可以在 Windows 命令提示符下尝试以下命令:
C:\java8\appletviewerhttp://www.jdojo.com/myapplets/helloapplet.html
图 4-6 显示了在 appletviewer 窗口中运行的 Applet。请注意,appletviewer 只显示 URL 中指定的文档中的 applet。所有其他 HTML 内容都会被忽略。例如,applet 不显示您在<h3>标签中添加的来自helloapplet.html文件的文本。
图 4-6。
The Hello Applet running from the file system in the Google Chrome browser
如果您想使用appletviewer命令查看多个 Applet,您可以通过在命令行上指定多个 URL 来实现。每个 Applet 将显示在单独的 Applet 查看器窗口中。以下命令可用于显示来自两个不同 web 服务器的两个 Applet,其中URL_PART1可以是http://www.myserver1.com/myapplets1URL_PART2可以是 http://www.myserver2.com/myapplets2 :
appletviewer URL_PART1/applet1.html URL_PART2/applet2.html
appletviewer 命令在单独的窗口中显示文档中的每个 Applet。例如,如果applet1.html包含两个 Applet,而applet2.html包含三个 Applet,上述命令将打开五个 Applet 查看器窗口。如果 URL 引用的文档不包含任何 applet,appletviewer命令不做任何事情。URL 引用的文档中的内容将被忽略,与 applet 相关的部分除外。appletviewer 窗口有一个名为“applet”的主菜单,可以让你重新加载、重启、停止、保存一个 Applet,等等。
您可以为appletviewer命令指定三个选项:
-debug-encoding-Jjavaoptions
–选项允许您在调试模式下启动 appletviewer。您可以使用–选项指定 URL 引用的文档编码。–选项允许您为 JVM 指定任何 Java 选项。选项的–J部分被删除,剩余部分被传递给 JVM。以下是使用这些选项的示例。注意,要为 appletviewer 指定 classpath 环境变量,需要指定两次–J选项。
appletviewer –debug your_document_url_goes_here
appletviewer –encoding ISO-8859-1 your_document_url_goes_here
appletviewer –J-classpath -Jc:\myclasses your_document_url_goes_here
Tip
如果您使用 NetBeans IDE 开发 Applet,请右键单击 Applet 文件,例如 IDE 中的HelloApplet.java,并选择Run File菜单选项,在 appletviewer 中运行您的 Applet。
使用 codebase 属性
在HelloApplet示例中,您将 Java 类文件和 HTML 文件放在同一个父目录下。您的文件放置如下:
ANY_DIR\html_fileANY_DIR\package_directories\class_file
您不必遵循上述目录结构来使用您的 Applet。存储 applet 的 HTML 文件的父目录称为文档。存储 Java 类文件(总是考虑放置 applet 类的包所需的目录结构)的父目录称为代码库。您可以使用codebase属性在<applet>标签中为您的 applet 指定一个代码库。如果不指定codebase属性,则文档库被用作codebase。codebase属性可以是相对 URL,也可以是绝对 URL。使用代码库的绝对 URL 为存储 applet 类文件开辟了另一种可能性。您可以将 applet 的 HTML 文件存储在一个 web 服务器上,而将 Java 类存储在另一个 web 服务器上。在这种情况下,您必须为 java 类指定一个绝对的codebase。
使用 HTML 文档中的<base>标签的href属性的值来解析codebase属性的相对 URL。如果 HTML 文档中没有指定<base>标签,则使用下载 HTML 文档的 URL 来解析相对的codebase URL。我们来看一些例子。
例 1
一个helloapplet.html文件的内容如下。注意,您包含了一个<base>标记,并且没有为<applet>标记指定codebase属性。
<html>
<head>
<title>Hello Applet</title>
<base href="http://www.jdojo.com/myapplets/myclasses
</head>
<body>
<applet code="com.jdojo.applet.HelloApplet" width="150" height="100">
This browser does not support Applets.
</applet>
</body>
</html>
使用 URL http://www.jdojo.com/myapplets/helloapplet.html 下载文档。既然你已经指定了<base>标签,浏览器将在 http://www.jdojo.com/myapplets/myclasses/com/jdojo/applet/HelloApplet.class 寻找 applet 的类文件。
示例 2
一个helloapplet.html文件的内容如下。注意,您包括了<base>标签,并且没有将<applet>标签的codebase属性指定为mydir。
<html>
<head>
<title>Hello Applet</title>
<base href="http://www.jdojo.com/myapplets/myclasses
</head>
<body>
<applet code="com.jdojo.applet.HelloApplet" width="150" height="100"
codebase="mydir">
This browser does not support Applets.
</applet>
</body>
</html>
使用 URL http://www.jdojo.com/myapplets/helloapplet.html 下载文档。既然你已经指定了<base>标签,浏览器将在 http://www.jdojo.com/myapplets/myclasses/mydir/com/jdojo/applet/HelloApplet.class 寻找 applet 的类文件。注意,mydir的codebase值是使用<base>标签的href值解析的。如果您将codebase值指定为../xyzdir(两个点表示向上一个目录),浏览器将在 http://www.jdojo.com/myapplets/xyzdir/com/jdojo/applet/HelloApplet.class 处查找类文件。注意,出于安全原因,有些浏览器不允许您指定两个点来表示目录层次结构中的上一级,作为codebase URL 的一部分。
例 3
一个helloapplet.html文件的内容如下。请注意,您没有包含<base>标签,而是为<applet>标签指定了codebase属性。
<html>
<head>
<title>Hello Applet</title>
</head>
<body>
<applet code="com.jdojo.applet.HelloApplet"
width="150" height="100" codebase="abcdir">
This browser does not support Applets.
</applet>
</body>
</html>
使用 URL http://www.jdojo.com/myapplets/helloapplet.html 下载文档。由于您没有指定<base>标签,代码库的相对 URL 将使用用于下载 HTML 文件的 URL 进行解析,浏览器将在 http://www.jdojo.com/myapplets/abcdir/com/jdojo/applet/HelloApplet.class 处查找类文件。
如果您为codebase使用绝对 URL,浏览器将使用该绝对 URL 查找 applet 的类文件,而不管 HTML 文件中是否存在标签,也不管 HTML 文件是从哪里下载的。让我们考虑下面的<applet>标签:
<applet code="com.jdojo.applet.HelloApplet" width="150" height="100"
codebase="http://www.jdojo.com/myclasses
This browser does not support Applets.
</applet>
浏览器会在 http://www.jdojo.com/myclasses/com/jdojo/applet/HelloApplet.class 寻找 Applet 的类文件。如果想将 applet 的类文件和 HTML 文件存储在不同的服务器上,需要将codebase值指定为绝对 URL。
Applet类提供了两个名为getDocumentBase()和getCodeBase()的方法来分别获取文档基 URL 和代码基 URL。getDocumentBase()方法返回嵌入了<applet>标签的文档的 URL。例如,如果您在浏览器中输入 URL http://www.jdojo.com/myapplets/helloapplet.html 来查看 Applet,那么getDocumentBase()方法将返回 http://www.jdojo.com/myapplets/helloapplet.html 。getCodeBase()方法返回用于下载 applet 的 Java 类的目录的 URL。从这个方法返回的 URL 取决于许多因素,正如您刚才在示例中看到的那样。
Applet 的生命周期
applet 在其存在期间会经历不同的阶段。它被创建、初始化、启动、停止和销毁。applet 首先通过调用其构造函数来创建。在创建它的时候,它是一个简单的 Java 对象,并没有获得它的“applet”状态。在创建之后,它获得它的 applet 状态,并且在Applet类中有四个方法被浏览器调用。您可以在这些方法中放置代码来执行不同种类的逻辑。这些方法如下:
init()start()stop()destroy()
init()方法
在 applet 被实例化和加载后,浏览器调用该方法。您可以重写此方法来放置为您的 applet 执行初始化逻辑的任何代码。通常,您将在这个方法中放置代码来为您的 applet 创建 GUI。这个方法在 applet 的生命周期中只被调用一次。
start()方法
紧接在init()方法之后调用start()方法。它可能被多次调用。假设您正在查看网页中的 applet,并且您通过替换 applet 的网页在同一浏览器窗口(或选项卡)中打开了另一个网页。如果你返回到前一个网页,如果 Applet 被缓存,它的start()方法将被再次调用。如果当你用另一个网页替换 Applet 的网页时,Applet 被破坏了,它的生命周期将重新开始,它的init()和start()方法将被依次调用。您可以在此方法中放置任何启动进程的代码,例如当 applet 显示在网页上时的动画。
stop()方法
该方法是start()方法的对应物。它可能被多次调用。通常,当显示 applet 的网页被另一个网页替换时,会调用该函数。它也在调用destroy()方法之前被调用。通常,在这个方法中放置代码来停止任何进程,比如在start()方法中启动的动画。
destroy()方法
当 applet 被销毁时,调用该方法。您可以放置执行逻辑的代码来释放在 applet 生命周期中被占用的任何资源。总是在调用destroy()方法之前调用stop()方法。这个方法在 applet 的生命周期中只被调用一次。
清单 4-4 包含了一个 applet 的代码,当调用 applet 的init()、start()、stop()和destroy()方法时,它会显示一个对话框。它包括消息中调用start()和stop()方法的次数。
清单 4-4。演示 Applet 生命周期的 Applet
// AppletLifeCycle.java
package com.jdojo.applet;
import javax.swing.JApplet;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
public class AppletLifeCycle extends {
private int startCount = 0;
private int stopCount = 0;
@Override
public void init() {
this.getContentPane().add(new JLabel("Applet Life Cycle!!!"));
JOptionPane.showMessageDialog(null, "init()");
}
@Override
public void start() {
startCount++;
JOptionPane.showMessageDialog(null, "start(): " + startCount);
}
@Override
public void stop() {
stopCount++;
JOptionPane.showMessageDialog(null, "stop(): " + stopCount);
}
@Override
public void destroy() {
JOptionPane.showMessageDialog(null, "destroy()");
}
}
清单 4-5 包含了查看AppletLifeCycleApplet 的 HTML 文件的内容。它假设 HTML 文件和 Java 类文件将放在如下所示的目录结构中:
ANY_DIR\appletlifecycle.html
ANY_DIR\com\jdojo\applet\AppletLifeCycle.class
如果您有不同的目录结构,您可能需要在一个<applet>标签中包含codebase属性。您可以使用前面描述的步骤查看 Applet。
清单 4-5。查看 AppletLifeCycle Applet 的 AppletLifeCycle 文件的内容
<html>
<head>
<title>Lifecycle of an Applet</title>
</head>
<body>
<applet code="com.jdojo.applet.AppletLifeCycle"
height="200" width="200">
This browser does not support Applets.
</applet>
</body>
</html>
将参数传递给 Applet
您可以让 Applet 的用户通过在 HTML 文档中向它传递参数来配置 Applet。您可以使用<applet>标签中的<param>标签向 applet 传递参数。<param>标签有两个属性叫做name和value。<param>标签的name和value属性分别用于指定参数的名称和值。您可以使用多个<param>标签向 applet 传递多个参数。下面的 HTML 代码片段向 applet 传递两个参数:
<applet code="MyApplet" width="100" height="100">
<param name="buttonHeight" value="20" />
<param name="buttonText" value="Hello" />
</applet>
参数名为buttonHeight和buttonText,其值分别为20和Hello。确保 applet 参数的名称有意义,对阅读它们的用户有意义。从技术上讲,任何字符串都可以作为参数名。比如,从技术上来说,p1和p2是和buttonHeight和buttonText一样好的参数名。然而,后者对用户更有意义。
Applet类提供了一个方法,该方法接受参数名作为其参数,并将参数值作为String返回。注意,不管参数的值是多少,它总是返回一个String。例如,如果您想将参数buttonHeight的值20作为一个整数,您需要在 applet 的 Java 代码中将String转换成一个整数。传递给getParameter()方法的参数名称不区分大小写;getParameter("buttonHeight")和getParameter("BUTTONHEIGHT")都返回与String相同的20值。如果 HTML 文档中没有设置指定的参数,getParameter()方法返回null。以下代码片段演示了如何在 applet 代码中使用getParameter()方法:
// buttonHeight and buttonText will get the values 20 and Hello
String buttonHeight = getParameter("buttonHeight");
String buttonText = getParameter("buttonText") ;
// bgColor will be null as there is no backgroundColor parameter set
String bgColor = getParameter("backgroundColor");
您可以使用参数自定义 applet 的一些方面。如果参数值发生变化,您不必更改代码。如果您向 applet 传递参数,请确保为每个参数分配一个默认值,以防在 HTML 文档中没有设置该值。例如,您可以将 applet 的背景颜色设置为 applet 的参数。如果没有设置,可以默认为灰色或白色。
清单 4-6 显示了一个AppletParameters applet 的代码。它使用两个 GUI 组件,一个显示欢迎消息的JTextArea和一个JButton。欢迎消息和按钮的文本可以通过两个名为welcomeText和helloButtonText的参数定制。applet 代码读取其init()方法中的两个参数值。如果 HTML 文档中没有设置参数的默认值,它将设置这些参数的默认值。清单 4-7 包含了 HTML 文件的内容,图 4-7 显示了 Applet 的运行。图 4-8 显示了当您点击 Say Hello 按钮时显示的消息框。
清单 4-6。使用标签向 Applet 传递参数
// AppletParameters.java
package com.jdojo.applet;
import java.awt.Container;
import java.awt.FlowLayout;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
public class AppletParameters extends JApplet {
private JTextArea welcomeTextArea = new JTextArea(2, 20);
private JButton helloButton = new JButton();
@Override
public void init() {
Container contentPane = this.getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.add(new JScrollPane(welcomeTextArea));
contentPane.add(helloButton);
// Show parameters when the button is clicked
helloButton.addActionListener(e -> showParameters());
// Make the welcome JTextArea non-editable
welcomeTextArea.setEditable(false);
// Display the welcome message
String welcomeMsg = this.getParameter("welcomeText");
if (welcomeMsg == null || welcomeMsg.equals("")) {
welcomeMsg = "Welcome!";
}
welcomeTextArea.setText(welcomeMsg);
// Set the hello button text
String helloButtonText = this.getParameter("helloButtonText");
if (helloButtonText == null || helloButtonText.equals("")) {
helloButtonText = "Hello";
}
helloButton.setText(helloButtonText);
}
private void showParameters() {
String welcomeText = this.getParameter("welcomeText");
String helloButtonText = this.getParameter("helloButtonText");
String msg = "Parameters passed from HTML are\nwelcomeText="
+ welcomeText + "\nhelloButtonText=" + helloButtonText;
JOptionPane.showMessageDialog(null, msg);
}
}
清单 4-7。用于查看 Applet 参数 Applet 的 AppletParameters 文件的内容
<html>
<head>
<title>Applet Parameters</title>
</head>
<body>
<applet code="com.jdojo.applet.AppletParameters"
width="300" height="50">
<param name="welcomeText"
value="Welcome to the applet world!"/>
<param name="helloButtonText"
value="Say Hello"/>
This browser does not support Applets.
</applet>
</body>
</html>
图 4-8。
The AppletParameters applet running in a browser
图 4-7。
The AppletParameters applet running in a browser Tip
您还可以使用Applet类的方法来获取标签的属性值。例如,您可以使用 getParameter("code")来获取标签的code属性的值。
发布 Applet 的参数信息
applet 允许您发布关于它所接受的参数的信息。您可以开发一个知道其参数的 applet。您的 Applet 可能会被其他用户使用不同的 Applet 查看器查看。发布您的 applet 接受的参数可能对托管 applet 的程序和查看它的用户有所帮助。例如,applet 查看器可以让用户交互地改变 applet 的参数并重新加载 applet。Applet类提供了一个方法,您需要在 applet 类中覆盖它来发布关于 applet 参数的信息。它返回一个二维(nX3)数组String。默认情况下,它返回null。数组的行数应该等于它接受的参数数。每行应该有三列,包含参数的名称、类型和描述。在你的 Applet 中实现getParameterInfo()方法并不是你的 Applet 工作的必要条件。但是,通过这种方法提供关于 applet 参数的信息是一种很好的做法。让我们假设下面的<applet>标签用于显示您的 applet:
<applet code="MyApplet" width="100" height="100">
<param name="buttonHeight" value="20" />
<param name="buttonText" value="Hello" />
</applet>
MyApplet类方法的一个可能实现如下。注意,作为开发者,你只是 Applet 参数信息的发布者。这取决于 applet 浏览器程序以他们选择的任何方式使用它。
public class MyApplet extends JApplet {
// Other code for applet goes here...
// Public applet's parameter info
public String[][] getParameterInfo() {
String[][] parametersInfo =
{ {"buttonHeight",
"integer",
"Height for the Hello button in pixel"
},
{"buttonText",
"String",
"Hello button's text"
}
};
return parametersInfo;
}
}
发布 Applet 的信息
Applet类提供了一个应该返回 applet 文本描述的方法。该方法的默认实现返回null。从这个方法返回 applet 的简短描述是一个很好的实践,这样 applet 的用户可以对 applet 有更多的了解。该描述可以由用于查看 Applet 的工具以某种方式显示。下面的代码片段说明了如何使用getAppletInfo()方法来提供关于 applet 的信息:
public class MyApplet extends JApplet {
// Other applet's logic goes here...
public String getAppletInfo() {
return "My Demo Applet, Version 1.0, No Copyright";
}
}
标签的其他属性
表 4-1 列出了<applet>标签的所有属性及其用法。除了这个表中列出的属性,你还可以使用其他一些标准的 HTML 属性,比如id、style等。带着<applet>标签。
表 4-1。
The List of Attributes for the tag
| 名字 | 使用 | | --- | --- | | `Code` | 指定 Applet 的类的完全限定名或 Applet 的类文件名。 | | `codebase` | 指定包含 applet 类的基本目录的 URL。如果未指定,则使用标记中指定的文档的基本 URL 或下载文档的 URL 作为其值。它的值可以是相对或绝对 URL。相对 URL 是基于``标签值中的文档基本 URL(如果存在)或下载文档的 URL 来解析的。 | | `Width` | 以像素或其容器宽度的百分比指定 Applet 的宽度。例如,`width="100"`将 applet 的宽度指定为 100 像素,而`width="30%"`将 applet 的宽度指定为其容器宽度的 30%。 | | `height` | 以像素或其容器高度的百分比指定 applet 的高度。例如,`height="200"`指定 applet 的高度为 200 像素,而`height="20%"`指定 applet 的高度为其容器高度的 20%。 | | `archive` | 指定逗号分隔的归档文件(JAR 或 ZIP 文件)列表。存档文件可能包含类别和其他资源,如图像、音频等。,由 applet 使用。存档文件可以使用相对或绝对 URL。使用`codebase`属性的值解析相对 URL。如果您的 applet 使用打包在归档文件中的多个类和其他资源,下载时间会大大减少。如果你不使用存档,你的 applet 的每个类和资源将在需要的时候单独下载。如果您对它们进行存档,存档中包含的所有文件都将通过一个到服务器的连接进行下载,从而减少了下载时间。您可以选择将一些文件放在归档中,将一些文件放在目录中。如果您的 applet 需要一个资源,它首先在归档中寻找,然后在服务器上由`codebase`属性值指定的目录中寻找。 | | `object` | 指定包含 applet 序列化形式的文件的名称。您可以指定一个`code`属性或一个`object`属性,但不能同时指定两者。当 applet 显示时,它将被反序列化,并且它的`init()`方法不会被调用。它的`start()`方法将被调用。这个属性不常使用。 | | `Name` | 指定 Applet 的名称。您可以使用 Applet 的名称来查找在同一网页中运行的其他 Applet。您还可以通过使用一个``标签来指定 applet 的名称,该标签的`name`属性值为“name”。下面这两个都会把 Applet 的名字设置为`myapplet1` : `` `...` ``或者`` `` ``你可以通过使用`Applet`类的`getParameter("name")`方法来获得一个 Applet 的名字。 | | `alt` | 指定当浏览器理解``标签但无法运行 applet 时要显示的替换文本。最好在``和``标签之间使用文本来显示替换文本,替换文本中也可以包含 HTML 格式。 | | `align` | 指定 Applet 相对于周围内容的位置。其值可以是`bottom`、`middle`、`top`、`left`或`right`。请注意,该属性指定了 applet 相对于其周围环境的位置,而不是相对于其容器的位置。比如使用`align="middle"`不会让 Applet 出现在浏览器窗口中间。如果您想将 applet 放在浏览器窗口的中间,您需要使用另一种 HTML 技术,例如将``标签放在另一个容器(如``)中,然后设置`align`属性。例如,下面这段 HTML 代码将在浏览器窗口的中央放置一个 applet:`
``...``
` | | `hspace` | 以像素为单位指定 applet 左侧和右侧的间距。 | | `vspace` | 以像素为单位指定 Applet 顶部和底部的间距。 |在 Applet 中使用图像
在 applet 中使用图像很简单。Applet类有一个重载的getImage()方法,该方法返回一个java.awt.Image对象。下面是这种方法的两个版本:
Image getImage(URL imageAbsoluteURL)Image getImage(URL baseURL, String imageURLPath)
第一个版本采用图像的绝对 URL,如 http://www.jdojo.com/myapplets/img/welcome.jpg 。第二个版本采用图像的基本 URL 和 URL 路径。使用第一个参数解析图像的 URL,该参数是基本 URL。考虑 applet 中的以下代码片段:
URL baseURL = new URL("http://www.jdojo.com/myapplets/abc.html
Image welcomeImage = getImage(baseURL, "img/welcome.jpg");
将使用基本 URL 和来自 http://www.jdojo.com/myapplets/img/welcome.jpg 的相对图像的 URL 来获取welcome.jpg文件的内容。考虑 applet 中的以下代码片段:
URL baseURL = new URL("http://www.jdojo.com/myapplets/abc.html
Image welcomeImage = getImage(baseURL, "/img/welcome.jpg");
这一次,图像 URL 路径(/img/welcome.jpg)以正斜杠开始。该 URL 路径将被解析为 http://www.jdojo.com/img/welcome.jpg 。您可以将所有图像存储在存储 HTML 文档的目录下,并始终使用从getDocumentBase()方法返回的文档基 URL 作为基 URL 来获取图像。
getImage()方法立即返回。当 Applet 需要绘制图像时,就会下载图像。
清单 4-8 包含了一个使用图像的 applet 的代码。清单 4-9 给出了查看 applet 的 HTML 内容。
清单 4-8。在 Applet 中使用图像
// ImageApplet.java
package com.jdojo.applet;
import java.awt.Container;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JApplet;
import javax.swing.JLabel;
public class ImageApplet extends JApplet {
JLabel imageLabel;
@Override
public void init() {
Container contentPane = this.getContentPane();
Image img = this.getWelcomeImage();
if (img == null) {
imageLabel = new JLabel("Image parameter not set...");
}
else {
imageLabel = new JLabel(new ImageIcon(img));
}
contentPane.add(imageLabel);
}
private Image getWelcomeImage() {
Image img = null;
String imageURL = this.getParameter("welcomeImageURL");
if (imageURL != null) {
img = this.getImage(this.getDocumentBase(), imageURL);
}
return img;
}
}
清单 4-9。imageapplet.html 档案的内容
<html>
<head>
<title>Using Images in Applet</title>
</head>
<body>
<applet code="com.jdojo.applet.ImageApplet"
width="250" height="200">
<param name="welcomeImageURL"
value="img/welcome.jpg"/>
This browser does not support Applets.
</applet>
</body>
</html>
此示例假设了以下目录结构,其中ANY_DIR表示您的 web 服务器或本地文件系统中的目录:
ANY_DIR\myapplets\imageapplet.htmlANY_DIR\myapplets\images\welcome.jpgANY_DIR\myapplets\com\jdojo\applet\ImageApplet.class
相对于文档库的图像 URL 路径被指定为参数。如果 HTML 代码中没有指定图像 URL,applet 会在JLabel中显示一个字符串。如果您的目录结构与上面列出的不同,您需要修改 applet 的代码或 HTML 内容,然后才能成功运行该示例。
在 Applet 中播放音频剪辑
在 Applet 中播放音频剪辑很容易。Applet类有一个重载的getAudioClip()方法,该方法返回一个java.applet.AudioClip接口的实例。下面是这种方法的两个版本:
AudioClip getAudioClip(URL audioAbsoluteURL)AudioClip getAudioClip(URL baseURL, String audioURLPath)
该方法的工作方式与getImage()方法相同。它会立即返回。音频剪辑在播放时被载入。AudioClip接口声明如下:
package java.applet;
public interface AudioClip {
void play();
void stop();
void loop();
}
您可以使用play()方法开始播放音频剪辑。您可以使用stop()方法停止播放音频剪辑。您可以使用其loop()方法循环播放音频剪辑。以下代码片段演示了如何在 applet 中使用音频剪辑:
// Assuming that audios/myaudio.wav is stored under a directory
// where the HTML file for the applet is stored
AudioClip clip = getAudioClip(getDocumentBase(), "audios/myaudio.wav");
clip.play(); //* Play the clip
// Other logic goes here
clip.stop(); // Stop the clip
Applet类包含一些方便的方法,让你不用处理AudioClip接口就可以播放一个音频剪辑。Applet 将下载音频剪辑并为您播放。您只需要指定音频剪辑的 URL 并使用Applet类的play()方法,如下所示:
// Assuming that the following code is inside your applet class
this.play(this.getDocumentBase(), "audios/myfav.wav");
如果你想在 Java 应用中播放一个音频剪辑,使用Applet类的newAudioClip()静态方法来获取AudioClip对象,如下所示:
URL myFavAudioURL = new URL("http://www.jdojo.com/myfav.wav
AudioClip clip = Applet.newAudioClip(myFavAudioURL);
clip.play();
与 Applet 环境交互
applet 上下文是指运行 applet 的环境,例如浏览器、applet 浏览器等。接口的一个实例代表了 applet 的上下文。Applet类提供了一个返回 applet 上下文的方法。使用一个AppletContext对象,你可以打开一个新的文档,在 applet 的容器状态栏中显示一条消息,并获得对同一文档中运行的另一个 applet 的引用。下面的代码片段演示了AppletContext对象的一些用法:
// Get the applet context object
AppletContext context = getAppletContext();
// Open the Yahoo's home page in a new window
URL yahooURL = null;
try {
yahooURL = new URL("http://www.yahoo.com
context.showDocument(yahooURL, "_blank");
}
catch (MalformedURLException e) {
e.printStackTrace();
}
// Show a brief message in the status bar
context.showStatus("Welcome to the applet world!");
// Get reference of another applet named "crazyApplet"
Applet crazyApplet = context.getApplet("crazyApplet");
if (crazyApplet != null) {
context.showStatus("Found the crazy applet...");
// Now you can invoke methods on crazyApplet
}
该方法打开由第一个参数指定的另一个文档。通过使用它的第二个参数,您可以控制显示新文档的窗口。第二个参数的有效值为:"_self"、"_parent"、"_top"、"_blank"和"any existing/non-existing frame/window name"。标准 HTML/JavaScript 代码中也使用相同的值。
方法用于在浏览器的状态栏中显示一条简短但不太重要的消息。浏览器也使用相同的状态栏来显示消息。您不应该使用这种方法显示需要用户注意的重要消息。用户可能看不到您的消息,或者它可能在用户有机会看到之前被覆盖。如果需要显示重要消息,应该考虑在 applet 区域显示。
getApplet()和getApplets()方法用于查找同一文档中运行的其他 Applet。有关 Applet 如何与其他 Applet 通信的更多详细信息,请参考下一节。
Tip
创建 applet 对象时,applet 上下文不可用。在 applet 的构造函数中调用getAppletContext()方法会返回null。getImage()和getAudioClip()方法调用AppletContext对象中相应的方法。由于 applet 的AppletContext对象在 applet 的构造函数执行时不可用,所以不要在其构造函数中使用Applet类的getImage()和getAudioClip()方法。
Applet、HTML 和 JavaScript 的交流
applet 可以使用它的showDocument()方法打开另一个 HTML 文档。它还可以通过使用它的showStatus()方法在浏览器的状态栏上显示一条简短的消息。当您使用 Applet 时,还有许多其他的可能性。事实上,小应用、HTML 和 JavaScript 可以愉快地共存,它们可以相互融合。以下是一些可能性:
- 一个 applet 可以与同一个 HTML 文档中的另一个 applet 通信。
- applet 可以通过调用 JavaScript 函数与 JavaScript 进行通信。
- JavaScript 可以通过访问 applet 的方法和字段与 applet 进行通信。
图 4-9 显示了 Applet、HTML 和 JavaScript 之间可能的交互。
图 4-9。
Communication between applets, HTML, and JavaScript
在一个 applet 能够与另一个 applet 通信之前,它必须找到它想要与之通信的 applet。AppletContext类有两个方法让 applet 在同一个 HTML 文档中找到另一个 applet:
Applet getApplet(String appletName)Enumeration<Applet> getApplets()
getApplet()方法要求您传递您正在寻找的 applet 的名称,如果找到了,它将返回 applet 的引用。如果没有找到 applet,它返回null。要使此方法有效,您必须为要找到的 applet 指定一个名称。您可以使用如下所示的方法:
import java.applet.Applet;
...
Applet app = getAppletContext().getApplet("applet2");
if (app == null) {
// applet2 is not found
}
else {
// Work with applet2 object.
}
getApplets()方法返回页面上所有 Applet 的Enumeration,包括调用该方法的 Applet。您可以使用如下所示的方法:
import java.applet.Applet;
import java.util.Enumeration;
...
Enumeration<Applet> e = getAppletContext().getApplets();
while(e.hasMoreElements()) {
Applet app = e.nextElement();
// Work with app applet now
}
applet 可以使用netscape.javascript.JSObject类与 JavaScript 通信。JSObject类不是标准 Java 库的一部分。如果你已经安装了 JRE,它被打包在plugin.jar文件中,这个文件存储在一个JRE_HOME\lib文件夹中。如果你在你的 Applet 中使用了JSObject,你需要在你的CLASSPATH中包含这个文件,这样你的 Applet 的代码才能被编译。您可以使用JSObject.getWindow()静态方法获取浏览器窗口的引用。您需要将 applet 的引用传递给JSObject.getWindow()方法。以下代码片段演示了如何从 applet 调用 JavaScript 方法:
// Need to import the JSObject class
import netscape.javascript.JSObject;
// Get the reference of the browser window
JSObject browserWindow = (JSObject)JSObject.getWindow(this);
/* You need to use the call() method of the browserWindow passing the
JavaScript function name as a string and arguments as an Object array.
Assume that helloJS(msgText) is a JavaScript function which accepts a
string argument and returns some value.
*/
String methodName = "helloJS";
Object[] methodArgs = {"Hello from applet"};
Object returnValue = browserWindow.call(methodName, methodArgs);
要从 applet 内部访问 JavaScript,您必须在您的<applet>标签中包含一个属性。您的 applet 标签将如下所示:
<applet code="MyApplet" width="100" height="100" MAYSCRIPT>
...
</applet>
JavaScript 提供对文档中所有 Applet 的引用,作为文档对象的一个applets属性。属性是一个数组。您可以使用从零开始的索引或 applet 名称来访问它。假设在一个名为applet1和applet2的文档中有两个 Applet。假设 HTML 文档中的所有 Applet 都有一个pushMessage(String msg)方法,下面的 JavaScript 函数具有调用页面上所有 Applet 和applet1的pushMessage()方法的代码:
// A JavaScript function.
// Call the pushMessage() method of all applets on the page
function pushMessageToAllApplets() {
for(var i = 0; i < document.applets.length; i++) {
document.applets[i].pushMessage("Hello");
}
}
// A JavaScript function.
// Call the pushMessage() method of applet1 on the page
function pushMessageToApplet1() {
document.applets["applet1"].pushMessage("Hello applet1");
}
您可以从 JavaScript 代码中访问 applet 的任何公共方法或字段。请注意,JavaScript 不是编译语言,如果 applet 的方法或字段名称不存在,它可能会抛出运行时错误。
打包归档中的 Applet
你可以打包所有的 Java 类和资源,比如图片、音频等。对于存档(JAR 或 ZIP 文件)中的 applet。您可以使用一个或多个归档来打包您的 applet 资源。所有归档文件的名称都在一个逗号分隔的列表中指定为<applet>标记的archive属性值。使用codebase属性值解析归档文件的名称。
<applet code="MyApplet"
width="200"
height="200"
codebase="resources"
archive="myclasses1.jar, myclasses2.jar, myimages.zip">
</applet>
如果将所有资源打包到档案中,就不必在 web 服务器上维护特定的目录结构来存储类和其他资源。在归档中为 applet 打包所有资源在 applet 加载时间方面有很大的优势。它大大缩短了 applet 的加载时间,因为它使用一个连接来下载整个档案,而不是为每个要下载的文件连接一次。但是,如果由于某种原因,您不能将所有的 applet 类和资源打包到档案中,您可以将一些保存在目录中,将一些打包到档案中。如果 Applet 需要资源(类文件、图像、音频剪辑等)。),它首先在归档中查找,然后在服务器上查找。
事件调度线程和 Applet
在第三章的中,我介绍了很多关于 Swing 应用中事件调度线程的角色。关于事件调度线程和 Swing 的讨论也适用于 Applet,因为 Applet 也使用 Swing 组件。四个 applet 生命周期方法init()、start()、stop()和destroy()由 applet 查看器(通常是 web 浏览器)调用,它们不在事件调度线程上调用。您应该编写自己的程序,以便所有与 Swing 相关的代码都在事件调度线程上执行。您已经用init()方法构建了 GUI,现在您知道了init()方法不是在事件调度线程上执行的。所以,你没有遵循正确的方法来使用 Swing 组件。在您的 Applet 中,您还没有遇到任何与事件调度线程相关的问题,因为到目前为止,这些例子都是微不足道的。如果您正在开发生产级别的 Applet,您需要遵循建议的准则。
您需要使用invokeAndWait()和SwingUtilities类的方法在事件调度线程上运行您的代码。通常,您使用invokeLater()方法,以便您的代码被安排稍后在事件调度线程上运行。invokeLater()方法立即返回。你不应该使用invokeLater()方法从 applet 的init()方法构建 GUI。原因很明显。applet 查看器(通常是 web 浏览器)依次调用 applet 的init()和start()方法。当init()方法返回时,它调用start()方法。如果您使用init()方法中的invokeLater()方法构建 GUI,那么init()方法将立即返回(不一定是在运行构建 GUI 的代码之后,而是在调度 GUI 构建代码稍后运行之后), applet 查看器将调用start()方法。也就是说,当start()方法执行时,您的 GUI 可能还没有准备好。然而,假设您的 applet 必须在init()方法返回之前初始化,这样您就可以在它的start()方法中执行接下来的步骤。这就是为什么您需要使用invokeAndWait()方法在 applet 的init()方法中构建 GUI 的原因,这样您就可以确保当调用start()方法时,您的 GUI 已经就位。下面是编写 applet 的init()方法的正确方法。清单 4-10 重写了HelloApplet类,并将其命名为BetterHelloApplet。它使用方法为 applet 构建 GUI。在事件上调用initApplet()方法——从init()方法调度线程。
清单 4-10。使用事件调度线程在 Applet 中构建 GUI
// BetterHelloApplet.java
package com.jdojo.applet;
import javax.swing.JApplet;
import javax.swing.SwingUtilities;
import java.awt.Container;
import java.awt.FlowLayout;
import java.lang.reflect.InvocationTargetException;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import static javax.swing.JOptionPane.INFORMATION_MESSAGE;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
public class BetterHelloApplet extends JApplet {
@Override
public void init() {
try {
// Build the GUI on thw event-dispatching thread
SwingUtilities.invokeAndWait(() -> initApplet());
}
catch (InterruptedException | InvocationTargetException e) {
JOptionPane.showMessageDialog(null, e.getMessage(),
"Error", ERROR_MESSAGE);
}
}
private void initApplet() {
// This method is supposed to be executed on the
// event-dispatching thread
// Create Swing components
JLabel nameLabel = new JLabel("Your Name:");
JTextField nameFld = new JTextField(15);
JButton sayHelloBtn = new JButton("Say Hello");
// Add an action litener to the button to show the hello message
sayHelloBtn.addActionListener(e -> sayHello(nameFld.getText()));
// Add Swing components to the content pane of the applet
Container contentPane = this.getContentPane();
contentPane.setLayout(new FlowLayout());
contentPane.add(nameLabel);
contentPane.add(nameFld);
contentPane.add(sayHelloBtn);
}
private void sayHello(String name) {
String msg = "Hello there";
if (name.length() > 0) {
msg = "Hello " + name;
}
// Display the hello message
JOptionPane.showMessageDialog(null, msg, "Hello", INFORMATION_MESSAGE);
}
}
在其他地方选择使用SwingUtilities类的invokeAndWait()和invokeLater()方法取决于手头的情况。根据经验,您需要使用SwingUtilities类的两个方法之一,在事件调度线程中执行 applet 的init()、start()、stop()和destroy()方法的代码。您可以使用SwingWorker类在后台线程中执行任何任务,并使用事件调度线程与 Swing 组件进行协调。请参考第三章了解在 Swing 应用中使用线程的详细信息。
用 Applet 绘画
Applet类继承自java.awt.Panel类。JApplet类继承自Applet类。如果你想直接在 applet 表面绘制图形或字符串,你需要覆盖它的paint(Graphics g)方法并编写你的代码。注意,如果您添加 Swing 组件并绘制到它们的表面上,您需要覆盖那些 Swing 组件的paintComponent(Graphics g)方法。或者,您可以覆盖Applet类的paint()方法并执行如清单 4-11 所示的绘制。
清单 4-11。使用 paint()方法绘制字符串的 Applet
// DrawingHelloApplet.java
package com.jdojo.applet;
import javax.swing.JApplet;
import java.awt.Graphics;
public class DrawingHelloApplet extends JApplet {
@Override
public void paint(Graphics g) {
super.paint(g);
g.drawString("Hello Applet!", 10, 20 );
}
}
Java 代码可信吗?
有两种 Java 代码可以在您的机器上运行:可信代码和不可信代码。没有硬性的规则来指定哪些 Java 代码总是可信的,哪些是不可信的。然而,有一些规则你可以遵循。默认情况下,您应该将 web 浏览器通过 Internet 下载来运行 Applet 的所有 Java 代码归类为不可信代码,因为您不知道是谁为 Applet 编写了代码。您可以将所有在您的机器上作为应用运行的本地 Java 代码归类为可信代码。当代码试图访问某些特权资源(如本地文件系统)时,可信代码和不可信代码之间的区别就显现出来了。默认情况下,Java 将所有本地存储的代码都视为可信代码,以授予对特权资源的完全访问权限。它将通过网络下载的代码视为不可信代码。它不会向不受信任的代码授予对特权资源的访问权限。Java 允许您使用策略文件将特权资源的访问权授予某些代码,而不是其他代码。让我们考虑一个例子来理解如何使用策略文件在 Java 中定制安全性。清单 4-12 包含了SecurityTest类的代码。它将一条文本消息写入一个名为c:\sec_demo.txt的文件。文件路径适用于 Windows 平台。您可以在运行该程序时根据自己的选择修改文件路径。
清单 4-12。将文本消息写入文件的 SecurityTest 类
// SecurityTest.java
package com.jdojo.applet;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class SecurityTest {
public static void main(String[] args) throws IOException {
// Message to be written to the file
String msg = "Testing Java filee permission security...";
// Change the path C:\sec_demo.txt to conform to the
// syntax supported by your operating system
Path filePath = Paths.get("C:\\sec_demo.txt");
// Write message to the file
Files.write(filePath, msg.getBytes());
// Print a message
System.out.println("Test message written to " + filePath);
}
}
您可以使用以下命令运行该类:
java com.jdojo.applet.SecurityTest
上述命令将在标准输出中打印以下消息:
Test message written to c:\sec_demo.txt
使用下面的命令运行同一个SecurityTest类会产生运行时错误。显示了部分错误消息:
java -Djava.security.manager com.jdojo.applet.SecurityTest
Exception in thread "main" java.security.AccessControlException: access denied ("java.io.FilePermission" "C:\sec_demo.txt" "write")
...
第二次运行SecurityTest类时,您将-Djava.security.作为 JVM 选项传递。该选项告诉 JVM 使用安全管理器运行该类。当您第一次运行这个类时,它是在没有安全管理器的情况下运行的,程序能够访问文件系统并写入文件。当安全管理器存在时,它检查授予执行代码的权限,执行代码需要访问一些资源。因为您没有授予代码写入文件的权限,所以当您第二次运行该类时会收到安全异常。
策略文件控制 Java 安全性。两个默认的策略文件授予 Java 代码权限。JRE_HOME\jre\lib\security\java.policy是系统范围的策略文件,其中JRE_HOME是安装 Java 运行时环境的目录。另一个策略文件是特定于用户的,它存储在USER_HOME\.java.policy中,其中USER_HOME是由user.home系统属性定义的用户主目录。注意特定于用户的默认 Java 策略文件名前面的点(.java.policy)。您还可以拥有自定义策略文件,并在运行应用时在命令行指定它们的 URL。
在JRE_HOME\lib\security\java.security中存储了一个配置文件,它包含关于默认策略文件位置的详细信息以及其他与安全相关的细节。以下是 JRE 安装中提供的java.security文件的部分内容。它说明了两个 Java 策略文件的名称——一个在 Java 主目录中,一个在用户主目录中。您可以按照键的模式向其中添加更多的默认策略文件。
# The default is to have a single system-wide policy file,
# and a policy file in the user's home directory.
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy
为了让示例工作并让它写入到一个C:\sec_demo.txt文件中,让我们创建一个名为jsec.policy的文件,并将以下文本添加到该文件中:
grant {
permission java.io.FilePermission "c:\\sec_demo.txt", "read, write"; };
};
将自定义安全文件另存为C:\jsec.policy。自定义策略文件中的 grant 语句声明如下:将对C:\sec_demo.txt文件的读写权限授予任何代码。写权限足以运行该示例。但是,您已经在策略文件中授予了读取和写入权限。注意 grant 语句中文件路径中的两个反斜杠(C:\\sec_demo.txt)。策略文件解析器将把两个反斜杠翻译成一个,文件路径将被视为C:\sec_demo.txt。用下面的命令运行SecurityTest类。整个命令在一行中输入。
java -Djava.security.manager -Djava.security.policy=file:/C:/jsec.policy com.jdojo.applet.SecurityTest
以下消息将打印在标准输出上:
Test message written to c:\sec_demo.txt
这一次,您指示 JVM 使用安全管理器和位于file:/C:/jsec.policy URL 的策略文件来运行SecurityTest类。请注意,您使用 URL 来定位策略文件,而不是文件路径。这意味着您可以将您的策略文件存储在 web 服务器上,并使用类似于 http://www.jdojo.com/mysec.policy 的 URL 来定位定制的策略文件。请注意,默认情况下,两个策略文件(一个系统范围的策略文件和一个用户特定的策略文件)仍然与您的自定义策略文件一起使用。如果不想创建自定义策略文件,可以在两个默认策略文件中的任何一个中添加上述权限,应用将运行相同的权限。
我不会详细讨论 Java 安全策略文件格式。JDK/JRE 附带了一个名为policytool的实用程序,它可以让你图形化地使用 Java 策略文件。它安装在JAVA_HOME\bin文件夹中,其中JAVA_HOME是你机器上的 JDK 或 JRE 安装文件夹。
为了开始讨论 applets 的安全限制和定制,我将讨论更多关于策略文件格式的内容。以下是在策略文件中授予权限的更多示例。您也可以在策略文件中使用 Java 注释。
在策略文件中授予权限的简化通用语法是
grant signedby "<signer names>", codebase "<code base URL>" {
permission <permission class name> "<target name>", "<actions>";
};
<...>中的文本由策略文件的作者提供。许多选项都是可选的。您可以在一个grant块中包含多个permission子句。一个策略文件中可以有多个grant块。signedby选项表示权限只授予由签名者签名的代码。例如,考虑下面的grant模块:
grant signedby "John" {
...
};
grant块表示如果代码由John签名,则许可被授予。
考虑下面的grant块:
grant signedby "John, Robert, Cheryl" {
...
};
grant块表示如果代码由John、Robert和Cheryl签名,则许可被授予。我将在下一节讨论更多关于代码签名的内容。如果signedby选项不存在,权限将根据其他标准授予代码,而不管代码是否经过签名。
选项用于向从特定 URL 执行的代码授予权限。考虑下面的grant块:
grant codebase "file:/c:/classes" {
...
};
grant块表示来自file:/c:/classes URL 的代码将被授予权限。如果codebase选项不存在,那么从任何位置下载和执行的代码都会被授予权限。在 Java 策略文件中授予权限的一些示例如下:
/* Grant read and write permission to the file c:\sec_demo.txt
to code signed or unsigned and downloaded from any location.
*/
grant {
permission java.io.FilePermission "c:\\sec_demo.txt", "read, write";
};
/* Grant write permission to the file c:\sec_demo.txt to code signed or
unsigned and downloaded from file:/C:/classes/ URL.
*/
grant codebase "file:/C:/classes/" {
permission java.io.FilePermission "c:\\sec_demo.txt", "write";
};
/* Grant two permissions to the code signed by John and downloaded
from the file:/C:/classes/ URL.
1\. Grant the execute permission on the file c:\crazy.exe
2\. Grant the read permission for the system property user.home, so
the code can execute the statement System.getProperty("user.home").
If this permission is not granted, reading the property "user.home"
will throw a security exception.
*/
grant signedby "John", codebase "file:/C:/classes/" {
permission java.io.FilePermission "c:\\crazy.exe", "execute";
permission java.util.PropertyPermission "user.home", "read";
};
您可以将java.io.FilePermission授予一个文件或目录。您可以使用文件或目录路径以及一组操作来授予对文件的权限。您可以在文件/目录上授予任意组合的read、write、delete和execute权限。多个操作用逗号分隔。策略文件支持不同的格式来指定文件/目录路径,如表 4-2 中所列。
表 4-2。
The List of File/Directory Path Format Used in Granting the java.io.FilePermission
| 文件/目录路径格式 | 描述 | | --- | --- | | 文件路径:`C:\mydir\test.txt` | 仅授予对此文件的权限。 | | 目录路径:`C:\mydir`或`C:\mydir\` | 仅授予对此目录的权限。(对于目录,一个尾随文件分隔符被视为没有尾随分隔符。在 UNIX 上是正斜杠比如`/usr/mydir`或者`/usr/mydir/`,在 Windows 上是反斜杠。 | | `C:\mydir\*` | 授予对`C:\mydir`目录下所有文件的权限。注意,它并没有授予对`C:\mydir`目录本身的权限。 | | `*` | 授予当前目录下所有文件的权限。 | | `C:\mydir\-` | 递归授予`C:\mydir`及其子目录下的所有文件和文件夹权限。 | | `-` | 递归授予当前目录及其子目录下的所有文件和文件夹权限。 | | `<>` | 授予对文件系统下所有文件和文件夹的权限。例如,下面的 grant 子句授予对文件系统中所有文件的所有代码的读取权限:`grant {` `permission java.io.FilePermission "<>", "read";` `};` |Applet 的安全限制
默认情况下,applet 的代码被视为不受信任的代码,它在安全管理器下运行。如果您使用文件协议从本地文件系统运行 applet,浏览器可能会放宽一些限制。这些限制适用于使用网络协议(如http或https)下载 Applet 代码的情况。默认情况下,通过网络下载的 applet 代码被认为是不可信的,即使下载代码的 web 服务器正在本地运行。以下是适用于不受信任的代码和 Applet 的部分限制列表:
- 它不能访问本地文件系统。
- 它不能连接到任何机器,除了从其下载代码的机器。
- 它无法加载本机库。
- 它不能在运行它的机器上启动程序。
- 它只能读取一些系统属性,这些属性被认为是无害的。它可以读取系统属性,如
OS.name、OS.version、java.version等。它不能读取有潜在风险的系统属性,如user.home、user.dir、java.class.path等。 - 它不能使用
System.exit()方法调用退出 JVM。 - 它向用户显示带有一些视觉提示的弹出窗口,以指示弹出窗口是从 applet 显示的,并且它是不可信的。
Applet 如何执行一些受限制的任务?有两种方法可以让 applet 执行受限制的任务:
- 您可以自定义策略文件并授予特定权限。
- 你可以使用签名的 Applet。
清单 4-13 包含了试图读取user.home系统属性的 applet 的代码。清单 4-14 包含了查看 applet 的 HTML 代码。
清单 4-13。试图读取 user.home 系统属性的 Applet
// ReadUserHomeApplet.java
package com.jdojo.applet;
import javax.swing.JApplet;
import javax.swing.JTextArea;
import java.io.StringWriter;
import java.io.PrintWriter;
import javax.swing.JScrollPane;
import java.awt.Container;
public class ReadUserHomeApplet extends JApplet {
JTextArea msgTextArea = null;
@Override
public void init() {
String msg = "";
try {
String userHome = System.getProperty("user.home");
msg = "User's Home Directory is '" + userHome + "'";
}
catch (Throwable t) {
msg = this.getStackTrace(t);
}
this.msgTextArea = new JTextArea(msg, 10, 40);
Container contentPane = this.getContentPane();
contentPane.add(new JScrollPane(msgTextArea));
}
public String getStackTrace(Throwable t) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
t.printStackTrace(pw);
pw.close();
return sw.toString();
}
}
清单 4-14。用于查看 ReadUserHomeApplet Applet 的 ReadUserHomeApplet 文件的内容
<html>
<head>
<title>Read User Home Directory</title>
</head>
<body>
<applet code="com.jdojo.applet.ReadUserHomeApplet"
width="400"
height="300">
This browser does not support Applets.
</applet>
</body>
</html>
清单 4-13 中的 applet 代码非常简单。它使用System.getProperty("user.home")方法读取用户的主目录。默认情况下,不允许 applet 读取user.home系统属性。当您查看这个 Applet 时,您会得到一个安全异常。部分异常消息如下:
java.security.AccessControlException: access denied ("java.util.PropertyPermission" "user.home" "read")
...
异常消息表明 applet 的代码没有类型为java.util.PropertyPermission的read权限来读取user.home系统属性。下面的grant块将这个权限授予所有代码:
grant {
permission java.util.PropertyPermission "user.home", "read";
};
将这个grant块添加到主目录下的一个.java.policy文件中。如果您的主目录中没有.java.policy文件,您可以用这个名称创建一个新文件,并将上面的授权添加到其中。在 Windows XP 上,您的主目录是C:\Documents and Settings\<your-user- name>。您还可以通过在一个没有运行在安全管理器下的独立 Java 应用中执行System.getProperty("user.home")语句来获取用户的主目录路径。在用户主目录的.java.policy文件中添加上述授权后,ReadUserHomeApplet applet 显示类似如下的消息:
User’s Home Directory is 'C:\Documents and Settings\sharan'
确保在完成这个例子后,从.java.policy文件中删除了授权块。否则,任何 Applet 都可以在您不知情的情况下读取您机器上用户的主目录。在下一节中,我将使用一个签名的 applet 来演示相同的例子。
签名 Applet
当 applet 在浏览器中运行时,它不能访问客户机上的任何东西,除了一些信息,如操作系统名、JVM 版本等。当您想要像访问 Java 应用一样访问 applet 时,您需要使用签名的 applet。
签名 applet 背后的概念与签名文档背后的概念相同。通过签署文档,您批准了它,并且通过批准它,您对文档中包含的内容负责。它不能保证在您签名后其他人不能篡改文档。但是,如果对文件的真实性有任何疑问,可以联系您进行核实。任何可以验证你的签名并信任你的人都可以认为你签署的文件是真实的。这个概念同样适用于 applet。回想一下,applet 的代码在默认情况下是不可信的。如果 applet 是一个签名的 applet,applet 的用户在 Java 策略文件中授予签名的 applet 权限,或者他可以信任 applet 并即时授予权限。
在签署 applet 之前,您必须有一个名为私钥/公钥的密钥对。您可以使用安装在JAVA_HOME\bin文件夹中的keytool命令生成一个密钥对,其中JAVA_HOME是您安装了 JDK 或 JRE 的文件夹。生成的密钥存储在一个称为密钥库的数据库中。keytool命令还允许您创建一个密钥库数据库。私钥是您的秘密密钥,而公钥是供想要验证您签名的公众使用的。一旦您有了一个密钥对,您需要生成一个证书请求(您可以使用带有–certreq选项的keytool命令来生成一个证书请求)发送给证书颁发机构(CA)以获得证书。CA 是颁发数字证书的组织。数字证书由您提供的公钥和您的身份组成。CA 将向您收取颁发证书的适当费用。DigiCert、Thawte 和 VeriSign 是一些可用的 ca。您也可以自己颁发证书,这就是您在本节中为了演示目的而要做的事情。但是,如果您在 web 上部署 applet 供公众使用,您需要花一些钱从可信的 CA 处获得证书,以增加 applet 签名的真实性。公众更有可能会相信 Thawte 颁发的证书,而不是你。注意从 Java 7 开始,您的 applet 必须被签名,打包在一个 JAR 文件中,其中包含一个具有属性Permissions的清单文件。否则,默认情况下,您的 Applet 不会运行。
您需要将您的 applet 类打包到一个 JAR 文件中,以便您可以对其进行签名。您可以使用JAVA_HOME\bin文件夹下的命令用您的秘密私钥对 JAR 文件进行签名,其中JAVA_HOME是 JDK 或 JRE 的安装文件夹。还有其他工具可以用来对 JAR 文件进行签名。签名过程将把证书和公钥放在 JAR 文件中,供 JAR 文件的用户验证签名。
以下步骤将引导您完成签名和使用 applet 的过程。
步骤 1:开发 Applet
您需要编写 applet 的源代码,并将其编译成类文件。使用清单 4-13 中列出的ReadUserHomeApplet类。在这一步结束时,您将拥有一个ReadUserHomeApplet.class文件。
步骤 2:将类文件打包到 JAR 文件中
用以下内容创建一个manifest.mf文件。记得在文件末尾加一个空行。
Manifest-Version: 1.0
Permissions: sandbox
使用以下命令创建一个signedapplet.jar文件。在你的JDK_HOME\bin文件夹中有jar命令。
jar cvfm signedapplet.jar manifest.mf com/jdojo/applet/ReadUserHomeApplet.class
确保在 JAR 文件中,类文件的路径被设置为com/jdojo/applet,,这与其包相同。要确保 JAR 文件包含类文件的正确路径,请使用以下命令:
jar -tf signedapplet.jar
META-INF/
META-INF/MANIFEST.MF
com/jdojo/applet/ReadUserHomeApplet.class
在这一步结束时,您将拥有一个signedapplet.jar文件。
步骤 3:生成私钥/公钥对
使用keytool命令生成一个私钥/公钥对,如下所示:
keytool -genkey -keystore mykeystore –alias Kishori
上面的命令将创建一个名为mykeystore的密钥库文件。它将生成一个私有/公共密钥对,您可以使用别名Kishori来使用它。请注意,您需要为您的密钥对指定一个别名。从现在开始,您将使用别名Kishori来引用您的密钥库中的密钥。上面的命令将询问识别您的详细信息。你需要输入这些信息。密钥库文件受密码保护。私钥也受密码保护。使用上述命令时,您需要使用新密码。记住这些密码,因为您将被要求提供这些密码来访问别名Kishori所引用的密钥库或密钥对。
您可以使用以下命令亲自验证生成的密钥:
keytool -keystore mykeystore -selfcert –alias Kishori
在这一步的最后,您将拥有一个名为mykeystore的文件,并且您将生成一个别名为Kishori的密钥对。
步骤 4:签署 JAR 文件
使用以下命令对 JAR 文件进行签名:
jarsigner -keystore mykeystore signedapplet.jar Kishori
以上命令将提示您输入密钥库密码。在这一步结束时,您将拥有一个签名的signedapplet.jar文件。如果您列出了signedapplet.jar文件的目录,您会注意到jarsigner命令向其中添加了更多的文件,如下所示:
jar -tf signedapplet.jar
META-INF/MANIFEST.MF
META-INF/KISHORI.SF
META-INF/KISHORI.DSA
META-INF/
com/jdojo/applet/ReadUserHomeApplet.class
步骤 5:创建 HTML 文件
创建一个 HTML 文件来查看签名的 applet,如清单 4-15 所示。注意<applet>标签使用了一个archive属性。
清单 4-15。signedreaduserhomeapplet.html 档案的内容
<html>
<head>
<title>Read User Home Directory (signed Applet)</title>
</head>
<body>
<applet code="com.jdojo.applet.ReadUserHomeApplet"
width="400" height="300"
archive="signedapplet.jar">
This browser does not support Applets.
</applet>
</body>
</html>
步骤 6:查看签名的 Applet
当您试图查看已签名的 applet 时,Java 插件会显示一个安全警告窗口。它允许您查看用于签署 applet 的签名的详细信息。您可以单击“运行”按钮来运行 Applet。勾选“始终信任来自该发行商的内容”复选框,点击“运行”将使 Java 插件将该证书作为可信证书导入,如图 4-10 所示。
图 4-10。
The Certificates dialog in the Java Control Panel
一旦 Java 插件将证书存储在其受信任的证书存储库中,它将在将来信任该证书,而不提示您。您可以稍后通过转到 Java 控制面板➤安全选项卡➤管理器证书从存储库中删除曾经信任的证书。
通过信任一个已签名的 Applet,您就给了它所有的权限。如果您仍然希望使用 Java 策略文件将权限应用于已签名的可信 Applet,您需要使用 Java 策略文件中的usePolicy java.lang.RuntimePermission,这将指示 Java 插件不提示用户接受已签名的 Applet 的证书。相反,它将应用策略文件中授予该 applet 的权限。
策略文件(系统范围的或用户特定的策略文件)中的以下条目将指导 Java 插件始终使用策略文件:
grant {
permission java.lang.RuntimePermission "usePolicy";
};
您的 applet 将能够访问您在策略文件中授予的资源。Java 插件不会提示用户信任 applet。
您还可以通过转到 Java 控制面板➤高级选项卡➤安全用户环境,关闭“允许用户授予对已签名内容的权限”选项,来指示 Java 插件不提示用户授予对已签名 Applet 的访问权限。如果未选中此选项,当用户试图查看 Applet 时,将会收到安全警告。默认情况下,此选项处于启用状态。
摘要
applet 是一种 Java 程序,设计用于嵌入 HTML 文档并在 web 浏览器中运行。从技术上讲,applet 是一个继承自Applet或JApplet类的 Java 类。如果您从JApplet类继承了 applet 的类,那么为 applet 编写代码与为 Swing 应用编写代码非常相似。Applet 使用 Swing 组件来构建 GUI。
使用<applet>标签将 applet 嵌入到 HTML 文档中。HTML 文档可以是静态 HTML 文件,也可以是动态生成的,例如在 JSP 页面中。通常,applet 的 Java 类文件和资源(如图像和音频)被打包在 JAR 文件中,并存储在 web 服务器上。在客户机上,浏览器请求 HTML 文档,解析 HTML 文档中的<applet>标记,下载 applet 的代码,并在浏览器中运行代码。默认情况下,Applet 在安全沙箱中运行,它们不能访问客户机的机器资源,如本地文件系统。使用 Java 策略文件或签署 applet 的 JAR 文件可以放宽这些限制。
标签用来在 HTML 文档中嵌入一个 applet。<applet>标签包含四个强制属性,称为code、width、height和archive。属性指定了 applet 类的完全限定名。width和height属性指定浏览器中 applet 显示区域的宽度和高度。属性指定了包含 applet 文件的归档文件(JAR/ZIP)。注意,如果您想使用默认的 Java 插件设置运行 applet,那么archive属性是强制的。
您需要安装 Java 插件(JRE 的一部分)来在您机器上的浏览器中运行 Applet。您可以使用随 Java 插件一起安装的 Java 控制面板程序来配置 Java 插件。
applet 有一个生命周期。它的四个方法init()、start()、stop()和destroy()在其生命周期中被自动调用。在 applet 被实例化和加载后,浏览器调用init()方法。start()方法在init()方法之后被调用,并且可能被多次调用。stop()方法是start()方法的对应方法,可能会被多次调用。当 applet 被销毁时,在 applet 的生命周期结束时调用destroy()方法。这些方法不会在事件调度线程上调用。使用SwingUtilities类,在第三章中有描述,在事件调度线程中运行任何 GUI 相关的代码。
对象的一个实例代表运行它的 Applet 的上下文。您可以使用这个对象与 applet 的上下文进行交互,比如来自 Java 代码的浏览器。
在 Java 7 和更高版本中,默认情况下,如果不满足以下条件,Applet 将被阻止:
- applet 应该打包在一个签名的 JAR 文件中。
- applet 的 JAR 文件中的清单文件应该包含一个
Permissions属性,其值可以是sandbox或all-permissions。 - applet 的 JAR 文件应该用可信机构颁发的证书进行签名。
您可以通过使用 Java 控制面板配置 Java 插件设置来放宽这些限制,尽管不建议这样做。
五、网络编程
在本章中,您将学习
- 什么是网络编程
- 什么是网络协议套件
- 什么是 IP 地址,不同的 IP 编址方案是什么
- 特殊 IP 地址及其用途
- 什么是端口号以及如何使用它们
- 使用 TCP 和 UDP 客户端和服务器套接字在远程计算机之间进行通信
- URI、URL 和 URN 的定义以及如何在 Java 程序中表示它们
- 如何使用非阻塞套接字
- 如何使用异步套接字通道
- 面向数据报的套接字通道和多播数据报通道
前几节旨在为没有计算机科学背景的读者快速概述与网络技术相关的基础知识。如果你理解 IP 地址、端口号、网络协议套件等术语。,您可以跳过这些章节,开始阅读“Socket API 和客户机-服务器范例”一节。
什么是网络编程?
网络是一组两台或多台计算机或其他类型的电子设备(如打印机),它们为了共享信息而连接在一起。连接到网络的每个设备称为一个节点。与网络相连的计算机称为主机。Java 中的网络编程包括编写 Java 程序,以促进网络上不同计算机上运行的进程之间的信息交换。
Java 使编写网络程序变得容易。向另一台计算机上运行的进程发送消息就像向本地文件系统写入数据一样简单。类似地,接收从另一台计算机上运行的进程发送的消息就像从本地文件系统中读取数据一样简单。本章中的大多数程序都将涉及通过网络读写数据,它们类似于文件 I/O。请参考《Java 语言特性入门》( ISBN: 978-1-4302-6658-7)一书中的第七章到第十章,了解文件 I/O 的更多详细信息。您将了解到本章中的几个新类,它们有助于网络上两台计算机之间的通信。
要理解或编写本章中的 Java 程序,您不需要具备网络技术的高级知识。本章涵盖了网络通信中所涉及的一些事物的高级细节。
网络可以根据不同的标准进行分类。根据网络分布的地理区域,网络可分为以下几类
- 局域网(LAN):它覆盖一个小区域,如一栋大楼或一组大楼。
- 校园网(CAN):它覆盖一个校园,如大学校园,在校园内互连多个局域网。
- 城域网:它比局域网覆盖更大的地理区域。通常,它覆盖一个城市。
- 广域网(WAN):它覆盖更大的地理区域,例如一个国家的一个区域或世界上不同国家的多个区域。
当两个或两个以上的网络通过路由器(也称为网关)连接在一起时,这被称为网间互联,由此形成的组合网络被称为网间互联,简称 internet(注意小写的 I)。全球互联网络,包括世界上所有连接在一起的网络,被称为互联网(注意大写的 I)。
基于拓扑结构(网络中节点的排列),网络可以分为星形、树形、环形、总线式、混合式等。
根据网络用于传输数据的技术,网络可以分为以太网、本地话、光纤分布式数据接口(FDDI)、令牌环、异步传输模式(ATM)等。
我不会涉及不同种类的网络的任何细节。请参考任何关于网络的标准教材,详细了解网络和网络技术。
计算机上两个进程之间的通信很简单,它是使用操作系统定义的进程间通信来实现的。当在互联网上的两台不同的计算机上运行的两个进程需要通信时,这是一项非常乏味的任务。在 internet 上两台计算机上的两个进程开始通信之前,您需要考虑通信的许多方面。您需要考虑的一些要点如下:
- 两台计算机可能使用不同的技术,如不同的操作系统、不同的硬件等。
- 它们可能位于使用不同网络技术的两个不同网络上。
- 它们可能被使用不同技术的许多其它网络分隔开。也就是说,两台计算机不在两个直接互连的网络上。您需要考虑的不仅仅是两个网络,而是来自一台计算机的数据必须通过才能到达另一台计算机的所有网络。
- 他们可能相隔几英里,或者在地球的另一边。你如何高效地传输信息而不用担心两台电脑之间的距离?
- 一台计算机可能不理解另一台计算机发送的信息。
- 通过网络发送的信息可能会被复制、延迟或丢失。接收方和发送方应该如何处理这些异常情况?
简单地说,网络上的两台计算机使用消息(0 和 1 的序列)进行通信。必须有定义良好的规则来处理上述问题(以及许多其他问题)。处理特定任务的规则集称为协议。处理网络通信涉及许多类型的任务。有一个协议来处理每个特定的任务。有一组协议(也称为协议组)一起用于处理网络通信。
网络协议组
现代网络之所以被称为网络,是因为它们以称为数据包的块传输数据。每个分组独立于其他分组传输。这使得使用不同的路由从同一台计算机向同一目的地传输数据包变得容易。然而,如果一台计算机向一台远程计算机发送两个数据包,而第二个数据包在第一个数据包到达之前到达,这可能会成为一个问题。因此,每个数据包都有一个数据包编号和目的地址。在目的计算机上有重新排列无序到达的数据包的规则。下面的讨论试图解释用于处理网络通信中的分组的一些机制。
图 5-1 显示了一个被称为互联网参考模型或 TCP/IP 分层模型的分层协议套件。这是使用最广泛的协议套件。模型中的每一层都执行定义明确的任务。拥有分层协议模型的主要优点是任何层都可以被改变而不影响其他层。新协议可以添加到任何层,而无需更改其他层。
图 5-1。
The Internet Protocol Suite showing its five protocol layers
每一层只知道它上面和下面的那一层。协议组中的每一层都有两个接口,一个用于上层,一个用于下层。例如,传输层有到应用层和互联网层的接口。也就是说,传输层只知道如何与应用层和 internet 层通信。它对网络接口层或物理层一无所知。
Java 程序等用户应用使用应用层与远程应用进行通信。用户应用必须指定它想要用来与远程应用通信的协议。应用层中的协议定义了用于格式化消息以及将含义与包含在消息中的信息(例如消息类型、描述它是请求还是响应等)相关联的规则。应用层格式化消息后,将消息移交给传输层。应用层中的协议的例子是超文本传输协议(HTTP)、文件传输协议(FTP)、Gopher、电信网络(Telnet)、简单邮件传输协议(SMTP)和网络新闻传输协议(NNTP)。
传输层协议处理将消息从一台计算机上的一个应用传输到远程计算机上的另一个应用的方式。它控制数据流、数据传输过程中的错误处理以及两个应用之间的连接。例如,用户应用可能会将非常大的数据块移交给传输层,以便传输到远程应用。远程计算机可能无法一次处理如此大量的数据。传输层负责一次向远程计算机传送适量的数据,这样远程应用就可以根据自己的能力处理数据。通过网络传递到远程计算机的数据可能由于各种原因在途中丢失。传输层负责重新传输丢失的数据。请注意,应用层只向传输层传递一次要传输的数据。在传输过程中,是传输层(而不是应用层)跟踪传递的数据和丢失的数据。可能有多个应用正在运行,所有这些应用都使用不同的协议,并与不同的远程应用交换信息。传输层负责将发送到远程应用的消息正确地传递出去。例如,您可能正在使用 HTTP 协议从一个远程 web 服务器浏览 Internet,并使用 FTP 协议从另一个 FTP 服务器下载文件。您的计算机正在接收来自两台远程计算机的消息,这些消息是针对您的计算机上运行的两个不同的应用的,一个 web 浏览器接收 HTTP 数据,一个 FTP 应用接收 FTP 数据。传输层负责将传入的数据传递给适当的应用。您可以看到协议组的不同层在网络数据传输中扮演着不同的角色。根据所使用的传输层协议,传输层将相关信息添加到消息中,并将其传递到下一层,即 internet 层。传输层中使用的协议示例有传输控制协议(TCP)、用户数据报协议(UDP)和流控制传输协议(SCTP)。
互联网层接受来自传输层的消息,并准备适合通过互联网发送的数据包。它包括互联网协议(IP)。IP 准备的数据包也称为 IP 数据报。除了其他信息外,它还包括一个报头和一个数据区。报头包含发送方的 IP 地址、目的 IP 地址、生存时间(TTL,整数)、报头校验和以及协议中指定的许多其他信息。IP 将消息准备成数据报,准备通过互联网传输。IP 数据报报头中的 TTL 根据路由器的数量指定了 IP 数据报在需要被丢弃之前可以保持传输多长时间。它的大小是一个字节,其值可以在 1 到 255 之间。当 IP 数据报在到达目的地的路由中到达路由器时,路由器将 TTL 值减 1。如果递减值为零,路由器将丢弃该数据报,并使用互联网控制消息协议(ICMP)向发送方发回一条错误消息。如果 TTL 值仍然是正数,路由器将数据报转发到下一个路由器。IP 使用地址方案,为每台计算机分配一个唯一的地址。该地址称为 IP 地址。我将在下一节详细讨论 IP 编址方案。互联网层将 IP 数据报交给下一层,即网络接口层。互联网层中的协议的例子是互联网协议(IP)、互联网控制消息协议(ICMP)、互联网组管理协议(IGMP)和互联网协议安全(IPsec)。
网络接口层准备要在网络上传输的数据包。这个数据包称为一个帧。网络接口层位于物理层之上,物理层包括硬件。请注意,IP 层使用 IP 地址来标识网络上的目的地。IP 地址是一个虚拟地址,完全由软件维护。硬件不知道 IP 地址,也不知道如何使用 IP 地址传输帧。必须为硬件提供硬件地址,也称为介质访问控制(MAC)地址,它需要将帧传输到目的地。这一层从 IP 地址中解析出目的硬件地址,并将其放入帧头中。它将帧移交给物理层。网络接口层中的协议示例有开放最短路径优先(OSPF)、点对点协议(PPP)、点对点隧道协议(PPTP)和第 2 层隧道协议(L2TP)。
物理层由硬件组成。它负责将信息比特转换成信号,并通过线路传输信号。
Tip
数据包是一个通用术语,在网络编程中用来表示一个独立的数据块。协议的每一层也使用特定的术语来表示它所处理的数据包。例如,一个数据包在 TCP 层被称为一个段;它在 IP 层被称为数据报;它在网络接口和物理层被称为帧。每一层在准备要通过网络传输的数据包时,都会将报头(有时也包括报尾)添加到从上一层接收的数据包中。当每一层从其下一层接收到数据包时,它会执行相反的操作。它从数据包中删除报头;如果需要,执行一些操作;并将数据包移交给其上一层。
当应用发送的数据包到达远程计算机时,它必须以相反的顺序通过同一层协议。每一层都将删除其报头,执行一些操作,并将数据包传递给其上一层。最后,数据包到达远程应用时的格式与它从发送方计算机上的应用开始时的格式相同。图 5-2 显示了发送方和接收方计算机的数据包传输。P1、P2、P3 和 P4 是相同数据的不同格式的数据包。目的地的协议层从其下一层接收相同的数据包,该数据包是由同一协议层传递给发送方计算机上的下一层的。
图 5-2。
Transmission of packets through the protocol layers on the sender’s and receiver’s computers
IP 寻址方案
IP 使用称为 IP 地址的唯一地址将 IP 数据报路由到目的地。IP 地址唯一标识计算机和路由器之间的连接。通常,IP 地址标识一台计算机。但是,需要强调的是,它标识的是计算机和路由器之间的连接,而不仅仅是计算机。路由器也会被分配一个 IP 地址。一台计算机可以使用多个路由器连接到多个网络,并且计算机和路由器之间的每个连接都有一个唯一的 IP 地址。在这种情况下,计算机将被分配多个 IP 地址,该计算机被称为多宿主计算机。多宿主增加了计算机网络连接的可用性。如果一个网络连接失败,计算机可以使用其他可用的网络连接。
IP 地址包含两部分——网络标识符(我称之为前缀)和主机标识符(我称之为后缀)。前缀唯一地标识互联网上的网络;后缀在网络中唯一标识主机。两台主机的 IP 地址可能有相同的后缀,只要它们有不同的前缀。
互联网协议有两个版本——IP v4(或简称 IP)和 IPv6,其中 v4 和 v6 分别代表版本 4 和版本 6。IPv6 也被称为下一代互联网协议(IPng)。注意没有 IPv5。IP 最流行的时候是在第 4 版。在 IPng 被分配第 6 版之前,第 5 版已经被分配给了另一个叫因特网流协议(ST)的协议。
IPv4 和 IPv6 都使用 IP 地址来标识网络上的主机。然而,两个版本中的寻址方案有很大不同。接下来的两节将讨论 IPv4 和 IPv6 使用的编址方案。
由于 IP 地址必须是唯一的,其分配由一个名为(IANA)的组织控制。IANA 为属于某个组织的每个网络分配一个唯一的地址。该组织使用网络地址和一个唯一的数字为网络上的每台主机形成一个唯一的 IP 地址。IANA 将 IP 地址分配给五个地区互联网注册管理机构(RIR),这些机构在表 5-1 中列出的特定地区分配 IP 地址。你可以在 www.iana.com 找到更多关于如何从 IANA 获得你所在地区的网络地址的信息。
表 5-1。
The List of Regional Internet Registries for Allocating Network IP Addresses
| 地区互联网注册管理机构名称 | 覆盖的区域 | | --- | --- | | 非洲网络信息中心 | 非洲地区 | | 亚太网络信息中心 | 亚洲/太平洋地区 | | 美国互联网号码注册局(ARIN) | 北美地区 | | 拉丁美洲和加勒比互联网地址注册处 | 拉丁美洲和一些加勒比海岛屿 | | 欧洲 IP 网络网络协调中心(RIPE NCC) | 欧洲、中东和中亚 |IPv4 寻址方案
IPv4(或简称 IP)使用 32 位数字来表示 IP 地址。IP 地址包含两部分—前缀和后缀。前缀标识网络,后缀标识网络上的主机,如图 5-3 所示。
图 5-3。
IPv4 addressing scheme
人类要记住二进制格式的 32 位数字并不容易。IPv4 允许您使用四个十进制数字的替代形式。每个十进制数的范围是从 0 到 255。程序负责将十进制数转换成计算机将使用的 32 位二进制数。IPv4 的十进制数字格式称为点分十进制格式,因为点用于分隔两个十进制数字。每个十进制数代表 32 位数字中 8 位包含的值。例如,二进制格式的 IPv4 地址11000000101010000000000111100111可以表示为点分十进制格式的192.168.1.231。将二进制 IPv4 转换为十进制的过程如图 5-4 所示。在192.168.1.231中,192.168.1部分标识网络地址(前缀),而231(后缀)部分标识该网络上的主机。
图 5-4。
Parts of an IPv4 address in binary and decimal formats
你怎么知道192.168.1代表 IPv4 地址192.168.1.231中的前缀?规则控制 IPv4 中前缀和后缀的值。在本节后面讨论网络的类类型时,我将讨论如何识别 IPv4 中的前缀和后缀。
IPv4 地址如何在前缀和后缀之间划分其 32 位?IPv4 地址空间分为五类,称为网络类,分别为A、B、C、D和E。类别类型定义了 32 位中有多少位将用于表示 IP 地址的网络地址部分。前缀中的前导位定义了 IP 地址的类别。这也称为自我识别或有类 IP 地址,因为您可以通过查看 IP 地址来判断它属于哪个类。
表 5-2 列出了 IPv4 中的五个网络类别及其特征。IP 地址中的前导位标识网络的类别。例如,如果一个 IP 地址看起来像0XXX,其中XXX是 32 位中的最后 31 位,那么它属于A类网络;如果一个 IP 地址看起来像110XXX,其中XXX是 32 位的最后 29 位,它属于C类网络。只能有类别A类型的128网络,每个网络可以有16777214台主机。一个A类网络可以拥有的主机数量非常大,一个网络不太可能拥有那么多主机。在 class C类型的网络中,一个网络可以拥有的最大主机数量限制为 254 台。
表 5-2。
Five Classes of IPv4 in the Classful Addressing Scheme
| 网络类 | 前缀 | 后缀 | 前缀中的前导位 | 网络数量 | 每个网络的主机数量 | | --- | --- | --- | --- | --- | --- | | `A` | 8 位 | 24 位 | `0` | `128` | `16777214` | | `B` | 16 位 | 16 位 | `10` | `16384` | `65534` | | `C` | 24 位 | 8 位 | `110` | `2097152` | `254` | | `D` | 未定义 | 未定义 | `1110` | 未定义 | 未定义 | | `E` | 未定义 | 未定义 | `1111` | 未定义 | 未定义 |如果为一个组织分配了一个来自类别C的网络地址,而该组织只有 10 台主机连接到网络,会发生什么情况?该网络中 IP 地址的剩余插槽仍未使用。回想一下,IP 地址中的主机(或后缀)部分在网络中必须是唯一的(前缀部分)。另一方面,如果一个组织需要将 300 台计算机连接到网络,它需要获得两个C类网络地址,因为获得一个能够容纳65534主机的B类网络地址将再次浪费大量 IP 地址。
注意,如果为后缀分配的位数是N,则可以使用的主机数是2 N -2。两位模式(全 0 和全 1)不能用于主机地址。它们有特殊的用途。这就是一个C类网络最多可以有 254 台主机而不是 256 台主机的原因。类别D地址用作组播地址。类别E地址被保留。
互联网的快速发展和大量未使用的 IP 地址促使人们制定新的编址方案。该方案仅基于一个标准,即应该能够在 IP 地址的前缀和后缀部分之间使用任意边界,而不是 8、16 和 24 位的预定义边界。这将使未使用的地址最少。例如,如果一个组织需要一个只有 20 台主机的网络的网络号,该组织只能使用 27 位前缀和 5 位后缀。
子网划分和超网划分这两个术语用于描述后缀中的一些位用作前缀、前缀中的一些位用作后缀的情况。当后缀中的位用作前缀时,实质上是以主机地址为代价创建更多的网络地址。额外的网络地址称为子网。子网划分是通过使用一个称为子网掩码或地址掩码的数字来实现的。子网掩码是一个 32 位的数字,用于根据 IP 地址计算网络地址。使用子网掩码消除了网络类别必须预先定义 IP 地址的网络号部分的限制。对 IP 地址和子网掩码执行逻辑AND以计算网络号。在这种编址方案中,IP 地址总是用其子网掩码来指定。IP 地址后面跟一个正斜杠和子网掩码。例如,140.10.11.9/255.255.0.0表示带有子网掩码255.255.0.0的 IP 地址140.10.11.9。可以使用四个十进制部分在 0 到 255 范围内的任何子网掩码。在这个例子中,140.10.11.9是一个类B地址。类别B地址使用 16 位作为前缀,16 位作为后缀。让我们把后缀去掉 6 位,加到前缀上。现在,前缀是 22 位,后缀只有 10 位。通过这样做,您以主机数量为代价创建了额外的网络数量。要描述这个子网划分方案中的 IP 地址,您需要使用子网掩码255.255.252.0。如果您将此子网掩码用作140.10.11.9/255.255.252.0来书写 IP 地址,网络地址将被计算为140.10.8.0,如下所示:
IP Address: 10001100 00001010 00001011 00001001
Subnet Mask: 11111111 11111111 11111100 00000000
------------------------------------------------
Logical AND: 10001100 00001010 00001000 00000000
(140) (10) (8) (0)
无类域间路由(CIDR)是另一种 IPv4 寻址方案,在该方案中,IPv4 地址被指定为四个带点的十进制数字以及由正斜杠分隔的另一个十进制数字,例如192.168.1.231/24,其中最后一个数字24表示 32 位 IPv4 地址中的前缀长度(或用于网络号的位数)。请注意,CIDR 编址方案允许您在 32 位 IPv4 的任意位定义前缀/后缀边界。通过将这些位从前缀移到后缀,您可以组合多个网络并增加每个网络的主机数量。这称为超网划分。您可以使用 CIDR 符号创建超网和子网。
IPv4 寻址方案中的一些 IP 地址是为广播和多播 IP 地址保留的。我将在本章后面讨论广播和组播。
IPv6 寻址方案
IPv6 是 IP 的新版本,是 IPv4 的继任者。在快速发展的互联网世界中,IPv4 中的地址空间正在耗尽。IPv6 旨在提供足够的地址空间,以便在未来几十年内,世界上的每台计算机都可以获得一个唯一的 IP 地址。以下是 IPv6 的一些主要功能:
- IPv6 使用 128 位数字作为 IP 地址,而不是 IPv4 中使用的 32 位数字。
- 它的 IP 数据包报头格式与 IPv4 不同。IPv4 的每个数据报只有一个报头,而 IPv6 的每个数据报只有一个基本报头,后跟多个可变长度的扩展报头。
- IPv6 支持比 IPv4 更大的数据报。
- 在 IPv4 中,路由器执行 IP 数据包分段。在 IPv6 中,应该由发送方主机而不是路由器来执行数据包分段。这意味着使用 IPv6 的主机必须预先知道最大传输单元(MTU)的路径,即所有网络到目的主机的最大数据包大小的最小值。当 IP 数据报不得不进入一个比数据报离开的网络具有更小传输容量的网络时,就会发生 IP 数据报的分段。在 IPv4 中,分段由路由器执行,它检测路由中传输容量较低的网络。因为 IPv6 只允许主机执行分段,所以主机必须发现最小大小的数据报,该数据报可以通过所有可能的路由从源主机路由到目的主机。
- IPv6 支持在报头中指定数据报的路由信息,以便路由器可以使用它通过特定的路由来路由数据报。此功能有助于传递时间关键的信息。
- IPv6 具有可扩展性。可以将任意数量的扩展报头添加到 IPv6 数据报中,这可以用一种新的方式来解释。
IPv6 使用 128 位 IP 地址。它使用易于理解的符号以文本形式表示 IP 地址。这 128 位被分成 8 个字段,每个字段 16 位。每个字段都以十六进制形式书写,并用冒号分隔。以下是 IPv6 地址的一些示例:
F6DC:0:0:4015:0:BA98:C0A8:1E7F6DC:0:0:7678:0:0:0:A21DF6DC:0:0:0:0:0:0:A21D0:0:0:0:0:0:0:1
IPv6 地址中有许多字段的值为零是很常见的,尤其是对于所有 IPv4 地址。IPv6 地址表示法允许您通过使用两个连续的冒号来压缩连续的零值字段。在一个地址中,只能使用两个冒号来隐藏一次连续的零值字段。可以使用零压缩技术重写上述 IPv6 地址:
F6DC::4015:0:BA98:C0A8:1E7F6DC:0:0:7678::A21DF6DC::A21D::1
注意,我们可以只抑制第二个地址F6DC:0:0:7678::A21D中两组连续零字段中的一组。将其重写为F6DC::7678::A21D是无效的,因为它不止一次使用了两个冒号。您可以使用两个冒号来隐藏连续的零字段,这些零字段可能出现在地址字符串的开头、中间或结尾。如果一个地址中包含所有的零,你可以简单地把它表示为::。
您也可以在 IPv6 地址中混合十六进制和十进制格式。当您有一个 IPv4 地址并希望以 IPv6 格式书写时,这种表示法非常有用。您可以使用如上所述的十六进制表示法编写前六个 16 位字段,并对 IPv4 的后两个 16 位字段使用点分十进制表示法。混合记数法采用X:X:X:X:X:X:D.D.D.D的形式,其中X是十六进制数,D是十进制数。您可以使用以下符号重写上述 IPv6 地址:
F6DC::4015:0:BA98:192.168.1.231F6DC:0:0:7678::0.0.162.29F6DC::0.0.162.29::0.0.0.1
与 IPv4 不同,IPv6 不基于网络类别分配 IP 地址。像 IPv4 一样,它使用 CIDR 地址,因此 IP 地址中前缀和后缀之间的边界可以在任意位指定。例如,::1可以用 CIDR 符号表示为::1/128,其中 128 是前缀长度。
Tip
当 IPv6 地址作为 URL 的一部分在文字字符串中使用时,应该用括号([])括起来。此规则不适用于 IPv4。例如,如果您使用 IPv4 地址访问回环地址上的 web 服务器,您可以使用类似于 http://127.0.0.1/index.html 的 URL。在 IPv6 地址符号中,您需要使用类似http://[::1]/index.html的 URL。在使用之前,请确保您的浏览器在其 URL 中支持 IPv6 地址表示法。
特殊 IP 地址
一些 IP 地址用于特殊目的。一些这样的 IP 地址如下:
- 环回 IP 地址
- 单播 IP 地址
- 多播 IP 地址
- 任播 IP 地址
- 广播 IP 地址
- 未指定的 IP 地址
以下部分详细描述了这些特殊 IP 地址的使用。
环回 IP 地址
您需要至少两台通过网络连接的计算机来测试或运行网络程序。当您想在项目的开发阶段测试网络程序时,有时建立网络可能不可行或不理想。IP 的设计者们意识到了这种需求。IP 编址方案中规定将 IP 地址视为环回地址,以便只使用一台计算机测试网络程序。当协议组中的 Internet 层检测到回送 IP 地址作为 IP 数据报的目的地时,它不会将数据包传递给它下面的协议层(即网络接口层)。相反,它会掉头(或环回,因此得名环回地址)并将数据包路由回同一台计算机上的传输层。传输层将数据包传送到同一台主机上的目的地进程,就像数据包来自远程主机一样。回送 IP 地址使得使用一台计算机测试网络程序成为可能。图 5-5 描述了 IP 处理发往环回 IP 地址的互联网数据包的方式。数据包不会离开源计算机。它被互联网层截获,并被路由回它所来自的同一台计算机。
图 5-5。
An Internet packet that has a loopback IP address as its destination is routed back to the same computer from the Internet protocol in the internet layer
回送 IP 地址是保留地址,IP 不需要将回送 IP 地址作为目的地址的数据包转发到网络接口层。
在 IPv4 寻址方案中,127.X.X.X块是为环回地址保留的,其中X是 0 到 255 之间的十进制数。通常,127.0.0.1被用作 IPv4 中的环回地址。但是,您并不局限于仅使用127.0.0.1作为唯一的环回地址。如果您愿意,也可以使用127.0.0.2或127.3.5.11作为有效的环回地址。通常,名称localhost被映射到计算机上的回送地址127.0.0.1。
在 IPv6 寻址方案中,只有一个环回地址,足以对网络程序执行任何本地测试。就是0:0:0:0:0:0:0:1或者干脆就是::1。
单播 IP 地址
单播是网络上两台计算机之间的一对一通信,其中 IP 数据包被传送到一台远程主机。单播 IP 地址标识网络上唯一的主机。IPv4 和 IPv6 支持单播 IP 地址。
多播 IP 地址
多播是一种一对多的通信,其中一台计算机发送一个 IP 数据包,该数据包被传送到多台远程计算机。多播让您实现组交互的概念,如音频或视频会议,其中一台计算机向组中的所有计算机发送信息。使用多播代替多个单播的好处是发送方只发送一份数据包。数据包的一个副本会尽可能长地沿着网络传输。如果包的接收者在多个网络上,则在需要时制作包的副本,并且包的每个副本被独立地路由。最后,每个接收者都会收到一份单独的数据包副本。多播是群组成员之间通信的有效方式,因为它减少了网络流量。
一个 IP 数据包只有一个目的 IP 地址。如何使用多播将 IP 数据包传送到多台主机?IP 在其地址空间中包含一些地址作为多播地址。如果数据包的地址是组播地址,该数据包将被传送到多台主机。多播数据包传送的概念与活动的组成员资格相同。当一个组形成时,该组被给予一个组 ID。寻址到该组 ID 的任何信息被传递给所有组成员。在多播通信中,使用多播 IP 地址(类似于组 ID)。多播数据包被寻址到该多播地址。每个感兴趣的主机向其感兴趣的本地路由器注册其 IP 地址,以便在该多播地址上进行通信。主机和本地路由器之间的注册过程是使用互联网组管理协议(IGMP)完成的。当路由器收到带有组播地址的数据包时,它会将该数据包的副本发送给向其注册了该组播地址的每台主机。接收者可以通过通知路由器来选择在任何时候离开多播组。
多播数据包在到达接收主机之前可能会经过许多路由器。多播数据包的所有接收者可能不在同一个网络上。有许多处理多播数据包路由的协议,如距离矢量多播路由协议(DVMRP)。
IPv4 和 IPv6 都支持组播寻址。在 IPv4 中,D类网络地址用于多播。也就是说,IPv4 中的多播地址中的四个最高位是1110。在 IPv6 中,组播地址的前 8 位设置为 1。也就是说,IPv6 中的组播地址总是以FF开头。例如,FF0X:0:0:0:0:0:2:0000是 IPv6 中的组播地址。
任播 IP 地址
任播是一种一对一的群组通信,其中一台计算机向一组计算机发送数据包,但该数据包只发送给该组中的一台计算机。IPv4 不支持任播。IPv6 支持任播。在任播中,同一个地址被分配给多台计算机。当路由器收到发往任播地址的数据包时,它会将该数据包传送到最近的计算机。当服务已经在许多主机上复制,并且您想要在离客户端最近的主机上提供服务时,任播是有用的。有时,任播寻址也称为集群寻址。使用单播地址空间中的任播地址。您无法通过查看位的排列来区分单播地址和任播地址。当同一个单播地址被分配给多台主机时,它被视为一个任播地址。请注意,路由器必须知道分配了任播地址的主机,这样它才能将寻址到该任播地址的数据包传送到最近的主机之一。
广播 IP 地址
广播是一种一对多的通信,其中一台计算机发送一个数据包,该数据包将被传送到网络上的所有计算机。IPv4 分配一些地址作为广播地址。当所有 32 位都设置为 1 时,它就形成了一个广播地址,数据包将被发送到本地子网上的所有主机。当主机地址中的所有位都设置为 1 并且指定了网络地址时,它就形成了指定网络号的广播地址。例如,255.255.255.255是本地子网的广播地址,192.168.1.255是网络192.168.1.0的广播地址。IPv6 没有广播地址。在 IPv6 中,您需要使用多播地址作为广播地址。
未指定的 IP 地址
IPv4 中的0.0.0.0和 IPv6 中的::(注意,::表示 128 位 IPv6 地址,所有位都设置为零)被称为未指定地址。主机使用此地址作为源地址来表示它还没有 IP 地址,例如在启动过程中,它还没有被分配 IP 地址。
端口号
端口号是 16 位无符号整数,范围从 0 到 65535。有时端口号也简称为端口。一台计算机运行许多进程,这些进程与远程计算机上运行的其他进程进行通信。当传输层收到来自 Internet 层的传入数据包时,它需要知道该数据包应该发送到该计算机上的哪个进程(运行在应用层)。端口号是一个逻辑编号,传输层使用它来识别计算机上数据包的目的进程。
每个传入传输层的数据包都有一个协议;例如,传输层中的 TCP 协议处理程序处理 TCP 数据包,而传输层中的 UDP 协议处理程序处理 UDP 数据包。
在应用层,一个进程使用它希望与远程进程通信的每个通信信道的独立协议。一个进程为它为特定协议打开的每个通信信道使用一个唯一的端口号,并在传输层的特定协议模块中注册该端口号。因此,对于特定协议,端口号必须是唯一的。例如,进程 P1 可以对 TCP 协议使用端口号 1988,而另一个被调用的进程 P2 可以在同一台计算机上对 UDP 协议使用相同的端口号 1988。主机上的进程使用远程进程的协议和端口号向远程进程发送数据。
计算机上的进程如何开始与远程进程通信?例如,当你访问雅虎的网站时,你只需输入 http://www.yahoo.com 作为网页地址。在该网页地址中,http表示应用层协议,其使用 TCP 作为传输层协议, www.yahoo.com 是机器名称,其使用域名系统(DNS)解析为 IP 地址。由 www.yahoo.com 标识的机器可能正在运行许多进程,这些进程可能使用http协议。您的网络浏览器连接到 www.yahoo.com 上的哪个进程?由于许多人使用雅虎的网站,它需要在一个众所周知的端口运行它的http服务,这样每个人都可以使用那个端口连接到它。通常,http web 服务器运行在端口 80。可以用 http://www.yahoo.com:80 ,和用 http://www.yahoo.com 一样。并不总是需要在端口 80 运行http web 服务器。如果您没有在端口 80 运行您的http web 服务器,想要使用您的http服务的人必须知道您正在使用的端口。IANA 负责推荐哪些端口号用于知名服务。IANA 将端口号分为三个范围:
- 已知端口:0 -1023
- 注册端口:1024 - 49151
- 动态和/或专用端口:49152 - 65535
众所周知的端口号被全球提供的最常用的服务使用,例如 HTTP、FTP 等。表 5-3 列出了一些用于知名应用层协议的知名端口。通常,您需要管理权限才能使用计算机上众所周知的端口。
表 5-3。
A Partial List of Well-Known Ports Used for Some Application Layer Protocols
| 应用层协议 | 通道数 | | --- | --- | | `echo` | `7` | | `FTP` | `21` | | `Telnet` | `23` | | `SMTP` | `25` | | `HTTP` | `80` | | `POP3` | `110` | | `NNTP` | `119` |组织(或用户)可以在应用要使用的已注册端口范围中向 IANA 注册一个端口号。例如,已经为 RMI 注册表注册了 1099 (TCP/UDP)端口(RMI 代表远程方法调用)。
任何应用都可以使用动态/专用端口号范围内的端口号。
套接字 API 和客户机-服务器范例
我还没有开始讨论在 Java 程序中使网络通信成为可能的 Java 类。在这一节中,我将介绍套接字和在两台远程主机之间的网络通信中使用的客户机-服务器范例。
在前面几节中,我简要介绍了不同的底层协议及其职责。是时候在协议栈中向上移动,讨论应用层和传输层之间的交互了。应用如何使用这些协议与远程应用通信?操作系统提供了一个称为套接字的应用接口(API ),它允许两个远程应用进行通信,从而利用协议栈中的低层协议。套接字不是另一层协议。它是传输层和应用层之间的接口。它提供了两层之间的标准通信方式,这反过来又提供了两个远程应用之间的标准通信方式。
有两种插座:
- 面向连接的套接字
- 无连接插座
面向连接的套接字也称为流套接字。无连接套接字也称为数据报套接字。请注意,数据总是使用 IP 数据报从互联网上的一台主机一次一个数据报地发送到另一台主机。
传输层中使用的传输控制协议(TCP)是提供面向连接的套接字的最广泛使用的协议之一。应用将数据移交给 TCP 套接字,TCP 负责将数据流式传输到目的主机。TCP 处理所有问题,如排序、分段、组装、丢失数据检测、重复数据传输等。这给应用的印象是,数据像连续的字节流一样从源应用流向目标应用。使用 TCP 套接字的两台主机之间不存在硬件级的物理连接。都是用软件实现的。有时也称为 a。两个套接字的组合唯一地定义了一个连接。
在面向连接的套接字通信中,客户端和服务器端创建套接字,建立连接,并交换信息。TCP 负责处理数据传输过程中可能出现的错误。TCP 也称为可靠的传输层协议,因为它保证数据的传输。如果它由于某种原因不能传递数据,它将通知发送方应用有关错误的情况。发送数据后,它会等待接收方的确认,以确保数据到达目的地。然而,TCP 提供的可靠性是有代价的。与无连接协议相比,开销更大,速度也更慢。TCP 确保发送方向接收方发送一定量的数据,这可以由接收方的缓冲区大小来处理。它还处理网络上的流量拥塞。当它检测到交通拥堵时,它会减慢数据传输速度。Java 支持 TCP 套接字。
在传输层中使用的用户数据报协议(UDP)是使用最广泛的提供无连接套接字的协议。它不可靠,但是快得多。它允许您发送有限大小的数据—一次一个包,这与 TCP 不同,TCP 允许您将数据作为任意大小的流发送,处理将数据分段成适当大小的包的细节。当您使用 UDP 发送数据时,无法保证数据传递。然而,它仍然在许多应用中使用,并且工作得非常好。发送方将 UDP 数据包发送到目的地,然后忘记了它。如果接收者得到它,它得到它。否则,接收方无法知道是否有 UDP 数据包发送给了它。您可以将 TCP 和 UDP 中使用的通信与电话和邮件中使用的通信进行比较。电话交谈是可靠的,它提供了通信双方之间的确认。当你邮寄一封信时,你不知道收信人什么时候收到,或者他是否收到了。UDP 和 TCP 还有一个重要的区别。UDP 不保证数据的排序。也就是说,如果您使用 UDP 向目的地发送五个数据包,这五个数据包可能以任何顺序到达。但是,TCP 保证数据包将按照发送的顺序传送。Java 支持 UDP 套接字。
你应该使用哪种协议:TCP 还是 UDP?这取决于应用将如何使用。如果数据完整性至关重要,您应该使用 TCP。如果速度优先于较低的数据完整性,您应该使用 UDP。例如,文件传输应用应该使用 TCP,而视频会议应用应该使用 UDP。如果您丢失了几个像素的视频数据,这对视频会议没有太大影响。它可以继续。但是,如果在传输文件时丢失了几个字节的数据,该文件可能根本就不可用。
两个远程应用如何开始通信?哪个应用发起通信?应用如何知道远程应用有兴趣与之通信?你有没有拨过一个公司的客服电话和客服代表通话?如果你与一家公司的客户服务代表交谈过,你就已经体验过两个远程应用的通信。我将在本节中参考使用公司客服的机制来解释远程通信。你和一个公司的代表在两个遥远的地方。你需要一项服务,公司就提供这项服务。换句话说,你是客户,公司是服务商(或者服务器)。你不知道什么时候你会需要公司的服务。公司提供了客服电话,你可以联系公司。公司还做了一件事。公司必须做什么来为你提供服务?你能猜到吗?它会按照给你的电话号码等待你的来电。沟通必须发生在你和公司之间,而公司已经通过被动等待你的电话在沟通中向前迈出了一步。一旦你拨了公司的号码,一个连接就建立了,你和公司的代表交换信息。最后,你们俩都挂断了电话,停止了交流。使用套接字的网络通信类似于您和公司代表之间的通信。如果你理解了这个通信的例子,理解套接字就很容易了。
两个远程应用使用一对套接字进行通信。任何通信都需要两个端点。套接字是通信信道两端的通信端点。一对套接字上的通信遵循典型的客户机-服务器通信范例。一个应用创建一个套接字,被动地等待另一个远程应用的联系。等待远程应用联系它的应用被称为服务器应用或简称为服务器。另一个应用创建一个套接字,并启动与等待的服务器应用的通信。这被称为客户端应用或简称为客户端。在客户端和服务器可以交换信息之前,必须执行许多其他步骤。例如,服务器必须公布其位置和其他详细信息,以便客户可以联系它。
套接字会经历不同的状态。每个状态都标志着一个事件。套接字的状态告诉我们套接字能做什么和不能做什么。通常,套接字的生命周期由表 5-4 中列出的八个原语来描述。
表 5-4。
The List of Typical Socket Primitives and Their Descriptions
| 基元 | 描述 | | --- | --- | | `Socket` | 创建一个套接字,应用使用该套接字作为通信端点。 | | `Bind` | 将本地地址与套接字关联。本地地址包括 IP 地址和端口号。端口号必须是 0 到 65535 之间的数字。对于计算机上用于套接字的协议,它应该是唯一的。例如,如果 TCP 套接字使用端口 12456,UDP 套接字也可以使用相同的端口号 12456。 | | `Listen` | 为客户端请求定义其等待队列的大小。它只能由面向连接的服务器套接字执行。 | | `Accept` | 等待客户端请求到达。它只能由面向连接的服务器套接字执行。 | | `Connect` | 尝试建立到服务器套接字的连接,该套接字正在等待一个`accept`原语。它是由面向连接的客户端套接字执行的。 | | `Send/Sendto` | 发送数据。通常`send`表示在面向连接的套接字上的发送操作,而`Sendto`表示在无连接套接字上的发送操作。 | | `Receive/ReceiveFrom` | 接收数据。他们是`Send`和`Sendto`的对应。 | | `Close` | 关闭连接。 |下面几节详细阐述了每个套接字原语。
套接字原语
服务器通过指定套接字的类型来创建套接字:流套接字或数据报套接字。
绑定原语
原语将套接字与本地 IP 地址和端口号相关联。请注意,一台主机可以有多个 IP 地址。套接字可以绑定到主机的一个 IP 地址或所有 IP 地址。将套接字绑定到主机的所有可用 IP 地址也称为绑定到通配符地址。绑定为这个套接字保留端口号。没有其他套接字可以使用该端口号进行通信。传输协议(TCP 和 UDP)将使用绑定端口来路由发往此套接字的数据。在这一节的后面,我将详细解释传输层和套接字之间的数据传输。现在,只需要理解,在绑定中,套接字告诉传输层这是我的 IP 地址和端口号,如果您获得了寻址到该地址的任何数据,请将该数据传递给我。套接字绑定的 IP 地址和端口号分别称为套接字的本地地址和本地端口。
Listen 原语
服务器通知操作系统将套接字置于被动模式,以便它等待传入的客户端请求。此时,服务器还没有准备好接受任何客户机请求。服务器还指定套接字的等待队列大小。当客户机在这个套接字上联系服务器时,客户机请求被放入那个队列中。最初,队列是空的。如果客户机在这个套接字上联系服务器,并且等待队列已满,那么客户机的请求将被拒绝。
接受原语
服务器通知操作系统这个套接字已经准备好接受客户机请求。如果服务器使用无连接传输协议(如 UDP)的套接字,则不执行此步骤。对 TCP 服务器套接字执行此步骤。当套接字向操作系统发送接受消息时,它会一直阻塞,直到接收到客户端对新连接的请求。
连接原语
只有面向连接的客户端套接字执行此步骤。这是套接字通信中最重要的阶段。客户端套接字向服务器套接字发送请求以建立连接。服务器套接字已经发出了accept,并且一直在等待客户端请求的到达。客户机套接字发送服务器套接字的 IP 地址和端口号。回想一下,服务器套接字在开始侦听和接受来自外部的连接之前绑定了 IP 地址和端口号。随着它的请求,客户机套接字也发送它自己的 IP 地址和它已经绑定到的端口号。
这时出现了一个重要的问题。TCP 等传输层如何知道来自客户端的数据包(以连接请求的形式)必须被传递给服务器套接字?在绑定阶段,套接字指定其本地 IP 地址和本地端口号,以及远程 IP 地址和远程端口号。如果服务器套接字只想接受来自特定远程主机 IP 地址和端口号的连接,它可以这样做。通常,服务器套接字将接受来自任何客户端的连接,并将指定一个未指定的 IP 地址和一个零端口号作为其远程地址。服务器套接字将五条信息(本地 IP 地址、本地端口号、远程 IP 地址、远程端口号和缓冲区)传递给传输层。传输层将它们存储在一个称为传输控制块(TCB)的特殊结构中,以备将来使用。当来自外部的数据包到达传输层时,它会根据传入数据包中包含的四条信息来查找其 TCB,。回想一下,客户端将每个 TCP 数据包中的源地址和目的地址发送给服务器。传输层试图找到与源地址和目的地址相关联的缓冲区。如果它找到一个缓冲区,它将传入的数据传输到缓冲区,并通知套接字缓冲区中有它的一些信息。如果服务器套接字接受来自任何客户机的请求(远程地址全为零),来自任何客户机的数据都将被路由到其缓冲区。
一旦服务器套接字检测到来自客户机的请求,它就用远程客户机的地址信息创建一个新的套接字。使用绑定新的套接字,并且创建新的缓冲区并将其绑定到该组合地址。事实上,为一个套接字创建了两个缓冲区:一个用于传入数据,一个用于传出数据。此时,服务器套接字让新套接字与请求连接的客户机套接字进行通信。服务器套接字本身可以关闭自己(不再接受客户端的连接请求),或者它可以再次开始等待接受另一个客户端的连接请求。
在两个套接字(客户机和服务器)之间建立连接后,它们可以交换信息。TCP 连接支持全双工连接。也就是说,数据可以同时双向发送或接收。
客户端套接字在尝试连接到服务器之前,知道其本地 IP 地址、本地端口号、远程 IP 地址和远程端口号。在客户端,TCB 的创建遵循类似的规则。
一旦客户机和服务器套接字就位,两个套接字(客户机套接字和专用于客户机的服务器套接字)就定义了一个连接。
服务器套接字就像坐在办公室(服务器)前台的接待员。一位客户走进来,先和接待员交谈。连接请求来自客户端到服务器,首先联系服务器套接字。接待员将客户交给另一名员工。在这一点上,接待员的工作对客户来说已经结束了。她继续她的工作,等待迎接另一个客户来到办公室。与此同时,第一个客户可以继续与另一个员工交谈,只要他需要。类似地,服务器套接字创建一个新的套接字,并将该新的套接字分配给客户机,以便进行任何进一步的通信。一旦服务器套接字为客户机分配了一个新的套接字,它的工作就结束了。它将等待来自另一个客户端的另一个连接请求。请注意,除了许多其他细节之外,一个套接字还有五个重要的相关信息:协议、本地 IP 地址、本地端口号、远程 IP 地址和远程端口号。
Send/Sendto 原语
这是套接字发送数据的阶段。
接收/接收自原语
这是套接字接收数据的阶段。
封闭原语
是说再见的时候了。最后,服务器和客户端套接字关闭连接。
后续部分将讨论支持不同类型套接字的 Java 类,以便于网络编程。与网络编程相关的 Java 类在java.net、javax.net和javax.net.ssl包中。
表示机器地址
互联网协议使用机器的 IP 地址来传送数据包。在程序中使用 IP 地址并不容易,因为它是数字格式。您可能能够记住并使用 IPv4 地址,因为它们的长度只有四个十进制数字。记住和使用 IPv6 地址有点困难,因为它们是十六进制格式的八个数字。每台电脑也有一个名字,如 www.yahoo.com 。在你的程序中使用一个计算机名会使你的生活变得更容易。Java 提供了允许您在 Java 程序中使用计算机名或 IP 地址的类。如果您使用计算机名,Java 会使用 DNS 将计算机名解析为其 IP 地址。
InetAddress类的对象代表一个 IP 地址。它有两个子类,Inet4Address和Inet6Address,分别代表 IPv4 和 IPv6 地址。InetAddress类没有公共构造函数。它提供了六个工厂方法来创建它的对象。它们如下。所有人都扔出一个检查过的UnknownHostException。
static InetAddress[] getAllByName(String host)static InetAddress getByAddress(byte[] addr)static InetAddress getByAddress(String host, byte[] addr)static InetAddress getByName(String host)static InetAddress getLocalHost()static InetAddress getLoopbackAddress()
host参数指的是标准格式的计算机名或 IP 地址。addr参数以字节数组的形式引用 IP 地址的各个部分。如果指定 IPv4 地址,addr必须是 4 元素的byte数组。对于 IPv6 地址,它应该是一个 16 元素的byte数组。InetAddress类负责使用 DNS 将主机名解析为 IP 地址。
有时,一台主机可能有多个 IP 地址。getAllByName()方法将所有地址作为InetAddress对象的数组返回。
通常,使用四个工厂方法之一创建一个InetAddress类的对象,并在套接字创建和连接期间将该对象传递给其他方法。下面的代码片段演示了它的一些用法。当您使用InetAddress类或它的子类时,您将需要处理异常。
// Get the IP address of the yahoo web server
InetAddress yahooAddress = InetAddress.getByName("www.yahoo.com
// Get the loopback IP address
InetAddress loopbackAddress = InetAddress.getByName(null);
/* Get the address of the local host. Typically, a name "localhost" is
mapped to a loopback address. Here, we are trying to get the IP address
of the local computer where this code executes and not the loopback address.
*/
InetAddress myComputerAddress = InetAddress.getLocalHost();
清单 5-1 展示了InetAddress类及其一些方法的使用。运行该程序时,您可能会得到不同的输出。
清单 5-1。演示 InetAddress 类的用法
// InetAddressTest.java
package com.jdojo.net;
import java.io.IOException;
import java.net.InetAddress;
public class InetAddressTest {
public static void main(String[] args) {
// Printwww.yahoo.com
printAddressDetails("www.yahoo.com
// Print the loopback address details
printAddressDetails(null);
// Print the loopback address details using IPv6 format
printAddressDetails("::1");
}
public static void printAddressDetails(String host) {
System.out.println("Host '" + host + "' details starts...");
try {
InetAddress addr = InetAddress.getByName(host);
System.out.println("Host IP Address: " + addr.getHostAddress());
System.out.println("Canonical Host Name: " + addr.getCanonicalHostName());
int timeOutinMillis = 10000;
System.out.println("isReachable(): " + addr.isReachable(timeOutinMillis));
System.out.println("isLoopbackAddress(): " + addr.isLoopbackAddress());
}
catch (IOException e) {
e.printStackTrace();
}
finally {
System.out.println("Host '" + host + "' details ends...");
System.out.println("");
}
}
}
Host 'www.yahoo.com
Host IP Address: 98.139.183.24
Canonical Host Name: ir2.fp.vip.bf1.yahoo.com
isReachable(): false
isLoopbackAddress(): false
Host 'www.yahoo.com
Host 'null' details starts...
Host IP Address: 127.0.0.1
Canonical Host Name: 127.0.0.1
isReachable(): true
isLoopbackAddress(): true
Host 'null' details ends...
Host '::1' details starts...
Host IP Address: 0:0:0:0:0:0:0:1
Canonical Host Name: BHMIS-J00BXFL-D.corporate.local
isReachable(): true
isLoopbackAddress(): true
Host '::1' details ends...
表示套接字地址
套接字地址包含两部分,一个 IP 地址和一个端口号。InetSocketAddress类的一个对象代表一个套接字地址。您可以使用以下构造函数来创建一个InetSocketAddress类的对象:
InetSocketAddress(InetAddress addr, int port)InetSocketAddress(int port)InetSocketAddress(String hostname, int port)
所有构造函数都会尝试将主机名解析为 IP 地址。如果主机名无法解析,套接字地址将被标记为未解析,您可以使用isUnresolved()方法进行测试。如果不希望该类在创建其对象时解析地址,可以使用以下工厂方法来创建套接字地址:
static InetSocketAddress createUnresolved(String host, int port)
getAddress()方法返回一个InetAddress对象。如果主机名没有被解析,getAddress()方法返回null。如果您将未解析的InetSocketAddress对象与套接字一起使用,则在绑定过程中会尝试解析主机名。
清单 5-2 显示了如何创建已解析和未解析的InetSocketAddress对象。运行该程序时,您可能会得到不同的输出。
清单 5-2。创建 InetSocketAddress 对象
// InetSocketAddressTest.java
package com.jdojo.net;
import java.net.InetSocketAddress;
public class InetSocketAddressTest {
public static void main(String[] args) {
InetSocketAddress addr1 = new InetSocketAddress("::1", 12889);
printSocketAddress(addr1);
InetSocketAddress addr2 =
InetSocketAddress.createUnresolved("::1", 12881);
printSocketAddress(addr2);
}
public static void printSocketAddress(InetSocketAddress sAddr) {
System.out.println("Socket Address: " + sAddr.getAddress());
System.out.println("Socket Host Name: " + sAddr.getHostName());
System.out.println("Socket Port: " + sAddr.getPort());
System.out.println("isUnresolved(): " + sAddr.isUnresolved());
System.out.println();
}
}
Socket Address: /0:0:0:0:0:0:0:1
Socket Host Name: HYE6754
Socket Port: 12889
isUnresolved(): false
Socket Address: null
Socket Host Name: ::1
Socket Port: 12881
isUnresolved(): true
创建 TCP 服务器套接字
ServerSocket类的对象代表 Java 中的 TCP 服务器套接字。一个ServerSocket对象用于接受来自远程客户端的连接请求。ServerSocket类提供了许多构造函数。您可以使用 no-args 构造函数创建一个未绑定的服务器套接字,并使用它的bind()方法将其绑定到一个本地端口和一个本地 IP 地址。以下代码片段显示了如何创建服务器套接字:
// Create an unbound server socket
ServerSocket serverSocket = new ServerSocket();
// Create a socket address object
InetSocketAddress endPoint = new InetSocketAddress("localhost", 12900);
// Set the wait queue size to 100
int waitQueueSize = 100;
// Bind the server socket to localhost and at port 12900 with
// a wait queue size of 100
serverSocket.bind(endPoint, waitQueueSize);
在ServerSocket类中没有单独的listen()方法对应于listen套接字原语。它的bind()方法负责指定套接字的等待队列大小。
通过使用ServerSocket类的以下任意构造函数,可以在一个步骤中组合create、bind和listen操作。等待队列大小的默认值是 50。本地 IP 地址的默认值是通配符地址,这意味着服务器的所有 IP 地址。
ServerSocket(int port)ServerSocket(int port, int waitQueueSize)ServerSocket(int port, int waitQueueSize, InetAddress bindAddr)
您可以将套接字创建和绑定步骤合并到一个语句中,如下所示:
// Create a server socket at port 12900, with 100 as the wait
// queue size and at the localhost loopback address
ServerSocket serverSocket =
new ServerSocket(12900, 100, InetAddress.getByName("localhost"));
一旦创建并绑定了服务器套接字,它就可以接受来自远程客户端的连接请求。要接受远程连接请求,需要在服务器套接字上调用accept()方法。在来自远程客户端的请求到达其等待队列之前,accept()方法调用会一直阻塞。当服务器套接字接收到一个连接请求时,它从请求中读取远程 IP 地址和远程端口号,并创建一个新的活动套接字。新创建的活动套接字的引用从accept()方法返回。Socket类的一个对象代表新的活动套接字。accept()方法返回一个新的主动套接字,因为它不是像服务器套接字那样等待远程请求的被动套接字。它是一个活动套接字,因为它是为与远程客户端的活动通信而创建的。有时这个活动套接字也称为连接套接字,因为它处理连接上的数据传输。
// Wait for a new remote connection request
Socket activeSocket = serverSocket.accept();
一旦服务器套接字从accept()方法调用返回,服务器应用中的套接字数量就增加一个。您有一个被动服务器套接字和一个主动套接字。新的活动套接字是新客户端连接在服务器上的端点。此时,您需要使用新的活动套接字来处理与客户端的通信。
现在,您可以在新套接字表示的连接上读写数据了。Java TCP 套接字提供全双工连接。它允许您从连接中读取数据以及向连接中写入数据。为此,Socket类包含两个名为getInputStream()和getOutputStream()的方法。getInputStream()方法返回一个InputStream对象,您可以使用它从连接中读取数据。getOutputStream()方法返回一个OutputStream对象,您可以使用它将数据写入连接。您可以使用InputStream和OutputStream对象,就好像您正在从本地文件系统上的一个文件中读取和写入一样。我假设您熟悉 Java I/O。如果您不熟悉 Java I/O,请在继续本节之前参考《Java 语言特性入门》( ISBN: 978-1-4302-6658-7)一书中的第七章。但是,您仍然可以在本章后面的小节中了解 UDP 套接字。当你在连接上读/写完数据后,关闭InputStream/OutputStream,最后关闭套接字。下面的代码片段从客户端读取一条消息,并将其回显给客户端。请注意,在开始通信之前,服务器和客户端必须就消息的格式达成一致。以下代码片段假设客户端一次发送一行文本:
// Create a buffered reader and a buffered writer from the socket's
// input and output streams, so that we can read/write one line at a time
BufferedReader br = new BufferedReader(
new InputStreamReader(activeSocket.getInputStream()));
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(activeSocket.getOutputStream()));
您可以使用br和bw来读取文件或写入文件。从输入流中读取数据的尝试会一直阻塞,直到数据在连接上可用。
// Read one line of text from the connection
String inMsg = br.readLine();
// Write some text to the output buffer
bw.write('hello from server");
bw.flush();
最后,使用套接字的close()方法关闭连接。关闭套接字也会关闭其输入和输出流。事实上,您可以关闭三个(输入流、输出流或套接字)中的一个,其他两个将自动关闭。试图在关闭的套接字上读/写会抛出一个java.net.SocketException。您可以通过使用其isClosed()方法来检查套接字是否关闭,如果套接字关闭,该方法将返回true。
// Close the socket
activeSocket.close();
Tip
一旦你关闭了一个套接字,你就不能再使用它。在使用新套接字之前,必须创建一个新套接字并绑定它。
服务器处理两种工作:接受新的连接请求和响应已经连接的客户端。如果回应客户只需要很少的时间,您可以使用如下所示的策略:
ServerSocket serverSocket = create a server socket here;
while(true) {
Socket activeSocket = serverSocket.accept();
// Handle the client request on activeSocket here
}
上述策略一次处理一个客户端。只有当并发传入连接数非常低,并且客户端的请求只需要很少的时间来响应时,它才适用。如果一个客户端请求需要很长时间才能得到响应,那么所有其他客户端都必须等待才能得到服务。
处理多个客户机请求的另一个策略是在一个单独的线程中处理每个客户机的请求,这样服务器就可以同时为多个客户机服务。以下伪代码概述了这一策略:
ServerSocket serverSocket = create a server socket here;
while(true) {
Socket activeSocket = serverSocket.accept();
Runnable runnable = () -> {
// Handle the client request on the activeSocket here
};
new Thread(runnable).start(); // start a new thread
}
上面的策略似乎工作得很好,直到为并发客户端连接创建了太多的线程。另一个在大多数情况下都有效的策略是用一个线程池来服务所有的客户端连接。如果池中的所有线程都忙于为客户端提供服务,那么请求应该等待,直到有一个线程可以为其提供服务。
清单 5-3 包含了一个 echo 服务器的完整代码。它创建一个新线程来处理每个客户端请求。您现在可以运行 echo 服务器程序了。然而,它不会做很多事情,因为你没有一个客户端程序连接到它。在下一节学习如何创建 TCP 客户端套接字之后,您将看到它的实际应用。
清单 5-3。基于 TCP 套接字的 Echo 服务器
// TCPEchoServer.java
package com.jdojo.net;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPEchoServer {
public static void main(String[] args) {
try {
// Create a Server socket
ServerSocket serverSocket = new ServerSocket(12900, 100,
InetAddress.getByName("localhost"));
System.out.println("Server started at: " + serverSocket);
// Keep accepting client connections in an infinite loop
while (true) {
System.out.println("Waiting for a connection...");
// Accept a connection
final Socket activeSocket = serverSocket.accept();
System.out.println("Received a connection from " +
activeSocket);
// Create a new thread to handle the new connection
Runnable runnable =
() -> handleClientRequest(activeSocket);
new Thread(runnable).start(); // start a new thread
}
}
catch (IOException e) {
e.printStackTrace();
}
}
public static void handleClientRequest(Socket socket) {
BufferedReader socketReader = null;
BufferedWriter socketWriter = null;
try {
// Create a buffered reader and writer for teh socket
socketReader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
socketWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()));
String inMsg = null;
while ((inMsg = socketReader.readLine()) != null) {
System.out.println("Received from client: " + inMsg);
// Echo the received message to the client
String outMsg = inMsg;
socketWriter.write(outMsg);
socketWriter.write("\n");
socketWriter.flush();
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
try {
socket.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
创建 TCP 客户端套接字
Socket类的一个对象代表一个 TCP 客户端套接字。您已经看到了Socket类的对象如何与 TCP 服务器套接字一起工作。对于服务器套接字,您从服务器套接字的accept()方法获得了一个Socket类的对象作为返回值。对于客户端套接字,您必须执行三个额外的步骤:创建、绑定和连接。Socket类提供了许多构造函数,让您指定远程 IP 地址和端口号。这些构造函数将套接字绑定到一个本地主机和一个可用的端口号。以下代码片段显示了如何创建 TCP 客户端套接字:
// Create a client socket, which is bound to the localhost at any
// available port; connected to remote IP of 192.168.1.2 at port 3456
Socket socket = new Socket("192.168.1.2", 3456);
// Create an unbound client socket. bind it, and connect it.
Socket socket = new Socket();
socket.bind(new InetSocketAddress("localhost", 14101));
socket.connect(new InetSocketAddress("localhost", 12900));
一旦获得一个连接的Socket对象,就可以分别使用getInputStream()和getOutputStream()方法来使用它的输入和输出流。您可以在连接上读/写,就像您使用输入和输出流读/写文件一样。
清单 5-4 包含了一个 echo 客户端应用的完整代码。它接收来自用户的输入,将输入发送到 echo 服务器,如清单 5-3 所示,并在标准输出上打印服务器的响应。echo 服务器和 echo 客户机这两个应用必须就它们将要交换的消息格式达成一致。他们一次交换一行文本。值得注意的是,您必须为通过连接发送的每条消息附加一个新行,因为您使用的是BufferedReader类的readLine()方法,该方法只有在遇到新行时才返回。客户端应用必须使用服务器套接字接受连接的相同 IP 地址和端口号。
清单 5-4。基于 TCP 套接字的 Echo 客户端
// TCPEchoClient.java
package com.jdojo.net;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class TCPEchoClient {
public static void main(String[] args) {
Socket socket = null;
BufferedReader socketReader = null;
BufferedWriter socketWriter = null;
try {
// Create a socket that will connect to localhost
// at port 12900\. Note that the server must also be
// running at localhost and 12900.
socket = new Socket("localhost", 12900);
System.out.println("Started client socket at " +
socket.getLocalSocketAddress());
// Create a buffered reader and writer using the socket's
// input and output streams
socketReader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
socketWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()));
// Create a buffered reader for user's input
BufferedReader consoleReader =
new BufferedReader(new InputStreamReader(System.in));
String promptMsg = "Please enter a message (Bye to quit):";
String outMsg = null;
System.out.print(promptMsg);
while ((outMsg = consoleReader.readLine()) != null) {
if (outMsg.equalsIgnoreCase("bye")) {
break;
}
// Add a new line to the message to the server,
// because the server reads one line at a time.
socketWriter.write(outMsg);
socketWriter.write("\n");
socketWriter.flush();
// Read and display the message from the server
String inMsg = socketReader.readLine();
System.out.println("Server: " + inMsg);
System.out.println(); // Print a blank line
System.out.print(promptMsg);
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
// Finally close the socket
if (socket != null) {
try {
socket.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
将 TCP 服务器和客户端放在一起
图 5-6 显示了三个客户端连接到一个服务器的设置。两个Socket对象,一端一个,代表一个连接。服务器中的ServerSocket对象一直在等待来自客户机的连接请求。
图 5-6。
A client-server setup using ServerSocket and socket objects
清单 5-3 和清单 5-4 列出了 TCP echo 服务器和客户端应用的完整程序。您需要首先运行TCPEchoServer类,然后运行TCPEchoClient类。服务器应用等待客户端应用连接。客户端应用提示用户在控制台上输入文本消息。一旦用户输入一条文本消息并按下Enter键,客户端应用就会将该文本发送到服务器。服务器用相同的消息进行响应。这两个应用都在标准输出中打印关于对话的详细信息。以下是 echo 服务器和 echo 客户端的输出。您可以运行TCPEchoClient应用的多个实例。服务器应用在单独的线程中处理每个客户端连接。
以下是服务器应用的输出示例:
Server started at: ServerSocket[addr=localhost/127.0.0.1,port=0,localport=12900]
Waiting for a connection ...
Received a connection from Socket[addr=/127.0.0.1,port=1698,localport=12900]
Waiting for a connection ...
Received from client: Hello
以下是客户端应用的输出示例:
Started client socket at /127.0.0.1:1698
Please enter a message (Bye to quit):Hello
Server: Hello
Please enter a message (Bye to quit):Bye
使用 UDP 套接字
基于 UDP 的套接字是无连接的,是基于数据报的,而 TCP 套接字是面向连接的,是基于流的。作为无连接套接字的效果是两个套接字(客户机和服务器)在通信之前不建立连接。回想一下,TCP 有一个服务器套接字,其唯一的功能是侦听来自远程客户端的连接请求。由于 UDP 是一种无连接协议,因此在使用 UDP 时不会有服务器套接字。在 TCP 套接字中,客户端和服务器之间具有面向流的数据传输的印象是由 TCP 在传输层产生的,因为它具有面向连接的特性。TCP 维护连接两端传输的数据的状态。UDP 是一种无连接协议,其含义是每一方(客户端和服务器)发送或接收一个数据块,而无需事先了解它们之间的通信。在使用 UDP 的通信中,发送到同一目的地的每个数据块都独立于以前发送的数据。使用 UDP 发送的数据块称为数据报或 UDP 数据包。每个 UDP 数据包都有数据、目的 IP 地址和目的端口号。UDP 是一种不可靠的协议,因为它不保证数据包到目标接收方的传递和传递顺序。
Tip
尽管 UDP 是一种无连接协议,但您可以在应用中使用 UDP 构建面向连接的通信。您将需要编写逻辑来处理丢失的数据包、无序的数据包传递以及许多其他事情。TCP 在传输层提供了所有这些特性,您的应用不必担心这些特性。
使用 UDP 套接字编写应用比使用 TCP 套接字编写应用更容易。您只需要处理两个类:
DatagramPacketDatagramSocket
DatagramPacket类的对象代表 UDP 数据报,它是 UDP 套接字上的数据传输单元。DatagramSocket类的一个对象代表一个 UDP 套接字,用于发送或接收数据报数据包。以下是使用 UDP 套接字需要执行的步骤:
Create an object of the DatagramSocket class and bind it to a local IP address and a local port number. Create an object of the DatagramPacket class to hold the destination address and the data to be transmitted. Use the send() method to send the datagram packet to its destination. On the receiving end, use the receive() method to read the datagram packet.
您可以使用其中一个构造函数来创建一个DatagramSocket类的对象。它们都将创建套接字,并将其绑定到本地 IP 地址和本地端口号。请注意,UDP 套接字没有远程 IP 地址和远程端口号,因为它从未连接到远程套接字。它可以从/向任何 UDP 套接字接收/发送数据报分组。
// Create a UDP Socket bound to a port number 15900 at localhost
DatagramSocket udpSocket = new DatagramSocket(15900, "localhost");
DatagramSocket类提供了一个bind()方法,允许您将套接字绑定到一个本地 IP 地址和一个本地端口号。通常,您不需要使用此方法,因为您在构造函数中指定了它需要绑定到的套接字地址,就像您刚才所做的那样。
一个DatagramPacket包含三样东西:目的 IP 地址、目的端口号和数据。DatagramPacket类的构造函数分为两类。其中一个类别的构造函数允许您创建一个DatagramPacket对象来接收数据包。它们只需要缓冲区大小、偏移量和缓冲区中数据的长度。另一类构造函数允许您创建一个DatagramPacket对象来发送数据包。它们要求您指定目的地址和数据。如果您已经创建了一个没有指定目的地址的DatagramPacket,您可以使用setAddress()和setPort()方法设置目的地址。
创建数据包以接收数据的DatagramPacket类的构造函数如下:
DatagramPacket(byte[] buf, int length)DatagramPacket(byte[] buf, int offset, int length)
创建数据包发送数据的DatagramPacket类的构造函数如下:
DatagramPacket(byte[] buf, int length, InetAddress address, int port)DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)DatagramPacket(byte[] buf, int length, SocketAddress address)DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
以下代码片段演示了创建数据报数据包的一些方法:
// Create a packet to receive 1024 bytes of data
byte[] data = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length);
// Create a packet that a has buffer size of 1024, but it will receive
// data starting at offset 8 (offset zero means the first element in
// the array) and it will receive only 32 bytes of data.
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2, 8, 32);
// Create a packet to send 1024 bytes of data that has a destination
// address of "localhost" and port 15900\. Will need to populate data3
// array before sending the packet.
byte[] data3 = new byte[1024];
DatagramPacket packet3 = new DatagramPacket(data3, 1024,
InetAddress.getByName("localhost"), 15900);
// Create a packet to send 1024 bytes of data that has a destination
// address of "localhost" and port 15900\. Will need to populate data4
// array before sending the packet. The code sets the destination
// address by calling methods on the packet instead of specifying it
// in its constructor.
byte[] data4 = new byte[1024];
DatagramPacket packet4 = new DatagramPacket(data4, 1024);
packet4.setAddress(InetAddress.getByName("localhost"));
packet4.setPort(15900);
了解数据包中的数据始终具有指定的偏移量和长度非常重要。在从数据包中读取数据时,您需要使用这两条信息。假设一个receivedPacket对象引用代表一个从远程 UDP 套接字接收的DatagramPacket。DatagramPacket类的getData()方法返回包的缓冲区(一个字节数组)。数据包可以有比从远程客户端接收的数据更大的缓冲区。在这种情况下,您必须使用偏移量和长度从缓冲区中读取接收到的数据,而不接触缓冲区中的垃圾数据。如果数据包的缓冲区大小小于接收的数据大小,多余的字节会被忽略。您应该使用类似如下所示的代码来读取套接字接收的数据。关键是你应该从指定的offset开始使用接收缓冲区中的数据,使用的字节数由它的length属性决定。
// Get the packet's buffer, offset, and length
byte[] dataBuffer = receivedPacket.getData();
int offset = receivedPacket.getOffset();
int length = receivedPacket.getLength();
// Copy the received data using offset and length to receivedData array,
// which will hold all good data
byte[] receivedData = new byte[length];
System.arraycopy(dataBuffer, offset, receivedData, 0, length);
创建 UDP 套接字(客户机和服务器)就像创建一个DatagramSocket类的对象一样简单。你可以用它的send()方法发送一个包。您可以使用receive()方法从远程套接字接收数据包。receive()方法阻塞,直到一个包到达。您向receive()方法提供一个空的数据包。套接字用它从远程套接字接收的信息填充它。如果所提供的数据报分组的数据缓冲区大小小于所接收的数据报分组的数据缓冲区大小,则所接收的数据被无声地截断以适合所提供的数据报分组。如果所提供的数据报包的数据缓冲区大小大于所接收的数据报包的数据缓冲区大小,则套接字会将所接收的数据复制到其偏移量和长度属性所指示的数据段中所提供的数据缓冲区,而不会触及缓冲区的其他部分。请注意,可用的数据缓冲区大小不是字节数组的大小。相反,它是由长度定义的。例如,假设您有一个数据报数据包,其字节数组包含 32 个元素,偏移量为 2,数据缓冲区长度为 8。如果您将这个数据报数据包传递给receive()方法,将复制最多 8 个字节的接收数据。数据将从缓冲器中的第三个元素复制到第十一个元素,分别由偏移量 2 和长度 8 表示。
// Create a UDP socket bound to a port number 15900 at localhost
DatagramSocket socket = new DatagramSocket(15900,
InetAddress.getByName("localhost"));
// Send a packet assuming that you have a datagram packet in p
socket.send(p);
// Receive a packet
DatagramPacket p2 = new DatagramPacket(new byte[1024], 1024);
socket.receive(p2);
创建 UDP Echo 服务器
使用 UDP 创建 echo 服务器非常容易。它只需要四行真正的代码。使用以下步骤创建 UDP echo 服务器:
Create a DatagramSocket object to represent a UDP socket. Create a DatagramPacket object to receive the packet from a remote client. Call the receive() method of the socket to wait for a packet to arrive. Call the send() method of the socket passing the same packet that you received. When a UDP packet is received by a server, it contains the sender’s address. You do not need to change anything in the packet to echo back the same message to the sender of the packet. When you prepare a datagram packet for sending, you need to set a destination address. When the packet arrives at its destination, it contains its sender’s address. This is useful in case the receiver wants to respond to the sender of the datagram packet.
以下代码片段显示了如何编写 UDP echo 服务器:
DatagramSocket socket = new DatagramSocket(15900);
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
while(true) {
// Receive the packet
socket.receive(packet);
//Send back the same packet to the sender
socket.send(packet);
}
清单 5-5 是 UDP echo 服务器相同代码的扩展版本。它包含与上面所示相同的基本逻辑。此外,它还有代码来处理错误,并在标准输出中打印数据包的详细信息。
清单 5-5。基于 UDP 套接字的 Echo 服务器
// UDPEchoServer.java
package com.jdojo.net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPEchoServer {
public static void main(String[] args) {
final int LOCAL_PORT = 15900;
final String SERVER_NAME = "localhost";
try {
DatagramSocket udpSocket =
new DatagramSocket(LOCAL_PORT,
InetAddress.getByName(SERVER_NAME));
System.out.println("Created UDP server socket at " +
udpSocket.getLocalSocketAddress() + "...");
// Wait for a message in a loop and echo the same
// message to the sender
while (true) {
System.out.println("Waiting for a UDP packet" +
" to arrive...");
// Prepare a packet to hold the received data
DatagramPacket packet =
new DatagramPacket(new byte[1024], 1024);
// Receive a packet
udpSocket.receive(packet);
// Print the packet details
displayPacketDetails(packet);
// Echo the same packet to the sender
udpSocket.send(packet);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
public static void displayPacketDetails(DatagramPacket packet) {
// Get the message
byte[] msgBuffer = packet.getData();
int length = packet.getLength();
int offset = packet.getOffset();
int remotePort = packet.getPort();
InetAddress remoteAddr = packet.getAddress();
String msg = new String(msgBuffer, offset, length);
System.out.println("Received a packet:[IP Address=" +
remoteAddr + ", port=" + remotePort +
", message=" + msg + "]");
}
}
清单 5-6 包含客户端应用的程序,该程序使用 UDP 套接字向/从 UDP echo 服务器发送/接收消息。请注意,客户端和服务器一次交换一行文本。
清单 5-6。基于 UDP 套接字的 Echo 客户端
// UDPEchoClient.java
package com.jdojo.net;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class UDPEchoClient {
public static void main(String[] args) {
DatagramSocket udpSocket = null;
BufferedReader br = null;
try {
// Create a UDP socket at localhost using an available port
udpSocket = new DatagramSocket();
String msg = null;
// Create a buffered reader to get an input from a user
br = new BufferedReader(new InputStreamReader(System.in));
String promptMsg = "Please enter a message (Bye to quit):";
System.out.print(promptMsg);
while ((msg = br.readLine()) != null) {
if (msg.equalsIgnoreCase("bye")) {
break;
}
// Prepare a packet to send to the server
DatagramPacket packet = UDPEchoClient.getPacket(msg);
// Send the packet to the server
udpSocket.send(packet);
// Wait for a packet from the server
udpSocket.receive(packet);
// Display the packet details received from
// the server
displayPacketDetails(packet);
System.out.print(promptMsg);
}
}
catch (Exception e) {
e.printStackTrace();
}
finally {
// Close the socket
if (udpSocket != null) {
udpSocket.close();
}
}
}
public static void displayPacketDetails(DatagramPacket packet) {
byte[] msgBuffer = packet.getData();
int length = packet.getLength();
int offset = packet.getOffset();
int remotePort = packet.getPort();
InetAddress remoteAddr = packet.getAddress();
String msg = new String(msgBuffer, offset, length);
System.out.println("[Server at IP Address=" + remoteAddr +
", port=" + remotePort + "]: " + msg);
// Add a line break
System.out.println();
}
public static DatagramPacket getPacket(String msg)
throws UnknownHostException {
// We will send and accept a message of 1024 bytes in length.
// longer messages will be truncated
final int PACKET_MAX_LENGTH = 1024;
byte[] msgBuffer = msg.getBytes();
int length = msgBuffer.length;
if (length > PACKET_MAX_LENGTH) {
length = PACKET_MAX_LENGTH;
}
DatagramPacket packet = new DatagramPacket(msgBuffer, length);
// Set the destination address and the port number
int serverPort = 15900;
final String SERVER__NAME = "localhost";
InetAddress serverIPAddress =
InetAddress.getByName(SERVER__NAME);
packet.setAddress(serverIPAddress);
packet.setPort(serverPort);
return packet;
}
}
为了测试 UDP echo 应用,您需要运行UDPEchoServer和UDPEchoClient类。您需要首先运行服务器。客户端应用将提示您输入一条消息。输入一条短信并按下Enter键将该短信发送至服务器。服务器将回显相同的消息。两个应用都在标准输出上显示正在交换的消息。它们还显示数据包的详细信息,如发送者的 IP 地址和端口号。服务器应用使用端口号 15900,客户端应用使用计算机上任何可用的 UDP 端口。如果您得到一个错误,这意味着端口号 15900 正在使用,因此您需要在服务器程序中更改端口号,并在客户端程序中使用新的端口号来寻址数据包。该服务器被设计为同时处理多个客户端。您可以运行UDPEchoClient类的多个实例。请注意,服务器在无限循环中运行,您必须手动停止服务器应用。
以下是服务器控制台上的日志示例:
Created UDP server socket at /127.0.0.1:15900...
Waiting for a UDP packet to arrive...
Received a packet:[IP Address=/127.0.0.1, port=1522, message=Hello]
Waiting for a UDP packet to arrive...
Received a packet:[IP Address=/127.0.0.1, port=1522, message=Nice talking to you]
Waiting for a UDP packet to arrive...
以下是客户端控制台上的日志示例:
Please enter a message (Bye to quit):Hello
[Server at IP Address=localhost/127.0.0.1, port=15900]: Hello
Please enter a message (Bye to quit):Nice talking to you
[Server at IP Address=localhost/127.0.0.1, port=15900]: Nice talking to
You
Please enter a message (Bye to quit):bye
已连接的 UDP 套接字
UDP 套接字不像 TCP 套接字那样支持端到端连接。DatagramSocket类包含一个connect()方法。这种方法允许应用将 UDP 数据包的发送和接收限制到特定端口号上的特定 IP 地址。考虑以下代码片段:
InetAddress localIPAddress = InetAddress.getByName("192.168.11.101");
int localPort = 15900;
DatagramSocket socket = new DatagramSocket(localPort, localIPAddress);
// Connect the socket to a remote address
InetAddress remoteIPAddress = InetAddress.getByName("192.168.12.115");
int remotePort = 17901;
socket.connect(remoteIPAddress, remotePort);
套接字绑定到本地 IP 地址192.168.11.101和本地 UDP 端口号15900。它连接到一个远程 IP 地址192.168.12.115和一个远程 UDP 端口号17901。这意味着socket对象只能用于向/从另一个运行在 IP 地址192.168.12.115和端口号17901的 UDP 套接字发送/接收数据包。在 UDP 套接字上调用了connect()方法之后,您不需要为传出的数据报数据包设置目的地 IP 地址和端口号。套接字会将在connect()方法调用中使用的目的 IP 地址和端口号添加到所有传出的数据包中。如果您在发送数据包之前提供了目的地址,那么套接字将确保数据包中提供的目的地址与connect()方法调用中使用的远程地址相同。否则,send()方法将抛出一个IllegalArgumentException。
使用 UDP 套接字的connect()方法有两个优点:
- 每次您发送数据包时,它都会设置传出数据包的目的地址。
- 它限制套接字只与远程主机通信,该主机的 IP 地址在
connect()方法的调用中使用。
现在您了解了 UDP 套接字是无连接的,并且您没有使用 UDP 套接字的真正连接。DatagramSocket类中的connect()方法没有为 UDP 套接字提供任何类型的连接。相反,它有助于将通信限制到特定的远程 UDP 套接字。
UDP 多播套接字
Java 支持 UDP 多播套接字,这些套接字可以接收发送到多播 IP 地址的数据报数据包。MulticastSocket类的一个对象代表一个多播套接字。使用MulticastSocket套接字与使用DatagramSocket套接字类似,只有一点不同:多播套接字是基于组成员的。在创建并绑定了一个多播套接字之后,需要调用它的joinGroup(InetAddress multiCastIPAddress)方法,使这个套接字成为由指定的多播 IP 地址multiCastIpAddress定义的多播组的成员。一旦它成为一个多播组的成员,任何发送到该组的数据包都将被传送到这个套接字。一个多播组中可以有多个成员。多播套接字可以是多个多播组的成员。如果一个成员决定不接收来自一个组的多播包,它可以通过调用leaveGroup(InetAddress multiCastIPAddress)方法离开该组。
在 IPv4 中,224.0.0.0到239.255.255.255范围内的任何 IP 地址都可以作为组播地址发送数据报包。IP 地址224.0.0.0是保留的,你不应该在你的应用中使用它。多播 IP 地址不能用作数据报数据包的源地址,这意味着您不能将套接字绑定到多播地址。
套接字本身不一定要成为多播组的成员才能将数据报数据包发送到多播地址。
Java 7 为DatagramChannel类添加了 IP 多播功能。请参阅本章后面的“使用数据报通道的多播”一节,了解如何使用数据报通道进行 IP 多播。注意,DatagramChannel类是在 Java 1.4 中添加的,它没有 IP 多播功能。
清单 5-7 包含一个创建组播套接字的程序,该套接字接收寻址到 230.1.1.1 组播 IP 地址的数据包。
清单 5-7。接收 UDP 多播消息的 UDP 多播套接字
// UDPMultiCastReceiver.java
package com.jdojo.net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
public class UDPMultiCastReceiver {
public static void main(String[] args) {
int mcPort = 18777;
String mcIPStr = "230.1.1.1";
MulticastSocket mcSocket = null;
InetAddress mcIPAddress = null;
try {
mcIPAddress = InetAddress.getByName(mcIPStr);
mcSocket = new MulticastSocket(mcPort);
System.out.println("Multicast Receiver running at:" +
mcSocket.getLocalSocketAddress());
// Join the group
mcSocket.joinGroup(mcIPAddress);
DatagramPacket packet =
new DatagramPacket(new byte[1024], 1024);
while (true) {
System.out.println("Waiting for a multicast message...");
mcSocket.receive(packet);
String msg = new String(packet.getData(),
packet.getOffset(),
packet.getLength());
System.out.println("[Multicast Receiver] Received:" + msg);
}
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (mcSocket != null) {
try {
mcSocket.leaveGroup(mcIPAddress);
mcSocket.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
清单 5-8 包含了一个向同一个组播地址发送消息的程序。请注意,您可以运行UDPMulticastReceiver类的多个实例,所有这些实例都将成为同一个多播组的成员。当您运行UDPMulticastSender类时,它将向组发送一条消息,组中的所有成员都将收到同一条消息的副本。UDPMulticastSender类使用DatagramSocket,而不是MulticastSocket来发送多播消息。
清单 5-8。UDP 数据报套接字、多播发送器应用
// UDPMultiCastSender.java
package com.jdojo.net;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPMultiCastSender {
public static void main(String[] args) {
int mcPort = 18777;
String mcIPStr = "230.1.1.1";
DatagramSocket udpSocket = null;
try {
// Create a datagram socket
udpSocket = new DatagramSocket();
// Prepare a message
InetAddress mcIPAddress = InetAddress.getByName(mcIPStr);
byte[] msg = "Hello multicast socket".getBytes();
DatagramPacket packet =
new DatagramPacket(msg, msg.length);
packet.setAddress(mcIPAddress);
packet.setPort(mcPort);
udpSocket.send(packet);
System.out.println("Sent a multicast message.");
System.out.println("Exiting application");
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (udpSocket != null) {
try {
udpSocket.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
URI、URL 和 URN
统一资源标识符(URI)是标识资源的字符序列。征求意见稿(RFC) 3986 定义了 URI 的通用语法。此 RFC 的全文可在 http://www.ietf.org/rfc/rfc3986.txt 获得。资源标识符可以通过位置、名称或两者来标识资源。本节概述了 URI。如果您对 URI 的细节感兴趣,建议您阅读 RFC3986。
使用位置来标识资源的 URI 称为统一资源定位器(URL)。例如, http://www.yahoo.com/index.html 表示在主机 www.yahoo.com 上标识名为index.html的文档的 URL。URL 的另一个例子是mailto: ksharan@jdojo.com,其中mailto协议指示解释它的应用打开电子邮件应用,向 URL 中指定的电子邮件地址发送电子邮件。在这种情况下,URL 没有定位任何资源。相反,它是识别电子邮件的细节。您还可以使用mailto协议设置电子邮件的主题和正文部分。因此,URL 并不总是意味着资源的位置。有时资源可能是抽象的,如mailto协议的情况。使用 URL 找到资源后,可以对资源执行一些操作,如检索、更新或删除。如何执行操作的细节取决于 URL 中使用的方案。URL 只是标识资源位置和定位它的方案的一部分,而不是可以在资源上执行的任何操作的细节。
使用名称来标识资源的 URI 称为统一资源名(URN)。例如,URN:ISBN:978-1-4302-6661-7表示一个 URN,它使用国际标准书号(ISBN)名称空间来标识一本书。
URL 和 URN 是 URI 的子集。因此,关于 URI 的讨论同时适用于 URL 和 URN。URI 的详细语法取决于它使用的方案。在这一节中,我将介绍 URI 的一般语法,它通常是一个 URL。下一节将探讨在 Java 程序中用来表示 URIs 和 URL 的 Java 类。
URI 可以是绝对的,也可以是相对的。一个相对的 URI 总是在另一个绝对的 URI 的背景下被解释,后者被称为基本的 URI。换句话说,你必须有一个绝对的 URI,才能使相对的 URI 有意义。
绝对 URI 具有以下通用格式:
<scheme>:<scheme-specific-part>
<scheme-specific-part>依赖于<scheme>。例如,http方案使用一种格式,而mailto方案使用另一种格式。URI 的另一种通用形式如下。通常,但不是必须的,它代表一个 URL。
<scheme>://<authority><path>?<query>#<fragment>
这里,<scheme>表示访问资源的方法。是协议名称,如http、ftp等。在 URI 规范中,我们都使用术语“协议”来表示“方案”。如果术语“scheme”使您迷惑,那么无论它何时出现在本节中,您都可以将其理解为“protocol”。URI 需要<scheme>和<path>零件。所有其他部分都是可选的。<path>部分可以是空字符串。
<authority>部分表示服务器名(或 IP 地址)或特定于方案的注册表。如果<authority>部分代表一个服务器名,它可以写成<userinfo>@host:port的形式。如果 URI 中有一个<authority>,它以两个正斜杠开始;这是一个可选部分。例如,一个标识机器上本地文件系统中的一个文件的 URL 使用file方案作为file:///c:/documents/welcome.doc。
URI 语法在它的<path>部分使用了分层语法,它在服务器上定位资源。<path>的多个部分由正斜杠(/)分隔。
<query>部分表示通过执行指定的查询获得资源。它由用“与”符号(&)分隔的名称-值对组成。名称和值由等号(=)分隔。例如,id=123&rate=5.5是一个查询,它有两个部分,id和rate。id的值是123,而rate的值是5.5。
<fragment>部分标识次要资源,通常是由 URI 的另一部分标识的主要资源的子集。
下面是一个 URI 的例子,它也被分成几个部分:
URI: http://www.jdojo.com/java/intro.html?id=123#conclusion
Scheme: http
Authority:www.jdojo.com
Path: /java/intro.html
Query: id=123
Fragment: conclusion
URI 代表一个 URL,该 URL 指向在 www.jdojo.com 服务器上名为intro.html的文档。方案http表明可以使用http协议检索文档。查询id=123表明该文档是通过执行该查询获得的。片段部分conclusion可以由使用该文档的不同应用进行不同的解释。在 HTML 文档的情况下,片段部分被 web 浏览器解释为主文档的一部分。
并非 URI 的所有部分都是强制性的。哪些部分是强制性的,哪些部分是可选的,这取决于所使用的方案。使用 URI 来标识资源的目标之一是使其具有普遍的可读性。因此,有一组定义明确的字符可以用来表示 URI。URI 语法使用一些具有特殊含义的保留字符,它们只能用于 URI 的特定部分。在其他部分,需要对保留字符进行转义。使用百分号后跟十六进制格式的 ASCII 值对字符进行转义。例如,空格的 ASCII 值在十进制格式中是 32,在十六进制格式中是 20。如果您想在 URI 中使用空格字符,您必须使用%20,它是空格的转义形式。因为百分号被用作转义字符的一部分,所以您必须使用%25来表示 URI 中的%字符(25 是十进制数 37 的十六进制值。%的 ASCII 值是十进制的 37)。例如,如果要在查询中使用值 5.2%,以下是无效的 URI:
http://www.jdojo.com/details?rate=5.2%
要使其成为有效的 URI,您需要将百分号字符转义为%25,如下所示:
http://www.jdojo.com/details?rate=5.2%25
理解相对 URI 的用法很重要。一个相对的 URI 总是在一个绝对的 URI 的背景下被解释,后者被称为基础 URI。绝对的 URI 始于一个计划。一个亲戚 URI 继承了它的基地 URI 的一些部分。让我们考虑一个引用 HTML 文档的 URI,如下所示:
http://www.jdojo.com/java/intro.html
URI 所指的文件是intro.html。它的路径是/java/intro.html。假设名为brief_intro.html和detailed_intro.html的两个文档(物理上或逻辑上)与intro.html位于相同的路径层次结构中。以下是所有三个文档的绝对 URIs:
http://www.jdojo.com/java/intro.htmlhttp://www.jdojo.com/java/brief_intro.htmlhttp://www.jdojo.com/java/detailed_intro.html
如果您已经在intro.html上下文中,使用它们的名字而不是它们的绝对 URI 来引用另外两个文档会更容易。在intro.html环境中意味着什么?当您使用 http://www.jdojo.com/java/intro.html URI 来标识一个资源时,它有三个部分:一个方案(http)、一个服务器名( www.jdojo.com )和一个文档路径(/java/intro.html)。该路径表明该文档位于java路径层次结构下,而后者又位于路径层次结构的根。所有细节——方案、服务器名称、路径细节,不包括文档名称本身(intro.html)—构成了intro.html文档的上下文。如果您查看上面列出的其他两个文档的 URI,您会注意到关于它们的所有细节都与intro.html相同。换句话说,您可以声明其他两个文档的上下文与intro.html的相同。在这种情况下,以intro.html文档的绝对 URI 作为基本 URI,其他两个文档的相对 URIs 是它们的名称:brief_intro.html和detailed_intro.html。可以列举如下:
Base URI:http://www.jdojo.com/java/intro.htmlRelative URI: brief_intro.htmlRelative URI: detailed_intro.html
在该列表中,两个相对 URIs 从基本 URI 继承方案、服务器名称和路径层次结构。需要强调的是,一个相对的 URI 如果不指明它的基地 URI 是没有意义的。
当必须使用相对 URI 时,它必须被解析为其等价的绝对 URI。URI 规范规定了解决相对 URI 的规则。我将讨论一些最常用的相对 URIs 形式及其解决方案。有两个特殊字符用于定义 URI 的<path>部分。它们是一个点和两个点。点表示当前的路径层次。两个点表示路径层次中的上一级。您一定见过在文件系统中使用这两组字符来表示当前目录和父目录。您可以用同样的方式思考它们在 URI 中的含义,但是 URI 并不假定任何目录层次结构。在 URI 中,路径被认为是分层的,它根本不依赖于文件系统的分层结构。然而,在实践中,当您使用基于 web 的应用时,URL 通常被映射到一个文件系统层次结构。在 URI 的规范化形式中,点会被适当替换。比如s://sn/a/./b归一化为s://sn/a/b,s://sn/a/../b归一化为s://sn/b。非规范化和规范化形式指的是同一个 URL。规范化格式移除了多余的字符。只看两个 URIs,你不能说他们是否指的是同一个资源。在比较它们是否相等之前,必须对它们进行规范化。在比较过程中,方案、服务器名称和十六进制数字不区分大小写。以下是解决相对 URI 的一些规则:
- 如果一个 URI 以一个计划开始,它被认为是一个绝对的 URI。
- 如果相对 URI 以权威开始,它从其基础 URI 继承方案。
- 如果一个相对 URI 是一个空字符串,它与基 URI 相同。
- 如果相对 URI 只有片段部分,则解析后的 URI 将使用新片段。如果一个 URI 有一个片段,它将被相对 URI 的片段所取代。否则,将相对 URI 的片段添加到基本 URI 中。
- 相对 URI 的路径不是以正斜杠(
/)开始的。如果基本 URI 有路径,则删除基本 URI 中路径的最后一个组件,并附加相对 URI。注意,路径的最后一部分可能是一个空字符串,如http://www.abc.com/。 - 如果相对 URL 以路径开头,而路径又以正斜杠(
/)开头,则基本 URI 路径将被相对 URI 路径替换。
表 5-5 包含使用这些规则的例子。表中的示例符合 Java URI 和 URL 类中遵循的规则。Java 规则在某些情况下与 URI 规范中的规则略有不同。
表 5-5。
Examples of How a Relative URI is Resolved to an Absolute URI Using a Base URI
| 基本 URI | 相对 URI | 解析相对 URI | 相对 URI 的描述 | | --- | --- | --- | --- | | `h://sn/a/b/c` | `http://sn2/foo` | `h://sn2/foo` | 这绝对是 URI。 | | `h://sn/a/b/c` | `//sn2/h/k` | `h://sn2/h/k` | 它始于一个权威 | | `h://sn/a/b/c` | | `h://sn/a/b/c` | 它是一个空字符串。 | | `h://sn/a/b/c` | `#k` | `h://sn/a/b/c#k` | 它只包含一个片段。 | | `h://sn/a/b/c#a` | `#k` | `h://sn/a/b/c#k` | 它只包含一个片段。 | | `h://sn/a/b/` | `foo` | `h://sn/a/b/foo` | 路径不是以/开头。 | | `h://sn/a/b/c` | `foo` | `h://sn/a/b/foo` | 路径不是以/开头。 | | `h://sn/a/b/c?d=3` | `foo` | `h://sn/a/b/foo` | 路径不是以/开头。 | | `h://sn/` | `foo` | `h://sn/foo` | 路径不是以/开头。 | | `h://sn` | `foo` | `h://sn/foo` | 路径不是以/开头。 | | `h://sn/a/b/` | `/foo` | `h://sn/foo` | 路径以/开头。 | | `h://sn/a/b/c` | `/foo` | `h://sn/foo` | 路径以/开头。 | | `h://sn/a/b/c?d=3` | `/foo` | `h://sn/foo` | 路径以/开头。 | | `h://sn/` | `/foo` | `h://sn/foo` | 路径以/开头。 | | `h://sn` | `/foo` | `h://sn/foo` | 路径以/开头。 |Tip
您也可以使用主机名或 IP 地址作为 URI 中的授权机构。IPv4 可以使用点分十进制格式,如 http://192.168.10.178/docs/toc.html 。IPv6 必须用括号括起来,如http://[1283::8:800:200C:A43A]/docs/toc.html。
作为 Java 对象的 URI 和 URL
Java 将 URI 和 URL 表示为对象。它提供了以下四个类,可用于在 Java 程序中将 URI 和 URL 作为对象进行处理:
java.net.URIjava.net.URLjava.net.URLEncoderjava.net.URLDecoder
一个URI类的对象代表一个 URI。URL类的一个对象代表一个 URL。URLEncoder和URLDecoder是帮助编码和解码 URI 字符串的实用程序类。在下一节中,我将讨论用于检索由 URL 标识的资源的其他 Java 类。
URI类有许多构造函数。它们允许您传递 URI 各部分(模式、权限、路径、查询和片段)的可变组合。所有的构造函数都会抛出一个名为URISyntaxException的检查异常。他们抛出一个异常,因为你用来构造一个URI对象的字符串可能不符合 URI 规范。
// Create a URI object
URI baseURI = new URI("http://www.yahoo.com
// Create a URI with relative URI string and resolve it using baseURI
URI relativeURI = new URI("welcome.html");
URI resolvedRelativeURI = baseURI.resolve(relativeURI);
清单 5-9 展示了如何在 Java 程序中使用URI类。
清单 5-9。演示 java.net.URI 类用法的示例类
// URITest.java
package com.jdojo.net;
import java.net.URI;
import java.net.URISyntaxException;
public class URITest {
public static void main(String[] args) {
String baseURIStr = "http://www.jdojo.com/javaintro.html
"id=25&rate=5.5%25#foo";
String relativeURIStr = "../sports/welcome.html";
try {
URI baseURI = new URI(baseURIStr);
URI relativeURI = new URI(relativeURIStr);
// Resolve the relative URI with respect to the base URI
URI resolvedURI = baseURI.resolve(relativeURI);
printURIDetails(baseURI);
printURIDetails(relativeURI);
printURIDetails(resolvedURI);
}
catch (URISyntaxException e) {
e.printStackTrace();
}
}
public static void printURIDetails(URI uri) {
System.out.println("URI:" + uri);
System.out.println("Normalized:" + uri.normalize());
String parts = "[Scheme=" + uri.getScheme() +
", Authority=" + uri.getAuthority() +
", Path=" + uri.getPath() +
", Query:" + uri.getQuery() +
", Fragment:" + uri.getFragment() + "]";
System.out.println(parts);
System.out.println();
}
}
URI: http://www.jdojo.com/javaintro.html?id=25&rate=5.5%25#foo
Normalized: http://www.jdojo.com/javaintro.html?id=25&rate=5.5%25#foo
[Scheme=http, Authority=www.jdojo.com
URI:../sports/welcome.html
Normalized:../sports/welcome.html
[Scheme=null, Authority=null, Path=../sports/welcome.html, Query:null, Fragment:null]
URI:http://www.jdojo.com/../sports/welcome.html
Normalized:http://www.jdojo.com/../sports/welcome.html
[Scheme=http, Authority=www.jdojo.com
您也可以使用toURL()方法从URI对象获取URL对象,如下所示:
URL baseURL = baseURI.toURL();
您还可以使用 URI 类的create(String str) static方法创建一个URI对象。create()方法不会抛出一个检查过的异常。它抛出一个运行时异常。因此,它的使用不会强制您处理异常。只有当您知道 URI 字符串是格式良好的时,才应该使用此方法。
URI uri2 = URI.create("http://www.yahoo.com
java.net.URL类的一个实例代表 Java 程序中的一个 URL。虽然每个 URL 也是一个 URI,但是 Java 并没有从URI类继承URL类。Java 使用术语协议来指代 URI 规范中的方案部分。您可以通过提供一个将 URL 的所有部分连接在一起的字符串,或者通过单独提供 URL 的各个部分来创建 URL 对象。如果您为创建 URL 对象提供的字符串无效,URL 类的构造函数将抛出一个MalformedURLException checked 异常。当你创建一个URL对象时,你必须处理这个异常。
清单 5-10 展示了如何创建一个URL对象。URL类允许您使用它的一个构造函数从一个相对 URL 和一个基本 URL 创建一个绝对 URL。
清单 5-10。演示 java.net.URL 类用法的示例类
// URLTest.java
package com.jdojo.net;
import java.net.URL;
public class URLTest {
public static void main(String[] args) {
String baseURLStr = "http://www.ietf.org/rfc/rfc3986.txt
String relativeURLStr = "rfc2732.txt";
try {
URL baseURL = new URL (baseURLStr);
URL resolvedRelativeURL = new URL(baseURL, relativeURLStr);
System.out.println("Base URL:" + baseURL);
System.out.println("Relative URL String:" + relativeURLStr);
System.out.println("Resolved Relative URL:" + resolvedRelativeURL);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Base URL:http://www.ietf.org/rfc/rfc3986.txt
Relative URL String:rfc2732.txt
Resolved Relative URL:http://www.ietf.org/rfc/rfc2732.txt
通常,创建一个URL对象来检索由 URL 标识的资源。注意,只要 URL 的文本格式良好,并且处理 URL 的协议可用,就可以创建一个URL类的对象。在 Java 程序中成功创建一个URL对象并不保证在 URL 中指定的服务器上存在该资源。URL类提供了一些方法,您可以结合其他类使用这些方法来检索由 URL 标识的资源。
URL类确保它能够处理 URL 字符串中指定的协议。例如,它不会让你创建一个带有字符串ppp:// www.sss.com/ 的URL对象,除非你为它开发并提供一个ppp协议的协议处理程序。我将在下一节详细讨论如何检索由 URL 标识的资源。
有时,您事先并不知道 URL 字符串的各个部分。您在运行时从程序的其他部分或用户那里获得 URL 的各个部分作为输入。在这种情况下,您需要对 URL 的各个部分进行编码,然后才能使用它们来创建一个URL对象。有时你得到一个编码形式的字符串,你希望它被解码。编码后的字符串将所有受限字符正确转义。
URLEncoder和URLDecoder类分别用于编码和解码字符串。URLEncoder.encode(String source, String encoding) static方法用于使用指定的编码对源字符串进行编码。URLDecoder.decode(String source, String encoding) static方法用于使用指定的编码对源字符串进行解码。以下代码片段显示了如何编码/解码字符串。通常,在 URL 的查询部分对名称-值对的值部分进行编码/解码。请注意,不要试图对整个 URL 字符串进行编码。否则,它将对一些保留字符(如正斜杠)进行编码,结果 URL 字符串将无效。
String source = "this is a test for 2.5% and &" ;
String encoded = URLEncoder.encode(source, "utf-8");
String decoded = URLDecoder.decode(encoded, "utf-8");
System.out.println("Source: " + source);
System.out.println("Encoded: " + encoded);
System.out.println("Decoded: " + decoded);
Source: this is a test for 2.5% and &
Encoded: this+is+a+test+for+2.5%25+and+%26
Decoded: this is a test for 2.5% and &
访问 URL 的内容
URL 有一个协议,用于与托管 URL 内容的远程应用进行通信。例如,URL http://www.yahoo.com/index.html 使用的是http协议。在 URL 中,您可以指定协议套件中应用层使用的协议。当您需要访问 URL 的内容时,计算机将使用协议组中较低层的某种协议(传输层、互联网层等)。)与远程主机通信。http应用层协议在较低层使用TCP/IP协议。在分布式应用中,经常需要检索(或读取)由 URL 标识的资源(可以是文本、html 内容、图像文件、音频/视频文件或任何其他类型的信息)。虽然每次需要读取 URL 的内容时都可以打开一个 socket,但是对于程序员来说,这是非常耗时和繁琐的。毕竟,程序员需要一些方法来提高效率,而不是为看似例行公事的工作编写重复的代码。Java 设计者意识到了这种需求,他们提供了一种非常简单(是的,非常简单)的方法来从/向 URL 读取/写入数据。本节将探索从非常简单到非常复杂的从/向 URL 读取/写入数据的一些方法。
当数据在协议组中从一层传递到另一层时,每一层都会向数据添加一个报头。由于 URL 使用应用层的协议,所以它也包含自己的头。报头的格式取决于所使用的协议。当http请求被发送到远程主机时,源主机中的应用层向数据添加http报头。远程主机有一个处理http协议的应用层,它使用报头信息来解释内容。总之,URL 数据将有两个部分:标题部分和内容部分。URL类和其他一些类可以让你读/写 URL 的头部和内容部分。我将从读取 URL 内容这个最简单的例子开始。
在读取/写入 URL 之前,您需要有一个可以访问的工作 URL。您可以阅读互联网上公开的任何 URL 的内容。对于这个讨论,我将假设您熟悉 Java Server Pages (JSP ),并且您可以访问一个 web 服务器来部署 JSP 页面。如果您不了解 JSP,您可以将本节示例中使用的 URL 替换为任何公开可用的 URL;例如,URL http://www.yahoo.com 就可以了,您应该能够运行所有的示例。将数据写入 URL 略有不同。如果你能运行你的 JSP 来看看如何写一个 URL 会更容易。我假设您已经在 web 服务器上部署了一个 web 应用,它有一个名为echo_params.jsp的网页。
清单 5-11 显示了这个 JSP 页面的内容。它执行两件事。它读取HTTP请求方法,可以是 GET 或 POST,并打印它。它读取通过HTTP请求传入的所有参数,并打印参数名称和值的列表。
清单 5-11。echo_params.jsp 文件的内容
<%@ page contentType="text/html;charset=windows-1252"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=windows-1252"/>
<title>Echo Request Method and Parameters</title>
</head>
<body>
<h1>URL Connection Test</h1>
<%
out.println("Request Method: " + request.getMethod());
out.println("<br/><br/>");
out.println("<u>List of Parameter Names and Values</u><br/>");
java.util.Enumeration paramNames =
request.getParameterNames();
while(paramNames.hasMoreElements()) {
String paramName = (String)paramNames.nextElement();
String paramValue = request.getParameter(paramName);
out.println("Name: " + paramName + ", Value: " + paramValue);
out.println("<br/>");
}
%>
</body>
</html>
通过编写如下所示的两行代码,URL类允许您读取 URL 的内容(不是标题):
URL url = new URL("your URL string goes here");
InputStream ins = url.openStream();
清单 5-12 给出了读取 URL 内容的完整程序。您需要根据您的 web 服务器设置更改该程序中的 URL。输出显示您确实访问了 JSP,JSP 获得传递给它的查询(id=123)并传回生成的 HTML 内容。HTML 请求是使用 GET 方法发送的。如果您想使用 POST 方法向一个 URL 发送请求,您将需要使用URLConnection类,我将在下面讨论它。为了更好的可读性,我对输出进行了格式化。
清单 5-12。一个简单的 URL 内容阅读器程序
// SimpleURLContentReader.java
package com.jdojo.net;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
public class SimpleURLContentReader {
public static String getURLContent(String urlStr) {
BufferedReader br = null;
try {
URL url = new URL(urlStr);
// Get the input stream
InputStream ins = url.openStream();
// Wrap input stream into a reader
br = new BufferedReader(new InputStreamReader(ins));
StringBuilder sb = new StringBuilder();
String msg = null;
while ((msg = br.readLine()) != null) {
sb.append(msg);
sb.append("\n"); // Append a new line
}
return sb.toString();
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (br != null) {
try {
br.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
// If we get here it means there was an error
return null;
}
public static void main(String[] args) {
String urlStr = "``http://localhost:8080/docsapp/
"echo_params.jsp?id=123";
String content = getURLContent(urlStr);
System.out.println(content);
}
}
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=windows-1252"/>
<title>Echo Request Method and Parameters</title>
</head>
<body>
<h1>URL Connection Test</h1>
Request Method: GET
<br/><br/>
<u>List of Parameter Names and Values</u><br/>
Name: id, Value: 123
<br/>
</body>
</html>
一旦获得输入流,就可以用它来读取 URL 的内容。读取 URL 内容的另一种方式是使用URL类的方法。因为getContent()可以返回任何类型的内容,所以它的返回类型是Object类型。在使用对象的内容之前,您需要检查它返回哪种对象。例如,它可能返回一个InputStream对象,在这种情况下,您将需要从输入流中读取数据。以下是两种版本的getContent()方法:
final Object getContent() throws IOExceptionfinal Object getContent(Class[] classes) throws IOException
方法的第二个版本允许您传递类类型的数组。它将尝试将内容对象转换为您按指定顺序传递给它的某个类。如果内容对象不匹配任何类型,它将返回null。您仍然需要编写if语句来知道从getContent()方法返回的是什么类型的对象,如下所示:
URL baseURL = new URL ("your url string goes here");
Class[] c = new Class[] {String.class, BufferedReader.class, InputStream.class};
Object content = baseURL.getContent(c);
if (content == null) {
// Contents are not of any of the three kinds
}
else if (content instanceof String) {
// You got a string
}
else if (content instanceof BufferedReader) {
// You got a reader
}
else if (content instanceof InputStream) {
// You got an input stream
}
如果您使用openStream()或getContent()方法读取 URL 的内容,那么URL类会处理许多在内部使用套接字的复杂性。这种方法的缺点是您无法控制连接设置。您不能使用这种方法将数据写入 URL。此外,您无权访问 URL 中使用的协议的标头信息。不要绝望;Java 提供了另一个名为URLConnection的类,让您以一种简单明了的方式完成所有这些工作。URLConnection是一个abstract类,你不能直接创建它的对象。您需要使用 URL 对象的openConnection()方法来获得一个URLConnection对象。URL类将处理URLConnection对象的创建,这将适合于处理 URL 中使用的协议的数据。下面的代码片段显示了如何使用一个URLConnection对象来读取和写入数据到一个 URL:
URL url = new URL("your URL string goes here");
// Get a connection object
URLConnection connection = url.openConnection();
// Indicate that you will be writing to the connection
connection.setDoOutput(true);
// Get output/input streams to write/read data
OutputStream ous = connection.getOutputStream();
InputStream ins = connection.getInputStream(); // Caution. Read below
URL类的方法返回一个URLConnection对象,它还没有连接到 URL 源。在连接之前,您必须为该对象设置所有与连接相关的参数。例如,如果您想将数据写入 URL,您必须在连接之前调用连接对象上的setDoOutput(true)方法。当你调用一个URLConnection对象的connect()方法时,它就会被连接起来。但是,当您调用需要连接的方法时,它是隐式连接的。例如,向 URL 写入数据并读取 URL 的数据或头字段将自动连接URLConnection对象,如果它还没有连接的话。
如果你想避免在使用URLConnection读写 URL 数据时出现问题,你必须遵循以下几点:
- 当您只从 URL 读取数据时,您可以使用它的
getInputStream()方法获得输入流。使用输入流读取数据。它将使用 GET 方法向远程主机发出请求。也就是说,如果要向 URL 传递一些参数,必须通过向 URL 添加查询部分来实现。 - 如果你在从一个 URL 中写入和读取数据,你必须在连接前调用
setDoOutput(true)。在开始读取数据之前,必须先将数据写入 URL。将数据写入 URL 会将请求方法更改为POST。在将数据写入 URL 之前,您甚至无法获得输入流。事实上,getInputStream()方法向远程主机发送一个请求。您的目的是将数据发送到远程主机,并从远程主机读取响应。这一次变得非常棘手。这里有更多的解释,使用一段代码,假设connection是一个URLConnection object:
// Incorrect – 1\. Get input and output streams
// you must get the output stream first
InputStream ins = connection.getInputStream();
OutputStream ous = connection.getOutputStream();
// Incorrect – 2\. Get output and input streams
// you must get the output stream and finish writing
// before you should get the input stream
OutputStream ous = connection.getOutputStream();
InputStream ins = connection.getInputStream();
// Correct. Get output stream and get done with it.
// And, then get the input stream and read data.
OutputStream ous = connection.getOutputStream();
// Write logic to write data using ous object here. Make sure
// you are done writing data before you call the
// getInputStream() method as shown below
InputStream ins = connection.getInputStream();
// Write logic to read data
- 使用
getInputStream()方法和使用任何方法(如getHeaderField(String headerName))读取标题字段具有相同的效果。URL 的服务器提供标题和内容。一个URLConnection必须发送请求才能得到它们。
清单 5-13 包含了向清单 5-11 中列出的echo_params.jsp页面写入/从中读取数据的完整代码。注意,您需要将echo_params.jsp页面部署到一个 web 服务器上,这个示例才能运行。为了更好的可读性,我对输出进行了格式化。
清单 5-13。向/从 URL 写入/读取数据的 URL 读取器/写入器类
// URLConnectionReaderWriter.java
package com.jdojo.net;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.Map;
public class URLConnectionReaderWriter {
public static String getURLContent(String urlStr, String input) {
BufferedReader br = null;
BufferedWriter bw = null;
try {
URL url = new URL(urlStr);
URLConnection connection = url.openConnection();
// Must call setDoOutput(true) to indicate that we
// will write to the connection. By default, it is fals
// By default, setDoInput() is set to true.
connection.setDoOutput(true);
// Now, connect to the remote object
connection.connect();
// Write data to the URL first before reading the response
OutputStream ous = connection.getOutputStream();
bw = new BufferedWriter(new OutputStreamWriter(ous));
bw.write(input);
bw.flush();
bw.close();
// Must be placed after writing the data. Otherwise,
// it will result in error, because if write is performed,
// read must be performed after the write
printRequestHeaders(connection);
InputStream ins = connection.getInputStream();
// Wrap the input stream into a reader
br = new BufferedReader(new InputStreamReader(ins));
StringBuffer sb = new StringBuffer();
String msg = null;
while ((msg = br.readLine()) != null) {
sb.append(msg);
sb.append("\n"); // Append a new line
}
return sb.toString();
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (br != null) {
try {
br.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
// If we arrive here it means there was an error
return null;
}
public static void printRequestHeaders(URLConnection connection) {
Map headers = connection.getHeaderFields();
System.out.println("Request Headers are:");
System.out.println(headers);
System.out.println();
}
public static void main(String[] args) {
// Change the URL to point to the echo_params.jsp page
// on your web server
String urlStr = "http://www.jdojo.com/docsapp/echo_params.jsp
String query = null;
try {
// Encode the query. We need to encode only the value
// of the name parameter. Other names and values are fine
query = "id=789&name=" +
URLEncoder.encode("John & Co.", "utf-8");
// Get the content and display it on the console
String content = getURLContent(urlStr, query);
System.out.println(content);
}
catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
Request Headers are:
{null=[HTTP/1.1 200 OK], Date=[Fri, 19 Dec 2008 02:15:14 GMT], Content-Length=[402], Set-Cookie=[JSESSIONID=567B1B9F853DD22DD73AB8452E220E0A; Path=/examples], Content-Type=[text/html;charset=windows-1252], Server=[Apache-Coyote/1.1]}
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=windows-1252"/>
<title>Echo Request Method and Parameters</title>
</head>
<body>
<h1>URL Connection Test</h1>
Request Method: POST
<br/><br/>
<u>List of Parameter Names and Values</u><br/>Name: name, Value: John & Co.
<br/>Name: id, Value: 789
<br/>
</body>
</html>
这一次,您将使用 POST 方法向 URL 发送数据。注意,您发送的数据已经使用URLEncoder类进行了编码。您只需要对 name 字段的值进行编码,即"John & Co.",因为值中的&符号将与查询字符串中的名称-值对分隔符冲突。如果你改变任何语句的顺序,程序有大量的注释来警告你任何危险。
该程序打印出一个java.util.Map对象中返回的所有标题的信息。该类提供了几种获取标头字段值的方法。对于常用的头,它提供了一个直接的方法。例如,名为getContentLength()、getContentType()和getContentEncoding()的方法返回头字段的值,分别表示 URL 内容的长度、类型和编码。如果您知道标题字段名或它的索引,您可以使用getHeaderField(String headerName)或getHeaderField(int headerIndex)方法来获取它的值。getHeaderFields()方法返回一个Map对象,它的键代表标题字段名称,值代表标题字段值。读取标题字段时要小心,因为它对URLConnection对象的影响与读取内容相同。如果您希望将数据写入 URL,您必须先写入数据,然后才能读取标题字段。
Java 允许您使用jar协议读取 JAR 文件的内容。假设您有一个名为myclasses.jar的 JAR 文件,其中有一个路径为myfolder/Abc.class的类文件。您可以从 URL 获取一个JarURLConnection,并使用它的方法来访问 JAR 文件数据。请注意,您只能从 URL 读取 JAR 文件内容。您不能写入 JAR 文件 URL。下面的代码片段显示了如何获得一个JarURLConnection对象。您将需要使用它的方法来获取 JAR 特定的数据。
String str = "jar:http://www.abc.com/myclasses.jar!/myfolder/Abc.class
URL url = new URL(str);
JarURLConnection connection = (JarURLConnection)url.openConnection();
// Use the connection object to access any jar related data.
Tip
在本节中,你已经读到了许多关于使用URLConnection对象的警告。还有一点:一个URLConnection对象只能用于一个请求。它基于“获得-使用-丢弃”的概念。如果您希望多次从一个 URL 写入或读取数据,您必须每次单独调用 URL 的openConnection()。
非阻塞套接字编程
在前面的章节中,我讨论了 TCP 和 UDP 套接字。Socket和ServerSocket类的connect()、accept()、read()和write()方法阻塞,直到操作完成。例如,如果客户端套接字的线程调用read()方法从服务器读取数据,直到数据可用,它就会被阻塞。如果您可以在客户端套接字上调用read()方法并开始做其他事情,直到来自服务器的数据到达,这不是很好吗?当从服务器获得数据时,将通知客户机套接字,客户机套接字将在适当的时间读取数据。套接字编程面临的另一个大问题是服务器应用的可伸缩性。在前面的小节中,我建议您需要创建一个新的线程来处理每个客户端连接,或者您将有一个线程池来处理所有的客户端连接。无论哪种方式,你都将在你的程序中创建和维护一堆线程。如果您不必在一个服务器程序中处理线程来处理多个客户端,那不是很好吗?非阻塞套接字通道提供了所有这些好的特性。一如既往,一个好的特性有一个与之相关的价格标签;非阻塞套接字通道也是如此。这需要一点学习过程。你习惯于编程,事情按顺序发生。有了非阻塞的套接字通道,你将需要改变在程序中执行事情的思维方式。改变心态需要时间。你的程序将会执行多项不会按顺序执行的任务。如果您是第一次学习 Java,您可以跳过这一节,以后当您在编写复杂的 Java 程序方面获得更多经验时再来看它。
假设您已经很好地理解了使用ServerSocket和Socket类的套接字编程。还假设您对使用缓冲区和通道的 Java 新输入/输出有基本的了解。本节使用了一些包含在java.nio、java.nio.channels和java.nio.charset包中的类。
让我们从比较阻塞和非阻塞套接字通信中涉及的类开始。表 5-6 列出了阻塞和非阻塞套接字应用中使用的主要类。
表 5-6。
Comparison of Classes Involved in Blocking and Non-Blocking Socket Programming
| 用于阻止基于套接字的通信的类 | 在基于非阻塞套接字的通信中使用的类 | | --- | --- | | `ServerSocket` | `ServerSocketChannel``ServerSocket`类依然存在于幕后。 | | `Socket` | `SocketChannel``Socket`类依然存在于幕后。 | | `InputStream` `OutputStream` | 不存在相应的类。`SocketChannel`用于读取/写入数据 | | 不存在相应的类。 | `Selector` | | 不存在相应的类。 | `SelectionKey` |您将使用一个ServerSocketChannel对象主要是为了在服务器中接受一个新的连接请求,而不是使用一个ServerSocket。ServerSocket并没有消失。它仍在幕后运作。如果需要内部使用的ServerSocket对象的引用,可以通过使用ServerSocketChannel对象的socket()方法来获取。你可以把一个ServerSocketChannel对象看作是一个ServerSocket对象的包装器。
您将使用一个SocketChannel来代替一个Socket在客户端和服务器之间进行通信。一个Socket物体仍在幕后发挥作用。您可以使用SocketChannel类的socket()方法来获取Socket对象的引用。你可以把一个SocketChannel对象看作是一个Socket对象的包装器。
在我开始讨论非阻塞套接字为您提供更高效、更可伸缩的应用接口所使用的机制之前,看一个真实的例子会有所帮助。让我们讨论一下在快餐店点餐和上菜的方式。假设餐馆在任何时候都期望最多十个顾客,最少零个顾客。一位顾客来到餐厅,下了订单,然后就可以享用食物了。那家餐馆应该雇用多少服务员?在最好的情况下,它可能只使用一台服务器来处理接收来自所有客户的订单并为他们提供食物。在最坏的情况下,它可能有十台服务器—一台服务器留给一个客户。在后一种情况下,如果餐厅只有三个客户,那么将有七个服务器处于空闲状态。
让我们在餐馆管理中走中间道路。让我们在厨房有几个服务器做饭,在柜台有一个服务器接收订单。一位顾客来了,向柜台的服务员下了订单,顾客得到一个订单 id,顾客离开柜台,柜台的服务员将订单传递给厨房的一个服务员,服务员开始接受下一位顾客的订单。此时,顾客可以在准备订单的同时自由地做其他事情。柜台的服务员正在接待其他顾客。厨房里的服务员正忙着根据订单准备食物。没有人在等谁。一旦订单中的食品准备好,柜台的服务员就会从厨房的服务员那里收到食品,并呼叫订单号,这样下订单的顾客就可以取走他的食品。顾客可以分多次购买食物。当厨房正在准备他点的菜的时候,他可以吃已经端给他的食物。这种建筑是餐馆中最高效的建筑。它让每个人大部分时间都很忙,并有效地利用了资源。这是非阻塞套接字通道遵循的方法。
另一种方法是,顾客进来,下订单,等到他的订单完成并有人为他服务,然后下一个顾客下订单,以此类推。这是阻塞套接字遵循的方法。如果您了解快餐店为有效利用资源所采取的方法,您就可以很容易地理解非阻塞套接字通道。在下面的讨论中,我将比较餐馆示例中使用的人和非阻塞套接字中使用的对象。
我们先来讨论一下服务器端的情况。服务器端是你的餐厅。在柜台与所有顾客打交道的人被称为“挑选者”。选择器是Selector类的一个对象。它唯一的工作就是与外界互动。它位于与服务器交互的远程客户端和服务器内部的事物之间。远程客户机从不与在服务器内部工作的对象交互,就像餐馆里的顾客从不与厨房里的服务器直接交互一样。图 5-7 显示了非阻塞套接字通道通信的架构。它显示了选择器在体系结构中的位置。
图 5-7。
Architecture of Non-Blocking Client-Server Sockets
不能使用选择器对象的构造函数直接创建选择器对象。您需要调用它的open()静态方法来获得一个选择器对象,如下所示:
// Get a selector object
Selector selector = Selector.open();
ServerSocketChannel用于监听来自客户端的新连接请求。同样,您不能使用其构造函数创建新的ServerSocketChannel对象。你需要调用它的open()静态方法如图所示:
// Get a server socket channel
ServerSocketChannel ssChannel = ServerSocketChannel.open();
默认情况下,服务器套接字通道或套接字通道是阻塞通道。您需要对其进行配置,使其成为非阻塞通道,如下所示:
// Configure the server socket channel to be non-blocking
ssChannel.configureBlocking(false);
您的服务器套接字通道需要绑定到一个本地 IP 地址和一个本地端口号,以便远程客户端可以联系它以获得新的连接。您使用它的bind()方法绑定一个服务器套接字通道。Java 7 中的ServerSocketChannel和SocketChannel中增加了bind()方法。在 Java 7 之前,您需要在与通道相关联的套接字上调用bind()方法。
InetAddress hostIPAddress = InetAddress.getByName("localhost");
int port = 19000;
// Prior to Java 7
ssChannel.socket().bind(new InetSocketAddress(hostIPAddress, port));
// Java 7 and later
ssChannel.bind(new InetSocketAddress(hostIPAddress, port));
现在迈出了最重要的一步。服务器套接字必须向选择器注册自己,以显示对某种操作的兴趣。这就像餐馆里的比萨饼师傅让柜台上的服务员知道他已经准备好为顾客做比萨饼了,他需要在下比萨饼的订单时得到通知。有四种操作可以在选择器中注册通道。它们被定义为表 5-7 中列出的SelectionKey类中的整数常量。
表 5-7。
The List of Operations Recognized by the Selector
| 操作类型 | 值(SelectionKey 类中的常数) | 谁可以注册这个操作 | 描述 | | --- | --- | --- | --- | | `Connect` | `OP_CONNECT` | `SocketChannel`在客户端 | 选择器将通知连接操作的进度。 | | `Accept` | `OP_ACCEPT` | 在服务器上 | 当客户端请求新连接时,选择器会发出通知 | | `Read` | `OP_READ` | `SocketChannel`在客户端和服务器端 | 当通道准备好读取某些数据时,选择器会发出通知。 | | `Write` | `OP_WRITE` | `SocketChannel`在客户端和服务器端 | 当通道准备好写入一些数据时,选择器将发出通知。 |ServerSocketChannel只监听接受新的客户端连接请求,因此,它只能注册一个操作,如下所示:
// Register the server socket channel with the selector for accept operation
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocketChannel的register()方法返回一个类型为SelectionKey的对象。您可以将这个对象视为带有选择器的注册证书。如果以后需要使用这个 key 对象,可以将它存储在一个变量中。该示例忽略了这一点。选择器有您的密钥(注册细节)的副本,并且它将在将来使用它来通知您您的通道已经准备好的任何操作。
此时,您的选择器已经准备好拦截客户机连接的传入请求,并将其传递给服务器套接字通道。假设此时有一个客户端试图连接到服务器套接字通道。选择器和服务器套接字通道之间的交互是如何发生的?当选择器检测到它有一个已注册的键,并准备好进行操作时,它将该键(SelectionKey类的一个对象)放在一个称为就绪集的单独组中。一个java.util.Set对象代表一个就绪集合。您可以通过调用一个Selector对象的select()方法来确定处于就绪状态的键的数量。
// Get the key count in the ready set
int readyCount = selector.select();
一旦您在就绪集合中获得至少一个就绪密钥,您需要获得该密钥并查看详细信息,您可以从就绪集合中获得所有就绪密钥,如下所示:
// Get the set of ready keys
Set readySet = selector.selectedKeys();
请注意,您为一个或多个操作注册了一个密钥。您需要查看特定操作的关键细节。如果一个键准备好接受新的连接请求,它的isAcceptable()方法将返回true。如果一个键准备好进行连接操作,它的isConnectable()方法将返回true。如果一个键准备好进行读写操作,它的isReadable()和isWritable()方法将返回true。您可能会发现有一种方法可以检查每种操作类型的准备情况。当您处理就绪集合时,您还需要从就绪集合中移除密钥。下面是在服务器应用中处理就绪集的一些典型代码。无限循环在服务器应用中很典型,因为一旦完成了当前的就绪集,就需要继续寻找下一个就绪集。
while(true) {
// Get count of keys in the ready set. If ready key count is
// greater than zero, process each key in the ready set.
}
以下代码片段显示了可用于处理就绪集中所有键的典型逻辑:
SelectionKey key = null;
Iterator iterator = readySet.iterator();
while (iterator.hasNext()) {
// Get the next ready selection key object
key = (SelectionKey)iterator.next();
// Remove the key from ready set
iterator.remove();
// Process the key according to the operation
if (key.isAcceptable()) {
// Process new connection
}
if (key.isReadable()) {
// Read from the channel
}
if (key.isWritable()) {
// Write to the channel
}
}
如何在服务器套接字通道上接受来自远程客户端的连接请求?逻辑类似于使用ServerSocket对象接受远程连接请求。一个SelectionKey对象引用了注册它的ServerSocketChannel。您可以使用channel()方法访问SelectionKey对象的ServerSocketChannel对象。您需要调用ServerSocketChannel对象上的accept()方法来接受新的连接请求。accept()方法返回SocketChannel类的一个对象,用于与远程客户端通信(读写)。您需要将新的SocketChannel对象配置为非阻塞套接字通道。您需要理解的最重要的一点是,新的SocketChannel对象必须向选择器注册自己的读、写或两种操作,以开始在连接通道上读/写数据。以下代码片段显示了接受远程连接请求的逻辑:
ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
SocketChannel sChannel = (SocketChannel)ssChannel.accept();
sChannel.configureBlocking(false);
// Register only for read. Your message is small and you write it back
// to the client as soon as you read it.
sChannel.register(key.selector(), SelectionKey.OP_READ);
如果您希望向选择器注册套接字通道以进行读取和写入,您可以如下所示进行操作:
// Register for read and write
sChannel.register(key.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE);
一旦您的套接字通道向选择器注册,当它从远程客户端接收到任何数据时,或者当您可以在其通道上向远程客户端写入数据时,它将通过选择器的就绪集得到通知。
如果数据在套接字通道上变得可用,key.isReadable()将为该套接字通道返回true。典型的读取操作如下所示。要使用通道和缓冲区读取数据,您必须对 Java NIO(新输入/输出)有基本的了解。
SocketChannel sChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesCount = sChannel.read(buffer);
String msg = "";
if (bytesCount > 0) {
buffer.flip();
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
msg = charBuffer.toString();
System.out.println("Received Message: " + msg);
}
如果您可以写入通道,选择器会将相关的键放入其就绪集合中,其isWritable()方法将返回true。同样,您需要理解 Java NIO 来使用ByteBuffer对象在通道上写数据。
SocketChannel sChannel = (SocketChannel)key.channel();
String msg = "message to be sent to remote client goes here";
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
sChannel.write(buffer);
客户端发生的事情很容易理解。首先获得一个选择器对象,然后通过调用SocketChannel.open()方法获得一个SocketChannel对象。此时,您需要在连接到服务器之前将套接字通道配置为非阻塞的。现在,您已经准备好向选择器注册您的套接字通道了。通常,您向选择器注册连接、读取和写入操作。处理选择器就绪集的方式与在服务器应用中处理选择器就绪集的方式相同。读取和写入通道的代码类似于服务器端代码。以下代码片段显示了客户端应用中使用的典型逻辑:
InetAddress serverIPAddress = InetAddress.getByName("localhost");
int port = 19000;
InetSocketAddress serverAddress = new InetSocketAddress(serverIPAddress, port);
// Get a selector
Selector selector = Selector.open();
// Create and configure a client socket channel
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
// Connect to the server
channel.connect(serverAddress);
// Register the channel for connect, read and write operations
int operations = SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE;
channel.register(selector, operations);
// Process the ready set of the selector here
当您在客户端SocketChannel获得一个连接操作时,这可能意味着连接成功或失败。您可以在SocketChannel对象上调用finishConnect()方法来完成连接过程。如果连接失败,finishConnect()调用将抛出一个IOException。通常,您可以按如下方式处理连接操作:
if (key.isConnectable()) {
try {
// Call to finishConnect() is in a loop as it is non-blocking
// for your channel
while(channel.isConnectionPending()) {
channel.finishConnect();
}
}
catch (IOException e) {
// Cancel the channel's registration with the selector
key.cancel();
e.printStackTrace();
}
}
是时候使用这些通道构建 echo 客户端应用和 echo 服务器应用了。清单 5-14 和清单 5-15 分别包含了 echo 服务器和 echo 客户端的非阻塞套接字通道的完整代码。
清单 5-14。一个非阻塞套接字通道回显服务器程序
// NonBlockingEchoServer.java
package com.jdojo.net;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;
import java.util.Set;
public class NonBlockingEchoServer {
public static void main(String[] args) throws Exception {
InetAddress hostIPAddress = InetAddress.getByName("localhost");
int port = 19000;
// Get a selector
Selector selector = Selector.open();
// Get a server socket channel
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// Make the server socket channel non-blocking and bind it to an address
ssChannel.configureBlocking(false);
ssChannel.socket().bind(new InetSocketAddress(hostIPAddress, port));
// Register a socket server channel with the selector for accept operation,
// so that it can be notified when a new connection request arrives
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
// Now we will keep waiting in a loop for any kind of request
// that arrives to the server - connection, read, or write
// request. If a connection request comes in, we will accept
// the request and register a new socket channel with the selector
// for read and write operations. If read or write requests come
// in, we will forward that request to the registered channel.
while (true) {
if (selector.select() <= 0) {
continue;
}
processReadySet(selector.selectedKeys());
}
}
public static void processReadySet(Set readySet) throws Exception {
SelectionKey key = null;
Iterator iterator = null;
iterator = readySet.iterator();
while (iterator.hasNext()) {
// Get the next ready selection key object
key = (SelectionKey) iterator.next();
// Remove the key from the ready key set
iterator.remove();
// Process the key according to the operation it is ready for
if (key.isAcceptable()) {
processAccept(key);
}
if (key.isReadable()) {
String msg = processRead(key);
if (msg.length() > 0) {
echoMsg(key, msg);
}
}
}
}
public static void processAccept(SelectionKey key) throws IOException {
// This method call indicates that we got a new connection
// request. Accept the connection request and register the new
// socket channel with the selector, so that client can
// communicate on a new channel
ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
SocketChannel sChannel = (SocketChannel) ssChannel.accept();
sChannel.configureBlocking(false);
// Register only for read. Our message is small and we write it
// back to the client as soon as we read it
sChannel.register(key.selector(), SelectionKey.OP_READ);
}
public static String processRead(SelectionKey key) throws Exception {
SocketChannel sChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesCount = sChannel.read(buffer);
String msg = "";
if (bytesCount > 0) {
buffer.flip();
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
msg = charBuffer.toString();
System.out.println("Received Message: " + msg);
}
return msg;
}
public static void echoMsg(SelectionKey key, String msg) throws IOException {
SocketChannel sChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
sChannel.write(buffer);
}
}
清单 5-15。一个非阻塞套接字通道回显客户端程序
// NonBlockingEchoClient.java
package com.jdojo.net;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;
import java.util.Set;
public class NonBlockingEchoClient {
private static BufferedReader userInputReader = null;
public static void main(String[] args) throws Exception {
InetAddress serverIPAddress = InetAddress.getByName("localhost");
int port = 19000;
InetSocketAddress serverAddress = new InetSocketAddress(serverIPAddress, port);
// Get a selector
Selector selector = Selector.open();
// Create and configure a client socket channel
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(serverAddress);
// Register the channel for connect, read and write operations
int operations =
SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE;
channel.register(selector, operations);
userInputReader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
if (selector.select() > 0) {
boolean doneStatus = processReadySet(selector.selectedKeys());
if (doneStatus) {
break;
}
}
}
channel.close();
}
public static boolean processReadySet(Set readySet) throws Exception {
SelectionKey key = null;
Iterator iterator = null;
iterator = readySet.iterator();
while (iterator.hasNext()) {
// Get the next ready selection key object
key = (SelectionKey) iterator.next();
// Remove the key from the ready key set
iterator.remove();
if (key.isConnectable()) {
boolean connected = processConnect(key);
if (!connected) {
return true; // Exit
}
}
if (key.isReadable()) {
String msg = processRead(key);
System.out.println("[Server]: " + msg);
}
if (key.isWritable()) {
String msg = getUserInput();
if (msg.equalsIgnoreCase("bye")) {
return true; // Exit
}
processWrite(key, msg);
}
}
return false; // Not done yet
}
public static boolean processConnect(SelectionKey key) {
SocketChannel channel = (SocketChannel) key.channel();
try {
// Call the finishConnect() in a loop as it is non-blocking
// for your channel
while (channel.isConnectionPending()) {
channel.finishConnect();
}
}
catch (IOException e) {
// Cancel the channel's registration with the selector
key.cancel();
e.printStackTrace();
return false;
}
return true;
}
public static String processRead(SelectionKey key) throws Exception {
SocketChannel sChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
sChannel.read(buffer);
buffer.flip();
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
String msg = charBuffer.toString();
return msg;
}
public static void processWrite(SelectionKey key, String msg) throws IOException {
SocketChannel sChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
sChannel.write(buffer);
}
public static String getUserInput() throws IOException {
String promptMsg = "Please enter a message(Bye to quit): ";
System.out.print(promptMsg);
String userMsg = userInputReader.readLine();
return userMsg;
}
}
您需要首先运行NonBlockingEchoServer类,然后运行NonBlockingEchoClient类的一个或多个实例。它们的工作方式类似于另外两个 echo 客户机-服务器程序。请注意,这一次,在客户端应用中输入消息后,您可能看不到来自服务器的消息。客户端应用向服务器发送消息,并且不等待消息被回显。相反,当套接字通道收到来自选择器的通知时,它会处理服务器消息。因此,有可能同时从服务器获得两条消息的回显。为了保持代码的简单性和可读性,这些例子中省略了异常处理。
套接字安全权限
您可以使用java.net.SocketPermission类的实例来控制 Java 程序使用套接字的访问。用于在 Java 策略文件中授予套接字权限的通用格式如下:
grant {
permission java.net.SocketPermission "target", "actions";
};
目标的形式是<host name>:<port range>。动作的可能值是accept、connect、listen和resolve。
只有当“localhost”用作主机名时,listen动作才有意义。resolve动作指的是 DNS 查找,如果其他三个动作中的任何一个存在,它就是隐含的。
主机名可以是 DNS 名称或 IP 地址。您可以在 DNS 主机名中使用星号(*)作为通配符。如果使用星号,它必须用作 DNS 名称中最左边的字符。如果主机名只包含一个星号,则表示任何主机。主机名的“localhost”指的是本地机器。如下所述,您可以用不同的格式指定主机名的端口范围。这里,N1 和 N2 表示端口号(0 到 65535),并假设 N1 小于 N2。表 5-8 列出了用于指示端口范围的格式。
表 5-8。
The Format for java.net.SocketPermission Security Settings
| 端口范围值 | 描述 | | --- | --- | | `N1` | 只有一个端口号——N1 | | `N1-N2` | 从 N1 到 N2 的端口号 | | `N1-` | N1 和更大地区的端口号 | | `-N1` | N1 及以下国家的端口号 |以下是在 Java 策略文件中使用java.net.SocketPermission的例子:
// Grant to all codebase
grant {
// Permission to connect with 192.168.10.123 at port 5000
permission java.net.SocketPermission "192.168.10.123:5000", "connect";
// Connect permission to any host at port 80
permission java.net.SocketPermission "*:80", "connect";
// All socket permissions to on port >=1024 on the localhost
permission java.net.SocketPermission "localhost:1024-", "listen, accept, connect";
};
异步套接字通道
Java 7 增加了对异步套接字操作的支持,比如连接、读取和写入。异步套接字操作使用以下两个套接字通道类来执行:
java.nio.channels.AsynchronousServerSocketChanneljava.nio.channels.AsynchronousSocketChannel
一个AsynchronousServerSocketChannel作为一个服务器套接字来监听新的客户端连接。一旦它接受了一个新的客户机连接,客户机和服务器之间的交互就由两端的一个AsynchronousSocketChannel处理。异步套接字通道的设置与同步套接字非常相似。这两种设置的主要区别在于,异步套接字操作的请求会立即返回,并在操作完成时通知请求者,而在同步套接字操作中,套接字操作的请求会一直阻塞,直到操作完成。由于异步套接字通道操作的异步特性,处理套接字操作完成或失败的代码有点复杂。
在异步套接字通道中,使用异步套接字通道类的方法之一请求操作。该方法立即返回。稍后您会收到操作完成或失败的通知。允许您请求异步操作的方法被重载。一个版本返回一个Future对象,让您检查所请求操作的状态。有关使用Future对象的详细信息,请参考《Java 语言特性入门》( ISBN: 978-1-4302-6658-7)一书中的第六章。这些方法的另一个版本让你通过一个CompletionHandler。当请求的操作成功完成时,调用CompletionHandler的completed()方法。当请求的操作失败时,调用CompletionHandler的failed()方法。下面的代码片段演示了处理请求的异步套接字操作的完成/失败的两种方法。它展示了服务器套接字通道如何异步接受客户端连接。
/* Using a Future Object */
// Get a server socket channel instance
AsynchronousServerSocketChannel server = get a server instance...;
// Bind the socket to a host and a port
server.bind(your_host, your_port);
// Start accepting a new client connection. Note that the accept()
// method returns immediately by returning a Future object
Future<AsynchronousSocketChannel> result = server.accept();
// Wait for the new client connection by calling the get() method of
// the Future object. Alternatively, you can poll the Future object
// periodically using its isDone() method
AsynchronousSocketChannel newClient = result.get();
// Handle the newClient here and call the server.accept() again to accept
// another client connection
/* Using a CompletionHandler Object */
// Get a server socket channel instance
AsynchronousServerSocketChannel server = get a server instance...;
// Bind the socket to a host and a port
server.bind(your_host, your_port);
// Start accepting a new client connection. The accept() method returns
// immediately. The completed() or failed() method of the ConnectionHandler
// will be called upon completion or failure of the requested operation
YourAnyClass attach = ...; // Get an attachment
server.accept(attach, new ConnectionHandler());
上面版本的accept()方法接受任何类的对象作为附件。它可能是一个null参考。附件被传递给完成处理程序的completed()和failed()方法,在本例中完成处理程序是ConnectionHandler的一个对象。ConnectionHandler类可能如下所示。
private static class ConnectionHandler implements CompletionHandler<AsynchronousSocketChannel, YourAnyClass> {
@Override
public void completed(AsynchronousSocketChannel client, YourAnyClass attach) {
// Handle the new client connection here and again start
// accepting a new client connection
}
@Override
public void failed(Throwable e, YourAnyClass attach) {
// Handle the failure here
}
}
在这一节中,我将详细介绍以下三个步骤。在讨论过程中,我将构建一个由 echo 服务器和客户机组成的应用。客户端将向服务器异步发送消息,服务器将消息异步回显给客户端。假设您熟悉使用缓冲区和通道。
- 设置异步服务器套接字通道
- 设置异步客户端套接字通道
- 将异步服务器和客户端套接字通道投入使用
设置异步服务器套接字通道
AsynchronousServerSocketChannel类的一个实例被用作异步服务器套接字通道来监听新的客户端连接。一旦建立了到客户机的连接,就使用AsynchronousSocketChannel类的一个实例与客户机通信。AsynchronousServerSocketChannel类的静态open()方法返回一个AsynchronousServerSocketChannel类的对象,该对象尚未绑定。
// Create an asynchronous server socket channel object
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
// Bind the server to the localhost and the port 8989
String host = "localhost";
int port = 8989;
InetSocketAddress sAddr = new InetSocketAddress(host, port);
server.bind(sAddr);
此时,您的服务器套接字通道可以通过调用它的accept()方法来接受新的客户端连接,如下所示。代码使用了两个类,Attachment和ConnectionHandler,这将在后面描述。
// Prepare the attachment
Attachment attach = new Attachment();
attach.server = server;
// Accept new connections
server.accept(attach, new ConnectionHandler());
通常,服务器应用会无限期运行。您可以通过在main()方法中等待主线程来使服务器应用永远运行,如下所示:
try {
// Wait indefinitely until someone interrups the main thread
Thread.currentThread().join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
您将使用完成处理程序机制来处理服务器套接字通道的完成/失败通知。下面这个类的对象将被用来作为完成处理程序的附件。附件对象用于传递服务器套接字的上下文,该服务器套接字可能在完成处理程序的completed()和failed()方法中使用。
class Attachment {
AsynchronousServerSocketChannel server;
AsynchronousSocketChannel client;
ByteBuffer buffer;
SocketAddress clientAddr;
boolean isRead;
}
您需要一个实现来处理一个accept()调用的完成。让我们称您的类为ConnectionHandler,如图所示:
private static class ConnectionHandler implements CompletionHandler<AsynchronousSocketChannel, Attachment> {
@Override
public void completed(AsynchronousSocketChannel client, Attachment attach) {
try {
// Get the client address
SocketAddress clientAddr = client.getRemoteAddress();
System.out.format("Accepted a connection from %s%n", clientAddr);
// Accept another connection
attach.server.accept(attach, this);
// Handle the client connection by invoking an asyn read
Attachment newAttach = new Attachment();
newAttach.server = attach.server;
newAttach.client = client;
newAttach.buffer = ByteBuffer.allocate(2048);
newAttach.isRead = true;
newAttach.clientAddr = clientAddr;
// Create a new completion handler for reading to and writing
// from the new client
ReadWriteHandler readWriteHandler = new ReadWriteHandler();
// Read from the client
client.read(newAttach.buffer, newAttach, readWriteHandler);
}
catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable e, Attachment attach) {
System.out.println("Failed to accept a connection.");
e.printStackTrace();
}
}
ConnectionHandler类很简单。在其failed()方法中,它打印异常栈跟踪。在它的completed()方法中,它打印一个新的客户端连接已经建立的消息,并通过再次调用服务器套接字上的accept()方法开始监听另一个新的客户端连接。注意附件在另一个completed()方法内部的accept()方法调用中的重用。它再次使用同一个CompletionHandler对象。注意,attach.server.accept(attach, this)方法调用使用关键字this来引用完成处理程序的同一个实例。最后,它准备了一个Attachment类的新实例,该实例包装了处理(读取和写入)新客户端连接的细节,并在客户端套接字上调用read()方法来从客户端读取。注意,read()方法使用了另一个完成处理程序,它是该类的一个实例。ReadWriteHandler的代码如下:
private static class ReadWriteHandler implements CompletionHandler<Integer, Attachment> {
@Override
public void completed(Integer result, Attachment attach) {
if (result == -1) {
try {
attach.client.close();
System.out.format("Stopped listening to the client %s%n",
attach.clientAddr);
}
catch (IOException ex) {
ex.printStackTrace();
}
return;
}
if (attach.isRead) {
// A read to the client was completed
// Get the buffer ready to read from it
attach.buffer.flip();
int limits = attach.buffer.limit();
byte bytes[] = new byte[limits];
attach.buffer.get(bytes, 0, limits);
Charset cs = Charset.forName("UTF-8");
String msg = new String(bytes, cs);
// Print the message from the client
System.out.format("Client at %s says: %s%n", attach.clientAddr, msg);
// Let us echo back the same message to the client
attach.isRead = false; // It is a write
// Prepare the buffer to be read again
attach.buffer.rewind();
// Write to the client again
attach.client.write(attach.buffer, attach, this);
}
else {
// A write to the client was completed.
// Perform another read from the client
attach.isRead = true;
// Prepare the buffer to be filled in
attach.buffer.clear();
// Perform a read from the client
attach.client.read(attach.buffer, attach, this);
}
}
@Override
public void failed(Throwable e, Attachment attach) {
e.printStackTrace();
}
}
completed()方法的第一个参数result是从客户端读取或写入客户端的字节数。它的值-1 表示流结束,在这种情况下,客户端套接字关闭。如果读操作完成,它会在标准输出上显示读取的文本,并将相同的文本写回客户端。如果对客户端的写操作已完成,它会在同一客户端上执行读操作。
清单 5-16 包含了异步服务器套接字通道的完整代码。它使用三个内部类:一个用于附件,一个用于连接完成处理程序,一个用于读/写完成处理程序。现在可以运行该类了。但是,它不会做任何工作,因为它需要一个客户端连接到它,以回显从客户端发送的消息。在下一节中,您将开发您的异步客户机套接字通道,然后,在下一节中,您将一起测试服务器和客户机套接字通道。
清单 5-16。使用异步服务器套接字通道的服务器应用
// AsyncEchoServerSocket.java
package com.jdojo.net;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.net.InetSocketAddress;
import java.nio.channels.CompletionHandler;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.AsynchronousServerSocketChannel;
public class AsyncEchoServerSocket {
private static class Attachment {
AsynchronousServerSocketChannel server;
AsynchronousSocketChannel client;
ByteBuffer buffer;
SocketAddress clientAddr;
boolean isRead;
}
private static class ConnectionHandler implements
CompletionHandler<AsynchronousSocketChannel, Attachment> {
@Override
public void completed(AsynchronousSocketChannel client, Attachment attach) {
try {
// Get the client address
SocketAddress clientAddr = client.getRemoteAddress();
System.out.format("Accepted a connection from %s%n",
clientAddr);
// Accept another connection
attach.server.accept(attach, this);
// Handle the client connection by using an asyn read
ReadWriteHandler rwHandler = new ReadWriteHandler();
Attachment newAttach = new Attachment();
newAttach.server = attach.server;
newAttach.client = client;
newAttach.buffer = ByteBuffer.allocate(2048);
newAttach.isRead = true;
newAttach.clientAddr = clientAddr;
client.read(newAttach.buffer, newAttach, rwHandler);
}
catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable e, Attachment attach) {
System.out.println("Failed to accept a connection.");
e.printStackTrace();
}
}
private static class ReadWriteHandler
implements CompletionHandler<Integer, Attachment> {
@Override
public void completed(Integer result, Attachment attach) {
if (result == -1) {
try {
attach.client.close();
System.out.format(
"Stopped listening to the client %s%n",
attach.clientAddr);
}
catch (IOException ex) {
ex.printStackTrace();
}
return;
}
if (attach.isRead) {
// A read to the client was completed
// Get the buffer ready to read from it
attach.buffer.flip();
int limits = attach.buffer.limit();
byte bytes[] = new byte[limits];
attach.buffer.get(bytes, 0, limits);
Charset cs = Charset.forName("UTF-8");
String msg = new String(bytes, cs);
// Print the message from the client
System.out.format("Client at %s says: %s%n",
attach.clientAddr, msg);
// Let us echo back the same message to the client
attach.isRead = false; // It is a write
// Prepare the buffer to be read again
attach.buffer.rewind();
// Write to the client
attach.client.write(attach.buffer, attach, this);
}
else {
// A write to the client was completed. Perform
// another read from the client
attach.isRead = true;
// Prepare the buffer to be filled in
attach.buffer.clear();
// Perform a read from the client
attach.client.read(attach.buffer, attach, this);
}
}
@Override
public void failed(Throwable e, Attachment attach) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try (AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open()) {
// Bind the server to the localhost and the port 8989
String host = "localhost";
int port = 8989;
InetSocketAddress sAddr =
new InetSocketAddress(host, port);
server.bind(sAddr);
// Display a message that server is ready
System.out.format("Server is listening at %s%n", sAddr);
// Prepare the attachment
Attachment attach = new Attachment();
attach.server = server;
// Accept new connections
server.accept(attach, new ConnectionHandler());
try {
// Wait until the main thread is interrupted
Thread.currentThread().join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
设置异步客户端套接字通道
在客户端应用中,AsynchronousSocketChannel类的一个实例被用作异步客户端套接字通道。AsynchronousSocketChannel类的静态open()方法返回一个AsynchronousSocketChannel类型的开放通道,该通道尚未连接到服务器套接字通道。通道的connect()方法用于连接到服务器套接字通道。下面的代码片段显示了如何创建异步客户端套接字通道并将其连接到服务器套接字通道。它使用一个Future对象来完成与服务器的连接。
// Create an asynchronous socket channel
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
// Connect the channel to the server
String serverName = "localhost";
int serverPort = 8989;
SocketAddress serverAddr = new InetSocketAddress(serverName, serverPort);
Future<Void> result = channel.connect(serverAddr);
System.out.println("Connecting to the server...");
// Wait for the connection to complete
result.get();
// Connection to the server is complete now
System.out.println("Connected to the server...");
一旦客户端套接字通道连接到服务器,您就可以开始使用通道的read()和write()方法异步地从服务器读取数据和向服务器写入数据。这两种方法都允许您使用一个Future对象或一个CompletionHandler对象来处理操作的完成。您将使用如图所示的Attachment类将上下文传递给完成处理程序:
class Attachment {
AsynchronousSocketChannel channel;
ByteBuffer buffer;
Thread mainThread;
boolean isRead;
}
在该类中,channel实例变量保存对客户端通道的引用。buffer实例变量保存对数据缓冲区的引用。您将使用相同的数据缓冲区进行读取和写入。mainThread实例变量保存对应用主线程的引用。当客户端通道完成时,您可以中断正在等待的主线程,这样客户端应用就终止了。isRead实例变量指示操作是读还是写。如果是true,则表示是读操作。否则,它是一个写操作。
清单 5-17 包含了异步客户端套接字通道的完整代码。它使用了两个名为Attachment和的内部类。Attachment类的一个实例被用作read()和write()异步操作的附件。一个ReadWriteHandler类的实例被用作read()和write()操作的完成处理器。它的getTextFromUser()方法提示用户在标准输入上输入消息,并返回用户输入的消息。完成处理程序的completed()方法检查它是读操作还是写操作。如果是读取操作,它会在标准输出中打印从服务器读取的文本。它会提示用户输入另一条消息。如果用户输入Bye,它通过中断正在等待的主线程来终止应用。注意,当程序退出try程序块时,通道自动关闭,因为它是在main()方法的try-with-resources程序块中打开的。
清单 5-17。异步客户端套接字通道
// AsyncEchoClientSocket.java
package com.jdojo.net;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.concurrent.Future;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import java.nio.channels.AsynchronousSocketChannel;
public class AsyncEchoClientSocket {
private static class Attachment {
AsynchronousSocketChannel channel;
ByteBuffer buffer;
Thread mainThread;
boolean isRead;
}
private static class ReadWriteHandler
implements CompletionHandler<Integer, Attachment> {
@Override
public void completed(Integer result, Attachment attach) {
if (attach.isRead) {
attach.buffer.flip();
// Get the text read from the server
Charset cs = Charset.forName("UTF-8");
int limits = attach.buffer.limit();
byte bytes[] = new byte[limits];
attach.buffer.get(bytes, 0, limits);
String msg = new String(bytes, cs);
// A read from the server was completed
System.out.format("Server Responded: %s%n", msg);
// Prompt the user for another message
msg = this.getTextFromUser();
if (msg.equalsIgnoreCase("bye")) {
// Interrupt the main thread, so the program terminates
attach.mainThread.interrupt();
return;
}
// Prepare buffer to be filled in again
attach.buffer.clear();
byte[] data = msg.getBytes(cs);
attach.buffer.put(data);
// Prepared buffer to be read
attach.buffer.flip();
attach.isRead = false; // It is a write
// Write to the server
attach.channel.write(attach.buffer, attach, this);
}
else {
// A write to the server was completed. Perform another
// read from the server
attach.isRead = true;
// Prepare the buffer to be filled in
attach.buffer.clear();
// Read from the server
attach.channel.read(attach.buffer, attach, this);
}
}
@Override
public void failed(Throwable e, Attachment attach) {
e.printStackTrace();
}
private String getTextFromUser() {
System.out.print("Please enter a message (Bye to quit):");
String msg = null;
BufferedReader consoleReader =
new BufferedReader(new InputStreamReader(System.in));
try {
msg = consoleReader.readLine();
}
catch (IOException e) {
e.printStackTrace();
}
return msg;
}
}
public static void main(String[] args) {
// Use a try-with-resources to open a channel
try (AsynchronousSocketChannel channel
= AsynchronousSocketChannel.open()) {
// Connect the client to the server
String serverName = "localhost";
int serverPort = 8989;
SocketAddress serverAddr =
new InetSocketAddress(serverName, serverPort);
Future<Void> result = channel.connect(serverAddr);
System.out.println("Connecting to the server...");
// Wait for the connection to complete
result.get();
// Connection to the server is complete now
System.out.println("Connected to the server...");
// Start reading from and writing to the server
Attachment attach = new Attachment();
attach.channel = channel;
attach.buffer = ByteBuffer.allocate(2048);
attach.isRead = false;
attach.mainThread = Thread.currentThread();
// Place the "Hello" message in the buffer
Charset cs = Charset.forName("UTF-8");
String msg = "Hello";
byte[] data = msg.getBytes(cs);
attach.buffer.put(data);
attach.buffer.flip();
// Write to the server
ReadWriteHandler readWriteHandler = new ReadWriteHandler();
channel.write(attach.buffer, attach, readWriteHandler) ;
// Let this thread wait for ever on its own death until interrupted
attach.mainThread.join();
}
catch (ExecutionException | IOException e) {
e.printStackTrace();
}
catch(InterruptedException e) {
System.out.println("Disconnected from the server.");
}
}
}
将服务器和客户端放在一起
此时,您的异步服务器和客户端程序已经准备好了。您需要使用以下步骤来运行服务器和客户端。
运行服务器应用
运行清单 5-16 中列出的AsyncEchoServerSocket类。您应该会在标准输出中得到如下消息:
Server is listening at localhost/127.0.0.1:8989
如果您得到上述消息,您需要进行下一步。如果您没有收到上述消息,很可能是端口 8989 正在被另一个进程使用。在这种情况下,您应该会收到以下错误消息:
java.net.BindException: Address already in use: bind
如果您得到“地址已被使用”的错误消息,您需要将AsyncEchoServerSocket类中的端口值从 8989 更改为其他值,然后重新运行AsyncEchoServerSocket类。如果在服务器程序中更改端口号,还必须在客户端程序中更改端口号以匹配服务器端口号。服务器套接字通道监听一个端口,客户端必须连接到服务器正在监听的同一个端口。
运行客户端应用
在继续此步骤之前,请确保您能够成功执行上一步。运行清单 5-17 中列出的AsyncEchoClientSocket类的一个或多个实例。如果客户端应用能够成功连接到服务器,您应该会在标准输出中看到以下消息:
Connecting to the server...
Connected to the server...
Server Responded: Hello
Please enter a message (Bye to quit):
当您尝试运行AsyncEchoClientSocket类时,可能会收到以下错误消息:
Connecting to the server...
java.util.concurrent.ExecutionException: java.io.IOException: The remote system refused the network connection.
通常,此错误消息指示以下问题之一:
- 服务器没有运行。如果是这种情况,请确保服务器正在运行。
- 客户端正试图连接到与服务器侦听的主机和端口不同的主机和端口上的服务器。如果是这种情况,请确保服务器和客户端使用相同的主机名(或 IP 地址)和端口号。
您需要手动停止服务器程序,例如在 Windows 的命令提示符下按下Ctrl + C键。
面向数据报的套接字通道
java.nio.channels.DatagramChannel类的一个实例代表一个数据报通道。默认情况下,它是阻止的。您可以使用configureBlocking(false)方法将其配置为非阻塞的。
要创建一个DatagramChannel,您需要调用它的一个open()静态方法。如果您想将其用于 IP 多播,您需要指定多播组的地址类型(或协议族)作为其open()方法的参数。open()方法创建了一个DatagramChannel对象,这个对象没有被连接。如果您希望您的数据报通道只向特定的远程主机发送和接收数据报,您需要使用它的connect()方法将通道连接到那个特定的主机。未连接的数据报通道可以向任何远程主机发送数据报,也可以从任何远程主机接收数据报。以下部分概述了使用数据报通道发送/接收数据报通常需要的步骤。
创建数据报通道
您可以使用DatagramChannel类的open()方法创建数据报通道。以下代码片段显示了创建数据报通道的三种不同方式:
// Create a new datagram channel to send/receive datagram
DatagramChannel channel = DatagramChannel.open();
// Create a datagram channel to receive datagrams from a multicast group
// that uses IPv4 address type
DatagramChannel ipv4MulticastChannel = DatagramChannel.open(StandardProtocolFamily.INET);
// Create a datagram channel to receive datagrams from a multicast group
// that uses IPv6 address type
DatagramChannel iPv6MulticastChannel = DatagramChannel.open(StandardProtocolFamily.INET6);
设置频道选项
您可以使用DatagramChannel类的setOption()方法设置通道选项。有些选项必须在将通道绑定到特定地址之前设置,而有些可以在绑定之后设置。在 Java 7 中,setOption()方法被添加到了DatagramChannel类中。如果您使用的是以前的 Java 版本,您将需要使用socket()方法来获取DatagramSocket引用,并使用DatagramSocket类的方法之一来设置通道选项。下面的代码片段显示了如何设置通道选项。表 5-9 包含插座选项列表及其描述。套接字选项在StandardSocketOptions类中被定义为常量。
// To bind multiple sockets to the same socket address,
// you need to set the SO_REUSEADDR option for the socket
// In Java 7 and later
channel.setOption(StandardSocketOptions.SO_REUSEADDR, true)
// Prior to Java 7
DatagramSocket socket = channel.socket();
socket.setReuseAddress(true);
表 5-9。
The List of Standard Socket Options
| 套接字选项名称 | 描述 | | --- | --- | | `SO_SNDBUF` | 套接字发送缓冲区的大小,以字节为单位。它的值属于`Integer`类型。 | | `SO_RCVBUF` | 套接字接收缓冲区的大小,以字节为单位。它的值属于`Integer`类型。 | | `SO_REUSEADDR` | 对于数据报套接字,它允许多个程序绑定到同一个地址。它的值属于`Boolean`类型。应该为使用数据报通道的 IP 多播启用此选项。 | | `SO_BROADCAST` | 允许传输广播数据报。它的值属于类型`Boolean`。 | | `IP_TOS` | 互联网协议(IP)报头中的服务类型(ToS)八位字节。它的值属于`Integer`类型。 | | `IP_MULTICAST_IF` | 网际协议(IP)多播数据报的网络接口。其值是一个`NetworkInterface`类型的引用。 | | `IP_MULTICAST_TTL` | 网际协议(IP)多播数据报的生存时间。其值属于类型`Integer`,范围为 0 到 255。 | | `IP_MULTICAST_LOOP` | 网际协议(IP)多播数据报的环回。它的值属于类型`Boolean`。 |绑定数据报通道
使用DatagramChannel类的bind()方法将数据报通道绑定到特定的本地地址和端口。如果使用null作为绑定地址,这个方法会自动将套接字绑定到一个可用的地址。Java 7 中的DatagramChannel类中添加了bind()方法。如果您使用的是以前的 Java 版本,您可以使用其底层套接字绑定数据报通道。以下代码片段显示了如何绑定数据报通道:
/* In Java 7 and later */
// Bind the channel to any available address automatically
channel.bind(null);
// Bind the channel to "localhost" and port 8989
InetSocketAddress sAddr = new InetSocketAddress("localhost", 8989);
channel.bind(sAddr);
/* Prior to Java 7 */
// Get the socket reference
DatagramSocket socket = channel.socket();
// Bind the channel to any available address automatically
socket.bind(null);
// Bind the channel to "localhost" and port 8989
InetSocketAddress sAddr = new InetSocketAddress("localhost", 8989);
socket.bind(sAddr);
发送数据报
要向远程主机发送数据报,请使用DatagramChannel类的send()方法。该方法接受一个ByteBuffer和一个远程SocketAddress。如果在未绑定的数据报通道上调用send()方法,send()方法会自动将通道绑定到一个可用的地址。
// Prepare a message to send
String msg = "Hello";
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
// Pack the remote address and port into an object
InetSocketAddress serverAddress = new InetSocketAddress("localhost", 8989);
// Send the message to the remote host
channel.send(buffer, serverAddress);
DatagramChannel类的receive()方法让数据报通道从远程主机接收数据报。这种方法要求你提供一个ByteBuffer来接收数据。接收到的数据被复制到指定的ByteBuffer的当前位置。如果ByteBuffer的可用空间少于接收到的数据,多余的数据将被自动丢弃。receive()方法返回远程主机的地址。如果数据报通道处于非阻塞模式,receive()方法通过返回null立即返回。否则,它会一直等待,直到收到数据报。
// Prepare a ByteBufer to receive data
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Wait to receive data from a remote host
SocketAddress remoteAddress = channel.receive(buffer);
关闭频道
最后,使用其方法关闭数据报通道。
// Close the channel
channel.close();
清单 5-18 包含了一个作为回应服务器的程序。清单 5-19 有一个程序作为客户端。echo 服务器等待来自远程客户端的消息。它回显从远程客户端接收的消息。在启动客户端程序之前,您需要启动 echo 服务器程序。您可以同时运行多个客户端程序。
清单 5-18。基于数据报通道的 Echo 服务器
// DGCEchoServer.java
package com.jdojo.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class DGCEchoServer {
public static void main(String[] args) {
DatagramChannel server = null;
try {
// Create a datagram channel and bind it to localhost at port 8989
server = DatagramChannel.open();
InetSocketAddress sAddr = new InetSocketAddress("localhost", 8989);
server.bind(sAddr);
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Wait in an infinite loop for a client to send data
while (true) {
System.out.println("Waiting for a message from" +
" a remote host at " + sAddr);
// Wait for a client to send a message
SocketAddress remoteAddr = server.receive(buffer);
// Prepare the buffer to read the message
buffer.flip();
// Convert the buffer data into a String
int limits = buffer.limit();
byte bytes[] = new byte[limits];
buffer.get(bytes, 0, limits);
String msg = new String(bytes);
System.out.println("Client at " + remoteAddr +
" says: " + msg);
// Reuse the buffer to echo the message to the client
buffer.rewind();
// Send the message back to the client
server.send(buffer, remoteAddr);
// Prepare the buffer to receive the next message
buffer.clear();
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
// Close the channel
if (server != null) {
try {
server.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
清单 5-19。基于数据报通道的客户端程序
// DGCEchoClient.java
package com.jdojo.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class DGCEchoClient {
public static void main(String[] args) {
DatagramChannel client = null;
try {
// Create a new datagram channel
client = DatagramChannel.open();
// Bind the client to any available local address and port
client.bind(null);
// Prepare a message for the server
String msg = "Hello";
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
InetSocketAddress serverAddress =
new InetSocketAddress("localhost", 8989);
// Send the message to the server
client.send(buffer, serverAddress);
// Reuse the buffer to receive a response from the server
buffer.clear();
// Wait for the server to respond
client.receive(buffer);
// Prepare the buffer to read the message
buffer.flip();
// Convert the buffer into a string
int limits = buffer.limit();
byte bytes[] = new byte[limits];
buffer.get(bytes, 0, limits);
String response = new String(bytes);
// Print the server message on the standard output
System.out.println("Server responded: " + response);
}
catch (IOException e) {
e.printStackTrace();
}
finally {
// Close the channel
if (client != null) {
try {
client.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
使用数据报信道的多播
Java 7 为数据报通道增加了对 IP 多播的支持。对接收多播数据报感兴趣的数据报通道加入多播组。发送到多播组的数据报被传递给其所有成员。以下部分概述了设置对接收多播数据报感兴趣的客户端应用通常需要的步骤。
创建数据报通道
创建数据报通道以使用特定的多播地址类型,如下所示。在您的应用中,您将使用 IPv4 或 IPv6,而不是两者都使用。
// Need to use INET protocol family for an IPv4 addressing scheme
DatagramChannel client = DatagramChannel.open(StandardProtocolFamily.INET);
// Need to use INET6 protocol family for an IPv6 addressing scheme
DatagramChannel client = DatagramChannel.open(StandardProtocolFamily.INET6);
设置频道选项
使用setOption()方法设置客户端通道的选项,如下所示:
// Let other sockets reuse the same address
client.setOption(StandardSocketOptions.SO_REUSEADDR, true);
绑定频道
将客户端通道绑定到本地地址和端口,如下所示:
int MULTICAST_PORT = 8989;
client.bind(new InetSocketAddress(MULTICAST_PORT));
设置多播网络接口
设置套接字选项IP_MULTICAST_IF,指定客户端通道将加入多播组的网络接口。
// Get the reference of a network interface named "eth1"
NetworkInterface interf = NetworkInterface.getByName("eth1");
// Set the IP_MULTICAST_IF option
client.setOption(StandardSocketOptions.IP_MULTICAST_IF, interf);
清单 5-20 包含了一个完整的程序,它打印出你的机器上所有可用的网络接口的名称。它还打印网络接口是否支持多播以及它是否启动。在您的机器上运行代码时,您可能会得到不同的输出。您需要使用一个支持多播的可用网络接口的名称,并且该网络接口应该处于运行状态。例如,如输出所示,在我的机器上,名为eth1的网络接口启动并支持多播,所以我使用eth1作为处理多播消息的网络接口。
清单 5-20。列出机器上可用的网络接口
// ListNetworkInterfaces.java
package com.jdojo.net;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
public class ListNetworkInterfaces {
public static void main(String[] args) {
try {
Enumeration<NetworkInterface> e =
NetworkInterface.getNetworkInterfaces();
while (e.hasMoreElements()) {
NetworkInterface nif = e.nextElement();
System.out.println("Name: " + nif.getName() +
", Supports Multicast: " + nif.supportsMulticast() +
", isUp(): " + nif.isUp()) ;
}
}
catch (SocketException ex) {
ex.printStackTrace();
}
}
}
Name: lo, Supports Multicast: true, isUp(): true
Name: eth0, Supports Multicast: true, isUp(): false
Name: wlan0, Supports Multicast: true, isUp(): false
Name: eth1, Supports Multicast: true, isUp(): true
Name: net0, Supports Multicast: false, isUp(): false
Name: net1, Supports Multicast: false, isUp(): true
Name: wlan1, Supports Multicast: true, isUp(): false
Name: eth2, Supports Multicast: true, isUp(): false
Name: eth3, Supports Multicast: true, isUp(): false
Name: eth4, Supports Multicast: true, isUp(): false
Name: wlan2, Supports Multicast: true, isUp(): false
Name: wlan3, Supports Multicast: true, isUp(): false
Name: wlan4, Supports Multicast: true, isUp(): false
Name: wlan5, Supports Multicast: true, isUp(): false
Name: wlan6, Supports Multicast: true, isUp(): false
Name: wlan7, Supports Multicast: true, isUp(): false
Name: wlan8, Supports Multicast: true, isUp(): false
Name: wlan9, Supports Multicast: true, isUp(): false
Name: wlan10, Supports Multicast: true, isUp(): false
加入多播组
现在是使用如下的join()方法加入多播组的时候了。请注意,您必须为该组使用多播 IP 地址。
String MULTICAST_IP = "239.1.1.1";
// Join the multicast group on interf interface
InetAddress group = InetAddress.getByName(MULTICAST_IP);
MembershipKey key = client.join(group, interf);
join()方法返回一个MembershipKey类的对象,表示数据报通道与多播组的成员关系。如果数据报通道不再对接收多播数据报感兴趣,它可以使用key的drop()方法从多播组中删除其成员。
Tip
数据报信道可以决定只从选择的源接收多播数据报。您可以使用MembershipKey类的block(InetAddress source)方法来阻止来自指定的source地址的多播数据报。它的unblock(InetAddress source)让你解锁一个先前被封锁的源地址。
接收消息
此时,接收寻址到多播组的数据报只需调用通道上的receive()方法,如下所示:
// Prepare a buffer to receive the message from the multicast group
ByteBuffer buffer = ByteBuffer.allocate(1048);
// Wait to receive a message from the multicast group
client.receive(buffer);
使用完该频道后,您可以从组中删除其成员,如下所示:
// We are no longer interested in receiving multicast message from the group.
// So, we need to drop the channel's membership from the group
key.drop();
关闭频道
最后,您需要使用它的close()方法关闭通道,如下所示:
// Close the channel
client.close();
要向多播组发送消息,您不需要成为该多播组的成员。您可以使用DatagramChannel类的send()方法向多播组发送数据报。
清单 5-21 包含一个有三个常量的类,这三个常量在后面的两个类中用来构建多播应用。这些常量包含多播 IP 地址、多播端口号和多播网络接口名称,将在后续示例中使用。请确保MULTICAST_INTERFACE_NAME常量的值eth1是您的机器上支持多播的网络接口名称,并且它是打开的。通过运行清单 5-20 中的程序,你可以得到你的机器上所有网络接口的列表。
清单 5-21。一个基于 DatagramChannel 的组播客户端程序
// DGCMulticastUtil.java
package com.jdojo.net;
public class DGCMulticastUtil {
public static final String MULTICAST_IP = "239.1.1.1";
public static final int MULTICAST_PORT = 8989;
/* You need to change the following network interface name "eth1"
to the network interface name that supports multicast and is up
on your machine. Please run class ListNetworkInterfaces to get
the list of all available network interface on your machine.
*/
public static final String MULTICAST_INTERFACE_NAME = "eth1";
}
清单 5-22 包含了一个加入多播组的程序。它等待来自多播组的消息到达,打印该消息,然后退出。清单 5-23 包含一个向多播组发送消息的程序。您可以运行DGCMulticastClient类的多个实例,然后运行DGCMulticastServer类。所有客户端实例都应该在标准输出中接收和打印相同的消息。
清单 5-22。一个基于 DatagramChannel 的组播客户端程序
// DGCMulticastClient.java
package com.jdojo.net;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.MembershipKey;
public class DGCMulticastClient {
public static void main(String[] args) {
MembershipKey key = null;
// Create, configure and bind the client datagram channel
try (DatagramChannel client =
DatagramChannel.open(StandardProtocolFamily.INET)) {
// Get the reference of a network interface
NetworkInterface interf = NetworkInterface.getByName(
DGCMulticastUtil.MULTICAST_INTERFACE_NAME);
client.setOption(StandardSocketOptions.SO_REUSEADDR, true);
client.bind(new InetSocketAddress(DGCMulticastUtil.MULTICAST_PORT));
client.setOption(StandardSocketOptions.IP_MULTICAST_IF, interf);
// Join the multicast group on the interf interface
InetAddress group =
InetAddress.getByName(DGCMulticastUtil.MULTICAST_IP);
key = client.join(group, interf);
// Print some useful messages for the user
System.out.println("Joined the multicast group:" + key);
System.out.println("Waiting for a message from the" +
" multicast group....");
// Prepare a data buffer to receive a message from the multicast group
ByteBuffer buffer = ByteBuffer.allocate(1048);
// Wait to receive a message from the multicast group
client.receive(buffer);
// Convert the message in the ByteBuffer into a string
buffer.flip();
int limits = buffer.limit();
byte bytes[] = new byte[limits];
buffer.get(bytes, 0, limits);
String msg = new String(bytes);
System.out.format("Multicast Message:%s%n", msg);
}
catch (IOException e) {
e.printStackTrace();
}
finally {
// Drop the membership from the multicast group
if (key != null) {
key.drop();
}
}
}
}
清单 5-23。一个基于 DatagramChannel 的多播程序,它向多播组发送消息
// DGCMulticastServer.java
package com.jdojo.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class DGCMulticastServer {
public static void main(String[] args) {
// Get a datagram channel object to act as a server
try (DatagramChannel server = DatagramChannel.open()) {
// Bind the server to any available local address
server.bind(null);
// Set the network interface for outgoing multicast data
NetworkInterface interf = NetworkInterface.getByName(
DGCMulticastUtil.MULTICAST_INTERFACE_NAME);
server.setOption(StandardSocketOptions.IP_MULTICAST_IF, interf);
// Prepare a message to send to the multicast group
String msg = "Hello from multicast!";
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
// Get the multicast group reference to send data to
InetSocketAddress group
= new InetSocketAddress(DGCMulticastUtil.MULTICAST_IP,
DGCMulticastUtil.MULTICAST_PORT);
// Send the message to the multicast group
server.send(buffer, group);
System.out.println("Sent the multicast message: " + msg);
}
catch (IOException e) {
e.printStackTrace();
}
}
}
进一步阅读
用 Java 进行网络编程是一个很大的话题。有几本书是专门写这个话题的。本章仅涵盖 Java 中可用的网络编程支持的基础知识。Java 还支持使用安全套接字层(SSL)协议的安全套接字通信。安全套接字通信编程的类在javax.net.ssl包中。本章不包括 SSL 套接字。我还没有介绍许多可以在 Java 程序中使用的套接字选项。如果你想用 Java 进行高级网络编程,建议你在读完这一章后,读一本专门用 Java 进行网络编程的书。
摘要
网络是一组两台或多台计算机或其他类型的电子设备(如打印机),它们为了共享信息而连接在一起。连接到网络的每个设备称为一个节点。与网络相连的计算机称为主机。Java 中的网络编程包括编写 Java 程序,以促进网络上不同计算机上运行的进程之间的信息交换。
两台远程主机之间的通信是通过称为 Internet 参考模型或 TCP/IP 分层模型的分层协议套件来执行的。该协议组由五层组成,即应用层、传输层、互联网层、网络接口层和物理层。Java 程序等用户应用使用应用层与远程应用进行通信。传输层协议处理将消息从一台计算机上的一个应用传输到远程计算机上的另一个应用的方式。互联网层接受来自传输层的消息,并准备适合通过互联网发送的数据包。它包括互联网协议(IP)。由 IP 准备的数据包也称为 IP 数据报,它由报头和数据区以及其他信息组成。网络接口层准备要在网络上传输的数据包。这个数据包称为一个帧。网络接口层位于物理层之上,物理层涉及硬件。物理层由硬件组成。它负责将信息比特转换成信号,并通过线路传输信号。
IP 地址唯一标识计算机和路由器之间的连接。互联网协议有两个版本——IP v4(或简称 IP)和 IPv6,其中 v4 和 v6 分别代表版本 4 和版本 6。IPv6 也被称为下一代互联网协议(IPng)。在 Java 程序中,InetAddress类的一个对象代表一个 IP 地址。InetAddress类有两个子类,Inet4Address和Inet6Address,分别代表 IPv4 和 IPv6 地址。
端口号是 16 位无符号整数,范围从 0 到 65535,用于唯一标识特定协议的进程。
InetSocketAddress类的一个对象表示一个套接字地址,它结合了一个 IP 地址和一个端口号。
ServerSocket类的一个对象代表一个 TCP 服务器套接字,用于接受来自远程主机的连接。Socket类的一个对象代表一个服务器/客户端套接字。客户端和服务器应用使用Socket类的对象交换信息。Socket类提供了getInputStream()和getOutputStream()方法来分别获取套接字的输入和输出流。套接字的输入流用于从套接字读取数据,套接字的输出流用于向套接字写入数据。
DatagramPacket类的对象代表 UDP 数据报,它是 UDP 套接字上的数据传输单元。DatagramSocket类的一个对象代表一个 UDP 服务器/客户端套接字。
统一资源标识符(URI)是标识资源的字符序列。使用位置来标识资源的 URI 称为统一资源定位器(URL)。使用名称来标识资源的 URI 称为统一资源名(URN)。URL 和 URN 是 URI 的子集。java.net.URI类的一个对象代表 Java 中的一个 URI。java.net.URL类的一个对象代表 Java 中的一个 URL。Java 提供了访问由 URL 标识的内容的类。
Java 支持使用java.nio.channels包中的ServerSocketChannel、SocketChannel、Selector和SelectionKey类的非阻塞套接字通道。
Java 还通过java.nio.channels包中的AsynchronousServerSocketChannel和AsynchronousSocketChannel类支持异步套接字通道。
Java 通过DatagramChannel类支持面向数据报的套接字通道。数据报通道也支持 IP 多播。