Java 中文官方教程 2022 版(四十五)

原文:docs.oracle.com/javase/tutorial/reallybigindex.html

教程:自定义网络

原文:docs.oracle.com/javase/tutorial/networking/index.html

Java 平台备受推崇,部分原因是其适用于编写使用和与互联网资源以及万维网进行交互的程序。事实上,兼容 Java 的浏览器极大地利用了 Java 平台的这种能力,在互联网上传输和运行小程序。

本教程将引导您了解编写可在互联网上使用的 Java 应用程序和小程序的复杂性。

有两个部分。第一部分描述了 Java 平台的网络功能,您可能已经在不知不觉中使用网络。第二部分简要概述了网络,使您熟悉在阅读如何使用 URL、套接字和数据报之前应了解的术语和概念。

讨论了您的 Java 程序如何使用 URL 来访问互联网上的信息。URL(统一资源定位符)是互联网上资源的地址。您的 Java 程序可以使用 URL 来连接并检索网络上的信息。本课程提供了 URL 的更完整定义,并向您展示如何创建和解析 URL,如何打开与 URL 的连接,以及如何从该连接读取和写入。

解释了如何使用套接字,以便您的程序可以与网络上的其他程序通信。套接字是运行在网络上的两个程序之间的双向通信链路的一个端点。本课程向您展示了客户端如何连接到标准服务器 Echo 服务器,并通过套接字与其通信。然后,它详细介绍了一个完整客户端/服务器示例的细节,展示了如何实现客户端和服务器端的客户端/服务器对。

逐步带您完成一个使用数据报进行通信的简单客户端/服务器示例。然后,它挑战您使用多播套接字重新编写示例。

解释了为什么您可能想要访问网络接口参数以及如何做到这一点。它提供了如何列出分配给计算机的所有 IP 地址以及其他有用信息的示例,例如接口是否正在运行。

讨论了如何使用 cookies 在客户端和服务器之间创建会话,以及如何在 HTTP URL 连接中利用 cookies。


安全考虑:

请注意,网络通信需经当前安全管理器批准。安全管理器 描述了安全管理器是什么以及它如何影响您的应用程序。有关 JDK 提供的安全功能的一般信息,请参考 Java SE 中的安全功能。

下面课程中的示例程序涵盖了 URL、套接字和数据报,它们都是独立的应用程序,默认情况下没有安全管理器。如果将这些应用程序转换为小程序,根据运行它们的浏览器或查看器的不同,它们可能无法在网络上通信。请参阅 小程序的功能和限制 以获取有关小程序所受安全限制的信息。


课程:网络概述

原文:docs.oracle.com/javase/tutorial/networking/overview/index.html

在接下来几节的示例之前,您应该对一些网络基础知识有所了解。此外,为了增强您的信心,我们还包括了一个回顾您可能已经了解的关于在 Java 中进行网络操作的部分。

您可能已经了解的关于在 Java 中进行网络操作的内容

如果您已经在本教程的其他部分工作过,那么您可能已经通过互联网加载了一个小程序,并且可能已经将网络中的图像加载到通过网络运行的小程序中。所有这些都是使用网络,而您已经知道如何做。本页突出了在本教程的其他部分和课程中涵盖的网络功能,这些功能您可能已经熟悉,并提供链接到讨论这些功能的页面。

网络基础知识

您将学习关于 TCP、UDP、套接字、数据报和端口的知识,以充分利用本教程中剩余课程的内容。如果您已经熟悉这些概念,可以随意跳过本节。

你可能已经了解的关于 Java 网络的知识

原文:docs.oracle.com/javase/tutorial/networking/overview/alreadyknow.html

“网络”这个词让许多程序员心生恐惧。不要害怕!在 Java 环境中提供的网络功能非常容易使用。事实上,你可能已经在不知不觉中使用网络了!

从网络加载小程序

如果你有一个支持 Java 的浏览器,你肯定已经执行了许多小程序。你运行的小程序是通过 HTML 文件中的特殊标签引用的 - <APPLET>标签。小程序可以位于任何地方,无论是在本地计算机上还是在互联网上的某个地方。小程序的位置对用户完全不可见。然而,小程序的位置被编码在<APPLET>标签中。浏览器解码这些信息,找到小程序并运行它。如果小程序位于你自己的机器之外的某台机器上,浏览器必须在运行之前下载小程序。

这是你从 Java 开发环境访问互联网的最高级别。其他人已经花时间编写了一个浏览器,完成了连接到网络和从中获取数据的所有繁重工作,从而使你能够从世界各地运行小程序。

更多信息:

“Hello World!”应用程序展示了如何编写你的第一个小程序并运行它。

Java 小程序系列描述了如何从头到尾编写 Java 小程序。

从 URL 加载图像

如果你尝试编写自己的 Java 小程序和应用程序,你可能已经遇到了 java.net 包中的一个类叫做 URL。这个类代表统一资源定位符,是网络上某些资源的地址。你的小程序和应用程序可以使用 URL 来引用甚至连接到网络上的资源。例如,要从网络加载图像,你的 Java 程序必须首先创建一个包含图像地址的 URL。

这是你可以与互联网互动的下一个最高级别 - 你的 Java 程序获取它想要的东西的地址,为其创建一个 URL,然后使用 Java 开发环境中的某个现有函数来连接到网络并检索资源。

更多信息:

如何使用图标展示了当你有图像的 URL 时如何将其加载到你的 Java 程序中(无论是小程序还是应用程序)。在加载图像之前,你必须创建一个包含资源地址的 URL 对象。

处理 URL,这一系列中的下一课,提供了关于 URL 的完整讨论,包括你的程序如何连接到它们以及如何从连接中读取和写入数据。

网络基础知识

原文:docs.oracle.com/javase/tutorial/networking/overview/networking.html

在互联网上运行的计算机使用传输控制协议(TCP)或用户数据报协议(UDP)相互通信,如此图所示:

网络通信示例。

当你编写通过网络进行通信的 Java 程序时,你是在应用层编程。通常情况下,你不需要关心 TCP 和 UDP 层。相反,你可以使用 java.net 包中的类。这些类提供了系统独立的网络通信。然而,为了决定你的程序应该使用哪些 Java 类,你需要了解 TCP 和 UDP 的区别。

TCP

当两个应用程序想要可靠地相互通信时,它们建立连接并在该连接上来回发送数据。这类似于打电话。如果你想和肯塔基州的比阿特丽斯阿姨通话,当你拨打她的电话号码并她接听时,就建立了连接。你通过电话线互相交流数据。像电话公司一样,TCP 保证从连接的一端发送的数据实际上到达另一端,并且按照发送顺序。否则,会报告错误。

TCP 为需要可靠通信的应用程序提供了一对一的通道。超文本传输协议(HTTP)、文件传输协议(FTP)和 Telnet 都是需要可靠通信通道的应用程序的示例。在网络上传输和接收数据的顺序对于这些应用程序的成功至关重要。当使用 HTTP 从 URL 读取数据时,数据必须按照发送顺序接收。否则,你会得到一个混乱的 HTML 文件、一个损坏的 zip 文件或其他无效信息。


定义:

TCP(传输控制协议)是一种基于连接的协议,它在两台计算机之间提供可靠的数据流。


UDP

UDP 协议提供了两个应用程序之间不保证通信的功能。UDP 不像 TCP 那样基于连接。相反,它从一个应用程序发送独立的数据包(称为数据报)到另一个应用程序。发送数据报就像通过邮政服务发送信件一样:交付顺序不重要,也不保证,每个消息都是独立的。


定义:

UDP(用户数据报协议)是一种协议,它发送独立的数据包(称为数据报)从一台计算机到另一台计算机,不保证到达。UDP 不像 TCP 那样基于连接。


对于许多应用程序,可靠性的保证对于信息从连接的一端传输到另一端的成功至关重要。然而,其他形式的通信并不需要如此严格的标准。事实上,它们可能会因为额外的开销或可靠连接可能使服务无效而变慢。

举个例子,考虑一个时钟服务器,当被请求时向其客户端发送当前时间。如果客户端错过了一个数据包,重新发送它并没有意义,因为当客户端第二次接收到时,时间将不正确。如果客户端发出两个请求并且接收到服务器发送的数据包顺序不对,也没关系,因为客户端可以发现数据包顺序不对并发出另一个请求。在这种情况下,TCP 的可靠性是不必要的,因为它会导致性能下降,可能会影响服务的实用性。

另一个不需要可靠通道保证的服务的例子是 ping 命令。ping 命令的目的是测试网络上两个程序之间的通信。事实上,ping 需要知道丢失或顺序错乱的数据包,以确定连接的好坏。可靠通道会使这项服务完全无效。

UDP 协议提供了在网络上两个应用程序之间不保证通信的功能。UDP 不像 TCP 那样基于连接。相反,它从一个应用程序发送独立的数据包到另一个应用程序。发送数据报就像通过邮件服务发送信件一样:交付顺序不重要,也不被保证,每个消息都独立于其他消息。


注意:

许多防火墙和路由器已经配置为不允许 UDP 数据包。如果您在防火墙外部连接到服务时遇到问题,或者客户端无法连接到您的服务,请询问系统管理员是否允许 UDP。


理解端口

一般来说,计算机与网络之间有一个物理连接。所有发送给特定计算机的数据都通过这个连接到达。然而,这些数据可能是针对计算机上运行的不同应用程序的。那么计算机如何知道将数据转发给哪个应用程序呢?通过端口

通过互联网传输的数据附带有标识计算机和目标端口的寻址信息。计算机通过其 32 位 IP 地址进行标识,IP 使用它将数据传递到网络上的正确计算机。端口由一个 16 位数字进行标识,TCP 和 UDP 使用它将数据传递给正确的应用程序。

在基于连接的通信(如 TCP)中,服务器应用程序将套接字绑定到特定端口号。这样做的效果是向系统注册服务器以接收所有发送到该端口的数据。然后客户端可以在服务器的端口处与服务器会合,如下图所示:

客户端通过其端口与服务器通信


定义:

TCP 和 UDP 协议使用端口将传入的数据映射到计算机上运行的特定进程。


在基于数据报的通信(如 UDP)中,数据报包含其目的地的端口号,UDP 将数据包路由到适当的应用程序,如下图所示:

将数据包路由到适当的应用程序。

端口号范围从 0 到 65,535,因为端口由 16 位数字表示。端口号从 0 到 1023 是受限制的;它们保留供 HTTP 和 FTP 等众所周知的服务以及其他系统服务使用。这些端口被称为众所周知的端口。您的应用程序不应尝试绑定到它们。

JDK 中的网络类

通过java.net中的类,Java 程序可以使用 TCP 或 UDP 在互联网上进行通信。URLURLConnectionSocketServerSocket类都使用 TCP 在网络上进行通信。DatagramPacketDatagramSocketMulticastSocket类用于 UDP。

教程:使用 URL

原文:docs.oracle.com/javase/tutorial/networking/urls/index.html

URL 是统一资源定位符的缩写。它是指向互联网上资源的引用(地址)。您向您喜爱的 Web 浏览器提供 URL,以便它可以定位互联网上的文件,就像您向邮局提供地址以便它可以找到您的通信对象一样。

