套接字和网络

一、客户端/服务器设计模式

客户端/服务器设计模式是用于与消息传递进行通信,再此模式中,有两种类型的进程:客户端和服务器。

  • 客户端通过连接到服务器来启动通信。
  • 客户端向服务器发送请求,服务器发回答复。
  • 最后,客户端断开连接。一台服务器可以同时处理来自多个客户端的连接,客户端也可能连接到多个服务器。

许多互联网应用程序都是这样工作的:Web浏览器是Web服务器的客户端,像Outlook这样的电子邮件程序是邮件服务器的客户端,等等。

在 Internet 上,客户端和服务器进程通常在不同的计算机上运行,仅通过网络连接。服务器也可以是与客户端在同一台计算机上运行的进程。

二、套接字和流

我们从一些与网络通信相关的重要概念开始,以及一般的输入/输出。输入/输出 (I/O) 是指与进程之间的通信 。可能通过网络,或与文件之间的通信,或在命令行或图形用户界面上与用户进行通信。

(一)IP地址

网络接口由IP地址标识。IP版本 4 地址是 32 位数字,由四个 8 位部分组成。

  • 18.9.22.69是 MIT Web 服务器的 IP 地址。
  • 173.194.193.99是谷歌网络服务器的地址。
  • 104.47.42.36是 Microsoft Outlook 电子邮件处理程序的地址。
  • 127.0.0.1是环回或本地主机地址:它始终引用本地计算机。从技术上讲,第一个八位字节为环回地址的任何地址都是环回地址,但都是标准的。127``127.0.0.1

(二)端口号

一台计算机可能具有客户端希望连接到的多个服务器应用程序,因此我们需要一种方法将同一网络接口上的流量定向到不同的进程。

网络接口具有由 16 位数字标识的多个端口。端口 0 是保留的,因此端口号有效地从 1 运行到 65535。

服务器进程绑定到特定端口 — 它现在正在侦听该端口。客户端必须知道服务器正在侦听的端口号。有一些已知端口是为系统级进程保留的,并为某些服务提供标准端口。例如

  • 端口 22 是标准的 SSH 端口。当您使用 SSH 连接到 时,软件会自动使用端口 22。athena.dialup.mit.edu
  • 端口 25 是标准电子邮件服务器端口。
  • 端口 80 是标准的 Web 服务器端口。当您在 Web 浏览器中连接到 URL 时,它将连接到端口 80 上的 URL。http://web.mit.edu``18.9.22.69

当端口不是标准端口时,将其指定为地址的一部分。例如,URL 引用计算机上的端口 9000。。http://128.2.39.10:9000``128.2.39.10

当客户端连接到服务器时,该传出连接还使用客户端网络接口上的端口号,该端口号通常从可用的非已知端口中随机选择。

(三)网络套接字

套接字表示客户端和服务器之间连接的一端。

  • 服务器进程使用侦听套接字来等待来自远程客户端的连接。
  • 连接的套接字可以向连接另一端的进程发送和接收消息。它由本地IP地址和端口号以及远程地址和端口标识,这允许服务器区分来自不同IP的并发连接,或者来自不同远程端口上的同一IP的并发连接。

(四)缓冲区

客户端和服务器通过网络交换的数据以块的形式发送。这些很少只是字节大小的块,尽管它们可能是。发送方(发送请求的客户端或发送响应的服务器)通常写入一大块(可能是整个字符串,如“HELLO,WORLD!”,也可能是20兆字节的视频数据)。

网络将该块切成数据包,并且每个数据包都通过网络单独路由。在另一端,接收器将数据包重新组合成字节流。

结果是一种突发的数据传输,当您想要读取它们时,数据可能已经存在,或者您可能必须等待它们到达并重新组装。

当数据到达时,它们进入缓冲区,缓冲区是内存中的一个数组,用于保存数据,直到您读取数据为止。

(五)字节流

进入或流出套接字的数据是字节

在 Java中, [InputStream]对象表示流入程序的数据源。例如:

  • 使用 [FileInputStream]从磁盘上的文件读取
  • [来自 System.in]的用户输入
  • 来自网络套接字的输入

