Java 套接字

转载 2011年09月13日 22:29:31

套接字(socket)为两台计算机之间的通信提供了一种机制,在 James Gosling 注意到 Java 语言之前,套接字就早已赫赫有名。该语言只是让您不必了解底层操作系统的细节就能有效地使用套接字。多数着重讨论 Java 编码的书或者未涵盖这个主题,或者给读者留下很大的想象空间。本教程将告诉您开始在代码中有效地使用套接字时,您真正需要知道哪些知识。我们将专门讨论以下问题:

  • 什么是套接字
  • 它位于您可能要写的程序的什么地方
  • 能工作的最简单的套接字实现 ― 以帮助您理解基础知识
  • 详细剖析另外两个探讨如何在多线程和具有连接池环境中使用套接字的示例
  • 简要讨论一个现实世界中的套接字应用程序

如果您能够描述如何使用 java.net 包中的类,那么本教程对您来说也许基础了点,虽然用它来提高一下还是不错的。如果您在 PC 和其它平台上使用套接字已经几年,那么最初的部分也许会使您觉得烦。但如果您不熟悉套接字,而且只是想知道什么是套接字以及如何在 Java 代码中有效地使用它们,那么本教程就是一个开始的好地方。


套接字基础
 1. 介绍

多数程序员,不管他们是否使用 Java 语言进行编码,都不想很多知道关于不同计算机上的应用程序彼此间如何通信的低级细节。程序员们希望处理更容易理解的更高级抽象。Java 程序员希望能用他们熟悉的 Java 构造,通过直观接口与对象交互。

套接字在两个领域中都存在 ― 我们宁愿避开的低级细节和我们更愿处理的抽象层。本教程讨论的低级细节将只限于理解抽象应用程序所必须的部分。


 2. 计算机组网 101

计算机以一种非常简单的方式进行相互间的操作和通信。计算机芯片是以 1 和 0 的形式存储并传输数据的开―闭转换器的集合。当计算机想共享数据时,它们所需做的全部就是以一致的速度、顺序、定时等等来回传输几百万比特和字节的数据流。每次想在两个应用程序之间进行信息通信时,您怎么会愿意担心那些细节呢?

为免除这些担心,我们需要每次都以相同方式完成该项工作的一组包协议。这将允许我们处理应用程序级的工作,而不必担心低级网络细节。这些成包协议称为协议栈(stack)。TCP/IP 是当今最常见的协议栈。多数协议栈(包括 TCP/IP)都大致对应于国际标准化组织(International Standards Organization,ISO)的开放系统互连参考模型(Open Systems Interconnect Reference Model,OSIRM)。OSIRM 认为在一个可靠的计算机组网中有七个逻辑层(见图)。各个地方的公司都对这个模型某些层的实现做了一些贡献,从生成电子信号(光脉冲、射频等等)到提供数据给应用程序。TCP/IP 映射到 OSI 模型中的两层的情形如图所示。

我们不想涉及层的太多细节,但您应该知道套接字位于什么地方。



 3. 套接字位于什么地方

套接字大致驻留在 OSI 模型的会话层(见图)。会话层夹在其上面向应用的层和其下的实时数据通信层之间。会话层为两台计算机之间的数据流提供管理和控制服务。作为该层的一部分,套接字提供一个隐藏从导线上获取比特和字节的复杂性的抽象。换句话说,套接字允许我们让应用程序表明它想发送一些字节即可传输数据。套接字隐藏了完成该项工作的具体细节。

当您打电话时,您的声音传到传感器,传感器把它转换成可以传输的电数据。电话机是人与电信网络的接口。您无须知道声音如何传输的细节,只要知道想打电话给谁就行了。同样地,套接字扮演隐藏在未知通道上传输 1 和 0 的复杂性的高级接口的角色。



 4. 把套接字暴露给应用程序

使用套接字的代码工作于表示层。表示层提供应用层能够使用的信息的公共表示。假设您打算把应用程序连接到只能识别 EBCDIC 的旧的银行系统。应用程序的域对象以 ASCII 格式存储信息。在这种情况下,您得负责在表示层上编写把数据从 EBCDIC 转换成 ASCII 的代码,然后(比方说)给应用层提供域对象。应用层然后就可以用域对象来做它想做的任何事情。

您编写的套接字处理代码只存在于表示层中。您的应用层无须知道套接字如何工作的任何事情。



 5. 什么是套接字?

既然我们已经知道套接字扮演的角色,那么剩下的问题是:什么是套接字?Bruce Eckel 在他的《Java 编程思想》一书中这样描述套接字:


套接字是一种软件抽象,用于表达两台机器之间的连接“终端”。对于一个给定的连接,每台机器上都有一个套接字,您也可以想象它们之间有一条虚拟的“电缆”,“电缆”的每一端都插入到套接字中。当然,机器之间的物理硬件和电缆连接都是完全未知的。抽象的全部目的是使我们无须知道不必知道的细节。

简言之,一台机器上的套接字与另一台机器上的套接字交谈就创建一条通信通道。程序员可以用该通道来在两台机器之间发送数据。当您发送数据时,TCP/IP 协议栈的每一层都会添加适当的报头信息来包装数据。这些报头帮助协议栈把您的数据送到目的地。好消息是 Java 语言通过"流"为您的代码提供数据,从而隐藏了所有这些细节,这也是为什么它们有时候被叫做流套接字(streaming socket)的原因。

把套接字想成两端电话上的听筒 ― 我和您通过专用通道在我们的电话听筒上讲话和聆听。直到我们决定挂断电话,对话才会结束(除非我们在使用蜂窝电话)。而且我们各自的电话线路都占线,直到我们挂断电话。

如果想在没有更高级机制如 ORB(以及 CORBA、RMI、IIOP 等等)开销的情况下进行两台计算机之间的通信,那么套接字就适合您。套接字的低级细节相当棘手。幸运的是,Java 平台给了您一些虽然简单但却强大的更高级抽象,使您可以容易地创建和使用套接字。


 6. 套接字的类型

一般而言,Java 语言中的套接字有以下两种形式:

  • TCP 套接字(由 Socket 类实现,稍后我们将讨论这个类)
  • UDP 套接字(由 DatagramSocket 类实现)

TCP 和 UDP 扮演相同角色,但做法不同。两者都接收传输协议数据包并将其内容向前传送到表示层。TCP 把消息分解成数据包(数据报,datagrams)并在接收端以正确的顺序把它们重新装配起来。TCP 还处理对遗失数据包的重传请求。有了 TCP,位于上层的层要担心的事情就少多了。UDP 不提供装配和重传请求这些功能。它只是向前传送信息包。位于上层的层必须确保消息是完整的并且是以正确的顺序装配的。

一般而言,UDP 强加给您的应用程序的性能开销更小,但只在应用程序不会突然交换大量数据并且不必装配大量数据报以完成一条消息的时候。否则,TCP 才是最简单或许也是最高效的选择。

因为多数读者都喜欢 TCP 胜过 UDP,所以我们将把讨论限制在 Java 语言中面向 TCP 的类。


一个秘密的套接字
 1. 介绍

Java 平台在 java.net 包中提供套接字的实现。在本教程中,我们将与 java.net 中的以下三个类一起工作:

  • URLConnection
  • Socket
  • ServerSocket

java.net 中还有更多的类,但这些是您将最经常碰到的。让我们从 URLConnection 开始。这个类为您不必了解任何底层套接字细节就能在 Java 代码中使用套接字提供一种途径。


 2. 甚至不用尝试就可使用套接字

URLConnection 类是所有在应用程序和 URL 之间创建通信链路的类的抽象超类。URLConnection 在获取 Web 服务器上的文档方面特别有用,但也可用于连接由 URL 标识的任何资源。该类的实例既可用于从资源中读,也可用于往资源中写。例如,您可以连接到一个 servlet 并发送一个格式良好的 XML String 到服务器上进行处理。URLConnection 的具体子类(例如 HttpURLConnection)提供特定于它们实现的额外功能。对于我们的示例,我们不想做任何特别的事情,所以我们将使用 URLConnection 本身提供的缺省行为。