与互联网交互的 Java 程序还可以使用 URL 来找到他们希望访问的互联网资源。Java 程序可以使用java.net包中的一个名为URL的类来表示 URL 地址。


术语说明:

术语URL可能会产生歧义。它可以指互联网地址或 Java 程序中的URL对象。在需要明确 URL 含义的情况下,本文使用"URL 地址"来指代互联网地址,使用"URL对象"来指代程序中URL类的实例。


什么是 URL?

URL 采用描述如何在互联网上找到资源的字符串形式。URL 有两个主要组成部分:访问资源所需的协议和资源的位置。

创建 URL

在您的 Java 程序中,您可以创建一个表示 URL 地址的 URL 对象。URL 对象始终指向绝对 URL,但可以从绝对 URL、相对 URL 或 URL 组件构造。

解析 URL

过去需要解析 URL 以查找主机名、文件名和其他信息的日子已经一去不复返了。有了有效的 URL 对象,您可以调用其任何访问器方法,从 URL 中获取所有这些信息,而无需进行任何字符串解析!

直接从 URL 读取

本节展示了您的 Java 程序如何使用openStream()方法从 URL 中读取信息。

连接到 URL

如果您想要的不仅仅是从 URL 中读取信息,您可以通过在 URL 上调用openConnection()来连接到它。openConnection()方法返回一个 URLConnection 对象,您可以用它进行更一般的与 URL 通信,如从中读取、向其中写入或查询其内容和其他信息。

从 URLConnection 读取和写入

一些 URL(例如许多与 cgi-bin 脚本相关的 URL)允许您(甚至要求您)向 URL 写入信息。例如,搜索脚本可能要求在执行搜索之前向 URL 写入详细的查询数据。本节展示了如何向 URL 写入信息以及如何获取结果。

什么是 URL?

原文:docs.oracle.com/javase/tutorial/networking/urls/definition.html

如果您一直在浏览网络,您无疑听说过 URL 这个术语,并且已经使用 URL 从网络访问 HTML 页面。

尽管不完全准确,但通常最容易将 URL 视为全球网络上文件的名称,因为大多数 URL 指向网络上某台机器上的文件。但请记住,URL 也可以指向网络上的其他资源,例如数据库查询和命令输出。


定义:

URL 是统一资源定位符的缩写,是指向互联网上资源的引用(地址)。


URL 有两个主要组成部分:

  • 协议标识符:对于 URL http://example.com,协议标识符是 http

  • 资源名称:对于 URL http://example.com,资源名称是 example.com

请注意,协议标识符和资源名称之间用冒号和两个正斜杠分隔。协议标识符指示要用于获取资源的协议的名称。此示例使用超文本传输协议(HTTP),通常用于提供超文本文档。HTTP 只是用于访问网络上不同类型资源的许多不同协议之一。其他协议包括文件传输协议(FTP)、Gopher、文件和新闻。

资源名称是资源的完整地址。资源名称的格式完全取决于所使用的协议,但对于许多协议,包括 HTTP,资源名称包含以下一个或多个组件:

主机名

存储资源的机器名称。

文件名

机器上文件的路径名。

端口号

要连接的端口号(通常是可选的)。

参考

指向资源内命名锚点的参考,通常标识文件内的特定位置(通常是可选的)。

对于许多协议,主机名和文件名是必需的,而端口号和参考是可选的。例如,HTTP URL 的资源名称必须指定网络上的服务器(主机名)和该机器上文档的路径(文件名);它还可以指定端口号和参考。

创建 URL

原文:docs.oracle.com/javase/tutorial/networking/urls/creatingUrls.html

创建 URL 对象的最简单方法是从表示 URL 地址的人类可读形式的 String 创建。这通常是另一个人将用于 URL 的形式。在您的 Java 程序中,您可以使用包含此文本的 String 来创建 URL 对象:

URL myURL = new URL("http://example.com/");

上面创建的 URL 对象表示一个绝对 URL。绝对 URL 包含到达所需资源的所有必要信息。您还可以从相对 URL地址创建 URL 对象。

创建相对于另一个的 URL

相对 URL 只包含足够的信息,以便相对于(或在另一个 URL 的上下文中)到达资源。

相对 URL 规范通常在 HTML 文件中使用。例如,假设您编写了一个名为 JoesHomePage.html 的 HTML 文件。在此页面中,有指向其他页面 PicturesOfMe.htmlMyKids.html 的链接,这些页面位于与 JoesHomePage.html 相同的机器和目录中。从 JoesHomePage.htmlPicturesOfMe.htmlMyKids.html 的链接可以像文件名一样指定,如下所示:

<a href="PicturesOfMe.html">Pictures of Me</a>
<a href="MyKids.html">Pictures of My Kids</a>

这些 URL 地址是相对 URL。也就是说,这些 URL 是相对于包含它们的文件——JoesHomePage.html来指定的。

在您的 Java 程序中,您可以从相对 URL 规范创建一个 URL 对象。例如,假设您在站点 example.com 知道两个 URL:

http://example.com/pages/page1.html
http://example.com/pages/page2.html

您可以相对于它们的共同基本 URL http://example.com/pages/ 创建这些页面的 URL 对象,如下所示:

URL myURL = new URL("http://example.com/pages/");
URL page1URL = new URL(myURL, "page1.html");
URL page2URL = new URL(myURL, "page2.html");

此代码片段使用了一个 URL 构造函数,让您可以从另一个 URL 对象(基础)和相对 URL 规范创建一个 URL 对象。此构造函数的一般形式是:

URL(URL *baseURL*, String *relativeURL*)

第一个参数是指定新 URL 的基础的 URL 对象。第二个参数是指定相对于基础的资源名称的其余部分的 String。如果 baseURL 为 null,则此构造函数将 relativeURL 视为绝对 URL 规范。相反,如果 relativeURL 是绝对 URL 规范,则构造函数将忽略 baseURL

这个构造函数也适用于为文件中的命名锚点(也称为引用)创建 URL 对象。例如,假设 page1.html 文件在文件底部有一个名为 BOTTOM 的命名锚点。您可以使用相对 URL 构造函数为其创建一个 URL 对象,如下所示:

URL page1BottomURL = new URL(page1URL,"#BOTTOM");

其他 URL 构造函数

URL 类提供了另外两个用于创建 URL 对象的构造函数。当您使用具有主机名、文件名、端口号和 URL 资源名称部分中的引用组件的 URL(如 HTTP URL)时,这些构造函数很有用。当您没有包含完整 URL 规范的 String,但您知道 URL 的各个组件时,这两个构造函数很有用。

例如,假设您设计一个类似文件浏览面板的网络浏览面板,允许用户选择协议、主机名、端口号和文件名。您可以从面板的组件构造一个URL。第一个构造函数从协议、主机名和文件名创建一个URL对象。以下代码片段创建一个URLexample.com站点的page1.html文件:

new URL("http", "example.com", "/pages/page1.html");

这等同于

new URL("http://example.com/pages/page1.html");

第一个参数是协议,第二个是主机名,最后一个是文件的路径名。请注意,文件名包含一个斜杠在开头。这表示文件名是从主机的根目录指定的。

最终的URL构造函数将端口号添加到前一个构造函数中使用的参数列表中:

URL gamelan = new URL("http", "example.com", 80, "pages/page1.html"); 

这将为以下 URL 创建一个URL对象:

http://example.com:80/pages/page1.html

如果使用这些构造函数之一构造URL对象,您可以通过使用URL对象的toString方法或等效的toExternalForm方法获得包含完整 URL 地址的String

具有特殊字符的 URL 地址

一些 URL 地址包含特殊字符,例如空格字符。就像这样:

http://example.com/hello world/

在将这些字符传递给 URL 构造函数之前,它们需要进行编码以使其合法。

URL url = new URL("http://example.com/hello%20world");

在这个例子中编码特殊字符很容易,因为只有一个字符需要编码,但是对于具有多个这些字符的 URL 地址,或者在编写代码时不确定需要访问哪些 URL 地址时,您可以使用java.net.URI类的多参数构造函数来自动处理编码。

URI uri = new URI("http", "example.com", "/hello world/", "");

然后将 URI 转换为 URL。

URL url = uri.toURL();

MalformedURLException

四个URL构造函数中的每一个如果构造函数的参数引用null或未知协议,则会抛出MalformedURLException。通常,您希望通过将 URL 构造函数语句嵌入到try/catch对中来捕获和处理此异常,就像这样:

try {
    URL myURL = new URL(...);
} 
catch (MalformedURLException e) {
    // *exception handler code here*
    // ...
}

有关处理异常的信息,请参阅异常。


注意:

URL是“一次写入”对象。一旦创建了URL对象,就无法更改其任何属性(协议、主机名、文件名或端口号)。


解析 URL

原文:docs.oracle.com/javase/tutorial/networking/urls/urlInfo.html

URL类提供了几种方法,让您可以查询URL对象。您可以使用这些访问器方法从 URL 中获取协议、权限、主机名、端口号、路径、查询、文件名和引用:

getProtocol

返回 URL 的协议标识符组件。

getAuthority

返回 URL 的权限组件。

getHost

返回 URL 的主机名组件。

getPort

返回 URL 的端口号组件。getPort方法返回一个整数,即端口号。如果端口未设置,getPort返回-1。

getPath

返回此 URL 的路径组件。

getQuery

返回此 URL 的查询组件。

getFile

返回 URL 的文件名组件。getFile方法返回与getPath相同的内容,再加上getQuery的值(如果有的话)。

getRef

返回 URL 的引用组件。


注意:

请记住,并非所有 URL 地址都包含这些组件。URL 类提供这些方法是因为 HTTP URL 包含这些组件,可能是最常用的 URL。URL 类在某种程度上是以 HTTP 为中心的。


您可以使用这些get*XXX*方法获取有关 URL 的信息,而不管您使用哪种构造函数创建 URL 对象。

URL 类以及这些访问器方法使您永远不必再解析 URL!只需给定任何 URL 的字符串规范,创建一个新的 URL 对象,并调用所需信息的任何访问器方法即可。这个小例子程序从字符串规范创建一个 URL,然后使用 URL 对象的访问器方法解析 URL:

import java.net.*;
import java.io.*;

public class ParseURL {
    public static void main(String[] args) throws Exception {

        URL aURL = new URL("http://example.com:80/docs/books/tutorial"
                           + "/index.html?name=networking#DOWNLOADING");

        System.out.println("protocol = " + aURL.getProtocol());
        System.out.println("authority = " + aURL.getAuthority());
        System.out.println("host = " + aURL.getHost());
        System.out.println("port = " + aURL.getPort());
        System.out.println("path = " + aURL.getPath());
        System.out.println("query = " + aURL.getQuery());
        System.out.println("filename = " + aURL.getFile());
        System.out.println("ref = " + aURL.getRef());
    }
}

下面是程序显示的输出:

protocol = http
authority = example.com:80
host = example.com
port = 80
path = /docs/books/tutorial/index.html
query = name=networking
filename = /docs/books/tutorial/index.html?name=networking
ref = DOWNLOADING

直接从 URL 读取

原文:docs.oracle.com/javase/tutorial/networking/urls/readingURL.html

成功创建URL后,您可以调用URLopenStream()方法来获取一个流,从中可以读取 URL 的内容。openStream()方法返回一个java.io.InputStream对象,因此从 URL 读取就像从输入流读取一样简单。

以下的小型 Java 程序使用openStream()来在 URL http://www.oracle.com/上获取输入流。然后在输入流上打开一个BufferedReader,并从BufferedReader读取,从而从 URL 读取。所有读取的内容都被复制到标准输出流:

import java.net.*;
import java.io.*;

public class URLReader {
    public static void main(String[] args) throws Exception {

        URL oracle = new URL("http://www.oracle.com/");
        BufferedReader in = new BufferedReader(
        new InputStreamReader(oracle.openStream()));

        String inputLine;
        while ((inputLine = in.readLine()) != null)
            System.out.println(inputLine);
        in.close();
    }
}

当您运行该程序时,您应该在命令窗口中看到从http://www.oracle.com/位置的 HTML 文件中滚动显示的 HTML 命令和文本内容。或者,程序可能会挂起,或者您可能会看到异常堆栈跟踪。如果出现后两种情况中的任何一种,您可能需要设置代理主机以便程序可以找到 Oracle 服务器。

连接到 URL

原文:docs.oracle.com/javase/tutorial/networking/urls/connecting.html

成功创建URL对象后,您可以调用URL对象的openConnection方法获取一个URLConnection对象,或者其协议特定的子类之一,例如java.net.HttpURLConnection

您可以使用此URLConnection对象在连接之前设置参数和一般请求属性。仅当调用URLConnection.connect方法时才会启动与 URL 表示的远程对象的连接。这样做时,您正在初始化您的 Java 程序与网络上的 URL 之间的通信链接。例如,以下代码打开到example.com站点的连接:

try {
    URL myURL = new URL("http://example.com/");
    URLConnection myURLConnection = myURL.openConnection();
    myURLConnection.connect();
} 
catch (MalformedURLException e) { 
    // new URL() failed
    // ...
} 
catch (IOException e) {   
    // openConnection() failed
    // ...
}

每次调用此 URL 的协议处理程序的openConnection方法都会创建一个新的URLConnection对象。

您并不总是需要显式调用connect方法来启动连接。依赖于连接的操作,如getInputStreamgetOutputStream等,将在必要时隐式执行连接。

现在您已成功连接到您的 URL,您可以使用URLConnection对象执行诸如从连接中读取或写入的操作。接下来的部分将向您展示如何操作。

从 URLConnection 读取和写入

原文:docs.oracle.com/javase/tutorial/networking/urls/readingWriting.html

URLConnection类包含许多方法,让您可以通过网络与 URL 通信。URLConnection是一个以 HTTP 为中心的类;也就是说,当您使用 HTTP URL 时,其中许多方法才有用。然而,大多数 URL 协议允许您从连接中读取和写入。本节描述了这两个功能。

从 URLConnection 读取

以下程序执行与直接从 URL 读取中显示的URLReader程序相同的功能。

然而,与直接从 URL 获取输入流不同,此程序显式检索URLConnection对象并从连接获取输入流。通过调用getInputStream隐式打开连接。然后,像URLReader一样,此程序在输入流上创建一个BufferedReader并从中读取。粗体语句突出了此示例与先前示例之间的区别:

import java.net.*;
import java.io.*;

public class URLConnectionReader {
    public static void main(String[] args) throws Exception {
        URL oracle = new URL("http://www.oracle.com/");
        URLConnection yc = oracle.openConnection();
        BufferedReader in = new BufferedReader(new InputStreamReader(
                                    yc.getInputStream()));
        String inputLine;
        while ((inputLine = in.readLine()) != null) 
            System.out.println(inputLine);
        in.close();
    }
}

该程序的输出与直接从 URL 打开流的程序的输出相同。您可以使用任一方式从 URL 读取。但是,与直接从 URL 读取不同,从URLConnection读取可能更有用。这是因为您可以同时将URLConnection对象用于其他任务(如向 URL 写入)。

再次,如果程序挂起或出现错误消息,您可能需要设置代理主机,以便程序可以找到 Oracle 服务器。

向 URLConnection 写入

许多 HTML 页面包含表单——文本字段和其他 GUI 对象,让您输入要发送到服务器的数据。在输入所需信息并通过单击按钮启动查询后,您的 Web 浏览器通过网络将数据写入 URL。在另一端,服务器接收数据,处理数据,然后向您发送响应,通常以新的 HTML 页面形式。

这些 HTML 表单中的许多使用 HTTP POST 方法将数据发送到服务器。因此,向 URL 写入通常称为向 URL 发布。服务器识别 POST 请求并读取从客户端发送的数据。

要使 Java 程序与服务器端进程交互,它只需能够写入 URL,从而向服务器提供数据。它可以通过以下步骤实现:

  1. 创建一个URL

  2. 检索URLConnection对象。

  3. URLConnection上设置输出功能。

  4. 打开到资源的连接。

  5. 从连接获取输出流。

  6. 写入输出流。

  7. 关闭输出流。

这里有一个名为ReverseServlet的小servlet(或者如果你更喜欢一个 cgi-bin 脚本)。您可以使用这个 servlet 来测试以下示例程序。

运行在容器中的 servlet 从其 InputStream 读取,反转字符串,并将其写入其 OutputStream。servlet 需要形式为string=string_to_reverse的输入,其中string_to_reverse是您想要以相反顺序显示其字符的字符串。

这是一个通过URLConnection在网络上运行ReverseServlet的示例程序:

import java.io.*;
import java.net.*;

public class Reverse {
    public static void main(String[] args) throws Exception {

        if (args.length != 2) {
            System.err.println("Usage:  java Reverse "
                + "http://<location of your servlet/script>"
                + " string_to_reverse");
            System.exit(1);
        }

        String stringToReverse = URLEncoder.encode(args[1], "UTF-8");

        URL url = new URL(args[0]);
        URLConnection connection = url.openConnection();
        connection.setDoOutput(true);

        OutputStreamWriter out = new OutputStreamWriter(
                                         connection.getOutputStream());
        out.write("string=" + stringToReverse);
        out.close();

        BufferedReader in = new BufferedReader(
                                    new InputStreamReader(
                                    connection.getInputStream()));
        String decodedString;
        while ((decodedString = in.readLine()) != null) {
            System.out.println(decodedString);
        }
        in.close();
    }
}

让我们来检查程序,看看它是如何工作的。首先,程序处理其命令行参数:

if (args.length != 2) {
    System.err.println("Usage:  java Reverse "
        + "http://*<location of your servlet/script>*"
        + " string_to_reverse");
    System.exit(1);
}       

String stringToReverse = URLEncoder.encode(args[1], "UTF-8");

这些语句确保用户向程序提供两个且仅有两个命令行参数。命令行参数是ReverseServlet的位置和将被反转的字符串。它可能包含空格或其他非字母数字字符。这些字符必须进行编码,因为字符串在传输到服务器时会被处理。URLEncoder类的方法对字符进行编码。

接下来,程序创建URL对象,并设置连接以便可以向其写入:

URL url = new URL(args[0]);
URLConnection connection = url.openConnection();
connection.setDoOutput(true);

程序然后在连接上创建一个输出流,并在其上打开一个OutputStreamWriter

OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream());

如果 URL 不支持输出,getOutputStream方法会抛出UnknownServiceException。如果 URL 支持输出,则此方法返回一个连接到服务器端 URL 的输入流的输出流 —— 客户端的输出是服务器的输入。

接下来,程序向输出流写入所需的信息并关闭流:

out.write("string=" + stringToReverse);
out.close();

这段代码使用write方法向输出流写入数据。因此你可以看到,向 URL 写入数据就像向流写入数据一样简单。客户端输出流中写入的数据是服务器端 servlet 的输入。Reverse程序通过在要反转的编码字符串前添加string=来构造脚本所需的输入形式。

servlet 读取您写入的信息,对字符串值执行反转操作,然后将其发送回给您。现在您需要读取服务器发送回的字符串。Reverse程序这样做:

BufferedReader in = new BufferedReader(
                            new InputStreamReader(
                            connection.getInputStream()));
String decodedString;
while ((decodedString = in.readLine()) != null) {
    System.out.println(decodedString);
}
in.close();

如果您的ReverseServlet位于http://example.com/servlet/ReverseServlet,那么当您使用Reverse程序运行时

http://example.com/servlet/ReverseServlet "Reverse Me"

作为参数(包括双引号),你应该看到这个输出:

Reverse Me
 reversed is: 
eM esreveR

教程:关于套接字的一切

原文:docs.oracle.com/javase/tutorial/networking/sockets/index.html

URLURLConnection提供了一个相对高级的机制,用于访问互联网上的资源。有时,您的程序需要更低级别的网络通信,例如,当您想编写一个客户端-服务器应用程序时。

在客户端-服务器应用程序中,服务器提供某些服务,比如处理数据库查询或发送当前股票价格。客户端使用服务器提供的服务,要么将数据库查询结果显示给用户,要么向投资者提供股票购买建议。客户端和服务器之间发生的通信必须是可靠的。也就是说,不能丢失任何数据,并且必须按照服务器发送的顺序到达客户端。

TCP 提供了一个可靠的点对点通信通道,用于互联网上的客户端-服务器应用程序之间的通信。要通过 TCP 进行通信,客户端程序和服务器程序要建立连接。每个程序将套接字绑定到连接的端点。为了通信,客户端和服务器分别从绑定到连接的套接字读取和写入。

什么是套接字?

套接字是网络上运行的两个程序之间的双向通信链路的一个端点。Socket 类用于表示客户端程序和服务器程序之间的连接。java.net 包提供了两个类——Socket 和 ServerSocket——分别实现连接的客户端部分和服务器端部分。

从套接字读取和写入

本页包含一个小例子,演示了客户端程序如何从套接字读取和写入。

编写客户端/服务器对

前一页展示了一个示例,演示了如何编写一个与现有服务器通过 Socket 对象交互的客户端程序。本页将向您展示如何编写一个实现连接的另一端的程序——服务器程序。

什么是套接字?

原文:docs.oracle.com/javase/tutorial/networking/sockets/definition.html

通常,服务器在特定计算机上运行,并具有绑定到特定端口号的套接字。服务器只是等待,监听套接字,等待客户端发出连接请求。

在客户端:客户端知道服务器正在运行的机器的主机名和服务器正在监听的端口号。为了发出连接请求,客户端尝试在服务器的机器和端口上与服务器会合。客户端还需要向服务器标识自己,因此它绑定到一个本地端口号,在此连接期间将使用该端口号。这通常由系统分配。

客户端的连接请求

如果一切顺利,服务器接受连接。接受后,服务器会获得一个新的套接字,绑定到相同的本地端口,并且其远程端点设置为客户端的地址和端口。它需要一个新的套接字,以便在继续监听原始套接字以接受连接请求的同时,满足已连接客户端的需求。

