编写套接字的服务器端

本节向您展示如何编写服务器及其附带的 Client 端。Client 端/服务器对中的服务器提供 Knock Knock 笑话。敲敲笑话受到 children 的青睐,通常是破坏 Double 关语的工具。他们像这样去:

服务器 :“敲门!”
Client :“谁在那儿?”
服务器 :“ Dexter”。
Client :“请问谁?”
服务器 :“带有冬青树枝的大厅。”
Client :“格兰”。

该示例由两个独立运行的 Java 程序组成:Client 端程序和服务器程序。Client 端程序由单个类KnockKnockClient实现,与上一节中的EchoClient示例非常相似。服务器程序由两个类实现:KnockKnockServerKnockKnockProtocolKnockKnockServerEchoServer类似,包含用于服务器程序的main方法,并执行侦听端口,构建连接以及从套接字读取和写入套接字的工作。 KnockKnockProtocol班笑话了。它跟踪当前的笑话,当前状态(发送敲敲声,发送线索等),并根据当前状态返回笑话的各种文本。该对象实现了协议,即 Client 端和服务器已同意使用的通信语言。

下一节将详细介绍 Client 端和服务器中的每个类,然后说明如何运行它们。

敲敲服务器

本节介绍实现 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类,它提供 Client 端/服务器套接字连接的服务器端的独立于系统的实现。 ServerSocket的构造函数无法在指定的端口上侦听(例如,该端口已在使用),它将引发异常。在这种情况下,KnockKnockServer别无选择,只能退出。

如果服务器成功绑定到其端口,则将成功创建ServerSocket对象,然后服务器 continue 进行下一步-接受来自 Client 端的连接(try -with-resources 语句中的下一条语句):

clientSocket = serverSocket.accept();

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

服务器成功构建与 Client 端的连接后,它将使用以下代码与 Client 端进行通信:

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

此代码执行以下操作:

  • 获取套接字的 Importing 和输出流,并在其上打开读取器和写入器。

  • 通过写入套接字来启动与 Client 端的通信(以粗体显示)。

  • 通过读取和写入套接字(while循环)与 Client 端进行通信。

步骤 1 已经很熟悉了。步骤 2 以粗体显示,值得一提。上面的代码段中的粗体语句启动与 Client 端的对话。该代码创建一个KnockKnockProtocol对象,该对象跟踪当前的笑话,笑话中的当前状态,等等。

创建KnockKnockProtocol之后,代码将调用KnockKnockProtocolprocessInput方法来获取服务器发送给 Client 端的第一条消息。对于此示例,服务器首先说的是“敲门!敲门!”。接下来,服务器将信息写入连接到 Client 端套接字的PrintWriter,从而将消息发送到 Client 端。

第 3 步编码在while循环中。只要 Client 端和服务器之间还有话要说,服务器就会从套接字读取和写入套接字,从而在 Client 端和服务器之间来回发送消息。

服务器通过“敲门!敲门!”启动对话。因此,之后服务器必须 awaitClient 端说“谁在那儿?”结果,while循环在对 Importing 流的读取中进行迭代。 readLine方法一直等到 Client 端通过向其输出流(服务器的 Importing 流)中写入内容来做出响应。当 Client 端做出响应时,服务器会将 Client 端的响应传递给KnockKnockProtocol对象,并向KnockKnockProtocol对象请求适当的答复。服务器使用对 println 的调用,立即通过连接到套接字的输出流将答复发送给 Client 端。如果从KnockKnockServer对象生成的服务器响应为“再见”。这表明 Client 端不再需要任何笑话,并且退出循环。

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

敲敲协议

KnockKnockProtocol类实现 Client 端和服务器用于通信的协议。此类跟踪 Client 端和服务器在对话中的位置,并提供服务器对 Client 端语句的响应。 KnockKnockProtocol对象包含所有笑话的文本,并确保 Client 端对服务器的语句给出正确的响应。让 Client 说“请 Dexter 是谁?”当服务器显示“敲门!敲门!”时

所有 Client 机/服务器对都必须具有某种协议,通过它们它们可以相互通信。否则,来回传递的数据将毫无意义。您自己的 Client 端和服务器使用的协议完全取决于它们完成任务所需的通信。

敲敲 Client 端

KnockKnockClient类实现与KnockKnockServer对话的 Client 端程序。 KnockKnockClient基于上一节读写套接字EchoClient程序,您应该有点熟悉。但是无论如何,我们还是会仔细研究该程序,并根据服务器中发生的情况查看 Client 端中发生的情况。

启动 Client 端程序时,服务器应该已经在运行并且正在侦听端口,awaitClient 端请求连接。因此,Client 端程序要做的第一件事是打开一个套接字,该套接字连接到在指定主机名和端口上运行的服务器:

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

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

接下来是实现 Client 机与服务器之间通信的while循环。服务器先讲话,所以 Client 端必须先听。Client 端通过从附加到套接字的 Importing 流中读取数据来完成此操作。如果服务器确实讲话,则说“再见”。Client 端退出循环。否则,Client 端将文本显示到标准输出,然后从用户 Importing 标准 Importing 的内容中读取响应。用户键入回车符后,Client 端将通过附加到套接字的输出流将文本发送到服务器。

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

当服务器询问 Client 是否希望听到另一个笑话时,通信结束,Client 说“否”,服务器说“再见”。

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

运行程序

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

java KnockKnockServer 4444

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

java KnockKnockClient knockknockserver.example.com 4444

如果太快,则可能在服务器没有机会初始化自身并开始监听端口之前启动 Client 端。如果发生这种情况,您将看到来自 Client 端的堆栈跟踪。如果发生这种情况,只需重新启动 Client 端即可。

如果在第一个 Client 端连接到服务器时try启动第二个 Client 端,则第二个 Client 端将挂起。下一节支持多个 Client讨论了如何支持多个 Client 端。

成功构建 Client 端与服务器之间的连接后,屏幕上将显示以下文本:

Server: Knock! Knock!

现在,您必须以以下方式回应:

Who's there?

Client 端会回显您键入的内容,并将文本发送到服务器。服务器以其曲目中众多敲门笑话之一的第一行作为响应。现在,您的屏幕应包含以下内容(您键入的文本以粗体显示):

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

现在,您以以下方式回复:

Turnip who?

Client 端再次回显您键入的内容,并将文本发送到服务器。服务器以打孔线响应。现在您的屏幕应包含以下内容:

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)

如果您想听到另一个笑话,请 Importing y ;如果不是,请 Importing n 。如果键入 y ,则服务器再次以“敲门!敲门!”开始。如果键入 n ,服务器将显示“ Bye”。因此导致 Client 端和服务器退出。

如果您在任何时候 Importing 错误,则KnockKnockServer对象会catch到该错误,服务器将以类似于以下的消息进行响应:

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

然后服务器再次开始玩笑:

Server: Try again. Knock! Knock!

请注意,KnockKnockProtocol对象是关于拼写和标点的,但不是大写的。

支持多个 Client 端

为了使KnockKnockServer示例简单,我们将其设计为侦听并处理单个连接请求。但是,多个 Client 端请求可以进入同一个端口,因此也可以进入同一个ServerSocket。Client 端连接请求在端口排队,因此服务器必须 Sequences 接受连接。但是,服务器可以通过使用线程(每个 Client 端连接每个线程一个线程)同时为它们提供服务。

这样的服务器中逻辑的基本流程是:

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

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

Try This:

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