连接到 URL 包括几个步骤:

  • 创建 URLConnection
  • 用各种 setter 方法配置它
  • 连接到 URL
  • 用各种 getter 方法与它交互

接着,我们将看一些演示如何用 URLConnection 来从服务器请求文档的样本代码


 3. URLClient 类

我们将从 URLClient 类的结构讲起。

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class URLClient {  
  5.     protected URLConnection connection;  
  6.   
  7.     public static void main(String[] args) {  
  8.     }  
  9.     public String getDocumentAt(String urlString) {  
  10.     }  
  11. }  

要做的第一件事是导入 java.netjava.io

我们给我们的类一个实例变量以保存一个 URLConnection

我们的类有一个 main() 方法,它处理浏览文档的逻辑流。我们的类还有一个 getDocumentAt() 方法,该方法连接到服务器并向它请求给定文档。下面我们将分别探究这些方法的细节。

 4. 浏览文档

main() 方法处理浏览文档的逻辑流:

Java代码  收藏代码
  1. public static void main(String[] args) {  
  2.     URLClient client = new URLClient();  
  3.     String yahoo = client.getDocumentAt("http://www.yahoo.com");  
  4.     System.out.println(yahoo);  
  5. }  

 

我们的 main() 方法只是创建一个新的 URLClient 并用一个有效的 URL String 调用 getDocumentAt()。当调用返回该文档时,我们把它存储在 String,然后将它打印到控制台。然而,实际的工作是在 getDocumentAt() 方法中完成的。

 5. 从服务器请求一个文档

getDocumentAt() 方法处理获取 Web 上的文档的实际工作:

Java代码  收藏代码
  1. public String getDocumentAt(String urlString) {  
  2.     StringBuffer document = new StringBuffer();  
  3.     try {  
  4.         URL url = new URL(urlString);  
  5.         URLConnection conn = url.openConnection();  
  6.         BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));  
  7.   
  8.         String line = null;  
  9.         while ((line = reader.readLine()) != null)  
  10.           document.append(line + "\n");  
  11.         reader.close();  
  12.     } catch (MalformedURLException e) {  
  13.         System.out.println("Unable to connect to URL: " + urlString);  
  14.     } catch (IOException e) {  
  15.         System.out.println("IOException when connecting to URL: " + urlString);  
  16.     }  
  17.     return document.toString();  
  18. }  
 

 

getDocumentAt() 方法有一个 String 参数,该参数包含我们想获取的文档的 URL。我们在开始时创建一个 StringBuffer 来保存文档的行。然后我们用我们传进去的 urlString 创建一个新 URL。接着创建一个 URLConnection 并打开它:

Java代码  收藏代码
  1. URLConnection conn = url.openConnection();  

一旦有了一个 URLConnection,我们就获取它的 InputStream 并包装进 InputStreamReader,然后我们又把 InputStreamReader 包装进 BufferedReader 以使我们能够读取想从服务器上获取的文档的行。在 Java 代码中处理套接字时,我们将经常使用这种包装技术,但我们不会总是详细讨论它。在我们继续往前讲之前,您应该熟悉它:

Java代码  收藏代码
  1. BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));  
 

有了 BufferedReader,就使得我们能够容易地读取文档内容。我们在 while 循环中调用 reader 上的 readLine()

Java代码  收藏代码
  1. String line = null;  
  2. while ((line = reader.readLine()) != null)  
  3.     document.append(line + "\n");  
 

readLine() 的调用将直至碰到一个从 InputStream 传入的行终止符(例如换行符)时才阻塞。如果没碰到,它将继续等待。只有当连接被关闭时,它才会返回 null。在这个案例中,一旦我们获取一个行(line),我们就把它连同一个换行符一起附加(append)到名为 documentStringBuffer 上。这保留了服务器端上读取的文档的格式。

我们在读完行之后关闭 BufferedReader

Java代码  收藏代码
  1. reader.close();  

如果提供给 URL 构造器的 urlString 是无效的,那么将抛出 MalformedURLException。如果发生了别的错误,例如当从连接上获取 InputStream 时,那么将抛出 IOException


 6. 总结

实际上,URLConnection 使用套接字从我们指定的 URL 中读取信息(它只是解析成 IP 地址),但我们无须了解它,我们也不关心。但有很多事;我们马上就去看看。

在继续往前讲之前,让我们回顾一下创建和使用 URLConnection 的步骤:

  1. 用您想连接的资源的有效 URL String 实例化一个 URL(如有问题则抛出 MalformedURLException)。

  2. 打开该 URL 上的一个连接。

  3. 把该连接的 InputStream 包装进 BufferedReader 以使您能够读取行。

  4. BufferedReader 读文档。

  5. 关闭 BufferedReader

附: URLClient 的完整代码清单:

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class URLClient {  
  5.     protected HttpURLConnection connection;  
  6.     public String getDocumentAt(String urlString) {  
  7.         StringBuffer document = new StringBuffer();  
  8.         try {  
  9.             URL url = new URL(urlString);  
  10.             URLConnection conn = url.openConnection();  
  11.             BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));  
  12.   
  13.             String line = null;  
  14.             while ((line = reader.readLine()) != null)  
  15.                 document.append(line + "\n");  
  16.   
  17.             reader.close();  
  18.         } catch (MalformedURLException e) {  
  19.             System.out.println("Unable to connect to URL: " + urlString);  
  20.         } catch (IOException e) {  
  21.             System.out.println("IOException when connecting to URL: " + urlString);  
  22.         }  
  23.   
  24.         return document.toString();  
  25.     }  
  26.     public static void main(String[] args) {  
  27.         URLClient client = new URLClient();  
  28.         String yahoo = client.getDocumentAt("http://www.yahoo.com");  
  29.   
  30.         System.out.println(yahoo);  
  31.     }  


一个简单示例
 1. 背景

我们将在本部分讨论的示例将阐明在 Java 代码中如何使用 SocketServerSocket。客户机用 Socket 连接到服务器。服务器用 ServerSocket 在端口 3000 侦听。客户机请求服务器 C: 驱动器上的文件内容。

为清楚起见,我们把示例分解成客户机端和服务器端。最后我们将把它们组合起来以使您能看到整体模样。

我们在使用 JDK 1.2 的 IBM VisualAge for Java 3.5 上开发这些代码。要自己创建这个示例,您应有完好的 JDK 1.1.7 或更高版本。客户机和服务器将只在一台机器上运行,所以您不必担心是否有一个可用的网络。


 2. 创建 RemoteFileClient 类

这里是 RemoteFileClient 类的结构:

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class RemoteFileClient {  
  5.     protected String hostIp;  
  6.     protected int hostPort;  
  7.     protected BufferedReader socketReader;  
  8.     protected PrintWriter socketWriter;  
  9.   
  10.     public RemoteFileClient(String aHostIp, int aHostPort) {  
  11.         hostIp = aHostIp;  
  12.         hostPort = aHostPort;  
  13.     }  
  14.     public static void main(String[] args) {  
  15.     }  
  16.     public void setUpConnection() {  
  17.     }  
  18.     public String getFile(String fileNameToGet) {  
  19.     }  
  20.     public void tearDownConnection() {  
  21.     }  
  22. }  

首先我们导入 java.netjava.iojava.net 包为您提供您需要的套接字工具。java.io 包为您提供对流进行读写的工具,这是您与 TCP 套接字通信的唯一途径。

我们给我们的类实例变量以支持对套接字流的读写和存储我们将连接到的远程主机的详细信息。

我们类的构造器有两个参数:远程主机的 IP 地址和端口号各一个,而且构造器将它们赋给实例变量。

我们的类有一个 main() 方法和三个其它方法。稍后我们将探究这些方法的细节。现在您只需知道 setUpConnection() 将连接到远程服务器,getFile() 将向远程服务器请求 fileNameToGet 的内容以及 tearDownConnection() 将从远程服务器上断开。


 3. 实现 main()

这里我们实现 main() 方法,它将创建 RemoteFileClient 并用它来获取远程文件的内容,然后打印结果:

Java代码  收藏代码
  1. public static void main(String[] args) {  
  2.     RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1"3000);  
  3.     remoteFileClient.setUpConnection();  
  4.     String fileContents =  
  5.         remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");  
  6.     remoteFileClient.tearDownConnection();  
  7.   
  8.     System.out.println(fileContents);  
  9. }   

