Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
本节中的示例包含两个应用程序:客户端和服务器。服务器通过数据报套接字连续接收数据报包。服务器接收的每个数据报包指示客户端对引语的请求。当服务器收到数据报时,它通过向客户端发送包含一行“quote of the moment”的数据报包来回复。
此示例中的客户端应用程序非常简单。它向服务器发送单个数据报包,指示客户端希望接收当前的引语。然后,客户端等待服务器发送数据报包作为响应。
两个类实现服务器应用程序:QuoteServer
和 QuoteServerThread
。单个类实现客户端应用程序:QuoteClient
。
让我们从包含服务器应用程序的 main
方法的类开始研究这些类。Working With a Server-Side Application 包含 QuoteClient
类的 applet 版本。
此处完整显示的 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
,并提供该线程的实现。有关线程的信息,请参阅 Defining and Starting a Thread。
run
方法包含 while
循环,只要文件中有更多引语,该循环就会继续。在循环的每次迭代期间,线程等待 DatagramPacket
通过 DatagramSocket
到达。数据包表示来自客户端的请求。为了响应客户端的请求,QuoteServerThread
从文件中获取引语,将其放在 DatagramPacket
中,并通过 DatagramSocket
将其发送到请求它的客户。
让我们首先看一下接收客户请求的部分:
byte[] buf = new byte[256]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet);
第一个语句创建一个字节数组,然后用于创建 DatagramPacket
。由于用于创建它的构造函数,DatagramPacket
将用于从套接字接收数据报。此构造函数只需要两个参数:包含特定于客户端的数据的字节数组和字节数组的长度。构造 DatagramPacket
以通过 DatagramSocket
发送时,还必须提供数据包目标的 Internet 地址和端口号。稍后当我们讨论服务器如何响应客户端请求时,你会看到这一点。
前一个代码段中的最后一个语句从套接字接收数据报(从客户端接收的信息被复制到数据包中)。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);
此代码段中的前两个语句分别从从客户端接收的数据报包中获取 Internet 地址和端口号。Internet 地址和端口号表示数据报包的来源。这是服务器必须发送其响应的位置。在此示例中,数据报包的字节数组不包含相关信息。数据包本身的到达表示来自客户端的请求,该请求可以在数据报包中指示的因特网地址和端口号处找到。
第三个语句创建一个新的 DatagramPacket
对象,用于通过数据报套接字发送数据报消息。你可以告诉新的 DatagramPacket
旨在通过套接字发送数据,因为用于创建它的构造函数。此构造函数需要四个参数。前两个参数与用于创建接收数据报的构造函数相同:一个字节数组,包含从发送方到接收方的消息以及该数组的长度。接下来的两个参数是不同的:Internet 地址和端口号。这两个参数是数据报包的目的地的完整地址,必须由数据报的发送者提供。最后一行代码在其路上发送 DatagramPacket
。
当服务器读取了引语文件中的所有引语时,while
循环终止并且 run
方法清除:
socket.close();
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
的端口号)来为那个互联网地址和端口号创建 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);
为了从服务器获得响应,客户端创建“receive”数据包并使用 DatagramSocket
接收方法从服务器接收回复。receive 方法等待,直到发往客户端的数据报包通过套接字到来。请注意,如果服务器的回复以某种方式丢失,则客户端将永远等待,因为数据报模型的无保证策略。通常,客户端设置一个计时器,以便它不会永远等待回复;如果没有回复,则计时器关闭,客户端重新发送。
当客户端从服务器收到回复时,客户端使用 getData 方法从数据包中获取该数据。然后,客户端将数据转换为字符串并显示它。
成功编译服务器和客户端程序后,运行它们。你必须先运行服务器程序。只需使用 Java 解释器并指定 QuoteServer
类名。
服务器启动后,你可以运行客户端程序。请记住使用一个命令行参数运行客户端程序:运行 QuoteServer
的主机的名称。
客户端发送请求并从服务器收到响应后,你应该看到与此类似的输出:
Quote of the Moment: Good programming is 99% sweat and 1% coffee.