编写数据报 Client 端和服务器

本节中的示例包括两个应用程序:Client 端和服务器。服务器通过数据报套接字连续接收数据报包。服务器接收到的每个数据报包都指示 Client 端请求报价。当服务器接收到数据报时,它将通过向 Client 端发送包含单行“当下报价”的数据报包进行答复。

此示例中的 Client 端应用程序非常简单。它将单个数据报包发送到服务器,指示 Client 端希望接收该 Moment 的报价。然后,Client 端 await 服务器发送数据报文包作为响应。

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

让我们研究这些类,从包含用于服务器应用程序的main方法的类开始。 使用服务器端应用程序包含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。这是服务器通过其与所有 Client 端通信的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循环,只要文件中有更多的引号,该循环就会 continue。在循环的每次迭代期间,线程都 awaitDatagramPacket到达DatagramSocket之上。该数据包指示来自 Client 端的请求。响应 Client 端的请求,QuoteServerThread从文件中获取一个报价,并将其放在DatagramPacket中,然后通过DatagramSocket发送给要求它的 Client 端。

让我们首先看一下接收来自 Client 端的请求的部分:

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

第一条语句创建一个字节数组,然后将其用于创建DatagramPacketDatagramPacket将用于从套接字接收数据报,这是因为用于创建数据报的构造函数。此构造函数仅需要两个参数:一个包含 Client 端特定数据的字节数组和该字节数组的 Long 度。构造DatagramPacket以通过DatagramSocket发送时,还必须提供数据包目标的 Internet 地址和端口号。稍后我们讨论服务器如何响应 Client 端请求时,您将看到此信息。

上一个代码片段中的最后一条语句从套接字接收数据报(从 Client 端接收的信息被复制到数据包中)。接收方法永远 await,直到接收到数据包为止。如果未收到任何数据包,则服务器不会 continue 执行任何操作,而是 await。

现在假设服务器已从 Client 端收到报价请求。现在服务器必须响应。 run 方法中的这段代码构造了响应:

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

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

现在,run方法使用以下代码通过DatagramSocket将响应发送到 Client 端:

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

此代码段中的前两个语句分别从 Client 端接收的数据报包中获取 Internet 地址和端口号。 Internet 地址和端口号指示数据报包的来源。这是服务器必须发送其响应的位置。在此示例中,数据报包的字节数组不包含任何相关信息。数据包本身的到来指示来自 Client 端的请求,可以在数据报数据包中指示的 Internet 地址和端口号上找到该请求。

第三条语句创建一个新的DatagramPacket对象,该对象用于通过数据报套接字发送数据报消息。您可以知道新的DatagramPacket旨在通过套接字发送数据,这是因为创建了该构造函数。该构造函数需要四个参数。前两个参数与用于创建接收数据报的构造函数所需的参数相同:一个字节数组,其中包含从发送方到接收方的消息以及此数组的 Long 度。接下来的两个参数是不同的:Internet 地址和端口号。这两个参数是数据报包目标的完整地址,必须由数据报的发送方提供。代码的最后一行按原样发送DatagramPacket

服务器从报价文件中读取所有报价后,while循环终止并且run方法清除:

socket.close();

QuoteClient 类

QuoteClient类为QuoteServer实现了 Client 端应用程序。该应用程序将请求发送到QuoteServer,await 响应,并在收到响应后将其显示到标准输出。让我们详细看一下代码。

QuoteClient类包含一种方法,Client 端应用程序使用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();

Client 端使用不需要端口号的构造函数。此构造函数仅将DatagramSocket绑定到任何可用的本地端口。Client 端绑定到哪个端口都没有关系,因为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 地址和端口号为 Object 地的DatagramPacket。因此,DatagramPacket将交付给报价服务器。

请注意,该代码创建一个具有空字节数组的DatagramPacket。字节数组为空,因为此数据报包只是向服务器请求信息。服务器需要知道的所有信息(发送回复的地址和端口号)自动成为数据包的一部分。

接下来,Client 端从服务器获取响应并显示它:

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);

为了从服务器获得响应,Client 端将创建一个“接收”数据包,并使用DatagramSocket接收方法从服务器接收响应。接收方法 await,直到发往 Client 端的数据报包通过套接字。请注意,如果服务器的答复由于某种原因丢失,则由于数据报模型的无保证策略,Client 端将永远 await。通常,Client 端会设置一个计时器,这样它就不会永远 await 答复。如果没有答复到达,计时器将关闭,Client 端将重新传输。

当 Client 端收到服务器的答复时,Client 端使用 getData 方法从数据包中检索该数据。然后,Client 端将数据转换为字符串 并显示。

运行服务器和 Client 端

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

服务器启动后,即可运行 Client 端程序。请记住使用一个命令行参数运行 Client 端程序:运行QuoteServer的主机的名称。

Client 端发送请求并收到服务器的响应后,您应该看到类似于以下的输出:

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