main() 方法用主机的 IP 地址和端口号实例化一个新 RemoteFileClient(客户机)。然后,我们告诉客户机建立一个到主机的连接(稍后有更详细的讨论)。接着,我们告诉客户机获取主机上一个指定文件的内容。最后,我们告诉客户机断开它到主机的连接。我们把文件内容打印到控制台,只是为了证明一切都是按计划进行的。


 4. 建立连接

这里我们实现 setUpConnection() 方法,它将创建我们的 Socket 并让我们访问该套接字的流:

Java代码  收藏代码
  1. public void setUpConnection() {  
  2.     try {  
  3.         Socket client = new Socket(hostIp, hostPort);  
  4.   
  5.         socketReader = new BufferedReader(  
  6.                    new InputStreamReader(client.getInputStream()));  
  7.         socketWriter = new PrintWriter(client.getOutputStream());  
  8.   
  9.     } catch (UnknownHostException e) {  
  10.         System.out.println("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);  
  11.     } catch (IOException e) {  
  12.         System.out.println("Error setting up socket connection: " + e);  
  13.     }  
  14. }  

setUpConnection() 方法用主机的 IP 地址和端口号创建一个 Socket

Java代码  收藏代码
  1. Socket client = new Socket(hostIp, hostPort);  

我们把 SocketInputStream 包装进 BufferedReader 以使我们能够读取流的行。然后,我们把 SocketOutputStream 包装进 PrintWriter 以使我们能够发送文件请求到服务器:

Java代码  收藏代码
  1. socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));  
  2. socketWriter = new PrintWriter(client.getOutputStream());  

请记住我们的客户机和服务器只是来回传送字节。客户机和服务器都必须知道另一方即将发送的是什么以使它们能够作出适当的响应。在这个案例中,服务器知道我们将发送一条有效的文件路径。

当您实例化一个 Socket 时,将抛出 UnknownHostException。这里我们不特别处理它,但我们打印一些信息到控制台以告诉我们发生了什么错误。同样地,当我们试图获取 SocketInputStreamOutputStream 时,如果抛出了一个一般 IOException,我们也打印一些信息到控制台。这是本教程的一般做法。在产品代码中,我们应该做得更完善些。


 5. 与主机交谈

这里我们实现 getFile() 方法,它将告诉服务器我们想要什么文件并在服务器传回其内容时接收该内容。

Java代码  收藏代码
  1. public String getFile(String fileNameToGet) {  
  2.     StringBuffer fileLines = new StringBuffer();  
  3.   
  4.     try {  
  5.         socketWriter.println(fileNameToGet);  
  6.         socketWriter.flush();  
  7.   
  8.         String line = null;  
  9.         while ((line = socketReader.readLine()) != null)  
  10.             fileLines.append(line + "\n");  
  11.     } catch (IOException e) {  
  12.         System.out.println("Error reading from file: " + fileNameToGet);  
  13.     }  
  14.   
  15.     return fileLines.toString();  
  16. }  

getFile() 方法的调用要求一个有效的文件路径 String。它首先创建名为 fileLinesStringBufferfileLines 用于存储我们读自服务器上的文件的每一行。

Java代码  收藏代码
  1. StringBuffer fileLines = new StringBuffer();  

try{}catch{} 块中,我们用 PrintWriter 把请求发送到主机,PrintWriter 是我们在创建连接期间建立的。

Java代码  收藏代码
  1. socketWriter.println(fileNameToGet);  
  2. socketWriter.flush();  

请注意这里我们是 flush()PrintWriter,而不是关闭它。这迫使数据被发送到服务器而不关闭 Socket

一旦我们已经写到 Socket,我们就希望有一些响应。我们不得不在 SocketInputStream 上等待它,我们通过在 while 循环中调用 BufferedReader 上的 readLine() 来达到这个目的。我们把每一个返回行附加到 fileLines StringBuffer(带有一个换行符以保护行):

Java代码  收藏代码
  1. String line = null;  
  2. while ((line = socketReader.readLine()) != null)  
  3.     fileLines.append(line + "\n");  


 6. 断开连接

这里我们实现 tearDownConnection() 方法,它将在我们使用完毕连接后负责“清除”:

Java代码  收藏代码
  1. public void tearDownConnection() {  
  2.     try {  
  3.         socketWriter.close();  
  4.         socketReader.close();  
  5.     } catch (IOException e) {  
  6.         System.out.println("Error tearing down socket connection: " + e);  
  7.     }  
  8. }  

tearDownConnection() 方法只是分别关闭我们在 SocketInputStreamOutputStream 上创建的 BufferedReaderPrintWriter。这样做会关闭我们从 Socket 获取的底层流,所以我们必须捕捉可能的 IOException


 7. 总结一下客户机

我们的类研究完了。在我们继续往前讨论服务器端的情况之前,让我们回顾一下创建和使用 Socket 的步骤:

  1. 用您想连接的机器的 IP 地址和端口实例化 Socket(如有问题则抛出 Exception)。

  2. 获取 Socket 上的流以进行读写。

  3. 把流包装进 BufferedReader/PrintWriter 的实例,如果这样做能使事情更简单的话。

  4. Socket 进行读写。

  5. 关闭打开的流。

   附:RemoteFileClient 的代码清单

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class RemoteFileClient {  
  5.     protected BufferedReader socketReader;  
  6.     protected PrintWriter socketWriter;  
  7.     protected String hostIp;  
  8.     protected int hostPort;  
  9.   
  10.     public RemoteFileClient(String aHostIp, int aHostPort) {  
  11.         hostIp = aHostIp;  
  12.         hostPort = aHostPort;  
  13.     }  
  14.     public String getFile(String fileNameToGet) {  
  15.         StringBuffer fileLines = new StringBuffer();  
  16.   
  17.         try {  
  18.             socketWriter.println(fileNameToGet);  
  19.             socketWriter.flush();  
  20.   
  21.             String line = null;  
  22.             while ((line = socketReader.readLine()) != null)  
  23.                 fileLines.append(line + "\n");  
  24.         } catch (IOException e) {  
  25.             System.out.println("Error reading from file: " + fileNameToGet);  
  26.         }  
  27.   
  28.         return fileLines.toString();  
  29.     }  
  30.     public static void main(String[] args) {  
  31.         RemoteFileClient remoteFileClient = new RemoteFileClient("127.0.0.1"3000);  
  32.         remoteFileClient.setUpConnection();  
  33.         String fileContents = remoteFileClient.getFile("C:\\WINNT\\Temp\\RemoteFile.txt");  
  34.         remoteFileClient.tearDownConnection();  
  35.   
  36.         System.out.println(fileContents);  
  37.     }  
  38.     public void setUpConnection() {  
  39.         try {  
  40.             Socket client = new Socket(hostIp, hostPort);  
  41.   
  42.             socketReader = new BufferedReader(new InputStreamReader(client.getInputStream()));  
  43.             socketWriter = new PrintWriter(client.getOutputStream());  
  44.   
  45.         } catch (UnknownHostException e) {  
  46.             System.out.println("Error setting up socket connection: unknown host at " + hostIp + ":" + hostPort);  
  47.         } catch (IOException e) {  
  48.             System.out.println("Error setting up socket connection: " + e);  
  49.         }  
  50.     }  
  51.     public void tearDownConnection() {  
  52.         try {  
  53.             socketWriter.close();  
  54.             socketReader.close();  
  55.         } catch (IOException e) {  
  56.             System.out.println("Error tearing down socket connection: " + e);  
  57.         }  
  58.     }  
  59. }  


 8. 创建 RemoteFileServer 类

这里是 RemoteFileServer 类的结构:

 

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class RemoteFileServer {  
  5.     protected int listenPort = 3000;  
  6.     public static void main(String[] args) {  
  7.     }  
  8.     public void acceptConnections() {  
  9.     }  
  10.     public void handleConnection(Socket incomingConnection) {  
  11.     }  
  12. }   

跟客户机中一样,我们首先导入 java.netjava.io。接着,我们给我们的类一个实例变量以保存端口,我们从该端口侦听进入的连接。缺省情况下,端口是 3000。