建立连接

在客户端,如果连接被接受,套接字将成功创建,客户端可以使用该套接字与服务器通信。

客户端和服务器现在可以通过写入或从套接字读取来进行通信。


定义:

套接字是网络上两个运行程序之间的双向通信链路的一个端点。套接字绑定到一个端口号,以便 TCP 层可以识别数据要发送到的应用程序。


一个端点是 IP 地址和端口号的组合。每个 TCP 连接可以通过其两个端点唯一标识。这样,您可以在主机和服务器之间建立多个连接。

Java 平台中的java.net包提供了一个类Socket,它实现了您的 Java 程序与网络上另一个程序之间的双向连接的一侧。Socket类位于一个平台相关的实现之上,隐藏了任何特定系统的细节,使您的 Java 程序能够以与平台无关的方式在网络上通信,而不是依赖本机代码。

此外,java.net包含ServerSocket类,它实现了服务器可以用来监听并接受客户端连接的套接字。本课程将向您展示如何使用SocketServerSocket类。

如果你想连接到网络,URL 类及其相关类(URLConnectionURLEncoder)可能比套接字类更合适。实际上,URL 是与网络连接的相对高级的方式,并且在其底层实现中使用了套接字。有关通过 URL 连接到网络的信息,请参阅使用 URL。

从套接字读取和写入

译文:docs.oracle.com/javase/tutorial/networking/sockets/readingWriting.html

让我们看一个简单的示例,说明程序如何使用Socket类与服务器程序建立连接,然后客户端如何通过套接字向服务器发送数据并从服务器接收数据。

该示例程序实现了一个客户端,EchoClient,连接到一个回显服务器。回显服务器从其客户端接收数据并将其回显。示例EchoServer实现了一个回显服务器。(或者,客户端可以连接到支持Echo Protocol的任何主机。)

EchoClient示例创建一个套接字,从而连接到回显服务器。它从标准输入流中读取用户输入,然后通过将文本写入套接字将该文本转发到回显服务器。服务器通过套接字将输入回显到客户端。客户端程序读取并显示从服务器传回的数据。

请注意,EchoClient示例既向其套接字写入数据,也从其套接字读取数据,从而向回显服务器发送数据并接收数据。

让我们逐步走过程序并研究有趣的部分。在EchoClient示例中的try-with-resources 语句中,以下语句至关重要。这些行建立了客户端和服务器之间的套接字连接,并在套接字上打开了一个PrintWriter和一个BufferedReader

String hostName = args[0];
int portNumber = Integer.parseInt(args[1]);

try (
    Socket echoSocket = new Socket(hostName, portNumber);        // 1st statement
    PrintWriter out =                                            // 2nd statement
        new PrintWriter(echoSocket.getOutputStream(), true);
    BufferedReader in =                                          // 3rd statement 
        new BufferedReader(
            new InputStreamReader(echoSocket.getInputStream()));
    BufferedReader stdIn =                                       // 4th statement 
        new BufferedReader(
            new InputStreamReader(System.in))
)

try-with 资源语句中的第一条语句创建了一个新的Socket对象,并命名为echoSocket。此处使用的Socket构造函数需要要连接的计算机的名称和端口号。示例程序使用第一个命令行参数作为计算机的名称(主机名),第二个命令行参数作为端口号。当您在计算机上运行此程序时,请确保您使用的主机名是要连接的计算机的完全限定 IP 名称。例如,如果您的回显服务器在计算机echoserver.example.com上运行,并且正在侦听端口号 7,则如果您想将EchoServer示例用作您的回显服务器,请先从计算机echoserver.example.com运行以下命令:

java EchoServer 7

后续,使用以下命令运行EchoClient示例:

java EchoClient echoserver.example.com 7

try-with 资源语句中的第二个语句获取套接字的输出流,并在其上打开名为outPrintWriter。类似地,第三个语句获取套接字的输入流,并在其上打开名为inBufferedReader。该示例使用读取器和写入器,以便可以通过套接字写入 Unicode 字符。如果您还不熟悉 Java 平台的 I/O 类,请阅读基本 I/O。

程序的下一个有趣部分是while循环。该循环从标准输入流中逐行读取数据,使用在try-with 资源语句中创建的BufferedReader对象stdIn。然后,该循环立即将该行数据通过写入到与套接字连接的PrintWriter发送到服务器:

String userInput;
while ((userInput = stdIn.readLine()) != null) {
    out.println(userInput);
    System.out.println("echo: " + in.readLine());
}

while循环中的最后一个语句从与套接字连接的BufferedReader中读取一行信息。readLine方法会等待服务器将信息回显给EchoClient。当readline返回时,EchoClient将信息打印到标准输出。

while循环会一直持续,直到用户输入结束符号。也就是说,EchoClient示例从用户那里读取输入,将其发送到 Echo 服务器,从服务器获取响应,并显示它,直到达到输入结束。 (您可以通过按下Ctrl-C键来输入结束符号。)然后while循环终止,并且 Java 运行时会自动关闭与套接字和标准输入流连接的读取器和写入器,并关闭与服务器的套接字连接。Java 运行时会自动关闭这些资源,因为它们是在try-with 资源语句中创建的。Java 运行时会按照它们创建的相反顺序关闭这些资源。(这很好,因为连接到套接字的流应该在套接字本身关闭之前关闭。)

该客户端程序非常简单直接,因为回显服务器实现了一个简单的协议。客户端向服务器发送文本,服务器将其回显。当您的客户端程序与更复杂的服务器(如 HTTP 服务器)通信时,您的客户端程序也会更复杂。但是,基本原理与本程序中的相同:

  1. 打开一个套接字。

  2. 打开一个输入流和输出流到套接字。

  3. 根据服务器的协议从流中读取和写入数据。

  4. 关闭流。

  5. 关闭套接字。

仅步骤 3 因客户端而异,取决于服务器。其他步骤基本保持不变。

编写套接字的服务器端

原文:docs.oracle.com/javase/tutorial/networking/sockets/clientServer.html

本部分向您展示如何编写一个服务器和与之配套的客户端。客户端/服务器对中的服务器提供叩叩笑话。叩叩笑话深受儿童喜爱,通常是糟糕双关语的载体。它们的形式如下:

服务器:"叩叩!"

客户端:"谁在那里?"

服务器:"迪克斯特尔。"

客户端:"迪克斯特尔是谁?"

服务器:"迪克斯特尔大厅挂满冬青树枝。"

客户端:"呻吟。"

该示例由两个独立运行的 Java 程序组成:客户端程序和服务器程序。客户端程序由一个类KnockKnockClient实现,与前一部分的EchoClient示例非常相似。服务器程序由两个类实现:KnockKnockServerKnockKnockProtocolKnockKnockServer类似于EchoServer,包含服务器程序的main方法,并负责监听端口、建立连接以及读取和写入套接字。类KnockKnockProtocol提供笑话。它跟踪当前笑话、当前状态(发送叩叩、发送提示等),并根据当前状态返回笑话的各个文本部分。该对象实现了协议——客户端和服务器约定用于通信的语言。

以下部分详细介绍了客户端和服务器中每个类的代码,然后演示如何运行它们。

叩叩笑话服务器

本部分将介绍实现叩叩笑话服务器程序KnockKnockServer的代码。

服务器程序首先通过创建一个新的ServerSocket对象来监听特定端口(请参见以下代码段中的粗体语句)。运行此服务器时,请选择一个尚未用于其他服务的端口。例如,以下命令启动服务器程序KnockKnockServer,使其监听端口 4444:

java KnockKnockServer 4444

服务器程序在try-with-resources 语句中创建ServerSocket对象:

int portNumber = Integer.parseInt(args[0]);

