文档

Java™教程
隐藏目录
编写Socket的服务器端
路径: 自定义网络
课程: 关于套接字的全部知识

编写套接字的服务器端

本节将向您展示如何编写服务器和与之配套的客户端。客户端/服务器对中的服务器提供“叩叩笑话”服务。叩叩笑话受到儿童的喜爱,通常是糟糕双关语的载体。它们的对话如下:

服务器:“叩叩!”
客户端:“谁在那里?”
服务器:“迪克斯特。”
客户端:“迪克斯特谁?”
服务器:“迪克斯特大厅装饰着冬青树枝。”
客户端:“咕噜。”

这个示例由两个独立运行的Java程序组成:客户端程序和服务器程序。客户端程序由一个类KnockKnockClient实现,它与前一节的EchoClient示例非常相似。服务器程序由两个类KnockKnockServerKnockKnockProtocol实现。KnockKnockServer类与EchoServer类类似,包含服务器程序的main方法,并负责监听端口、建立连接、读写套接字等操作。类KnockKnockProtocol提供笑话。它跟踪当前的笑话、当前状态(发送叩叩、发送线索等等),并根据当前状态返回笑话的各个文本部分。该对象实现了协议-客户端和服务器之间约定使用的通信语言。

接下来的部分将详细介绍客户端和服务器中的每个类,并向您展示如何运行它们。

叩叩服务器

本节将介绍实现叩叩服务器程序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()));
) {

ServerSocket是一个java.net类,提供了一个独立于系统的客户端/服务器套接字连接的服务器端实现。如果构造函数无法监听指定端口(例如,端口已被占用),则会抛出异常。在这种情况下,KnockKnockServer只能退出。

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

clientSocket = serverSocket.accept();

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

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

try (
    // ...
    PrintWriter out =
        new PrintWriter(clientSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(
        new InputStreamReader(clientSocket.getInputStream()));
) {
    String inputLine, outputLine;
            
    // 与客户端初始化对话
    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方法获取服务器发送给客户端的第一条消息。对于这个例子,服务器首先说的是“谁在敲门?”然后,服务器将信息写入连接到客户端套接字的PrintWriter中,从而将消息发送给客户端。

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

服务器用“谁在敲门?”初始化了对话,因此服务器必须等待客户端说“谁在那里?”作为回应。因此,while循环在输入流上进行读取迭代。readLine方法将等待,直到客户端通过写入其输出流(服务器的输入流)作出回应。当客户端回应时,服务器将客户端的回应传递给KnockKnockProtocol对象,并向KnockKnockProtocol对象请求合适的回复。服务器立即通过连接到套接字的输出流,使用println调用将回复发送给客户端。如果服务器从KnockKnockServer对象生成的响应是“再见。”,这表示客户端不想要更多笑话,循环结束。

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

敲敲协议

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

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

敲敲客户端

KnockKnockClient类实现了与KnockKnockServer通信的客户端程序。 KnockKnockClient基于前一节中的EchoClient程序,从套接字读取和写入,对你来说应该有点熟悉。但是我们还是会复习一下程序,并从服务器的角度看客户端中正在发生的事情。

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

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正在监听的端口。例如,以下命令使用knockknockserver.example.com作为运行服务器程序KnockKnockServer的计算机的名称,以及4444作为远程端口号来运行KnockKnockClient示例:

java KnockKnockClient knockknockserver.example.com 4444

客户端的套接字绑定到任何可用的本地端口——客户端计算机上的一个端口。请记住,服务器也会得到一个新的套接字。如果你使用上一个示例中的命令行参数运行KnockKnockClient示例,那么这个套接字将绑定到从中运行KnockKnockClient示例的计算机上的本地端口号4444。服务器的套接字和客户端的套接字连接在一起。

接下来是实现客户端和服务器之间通信的while循环。服务器首先发言,所以客户端必须首先监听。客户端通过从与套接字连接的输入流中读取来实现这一点。如果服务器发言,它会说“再见。”,客户端会退出循环。否则,客户端会将文本显示到标准输出,并从用户读取响应,用户通过标准输入进行输入。用户输入回车后,客户端通过连接到套接字的输出流将文本发送给服务器。

while ((fromServer = in.readLine()) != null) {
    System.out.println("服务器: " + fromServer);
    if (fromServer.equals("再见。"))
        break;

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

当服务器询问客户端是否想听另一个笑话时,通信结束,客户端回答否,服务器说“再见。”

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

运行程序

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

java KnockKnockServer 4444

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

java KnockKnockClient knockknockserver.example.com 4444

如果你太快启动客户端,可能会在服务器有机会初始化自己并开始监听端口之前启动客户端。如果发生这种情况,你会在客户端看到一个堆栈跟踪。如果发生这种情况,只需重新启动客户端。

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

当客户端和服务器成功建立连接时,您将在屏幕上看到以下文本:

服务器:敲敲!

现在,您必须回答:

谁在那里?

客户端会复述您输入的内容并将其发送到服务器。服务器会以其库存中的众多"敲敲"笑话中的第一行回应。现在您的屏幕应该包含以下内容(您输入的文本以粗体显示):

服务器:敲敲!
谁在那里?
客户端:谁在那里?
服务器:大头菜

现在,您回答:

大头菜是谁?

同样地,客户端会复述您输入的内容并将其发送到服务器。服务器会回应笑话的结尾。现在您的屏幕应该包含以下内容:

服务器:敲敲!
谁在那里?
客户端:谁在那里?
服务器:大头菜
大头菜是谁?
客户端:大头菜是谁?
服务器:大头菜发热,这里很冷!还要听一个笑话吗?(y/n)

如果您想听另一个笑话,请输入y;如果不想听,请输入n。如果输入y,服务器会重新开始"敲敲"笑话。如果输入n,服务器会说"再见",导致客户端和服务器都退出。

如果在任何时候您打错了字,KnockKnockServer对象会捕获错误并回应类似于以下的消息:

服务器:你应该说"谁在那里?"!

然后服务器会重新开始讲笑话:

服务器:再试试。敲敲!

请注意,KnockKnockProtocol对象对拼写和标点要求严格,但对大小写不敏感。

支持多个客户端

为了保持KnockKnockServer示例的简单性,我们设计它用于监听和处理单个连接请求。然而,多个客户端请求可以进入同一个端口,因此也进入同一个ServerSocket。客户端连接请求在端口处排队等待,所以服务器必须按顺序接受这些连接。但是,通过使用线程,服务器可以同时为它们提供服务——每个客户端连接一个线程。

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

while (true) {
    接受一个连接;
    创建一个线程来处理客户端;
}

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


试一试: 

修改KnockKnockServer以便它可以同时为多个客户端提供服务。我们的解决方案由两个类组成:KKMultiServerKKMultiServerThreadKKMultiServer循环不断地监听ServerSocket上的客户端连接请求。当有请求进来时,KKMultiServer接受连接,创建一个新的KKMultiServerThread对象来处理它,将从accept返回的套接字传递给它,并启动线程。然后服务器回到监听连接请求的状态。KKMultiServerThread对象通过从套接字读取和写入来与客户端通信。运行新的Knock Knock服务器KKMultiServer,然后依次运行几个客户端。



上一页: 从套接字读取和写入
下一页: 关于数据报的一切