我们的类有一个 main() 方法和两个其它方法。稍后我们将探究这些方法的细节。现在您只需知道 acceptConnections() 将允许客户机连接到服务器以及 handleConnection() 与客户机 Socket 交互以将您所请求的文件的内容发送到客户机。


 9. 实现 main()

这里我们实现 main() 方法,它将创建 RemoteFileServer 并告诉它接受连接:

 

Java代码  收藏代码
  1. public static void main(String[] args) {  
  2.     RemoteFileServer server = new RemoteFileServer();  
  3.     server.acceptConnections();  
  4. }   

服务器端的 main() 方法甚至比客户机端的更简单。我们实例化一个新 RemoteFileServer,它将在缺省侦听端口上侦听进入的连接请求。然后我们调用 acceptConnections() 来告诉该 server 进行侦听。


 10. 接受连接

这里我们实现 acceptConnections() 方法,它将创建一个 ServerSocket 并等待连接请求:

Java代码  收藏代码
  1. public void acceptConnections() {  
  2.     try {  
  3.         ServerSocket server = new ServerSocket(listenPort);  
  4.         Socket incomingConnection = null;  
  5.         while (true) {  
  6.             incomingConnection = server.accept();  
  7.             handleConnection(incomingConnection);  
  8.         }  
  9.     } catch (BindException e) {  
  10.         System.out.println("Unable to bind to port " + listenPort);  
  11.     } catch (IOException e) {  
  12.         System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);  
  13.     }  
  14. } <span>   
  15. </span>  

 

acceptConnections() 用欲侦听的端口号来创建 ServerSocket。然后我们通过调用该 ServerSocketaccept() 来告诉它开始侦听。accept() 方法将造成阻塞直到来了一个连接请求。此时,accept() 返回一个新的 Socket,这个 Socket 绑定到服务器上一个随机指定的端口,返回的 Socket 被传递给 handleConnection()。请注意我们在一个无限循环中处理对连接的接受。这里不支持任何关机。

无论何时如果您创建了一个无法绑定到指定端口(可能是因为别的什么控制了该端口)的 ServerSocket,Java 代码都将抛出一个错误。所以这里我们必须捕捉可能的 BindException。就跟在客户机端上时一样,我们必须捕捉 IOException,当我们试图在 ServerSocket 上接受连接时,它就会被抛出。请注意,您可以通过用毫秒数调用 setSoTimeout() 来为 accept() 调用设置超时,以避免实际长时间的等待。调用 setSoTimeout() 将使 accept() 经过指定占用时间后抛出 IOException


 11. 处理连接

这里我们实现 handleConnection() 方法,它将用连接的流来接收输入和写输出:

Java代码  收藏代码
  1. public void handleConnection(Socket incomingConnection) {  
  2.     try {  
  3.         OutputStream outputToSocket = incomingConnection.getOutputStream();  
  4.         InputStream inputFromSocket = incomingConnection.getInputStream();  
  5.   
  6.         BufferedReader streamReader =  
  7.             new BufferedReader(new InputStreamReader(inputFromSocket));  
  8.   
  9.         FileReader fileReader = new FileReader(new File(streamReader.readLine()));  
  10.   
  11.         BufferedReader bufferedFileReader = new BufferedReader(fileReader);  
  12.         PrintWriter streamWriter =  
  13.             new PrintWriter(incomingConnection.getOutputStream());  
  14.         String line = null;  
  15.         while ((line = bufferedFileReader.readLine()) != null) {  
  16.             streamWriter.println(line);  
  17.         }  
  18.   
  19.         fileReader.close();  
  20.         streamWriter.close();  
  21.         streamReader.close();  
  22.     } catch (Exception e) {  
  23.         System.out.println("Error handling a client: " + e);  
  24.     }  
  25. }  

跟在客户机中一样,我们用 getOutputStream()getInputStream() 来获取与我们刚创建的 Socket 相关联的流。跟在客户机端一样,我们把 InputStream 包装进 BufferedReader,把 OutputStream 包装进 PrintWriter。在服务器端上,我们需要添加一些代码,用来读取目标文件和把内容逐行发送到客户机。这里是重要的代码:

Java代码  收藏代码
  1. FileReader fileReader = new FileReader(new File(streamReader.readLine()));  
  2. BufferedReader bufferedFileReader = new BufferedReader(fileReader);  
  3. String line = null;  
  4. while ((line = bufferedFileReader.readLine()) != null) {  
  5.      streamWriter.println(line);  
  6. }  

这些代码值得详细解释。让我们一点一点来看:

 

Java代码  收藏代码
  1. FileReader fileReader = new FileReader(new File(streamReader.readLine()));  

 首先,我们使用 SocketInputStreamBufferedReader。我们应该获取一条有效的文件路径,所以我们用该路径名构造一个新 File。我们创建一个新 FileReader 来处理读文件的操作。

Java代码  收藏代码
  1. BufferedReader bufferedFileReader = new BufferedReader(fileReader);  

这里我们把 FileReader 包装进 BufferedReader 以使我们能够逐行地读该文件。

接着,我们调用 BufferedReaderreadLine()。这个调用将造成阻塞直到有字节到来。我们获取一些字节之后就把它们放到本地的 line 变量中,然后再写出到客户机上。完成读写操作之后,我们就关闭打开的流。

请注意我们在完成从 Socket 的读操作之后关闭 streamWriterstreamReader。您或许会问我们为什么不在读取文件名之后立刻关闭 streamReader。原因是当您这样做时,您的客户机将不会获取任何数据。如果您在关闭 streamWriter 之前关闭 streamReader,则您可以往 Socket 写任何东西,但却没有任何数据能通过通道(通道被关闭了)。




 12. 总结一下服务器

在我们接着讨论另一个更实际的示例之前,让我们回顾一下创建和使用 ServerSocket 的步骤:

  1. 用一个您想让它侦听传入客户机连接的端口来实例化一个 ServerSocket(如有问题则抛出 Exception)。

  2. 调用 ServerSocketaccept() 以在等待连接期间造成阻塞。

  3. 获取位于该底层 Socket 的流以进行读写操作。

  4. 按使事情简单化的原则包装流。

  5. Socket 进行读写。

  6. 关闭打开的流(并请记住,永远不要在关闭 Writer 之前关闭 Reader)。

  附: RemoteFileServer 的完整的代码清单

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class RemoteFileServer {  
  5.     int listenPort;  
  6.     public RemoteFileServer(int aListenPort) {  
  7.         listenPort = aListenPort;  
  8.     }  
  9.     public void acceptConnections() {  
  10.         try {  
  11.             ServerSocket server = new ServerSocket(listenPort);  
  12.             Socket incomingConnection = null;  
  13.             while (true) {  
  14.                 incomingConnection = server.accept();  
  15.                 handleConnection(incomingConnection);  
  16.             }  
  17.         } catch (BindException e) {  
  18.             System.out.println("Unable to bind to port " + listenPort);  
  19.         } catch (IOException e) {  
  20.             System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);  
  21.         }  
  22.     }  
  23.     public void handleConnection(Socket incomingConnection) {  
  24.         try {  
  25.             OutputStream outputToSocket = incomingConnection.getOutputStream();  
  26.             InputStream inputFromSocket = incomingConnection.getInputStream();  
  27.   
  28.             BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputFromSocket));  
  29.   
  30.             FileReader fileReader = new FileReader(new File(streamReader.readLine()));  
  31.   
  32.             BufferedReader bufferedFileReader = new BufferedReader(fileReader);  
  33.             PrintWriter streamWriter = new PrintWriter(incomingConnection.getOutputStream());  
  34.             String line = null;  
  35.             while ((line = bufferedFileReader.readLine()) != null) {  
  36.                 streamWriter.println(line);  
  37.             }  
  38.   
  39.             fileReader.close();  
  40.             streamWriter.close();  
  41.             streamReader.close();  
  42.         } catch (Exception e) {  
  43.             System.out.println("Error handling a client: " + e);  
  44.         }  
  45.     }  
  46.     public static void main(String[] args) {  
  47.         RemoteFileServer server = new RemoteFileServer(3000);  
  48.         server.acceptConnections();  
  49.     }  
  50. }  
 

