From 8fc1da0364f4729202f3e1a3f42c1b999b1c7fad Mon Sep 17 00:00:00 2001 From: wizardforcel <562826179@qq.com> Date: Sun, 1 Oct 2017 20:14:31 +0800 Subject: [PATCH] 15.2~4 --- ...2 \345\245\227\346\216\245\345\255\227.md" | 61 +++++++-------- ...32\344\270\252\345\256\242\346\210\267.md" | 54 ++++++------- ...4 \346\225\260\346\215\256\346\212\245.md" | 76 +++++++++++++------ 3 files changed, 110 insertions(+), 81 deletions(-) diff --git "a/15.2 \345\245\227\346\216\245\345\255\227.md" "b/15.2 \345\245\227\346\216\245\345\255\227.md" index 1522656..4946f54 100644 --- "a/15.2 \345\245\227\346\216\245\345\255\227.md" +++ "b/15.2 \345\245\227\346\216\245\345\255\227.md" @@ -1,20 +1,20 @@ # 15.2 套接字 -“套接字”或者“插座”(Socket)也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知的。抽象的基本宗旨是让我们尽可能不必知道那些细节。 +“套接字”或者“插座”(`Socket`)也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知的。抽象的基本宗旨是让我们尽可能不必知道那些细节。 -在Java中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个InputStream以及OutputStream(若使用恰当的转换器,则分别是Reader和Writer),以便将连接作为一个IO流对象对待。有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;以及Socket,客户用它初始一次连接。一旦客户(程序)申请建立一个套接字连接,ServerSocket就会返回(通过accept()方法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字”连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用getInputStream()以及getOutputStream()从每个套接字产生对应的InputStream和OutputStream对象。这些数据流必须封装到缓冲区内。可按第10章介绍的方法对类进行格式化,就象对待其他任何流对象那样。 +在Java中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个`InputStream`以及`OutputStream`(若使用恰当的转换器,则分别是`Reader`和`Writer`),以便将连接作为一个IO流对象对待。有两个基于数据流的套接字类:`ServerSocket`,服务器用它“侦听”进入的连接;以及`Socket`,客户用它初始一次连接。一旦客户(程序)申请建立一个套接字连接,`ServerSocket`就会返回(通过`accept()`方法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字”连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用`getInputStream()`以及`getOutputStream()`从每个套接字产生对应的`InputStream`和`OutputStream`对象。这些数据流必须封装到缓冲区内。可按第10章介绍的方法对类进行格式化,就象对待其他任何流对象那样。 -对于Java库的命名机制,ServerSocket(服务器套接字)的使用无疑是容易产生混淆的又一个例证。大家可能认为ServerSocket最好叫作“ServerConnector”(服务器连接器),或者其他什么名字,只是不要在其中安插一个“Socket”。也可能以为ServerSocket和Socket都应从一些通用的基类继承。事实上,这两种类确实包含了几个通用的方法,但还不够资格把它们赋给一个通用的基类。相反,ServerSocket的主要任务是在那里耐心地等候其他机器同它连接,再返回一个实际的Socket。这正是“ServerSocket”这个命名不恰当的地方,因为它的目标不是真的成为一个Socket,而是在其他人同它连接的时候产生一个Socket对象。 +对于Java库的命名机制,`ServerSocket`(服务器套接字)的使用无疑是容易产生混淆的又一个例证。大家可能认为`ServerSocket`最好叫作`ServerConnector`(服务器连接器),或者其他什么名字,只是不要在其中安插一个`Socket`。也可能以为`ServerSocket`和`Socket`都应从一些通用的基类继承。事实上,这两种类确实包含了几个通用的方法,但还不够资格把它们赋给一个通用的基类。相反,`ServerSocket`的主要任务是在那里耐心地等候其他机器同它连接,再返回一个实际的`Socket`。这正是`ServerSocket`这个命名不恰当的地方,因为它的目标不是真的成为一个`Socket`,而是在其他人同它连接的时候产生一个`Socket`对象。 -然而,ServerSocket确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听进入的连接,然后利用accept()方法返回一个“已建立”套接字(本地和远程端点均已定义)。容易混淆的地方是这两个套接字(侦听和已建立)都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接请求,不能接收实际的数据包。所以尽管ServerSocket对于编程并无太大的意义,但它确实是“物理性”的。 +然而,`ServerSocket`确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听进入的连接,然后利用`accept()`方法返回一个“已建立”套接字(本地和远程端点均已定义)。容易混淆的地方是这两个套接字(侦听和已建立)都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接请求,不能接收实际的数据包。所以尽管`ServerSocket`对于编程并无太大的意义,但它确实是“物理性”的。 -创建一个ServerSocket时,只需为其赋予一个端口编号。不必把一个IP地址分配它,因为它已经在自己代表的那台机器上了。但在创建一个Socket时,却必须同时赋予IP地址以及要连接的端口编号(另一方面,从ServerSocket.accept()返回的Socket已经包含了所有这些信息)。 +创建一个`ServerSocket`时,只需为其赋予一个端口编号。不必把一个IP地址分配它,因为它已经在自己代表的那台机器上了。但在创建一个`Socket`时,却必须同时赋予IP地址以及要连接的端口编号(另一方面,从`ServerSocket.accept()`返回的`Socket`已经包含了所有这些信息)。 15.2.1 一个简单的服务器和客户端程序 -这个例子将以最简单的方式运用套接字对服务器和客户端进行操作。服务器的全部工作就是等候建立一个连接,然后用那个连接产生的Socket创建一个InputStream以及一个OutputStream。在这之后,它从InputStream读入的所有东西都会反馈给OutputStream,直到接收到行中止(END)为止,最后关闭连接。 +这个例子将以最简单的方式运用套接字对服务器和客户端进行操作。服务器的全部工作就是等候建立一个连接,然后用那个连接产生的`Socket`创建一个`InputStream`以及一个`OutputStream`。在这之后,它从`InputStream`读入的所有东西都会反馈给`OutputStream`,直到接收到行中止(END)为止,最后关闭连接。 -客户端连接与服务器的连接,然后创建一个OutputStream。文本行通过OutputStream发送。客户端也会创建一个InputStream,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。 +客户端连接与服务器的连接,然后创建一个`OutputStream`。文本行通过`OutputStream`发送。客户端也会创建一个`InputStream`,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。 服务器与客户端(程序)都使用同样的端口号,而且客户端利用本地主机地址连接位于同一台机器中的服务器(程序),所以不必在一个物理性的网络里完成测试(在某些配置环境中,可能需要同真正的网络建立连接,否则程序不能工作——尽管实际并不通过那个网络通信)。 @@ -30,7 +30,7 @@ import java.net.*; public class JabberServer { // Choose a port outside of the range 1-1024: public static final int PORT = 8080; - public static void main(String[] args) + public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Started: " + s); @@ -40,13 +40,13 @@ public class JabberServer { try { System.out.println( "Connection accepted: "+ socket); - BufferedReader in = + BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Output is automatically flushed // by PrintWriter: - PrintWriter out = + PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( @@ -65,17 +65,17 @@ public class JabberServer { } finally { s.close(); } - } + } } ///:~ ``` -可以看到,ServerSocket需要的只是一个端口编号,不需要IP地址(因为它就在这台机器上运行)。调用accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行(参考第14章)。建好一个连接以后,accept()就会返回一个Socket对象,它是那个连接的代表。 +可以看到,`ServerSocket`需要的只是一个端口编号,不需要IP地址(因为它就在这台机器上运行)。调用`accept()`时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行(参考第14章)。建好一个连接以后,`accept()`就会返回一个`Socket`对象,它是那个连接的代表。 -清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket构造器失败,则程序简单地退出(注意必须保证ServerSocket的构造器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“抛”出一个IOException异常,所以不必使用一个try块。若ServerSocket构造器成功执行,则其他所有方法调用都必须到一个try-finally代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket都能正确地关闭。 +清除套接字的责任在这里得到了很艺术的处理。假如`ServerSocket`构造器失败,则程序简单地退出(注意必须保证`ServerSocket`的构造器在失败之后不会留下任何打开的网络套接字)。针对这种情况,`main()`会“抛”出一个`IOException`异常,所以不必使用一个`try`块。若`ServerSocket`构造器成功执行,则其他所有方法调用都必须到一个`try-finally`代码块里寻求保护,以确保无论块以什么方式留下,`ServerSocket`都能正确地关闭。 -同样的道理也适用于由accept()返回的Socket。若accept()失败,那么我们必须保证Socket不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个try-finally块内,以保障在它们失败的情况下,Socket仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除(Java中没有提供“析构器”来帮助我们做这件事情)。 +同样的道理也适用于由`accept()`返回的`Socket`。若`accept()`失败,那么我们必须保证`Socket`不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个`try-finally`块内,以保障在它们失败的情况下,`Socket`仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除(Java中没有提供“析构器”来帮助我们做这件事情)。 -无论ServerSocket还是由accept()产生的Socket都打印到System.out里。这意味着它们的toString方法会得到自动调用。这样便产生了: +无论`ServerSocket`还是由`accept()`产生的`Socket`都打印到`System.out`里。这意味着它们的`toString`方法会得到自动调用。这样便产生了: ``` ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] @@ -84,14 +84,15 @@ Socket[addr=127.0.0.1,PORT=1077,localport=8080] 大家不久就会看到它们如何与客户程序做的事情配合。 -程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是InputStream和OutputStream是从Socket对象创建的。利用两个“转换器”类InputStreamReader和OutputStreamWriter,InputStream和OutputStream对象已经分别转换成为Java 1.1的Reader和Writer对象。也可以直接使用Java1.0的InputStream和OutputStream类,但对输出来说,使用Writer方式具有明显的优势。这一优势是通过PrintWriter表现出来的,它有一个重载的构造器,能获取第二个参数——一个布尔值标志,指向是否在每一次println()结束的时候自动刷新输出(但不适用于print()语句)。每次写入了输出内容后(写进out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。 +程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是`InputStream`和`OutputStream`是从`Socket`对象创建的。利用两个“转换器”类I`nputStreamReader`和`OutputStreamWriter`,`InputStream`和`OutputStream`对象已经分别转换成为Java 1.1的`Reader`和`Writer`对象。也可以直接使用Java1.0的`InputStream`和`OutputStream`类,但对输出来说,使用`Writer`方式具有明显的优势。这一优势是通过`PrintWriter`表现出来的,它有一个重载的构造器,能获取第二个参数——一个布尔值标志,指向是否在每一次`println()`结束的时候自动刷新输出(但不适用于`print()`语句)。每次写入了输出内容后(写进`out`),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。 + 编写网络应用程序时,需要特别注意自动刷新机制的使用。每次刷新缓冲区时,必须创建和发出一个数据包(数据封)。就目前的情况来说,这正是我们所希望的,因为假如包内包含了还没有发出的文本行,服务器和客户端之间的相互“握手”就会停止。换句话说,一行的末尾就是一条消息的末尾。但在其他许多情况下,消息并不是用行分隔的,所以不如不用自动刷新机制,而用内建的缓冲区判决机制来决定何时发送一个数据包。这样一来,我们可以发出较大的数据包,而且处理进程也能加快。 注意和我们打开的几乎所有数据流一样,它们都要进行缓冲处理。本章末尾有一个练习,清楚展现了假如我们不对数据流进行缓冲,那么会得到什么样的后果(速度会变慢)。 -无限while循环从BufferedReader in内读取文本行,并将信息写入System.out,然后写入PrintWriter.out。注意这可以是任何数据流,它们只是在表面上同网络连接。 +无限`while`循环从`BufferedReader in`内读取文本行,并将信息写入`System.out`,然后写入`PrintWriter.out`。注意这可以是任何数据流,它们只是在表面上同网络连接。 -客户程序发出包含了"END"的行后,程序会中止循环,并关闭Socket。 +客户程序发出包含了`"END"`的行后,程序会中止循环,并关闭`Socket`。 下面是客户程序的源码: @@ -104,21 +105,21 @@ import java.net.*; import java.io.*; public class JabberClient { - public static void main(String[] args) + public static void main(String[] args) throws IOException { // Passing null to getByName() produces the // special "Local Loopback" IP address, for // testing on one machine w/o a network: - InetAddress addr = + InetAddress addr = InetAddress.getByName(null); - // Alternatively, you can use + // Alternatively, you can use // the address or name: - // InetAddress addr = + // InetAddress addr = // InetAddress.getByName("127.0.0.1"); - // InetAddress addr = + // InetAddress addr = // InetAddress.getByName("localhost"); System.out.println("addr = " + addr); - Socket socket = + Socket socket = new Socket(addr, JabberServer.PORT); // Guard everything in a try-finally to make // sure that the socket is closed: @@ -149,30 +150,30 @@ public class JabberClient { } ///:~ ``` -在main()中,大家可看到获得本地主机IP地址的InetAddress的三种途径:使用null,使用localhost,或者直接使用保留地址127.0.0.1。当然,如果想通过网络同一台远程主机连接,也可以换用那台机器的IP地址。打印出InetAddress addr后(通过对toString()方法的自动调用),结果如下: +在`main()`中,大家可看到获得本地主机IP地址的`InetAddress`的三种途径:使用`null`,使用`localhost`,或者直接使用保留地址`127.0.0.1`。当然,如果想通过网络同一台远程主机连接,也可以换用那台机器的IP地址。打印出`InetAddress addr`后(通过对`toString()`方法的自动调用),结果如下: ``` localhost/127.0.0.1 ``` -通过向getByName()传递一个null,它会默认寻找localhost,并生成特殊的保留地址127.0.0.1。注意在名为socket的套接字创建时,同时使用了InetAddress以及端口号。打印这样的某个Socket对象时,为了真正理解它的含义,请记住一次独一无二的因特网连接是用下述四种数据标识的:clientHost(客户主机)、clientPortNumber(客户端口号)、serverHost(服务主机)以及serverPortNumber(服务端口号)。服务程序启动后,会在本地主机(127.0.0.1)上建立为它分配的端口(8080)。一旦客户程序发出请求,机器上下一个可用的端口就会分配给它(这种情况下是1077),这一行动也在与服务程序相同的机器(127.0.0.1)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况: +通过向`getByName()`传递一个`null`,它会默认寻找`localhost`,并生成特殊的保留地址`127.0.0.1`。注意在名为`socket`的套接字创建时,同时使用了`InetAddress`以及端口号。打印这样的某个`Socket`对象时,为了真正理解它的含义,请记住一次独一无二的因特网连接是用下述四种数据标识的:`clientHost`(客户主机)、`clientPortNumber`(客户端口号)、`serverHost`(服务主机)以及`serverPortNumber`(服务端口号)。服务程序启动后,会在本地主机(`127.0.0.1`)上建立为它分配的端口(8080)。一旦客户程序发出请求,机器上下一个可用的端口就会分配给它(这种情况下是1077),这一行动也在与服务程序相同的机器(`127.0.0.1`)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况: ``` Socket[addr=127.0.0.1,port=1077,localport=8080] ``` -这意味着服务器刚才已接受了来自127.0.0.1这台机器的端口1077的连接,同时监听自己的本地端口(8080)。而在客户端: +这意味着服务器刚才已接受了来自`127.0.0.1`这台机器的端口1077的连接,同时监听自己的本地端口(8080)。而在客户端: ``` Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077] ``` -这意味着客户已用自己的本地端口1077与127.0.0.1机器上的端口8080建立了 连接。 +这意味着客户已用自己的本地端口1077与`127.0.0.1`机器上的端口8080建立了 连接。 大家会注意到每次重新启动客户程序的时候,本地端口的编号都会增加。这个编号从1025(刚好在系统保留的1-1024之外)开始,并会一直增加下去,除非我们重启机器。若重新启动机器,端口号仍然会从1025开始增值(在Unix机器中,一旦超过保留的套按字范围,数字就会再次从最小的可用数字开始)。 -创建好Socket对象后,将其转换成BufferedReader和PrintWriter的过程便与在服务器中相同(同样地,两种情况下都要从一个Socket开始)。在这里,客户通过发出字符串"howdy",并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给PrintWriter构造器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“howdy”永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入System.out,以验证一切都在正常运转。为中止会话,需要发出一个"END"。若客户程序简单地挂起,那么服务器会“抛”出一个异常。 +创建好`Socket`对象后,将其转换成`BufferedReader`和`PrintWriter的`过程便与在服务器中相同(同样地,两种情况下都要从一个`Socket`开始)。在这里,客户通过发出字符串`"howdy"`,并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给`PrintWriter`构造器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的`"howdy"`永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入`System.out`,以验证一切都在正常运转。为中止会话,需要发出一个`"END"`。若客户程序简单地挂起,那么服务器会“抛”出一个异常。 -大家在这里可以看到我们采用了同样的措施来确保由Socket代表的网络资源得到正确的清除,这是用一个try-finally块实现的。 +大家在这里可以看到我们采用了同样的措施来确保由`Socket`代表的网络资源得到正确的清除,这是用一个`try-finally`块实现的。 套接字建立了一个“专用”连接,它会一直持续到明确断开连接为止(专用连接也可能间接性地断开,前提是某一端或者中间的某条链路出现故障而崩溃)。这意味着参与连接的双方都被锁定在通信中,而且无论是否有数据传递,连接都会连续处于开放状态。从表面看,这似乎是一种合理的连网方式。然而,它也为网络带来了额外的开销。本章后面会介绍进行连网的另一种方式。采用那种方式,连接的建立只是暂时的。 diff --git "a/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" "b/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" index a1ca9f0..a3c2057 100644 --- "a/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" +++ "b/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" @@ -1,15 +1,15 @@ # 15.3 服务多个客户 -JabberServer可以正常工作,但每次只能为一个客户程序提供服务。在典型的服务器中,我们希望同时能处理多个客户的请求。解决这个问题的关键就是多线程处理机制。而对于那些本身不支持多线程的语言,达到这个要求无疑是异常困难的。通过第14章的学习,大家已经知道Java已对多线程的处理进行了尽可能的简化。由于Java的线程处理方式非常直接,所以让服务器控制多名客户并不是件难事。 +`JabberServer`可以正常工作,但每次只能为一个客户程序提供服务。在典型的服务器中,我们希望同时能处理多个客户的请求。解决这个问题的关键就是多线程处理机制。而对于那些本身不支持多线程的语言,达到这个要求无疑是异常困难的。通过第14章的学习,大家已经知道Java已对多线程的处理进行了尽可能的简化。由于Java的线程处理方式非常直接,所以让服务器控制多名客户并不是件难事。 -最基本的方法是在服务器(程序)里创建单个ServerSocket,并调用accept()来等候一个新连接。一旦accept()返回,我们就取得结果获得的Socket,并用它新建一个线程,令其只为那个特定的客户服务。然后再调用accept(),等候下一次新的连接请求。 +最基本的方法是在服务器(程序)里创建单个`ServerSocket`,并调用`accept()`来等候一个新连接。一旦`accept()`返回,我们就取得结果获得的`Socket`,并用它新建一个线程,令其只为那个特定的客户服务。然后再调用`accept()`,等候下一次新的连接请求。 -对于下面这段服务器代码,大家可发现它与JabberServer.java例子非常相似,只是为一个特定的客户提供服务的所有操作都已移入一个独立的线程类中: +对于下面这段服务器代码,大家可发现它与`JabberServer.java`例子非常相似,只是为一个特定的客户提供服务的所有操作都已移入一个独立的线程类中: ``` //: MultiJabberServer.java -// A server that uses multithreading to handle +// A server that uses multithreading to handle // any number of clients. import java.io.*; import java.net.*; @@ -18,20 +18,20 @@ class ServeOneJabber extends Thread { private Socket socket; private BufferedReader in; private PrintWriter out; - public ServeOneJabber(Socket s) + public ServeOneJabber(Socket s) throws IOException { socket = s; - in = + in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Enable auto-flush: - out = + out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); - // If any of the above calls throw an + // If any of the above calls throw an // exception, the caller is responsible for // closing the socket. Otherwise the thread // will close it. @@ -76,19 +76,19 @@ public class MultiJabberServer { } finally { s.close(); } - } + } } ///:~ ``` -每次有新客户请求建立一个连接时,ServeOneJabber线程都会取得由accept()在main()中生成的Socket对象。然后和往常一样,它创建一个BufferedReader,并用Socket自动刷新PrintWriter对象。最后,它调用Thread的特殊方法start(),令其进行线程的初始化,然后调用run()。这里采取的操作与前例是一样的:从套扫字读入某些东西,然后把它原样反馈回去,直到遇到一个特殊的"END"结束标志为止。 +每次有新客户请求建立一个连接时,`ServeOneJabber`线程都会取得由`accept()`在`main()`中生成的`Socket`对象。然后和往常一样,它创建一个`BufferedReader`,并用`Socket`自动刷新`PrintWriter`对象。最后,它调用`Thread`的特殊方法`start()`,令其进行线程的初始化,然后调用`run()`。这里采取的操作与前例是一样的:从套扫字读入某些东西,然后把它原样反馈回去,直到遇到一个特殊的`"END"`结束标志为止。 -同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在ServeOneJabber外部创建的,所以清除工作可以“共享”。若ServeOneJabber构造器失败,那么只需向调用者“抛”出一个异常即可,然后由调用者负责线程的清除。但假如构造器成功,那么必须由ServeOneJabber对象负责线程的清除,这是在它的run()里进行的。 +同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在`ServeOneJabber`外部创建的,所以清除工作可以“共享”。若`ServeOneJabber`构造器失败,那么只需向调用者“抛”出一个异常即可,然后由调用者负责线程的清除。但假如构造器成功,那么必须由`ServeOneJabber`对象负责线程的清除,这是在它的`run()`里进行的。 -请注意MultiJabberServer有多么简单。和以前一样,我们创建一个ServerSocket,并调用accept()允许一个新连接的建立。但这一次,accept()的返回值(一个套接字)将传递给用于ServeOneJabber的构造器,由它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。 +请注意`MultiJabberServer`有多么简单。和以前一样,我们创建一个`ServerSocket`,并调用`accept()`允许一个新连接的建立。但这一次,`accept()`的返回值(一个套接字)将传递给用于`ServeOneJabber`的构造器,由它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。 -如果ServerSocket创建失败,则再一次通过main()抛出异常。如果成功,则位于外层的try-finally代码块可以担保正确的清除。位于内层的try-catch块只负责防范ServeOneJabber构造器的失败;若构造器成功,则ServeOneJabber线程会将对应的套接字关掉。 +如果`ServerSocket`创建失败,则再一次通过`main()`抛出异常。如果成功,则位于外层的`try-finally`代码块可以担保正确的清除。位于内层的`try-catch`块只负责防范`ServeOneJabber`构造器的失败;若构造器成功,则`ServeOneJabber`线程会将对应的套接字关掉。 -为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许创建的线程的最大数量是由final int maxthreads决定的。大家会注意到这个值非常关键,因为假如把它设得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。 +为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许创建的线程的最大数量是由`final int maxthreads`决定的。大家会注意到这个值非常关键,因为假如把它设得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。 ``` //: MultiJabberClient.java @@ -104,34 +104,34 @@ class JabberClientThread extends Thread { private static int counter = 0; private int id = counter++; private static int threadcount = 0; - public static int threadCount() { - return threadcount; + public static int threadCount() { + return threadcount; } public JabberClientThread(InetAddress addr) { System.out.println("Making client " + id); threadcount++; try { - socket = + socket = new Socket(addr, MultiJabberServer.PORT); } catch(IOException e) { - // If the creation of the socket fails, + // If the creation of the socket fails, // nothing needs to be cleaned up. } try { - in = + in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Enable auto-flush: - out = + out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())), true); start(); } catch(IOException e) { - // The socket should be closed on any - // failures other than the socket + // The socket should be closed on any + // failures other than the socket // constructor: try { socket.close(); @@ -161,12 +161,12 @@ class JabberClientThread extends Thread { public class MultiJabberClient { static final int MAX_THREADS = 40; - public static void main(String[] args) + public static void main(String[] args) throws IOException, InterruptedException { - InetAddress addr = + InetAddress addr = InetAddress.getByName(null); while(true) { - if(JabberClientThread.threadCount() + if(JabberClientThread.threadCount() < MAX_THREADS) new JabberClientThread(addr); Thread.currentThread().sleep(100); @@ -175,6 +175,6 @@ public class MultiJabberClient { } ///:~ ``` -JabberClientThread构造器获取一个InetAddress,并用它打开一个套接字。大家可能已看出了这样的一个套路:Socket肯定用于创建某种Reader以及/或者Writer(或者InputStream和/或OutputStream)对象,这是运用Socket的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重复的代码编写工作)。同样地,start()执行线程的初始化,并调用run()。在这里,消息发送给服务器,而来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接字创建好以后,但在构造器完成之前,假若构造器失败,套接字会被清除。否则,为套接字调用close()的责任便落到了run()方法的头上。 +`JabberClientThread`构造器获取一个`InetAddress`,并用它打开一个套接字。大家可能已看出了这样的一个套路:`Socket`肯定用于创建某种`Reader`以及/或者`Writer`(或者`InputStream`和/或`OutputStream`)对象,这是运用`Socket`的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重复的代码编写工作)。同样地,`start()`执行线程的初始化,并调用`run()`。在这里,消息发送给服务器,而来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接字创建好以后,但在构造器完成之前,假若构造器失败,套接字会被清除。否则,为套接字调用`close()`的责任便落到了`run()`方法的头上。 -threadcount跟踪计算目前存在的JabberClientThread对象的数量。它将作为构造器的一部分增值,并在run()退出时减值(run()退出意味着线程中止)。在MultiJabberClient.main()中,大家可以看到线程的数量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS,看看对于你使用的系统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。 +`threadcount`跟踪计算目前存在的`JabberClientThread`对象的数量。它将作为构造器的一部分增值,并在`run()`退出时减值(`run()`退出意味着线程中止)。在`MultiJabberClient.main()`中,大家可以看到线程的数量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大`MAX_THREADS`,看看对于你使用的系统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。 diff --git "a/15.4 \346\225\260\346\215\256\346\212\245.md" "b/15.4 \346\225\260\346\215\256\346\212\245.md" index 5b306ef..812c5c6 100644 --- "a/15.4 \346\225\260\346\215\256\346\212\245.md" +++ "b/15.4 \346\225\260\346\215\256\346\212\245.md" @@ -1,24 +1,39 @@ # 15.4 数据报 -15.4 数据报 大家迄今看到的例子使用的都是“传输控制协议”(TCP),亦称作“基于数据流的套接字”。根据该协议的设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地。换言之,它允许重传那些由于各种原因半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然,这种控制与可靠性需要我们付出一些代价:TCP具有非常高的开销。 + 还有另一种协议,名为“用户数据报协议”(UDP),它并不刻意追求数据包会完全发送出去,也不能担保它们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”(TCP当然是“可靠协议”)。听起来似乎很糟,但由于它的速度快得多,所以经常还是有用武之地的。对某些应用来说,比如声音信号的传输,如果少量数据包在半路上丢失了,那么用不着太在意,因为传输的速度显得更重要一些。大多数互联网游戏,如Diablo,采用的也是UDP协议通信,因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想一台报时服务器,如果某条消息丢失了,那么也真的不必过份紧张。另外,有些应用也许能向服务器传回一条UDP消息,以便以后能够恢复。如果在适当的时间里没有响应,消息就会丢失。 -Java对数据报的支持与它对TCP套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在客户和服务器程序都可以放置一个DatagramSocket(数据报套接字),但与ServerSocket不同,前者不会干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一项本质的区别的是对TCP套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。 -DatagramSocket用于收发数据包,而DatagramPacket包含了具体的信息。准备接收一个数据报时,只需提供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过DatagramSocket,作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的DatagramPacket构造器是: + +Java对数据报的支持与它对TCP套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在客户和服务器程序都可以放置一个`DatagramSocket`(数据报套接字),但与`ServerSocket`不同,前者不会干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一项本质的区别的是对TCP套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。 + +`DatagramSocket`用于收发数据包,而`DatagramPacket`包含了具体的信息。准备接收一个数据报时,只需提供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过`DatagramSocket`,作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的`DatagramPacket`构造器是: + +``` DatagramPacket(buf, buf.length) -其中,buf是一个字节数组。既然buf是个数组,大家可能会奇怪为什么构造器自己不能调查出数组的长度呢?实际上我也有同感,唯一能猜到的原因就是C风格的编程使然,那里的数组不能自己告诉我们它有多大。 +``` + +其中,`buf`是一个字节数组。既然`buf`是个数组,大家可能会奇怪为什么构造器自己不能调查出数组的长度呢?实际上我也有同感,唯一能猜到的原因就是C风格的编程使然,那里的数组不能自己告诉我们它有多大。 + 可以重复使用数据报的接收代码,不必每次都建一个新的。每次用它的时候(复用),缓冲区内的数据都会被覆盖。 + 缓冲区的最大容量仅受限于允许的数据报包大小,这个限制位于比64KB稍小的地方。但在许多应用程序中,我们都宁愿它变得还要小一些,特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要求。 -发出一个数据报时,DatagramPacket不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它的目的地。所以用于输出DatagramPacket的构造器是: + +发出一个数据报时,`DatagramPacket`不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它的目的地。所以用于输出`DatagramPacket`的构造器是: + +``` DatagramPacket(buf, length, inetAddress, port) -这一次,buf(一个字节数组)已经包含了我们想发出的数据。length可以是buf的长度,但也可以更短一些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一个目标端口(注释②)。 +``` + +这一次,`buf`(一个字节数组)已经包含了我们想发出的数据。`length`可以是`buf`的长度,但也可以更短一些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一个目标端口(注释②)。 ②:我们认为TCP和UDP端口是相互独立的。也就是说,可以在端口8080同时运行一个TCP和UDP服务程序,两者之间不会产生冲突。 -大家也许认为两个构造器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的面向对象的设计模式,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,DatagramPacket的使用相当简单,我们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对TCP套接字的MultiJabberServer和MultiJabberClient例子。多个客户都会将数据报发给服务器,后者会将其反馈回最初发出消息的同样的客户。 -为简化从一个String里创建DatagramPacket的工作(或者从DatagramPacket里创建String),这个例子首先用到了一个工具类,名为Dgram: +大家也许认为两个构造器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的面向对象的设计模式,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,`DatagramPacket`的使用相当简单,我们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对TCP套接字的`MultiJabberServer`和`MultiJabberClient`例子。多个客户都会将数据报发给服务器,后者会将其反馈回最初发出消息的同样的客户。 +为简化从一个`String`里创建`DatagramPacket`的工作(或者从`DatagramPacket`里创建`String`),这个例子首先用到了一个工具类,名为`Dgram`: + +``` //: Dgram.java // A utility class to convert back and forth // Between Strings and DataGramPackets. @@ -33,23 +48,27 @@ public class Dgram { // The correct Java 1.1 approach, but it's // Broken (it truncates the String): // byte[] buf = s.getBytes(); - return new DatagramPacket(buf, buf.length, + return new DatagramPacket(buf, buf.length, destIA, destPort); } public static String toString(DatagramPacket p){ // The Java 1.0 approach: - // return new String(p.getData(), + // return new String(p.getData(), // 0, 0, p.getLength()); // The Java 1.1 approach: - return + return new String(p.getData(), 0, p.getLength()); } } ///:~ +``` + +`Dgram`的第一个方法采用一个`String`、一个`InetAddress`以及一个端口号作为自己的参数,将`String`的内容复制到一个字节缓冲区,再将缓冲区传递进入`DatagramPacket`构造器,从而构建一个`DatagramPacket`。注意缓冲区分配时的`"+1"`——这对防止截尾现象是非常重要的。`String`的`getByte()`方法属于一种特殊操作,能将一个字符串包含的`char`复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1.1有一个“更好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉`String`的部分内容。所以尽管我们在Java 1.1下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你读到这里的时候修正了)。 + +`Dgram.toString()`方法同时展示了Java 1.0的方法和Java 1.1的方法(两者是不同的,因为有一种新类型的`String`构造器)。 -Dgram的第一个方法采用一个String、一个InetAddress以及一个端口号作为自己的参数,将String的内容复制到一个字节缓冲区,再将缓冲区传递进入DatagramPacket构造器,从而构建一个DatagramPacket。注意缓冲区分配时的"+1"——这对防止截尾现象是非常重要的。String的getByte()方法属于一种特殊操作,能将一个字符串包含的char复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1.1有一个“更好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉String的部分内容。所以尽管我们在Java 1.1下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你读到这里的时候修正了)。 -Dgram.toString()方法同时展示了Java 1.0的方法和Java 1.1的方法(两者是不同的,因为有一种新类型的String构造器)。 下面是用于数据报演示的服务器代码: +``` //: ChatterServer.java // A server that echoes datagrams import java.net.*; @@ -59,7 +78,7 @@ import java.util.*; public class ChatterServer { static final int INPORT = 1711; private byte[] buf = new byte[1000]; - private DatagramPacket dp = + private DatagramPacket dp = new DatagramPacket(buf, buf.length); // Can listen & send on the same socket: private DatagramSocket socket; @@ -75,12 +94,12 @@ public class ChatterServer { ", from address: " + dp.getAddress() + ", port: " + dp.getPort(); System.out.println(rcvd); - String echoString = + String echoString = "Echoed: " + rcvd; // Extract the address and port from the // received datagram to find out where to // send it back: - DatagramPacket echo = + DatagramPacket echo = Dgram.toDatagram(echoString, dp.getAddress(), dp.getPort()); socket.send(echo); @@ -97,11 +116,15 @@ public class ChatterServer { new ChatterServer(); } } ///:~ +``` + +`ChatterServer`创建了一个用来接收消息的`DatagramSocket`(数据报套接字),而不是在我们每次准备接收一条新消息时都新建一个。这个单一的`DatagramSocket`可以重复使用。它有一个端口号,因为这属于服务器,客户必须确切知道自己把数据报发到哪个地址。尽管有一个端口号,但没有为它分配因特网地址,因为它就驻留在“这”台机器内,所以知道自己的因特网地址是什么(目前是默认的`localhost`)。在无限`while`循环中,套接字被告知接收数据(`receive()`)。然后暂时挂起,直到一个数据报出现,再把它反馈回我们希望的接收人——`DatagramPacket dp`——里面。数据包(`Packet`)会被转换成一个字符串,同时插入的还有数据包的起源因特网地址及套接字。这些信息会显示出来,然后添加一个额外的字符串,指出自己已从服务器反馈回来了。 + +大家可能会觉得有点儿迷惑。正如大家会看到的那样,许多不同的因特网地址和端口号都可能是消息的起源地——换言之,客户程序可能驻留在任何一台机器里(就这一次演示来说,它们都驻留在`localhost`里,但每个客户使用的端口编号是不同的)。为了将一条消息送回它真正的始发客户,需要知道那个客户的因特网地址以及端口号。幸运的是,所有这些资料均已非常周到地封装到发出消息的`DatagramPacket`内部,所以我们要做的全部事情就是用`getAddress()`和`getPort()`把它们取出来。利用这些资料,可以构建`DatagramPacket echo`——它通过与接收用的相同的套接字发送回来。除此以外,一旦套接字发出数据报,就会添加“这”台机器的因特网地址及端口信息,所以当客户接收消息时,它可以利用`getAddress()`和`getPort()`了解数据报来自何处。事实上,`getAddress()`和`getPort()`唯一不能告诉我们数据报来自何处的前提是:我们创建一个待发送的数据报,并在正式发出之前调用`了getAddress()`和`getPort()`。到数据报正式发送的时候,这台机器的地址以及端口才会写入数据报。所以我们得到了运用数据报时一项重要的原则:不必跟踪一条消息的来源地!因为它肯定保存在数据报里。事实上,对程序来说,最可靠的做法是我们不要试图跟踪,而是无论如何都从目标数据报里提取出地址以及端口信息(就象这里做的那样)。 -ChatterServer创建了一个用来接收消息的DatagramSocket(数据报套接字),而不是在我们每次准备接收一条新消息时都新建一个。这个单一的DatagramSocket可以重复使用。它有一个端口号,因为这属于服务器,客户必须确切知道自己把数据报发到哪个地址。尽管有一个端口号,但没有为它分配因特网地址,因为它就驻留在“这”台机器内,所以知道自己的因特网地址是什么(目前是默认的localhost)。在无限while循环中,套接字被告知接收数据(receive())。然后暂时挂起,直到一个数据报出现,再把它反馈回我们希望的接收人——DatagramPacket dp——里面。数据包(Packet)会被转换成一个字符串,同时插入的还有数据包的起源因特网地址及套接字。这些信息会显示出来,然后添加一个额外的字符串,指出自己已从服务器反馈回来了。 -大家可能会觉得有点儿迷惑。正如大家会看到的那样,许多不同的因特网地址和端口号都可能是消息的起源地——换言之,客户程序可能驻留在任何一台机器里(就这一次演示来说,它们都驻留在localhost里,但每个客户使用的端口编号是不同的)。为了将一条消息送回它真正的始发客户,需要知道那个客户的因特网地址以及端口号。幸运的是,所有这些资料均已非常周到地封装到发出消息的DatagramPacket内部,所以我们要做的全部事情就是用getAddress()和getPort()把它们取出来。利用这些资料,可以构建DatagramPacket echo——它通过与接收用的相同的套接字发送回来。除此以外,一旦套接字发出数据报,就会添加“这”台机器的因特网地址及端口信息,所以当客户接收消息时,它可以利用getAddress()和getPort()了解数据报来自何处。事实上,getAddress()和getPort()唯一不能告诉我们数据报来自何处的前提是:我们创建一个待发送的数据报,并在正式发出之前调用了getAddress()和getPort()。到数据报正式发送的时候,这台机器的地址以及端口才会写入数据报。所以我们得到了运用数据报时一项重要的原则:不必跟踪一条消息的来源地!因为它肯定保存在数据报里。事实上,对程序来说,最可靠的做法是我们不要试图跟踪,而是无论如何都从目标数据报里提取出地址以及端口信息(就象这里做的那样)。 为测试服务器的运转是否正常,下面这程序将创建大量客户(线程),它们都会将数据报包发给服务器,并等候服务器把它们原样反馈回来。 +``` //: ChatterServer.java // A server that echoes datagrams import java.net.*; @@ -111,7 +134,7 @@ import java.util.*; public class ChatterServer { static final int INPORT = 1711; private byte[] buf = new byte[1000]; - private DatagramPacket dp = + private DatagramPacket dp = new DatagramPacket(buf, buf.length); // Can listen & send on the same socket: private DatagramSocket socket; @@ -127,12 +150,12 @@ public class ChatterServer { ", from address: " + dp.getAddress() + ", port: " + dp.getPort(); System.out.println(rcvd); - String echoString = + String echoString = "Echoed: " + rcvd; // Extract the address and port from the // received datagram to find out where to // send it back: - DatagramPacket echo = + DatagramPacket echo = Dgram.toDatagram(echoString, dp.getAddress(), dp.getPort()); socket.send(echo); @@ -149,9 +172,14 @@ public class ChatterServer { new ChatterServer(); } } ///:~ +``` + +`ChatterClient`被创建成一个线程(`Thread`),所以可以用多个客户来“骚扰”服务器。从中可以看到,用于接收的`DatagramPacket`和用于`ChatterServer`的那个是相似的。在构造器中,创建`DatagramPacket`时没有附带任何参数,因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因特网地址将成为“这台机器”(比如`localhost`),而且会自动分配端口编号,这从输出结果即可看出。同用于服务器的那个一样,这个`DatagramPacket`将同时用于发送和接收。 + +`hostAddress`是我们想与之通信的那台机器的因特网地址。在程序中,如果需要创建一个准备传出去的`DatagramPacket`,那么必须知道一个准确的因特网地址和端口号。可以肯定的是,主机必须位于一个已知的地址和端口号上,使客户能启动与主机的“会话”。 + +每个线程都有自己独一无二的标识号(尽管自动分配给线程的端口号是也会提供一个唯一的标识符)。在`run()`中,我们创建了一个`String`消息,其中包含了线程的标识编号以及该线程准备发送的消息编号。我们用这个字符串创建一个数据报,发到主机上的指定地址;端口编号则直接从`ChatterServer`内的一个常数取得。一旦消息发出,`receive()`就会暂时被“堵塞”起来,直到服务器回复了这条消息。与消息附在一起的所有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中,尽管是一种“不可靠”协议,但仍然能够检查数据报是否到去过了它们该去的地方(这在`localhost`和LAN环境中是成立的,但在非本地连接中却可能出现一些错误)。 -ChatterClient被创建成一个线程(Thread),所以可以用多个客户来“骚扰”服务器。从中可以看到,用于接收的DatagramPacket和用于ChatterServer的那个是相似的。在构造器中,创建DatagramPacket时没有附带任何参数,因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因特网地址将成为“这台机器”(比如localhost),而且会自动分配端口编号,这从输出结果即可看出。同用于服务器的那个一样,这个DatagramPacket将同时用于发送和接收。 -hostAddress是我们想与之通信的那台机器的因特网地址。在程序中,如果需要创建一个准备传出去的DatagramPacket,那么必须知道一个准确的因特网地址和端口号。可以肯定的是,主机必须位于一个已知的地址和端口号上,使客户能启动与主机的“会话”。 -每个线程都有自己独一无二的标识号(尽管自动分配给线程的端口号是也会提供一个唯一的标识符)。在run()中,我们创建了一个String消息,其中包含了线程的标识编号以及该线程准备发送的消息编号。我们用这个字符串创建一个数据报,发到主机上的指定地址;端口编号则直接从ChatterServer内的一个常数取得。一旦消息发出,receive()就会暂时被“堵塞”起来,直到服务器回复了这条消息。与消息附在一起的所有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中,尽管是一种“不可靠”协议,但仍然能够检查数据报是否到去过了它们该去的地方(这在localhost和LAN环境中是成立的,但在非本地连接中却可能出现一些错误)。 运行该程序时,大家会发现每个线程都会结束。这意味着发送到服务器的每个数据报包都会回转,并反馈回正确的接收者。如果不是这样,一个或更多的线程就会挂起并进入“堵塞”状态,直到它们的输入被显露出来。 + 大家或许认为将文件从一台机器传到另一台的唯一正确方式是通过TCP套接字,因为它们是“可靠”的。然而,由于数据报的速度非常快,所以它才是一种更好的选择。我们只需将文件分割成多个数据报,并为每个包编号。接收机器会取得这些数据包,并重新“组装”它们;一个“标题包”会告诉机器应该接收多少个包,以及组装所需的另一些重要信息。如果一个包在半路“走丢”了,接收机器会返回一个数据报,告诉发送者重传。 -- GitLab