[OutputStream]对象表示数据接收器,我们可以将数据写入的位置。例如:

  • [用于保存到文件的文件输出流]
  • [系统输出],用于向用户正常输出
  • [系统错误]输出
  • 输出到网络套接字

(六)字符流

我们可能需要将字节流解释为Unicode字符流,因为Unicode可以表示各种各样的人类语言(更不用说表情符号了)。A是Unicode字符的序列,而不是字节序列,因此,如果我们想使用字符串来操作程序中的数据,那么我们需要将传入的字节转换为Unicode,并在写出时将Unicode转换回字节。InputStream``OutputStream``String

在 Java [中,读取器]和[写入器]表示 Unicode 字符的传入和传出流。例如:

  • [FileReader]和 [FileWriter] 将文件视为字符序列而不是字节
  • 包装器 [InputStreamReader]和 [OutputStreamWriter] 将字节流改编为字符流

I/O的一个陷阱是确保程序使用正确的字符编码,这意味着字节序列表示字符序列的方式。Unicode 字符最常见的字符编码是 UTF-8。对于网络通信,UTF-8 是正确的选择。通常,当您创建或 时,Java 将默认为 UTF-8 编码。但是,当计算机上的其他程序使用不同的字符编码来读取和写入文件时,就会出现问题,这意味着您的Java程序无法与它们进行互操作。为了弥补这种文件兼容性问题,您平台上的Java可能会默认使用不同的字符编码,从系统设置中获取它 - 然后弄乱执行网络通信的Java代码,其更好的默认值是UTF-8。例如,Microsoft Windows有一种名为CP-1252的非标准编码,对于在Windows上运行的Java程序,这可能是默认设置。Reader``Writer

字符编码错误可能难以检测。UTF-8、CP-1252 和大多数其他字符编码恰好是最古老的标准化字符编码之一ASCII的超集。ASCII 足够大,可以表示英语,因此英语文本往往不受字符编码错误的影响。但是这个错误在于等待重音拉丁字符,或拉丁字母以外的脚本,或表情符号,甚至只是“花哨的”“弯曲”引号。当存在字符编码分歧时,这些字符会变成垃圾。

若要避免字符编码问题,请确保在构造 或 对象时显式指定字符编码。此读数中的示例代码始终指定 UTF-8。Reader``Writer

(七)阻塞

输入/输出流表现出阻塞行为。例如,对于套接字流:

  • 当传入套接字的缓冲区为空时,调用块直到数据可用。read
  • 当目标套接字的缓冲区已满时,调用块直到空间可用。write

从程序员的角度来看,阻塞非常方便,因为程序员可以编写代码,就好像(或)调用总是有效一样,无论数据到达的时间如何。如果缓冲区中已有数据(或 for 的空间),则调用可能会很快返回。但是,如果读取或写入无法成功,则调用会阻塞。操作系统负责延迟该线程直到或可以成功为止的详细信息。read``write``write``read``write

正如我们所看到的,阻塞发生在整个并发编程中,而不仅仅是在I / O中,并发模块不像顺序程序那样以锁步方式工作,因此当需要协调操作时,它们通常必须等待彼此赶上。

三、在Java中使用网络套接字

让我们来看看Java中套接字编程的具体细节。为了便于介绍,我们将看一个简单方法,它只回显客户端发送的所有内容,以及一个从用户获取控制台输入并将其发送到 EchoServer``EchoClient``EchoServer

(一)客户端代码

首先,我们将从客户的角度来看。客户端通过构造 [Socket]对象打开与主机名和端口的连接:

String hostname = "localhost";
int port = 4589;
Socket socket = new Socket(hostname, port);

如果有一个服务器进程在给定的主机名上运行(在本例中,表示运行客户端进程的同一台计算机)并侦听与指定端口的连接,则此构造函数将成功并生成一个打开的对象。如果没有服务器进程侦听该端口,则连接将失败并引发 .localhost``Socket``new Socket()``IOException

假设连接成功,客户端现在可以获得与服务器通信的两个字节流:

OutputStream outToServer = socket.getOutputStream();
InputStream inFromServer = socket.getInputStream();

使用套接字时,请记住,一个进程的输出是另一个进程的输入。如果客户端和服务器具有套接字连接,则该客户端具有流向服务器输入流的输出流,反之亦然。

客户端通常需要比简单和接口提供的更强大的操作。对于 ,我们希望使用前面描述的 和 接口来使用字符流而不是字节。我们还希望读取和写入整行字符,以换行符终止,这是 和 提供的功能。因此,我们将流包装在提供这些操作的类中:InputStream``OutputStream``EchoClient``Reader``Writer``BufferedReader``PrintWriter

PrintWriter writeToServer =
    new PrintWriter(new OutputStreamWriter(outToServer, StandardCharsets.UTF_8));
BufferedReader readFromServer =
    new BufferedReader(new InputStreamReader(inFromServer, StandardCharsets.UTF_8));

请注意 UTF-8 字符编码的明确规范,这是便携式网络通信的最佳选择。

的基本循环通过让用户在键盘上键入消息,然后将消息发送到服务器,然后等待回复,为服务器准备消息:EchoClient

BufferedReader readFromUser =
    new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));

while (true) {
  String message = readFromUser.readLine();
  ...
  writeToServer.println(message);
  ...
  String reply = readFromServer.readLine();
  ...
}

请注意,所有这三个方法调用都可能被阻止。首先,客户端进程将阻塞,直到用户在控制台上键入某些内容并按 Enter。然后,它会将其与 一起发送到服务器,但如果服务器的缓冲区恰好已满,则此调用将阻塞,直到它可以将消息放入缓冲区。然后,该进程将阻塞,直到服务器发送其答复。println()``println()

我们不应该感到惊讶的是,此代码与用于实现具有阻塞队列的消息传递的代码非常相似。为了向服务器发送请求,我们将请求写入套接字输出流,就像我们在队列范例中使用的一样。为了接收回复,我们从输入流中读取它,我们将使用 。发送和接收都使用阻止呼叫。BlockingQueue.put()``BlockingQueue.take()

上面的代码草图部分隐藏了两个重要细节。首先,如果它正在读取的流已被另一端关闭,则返回。对于套接字流,这表示服务器已关闭其连接端。 通过退出循环来响应:...``readLine()``null``EchoClient

String reply = readFromServer.readLine();
if (reply == null) break; // server closed the connection

其次,最初将消息放入对象内的缓冲区中,即连接的客户端端。在缓冲区填满或客户端关闭其连接端之前,不会将消息内容发送到服务器。因此,在写入后刷新缓冲区,强制发送所有缓冲区内容至关重要:println()``PrintWriter

writeToServer.println(message);
writeToServer.flush(); // important! otherwise the line may just sit in a buffer, unsent
PrintWriter`具有[启用自动刷新的构造函数](http://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/io/PrintWriter.html#(java.io.Writer,boolean)),但它仅适用于某些操作。打开自动刷新后,会自动刷新缓冲区,但看似等效的缓冲区可能会位于缓冲区中,未发送。`println(message)``print(message + "\n")

最后,在退出循环后,我们关闭流和套接字,它们都向服务器发出客户端已完成的信号,并释放与流关联的缓冲区内存和其他资源:

readFromServer.close();
writeToServer.close();
socket.close();

(二)服务器代码

服务器从由对象表示的侦听套接字开始。服务器创建一个对象来侦听特定端口号上的传入客户端连接:ServerSocket``ServerSocket

int port = 4589;
ServerSocket serverSocket = new ServerSocket(port);

如果另一个侦听套接字已经在侦听此端口,可能在另一个进程中,则此构造函数将抛出 a 以告诉您该地址已在使用中。BindException

请注意,创建此套接字不需要主机名,因为默认情况下,服务器套接字侦听与运行服务器进程的计算机的任何网络接口的连接。

与普通人不同,不提供字节流来读取或写入。相反,它会生成一系列新的客户端连接。每次客户端打开与指定端口的连接时,都会为新连接生成一个新对象。下一个客户端连接可以使用以下方法获得:Socket``ServerSocket``ServerSocket``Socket``accept()

Socket socket = serverSocket.accept();

