文档

Java™ 教程-Java Tutorials 中文版
编写数据报客户端和服务器
Trail: Custom Networking
Lesson: All About Datagrams

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

本节中的示例包含两个应用程序:客户端和服务器。服务器通过数据报套接字连续接收数据报包。服务器接收的每个数据报包指示客户端对引语的请求。当服务器收到数据报时,它通过向客户端发送包含一行“quote of the moment”的数据报包来回复。

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

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

让我们从包含服务器应用程序的 main 方法的类开始研究这些类。Working With a Server-Side Application 包含 QuoteClient 类的 applet 版本。

QuoteServer 类

此处完整显示的 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 类

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.

Previous page: What Is a Datagram?
Next page: Broadcasting to Multiple Recipients