关闭连接
可能你从没有想过由谁来关闭一个连接。在电话交谈中,任何一方都可以发起结束交谈的过程。这通常是这样的:
"好了,我得走了。"
"好的,再见。"
"再见。"
另一方面,网络协议通常明确指定了由谁来发起"关闭"连接。在回显协议中,见图4.1(a),服务器原原本本地将客户端发送的一切数据回显回去。当客户端完成数据发送后,则调用close()方法。在服务器接收并回显了客户端调用close()方法前的所有数据后,它的read操作将返回-1,以表示客户端已经完成了数据发送。然后,服务器端套接字将调用close()方法。关闭连接是协议中的关键部分,因为如果没有关闭,服务器将不知道客户端什么时候发送完了要回显的字符。对于HTTP协议,见图4.1(b),是由服务器端发起的关闭连接。客户端先向服务器发送一个请求("get"),然后服务器发送回一个响应头信息(通常由"200OK"开始),后面跟的是所请求的文件。由于客户端不知道文件的大小,因此服务器必须通过关闭套接字来指示文件的结束。
调用Socket的close()方法将同时终止两个方向(输入和输出)的数据流。(第6.4.2节将对TCP连接的终止进行更加详细的介绍。)一旦一个终端(客户端或服务器端)关闭了套接字,它将无法再发送或接收数据。这就意味着close()方法只能在调用者完成通信之后用来给另一端发送信号。在回显协议中,只要服务器收到了客户端的关闭信号,就立即关闭连接。
(点击查看大图)图4.1:回显协议(a)和HTTP协议(b)的终止
Echo Client:回显客户端;Echo Server:回显服务器;Closed:关闭;Web Browser:网络浏览器;HTTP Server:HTTP服务器;Closed:关闭
实际上,客户端的关闭表示通信已经完成。HTTP协议也是一样的原理,只是它的通信终止发起者是服务器。
下面考虑另一种协议。假设你需要一个压缩服务器,将接收到的字节流压缩后,发回给客户端。这种情况下应该由哪一端来关闭连接呢?由于从客户端发来的字节流的长度是任意的,客户端需要关闭连接以通知服务器要压缩的字节流已经发送完毕。那么客户端应该什么时候调用close()方法呢?如果客户端在其发送完最后一个字节后立即调用套接字的close(),它将无法接收到压缩后数据的最后一些字节。或许客户端可以像回显协议那样,在接收完所有压缩后的数据才关闭连接。但不幸的是,这样一来服务器和客户端都不知道到底有多少数据要接收,因此这也不可行。我们需要一种方法来告诉连接的另一端"我已经发送完所有数据",同时还要保持接收数据的能力。
幸运的是套接字提供了一种实现这个功能的方法。Socket类的shutdownInput()和shutdownOutput()方法能够将输入输出流相互独立地关闭。调用shutdownInput()后,套接字的输入流将无法使用。任何没有发送的数据都将毫无提示地被丢弃,任何想从套接字的输入流读取数据的操作都将返回-1。当Socket调用shutdownOutput() 方法后,套接字的输出流将无法再发送数据,任何尝试向输出流写数据的操作都将抛出一个IOException异常。在调用shutdownOutput()之前写出的数据可能能够被远程套接字读取,之后,在远程套接字输入流上的读操作将返回-1。应用程序调用shutdownOutput()后还能继续从套接字读取数据,类似的,在调用shutdownInput()后也能够继续写数据。
在压缩协议中(见图4.2),客户端向服务器发送待压缩的字节,发送完成后调用shutdownOutput()关闭输出流,并从服务器读取压缩后的字节流。服务器反复地获取未压缩的数据,并将压缩后的数据发回给客户端,直到客户端执行了停机操作,导致服务器的read操作返回-1,这表示数据流的结束。然后服务器关闭连接并退出。
图4.2:压缩协议终止
Compression Client:压缩客户端;Compression Server:压缩服务器;Uncompressed Bytes:未压缩字节;Compressed Bytes:已压缩字节;Shutdown:停机;Closed:关闭在客户端调用了shutdownOutput之后,它还要从服务器读取剩余的已经压缩的字节。
下面的压缩客户端示例程序,CompressClient.java,实现了压缩协议的客户端。程序从命令行中指定的文件读取未压缩字节,然后将压缩后的字节写入一个新的文件。设未压缩文件名是"data",压缩后文件名是"data.gz"。注意,这个程序只适用于处理小文件,对于大文件来说其存在一个缺陷将导致死锁。(我们将在第6.2节讨论并改正这个缺陷。)
CompressClient.java
0 import java.net.Socket;
1 import java.io.IOException;
2 import java.io.InputStream;
3 import java.io.OutputStream;
4 import java.io.FileInputStream;
5 import java.io.FileOutputStream;
6
7 /* WARNING: this code can deadlock if a large file (more
than a few
8 * 10's of thousands of bytes) is sent.
9 */
10
11 public class CompressClient {
12
13 public static final int BUFSIZE = 256; // Size of read
buffer
14
15 public static void main(String[] args) throws
IOException {
16
17 if (args.length != 3) { // Test for correct # of args
18 throw new IllegalArgumentException("Parameter(s):
<Server> <Port> <File>");
19 }
20
21 String server = args[0]; // Server name or IP address
22 int port = Integer.parseInt(args[1]); // Server port
23 String filename = args[2]; // File to read data from
24
25 // Open input and output file (named input.gz)
26 FileInputStream fileIn = new
FileInputStream(filename);
27 FileOutputStream fileOut = new
FileOutputStream(filename + ".gz");
28
29 // Create socket connected to server on specified port
30 Socket sock = new Socket(server, port);
31
32 // Send uncompressed byte stream to server
33 sendBytes(sock, fileIn);
34
35 // Receive compressed byte stream from server
36 InputStream sockIn = sock.getInputStream();
37 int bytesRead; // Number of bytes read
38 byte[] buffer = new byte[BUFSIZE]; // Byte buffer
39 while ((bytesRead = sockIn.read(buffer)) != -1) {
40 fileOut.write(buffer, 0, bytesRead);
41 System.out.print("R"); // Reading progress indicator
42 }
43 System.out.println(); // End progress indicator line
44
45 sock.close(); // Close the socket and its streams
46 fileIn.close(); // Close file streams
47 fileOut.close();
48 }
49
50 private static void sendBytes(Socket sock, InputStream
fileIn)
51 throws IOException {
52 OutputStream sockOut = sock.getOutputStream();
53 int bytesRead; // Number of bytes read
54 byte[] buffer = new byte[BUFSIZE]; // Byte buffer
55 while ((bytesRead = fileIn.read(buffer)) != -1) {
56 sockOut.write(buffer, 0, bytesRead);
57 System.out.print("W"); // Writing progress indicator
58 }
59 sock.shutdownOutput(); // Finished sending
60 }
61 }
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
0 import java.net.Socket;
1 import java.io.IOException;
2 import java.io.InputStream;
3 import java.io.OutputStream;
4 import java.util.zip.GZIPOutputStream;
5 import java.util.logging.Logger;
6 import java.util.logging.Level;
7
8 public class CompressProtocol implements Runnable {
9
10 public static final int BUFSIZE = 1024; // Size of receive
buffer
11 private Socket clntSock;
12 private Logger logger;
13
14 public CompressProtocol(Socket clntSock, Logger
logger) {
15 this.clntSock = clntSock;
16 this.logger = logger;
17 }
18
19 public static void handleCompressClient(Socket
clntSock, Logger logger) {
20 try {
21 // Get the input and output streams from socket
22 InputStream in = clntSock.getInputStream();
23 GZIPOutputStream out = new
GZIPOutputStream(clntSock.getOutputStream());
24
25 byte[] buffer = new byte[BUFSIZE]; // Allocate
read/write buffer
26 int bytesRead; // Number of bytes read
27 // Receive until client closes connection, indicated
by -1 return
28 while ((bytesRead = in.read(buffer)) != -1)
29 out.write(buffer, 0, bytesRead);
30 out.finish(); // Flush bytes from GZIPOutputStream
31
32 logger.info("Client " +
clntSock.getRemoteSocketAddress() + " finished");
33 } catch (IOException ex) {
34 logger.log(Level.WARNING, "Exception in echo
protocol", ex);
35 }
36
37 try { // Close socket
38 clntSock.close();
39 } catch (IOException e) {
40 logger.info("Exception = " + e.getMessage());
41 }
42 }
43
44 public void run() {
45 handleCompressClient(this.clntSock, this.logger);
46 }
47 }
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实例:
service.execute(new CompressProtocol(clntSock,logger));
相关下载:
Java_TCPIP_Socket编程(doc)
http://download.csdn.net/detail/undoner/4940239
文献来源:
UNDONER(小杰博客) :http://blog.csdn.net/undoner
LSOFT.CN(琅软中国) :http://www.lsoft.cn