该方法正在阻塞。如果没有客户端连接处于挂起状态,则等到客户端到达后再返回该客户端连接的对象。accept()``accept()``Socket

一旦服务器连接到客户端,它就会以与客户端大致相同的方式使用套接字的输入和输出流:从客户端接收消息,并准备和发送回信。

PrintWriter writeToClient =
    new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
BufferedReader readFromClient =
    new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

while (true) {
    // read a message from the client
    String message = readFromClient.readLine();
    if (message == null) break; // client closed its side of the connection
    if (message.equals("quit")) break; // client sent a quit message

    // prepare a reply, in this case just echoing the message
    String reply = "echo: " + message;

    // write the reply
    writeToClient.println(reply);
    writeToClient.flush(); // important! otherwise the reply may just sit in a buffer, unsent
}

// close the streams and socket
readFromClient.close();
writeToClient.close();
socket.close();

服务器实现两种不同的方法来停止客户端和服务器之间的通信。我们已经在队列消息传递中看到的一种方式:客户端发送毒丸消息,在本例中。但是客户端可以停止的另一种方法是简单地关闭其连接的末端。 将此事件识别为套接字输入流的结束,并通过返回 来发出信号。"quit"``readLine()``null

(三)多线程服务器代码

我们编写的服务器代码具有一次只能处理一个客户端的限制。服务器循环专用于单个客户端,阻止并重复读取和回复来自该客户端的消息,直到客户端断开连接。只有这样,服务器才会从排队等待的下一个客户端返回到其连接。readFromClient.readLine()``ServerSocket``accept

如果我们想使用阻塞 I/O 同时处理多个客户端,则服务器需要一个新线程来处理每个新客户端的 I/O。当每个特定于客户端的线程使用自己的客户端时,另一个线程(可能是主线程)已准备好进行新连接。accept

以下是多线程的工作原理。连接接受循环由主线程运行:EchoServer

while (true) {
    // get the next client connection
    Socket socket = serverSocket.accept();

    // handle the client in a new thread, so that the main thread
    // can resume waiting for another client
    new Thread(new Runnable() {
        public void run() {
            handleClient(socket);
        }
    }).start();
}

然后,客户端处理循环由为每个新连接创建的新线程运行:

private static void handleClient(Socket socket) {
    // same server loop code as above:
    //   open readFromClient and writeToClient streams
    //   while (true) {
    //      read message from client
    //      prepare reply
    //      write reply to client
    //   }
    //   close streams and socket 
}

(四)使用资源试用关闭流和套接字

Java 语法的一个新位对于处理流和套接字特别有用:try-with-resources语句。此语句自动调用在其括号前导码中声明的变量:close()

try (
    // preamble: declare variables initialized to objects that need closing after use
) {
    // body: runs with those variables in scope
} catch(...) {
    // catch clauses: optional, handles exceptions thrown by the preamble or body
} finally {
    // finally clause: optional, runs after the body and any catch clause
}
// no matter how the try statement exits, it automatically calls
// close() on all variables declared in the preamble

例如,下面介绍如何使用它来确保关闭客户端套接字连接:

try (
    Socket socket = new Socket(hostname, port);
) {
    // read and write to the socket
} catch (IOException ioe) {
    ioe.printStackTrace();
} // socket.close() is automatically called here

try-with-resources 语句对于使用后应关闭的任何对象都很有用:

  • 字节流:InputStream``OutputStream
  • 字符流: Reader``Writer
  • 文件:FileInputStream``FileOutputStream``FileReader``FileWriter
  • 插座:Socket``ServerSocket

Python with 语句具有类似的语义

四、有线协议

(一)远程登录客户端

telnet是一个实用程序,允许您与侦听服务器建立直接网络连接,并通过终端接口与其通信。Windows,Linux和Mac OS X都可以运行,尽管默认情况下较新的操作系统不再安装它。

您应首先通过在命令行上运行命令来检查是否安装了 telnet。如果您没有它,请查找有关如何安装它的说明([Linux],[Mac OS])。在Windows上,另一个telnet客户端是[PuTTY],它具有图形用户界面。telnet

