技巧: 异步 SAX

虽然对 SAX 的所有抽象描述都强调它是一种事件驱动的接口,但极少有 SAX 应用程序真正地将 SAX 用于事件驱动的编程。反而,主要将 SAX 作为从 XML 文档抽取数据时节省内存的一种方法。然而,在异步通道上 ? 如长期产生数据的套接字 ? SAX 是一种用于解析进入消息的极其轻量级的编程技术。

通常,您可能会将 XML 当作一种文件格式。使用 SAX 解析 XML 文件意味着打开该文件,按顺序读遍该文件以找到标记和内容、处理所出现的每个内容,然后在处理完成时关闭该文件。但是,XML 规范就象适用于磁盘文件一样适用于异步流。而且,由于 SAX 是严格单向的,因此它在流上工作得特别好。

原则上,
流可以是很多事情,但对于 Internet 编程的大多数情况而言,这个术语指 BSD 套接字 ? 一种在所有现代操作系统上都实现了的接口。没有什么理由可以阻止您将本技巧文章中的技术用于串口设备连接,监控 GUI 事件,或者用于类似的长期运行或间歇的数据流。

本技巧文章所要表达的基本思想是:XML 常常是有线协议的极佳选择,而在对利用这一协议的客户机应用程序进行编码时,SAX 是最适合的技术。虽然 XML 的冗长对于监控大量数据可能是个问题,但如果要处理的数据流大小适中,那么 SAX(和 XML 本身)就是通信 API 的极佳选择。
一种实现
对于本技巧文章,我需要一个测试案例,它合并由远程主机提供的非定形的数据流,这些数据流对客户机很有用。由于我维护了一个网站,因此一个显而易见的示例是远程监控站点的点击率的方法 ? 点击以不规则的几率连续发生,而且总的数据带宽适中。让我的主系统上的某个实用程序跟踪对我的 Web 服务器的点击率,这可能十分有用,至少非常有趣。

在我的特定 Web 服务器上,日志记录被追加到一个文件,一行一条记录,记录中主要是由空格分隔的字段。但有些加引号的字段内部有空格,因此解析行就有些复杂。当然,我
可以在写日志时,直接将这些原始日志行发送给客户机。但 XML 有一些很好的功能,您可能对它们非常熟悉:

  • 它可以进行一定程度的自我编制文档
  • 它允许在属性顺序和空格方面有所变化
  • 在限制以内,模式可以不断加强,并可以保持向后兼容性
  • 具体到我的应用程序,只要每个 Web 服务器以公共的 XML 格式传输其日志数据,我可以安排同样用这种方法监控几个 Web 服务器。

我的 XML 日志服务器是一个非常基本的套接字应用程序,它是用 Python 编写的(但不同的语言可能适用于服务器和/或客户机)。以下是精简的版本:
清单 1. 服务器应用程序(weblog-xml.py)

from SocketServer
import BaseRequestHandler, TCPServer
from time
import sleep
import sys, socket
# ...Define hit_tag template and log_fields() function...

class WebLogHandler(BaseRequestHandler):

def handle(self):

print
"Connected from", self.client_address
self.request.sendall(
‘<hits>‘)

try:

while True:

for hit
in LOG.readlines():
self.request.sendall(hit_tag % log_fields(hit))
sleep(
5)

except socket.error:
self.request.close()

print
"Disconnected from", self.client_address
if __name__==
‘__main__‘:

global LOG
LOG = open(
‘../access-log‘)
LOG.seek(
0,
2)
# Start at end of current access log
srv = TCPServer((
‘‘,
8888), WebLogHandler)
srv.serve_forever()

当打开套接字时,会立即发送文档根元素
<hits> ,随后是新点击发生时(但以 5 秒一块进行批处理)由日志记录的新点击数,带有类似于清单 2 中的元素。

清单 2. 样本 <hit> XML 元素

<hit
ip="210.8.XX.XXX"
timestamp="11/May/2003:01:47:53 -0500"
request="GET /publish/programming/code_recognizer.gif HTTP/1.1"
status="200"
bytes="12718"
referrer="http://gnosis.cx/publish/programming/neural_networks.htm"
agent="Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)"/>

SAX 客户机
在服务器中,我根本还没有使用 SAX;我仅使用字符串格式化来构成 XML 元素。在客户机应用程序中,SAX 省去了一些工作。以下是我目前的整个客户机应用程序:

清单 3. 基于 SAX 的日志监控客户机

#!/usr/bin/env python
import socket
import xml.sax
from xml.sax.handler import ContentHandler
class AsyncWebLog(ContentHandler):
def startDocument(self):
print "Connected to gnosis.cx server"
def startElement(self, name, attrs):
if (name==‘hit‘ and attrs[‘status‘]==‘200‘
and attrs[‘referrer‘]!=‘-‘):
print attrs[‘referrer‘],"->",attrs[‘request‘].split()[1]
parser = xml.sax.make_parser()
parser.setContentHandler(AsyncWebLog())
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((‘gnosis.cx‘, 8888))
try:
while 1:
xml_data = sock.recv(8192)
parser.feed(xml_data)
finally:
sock.close()

由于日志记录已经被很好地格式化成了 XML,解析其元素基本上不费多大力气。我需要做的全部工作是定义一个内容处理程序,它有一个
.startElement() 方法,并在该方法内做一些需要的工作。为了让客户机具有一点“友好性”,我也让客户机使用一条消息来确认连接,这由服务器发送的根
<hits> 元素触发。

我的
startElement() 方法就其想显示的内容做了几点决定。我决定只处理名为
<hit> 的元素 ? 增强的服务器也可能会开始发送其它种类的 XML 元素作为消息;我的客户机将恰当地忽略它们,而不会阻塞在流上。可以使用很多属性,但我决定只关注页面的引用者。对于只针对成功传送、且引用者已知的页面,我的测试演示了类似这样的检查各种属性的布尔代数。此后,我向我的客户机屏幕打印一个描述。显然,更精致的客户机应用程序可以使用 GUI 来显示这些信息,或用不同的方法操作和处理接收到的数据。

快速完成
当客户机和服务器都在运行时,我的本地终端不时会用一些网络冲浪者的列表更新自己,这些冲浪者根据链接访问我的网站。只要我让这些进程继续运行,我就能够始终接收更新 ? 底层的 XML 没有大小限制。任何应用程序,只要它与监控长期数据流的某一方面类似,都可以有效地且方便地利用他们最喜欢的编程工具中可用的 XML 和 SAX 库来实现这一目的。