一个多线程的示例
 1. 介绍

前面的示例教给您基础知识,但并不能令您更深入。如果您到此就停止了,那么您一次只能处理一台客户机。原因是 handleConnection() 是一个阻塞方法。只有当它完成了对当前连接的处理时,服务器才能接受另一个客户机。在多数时候,您将需要(也有必要)一个多线程服务器。

要开始同时处理多台客户机,并不需要对 RemoteFileServer 作太多改变。事实上,要是我们前面讨论过待发(backlog),那我们就只需改变一个方法,虽然我们将需要创建一些新东西来处理进入的连接。这里我们还将向您展示 ServerSocket 如何处理众多等待(备份)使用服务器的客户机。本示例对线程的低效使用,所以请耐心点。


 2. 接受(太多)连接

这里我们实现改动过的 acceptConnections() 方法,它将创建一个能够处理待发请求的 ServerSocket,并告诉 ServerSocket 接受连接:

Java代码  收藏代码
  1. public void acceptConnections() {  
  2.         try {  
  3.         ServerSocket server = new ServerSocket(listenPort, 5);  
  4.         Socket incomingConnection = null;  
  5.         while (true) {  
  6.             incomingConnection = server.accept();  
  7.             handleConnection(incomingConnection);  
  8.         }  
  9.     } catch (BindException e) {  
  10.     System.out.println("Unable to bind to port " + listenPort);  
  11.     } catch (IOException e) {  
  12.     System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);  
  13.     }  
  14. }  

新的 server 仍然需要 acceptConnections(),所以这些代码实际上是一样的。突出显示的行表示一个重大的不同。对这个多线程版,我们现在可以指定客户机请求的最大数目,这些请求都能在实例化 ServerSocket 期间处于待发状态。如果我们没有指定客户机请求的最大数目,则我们假设使用缺省值 50。

这里是它的工作机制。假设我们指定待发数(backlog 值)是 5 并且有五台客户机请求连接到我们的服务器。我们的服务器将着手处理第一个连接,但处理该连接需要很长时间。由于我们的待发值是 5,所以我们一次可以放五个请求到队列中。我们正在处理一个,所以这意味着还有其它五个正在等待。等待的和正在处理的一共有六个。当我们的服务器仍忙于接受一号连接(记住队列中还有 2―6 号)时,如果有第七个客户机提出连接申请,那么,该第七个客户机将遭到拒绝。我们将在带有连接池服务器示例中说明如何限定能同时连接的客户机数目。


 3. 处理连接:第 1 部分

这里我们将讨论 handleConnection() 方法的结构,这个方法生成一个新的 Thread 来处理每个连接。我们将分两部分讨论这个问题。这一屏我们将着重该方法本身,然后在下一屏研究该方法所使用的 ConnectionHandler 助手类的结构。

Java代码  收藏代码
  1. public void handleConnection(Socket connectionToHandle) {  
  2.      new Thread(new ConnectionHandler(connectionToHandle)).start();  
  3. }  

我们对 RemoteFileServer 所做的大改动就体现在这个方法上。我们仍然在服务器接受一个连接之后调用 handleConnection(),但现在我们把该 Socket 传递给 ConnectionHandler 的一个实例,它是 Runnable 的。我们用 ConnectionHandler 创建一个新 Thread 并启动它。ConnectionHandlerrun() 方法包含Socket 读/写和读 File 的代码,这些代码原来在 RemoteFileServerhandleConnection() 中。


 4. 处理连接:第 2 部分

这里是 ConnectionHandler 类的结构:

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class ConnectionHandler implements Runnable{  
  5.    Socket socketToHandle;  
  6.   
  7.    public ConnectionHandler(Socket aSocketToHandle) {  
  8.       socketToHandle = aSocketToHandle;  
  9.    }  
  10.   
  11.    public void run() {  
  12.    }  
  13. }  

这个助手类相当简单。跟我们到目前为止的其它类一样,我们导入 java.netjava.io。该类只有一个实例变量 socketToHandle,它保存由该实例处理的 Socket

类的构造器用一个 Socket 实例作参数并将它赋给 socketToHandle

请注意该类实现了 Runnable 接口。实现这个接口的类都必须实现 run() 方法,我们的类就是这样做的。稍后我们将探究 run() 的细节。现在只需知道它将实际处理连接,所用的代码跟我们先前在 RemoteFileServer 类中看到的是一样的。


 5. 实现 run()

这里我们实现 run() 方法,它将攫取我们的连接的流,用它来读写该连接,并在任务完成之后关闭它:

Java代码  收藏代码
  1. public void run() {  
  2.         try {  
  3.             PrintWriter streamWriter = new PrintWriter(socketToHandle.getOutputStream());  
  4.             BufferedReader streamReader =  
  5.                 new BufferedReader(new InputStreamReader(socketToHandle.getInputStream()));  
  6.   
  7.             String fileToRead = streamReader.readLine();  
  8.             BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));  
  9.   
  10.             String line = null;  
  11.             while ((line = fileReader.readLine()) != null)  
  12.                 streamWriter.println(line);  
  13.   
  14.             fileReader.close();  
  15.             streamWriter.close();  
  16.             streamReader.close();  
  17.         } catch (Exception e) {  
  18.             System.out.println("Error handling a client: " + e);  
  19.         }  
  20.     }  

ConnectionHandlerrun() 方法所做的事情就是 RemoteFileServer 上的 handleConnection() 所做的事情。首先,我们把 InputStreamOutputStream 分别包装(用 Socket getOutputStream()getInputStream())进 BufferedReaderPrintWriter。然后我们用这些代码逐行地读目标文件:

Java代码  收藏代码
  1. FileReader fileReader = new FileReader(new File(streamReader.readLine()));  
  2.         BufferedReader bufferedFileReader = new BufferedReader(fileReader);  
  3.         String line = null;  
  4.         while ((line = bufferedFileReader.readLine()) != null) {  
  5.              streamWriter.println(line);  
  6.         }   

请记住我们应该从客户机获取一条有效的文件路径,这样用该路径名构造一个新 File,把它包装进 FileReader 以处理读文件的操作,然后把它包装进 BufferedReader 以让我们逐行地读该文件。我们在 while 循环中调用 BufferedReader 上的 readLine() 直到不再有要读的行。请记注,对 readLine() 的调用将造成阻塞,直到有字节来到为止。我们获取一些字节之后就把它们放到本地的 line 变量中,然后写出到客户机上。完成读写操作之后,我们关闭打开的流。


 6. 总结一下多线程服务器

我们的多线程服务器研究完了。在我们接着讨论带有连接池示例之前,让我们回顾一下创建和使用“多线程版”的服务器的步骤:

  1. 修改 acceptConnections() 以用缺省为 50(或任何您想要的大于 1 的指定数字)实例化 ServerSocket
  2. 修改 ServerSockethandleConnection() 以用 ConnectionHandler 的一个实例生成一个新的 Thread

  3. 借用 RemoteFileServerhandleConnection() 方法的代码实现 ConnectionHandler 类。

   附: MultithreadedRemoteFileServer 的完整代码清单

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class MultithreadedRemoteFileServer {  
  5.     protected int listenPort;  
  6.     public MultithreadedRemoteFileServer(int aListenPort) {  
  7.         listenPort = aListenPort;  
  8.     }  
  9.     public void acceptConnections() {  
  10.         try {  
  11.             ServerSocket server = new ServerSocket(listenPort, 5);  
  12.             Socket incomingConnection = null;  
  13.             while (true) {  
  14.                 incomingConnection = server.accept();  
  15.                 handleConnection(incomingConnection);  
  16.             }  
  17.         } catch (BindException e) {  
  18.             System.out.println("Unable to bind to port " + listenPort);  
  19.         } catch (IOException e) {  
  20.             System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);  
  21.         }  
  22.     }  
  23.     public void handleConnection(Socket connectionToHandle) {  
  24.         new Thread(new ConnectionHandler(connectionToHandle)).start();  
  25.     }  
  26.     public static void main(String[] args) {  
  27.         MultithreadedRemoteFileServer server = new MultithreadedRemoteFileServer(3000);  
  28.         server.acceptConnections();  
  29.     }  
  30. }  
 

        ConnectionHandler 的完整代码清单

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3.   
  4. public class ConnectionHandler implements Runnable {  
  5.     protected Socket socketToHandle;  
  6.     public ConnectionHandler(Socket aSocketToHandle) {  
  7.         socketToHandle = aSocketToHandle;  
  8.     }  
  9.     public void run() {  
  10.         try {  
  11.             PrintWriter streamWriter = new PrintWriter(socketToHandle.getOutputStream());  
  12.             BufferedReader streamReader = new BufferedReader(new InputStreamReader(socketToHandle.getInputStream()));  
  13.   
  14.             String fileToRead = streamReader.readLine();  
  15.             BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));  
  16.   
  17.             String line = null;  
  18.             while ((line = fileReader.readLine()) != null)  
  19.                 streamWriter.println(line);  
  20.   
  21.             fileReader.close();  
  22.             streamWriter.close();  
  23.             streamReader.close();  
  24.         } catch (Exception e) {  
  25.             System.out.println("Error handling a client: " + e);  
  26.         }  
  27.     }  
  28. }  
 

一个带有连接池的示例
 1. 介绍

我们现在已经拥有的 MultithreadedServer 每当有客户机申请一个连接时都在一个新 Thread 中创建一个新 ConnectionHandler。这意味着可能有一捆 Thread “躺”在我们周围。而且创建 Thread 的系统开销并不是微不足道的。如果性能成为了问题(也请不要事到临头才意识到它),更高效地处理我们的服务器是件好事。那么,我们如何更高效地管理服务器端呢?我们可以维护一个进入的连接池,一定数量的 ConnectionHandler 将为它提供服务。这种设计能带来以下好处:

  • 它限定了允许同时连接的数目。
  • 我们只需启动 ConnectionHandler Thread 一次。

幸运的是,跟在我们的多线程示例中一样,往代码中添加“池”不需要来一个大改动。事实上,应用程序的客户机端根本就不受影响。在服务器端,我们在服务器启动时创建一定数量的 ConnectionHandler,我们把进入的连接放入“池”中并让 ConnectionHandler 打理剩下的事情。这种设计中有很多我们不打算讨论的可能存在的技巧。例如,我们可以通过限定允许在“池”中建立的连接的数目来拒绝客户机。

请注意:我们将不会再次讨论 acceptConnections()。这个方法跟前面示例中的完全一样。它无限循环地调用 ServerSocket 上的 accept() 并把连接传递到 handleConnection()


 2. 创建 PooledRemoteFileServer 类

这里是 PooledRemoteFileServer 类的结构:

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3. import java.util.*;  
  4.   
  5. public class PooledRemoteFileServer {  
  6.     protected int maxConnections;  
  7.     protected int listenPort;  
  8.     protected ServerSocket serverSocket;  
  9.   
  10.     public PooledRemoteFileServer(int aListenPort, int maxConnections) {  
  11.         listenPort = aListenPort;  
  12.         this.maxConnections = maxConnections;  
  13.     }  
  14.     public static void main(String[] args) {  
  15.     }  
  16.     public void setUpHandlers() {  
  17.     }  
  18.     public void acceptConnections() {  
  19.     }  
  20.     protected void handleConnection(Socket incomingConnection) {  
  21.     }  
  22. }  
 

请注意一下您现在应该熟悉了的 import 语句。我们给类以下实例变量以保存:

  • 我们的服务器能同时处理的活动客户机连接的最大数目

  • 进入的连接的侦听端口(我们没有指定缺省值,但如果您想这样做,并不会受到限制)

  • 将接受客户机连接请求的 ServerSocket

类的构造器用的参数是侦听端口和连接的最大数目

我们的类有一个 main() 方法和三个其它方法。稍后我们将探究这些方法的细节。现在只须知道 setUpHandlers() 创建数目为 maxConnections 的大量 PooledConnectionHandler,而其它两个方法则与我们前面已经看到的相似:acceptConnections()ServerSocket 上侦听传入的客户机连接,而 handleConnection 则在客户机连接一旦被建立后就实际处理它。


 3. 实现 main()

这里我们实现需作改动的 main() 方法,该方法将创建能够处理给定数目的客户机连接的 PooledRemoteFileServer,并告诉它接受连接:

Java代码  收藏代码
  1. public static void main(String[] args) {  
  2.     PooledRemoteFileServer server = new PooledRemoteFileServer(30003);  
  3.     server.setUpHandlers();  
  4.     server.acceptConnections();  
  5. }  

我们的 main() 方法很简单。我们实例化一个新的 PooledRemoteFileServer,它将通过调用 setUpHandlers() 来建立三个 PooledConnectionHandler。一旦服务器就绪,我们就告诉它 acceptConnections()


 4. 建立连接处理程序

Java代码  收藏代码
  1. public void setUpHandlers() {  
  2.     for (int i = 0; i < maxConnections; i++) {  
  3.         PooledConnectionHandler currentHandler = new PooledConnectionHandler();  
  4.         new Thread(currentHandler, "Handler " + i).start();  
  5.     }  
  6. }  
 

setUpHandlers() 方法创建 maxConnections(例如 3)个 PooledConnectionHandler 并在新 Thread 中激活它们。用实现了 Runnable 的对象来创建 Thread 使我们可以在 Thread 调用 start() 并且可以期望在 Runnable 上调用了 run()。换句话说,我们的 PooledConnectionHandler 将等着处理进入的连接,每个都在它自己的 Thread 中进行。我们在示例中只创建三个 Thread,而且一旦服务器运行,这就不能被改变。


 5. 处理连接

这里我们实现需作改动的 handleConnections() 方法,它将委派 PooledConnectionHandler 处理连接:

Java代码  收藏代码
  1. protected void handleConnection(Socket connectionToHandle) {  
  2. PooledConnectionHandler.processRequest(connectionToHandle);  
  3. }  
 

我们现在叫 PooledConnectionHandler 处理所有进入的连接(processRequest() 是一个静态方法)。

这里是 PooledConnectionHandler 类的结构:

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3. import java.util.*;  
  4.   
  5. public class PooledConnectionHandler implements Runnable {  
  6.     protected Socket connection;  
  7.     protected static List pool = new LinkedList();  
  8.   
  9.     public PooledConnectionHandler() {  
  10.     }  
  11.     public void handleConnection() {  
  12.     }  
  13.     public static void processRequest(Socket requestToHandle) {  
  14.     }  
  15.     public void run() {  
  16.     }  
  17. }  
 

这个助手类与 ConnectionHandler 非常相似,但它带有处理连接池的手段。该类有两个实例变量:

  • connection 是当前正在处理的 Socket
  • 名为 pool 的静态 LinkedList 保存需被处理的连接


 6. 填充连接池

这里我们实现 PooledConnectionHandler 上的 processRequest() 方法,它将把传入请求添加到池中,并告诉其它正在等待的对象该池已经有一些内容:

Java代码  收藏代码
  1. public static void processRequest(Socket requestToHandle) {  
  2.     synchronized (pool) {  
  3.         pool.add(pool.size(), requestToHandle);  
  4.         pool.notifyAll();  
  5.     }  
  6. }  
 

理解这个方法要求有一点关于 Java 的关键字 synchronized 如何工作的背景知识。我们将简要讲述一下线程。

先来看一些定义:

  • 原子方法。在执行过程中不能被中断的方法(或代码块)
  • 互斥锁。客户机欲执行原子方法时必须获得的单个“锁”

因此,当对象 A 想使用对象 B 的 synchronized 方法 doSomething() 时,对象 A 必须首先尝试获取对象 B 的互斥锁。是的,这意味着当对象 A 拥有该互斥锁时,没有其它对象可以调用对象 B 上任何其它 synchronized 方法。

synchronized 块是个稍微有些不同的东西。您可以同步任何对象上的一个块,而不只是在本身的某个方法中含有该块的对象。在我们的示例中,processRequest() 方法包含有一个 pool(请记住它是一个 LinkedList,保存等待处理的连接池)的 synchronized块。我们这样做的原因是确保没有别人能跟我们同时修改连接池。