让我们看一些有线协议的例子。

(二)断续器

1.超文本传输协议(HTTP)

超文本传输协议(HTTP)是万维网的语言。我们已经知道端口 80 是众所周知的端口,用于向 Web 服务器讲 HTTP ,因此让我们在命令行上与一个端谈。

尝试通过以下命令使用远程登录客户端。用户输入显示为绿色,对于 telnet 连接的输入,换行符(按 Enter)显示为 。(如果您在Windows上使用PuTTY,您将在PuTTY的连接对话框中输入主机名和端口,并且还应该选择连接类型:原始,并在退出时关闭窗口:从不。最后一个选项将防止窗口在服务器关闭其连接结束后立即消失。

$ telnet www.eecs.mit.edu 80
Trying 18.62.0.96...
Connected to eecsweb.mit.edu.
Escape character is '^]'.
GET /↵
<!DOCTYPE html>
... lots of output ...
<title>Homepage | MIT EECS</title>
... lots more output ...

该命令获取网页。是要在网站上显示的页面的路径。因此,此命令将获取位于 的页面。由于 80 是 HTTP 的默认端口,因此这等效于在 Web 浏览器中访问 http://www.eecs.mit.edu/。结果是浏览器呈现的 HTML 代码以显示 EECS 主页。GET``/``http://www.eecs.mit.edu:80/

互联网协议由RFC规范定义(RFC代表“征求意见”,一些RFC最终被采用为标准)。[RFC 1945]定义了 HTTP 版本 1.0,并在 [RFC 2616 中被 HTTP 1.1 取代]。因此,对于许多网站,如果您想与它们交谈,则可能需要使用HTTP 1.1。例如:

$ telnet web.mit.edu 80
Trying 18.9.22.69...
Connected to web.mit.edu.
Escape character is '^]'.
GET /about/ HTTP/1.1↵
Host: web.mit.edu↵
↵
HTTP/1.1 200 OK
Date: Tue, 18 Apr 2017 15:25:23 GMT
... more headers ...

9b7
<!DOCTYPE html>
... more HTML ...
<title>About MIT | MIT - Massachusetts Institute of Technology</title>
... lots more HTML ...
</html>

0

这一次,您的请求必须以空行结尾。HTTP 版本 1.1 要求客户端在请求中指定一些额外的信息(称为标头),空行表示标头的结束。

您还很可能会发现 telnet 在发出此请求后不会退出 - 这一次,服务器保持连接打开,以便您可以立即发出另一个请求。要手动退出 Telnet,请键入转义字符(可能是 -)以显示提示符,然后键入 :Ctrl``]``telnet>``quit

... lots more HTML ...
</html>
0
Ctrl-]
telnet> quit↵
Connection closed.

2.简单邮件传输协议 (SMTP)

简单邮件传输协议 (SMTP)是用于发送电子邮件的协议(不同的协议用于从收件箱中检索电子邮件的客户端程序)。由于电子邮件系统是在垃圾邮件出现之前的时代设计的,因此现代电子邮件通信充满了旨在防止滥用的陷阱和启发式方法。但是我们仍然可以尝试使用SMTP。回想一下,众所周知的SMTP端口是25,MIT的传入电子邮件处理程序是。mit-edu.mail.protection.outlook.com

您需要在此处填写您的 IP 地址和在此处填写您的用户名,为清楚起见,↵ 表示换行符。这只有在您在MITnet上时才有效,即使这样,您的邮件也可能因为看起来可疑而被拒绝:

$ telnet mit-edu.mail.protection.outlook.com 25
Trying 104.47.40.36...
Connected to mit-edu.mail.protection.outlook.com.
Escape character is '^]'.
220 ABC123000.mail.protection.outlook.com Microsoft ESMTP MAIL Service
HELO your-IP-address-here↵
250 ABC123000.mail.protection.outlook.com Hello [your-ip-address]
MAIL FROM: <your-username-here@mit.edu>↵
250 2.1.0 Sender OK
RCPT TO: <your-username-here@mit.edu>↵
250 2.1.5 Recipient OK
DATA↵
354 Start mail input; end with <CRLF>.<CRLF>
From: <your-username-here@mit.edu>↵
To: <your-username-here@mit.edu>↵
Subject: testing↵
↵
This is a hand-crafted artisanal email.↵
.↵
250 2.6.0 <111111-22-33-44-55555555@ABC.eop-123.prod.protection.outlook.com>
QUIT↵
221 2.0.0 Service closing transmission channel
Connection closed by foreign host.

