一、Socket概念

通常情况下,服务器运行在特定的计算机上,并且具有绑定到特定端口号的Socket。服务器只是等待,监听Socket,等待客户端发出连接请求。
在客户端:客户端知道服务器正在运行的机器的主机名以及服务器正在侦听的端口号。要发出连接请求,客户端会尝试在服务器的计算机和端口上与服务器会合。客户端还需要向服务器标识自己,以便绑定到将在连接过程中使用的本地端口号。这通常是由系统分配的。

如果一切顺利,服务器将接受连接。接受后,服务器将获得一个绑定到同一本地端口的新Socket,并将其远程端点设置为客户端的地址和端口。它需要一个新的Socket,这样它就可以继续侦听原始套接字的连接请求,同时满足连接的客户端的需求。

 在客户端,如果连接被接受,则成功创建了一个Socket,客户端可以使用该Socket与服务器通信。
客户端和服务器现在可以通过向Socket写入或从Socket读取数据来进行通信。

端点是IP地址和端口号的组合。每个TCP连接都可以由其两个端点唯一标识。这样,您就可以在主机和服务器之间建立多个连接。
java平台中的java.net包提供了一个类Socket,它实现了java程序和网络上另一个程序之间双向连接的一端。Socket类位于依赖于平台的实现之上,可以向Java程序隐藏任何特定系统的详细信息。通过使用java.net.Socket类而不是依赖于本机代码,java程序可以以独立于平台的方式通过网络进行通信。
此外,java.net还包括ServerSocket类,它实现了一个套接字,服务器可以使用该套接字来侦听和接受与客户端的连接。本课程向您展示如何使用Socket和ServerSocket类。
如果您试图连接到Web,URL类和相关类(URLConnection、URLEncoder)可能比套接字类更合适。事实上,URL是到Web的一种相对高级的连接,并使用套接字作为底层实现的一部分。

二、读取和写入Socket

让我们看一个简单的例子,它说明了程序如何使用Socket类建立与服务器程序的连接,然后说明了客户端如何通过套接字向服务器发送数据和从服务器接收数据。
示例程序实现了一个客户端EchoClient,该客户端连接到echo服务器。echo服务器从其客户端接收数据并将其回显。示例EchoServer实现了一个回声服务器。(或者,客户端可以连接到任何支持回声协议的主机。)
EchoClient示例创建一个套接字,从而连接到echo服务器。它在标准输入流上读取用户的输入,然后通过将文本写入套接字将文本转发到echo服务器。服务器通过套接字将输入返回到客户端。客户端程序读取并显示从服务器传回的数据。
请注意,EchoClient示例既向其套接字写入数据,又从其套接字读取数据,从而向echo服务器发送数据,并从其接收数据。
让我们浏览一下这个程序,并调查其中有趣的部分。EchoClient示例中的try with resources语句中的以下语句至关重要。这些行在客户端和服务器之间建立套接字连接,并在套接字上打开PrintWriter和BufferedReader:

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

try (
    Socket echoSocket = new Socket(hostName, portNumber);        // 1st statement
    PrintWriter out =                                            // 2nd statement
        new PrintWriter(echoSocket.getOutputStream(), true);
    BufferedReader in =                                          // 3rd statement 
        new BufferedReader(
            new InputStreamReader(echoSocket.getInputStream()));
    BufferedReader stdIn =                                       // 4th statement 
        new BufferedReader(
            new InputStreamReader(System.in))
)

try-with-resources语句中的第一条语句创建了一个新的Socket对象,并将其命名为echoSocket。这里使用的Socket构造函数需要计算机的名称和要连接的端口号。示例程序使用第一个命令行参数作为计算机名称(主机名),使用第二个命令行自变量作为端口号。在计算机上运行此程序时,请确保使用的主机名是要连接的计算机的完全限定IP名。例如,如果您的echo服务器正在计算机echoserver.example.com上运行,并且正在侦听端口号7,如果您想使用echoserver示例作为echo服务器,请首先从计算机echoseserver.example.com运行以下命令:

java EchoServer 7

之后,使用以下命令运行EchoClient示例:

java EchoClient echoserver.example.com 7

try-with-resources语句中的第二条语句获取套接字的输出流,并在其上打开一个名为out的PrintWriter。类似地,第三条语句获取套接字的输入流,并在其中打开一个名为的BufferedReader。该示例使用读取器和写入器,以便可以在套接字上写入Unicode字符。如果您还不熟悉Java平台的I/O类,您可能希望阅读Basic I/O。
程序的下一个有趣部分是while循环。循环每次从带有BufferedReader对象stdIn的标准输入流中读取一行,该对象是在try-with-resources语句的第四条语句中创建的。然后,循环通过将该行写入连接到套接字的PrintWriter,立即将该行发送到服务器:

String userInput;
while ((userInput = stdIn.readLine()) != null) {
    out.println(userInput);
    System.out.println("echo: " + in.readLine());
}

while循环中的最后一条语句从连接到套接字的BufferedReader中读取一行信息。readLine方法等待,直到服务器将信息回显给EchoClient。当readline返回时,EchoClient将信息打印到标准输出。
while循环一直持续到用户键入输入字符的末尾。也就是说,EchoClient示例读取用户的输入,将其发送到Echo服务器,从服务器获取响应并显示,直到输入结束。(您可以按Ctrl-C键键入输入字符的末尾。)while循环随后终止,Java运行时会自动关闭连接到套接字和标准输入流的读写器,并关闭与服务器的套接字连接。Java运行时会自动关闭这些资源,因为它们是在try-with-resources语句中创建的。Java运行时以与创建这些资源相反的顺序关闭这些资源。(这很好,因为连接到套接字的流应该在套接字本身关闭之前关闭。)
这个客户端程序简单明了,因为echo服务器实现了一个简单的协议。客户端将文本发送到服务器,服务器将其回显。当客户端程序与更复杂的服务器(如HTTP服务器)通信时,客户端程序也会更复杂。然而,基本原理与本程序中的基本原理基本相同:

  1. 打开Socket。
  2. 打开输入流并将输出流输出到套接字。
  3. 根据服务器的协议读取和写入流。
  4. 关闭流。
  5. 关闭Socket。

只有步骤 3 因客户端而异,具体取决于服务器。其他步骤基本保持不变。

三、写入服务端Socket

该示例由两个独立运行的Java程序组成:客户端程序和服务器程序。客户端程序由一个类KnockKnockClient实现,与上一节中的EchoClient示例非常相似。服务器程序由两个类实现:KnockKnockServer和KnockKnocksProtocol。KnockKnockServer类似于EchoServer,包含服务器程序的主要方法,并执行侦听端口、建立连接以及读取和写入套接字的工作。KnockKnockProtocol这门课讲笑话。它跟踪当前笑话和当前状态(发送的敲门声、发送的线索等),并根据当前状态返回笑话的各种文本片段。这个对象实现了协议——客户端和服务器已经同意使用的语言来进行通信。
以下部分详细介绍了客户端和服务器中的每个类,然后向您展示了如何运行它们。

Knock Knock服务器

本节将介绍实现KnockKnock服务器程序KnockKnockServer的代码。
服务器程序首先创建一个新的ServerSocket对象来侦听特定的端口(请参阅下面代码段中的粗体语句)。运行此服务器时,请选择一个尚未专用于其他服务的端口。例如,此命令启动服务器程序KnockKnockServer,使其在端口8888上侦听:

java KnockKnockServer 8888

服务器程序在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()));
        } catch (Exception e) {
            
        }

