# 编写套接字的服务器端

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

本节介绍如何编写服务器以及随之而来的客户端。客户端/服务器对中的服务器提供 Knock Knock 笑话。 Knock Knock 笑话受到孩子们的青睐，通常是坏双关语的载体。它们是这样的：

**服务器**：“敲门！”
**客户**：“谁在那里？”
**服务器**：“德克斯特。”
**客户端**：“德克斯特是谁？”
**服务器**：“德克斯特大厅里有冬青树枝。”
**客户**：“呻吟。”

该示例包含两个独立运行的 Java 程序：客户端程序和服务器程序。客户端程序由单个类 [`KnockKnockClient`](examples/KnockKnockClient.java) 实现，与上一节中的 [`EchoClient`](examples/EchoClient.java) 示例非常相似。服务器程序由两个类实现： [`KnockKnockServer`](examples/KnockKnockServer.java) 和 [`KnockKnockProtocol`](examples/KnockKnockProtocol.java) 。 `KnockKnockServer`类似于 [`EchoServer`](examples/EchoServer.java) ，包含服务器程序的`main`方法，并执行监听端口，建立连接以及读取和写入套接字的工作。类 [`KnockKnockProtocol`](examples/KnockKnockProtocol.java) 提供了笑话。它跟踪当前的笑话，当前状态（发送敲门声，发送线索等），并根据当前状态返回笑话的各种文本片段。此对象实现协议 - 客户端和服务器已同意用于通信的语言。

以下部分详细介绍了客户端和服务器中的每个类，然后向您展示了如何运行它们。

## 敲击服务器

本节将介绍实现 Knock Knock 服务器程序的代码 [`KnockKnockServer`](examples/KnockKnockServer.java) 。