与HTTP相比,SMTP非常健谈,甚至包括人类可读的说明,告诉客户端如何提交他们的消息。

(三)设计有线协议

在设计连线协议时,应用与设计抽象数据类型的操作相同的经验法则:

  • 保持较小的不同消息的数量。最好有一些可以组合的命令和响应,而不是许多复杂的消息。
  • 每条消息都应该有明确的目的和连贯的行为。
  • 这组消息必须足以让客户端发出他们需要发出的请求,并且服务器必须能够提供结果。

正如我们要求代表独立于我们的类型一样,我们应该在协议中实现平台独立性。HTTP可以被任何操作系统上的任何Web服务器和任何Web浏览器使用。该协议没有说明网页如何存储在磁盘上,服务器如何准备或生成网页,客户端将使用什么算法来呈现它们等。

我们还可以应用这门课中的三个大想法:

  • 免受错误侵害

    • 该协议应该易于客户端和服务器生成和解析。用于读取和编写协议的更简单代码(例如,从语法自动生成的解析器,或具有正则表达式匹配库的简单正则表达式)将减少错误的机会。

    • 考虑一下损坏或恶意的客户端或服务器可能将垃圾数据填充到协议中以破坏另一端的进程的方式。

      垃圾邮件就是一个例子:当我们在上面说SMTP时,邮件服务器要求我们说出谁在发送电子邮件,SMTP中没有任何东西可以阻止我们直接撒谎。我们不得不在SMTP之上构建系统,以试图阻止在地址上撒谎的垃圾邮件发送者。From:

      安全漏洞是一个更严重的例子。例如,允许客户端发送包含任意数据量的请求的协议需要在服务器上进行仔细处理,以避免缓冲区空间不足.

  • 易于理解:例如,选择基于文本的协议意味着我们可以通过读取客户端/服务器交换的文本来调试通信错误。它甚至允许我们“手”说出协议,正如我们上面看到的那样。

  • 准备更改:例如,HTTP包括指定版本号的功能,因此客户端和服务器可以相互同意它们将使用哪个版本的协议。如果我们将来需要对协议进行更改,较旧的客户端或服务器可以通过宣布它们将使用的版本来继续工作。

