文档

Java™ 教程-Java Tutorials 中文版
编写套接字的服务器端
Trail: Custom Networking
Lesson: All About Sockets

编写套接字的服务器端

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

Server: "Knock knock!"
Client: "Who's there?"
Server: "Dexter."
Client: "Dexter who?"
Server: "Dexter halls with boughs of holly."
Client: "Groan."

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

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

Knock Knock 服务器

本节将介绍实现 Knock Knock 服务器程序的代码,KnockKnockServer

服务器程序首先创建一个新的 ServerSocket 对象以侦听特定端口(请参阅以下代码段中的粗体语句)。运行此服务器时,请选择尚未专用于某些其他服务的端口。例如,此命令启动服务器程序 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()));
) {

ServerSocketjava.net 类,它提供客户端/服务器套接字连接的服务器端的系统无关实现。如果 ServerSocket 的构造函数无法侦听指定的端口(例如,端口已被使用),则会引发异常。在这种情况下,KnockKnockServer 别无选择,只能退出。

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

clientSocket = serverSocket.accept();

accept 方法等待,直到客户端启动并请求此服务器的主机和端口上的连接。(假设你在名为 knockknockserver.example.com 的计算机上运行了服务器程序 KnockKnockServer。)在此示例中,服务器运行在由第一个命令行参数指定端口号上。请求并成功建立连接时,accept 方法返回一个新的 Socket 对象,该对象绑定到同一本地端口并将其远程地址和远程端口设置为客户端。服务器可以通过这个新的 Socket 与客户端通信,并继续监听原始 ServerSocket 上的客户端连接请求。该程序的特定版本不会监听更多客户端连接请求。但是,在 Supporting Multiple Clients 中提供了该程序的修改版本。

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

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 后,代码调用 KnockKnockProtocolprocessInput 方法以获取服务器发送给客户端的第一条消息。对于这个例子,服务器说的第一件事是“Knock!Knock!“接下来,服务器将信息写入连接到客户端套接字的 PrintWriter,从而将消息发送到客户端。

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

服务器发起对话使用 "Knock!Knock!" 所以之后服务器必须等待客户端说”Who's there?“结果,while 循环迭代读取输入流。readLine 方法等待,直到客户端通过向其输出流(服务器的输入流)写入内容来响应。当客户端响应时,服务器将客户端的响应传递给 KnockKnockProtocol 对象,并要求 KnockKnockProtocol 对象进行适当的回复。服务器使用对 println 的调用,立即通过连接到套接字的输出流将回复发送到客户端。如果从 KnockKnockServer 对象生成的服务器响应是 "Bye." 这表明客户端不再需要笑话和循环退出。

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

Knock Knock 协议

KnockKnockProtocol 类实现客户端和服务器用于通信的协议。该类跟踪客户端和服务器在对话中的位置,并提供服务器对客户端语句的响应。KnockKnockProtocol 对象包含所有笑话的文本,并确保客户端对服务器的语句给出正确的响应。让客户说“Dexter who?”是不行的。当服务器说"Knock!Knock!"

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

Knock Knock 客户端

KnockKnockClient 类实现了与 KnockKnockServer 对话的客户端程序。KnockKnockClient 基于上一节中的 EchoClient 程序,Reading from and Writing to a Socket,对你来说应该有些熟悉。但是我们还是会检查程序,然后根据服务器中发生的情况来查看客户端中发生的情况。

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

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 示例在创建套接字时使用第二个命令行参数作为端口号。这是 remote port number (远程端口号) - 服务器计算机上端口号,是 KnockKnockServer 正在侦听的端口。例如,以下命令运行 KnockKnockClient 示例,使用 knockknockserver.example.com 作为运行服务器程序 KnockKnockServer 的计算机的名称和 4444 作为远程端口号:

java KnockKnockClient knockknockserver.example.com 4444

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

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

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

当服务器询问客户是否希望听到另一个笑话,客户端拒绝,并且服务器说 "Bye." 时,通信结束。

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

运行程序

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

java KnockKnockServer 4444

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

java KnockKnockClient knockknockserver.example.com 4444

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

如果在第一个客户端连接到服务器时尝试启动第二个客户端,则第二个客户端将挂起。下一节 Supporting Multiple Clients,讨论支持多个客户端。

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

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,服务器会说 "Bye.",从而导致客户端和服务器都退出。

如果在任何时候你输入错误,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;
}

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


试试这个: 

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



Previous page: Reading from and Writing to a Socket
Next page: All About Datagrams