既然我们已经保证了我们是唯一“涉水”池中的人,我们就可以把传入的 Socket 添加到 LinkedList 的尾端。一旦我们添加了新的连接,我们就用以下代码通知其它正在等待该池的 Thread,池现在已经可用:

Java代码  收藏代码
  1. pool.notifyAll();  
 

Object 的所有子类都继承这个 notifyAll() 方法。这个方法,连同我们下一屏将要讨论的 wait() 方法一起,就使一个 Thread 能够让另一个 Thread 知道一些条件已经具备。这意味着该第二个 Thread 一定正在等待那些条件的满足。


 7. 从池中获取连接

这里我们实现 PooledConnectionHandler 上需作改动的 run()方法,它将在连接池上等待,并且池中一有连接就处理它:

Java代码  收藏代码
  1. public void run() {  
  2.         while (true) {  
  3.              synchronized (pool) {  
  4.                   while (pool.isEmpty()) {  
  5.                        try {  
  6.                             pool.wait();  
  7.                        } catch (InterruptedException e) {  
  8.                             return;  
  9.                        }  
  10.                    }  
  11.                    connection = (Socket) pool.remove(0);  
  12.              }  
  13.              handleConnection();  
  14.         }  
  15. }  

 回想一下在前一屏讲过的:一个 Thread 正在等待有人通知它连接池方面的条件已经满足了。在我们的示例中,请记住我们有三个 PooledConnectionHandler 在等待使用池中的连接。每个 PooledConnectionHandler 都在它自已的 Thread 中运行,并通过调用 pool.wait() 产生阻塞。当我们的 processRequest() 在连接池上调用 notifyAll() 时,所有正在等待的 PooledConnectionHandler 都将得到“池已经可用”的通知。然后各自继续前行调用 pool.wait(),并重新检查 while(pool.isEmpty()) 循环条件。除了一个处理程序,其它池对所有处理程序都将是空的,因此,在调用 pool.wait() 时,除了一个处理程序,其它所有处理程序都将再次产生阻塞。恰巧碰上非空池的处理程序将跳出 while(pool.isEmpty()) 循环并攫取池中的第一个连接:

Java代码  收藏代码
  1. connection = (Socket) pool.remove(0);  
 

处理程序一旦有一个连接可以使用,就调用 handleConnection() 处理它。

在我们的示例中,池中可能永远不会有多个连接,只是因为事情很快就被处理掉了。如果池中有一个以上连接,那么其它处理程序将不必等待新的连接被添加到池。当它们检查 pool.isEmpty() 条件时,将发现其值为假,然后就从池中攫取一个连接并处理它。

还有另一件事需注意。当 run() 拥有池的互斥锁时,processRequest() 如何能够把连接放到池中呢?答案是对池上的 wait() 的调用释放锁,而 wait() 接着就在自己返回之前再次攫取该锁。这就使得池对象的其它同步代码可以获取该锁。


 8. 处理连接:再一次

这里我们实现需做改动的 handleConnection() 方法,该方法将攫取连接的流,使用它们,并在任务完成之后清除它们:

Java代码  收藏代码
  1. public void handleConnection() {  
  2.     try {  
  3.         PrintWriter streamWriter = new PrintWriter(connection.getOutputStream());  
  4.         BufferedReader streamReader =  
  5.             new BufferedReader(new InputStreamReader(connection.getInputStream()));  
  6.   
  7.         String fileToRead = streamReader.readLine();  
  8.         BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));  
  9.   
  10.         String line = null;  
  11.         while ((line = fileReader.readLine()) != null)  
  12.             streamWriter.println(line);  
  13.   
  14.         fileReader.close();  
  15.         streamWriter.close();  
  16.         streamReader.close();  
  17.     } catch (FileNotFoundException e) {  
  18.         System.out.println("Could not find requested file on the server.");  
  19.     } catch (IOException e) {  
  20.         System.out.println("Error handling a client: " + e);  
  21.     }  
  22. }  
 

跟在多线程服务器中不同,我们的 PooledConnectionHandler 有一个 handleConnection() 方法。这个方法的代码跟非池式的 ConnectionHandler 上的 run() 方法的代码完全一样。首先,我们把 OutputStreamInputStream 分别包装进(用 Socket 上的 getOutputStream()getInputStream()BufferedReaderPrintWriter。然后我们逐行读目标文件,就象我们在多线程示例中做的那样。再一次,我们获取一些字节之后就把它们放到本地的 line 变量中,然后写出到客户机。完成读写操作之后,我们关闭 FileReader 和打开的流。


 9. 总结一下带有连接池的服务器

我们的带有连接池的服务器研究完了。让我们回顾一下创建和使用“池版”服务器的步骤:

  1. 创建一个新种类的连接处理程序(我们称之为 PooledConnectionHandler)来处理池中的连接。

  2. 修改服务器以创建和使用一组 PooledConnectionHandler

   附: PooledRemoteFileServer 的完整代码清单

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3. import java.util.*;  
  4.   
  5. public class PooledRemoteFileServer {  
  6.     protected int maxConnections;  
  7.     protected int listenPort;  
  8.     protected ServerSocket serverSocket;  
  9.     public PooledRemoteFileServer(int aListenPort, int maxConnections) {  
  10.         listenPort = aListenPort;  
  11.         this.maxConnections = maxConnections;  
  12.     }  
  13.     public void acceptConnections() {  
  14.         try {  
  15.             ServerSocket server = new ServerSocket(listenPort, 5);  
  16.             Socket incomingConnection = null;  
  17.             while (true) {  
  18.                 incomingConnection = server.accept();  
  19.                 handleConnection(incomingConnection);  
  20.             }  
  21.         } catch (BindException e) {  
  22.             System.out.println("Unable to bind to port " + listenPort);  
  23.         } catch (IOException e) {  
  24.             System.out.println("Unable to instantiate a ServerSocket on port: " + listenPort);  
  25.         }  
  26.     }  
  27.     protected void handleConnection(Socket connectionToHandle) {  
  28.         PooledConnectionHandler.processRequest(connectionToHandle);  
  29.     }  
  30.     public static void main(String[] args) {  
  31.         PooledRemoteFileServer server = new PooledRemoteFileServer(30003);  
  32.         server.setUpHandlers();  
  33.         server.acceptConnections();  
  34.     }  
  35.     public void setUpHandlers() {  
  36.         for (int i = 0; i < maxConnections; i++) {  
  37.             PooledConnectionHandler currentHandler = new PooledConnectionHandler();  
  38.             new Thread(currentHandler, "Handler " + i).start();  
  39.         }  
  40.     }  
  41. }  
 

       PooledConnectionHandler 的完整代码清单

Java代码  收藏代码
  1. import java.io.*;  
  2. import java.net.*;  
  3. import java.util.*;  
  4.   
  5. public class PooledConnectionHandler implements Runnable {  
  6.     protected Socket connection;  
  7.     protected static List pool = new LinkedList();  
  8.     public PooledConnectionHandler() {  
  9.     }  
  10.     public void handleConnection() {  
  11.         try {  
  12.             PrintWriter streamWriter = new PrintWriter(connection.getOutputStream());  
  13.             BufferedReader streamReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));  
  14.   
  15.             String fileToRead = streamReader.readLine();  
  16.             BufferedReader fileReader = new BufferedReader(new FileReader(fileToRead));  
  17.   
  18.             String line = null;  
  19.             while ((line = fileReader.readLine()) != null)  
  20.                 streamWriter.println(line);  
  21.   
  22.             fileReader.close();  
  23.             streamWriter.close();  
  24.             streamReader.close();  
  25.         } catch (FileNotFoundException e) {  
  26.             System.out.println("Could not find requested file on the server.");  
  27.         } catch (IOException e) {  
  28.             System.out.println("Error handling a client: " + e);  
  29.         }  
  30.     }  
  31.     public static void processRequest(Socket requestToHandle) {  
  32.         synchronized (pool) {  
  33.             pool.add(pool.size(), requestToHandle);  
  34.             pool.notifyAll();  
  35.         }  
  36.     }  
  37.     public void run() {  
  38.         while (true) {  
  39.             synchronized (pool) {  
  40.                 while (pool.isEmpty()) {  
  41.                     try {  
  42.                         pool.wait();  
  43.                     } catch (InterruptedException e) {  
  44.                         return;  
  45.                     }  
  46.                 }  
  47.                 connection = (Socket) pool.remove(0);  
  48.             }  
  49.             handleConnection();  
  50.         }  
  51.     }  