[序列化]是将内存中的数据结构转换为可以轻松存储或传输的格式的过程(与[线程安全中的可序列化性](不同)。与其发明一种在客户端和服务器之间序列化数据的新格式,不如使用现有的格式。例如,[JSON(JavaScript 对象表示法]是一种简单且广泛使用的格式,用于序列化基本值、数组和具有字符串键的映射。

(四)指定连线协议

为了精确地为客户端和服务器定义协议允许哪些消息,请使用语法。

request ::= request-line
            ((general-header | request-header | entity-header) CRLF)*
            CRLF
            message-body?
request-line ::= method SPACE request-uri SPACE http-version CRLF
method ::= "OPTIONS" | "GET" | "HEAD" | "POST" | ...
...

使用语法,我们可以看到,在前面的此示例请求中:

GET /about/ HTTP/1.1
Host: web.mit.edu
  • GET是:我们要求服务器为我们获取一个页面。method
  • /about/是:我们想要得到什么的描述。request-uri
  • HTTP/1.1是 .http-version
  • Host: web.mit.edu是某种标头 — 我们必须检查每个选项的规则才能发现哪个选项。...-header
  • 我们可以看到为什么我们必须用空行结束请求:由于单个可以有多个以CRLF(换行符)结尾的标头,因此我们在末尾有另一个CRLF来完成.request``request
  • 我们没有任何东西 - 而且由于服务器没有等待我们是否会发送一个,大概这仅适用于其他类型的请求。message-body

语法是不够的:在定义 ADT 时,它扮演的角色与方法签名类似。我们仍然需要以下规范:

  • 消息的前提条件是什么?

例如,如果消息中的特定字段是一串数字,那么任何数字是否有效?或者它必须是服务器已知的记录的 ID 号?在什么情况下可以发送消息?某些消息是否仅在按特定顺序发送时才有效?

  • 后置条件是什么?服务器将根据邮件执行什么操作?哪些服务器端数据将被更改?服务器将向客户端发送回什么回复?

五、测试客户端/服务器代码

(一)将网络代码与数据结构和算法分开

客户端/服务器程序中的大多数 ADT 不需要依赖网络。确保将它们指定、测试和实现为单独的组件,这些组件可以免受错误、易于理解并随时进行更改 — 部分原因是它们不涉及任何网络代码。

如果需要从多个线程(例如,处理不同客户端连接的线程)并发使用这些 ADT,请尽可能[将消息传递与线程安全队列]结合使用,如有必要,还可以[使用同步]或[限制、不可变性和现有线程安全数据类型的线程安全策略]。

(二)将套接字代码与流代码分开

需要读取和写入套接字的函数或模块可能只需要访问输入/输出流,而不需要访问套接字本身。此设计允许您通过将模块连接到不是来自套接字的流来测试模块。

两个有用的Java类是[ByteArrayInputStream]和[ByteArrayOutputStream]。假设我们要测试此方法:

void upperCaseLine(BufferedReader input, PrintWriter output) throws IOException
  • 需要:

    input并且是开放的output

  • 影响:

    尝试从中读取一行,并尝试将该行(大写)写入input``output

该方法通常与套接字一起使用:

Socket sock = ...

// read a stream of characters from the socket input stream
BufferedReader in = 
    new BufferedReader(new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8));

// write characters to the socket output stream, with autoflushing set to true
PrintWriter out = 
    new PrintWriter(new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8), 
                    true /* autoflush */);

upperCaseLine(in, out);

如果基础到大写的转换是我们实现的函数,则应该已经单独指定,测试和实现它。但现在我们还可以测试以下各项的读/写行为:upperCaseLine

// fixed input stream of "dog" (line 1) and "cat" (line 2)
String inString = "dog\ncat\n";
ByteArrayInputStream inBytes = new ByteArrayInputStream(inString.getBytes());
ByteArrayOutputStream outBytes = new ByteArrayOutputStream();

// read a stream of characters from the fixed input string
BufferedReader in = new BufferedReader(new InputStreamReader(inBytes, StandardCharsets.UTF_8));
// write characters to temporary storage, with autoflushing
PrintWriter out = new PrintWriter(new OutputStreamWriter(outBytes, StandardCharsets.UTF_8), true);

upperCaseLine(in, out);

// check that it read the expected amount of input
assertEquals("cat", in.readLine(), "expected input line 2 remaining");
// check that it wrote the expected output
assertEquals("DOG\n", outBytes.toString(), "expected upper case of input line 1");

在此测试中,并且是测试存根。为了隔离和测试,我们将它通常依赖的组件(来自套接字的输入/输出流)替换为满足相同规范但具有固定行为的组件:具有固定输入的输入流和将输出存储在内存中的输出流。inBytes``outBytes``upperCaseLine

更复杂模块的测试策略可能使用模拟对象来模拟真实客户端或服务器的行为,方法是生成整个固定的交互序列并断言从其他组件接收的每条消息的正确性。

六、总结

客户端/服务器设计模式中,并发是不可避免的:多个客户端和多个服务器在网络上连接,同时发送和接收消息,并期望及时回复。当有其他客户端等待连接到某个慢速客户端或接收答复时,如果服务器阻止等待该客户端,则不会使这些客户端满意。同时,由于不同客户端对共享可变数据的并发修改而执行不正确计算或返回虚假结果的服务器不会让任何人满意。

当我们设计网络客户端和服务器时,使我们的多线程代码免受错误易于理解准备更改的所有挑战都适用。这些进程彼此并发运行(通常在不同的计算机上),任何想要同时与多个客户端通信的服务器(或想要与多个服务器通信的客户端)都必须管理该多线程通信。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值