4.5 关闭连接
可能你从没有想过由谁来关闭一个连接。在电话交谈中,任何一方都可以发起结束交谈的过程。这通常是这样的:
“好了,我得走了。”
“好的,再见。”
“再见。”
另一方面,网络协议通常明确指定了由谁来发起“关闭”连接。在回显协议中,见图4-1(a),服务器原原本本地将客户端发送的一切数据回显回去。当客户端完成数据发送后,则调用close()方法。在服务器接收并回显了客户端调用close()方法前的所有数据后,它的read操作将返回-1,以表示客户端已经完成了数据发送。然后,服务器端套接字将调用close()方法。关闭连接是协议中的关键部分,因为如果没有关闭,服务器将不知道客户端什么时候发送完了要回显的字符。对于HTTP协议,见图4-1(b),是由服务器端发起的关闭连接。客户端先向服务器发送一个请求(“get”),然后服务器发送回一个响应头信息(通常由“200 OK”开始),后面跟的是所请求的文件。由于客户端不知道文件的大小,因此服务器必须通过关闭套接字来指示文件的结束。
图 4-1 回显协议 (a)和HTTP协议 (b)的终止
调用Socket的close()方法将同时终止两个方向(输入和输出)的数据流。(第6.4.2节将对TCP连接的终止进行更加详细的介绍。)一旦一个终端(客户端或服务器端)关闭了套接字,它将无法再发送或接收数据。这就意味着close()方法只能在调用者完成通信之后用来给另一端发送信号。在回显协议中,只要服务器收到了客户端的关闭信号,就立即关闭连接。
实际上,客户端的关闭表示通信已经完成。HTTP协议也是一样的原理,只是它的通信终止发起者是服务器。
下面考虑另一种协议。假设你需要一个压缩服务器,将接收到的字节流压缩后,发回给客户端。这种情况下应该由哪一端来关闭连接呢?由于从客户端发来的字节流的长度是任意的,客户端需要关闭连接以通知服务器要压缩的字节流已经发送完毕。那么客户端应该什么时候调用close()方法呢?如果客户端在其发送完最后一个字节后立即调用套接字的close(),它将无法接收到压缩后数据的最后一些字节。或许客户端可以像回显协议那样,在接收完所有压缩后的数据才关闭连接。但不幸的是,这样一来服务器和客户端都不知道到底有多少数据要接收,因此这也不可行。我们需要一种方法来告诉连接的另一端“我已经发送完所有数据”,同时还要保持接收数据的能力。
幸运的是套接字提供了一种实现这个功能的方法。Socket类的shutdownInput()和shutdownOutput()方法能够将输入输出流相互独立地关闭。调用shutdownInput()后,套接字的输入流将无法使用。任何没有发送的数据都将毫无提示地被丢弃,任何想从套接字的输入流读取数据的操作都将返回-1。当Socket调用shutdownOutput()方法后,套接字的输出流将无法再发送数据,任何尝试向输出流写数据的操作都将抛出一个IOException异常。在调用shutdownOutput()之前写出的数据可能能够被远程套接字读取,之后,在远程套接字输入流上的读操作将返回-1。应用程序调用shutdownOutput()后还能继续从套接字读取数据,类似的,在调用shutdownInput()后也能够继续写数据。
在压缩协议中(见图4-2),客户端向服务器发送待压缩的字节,发送完成后调用shutdownOutput()关闭输出流,并从服务器读取压缩后的字节流。服务器反复地获取未压缩的数据,并将压缩后的数据发回给客户端,直到客户端执行了停机操作,导致服务器的read操作返回-1,这表示数据流的结束。然后服务器关闭连接并退出。
图 4-2 压缩协议终止
在客户端调用了shutdownOutput之后,它还要从服务器读取剩余的已经压缩的字节。
下面的压缩客户端示例程序,CompressClient.java,实现了压缩协议的客户端。程序从命令行中指定的文件读取未压缩字节,然后将压缩后的字节写入一个新的文件。设未压缩文件名是“data”,压缩后文件名是“data.gz”。注意,这个程序只适用于处理小文件,对于大文件来说其存在一个缺陷将导致死锁。(我们将在第6.2节讨论并改正这个缺陷。)
CompressClient.java
1.应用程序设置和参数解析:第17~23行
2.创建套接字和打开文件:第25~30行
3.调用sendBytes()方法传输字节:第33行
4.接收压缩后的数据流:第35~42行
while循环反复接收压缩后的数据流并将字节写入输出文件,直到read()方法返回-1表示数据流的结束。
5.关闭套接字和文件流:第45~47行
6.sendBytes():第50~60行
给定一个连接到压缩服务器的套接字和一个文件输入流,从文件中读取所有未压缩的字节,并将其写入套接字的输出流。
·获取套接字输出流:第52行
·向压缩服务器发送未压缩字节:第55~58行
while循环从输入流读(在这个例子中是从一个文件)取数据并反复将字节发送到套接字的输出流,直到read()方法返回-1表示到达文件结尾。每一次写操作由打印到控制台的“W”指示。
·关闭套接字输出流:第59行
在读取和发送完输入文件的所有字节后,关闭输出流,以通知服务器客户端已经完成了数据发送。close操作将导致服务器端的read()方法返回-1。
我们简单地为多线程的服务器构架写了一个协议,来实现压缩服务器。我们的协议实现,CompressProtocol.java,使用GZIP压缩算法实现了服务器端的压缩协议。服务器从客户端接收未压缩的字节,并将其写入GZIPOutputStream,它对套接字的输出流进行了包装。
CompressProtocol.java
1.变量和构造函数:第10~17行
2.handleCompressClient():第19~42行
给定一个连接到压缩客户端的套接字,从客户端读取未压缩字节并将压缩后的字节写回客户端。
·获取套接字I/O流:第22~23行
套接字的输出流包装在一个GZIPOutputStream中。写向这个流的字节序列将由GZIP算法对其进行压缩,然后再写入底层的输出流。
·读取未压缩字节和写压缩后的字节:第28~29行
while循环从套接字输入流读取数据,并写入GZIPOutputStream,再由它将压缩后的数据写入套接字的输出流,直到接收到流结束标记。
·刷新和关闭:第30~42行
在关闭GZIPOutputStream之前需要刷新提交可能被压缩算法缓存的字节。
·run()方法:第44~46行
run()方法只是简单地对handleCompressClient()方法进行调用。
为了使用这个协议,我们对TCPEchoServerExecutor.java进行了简单的修改,创建了一个CompressProtocol实例来替代EchoProtocol实例: