文档

Java™ 教程
隐藏目录
编写数据报客户端和服务器
路径:自定义网络
课程:关于数据报的一切

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

本节中的示例包括两个应用程序:客户端和服务器。服务器持续通过数据报套接字接收数据报包。服务器接收到的每个数据报包都表示客户端请求报价。服务器接收到数据报时,会通过发送一个包含一行“此刻报价”的数据报包回复客户端。

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

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

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

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("无法打开报价文件。将返回时间。");
    }
}  

请记住,某些端口专门用于众所周知的服务,您不能使用它们。如果指定了一个已被使用的端口,将无法创建DatagramSocket

构造方法还在一个名为one-liners.txt的文件上打开了一个BufferedReader。该文件包含了一系列的引用语。文件中的每个引用语都占据一行。

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

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

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

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

第一条语句创建了一个字节数组,然后用它创建了一个DatagramPacket。由于使用的是构造方法,该DatagramPacket将用于从套接字接收数据报。该构造方法仅需要两个参数:包含客户端特定数据的字节数组和字节数组的长度。在构造要通过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);

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

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

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

socket.close();

QuoteClient类

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

QuoteClient类包含一个方法,即用于客户端应用程序的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,目标是该Internet地址和端口号。因此,DatagramPacket将被传递到报价服务器。

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

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

packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("此刻的名言:" + received);

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

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

运行服务器和客户端

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

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

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

此刻的名言:
好的编程是99%的汗水和1%的咖啡。

上一页: 什么是数据报?
下一页: 广播给多个接收方