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

> 原文： [https://docs.oracle.com/javase/tutorial/networking/datagrams/clientServer.html](https://docs.oracle.com/javase/tutorial/networking/datagrams/clientServer.html)

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

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

两个类实现服务器应用程序：`QuoteServer`和`QuoteServerThread`。单个类实现客户端应用程序：`QuoteClient`。

让我们从包含服务器应用程序的`main`方法的类开始研究这些类。 [使用服务器端应用程序](../../deployment/applet/server.html)包含`QuoteClient`类的 applet 版本。

[`QuoteServer`](examples/QuoteServer.java) 类，完整地显示，包含一个方法：报价服务器应用程序的`main`方法。 `main`方法只是创建一个新的`QuoteServerThread`对象并启动它：

```
import java.io.*;

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

```

`QuoteServerThread`类实现报价服务器的主要逻辑。

## `QuoteServerThread`类

创建时， [`QuoteServerThread`](examples/QuoteServerThread.java) 在端口 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`](examples/one-liners.txt)

现在是`QuoteServerThread`的有趣部分：它的`run`方法。 `run`方法覆盖`Thread`类中的`run`并提供线程的实现。有关线程的信息，请参见[定义和启动线程](../../essential/concurrency/runthread.html)。

`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`](examples/QuoteClient.java) 类为`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`接收方法从服务器接收回复。 receive 方法等待，直到发往客户端的数据报包通过套接字。请注意，如果服务器的回复以某种方式丢失，则客户端将永远等待，因为数据报模型的无保证策略。通常，客户端设置一个计时器，以便它不会永远等待回复;如果没有回复，则计时器关闭，客户端重新发送。

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

## 运行服务器和客户端

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

服务器启动后，您可以运行客户端程序。请记住使用一个命令行参数运行客户端程序：运行`QuoteServer`的主机的名称。

客户端发送请求并从服务器收到响应后，您应该看到与此类似的输出：

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

```