编写套接字的服务器端
本节向您展示如何编写服务器及其附带的 Client 端。Client 端/服务器对中的服务器提供 Knock Knock 笑话。敲敲笑话受到 children 的青睐,通常是破坏 Double 关语的工具。他们像这样去:
服务器 :“敲门!”
Client :“谁在那儿?”
服务器 :“ Dexter”。
Client :“请问谁?”
服务器 :“带有冬青树枝的大厅。”
Client :“格兰”。
该示例由两个独立运行的 Java 程序组成:Client 端程序和服务器程序。Client 端程序由单个类KnockKnockClient实现,与上一节中的EchoClient示例非常相似。服务器程序由两个类实现:KnockKnockServer和KnockKnockProtocol。 KnockKnockServer
与EchoServer类似,包含用于服务器程序的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()));
) {
ServerSocket
是java.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
之后,代码将调用KnockKnockProtocol
的processInput
方法来获取服务器发送给 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 端提供服务。我们的解决方案由两个类组成:KKMultiServer和KKMultiServerThread。 KKMultiServer
永远循环,在ServerSocket
上侦听 Client 端连接请求。当请求进入时,KKMultiServer
接受连接,创建一个新的KKMultiServerThread
对象以处理该连接,将其从接受返回的套接字移交给它,然后启动线程。然后,服务器返回侦听连接请求。 KKMultiServerThread
对象通过读取和写入套接字与 Client 端通信。运行新的 Knock Knock 服务器KKMultiServer
,然后连续运行多个 Client 端。