现实生活中的套接字
 1. 介绍

我们到目前为止讨论过的示例已经涵盖了 Java 编程的套接字机制,但在“现实”的一些例子中如何使用它们呢?即便用了多线程和带有连接池,如此简单地使用套接字,在多数应用程序中仍然是不合适的。相反地,在构成您的问题域的模型的其它类中使用套接字可能是明智的。

最近我们在把一个应用程序从大型机/SNA 环境移植到 TCP/IP 环境时就是这样做的。该应用程序的工作是简化零售渠道(例如硬件商店)和金融机构之间的通信。我们的应用程序是中间人。同样地,它必须与一端的零售渠道和另一端的金融渠道通信。我们必须处理客户机通过套接字与服务器进行的交谈,我们还必须把域对象转换成套接字就绪的形式以进行传输。

我们不能在本教程中涵盖这个应用程序的所有细节,但我们将带您浏览一些高层概念。您可以据此对您自己的问题域做些推断。


 2. 客户机端

在客户机端,我们系统中的主角是 SocketClientSocketFacadeStreamAdapter。客户机端的 UML 如下图所示:



 

我们创建了一个 ClientSocketFacade,它是 Runnable 的并且拥有一个 Socket 实例。我们的应用程序可以用一个特定的主机 IP 地址和端口号来实例化一个 ClientSocketFacade,并在一个新 Thread 中运行它。ClientSocketFacaderun() 方法调用 connect()connect() 惰性初始化一个 Socket。有了 Socket 实例,我们的 ClientSocketFacade 就调用自己的 receive()receive() 将造成阻塞直到服务器在 Socket 上发送数据。一旦服务器发送数据,我们的 ClientSocketFacade 就将醒来并处理传入的数据。数据的发送是直接的。我们的应用程序可以通过用一个 StreamObject 调用 send() 方法来简单地告诉它的 ClientSocketFacade 把数据发送到服务器。

上述讨论中唯一遗漏的一个是 StreamAdapter。当应用程序告诉 ClientSocketFacade 发送数据时,该 Facade 将委派 StreamAdapter 的实例处理有关操作。ClientSocketFacade 委派 StreamAdapter 的同一个实例处理接收数据的操作。StreamAdapter 把消息加工成最终格式并将它放到 SocketOutputStream 上,并以逆过程处理从 Socket InputStream 传入的消息。

例如,或许您的服务器需要知道发送中的消息的字节数。StreamAdapter 可以在发送之前计算消息的长度并将它附加在消息的前端。当服务器接收消息时,同样的 StreamAdapter 能够剥离长度信息并读取正确数量的字节以构建一个 StreamReadyObject

 3. 服务器端

服务器端的情形差不多:



 

我们把 ServerSocket 包装进 ServerSocketFacadeServerSocketFacadeRunnable 的并且拥有一个 ServerSocket 实例。我们的应用程序可以用一个特定的服务器端侦听端口和客户机连接的最大允许数目(缺省值是 50)来实例化一个 ServerSocketFacade。应用程序然后在一个新 Thread 中运行 Facade 以隐藏 ServerSocket 的交互操作细节。

ServerSocketFacade 上的 run() 方法调用 acceptConnections()acceptConnections() 创建一个新的 ServerSocket,并调用 ServerSocket 上的 accept() 以造成阻塞直到有客户机请求一个连接。每当有客户机请求连接,我们的 ServerSocketFacade 就醒来并通过调用 handleSocket() 来把 accept() 返回的新 Socket 传递给 SocketHandler 的实例。SocketHandler 的分内工作是处理从客户机到服务器的新通道。

 4. 业务逻辑

一旦我们正确布置了这些 Socket Facade,实现应用程序的业务逻辑就变得容易多了。我们的应用程序使用 ClientSocketFacade 的一个实例来在 Socket 上把数据发送到服务器并取回响应。应用程序负责把我们的域对象转换成 ClientSocketFacade 理解的格式并根据响应构建域对象。


 5. 发送消息到服务器

下图显示我们的应用程序发送消息的 UML 交互作用图:



 

为简单起见,我们未显示 aClientSocketFacade 向它的 Socket 实例请求其 OutputStream 的交互作用(用 getOutputStream() 方法)。一旦我们有了一个 OutputStream 引用,我们就如图所示那样与它交互。请注意 ClientSocketFacade 对我们的应用程序隐藏了套接字交互作用的低级细节。我们的应用程序与 aClientSocketFacade 交互,而不与任何更低级类交互,这些类使把字节放到 Socket OutputStream 上更容易。

 6. 接收来自服务器的消息

下图显示我们的应用程序接收消息的 UML 交互作用图:



 

请注意我们的应用程序在一个 Thread 中运行 aClientSocketFacade。当 aClientSocketFacade 启动时,它告诉自己在自己的 Socket 实例的 InputStream 上进行 receive()receive() 方法调用 InputStream 自身的 read(byte[])read([]) 方法将造成阻塞直到它接收到数据,并把在 InputStream 接收到的数据放到一个 byte 数组中。当数据到来时,aClientSocketFacadeaStreamAdapteraDomainAdapter 构造(最终地)应用程序能够使用的域对象。接着它把该域对象传回给应用程序。再一次,我们的 ClientSocketFacade 对应用程序隐藏了更低级细节,从而简化了应用层。


总结

Java 语言简化了套接字在应用程序中的使用。它的基础实际上是 java.net 包中的 SocketServerSocket 类。一旦您理解了表象背后发生的情况,就能容易地使用这些类。在现实生活中使用套接字只是这样一件事,即通过贯彻优秀的 OO 设计原则来保护应用程序中各层间的封装。我们为您展示了一些有帮助的类。这些类的结构对我们的应用程序隐藏了 Socket 交互作用的低级细节 ― 使应用程序能只使用可插入的 ClientSocketFacade ServerSocketFacade。在有些地方(在 Facade 内),您仍然必须管理稍显杂乱的字节细节,但您只须做一次就可以了。更好的是,您可以在将来的项目中重用这些低级别的助手类。


参考资料





相关文章推荐

Java套接字编程简单示例

服务端:package com.zzj.test; import java.io.BufferedReader; import java.io.IOException; import java.io...

Java套接字编程基础

本文节选自:http://www.builder.com.cn/2008/0414/813590.shtml 什么是套接字(Socket)? Network API是典型的用于基于TC...
  • resouer
  • resouer
  • 2012年09月11日 14:32
  • 3135

Java中的套接字

套接字(socket)为两台计算机之间的通信提供了一种机制,在 James Gosling 注意到 Java 语言之前,套接字就早已赫赫有名。该语言只是让您不必了解底层操作系统的细节就能有效地使用套接...
  • weifeib
  • weifeib
  • 2015年06月16日 18:50
  • 1065

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

java中套接字socket

餐前甜点 Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本。当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限。...

Java 网络编程(五) 使用TCP/IP的套接字(Socket)进行通信

使用TCP/IP的套接字(Socket)进行通信   套接字Socket的引入   为了能够方便地开发网络应用软件,由美国伯克利大学在Unix上推出了一种应用程序访问通信协议的操作系统用调用so...

Java套接字socket的使用

在JAVA网络程序中,客户端只要符合连接的通讯协议,那么服务器端都可以进行接收。 ServerSocket类主要用在服务端程序的开发上,用于接收客户端的链接请求: 实例:建...

Java套接字Socket编程--TCP参数

在Java的Socket中,主要包含了以下可设置的TCP参数。 属性 说明 默认值 SO_TIMEOUT 对ServerSocket来说表示等待连接的最长空等待时间; 对S...

JAVA 通过 Socket 实现 TCP 编程

简介 所谓socket 通常也称作"**套接字**",用于描述IP地址和端口,是一个通信链的句柄。应用程序通常**通过"套接字"向网络发出请求或者应答网络请求**。 以J2SDK-1.3为例,Soc...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Java 套接字
举报原因:
原因补充:

(最多只允许输入30个字)