现实生活中的套接字
1. 介绍
我们到目前为止讨论过的示例已经涵盖了 Java 编程的套接字机制,但在“现实”的一些例子中如何使用它们呢?即便用了多线程和带有连接池,如此简单地使用套接字,在多数应用程序中仍然是不合适的。相反地,在构成您的问题域的模型的其它类中使用套接字可能是明智的。
最近我们在把一个应用程序从大型机/SNA 环境移植到 TCP/IP 环境时就是这样做的。该应用程序的工作是简化零售渠道(例如硬件商店)和金融机构之间的通信。我们的应用程序是中间人。同样地,它必须与一端的零售渠道和另一端的金融渠道通信。我们必须处理客户机通过套接字与服务器进行的交谈,我们还必须把域对象转换成套接字就绪的形式以进行传输。
我们不能在本教程中涵盖这个应用程序的所有细节,但我们将带您浏览一些高层概念。您可以据此对您自己的问题域做些推断。
2. 客户机端
在客户机端,我们系统中的主角是 Socket
、ClientSocketFacade
和 StreamAdapter
。客户机端的 UML 如下图所示:
我们创建了一个 ClientSocketFacade
,它是 Runnable
的并且拥有一个 Socket
实例。我们的应用程序可以用一个特定的主机 IP 地址和端口号来实例化一个 ClientSocketFacade
,并在一个新 Thread
中运行它。ClientSocketFacade
的 run()
方法调用 connect()
,connect()
惰性初始化一个 Socket
。有了 Socket
实例,我们的 ClientSocketFacade
就调用自己的 receive()
,receive()
将造成阻塞直到服务器在 Socket
上发送数据。一旦服务器发送数据,我们的 ClientSocketFacade
就将醒来并处理传入的数据。数据的发送是直接的。我们的应用程序可以通过用一个 StreamObject
调用 send()
方法来简单地告诉它的 ClientSocketFacade
把数据发送到服务器。
上述讨论中唯一遗漏的一个是 StreamAdapter
。当应用程序告诉 ClientSocketFacade
发送数据时,该 Facade 将委派 StreamAdapter
的实例处理有关操作。ClientSocketFacade
委派 StreamAdapter
的同一个实例处理接收数据的操作。StreamAdapter
把消息加工成最终格式并将它放到 Socket
的 OutputStream
上,并以逆过程处理从 Socket
的 InputStream
传入的消息。
例如,或许您的服务器需要知道发送中的消息的字节数。StreamAdapter
可以在发送之前计算消息的长度并将它附加在消息的前端。当服务器接收消息时,同样的 StreamAdapter
能够剥离长度信息并读取正确数量的字节以构建一个 StreamReadyObject
。
3. 服务器端
服务器端的情形差不多
我们把 ServerSocket
包装进 ServerSocketFacade
,ServerSocketFacade
是 Runnable
的并且拥有一个 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 数组中。当数据到来时,aClientSocketFacade
用 aStreamAdapter
和 aDomainAdapter
构造(最终地)应用程序能够使用的域对象。接着它把该域对象传回给应用程序。再一次,我们的 ClientSocketFacade
对应用程序隐藏了更低级细节,从而简化了应用层。
总结
Java 语言简化了套接字在应用程序中的使用。它的基础实际上是 java.net
包中的 Socket
和 ServerSocket
类。一旦您理解了表象背后发生的情况,就能容易地使用这些类。在现实生活中使用套接字只是这样一件事,即通过贯彻优秀的 OO 设计原则来保护应用程序中各层间的封装。我们为您展示了一些有帮助的类。这些类的结构对我们的应用程序隐藏了 Socket
交互作用的低级细节 ― 使应用程序能只使用可插入的 ClientSocketFacade
和 ServerSocketFacade
。在有些地方(在 Facade 内),您仍然必须管理稍显杂乱的字节细节,但您只须做一次就可以了。更好的是,您可以在将来的项目中重用这些低级别的助手类。
参考资料 |
Thread
带有连接池的问题。我们在本教程中没有研究得这么深,而是使 PooledRemoteFileServer
和 PooledConnectionHandler
能让您更容易些地学,但 Allen 谈到的战略是非常适合的。事实上,他用支持多用途、可配置服务器的回调机制的 Java 实现来处理 ServerSocket
是一种强大的处理方式。