try ( 
    ServerSocket serverSocket = new ServerSocket(portNumber);
    Socket clientSocket = serverSocket.accept();
    PrintWriter out =
        new PrintWriter(clientSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(
        new InputStreamReader(clientSocket.getInputStream()));
) {

ServerSocketjava.net类,提供了客户端/服务器套接字连接的服务器端的系统独立实现。如果ServerSocket的构造函数无法监听指定端口(例如,端口已被使用),则会抛出异常。在这种情况下,KnockKnockServer别无选择,只能退出。

如果服务器成功绑定到其端口,则ServerSocket对象成功创建,服务器继续下一步——接受来自客户端的连接(try-with-resources 语句中的下一条语句):

clientSocket = serverSocket.accept();

accept方法会等待,直到客户端在此服务器的主机和端口上启动并请求连接。(假设您在名为knockknockserver.example.com的计算机上运行了服务器程序KnockKnockServer。)在这个例子中,服务器正在运行在第一个命令行参数指定的端口号上。当请求连接并成功建立连接时,accept 方法会返回一个新的Socket对象,该对象绑定到相同的本地端口,并将其远程地址和远程端口设置为客户端的地址和端口。服务器可以通过这个新的Socket与客户端通信,并继续监听原始ServerSocket上的客户端连接请求。这个程序的特定版本不会监听更多的客户端连接请求。然而,在支持多个客户端中提供了程序的修改版本。

在服务器成功与客户端建立连接后,使用此代码与客户端通信:

try (
    // ...
    PrintWriter out =
        new PrintWriter(clientSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(
        new InputStreamReader(clientSocket.getInputStream()));
) {
    String inputLine, outputLine;

    // Initiate conversation with client
    KnockKnockProtocol kkp = new KnockKnockProtocol();
    outputLine = kkp.processInput(null);
    out.println(outputLine);

    while ((inputLine = in.readLine()) != null) {
        outputLine = kkp.processInput(inputLine);
        out.println(outputLine);
        if (outputLine.equals("Bye."))
            break;
    }

该代码执行以下操作:

  1. 获取套接字的输入和输出流,并在其上打开读取器和写入器。

  2. 通过向套接字写入来与客户端启动通信(以粗体显示)。

  3. 通过读取和写入套接字(while循环)与客户端通信。

第 1 步已经很熟悉。第 2 步以粗体显示,并值得一些评论。上面代码段中的粗体语句启动了与客户端的对话。代码创建了一个KnockKnockProtocol对象,该对象跟踪当前笑话,笑话中的当前状态等。

创建KnockKnockProtocol后,代码调用KnockKnockProtocolprocessInput方法,以获取服务器发送给客户端的第一条消息。在这个例子中,服务器首先说的是“敲门!”。接下来,服务器将信息写入连接到客户端套接字的PrintWriter,从而将消息发送给客户端。

第 3 步在while循环中编码。只要客户端和服务器仍有话要说,服务器就会从套接字中读取并写入,来回发送消息。

服务器通过一个"敲门!敲门!"的方式开始对话,因此服务器必须等待客户端说"谁在那里?"。因此,while循环在从输入流读取时进行迭代。readLine方法会等待客户端通过写入内容到其输出流(服务器的输入流)来做出响应。当客户端做出响应时,服务器将客户端的响应传递给KnockKnockProtocol对象,并要求KnockKnockProtocol对象提供合适的回复。服务器立即通过连接到套接字的输出流将回复发送给客户端,使用println方法。如果从KnockKnockServer对象生成的服务器响应是"再见。",这表示客户端不想再听笑话了,循环结束。

Java 运行时会自动关闭输入和输出流、客户端套接字和服务器套接字,因为它们是在try-with-resources 语句中创建的。

敲门协议

KnockKnockProtocol类实现了客户端和服务器用于通信的协议。该类跟踪客户端和服务器在对话中的位置,并为客户端的陈述提供服务器的响应。KnockKnockProtocol对象包含所有笑话的文本,并确保客户端对服务器的陈述给出正确的回应。当服务器说"敲门!敲门!"时,客户端说"谁在那里?"才是正确的回应。

所有的客户端/服务器对都必须有一些协议来进行通信;否则,来回传递的数据将毫无意义。你自己的客户端和服务器使用的协议完全取决于它们完成任务所需的通信。

敲门客户端

KnockKnockClient类实现了与KnockKnockServer通信的客户端程序。KnockKnockClient基于前一节中的EchoClient程序从套接字读取和写入,应该对您来说有些熟悉。但我们仍然会查看程序并了解客户端中发生的事情,以及与服务器中发生的情况相比较。

当您启动客户端程序时,服务器应该已经在运行并监听端口,等待客户端请求连接。因此,客户端程序的第一步是打开一个连接到指定主机名和端口上运行的服务器的套接字:

String hostName = args[0];
int portNumber = Integer.parseInt(args[1]);

try (
    Socket kkSocket = new Socket(hostName, portNumber);
    PrintWriter out = new PrintWriter(kkSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(
        new InputStreamReader(kkSocket.getInputStream()));
)

在创建套接字时,KnockKnockClient示例使用第一个命令行参数的主机名,即运行服务器程序KnockKnockServer的网络计算机的名称。

KnockKnockClient示例在创建套接字时使用第二个命令行参数作为端口号。这是一个远程端口号——服务器计算机上的端口号——是KnockKnockServer正在监听的端口。例如,以下命令在运行KnockKnockServer程序的计算机上以knockknockserver.example.com作为服务器程序KnockKnockServer的计算机名称,4444 作为远程端口号运行KnockKnockClient示例:

java KnockKnockClient knockknockserver.example.com 4444

客户端的套接字绑定到任何可用的本地端口——客户端计算机上的端口。请记住,服务器也会获得一个新的套接字。如果你使用前面示例中的命令行参数运行KnockKnockClient示例,那么这个套接字将绑定到你运行KnockKnockClient示例的计算机上的本地端口号 4444。服务器的套接字和客户端的套接字已连接。

接下来是实现客户端和服务器之间通信的while循环。服务器先发言,所以客户端必须先听。客户端通过从连接到套接字的输入流中读取来实现这一点。如果服务器发言,它会说“再见”,然后客户端退出循环。否则,客户端将文本显示到标准输出,然后从用户那里读取响应,用户通过标准输入键入。用户键入回车后,客户端通过连接到套接字的输出流将文本发送到服务器。

while ((fromServer = in.readLine()) != null) {
    System.out.println("Server: " + fromServer);
    if (fromServer.equals("Bye."))
        break;

    fromUser = stdIn.readLine();
    if (fromUser != null) {
        System.out.println("Client: " + fromUser);
        out.println(fromUser);
    }
}

当服务器询问客户端是否想听另一个笑话时,通信结束,客户端说不想听,服务器说“再见”。

客户端会自动关闭其输入和输出流以及套接字,因为它们是在try-with-resources 语句中创建的。

运行程序

你必须先启动服务器程序。为此,请使用 Java 解释器运行服务器程序,就像运行任何其他 Java 应用程序一样。指定服务器程序监听的端口号作为命令行参数:

java KnockKnockServer 4444

接下来,运行客户端程序。请注意,你可以在网络上的任何计算机上运行客户端;它不必在与服务器相同的计算机上运行。指定运行KnockKnockServer服务器程序的计算机的主机名和端口号作为命令行参数:

java KnockKnockClient knockknockserver.example.com 4444

如果你太快了,可能会在服务器有机会初始化并开始监听端口之前启动客户端。如果发生这种情况,你将会看到客户端的堆栈跟踪。如果发生这种情况,只需重新启动客户端。

如果在第一个客户端连接到服务器时尝试启动第二个客户端,第二个客户端将会挂起。下一节,支持多个客户端,讨论了支持多个客户端的问题。

当客户端和服务器成功建立连接时,你将在屏幕上看到以下文本显示:

Server: Knock! Knock!

现在,你必须回复:

Who's there?

客户端会回显你输入的内容并将文本发送到服务器。服务器会回复其中一个众多“敲敲”笑话的第一行。现在你的屏幕应该显示如下内容(你输入的文本用粗体表示):

Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip

现在,你回复:

Turnip who?

再次,客户端会回显你输入的内容并将文本发送到服务器。服务器会回复一个笑话的结尾。现在你的屏幕应该显示如下内容:

Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Turnip who?
Client: Turnip who?
Server: Turnip the heat, it's cold in here! Want another? (y/n)   

如果你想听另一个笑话,输入y;如果不想,输入n。如果输入y,服务器会重新开始“敲敲”;如果输入n,服务器会说“再见”,导致客户端和服务器都退出。

如果在任何时候你打错字,KnockKnockServer对象会捕捉到并回复类似于这样的消息:

Server: You're supposed to say "Who's there?"!

然后服务器会重新开始讲笑话:

Server: Try again. Knock! Knock!

注意,KnockKnockProtocol对象对拼写和标点符号很在意,但对大小写不在意。

支持多个客户端

为了保持KnockKnockServer示例简单,我们设计它来监听并处理单个连接请求。然而,多个客户端请求可以进入同一端口,因此也进入同一个ServerSocket。客户端连接请求在端口处排队,因此服务器必须按顺序接受连接。然而,服务器可以通过使用线程同时为它们提供服务——每个客户端连接一个线程。

这样一个服务器的基本逻辑流程是这样的:

while (true) {
    accept a connection;
    create a thread to deal with the client;
}

线程根据需要从客户端连接读取和写入。


试一试:

修改KnockKnockServer以便能够同时为多个客户端提供服务。我们的解决方案由两个类组成:KKMultiServerKKMultiServerThreadKKMultiServer永远循环,监听ServerSocket上的客户端连接请求。当有请求进来时,KKMultiServer接受连接,创建一个新的KKMultiServerThread对象来处理它,将从 accept 返回的套接字传递给它,并启动线程。然后服务器继续监听连接请求。KKMultiServerThread对象通过读取和写入套接字与客户端通信。运行新的“敲敲”服务器KKMultiServer,然后依次运行几个客户端。


教训:关于数据报的一切

原文:docs.oracle.com/javase/tutorial/networking/datagrams/index.html

您编写的一些用于在网络上通信的应用程序不需要 TCP 提供的可靠的点对点通道。相反,您的应用程序可能会从一种通信模式中受益,该模式传递独立的信息包,其到达和到达顺序不受保证。

UDP 协议提供了一种网络通信模式,应用程序可以向彼此发送数据包,称为数据报。数据报是通过网络发送的独立、自包含的消息,其到达、到达时间和内容均不受保证。java.net包中的DatagramPacketDatagramSocket类使用 UDP 实现了系统独立的数据报通信。

什么是数据报?

数据报是通过网络发送的独立、自包含的消息,其到达、到达时间和内容均不受保证。

编写数据报客户端和服务器

本节将通过一个示例引导您,其中包含两个使用数据报进行通信的 Java 程序。服务器端是一个引用服务器,它监听其DatagramSocket并在客户端请求时向客户端发送引用。客户端是一个简单的程序,只需向服务器发出请求。

向多个接收方广播

本节修改引用服务器,使其不再在请求时向单个客户端发送引用,而是每分钟向所有正在侦听的客户端广播一条引用。客户端程序必须相应地进行修改。


注意:

许多防火墙和路由器被配置为不允许 UDP 数据包。如果您在防火墙外部连接到服务时遇到问题,或者客户端无法连接到您的服务,请向系统管理员询问是否允许 UDP。


什么是数据报?

原文:docs.oracle.com/javase/tutorial/networking/datagrams/definition.html

客户端和服务器通过可靠的通道(如 TCP 套接字)进行通信,它们之间有一个专用的点对点通道,或者至少有这种幻觉。为了通信,它们建立连接,传输数据,然后关闭连接。所有通过通道发送的数据都按照发送顺序接收。这由通道保证。

相反,通过数据报通信的应用程序发送和接收完全独立的信息包。这些客户端和服务器没有也不需要专用的点对点通道。数据报的传递到目的地不被保证。它们的到达顺序也不被保证。


定义:

一个数据报是独立的、自包含的消息,通过网络发送,其到达、到达时间和内容都不被保证。


java.net包含三个类,帮助你编写使用数据报在网络上传输和接收数据包的 Java 程序:DatagramSocketDatagramPacketMulticastSocket。一个应用程序可以通过DatagramSocket发送和接收DatagramPacket。此外,DatagramPacket可以广播到多个接收者,所有接收者都监听MulticastSocket

编写数据报客户端和服务器

原文:docs.oracle.com/javase/tutorial/networking/datagrams/clientServer.html

本节中展示的示例由两个应用程序组成:客户端和服务器。服务器持续接收数据报包,每个数据报包都表示客户端请求报价。当服务器接收到数据报时,它会通过数据报包将一行“此刻引用”发送回客户端。

本示例中的客户端应用程序非常简单。它向服务器发送一个数据报包,指示客户端希望接收此刻引用。然后客户端等待服务器发送数据报包作为响应。

两个类实现了服务器应用程序:QuoteServerQuoteServerThread。一个类实现了客户端应用程序:QuoteClient

让我们从包含服务器应用程序的main方法的类开始研究这些类。使用服务器端应用程序包含QuoteClient类的小程序版本。

引用服务器类

在这里完整显示的QuoteServer类包含一个方法:引用服务器应用程序的main方法。main方法只是创建一个新的QuoteServerThread对象并启动它:

import java.io.*;

public class QuoteServer {
    public static void main(String[] args) throws IOException {
        new QuoteServerThread().start();
    }
}

QuoteServerThread 类实现了引用服务器的主要逻辑。

QuoteServerThread

创建时,QuoteServerThread在端口 4445 上创建了一个DatagramSocket(任意选择)。这是服务器与所有客户端通信的DatagramSocket

public QuoteServerThread() throws IOException {
    this("QuoteServer");
}

public QuoteServerThread(String name) throws IOException {
    super(name);
    socket = new DatagramSocket(4445);

    try {
        in = new BufferedReader(new FileReader("one-liners.txt"));
    }   
    catch (FileNotFoundException e){
        System.err.println("Couldn't open quote file.  Serving time instead.");
    }
}  

请记住,某些端口专用于已知服务,您不能使用它们。如果指定一个正在使用的端口,DatagramSocket的创建将失败。

构造函数还在名为one-liners.txt的文件上打开了一个BufferedReader,其中包含引用列表。文件中的每个引用都在单独的一行上。

现在来看QuoteServerThread的有趣部分:它的run方法。run方法覆盖了Thread类中的run方法,并为线程提供了实现。有关线程的信息,请参见定义和启动线程。

run 方法包含一个while循环,只要文件中还有更多引用,循环就会继续。在循环的每次迭代中,线程会等待一个DatagramPacket通过DatagramSocket到达。数据包表示客户端的请求。作为对客户端请求的响应,QuoteServerThread从文件中获取引用,将其放入DatagramPacket中,并通过DatagramSocket将其发送到请求的客户端。

让我们首先看一下接收客户端请求的部分:

byte[] buf = new byte[256];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);

第一条语句创建了一个字节数组,然后用它创建了一个DatagramPacketDatagramPacket将用于从套接字接收数据报,因为使用了创建它的构造函数。这个构造函数只需要两个参数:一个包含客户端特定数据的字节数组和字节数组的长度。在构造要通过DatagramSocket发送的DatagramPacket时,还必须提供数据包目的地的互联网地址和端口号。当我们讨论服务器如何响应客户端请求时,你会看到这一点。

前一个代码片段中的最后一条语句从套接字接收一个数据报(从客户端接收的信息被复制到数据包中)。receive方法会一直等待,直到接收到一个数据包。如果没有接收到数据包,服务器将不会有进一步的进展,只会等待。

现在假设,服务器收到了客户端请求报价的请求。现在服务器必须做出响应。run方法中的这部分代码构建了响应:

String dString = null;
if (in == null)
    dString = new Date().toString();
else
    dString = getNextQuote();
buf = dString.getBytes();

如果由于某种原因未打开报价文件,则in等于 null。如果是这种情况,报价服务器将提供当天的时间。否则,报价服务器从已打开的文件中获取下一个报价。最后,代码将字符串转换为字节数组。

现在,run方法使用以下代码向客户端通过DatagramSocket发送响应:

InetAddress address = packet.getAddress();
int port = packet.getPort();
packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);

这段代码中的前两条语句分别从客户端接收的数据报中获取了互联网地址和端口号。互联网地址和端口号指示了数据报包的来源。这是服务器必须发送响应的地方。在这个例子中,数据报包的字节数组不包含相关信息。根据报包的到达本身就表示了来自互联网地址和端口号所指示的客户端的请求。

第三条语句创建了一个新的DatagramPacket对象,用于通过数据报套接字发送数据报消息。你可以通过创建它的构造函数来判断新的DatagramPacket是用于通过套接字发送数据的。这个构造函数需要四个参数。前两个参数与用于创建接收数据报的构造函数相同:包含从发送方到接收方的消息的字节数组和该数组的长度。接下来的两个参数不同:一个互联网地址和一个端口号。这两个参数是数据报包的目的地的完整地址,必须由数据报的发送方提供。最后一行代码将DatagramPacket发送出去。

当服务器从报价文件中读取了所有报价后,while循环终止,run方法进行清理:

socket.close();

QuoteClient 类

QuoteClient类实现了QuoteServer的客户端应用程序。此应用程序向QuoteServer发送请求,等待响应,并在接收到响应时将其显示到标准输出。让我们详细看看代码。

QuoteClient类包含一个方法,即客户端应用程序的main方法。main方法的顶部声明了几个局部变量供其使用:

int port;
InetAddress address;
DatagramSocket socket = null;
DatagramPacket packet;
byte[] sendBuf = new byte[256];

首先,main方法处理用于调用QuoteClient应用程序的命令行参数:

if (args.length != 1) {
    System.out.println("Usage: java QuoteClient <hostname>");
    return;
}

QuoteClient应用程序需要一个命令行参数:运行QuoteServer的机器的名称。

接下来,main方法创建一个DatagramSocket

DatagramSocket socket = new DatagramSocket();

客户端使用一个不需要端口号的构造函数。此构造函数只是将DatagramSocket绑定到任何可用的本地端口。客户端绑定到的端口号并不重要,因为DatagramPacket包含寻址信息。服务器从DatagramPacket中获取端口号并将其响应发送到该端口。

接下来,QuoteClient程序向服务器发送一个请求:

byte[] buf = new byte[256];
InetAddress address = InetAddress.getByName(args[0]);
DatagramPacket packet = new DatagramPacket(buf, buf.length, 
                                address, 4445);
socket.send(packet);

代码段获取命令行上命名的主机的 Internet 地址(假设服务器运行的机器的名称)。然后使用此InetAddress和端口号 4445(服务器用于创建其DatagramSocket的端口号)创建一个发送到该 Internet 地址和端口号的DatagramPacket。因此,DatagramPacket将被传递到引用服务器。

请注意,代码创建了一个带有空字节数组的DatagramPacket。字节数组为空,因为此数据报包仅仅是向服务器请求信息。服务器发送响应所需的所有信息—地址和端口号—自动包含在数据包中。

接下来,客户端从服务器获取响应并显示出来:

packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("Quote of the Moment: " + received);

要从服务器获取响应,客户端创建一个“接收”数据包,并使用DatagramSocket的接收方法从服务器接收回复。接收方法会等待直到一个发送给客户端的数据包通过套接字传输过来。请注意,如果服务器的回复某种原因丢失,客户端将永远等待,因为数据报模型没有保证策略。通常,客户端会设置一个计时器,以便不会永远等待回复;如果没有收到回复,计时器会触发,客户端会重新发送。

当客户端从服务器接收到回复时,客户端使用getData方法从数据包中检索数据。然后客户端将数据转换为字符串并显示出来。

运行服务器和客户端

在成功编译服务器和客户端程序之后,您可以运行它们。必须先运行服务器程序。只需使用 Java 解释器并指定QuoteServer类名。

一旦服务器启动,你可以运行客户端程序。记得用一个命令行参数来运行客户端程序:QuoteServer运行的主机名。

当客户端发送请求并从服务器接收到响应后,你应该看到类似于以下输出:

Quote of the Moment:
Good programming is 99% sweat and 1% coffee.

向多个接收者广播

原文:docs.oracle.com/javase/tutorial/networking/datagrams/broadcasting.html

除了DatagramSocket,它让程序之间可以发送数据包,java.net 还包括一个名为MulticastSocket的类。这种类型的套接字用于客户端监听服务器广播给多个客户端的数据包。

让我们重写引用服务器,使其可以向多个接收者广播DatagramPacket。而不是向请求的特定客户端发送引用,新服务器现在需要定期广播引用。客户端需要进行修改,以便被动地监听引用,并在MulticastSocket上这样做。

这个示例由三个类组成,这三个类是前一个示例中三个类的修改版本:MulticastServerMulticastServerThread,和MulticastClient。本讨论重点介绍了这些类的有趣部分。

这是服务器主程序的新版本。这段代码与之前版本QuoteServer的不同之处用粗体显示出来:

import java.io.*;

public class MulticastServer {
    public static void main(String[] args) throws IOException {
        new MulticastServerThread().start();
    }
}

基本上,服务器得到了一个新的名称,并创建了一个MulticastServerThread而不是QuoteServerThread。现在让我们看看MulticastServerThread,其中包含服务器的核心。这是它的类声明:

public class MulticastServerThread extends QuoteServerThread {
    // ...
}

我们将这个类作为QuoteServerThread的子类,以便它可以使用构造函数,并继承一些成员变量和getNextQuote方法。回想一下,QuoteServerThread创建了一个绑定到端口 4445 的DatagramSocket并打开引用文件。在这个示例中,DatagramSocket的端口号实际上并不重要,因为客户端从不向服务器发送任何内容。

MulticastServerThread中唯一显式实现的方法是其run方法。这个run方法与QuoteServerThread中的方法的不同之处用粗体显示出来:

public void run() {
    while (moreQuotes) {
        try {
            byte[] buf = new byte[256];
            // don't wait for request...just send a quote

            String dString = null;
            if (in == null)
                dString = new Date().toString();
            else
                dString = getNextQuote();
            buf = dString.getBytes();

            InetAddress group = InetAddress.getByName("203.0.113.0");
            DatagramPacket packet;
            packet = new DatagramPacket(buf, buf.length, group, 4446);
            socket.send(packet);

            try {
                sleep((long)Math.random() * FIVE_SECONDS);
            } 
            catch (InterruptedException e) { }
        }
        catch (IOException e) {
            e.printStackTrace();
            moreQuotes = false;
        }
    }
    socket.close();
}

有趣的变化是如何构造DatagramPacket,特别是用于构造DatagramPacketInetAddress和端口。回想一下,前一个示例从客户端发送到服务器的数据包中检索了InetAddress和端口号。这是因为服务器需要直接回复给客户端。现在,服务器需要向多个客户端发送数据。因此,这次InetAddress和端口号都是硬编码的。

硬编码的端口号是 4446(客户端必须将MulticastSocket绑定到此端口)。DatagramPacket的硬编码InetAddress是"203.0.113.0",是一个组标识符(而不是单个客户端所在机器的互联网地址)。这个特定地址是从保留给此目的的地址中任意选择的。

以这种方式创建的DatagramPacket将发送到所有监听端口号为 4446 且属于"203.0.113.0"组的客户端。

要监听端口号 4446,新创建的客户端程序只需使用该端口号创建其MulticastSocket。要成为"203.0.113.0"组的成员,客户端调用MulticastSocketjoinGroup方法,并提供标识该组的InetAddress。现在,客户端已设置好接收发送到指定端口和组的DatagramPacket。以下是新客户端程序中相关的代码(还对其进行了重写,以 passively 接收引用而不是主动请求)。粗体语句是与MulticastSocket交互的语句:

MulticastSocket socket = new MulticastSocket(4446);
InetAddress group = InetAddress.getByName("203.0.113.0");
socket.joinGroup(group);

DatagramPacket packet;
for (int i = 0; i < 5; i++) {
    byte[] buf = new byte[256];
    packet = new DatagramPacket(buf, buf.length);
    socket.receive(packet);

    String received = new String(packet.getData());
    System.out.println("Quote of the Moment: " + received);
}

socket.leaveGroup(group);
socket.close();

注意,服务器使用DatagramSocket广播客户端接收的DatagramPacket,而客户端使用MulticastSocket。或者,它可以使用MulticastSocket。服务器用于发送DatagramPacket的套接字并不重要。在广播数据包时重要的是DatagramPacket中包含的寻址信息,以及客户端用于监听的套接字


试试这个:

运行MulticastServer和几个客户端。观察客户端如何都收到相同的引用。


课程:程序化访问网络参数

原文:docs.oracle.com/javase/tutorial/networking/nifs/index.html

系统通常会同时运行多个活动网络连接,比如有线以太网,802.11 b/g(无线)和蓝牙。一些应用程序可能需要访问这些信息,以便在特定连接上执行特定的网络活动。

java.net.NetworkInterface 类提供了访问这些信息的途径。

本课程将指导您了解此类的一些常见用法,并提供列出机器上所有网络接口及其 IP 地址和状态的示例。

什么是网络接口?

本页面描述了一个网络接口,并解释了为什么您可能想要使用它。

检索网络接口

本页面包含一个示例,演示了客户端程序如何检索机器上的所有网络接口。

列出网络接口地址

本页面向您展示如何列出分配给机器上所有网络接口的 IP 地址。

网络接口参数

本页面向您展示如何确定网络接口是否正在运行,或者网络接口是环回接口、点对点接口还是虚拟接口。您还可以了解如何确定接口是否支持多播。

什么是网络接口?

原文:docs.oracle.com/javase/tutorial/networking/nifs/definition.html

网络接口是计算机与私有或公共网络之间的连接点。网络接口通常是网络接口卡(NIC),但不一定要有物理形式。相反,网络接口可以在软件中实现。例如,环回接口(IPv4 的127.0.0.1和 IPv6 的::1)不是物理设备,而是模拟网络接口的软件部分。环回接口通常用于测试环境。

java.net.NetworkInterface类代表两种类型的接口。

NetworkInterface对于具有多个 NIC 的多宿主系统非常有用。使用NetworkInterface,您可以指定要用于特定网络活动的 NIC。

例如,假设您有一台配置了两个 NIC 的机器,并且您想向服务器发送数据。您可以这样创建一个套接字:

Socket soc = new java.net.Socket();
soc.connect(new InetSocketAddress(address, port));

发送数据时,系统会确定使用哪个接口。但是,如果您有偏好或需要指定要使用的 NIC,您可以查询系统以找到适当的接口并在您想要使用的接口上找到一个地址。当您创建套接字并将其绑定到该地址时,系统将使用相关联的接口。例如:

NetworkInterface nif = NetworkInterface.getByName("bge0");
Enumeration<InetAddress> nifAddresses = nif.getInetAddresses();

Socket soc = new java.net.Socket();
soc.bind(new InetSocketAddress(nifAddresses.nextElement(), 0));
soc.connect(new InetSocketAddress(address, port));

您还可以使用NetworkInterface来识别要加入多播组的本地接口。例如:

NetworkInterface nif = NetworkInterface.getByName("bge0");
MulticastSocket ms = new MulticastSocket();
ms.joinGroup(new InetSocketAddress(hostname, port), nif);

NetworkInterface可以与 Java API 一起以许多其他方式使用,超出了此处描述的两种用途。

检索网络接口

原文:docs.oracle.com/javase/tutorial/networking/nifs/retrieving.html

NetworkInterface 类没有公共构造函数。因此,您不能只使用new运算符创建此类的新实例。相反,提供了以下静态方法,以便您可以从系统中检索接口详细信息:getByInetAddress()getByName()getNetworkInterfaces()。前两种方法用于当您已经知道特定接口的 IP 地址或名称时。第三种方法,getNetworkInterfaces() 返回计算机上所有接口的完整列表。

网络接口可以按层次结构组织。NetworkInterface 类包括两个方法,getParent()getSubInterfaces(),这些方法与网络接口层次结构相关。getParent() 方法返回接口的父NetworkInterface。如果网络接口是子接口,则getParent() 返回一个非空值。getSubInterfaces() 方法返回网络接口的所有子接口。

以下示例程序列出了计算机上所有网络接口和子接口(如果存在)的名称。

import java.io.*;
import java.net.*;
import java.util.*;
import static java.lang.System.out;

public class ListNIFs 
{
    public static void main(String args[]) throws SocketException {
        Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();

        for (NetworkInterface netIf : Collections.list(nets)) {
            out.printf("Display name: %s\n", netIf.getDisplayName());
            out.printf("Name: %s\n", netIf.getName());
            displaySubInterfaces(netIf);
            out.printf("\n");
        }
    }

    static void displaySubInterfaces(NetworkInterface netIf) throws SocketException {
        Enumeration<NetworkInterface> subIfs = netIf.getSubInterfaces();

        for (NetworkInterface subIf : Collections.list(subIfs)) {
            out.printf("\tSub Interface Display name: %s\n", subIf.getDisplayName());
            out.printf("\tSub Interface Name: %s\n", subIf.getName());
        }
     }
}  

以下是示例程序的样本输出:

Display name: bge0
Name: bge0
    Sub Interface Display name: bge0:3
    Sub Interface Name: bge0:3
    Sub Interface Display name: bge0:2
    Sub Interface Name: bge0:2
    Sub Interface Display name: bge0:1
    Sub Interface Name: bge0:1

Display name: lo0
Name: lo0

列出网络接口地址

原文:docs.oracle.com/javase/tutorial/networking/nifs/listing.html

从网络接口获取的最有用的信息之一是分配给它的 IP 地址列表。您可以通过使用两种方法之一从NetworkInterface实例中获取此信息。第一种方法getInetAddresses()返回一个InetAddressEnumeration。另一种方法getInterfaceAddresses()返回一个java.net.InterfaceAddress实例的列表。当您需要有关接口地址的更多信息时,可以使用此方法。例如,当地址是 IPv4 地址时,您可能需要有关子网掩码和广播地址的附加信息,以及在 IPv6 地址的情况下的网络前缀长度。

以下示例程序列出了计算机上所有网络接口及其地址:

import java.io.*;
import java.net.*;
import java.util.*;
import static java.lang.System.out;

public class ListNets {

    public static void main(String args[]) throws SocketException {
        Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
        for (NetworkInterface netint : Collections.list(nets))
            displayInterfaceInformation(netint);
    }

    static void displayInterfaceInformation(NetworkInterface netint) throws SocketException {
        out.printf("Display name: %s\n", netint.getDisplayName());
        out.printf("Name: %s\n", netint.getName());
        Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();
        for (InetAddress inetAddress : Collections.list(inetAddresses)) {
            out.printf("InetAddress: %s\n", inetAddress);
        }
        out.printf("\n");
     }
}  

以下是示例程序的输出样本:

Display name: TCP Loopback interface
Name: lo
InetAddress: /127.0.0.1

Display name: Wireless Network Connection
Name: eth0
InetAddress: /192.0.2.0

网络接口参数

原文:docs.oracle.com/javase/tutorial/networking/nifs/parameters.html

你可以访问有关网络接口的网络参数,除了分配给它的名称和 IP 地址之外。

你可以使用isUP()方法来发现网络接口是否处于“up”(即运行)状态。以下方法指示网络接口类型:

  • isLoopback()指示网络接口是否为环回接口。

  • isPointToPoint()指示接口是否为点对点接口。

  • isVirtual()指示接口是否为虚拟接口。

supportsMulticast()方法指示网络接口是否支持多播。getHardwareAddress()方法在可用时返回网络接口的物理硬件地址,通常称为 MAC 地址。getMTU()方法返回最大传输单元(MTU),即最大数据包大小。

以下示例在列出网络接口地址的基础上,通过添加本页描述的额外网络参数进行了扩展:

import java.io.*;
import java.net.*;
import java.util.*;
import static java.lang.System.out;

public class ListNetsEx {

    public static void main(String args[]) throws SocketException {
        Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
        for (NetworkInterface netint : Collections.list(nets))
            displayInterfaceInformation(netint);
    }

    static void displayInterfaceInformation(NetworkInterface netint) throws SocketException {
        out.printf("Display name: %s\n", netint.getDisplayName());
        out.printf("Name: %s\n", netint.getName());
        Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();

        for (InetAddress inetAddress : Collections.list(inetAddresses)) {
            out.printf("InetAddress: %s\n", inetAddress);
        }

        out.printf("Up? %s\n", netint.isUp());
        out.printf("Loopback? %s\n", netint.isLoopback());
        out.printf("PointToPoint? %s\n", netint.isPointToPoint());
        out.printf("Supports multicast? %s\n", netint.supportsMulticast());
        out.printf("Virtual? %s\n", netint.isVirtual());
        out.printf("Hardware address: %s\n",
                    Arrays.toString(netint.getHardwareAddress()));
        out.printf("MTU: %s\n", netint.getMTU());
        out.printf("\n");
     }
}  

以下是示例程序的输出样本:

Display name: bge0
Name: bge0
InetAddress: /fe80:0:0:0:203:baff:fef2:e99d%2
InetAddress: /129.156.225.59
Up? true
Loopback? false
PointToPoint? false
Supports multicast? false
Virtual? false
Hardware address: [0, 3, 4, 5, 6, 7]
MTU: 1500

Display name: lo0
Name: lo0
InetAddress: /0:0:0:0:0:0:0:1%1
InetAddress: /127.0.0.1
Up? true
Loopback? true
PointToPoint? false
Supports multicast? false
Virtual? false
Hardware address: null
MTU: 8232

课程:处理 Cookies

原文:docs.oracle.com/javase/tutorial/networking/cookies/index.html

虽然您可能已经熟悉 cookies,但您可能不知道如何在您的 Java 应用程序中充分利用它们。本课程将引导您了解 cookies 的概念,并解释如何设置 cookie 处理程序,以便您的 HTTP URL 连接将使用它。

Java SE 为此功能提供了一个主要类,java.net.CookieHandler,以及以下支持类和接口:java.net.CookieManagerjava.net.CookiePolicyjava.net.CookieStore,和 java.net.HttpCookie

使用 Cookies 进行 HTTP 状态管理

本页面描述了 cookies 并解释了它们如何用于提供会话。

CookieHandler 回调机制

当您访问网站时,本页面解释了 cookie 处理程序是如何被调用以及如何设置 cookie 处理程序。

默认 CookieManager

Java SE 提供了一个默认的 cookie 处理程序实现,在大多数情况下都足够使用,并且高度可定制。

自定义 CookieManager

这里有一些如何自定义 cookie 策略并编写自己的 cookie 存储的示例。

使用 Cookies 进行 HTTP 状态管理

原文:docs.oracle.com/javase/tutorial/networking/cookies/definition.html

HTTP 状态管理机制指定了一种通过 HTTP 请求和响应创建有状态会话的方式。

通常,HTTP 请求/响应对是相互独立的。然而,状态管理机制使得可以交换状态信息的客户端和服务器将这些对放入更大的上下文中,这被称为会话。用于创建和维护会话的状态信息被称为cookie

一个 cookie 是可以存储在浏览器缓存中的数据片段。如果您访问一个网站然后再次访问它,cookie 数据可以用来识别您为回访者。Cookies 使得状态信息,比如在线购物车,可以被记住。一个 cookie 可以是短期的,保存数据一个网页会话,也就是直到您关闭浏览器,或者一个 cookie 可以是长期的,保存数据一周或一年。

关于 HTTP 状态管理的更多信息,请参阅RFC 2965: HTTP 状态管理机制

CookieHandler 回调机制

原文:docs.oracle.com/javase/tutorial/networking/cookies/cookiehandler.html

Java SE 中通过 java.net.CookieHandler 类实现了 HTTP 状态管理。CookieHandler 对象提供了一个回调机制,在 HTTP 协议处理程序中提供 HTTP 状态管理策略的实现。也就是说,使用 HTTP 协议的 URL,例如 new URL("http://example.com"),将使用 HTTP 协议处理程序。如果设置了 CookieHandler 对象,此协议处理程序将回调到 CookieHandler 对象以处理状态管理。

CookieHandler 类是一个抽象类,具有两对相关方法。第一对方法,getDefault()setDefault(cookieHandler),是静态方法,允许您发现当前安装的处理程序并安装自己的处理程序。

没有安装默认处理程序,安装处理程序是在系统范围内进行的。对于在安全环境中运行的应用程序,即已安装了安全管理器的应用程序,您必须具有特殊权限才能获取和设置处理程序。有关更多信息,请参阅 java.net.CookieHandler.getDefault

第二对相关方法,put(uri, responseHeaders)get(uri, requestHeaders),允许您为指定的 URI 在响应/请求头中设置和获取所有适用的 cookie 到 cookie 缓存中。这些方法是抽象的,CookieHandler 的具体实现必须提供实现。

Java Web Start 和 Java Plug-in 都安装了默认的 CookieHandler。但是,如果您正在运行一个独立的应用程序并希望启用 HTTP 状态管理,您必须设置一个系统范围的处理程序。本课程的下两页将向您展示如何操作。

默认 CookieManager

原文:docs.oracle.com/javase/tutorial/networking/cookies/cookiemanager.html

java.net.CookieManager提供了CookieHandler的具体实现,对于大多数用户来说,足以处理 HTTP 状态管理。CookieManager将 cookie 的存储与接受、拒绝的策略分开。CookieManager通过java.net.CookieStorejava.net.CookiePolicy进行初始化。CookieStore管理 cookie 的存储。CookiePolicy根据策略决定是否接受或拒绝 cookie。

以下代码显示了如何创建并设置系统范围的CookieManager

java.net.CookieManager cm = new java.net.CookieManager();
java.net.CookieHandler.setDefault(cm);

第一行调用默认的CookieManager构造函数创建实例。第二行调用CookieHandler的静态setDefault方法来设置系统范围的处理程序。

默认的CookieManager构造函数创建一个具有默认 cookie 存储和接受策略的新CookieManager实例。CookieStore是存储任何接受的 HTTP cookie 的地方。如果在创建时未指定,CookieManager实例将使用内部的内存实现。这种实现不是持久的,只存在于 Java 虚拟机的生命周期内。需要持久存储的用户必须实现自己的存储。

CookieManager使用的默认 cookie 策略是CookiePolicy.ACCEPT_ORIGINAL_SERVER,只接受来自原始服务器的 cookie。因此,服务器的Set-Cookie响应必须设置“domain”属性,并且必须与 URL 中的主机域匹配。有关更多信息,请参阅java.net.HttpCookie.domainMatches。需要不同策略的用户必须实现CookiePolicy接口,并将其传递给CookieManager构造函数,或者通过使用setCookiePolicy(cookiePolicy)方法将其设置为已构造的CookieManager实例。

在从 cookie 存储中检索 cookie 时,CookieManager还执行来自RFC 2965第 3.3.4 节的路径匹配规则。因此,cookie 还必须设置其“path”属性,以便在从 cookie 存储中检索 cookie 之前应用路径匹配规则。

总之,CookieManager提供了处理 cookie 的框架,并为CookieStore提供了良好的默认实现。通过允许您设置自己的CookieStoreCookiePolicy或两者,CookieManager具有高度的可定制性。

自定义 CookieManager

原文:docs.oracle.com/javase/tutorial/networking/cookies/custom.html

CookieManager 类的两个方面可以定制,即 CookiePolicyCookieStore

CookiePolicy

为方便起见,CookiePolicy 定义了以下预定义的接受 cookie 的策略:

  • CookiePolicy.ACCEPT_ORIGINAL_SERVER 仅接受来自原始服务器的 cookie。

  • CookiePolicy.ACCEPT_ALL 接受所有 cookie。

  • CookiePolicy.ACCEPT_NONE 不接受任何 cookie。

  • 你还可以通过实现 CookiePolicyshouldAccept 方法来定义自己的 cookie 策略。然后,通过将其传递给多参数 CookieManager 构造函数或调用 setCookiePolicy(cookiePolicy) 方法来使用此 CookiePolicy 更改已经存在的 cookie 管理器。

    以下是一个拒绝来自黑名单域的 cookie,然后应用 CookiePolicy.ACCEPT_ORIGINAL_SERVER 策略的示例:

    import java.net.*;
    
    public class BlacklistCookiePolicy implements CookiePolicy {
        String[] blacklist;
    
        public BlacklistCookiePolicy(String[] list) {
            blacklist = list;
        }
    
        public boolean shouldAccept(URI uri, HttpCookie cookie)  {
            String host;
            try {
                host =  InetAddress.getByName(uri.getHost()).getCanonicalHostName();
            } catch (UnknownHostException e) {
                host = uri.getHost();
            }
    
            for (int i = 0; i<blacklist.length; i++) {
    	    if (HttpCookie.domainMatches(blacklist[i], host)) {
                    return false;
                }
            }
    
            return CookiePolicy.ACCEPT_ORIGINAL_SERVER.shouldAccept(uri, cookie);
        }
    }
    
    

    当你创建一个 BlacklistCookiePolicy 实例时,你需要传递一个表示不希望接受来自的域的字符串数组。然后,将此 BlacklistCookiePolicy 实例设置为 CookieManager 的 cookie 策略。例如:

    String[] list = new String[]{ ".example.com" };
    CookieManager cm = new CookieManager(null, new BlacklistCookiePolicy(list));
    CookieHandler.setDefault(cm);
    
    

    此示例代码将不接受来自以下主机的 cookie:

    host.example.com
    domain.example.com
    
    

    然而,此示例代码将接受来自以下主机的 cookie:

    example.com
    example.org
    myhost.example.org
    
    

    CookieStore

    CookieStore 是一个代表存储 cookie 的接口。CookieManager 为每个 HTTP 响应添加 cookie 到 CookieStore,并为每个 HTTP 请求从 CookieStore 检索 cookie。

    你可以实现此接口以提供自己的 CookieStore 并在创建时将其传递给 CookieManager。在创建 CookieManager 实例后无法设置 CookieStore。但是,你可以通过调用 CookieManager.getCookieStore() 来获取对 cookie 存储的引用。这样做很有用,因为它使你能够利用 Java SE 提供的默认内存中 CookieStore 实现,并补充其功能。

    例如,你可能想创建一个持久性 cookie 存储,以便保存 cookie,即使 Java 虚拟机重新启动也可以使用。你的实现方式类似于以下方式:

    1. 之前保存的任何 cookie 都将被读取。

    2. 在运行时,cookie 被存储和检索到内存中。

    3. 在退出之前,cookie 被写入持久存储。

    以下是此 cookie 存储的不完整示例。此示例展示了如何利用 Java SE 默认的内存中 cookie 存储,并如何扩展其功能。

    import java.net.*;
    import java.util.*;
    
    public class PersistentCookieStore implements CookieStore, Runnable {
        CookieStore store;
    
        public PersistentCookieStore() {
            // get the default in memory cookie store
            store = new CookieManager().getCookieStore();
    
            // todo: read in cookies from persistant storage
            // and add them store
    
            // add a shutdown hook to write out the in memory cookies
            Runtime.getRuntime().addShutdownHook(new Thread(this)); 
        }
    
        public void run() {
            // todo: write cookies in store to persistent storage
        }
    
        public void	add(URI uri, HttpCookie cookie) {
            store.add(uri, cookie);
        }
    
        public List<HttpCookie> get(URI uri) {
            return store.get(uri);
        }
    
        public List<HttpCookie> getCookies() {
            return store.getCookies();
        }
    
        public List<URI> getURIs() {
            return store.getURIs();
        }
    
        public boolean remove(URI uri, HttpCookie cookie) {
            return store.remove(uri, cookie);
        }
    
        public boolean removeAll()  {
            return store.removeAll();
        }
    }
    
    

Trail: Bonus

原文:docs.oracle.com/javase/tutorial/extra/index.html

,引入于 J2SE 5.0,是对类型系统的期待已久的增强。它允许类型或方法在各种类型的对象上操作,同时提供编译时类型安全性。它为集合框架添加了编译时类型安全性,并消除了强制转换的繁琐工作。

教你关于 J2SE™版本 1.4 的一个强大功能,允许程序员暂停窗口系统,以便直接在屏幕上进行绘图。编写游戏或其他图形密集型应用程序的程序员应该阅读这个教训。

提供了关于培训资源的信息,并列出了教程中涵盖潜在考试主题的部分。

课程:泛型

原文:docs.oracle.com/javase/tutorial/extra/generics/index.html

作者:吉拉德·布拉查

J2SE 5.0 中引入的这一期待已久的类型系统增强允许类型或方法在提供编译时类型安全性的同时操作各种类型的对象。它为集合框架添加了编译时类型安全性,并消除了强制类型转换的繁琐工作。

介绍

定义简单的泛型

泛型和子类型化

通配符

泛型方法

与旧代码的互操作

详细信息

类文字作为运行时类型标记

更多有趣的通配符

将旧代码转换为使用泛型

致谢

介绍

原文:docs.oracle.com/javase/tutorial/extra/generics/intro.html

JDK 5.0 引入了 Java 编程语言的几个新扩展之一就是泛型

本教程是关于泛型的介绍。您可能熟悉其他语言中类似构造的概念,尤其是 C++的模板。如果是这样,您会发现有相似之处,也有重要的区别。如果您对其他地方类似的构造不熟悉,那就更好了;您可以从头开始,无需消除任何误解。

泛型允许您对类型进行抽象。最常见的例子是容器类型,比如在 Collections 层次结构中的类型。

这是一个典型的使用方式:

List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3        

第 3 行的强制类型转换略显烦人。通常,程序员知道特定列表中放入了什么类型的数据。然而,强制类型转换是必不可少的。编译器只能保证迭代器返回一个Object。为了确保将其分配给类型为Integer的变量是类型安全的,需要进行强制类型转换。

当然,强制类型转换不仅引入了混乱。它还引入了运行时错误的可能性,因为程序员可能会犯错。

如果程序员能够真正表达他们的意图,并将列表标记为仅包含特定数据类型,会怎么样?这就是泛型背后的核心思想。以下是使用泛型给出的程序片段的一个版本:

List<Integer> 
    myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'

注意变量myIntList的类型声明。它指定这不仅仅是一个任意的List,而是一个IntegerList,写作List<Integer>。我们说List是一个接受类型参数的泛型接口--在这种情况下是Integer。我们在创建列表对象时也指定了类型参数。

注意,第 3 行的强制类型转换已经消失了。

现在,您可能会认为我们所做的只是将混乱的地方移动了一下。在第 3 行进行Integer的强制类型转换,我们在第 1 行将Integer作为类型参数。然而,这里有一个非常大的区别。编译器现在可以在编译时检查程序的类型正确性。当我们说myIntList声明为类型List<Integer>时,这告诉我们关于变量myIntList的一些信息,无论何时何地使用它,编译器都会保证这一点。相比之下,强制类型转换告诉我们程序员认为在代码中的某个单一点上是正确的。

整体效果,尤其是在大型程序中,是提高了可读性和健壮性。

posted @ 2024-04-12 15:12  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报