服务器程序首先创建一个新的 [`ServerSocket`](https://docs.oracle.com/javase/8/docs/api/java/net/ServerSocket.html) 对象来侦听特定端口（请参阅以下代码段中的粗体语句）。运行此服务器时，请选择尚未专用于某些其他服务的端口。例如，此命令启动服务器程序`KnockKnockServer`，以便它侦听端口 4444：

```
java KnockKnockServer 4444

```

服务器程序在`try` -with-resources 语句中创建`ServerSocket`对象：

```
int portNumber = Integer.parseInt(args[0]);

try ( 
    ServerSocket serverSocket = new ServerSocket(portNumber);
    Socket clientSocket = serverSocket.accept();
    PrintWriter out =
        new PrintWriter(clientSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(
        new InputStreamReader(clientSocket.getInputStream()));
) {

```

`ServerSocket`是 [`java.net`](https://docs.oracle.com/javase/8/docs/api/java/net/package-frame.html) 类，它提供与客户端/服务器套接字连接的服务器端的系统无关的实现。如果`ServerSocket`无法侦听指定的端口（例如，端口已被使用），则`ServerSocket`的构造函数将引发异常。在这种情况下，`KnockKnockServer`别无选择，只能退出。

如果服务器成功绑定到其端口，则`ServerSocket`对象成功创建，服务器继续执行下一步 - 接受来自客户端的连接（`try` -with-resources 语句中的下一个语句）：

```
clientSocket = serverSocket.accept();

```

[`accept`](https://docs.oracle.com/javase/8/docs/api/java/net/ServerSocket.html#accept--) 方法等待客户端启动并请求此服务器的主机和端口上的连接。 （假设您在名为`knockknockserver.example.com`的计算机上运行了服务器程序`KnockKnockServer`。）在此示例中，服务器正在运行第一个命令行参数指定的端口号。请求并成功建立连接时，accept 方法返回一个新的 [`Socket`](https://docs.oracle.com/javase/8/docs/api/java/net/Socket.html) 对象，该对象绑定到同一本地端口，并将其远程地址和远程端口设置为客户端的端口。服务器可以通过此新`Socket`与客户端通信，并继续侦听原始`ServerSocket`上的客户端连接请求。此特定版本的程序不会侦听更多客户端连接请求。但是，在[支持多个客户端](#later)中提供了该程序的修改版本。

服务器成功建立与客户端的连接后，它使用以下代码与客户端通信：

```
try (
    // ...
    PrintWriter out =
        new PrintWriter(clientSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(
        new InputStreamReader(clientSocket.getInputStream()));
) {
    String inputLine, outputLine;

    // Initiate conversation with client
    KnockKnockProtocol kkp = new KnockKnockProtocol();
    outputLine = kkp.processInput(null);
    out.println(outputLine);

    while ((inputLine = in.readLine()) != null) {
        outputLine = kkp.processInput(inputLine);
        out.println(outputLine);
        if (outputLine.equals("Bye."))
            break;
    }

```

此代码执行以下操作：

1.  获取套接字的输入和输出流，并在其上打开读者和编写者。
2.  通过写入套接字启动与客户端的通信（以粗体显示）。
3.  通过读取和写入套接字（`while`循环）与客户端进行通信。

第 1 步已经很熟悉了。第 2 步以粗体显示，值得一些评论。上面代码段中的粗体语句启动与客户端的对话。代码创建一个`KnockKnockProtocol`对象 - 跟踪当前笑话的对象，笑话中的当前状态，等等。

创建`KnockKnockProtocol`后，代码调用`KnockKnockProtocol`的`processInput`方法获取服务器发送给客户端的第一条消息。对于这个例子，服务器说的第一件事就是“敲门！敲门！”接下来，服务器将信息写入连接到客户端套接字的 [`PrintWriter`](https://docs.oracle.com/javase/8/docs/api/java/io/PrintWriter.html) ，从而将消息发送到客户端。

步骤 3 在`while`循环中编码。只要客户端和服务器仍然有相互说话的内容，服务器就会读取和写入套接字，在客户端和服务器之间来回发送消息。

服务器用“Knock！Knock！”发起了对话。所以之后服务器必须等待客户说“谁在那里？”结果，`while`循环对输入流的读取进行迭代。 `readLine`方法等待，直到客户端通过向其输出流（服务器的输入流）写入内容来响应。当客户端响应时，服务器将客户端的响应传递给`KnockKnockProtocol`对象，并向`KnockKnockProtocol`对象请求合适的回复。服务器使用对 println 的调用，立即通过连接到套接字的输出流将回复发送到客户端。如果服务器从`KnockKnockServer`对象生成的响应是“Bye”。这表明客户端不再需要笑话和循环退出。

Java 运行时会自动关闭输入和输出流，客户端套接字和服务器套接字，因为它们是在`try` -with-resources 语句中创建的。

## Knock Knock Protocol

[`KnockKnockProtocol`](examples/KnockKnockProtocol.java) 类实现客户端和服务器用于通信的协议。该类跟踪客户端和服务器在对话中的位置，并提供服务器对客户端语句的响应。 `KnockKnockProtocol`对象包含所有笑话的文本，并确保客户端对服务器的语句给出正确的响应。让客户说“德克斯特是谁？”是不行的。当服务器说“敲门！敲门！”

所有客户端/服务器对必须具有一些协议，通过它们相互通信;否则，来回传递的数据将毫无意义。您自己的客户端和服务器使用的协议完全取决于它们完成任务所需的通信。

## 敲敲客户端

[`KnockKnockClient`](examples/KnockKnockClient.java) 类实现与`KnockKnockServer`对话的客户端程序。 `KnockKnockClient`基于上一节[COG3]程序，[读取和写入套接字](readingWriting.html)，应该对您有点熟悉。但是我们还是会检查程序，然后在服务器中发生的情况下查看客户端中发生的情况。

启动客户端程序时，服务器应该已经在运行并监听端口，等待客户端请求连接。因此，客户端程序所做的第一件事就是打开一个连接到运行在指定主机名和端口上的服务器的套接字：

```
String hostName = args[0];
int portNumber = Integer.parseInt(args[1]);

try (
    Socket kkSocket = new Socket(hostName, portNumber);
    PrintWriter out = new PrintWriter(kkSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(
        new InputStreamReader(kkSocket.getInputStream()));
)

```

创建套接字时，`KnockKnockClient`示例使用第一个命令行参数的主机名，即运行服务器程序`KnockKnockServer`的网络上的计算机名称。

`KnockKnockClient`示例在创建套接字时使用第二个命令行参数作为端口号。这是 _ 远程端口号 _ - 服务器计算机上端口号，是`KnockKnockServer`正在侦听的端口。例如，以下命令运行`KnockKnockClient`示例，其中`knockknockserver.example.com`作为运行服务器程序`KnockKnockServer`的计算机的名称，4444 作为远程端口号：

```
java KnockKnockClient knockknockserver.example.com 4444

```

客户端的套接字绑定到客户端计算机上任何可用的 _ 本地端口 _ - 端口。请记住，服务器也会获得一个新的套接字。如果在前面的示例中使用命令行参数运行`KnockKnockClient`示例，则此套接字绑定到运行`KnockKnockClient`示例的计算机上的本地端口号 4444。服务器的套接字和客户端的套接字已连接。

接下来是`while`循环，它实现了客户端和服务器之间的通信。服务器首先说话，所以客户端必须先听。客户端通过读取连接到套接字的输入流来完成此操作。如果服务器说话，它会说“再见”。并且客户端退出循环。否则，客户端将文本显示到标准输出，然后读取用户的响应，用户键入标准输入。用户键入回车符后，客户端通过附加到套接字的输出流将文本发送到服务器。

```
while ((fromServer = in.readLine()) != null) {
    System.out.println("Server: " + fromServer);
    if (fromServer.equals("Bye."))
        break;

    fromUser = stdIn.readLine();
    if (fromUser != null) {
        System.out.println("Client: " + fromUser);
        out.println(fromUser);
    }
}

```

当服务器询问客户是否希望听到另一个笑话，客户端拒绝，并且服务器说“再见”时，通信结束。

客户端自动关闭其输入和输出流以及套接字，因为它们是在`try` -with-resources 语句中创建的。

## 运行程序

您必须首先启动服务器程序。为此，请使用 Java 解释器运行服务器程序，就像使用任何其他 Java 应用程序一样。指定服务器程序侦听的端口号作为命令行参数：

```
java KnockKnockServer 4444

```

接下来，运行客户端程序。请注意，您可以在网络上的任何计算机上运行客户端;它不必与服务器在同一台计算机上运行。指定运行`KnockKnockServer`服务器程序的计算机的主机名和端口号作为命令行参数：

```
java KnockKnockClient knockknockserver.example.com 4444

```

如果您太快，可以在服务器有机会初始化并开始侦听端口之前启动客户端。如果发生这种情况，您将看到来自客户端的堆栈跟踪。如果发生这种情况，请重启客户端。

如果在第一个客户端连接到服务器时尝试启动第二个客户端，则第二个客户端将挂起。下一节[支持多个客户端](#later)，讨论支持多个客户端。

当您成功获得客户端和服务器之间的连接后，您将在屏幕上看到以下文本：

```
Server: Knock! Knock!

```

现在，您必须回复：

```
Who's there?

```

客户端回显您键入的内容并将文本发送到服务器。服务器响应其剧目中众多 Knock Knock 笑话中的第一行。现在你的屏幕应该包含这个（你输入的文字是粗体）：

```
Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip

```

现在，您回复：

```
Turnip who?

```

同样，客户端回应您键入的内容并将文本发送到服务器。服务器以打孔线响应。现在你的屏幕应该包含这个：

```
Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Turnip who?
Client: Turnip who?
Server: Turnip the heat, it's cold in here! Want another? (y/n)   

```

如果你想听另一个笑话，输入 **y** ;如果没有，输入 **n** 。如果您输入 **y** ，服务器将再次以“Knock！Knock！”开始。如果输入 **n** ，服务器会显示“再见”。从而导致客户端和服务器都退出。

如果在任何时候你输入错误，`KnockKnockServer`对象会捕获它，服务器会响应类似这样的消息：

```
Server: You're supposed to say "Who's there?"!

```

然后服务器再次启动笑话：

```
Server: Try again. Knock! Knock!

```

请注意，`KnockKnockProtocol`对象特别关于拼写和标点符号，但与大写不一致。

为了简化`KnockKnockServer`示例，我们将其设计为侦听和处理单个连接请求。但是，多个客户端请求可以进入同一个端口，因此也可以进入相同的`ServerSocket`。客户端连接请求在端口排队，因此服务器必须按顺序接受连接。但是，服务器可以通过使用线程同时为它们提供服务 - 每个客户端连接一个线程。

这种服务器的逻辑基本流程如下：

```
while (true) {
    accept a connection;
    create a thread to deal with the client;
}

```

线程根据需要读取和写入客户端连接。

* * *

**Try This:** 

修改`KnockKnockServer`，使其可以同时为多个客户端提供服务。两个类构成我们的解决方案： [`KKMultiServer`](examples/KKMultiServer.java) 和 [`KKMultiServerThread`](examples/KKMultiServerThread.java) 。 `KKMultiServer`永远循环，在`ServerSocket`上侦听客户端连接请求。当请求进入时，`KKMultiServer`接受连接，创建一个新的`KKMultiServerThread`对象来处理它，将它从 accept 接收的套接字交给它，然后启动该线程。然后服务器返回监听连接请求。 `KKMultiServerThread`对象通过读取和写入套接字与客户端通信。运行新的 Knock Knock 服务器`KKMultiServer`，然后连续运行多个客户端。

* * *