套接字(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 并发送一个格式良好的 XMLString
到服务器上进行处理。URLConnection
的具体子类(例如 HttpURLConnection
)提供特定于它们实现的额外功能。对于我们的示例,我们不想做任何特别的事情,所以我们将使用URLConnection
本身提供的缺省行为。
连接到 URL 包括几个步骤:
- 创建
URLConnection
- 用各种 setter 方法配置它
- 连接到 URL
- 用各种 getter 方法与它交互
接着,我们将看一些演示如何用 URLConnection
来从服务器请求文档的样本代码
3. URLClient 类
我们将从 URLClient
类的结构讲起。
- import java.io.*;
- import java.net.*;
- public class URLClient {
- protected URLConnection connection;
- public static void main(String[] args) {
- }
- public String getDocumentAt(String urlString) {
- }
- }
- import java.io.*;
- import java.net.*;
- public class URLClient {
- protected URLConnection connection;
- public static void main(String[] args) {
- }
- public String getDocumentAt(String urlString) {
- }
- }
要做的第一件事是导入 java.net
和 java.io
。
我们给我们的类一个实例变量以保存一个 URLConnection
。
我们的类有一个 main()
方法,它处理浏览文档的逻辑流。我们的类还有一个 getDocumentAt()
方法,该方法连接到服务器并向它请求给定文档。下面我们将分别探究这些方法的细节。
main()
方法处理浏览文档的逻辑流:
- public static void main(String[] args) {
- URLClient client = new URLClient();
- String yahoo = client.getDocumentAt("http://www.yahoo.com");
- System.out.println(yahoo);
- }
- public static void main(String[] args) {
- URLClient client = new URLClient();
- String yahoo = client.getDocumentAt("http://www.yahoo.com");
- System.out.println(yahoo);
- }
我们的 main()
方法只是创建一个新的 URLClient
并用一个有效的 URL String
调用getDocumentAt()
。当调用返回该文档时,我们把它存储在String
,然后将它打印到控制台。然而,实际的工作是在getDocumentAt()
方法中完成的。
getDocumentAt()
方法处理获取 Web 上的文档的实际工作:
- public String getDocumentAt(String urlString) {
- StringBuffer document = new StringBuffer();
- try {
- URL url = new URL(urlString);
- URLConnection conn = url.openConnection();
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
- reader.close();
- } catch (MalformedURLException e) {
- System.out.println("Unable to connect to URL: " + urlString);
- } catch (IOException e) {
- System.out.println("IOException when connecting to URL: " + urlString);
- }
- return document.toString();
- }
- public String getDocumentAt(String urlString) {
- StringBuffer document = new StringBuffer();
- try {
- URL url = new URL(urlString);
- URLConnection conn = url.openConnection();
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
- reader.close();
- } catch (MalformedURLException e) {
- System.out.println("Unable to connect to URL: " + urlString);
- } catch (IOException e) {
- System.out.println("IOException when connecting to URL: " + urlString);
- }
- return document.toString();
- }
getDocumentAt()
方法有一个 String
参数,该参数包含我们想获取的文档的 URL。我们在开始时创建一个StringBuffer
来保存文档的行。然后我们用我们传进去的urlString
创建一个新 URL
。接着创建一个 URLConnection
并打开它:
- URLConnection conn = url.openConnection();
- URLConnection conn = url.openConnection();
一旦有了一个 URLConnection
,我们就获取它的 InputStream
并包装进 InputStreamReader
,然后我们又把 InputStreamReader
包装进 BufferedReader
以使我们能够读取想从服务器上获取的文档的行。在 Java 代码中处理套接字时,我们将经常使用这种包装技术,但我们不会总是详细讨论它。在我们继续往前讲之前,您应该熟悉它:
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
有了 BufferedReader
,就使得我们能够容易地读取文档内容。我们在 while
循环中调用 reader
上的 readLine()
:
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
对 readLine()
的调用将直至碰到一个从 InputStream
传入的行终止符(例如换行符)时才阻塞。如果没碰到,它将继续等待。只有当连接被关闭时,它才会返回null
。在这个案例中,一旦我们获取一个行(line),我们就把它连同一个换行符一起附加(append)到名为document
的StringBuffer
上。这保留了服务器端上读取的文档的格式。
我们在读完行之后关闭 BufferedReader
:
- reader.close();
- reader.close();
如果提供给 URL
构造器的 urlString
是无效的,那么将抛出 MalformedURLException
。如果发生了别的错误,例如当从连接上获取InputStream
时,那么将抛出IOException
。
6. 总结
实际上,URLConnection
使用套接字从我们指定的 URL 中读取信息(它只是解析成 IP 地址),但我们无须了解它,我们也不关心。但有很多事;我们马上就去看看。
在继续往前讲之前,让我们回顾一下创建和使用 URLConnection
的步骤:
- 用您想连接的资源的有效 URL
String
实例化一个URL
(如有问题则抛出MalformedURLException
)。
- 打开该
URL
上的一个连接。
- 把该连接的
InputStream
包装进BufferedReader
以使您能够读取行。
- 用
BufferedReader
读文档。
- 关闭
BufferedReader
。
附: URLClient
的完整代码清单:
- import java.io.*;
- import java.net.*;
- public class URLClient {
- protected HttpURLConnection connection;
- public String getDocumentAt(String urlString) {
- StringBuffer document = new StringBuffer();
- try {
- URL url = new URL(urlString);
- URLConnection conn = url.openConnection();
- BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
- String line = null;
- while ((line = reader.readLine()) != null)
- document.append(line + "\n");
- reader.close();
- } catch (MalformedURLException e) {
- System.out.println("Unable to connect to URL: " + urlString);
- } catch (IOException e) {
- System.out.println("IOException when connecting to URL: " + urlString);
- }
- return document.toString();
- }
- public static void main(String[] args) {
- URLClient client = new URLClient();
- String yahoo = client.getDocumentAt("http://www.yahoo.com");
- System.out.println(yahoo);
- }
- }