ServerSocket是一个java.net类,它提供了客户端/服务器套接字连接的服务器端的独立于系统的实现。如果ServerSocket的构造函数无法侦听指定的端口(例如,该端口已在使用中),则会引发异常。在这种情况下,KnockKnockServer别无选择,只能退出。
如果服务器成功绑定到其端口,则ServerSocket对象将成功创建,服务器将继续执行下一步,接受来自客户端的连接(try-with-resources语句中的下一条语句):

clientSocket = serverSocket.accept();

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

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;
            }
        }
        catch (Exception e)
        {
            
        }

此代码执行以下操作:

  1. 获取Socket的输入和输出流,并打开它们上的读写器。
  2. 通过写入Socket(以粗体显示)来启动与客户端的通信。
  3. 通过读取和写入套接字与客户端通信(while循环)。

步骤1已经很熟悉了。步骤2以粗体显示,值得一些评论。上面代码段中的粗体语句启动了与客户的对话。该代码创建了一个KnockKnockProtocol对象,该对象跟踪当前笑话、笑话中的当前状态等。
创建KnockKnockProtocol后,代码调用KnockKnocksProtocol的processInput方法来获取服务器发送给客户端的第一条消息。对于这个例子,服务器说的第一句话是“Knock!Knock!”接下来,服务器将信息写入连接到客户端套接字的PrintWriter,从而将消息发送到客户端。
步骤3在while循环中进行编码。只要客户端和服务器之间还有话要说,服务器就会从套接字中读取和写入,在客户端和服务器间来回发送消息。
服务器用“Knock!Knock!”启动对话,因此之后服务器必须等待客户端说“谁在那里?”因此,while循环迭代输入流中的读取。readLine方法等待,直到客户端通过向其输出流(服务器的输入流)写入内容来做出响应。当客户端响应时,服务器将客户端的响应传递给KnockKnockProtocol对象,并向KnockKnock Protocol对象请求合适的答复。服务器使用对println的调用,通过连接到套接字的输出流立即向客户端发送回复。如果服务器从KnockKnockServer对象生成的响应是“再见”,这表明客户端不想再开玩笑了,循环就退出了。
Java运行时会自动关闭输入和输出流、客户端套接字和服务器套接字,因为它们是在try-with-resources语句中创建的。

Knock Knock客户端

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()));
        }catch (Exception e)
        {
            
        }

在创建套接字时,KnockKnockClient示例使用第一个命令行参数的主机名,即网络上运行服务器程序KnockKnockServer的计算机的名称。
KnockKnockClient示例在创建套接字时使用第二个命令行参数作为端口号。这是一个远程端口号,即服务器计算机上的端口号,是KnockKnockServer正在侦听的端口。例如,以下命令运行KnockKnockClient示例,knockknockserver.example.com是运行服务器程序knockknockserver的计算机的名称,8888是远程端口号:

java KnockKnockClient knockknockserver.example.com 8888

客户端的套接字绑定到任何可用的本地端口,即客户端计算机上的端口。请记住,服务器也会得到一个新的套接字。如果使用上一个示例中的命令行参数运行KnockKnockClient示例,则此套接字将绑定到运行KnockKnockClient示例的计算机上的本地端口号8888。服务器的套接字和客户端的套接字已连接。
接下来是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 8888

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

java KnockKnockClient knockknockserver.example.com 8888

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

支持多个客户端

为了使KnockKnockServer示例保持简单,我们将其设计为侦听和处理单个连接请求。但是,多个客户端请求可以进入同一端口,从而进入同一ServerSocket。客户端连接请求在端口排队,因此服务器必须按顺序接受连接。然而,服务器可以通过使用线程同时为它们提供服务,每个客户端连接一个线程。
这种服务器中的基本逻辑流程是:

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

线程根据需要读取和写入客户端连接。
java官网https://docs.oracle.com/javase/tutorial/networking/sockets/index.html

大家好,我是Doker品牌的Sinbad,欢迎点赞和评论,您的鼓励是我们持续更新的动力!欢迎加微信进入技术群聊! 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