Python Web 服务开发者: 通过 SMTP 处理 SOAP


通过 SMTP 发送和接收 SOAP 请求




文档选项





[url=javascript:document.email.submit();]将此页作为电子邮件发送[/url]


未显示需要 JavaScript 的文档选项
对此页的评价


帮助我们改进这些内容
级别: 初级
Mike Olson
, 首席顾问, Fourthought, Inc.
Uche Ogbuji
, 首席顾问, Fourthought, Inc.
2003 年 3 月 01 日
大多数人在想到 SOAP 时,都会想到通过 HTTP 协议发送 XML 请求并获得 XML 响应。但情况并不总是如此。事实上,SOAP 协议将 HTTP 定义为一种用于传输 SOAP 消息的可能的传输协议。在本文中,Mike Olson 和 Uche Ogbuji 探讨了如何通过简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)发送和接收 SOAP 请求。
引言
由于某些原因,大多数人只想到通过 HTTP 处理 SOAP。首先,HTTP 是 SOAP 协议的最常见传输协议,迄今为止 Web 上可用的大多数服务都要使用它。其次是由于 HTTP 协议的工作方式,它非常适合 SOAP 请求/响应结构。在 HTTP 中,您向服务器发送消息。HTTP 请求的格式非常灵活,使您能够在主体内嵌入一个 SOAP 请求。然后,HTTP 协议(为所有的 HTTP 请求)指定一个也足够灵活、可以嵌入一个 SOAP 响应的响应。这样就可以非常直接地实现 SOAP 服务。
SMTP 协议的情况就不是这样了。当使用 SMTP 时,请求格式很灵活,可以让您附加一个 SOAP 请求。不过,响应结构并不是十分灵活。SMTP 响应和“O.K.”一样简单。 SMTP 服务扩展(SMTP Service Extension,ESMTP)规范确实为响应添加了一点信息(如“未知的用户(Unknown User)”),但响应格式仍没有灵活到可以放入一个完整的 SOAP 响应。用 SMTP 发送响应的唯一方法就是使用另一条电子邮件消息。
出于这个原因,SMTP SOAP 服务的开发者需要添加额外的逻辑,以便能够跟踪传入的 SOAP 请求,然后通过一条单独的 SMTP 消息向接收方发送 SOAP 响应。这就使使用 SMTP 的 SOAP 服务的复杂性增加了许多(不久我们就会看到这一点)。
那么,您为什么想通过 SMTP 处理 SOAP 呢?最常见的答案是“因为我不能使用 HTTP。”其中不能使用 HTTP 的较为常见原因之一是防火墙。如果您位于防火墙的后面,就很可能无法控制在何处处理 HTTP 请求。不过,倒是有一个接收电子邮件的绝好机会。另一个原因是请求/响应消息传递模型可能不是您的应用程序所期望的模型。SMTP 最适合发布/订阅或单向消息传递模型。最后一个原因,您正在编写的服务不是实时的。如果您的服务不得不执行复杂的查询或者可能会花费 300 多秒(常见的 HTTP 请求超时)的复杂计算,那么就需要一种异步方法,如 SMTP。


回页首
处理 SOAP 消息
通过 SMTP 处理 SOAP 请求和响应有三种可能的方法。第一种方法取决于您的 SMTP 服务器的能力。使用这种方法时,您编写 SOAP 处理应用程序,然后将它连接到一个电子邮件地址。大多数电子邮件服务器都将允许您把在特定地址上接收到的消息管道传送给应用程序。然后,您的应用程序将读取 SMTP 消息(最有可能从标准过程输入读取),处理请求并发出响应。
第二种方法假定您拥有对 mailbox 文件的访问权。使用这种方法时,您将定期扫描邮件文件以查找 SOAP 请求,从邮件文件中删除这些消息,处理请求并发出响应。
最后一种方法 — 也是我们将在这里使用的方法,因为另两种方法过于依赖本地配置选项 - 是编写一个 SMTP 服务器。使用这种方法时,您在一个端口上侦听 SMTP 请求,当接收到请求时对其进行处理并在处理完成后发出响应。
把请求和响应链接起来
无论您决定怎样通过 SMTP 来处理 SOAP,您都需要一种机制把 SOAP 请求和 SOAP 响应链接起来。SOAP 的试验性电子邮件绑定(请参阅
参考资料
)推荐使用“Message-ID”和“In-Reply-To”SMTP 头。当消息被发送时,客户机将一个唯一的标识符放入“Message-ID”头中,接着,服务器在响应消息的“In-Reply-To”头中使用相同的标识符。这样就使客户机能够将接收到的响应与适当的请求匹配起来。


回页首
示例相关性
本专栏的示例是用 Python 2.2.1 编写的,并且使用了一些功能,主要是用了新的服务器体系结构。我们还将用 ZSI 1.2 来处理 SOAP 消息。ZSI 的安装相当轻松,因为它用标准的 python distutils 作为其分发版。请参阅本专栏结尾处的
参考资料
部分以获取下载信息。


回页首
SOAP SMTP 服务器
使用 Python 的内建 smtpd 库编写 SMTP 服务器相当轻松。就象在 Python 的所有服务器体系结构中那样,您从一个基类 — 在这个例子中是 smtpd.SMTPServer — 继承(请参阅
清单 1
),并根据您所期望的功能覆盖该类中的某些方法。在这个例子中,我们覆盖了 process_message 方法(每当新消息到达时,就调用该方法)。为了启动服务器,我们使用 Python 2.2 的新的 asyncore 开始处理请求。
清单 1. 服务器的定义代码
class OurServer(smtpd.SMTPServer):
    #A place to store the current ID of the message that we are processing.
    currentID = None
    #This is the callback from SMTPServer that all SMPTServers
    #must implement to handle messages
    def process_message(self, peer, mailfrom, rcpttos, data):
       #
       # Snip out code for process_message
       # It will be discussed below
       #
#This is the 2.2 way of running asynchronous servers
server = OurServer(("localhost", 8023),
                   (None, 0))
try:
    asyncore.loop()
except KeyboardInterrupt:
    pass
一旦接收到一条消息,我们就可以做一些简单的测试来看看它到底是不是一个 SOAP 请求。如果是,那么我们可以将它传送给 ZSI 来分派该消息。由于 ZSI _Dispatch 方法不允许额外的参数,所以我们需要在服务器的实例中存储当前消息标识(这样我们就可以在结果中设置它了)。当然,在一台流量很大的服务器上,这种解决方案永远不会像您期望的那样工作,即一次分派多个传入的请求。为避免出现这种情况,我们需要以某种方式使该信息通过 ZSI,或者为每个请求创建一个单独的服务器实例。
一旦我们有了结果,不管这些结果是一个有效的响应还是错误,我们都把结果放在一条电子邮件消息中发送出去。您将注意到,当我们发送电子邮件消息时,我们使用常见的方法 common.SendMessage (请参阅
清单 2
)。这个助手函数使用另一个线程将消息发送到指定的服务器。这是必需的,只有这样发送过程才不会因为等待服务器连接、网络延迟等原因被阻塞。在生产系统中,这个函数一定会变得更复杂,并需要使用某种排队机制来处理网络故障、服务器宕机等问题。
清单 2. 用于处理 SOAP 消息的服务器代码
    def process_message(self, peer, mailfrom, rcpttos, data):
        #Parse the message into an email.Message instance
        p = Parser.Parser()
        m = p.parsestr(data)
        print "Received Message"
        #See if it is a SOAP request
        if m.has_key('To') and m['To'] == 'calendar@localhost':
            self.process_soap(m)
        else :
            #In normal circumstances, this would probably
            #forward the email message to another SMTP Server
            print "Unknown Email message"
            print m
    def process_soap(self,message):
        #Parse the SOAP Message
        ps = parse.ParsedSoap(message.get_payload(decode=1))
        #Store the current ID
        self.currentID = message['Message-Id']
        print "Processing Message: " + self.currentID
        #Use ZSI's dispatcher to call the correct function based on the message.
        dispatch._Dispatch(ps,
                           [self],
                           self.send_xml,
                           self.send_fault)
    #ZSI Callback to send an SOAP(non-Fault) response.
    def send_xml(self,xml):
        self.return_soap(xml)
    #ZSI callback to send a fault.
    def send_fault(self,fault):
        sys.stderr.write("FAULT While processing request:\\n");
        s = cStringIO.StringIO()
        fault.serialize(s)
        st = s.getvalue()
        print st
        #Serialize the fault and send it to the client
        self.return_soap(st)
    #Called by our code to send result XML.
    def return_soap(self,st):
        msg = MIMEText.MIMEText(st)
        msg['Subject'] = "Test Message"
        msg['To'] = 'calendar@localhost'
        msg['From'] = 'Mike.Olson@Fourthought.com'
        msg['Message-Id'] = "2"
        msg['In-Reply-To'] = self.currentID or 0
        print "Sending Reply"
        common.SendMessage("127.0.0.1",8024,"me@fourthought.com",
        ["Mike.Olson@Fourthought.com"],msg)
                        
    #Implementation of our SOAP Service.
    def getMonth(self,year,month):
        print "Request for %d,%d" % (year,month)
        return calendar.month(year, month)


回页首
SOAP SMTP 客户机
SMTP SOAP 客户机需要侦听传入的邮件消息(它们代表应答),所以它看上去很象一个服务器实现。我们再次覆盖 smtpd.SMTPServer 来创建一个侦听器(在另一个端口上)以侦听应答(请参阅
清单 3
)。服务器侦听器和客户机侦听器之间的一个很大的区别就是:我们在一个单独的线程中启动侦听器。这样我们就可以拥有两个线程,一个处理用户输入,另一个作为侦听器侦听并处理响应。
另一个区别就是侦听器包含一个“响应”字典,该字典将“In-Reply-To”标识映射为回调方法。使用这种方法,只要主输入线程向侦听器注册了每个请求,那么它愿意发出多少请求就可以发出多少。然后,主线程就可以继续快乐地工作,只要有响应进入,侦听器就会调用回调来处理它。
清单 3. 客户机侦听器代码
class ClientServer(smtpd.SMTPServer):
    #A simple server to receive our SOAP responses.
    #The responses dictionary is a mapping from
    #Message ID to a callback to handle the response.
    responses = {}
    #this is the method we must override in to handle SMTP messages
    def process_message(self, peer, mailfrom, rcpttos, data):
        #Parse the message into a email.Message instance
        p = Parser.Parser()
        m = p.parsestr(data)
        #See if this is a reply that we were waiting for.
        if m.has_key('In-Reply-To') and self.responses.has_key(m['In-Reply-To']):
            mID = m['In-Reply-To']
            #Invoke the response callback with the parsed SOAP.
            self.responses[mID](mID,parse.ParsedSoap(m.get_payload(decode=1)))
            del self.responses[mID]
        else:
            #In a product server, this would probably forward the message to another
            #SMTP Server.
            print "Unknown Email message"
            print m
            
    #method used to register that we are expecting a response from the server.
    def expectResponse(self,mId,callback):
        self.responses[str(mId)] = callback
用户输入包含在 HandleInput 方法(请参阅
清单 4
)中。该方法要求用户输入月份和年份。然后,它创建一个 SOAP 请求并将其发送给服务器。它还向侦听器注册这个请求,这样侦听器就知道要期待一个响应。在这个示例中,所有请求都向 DisplayResults 回调注册了。DisplayResults 方法只是将结果显示到屏幕上。
清单 4. 余下的客户机代码
def DisplayResults(ID,ps):
    #This method is the generic callback used by all requests.
    #It uses the parsed SOAP to print out the results.
    print "\\nResults for ID: " + ID
    tc = TC.String()
    data = _child_elements(ps.body_root)
    if len(data) == 0: print None
    print tc.parse(data[0], ps)
def HandleInput(server):
    #This method is used to query the user for a year and a month.
    #When one is received, then a new message is sent, and the server
    #is told to expect the results
    done = 0
    lastID = 1
    while not done:
        year = raw_input("Year of request(Return to exit): ")
        if not year: done = 1
        else:
            year = int(year)
            month = int(raw_input("Month of request: "))
            lastID += 1
            mID = lastID
            
            msg = MIMEText.MIMEText(BODY_TEMPLATE%(year,month))
            msg['Subject'] = "Test Message"
            msg['To'] = 'calendar@localhost'
            msg['From'] = 'Mike.Olson@Fourthought.com'
            msg['Message-Id'] = str(mID)
            server.expectResponse(mID,DisplayResults)
            print "Sending out message ID: " + str(mID)
            common.SendMessage("127.0.0.1",8023,"me@fourthought.com",
             ["Mike.Olson@Fourthought.com"],msg)
def StartServer():
    #Start up our response server in another thread.
    server = ClientServer(("localhost", 8024),
                          (None, 0))
    def run():
        try:
            asyncore.loop()
        except KeyboardInterrupt:
            pass
    print "Starting Client Server"
    t = threading.Thread(None,run)
    t.start()
    return server
if __name__ == '__main__':
    server = StartServer()
    HandleInput(server)


回页首
运行示例
客户机应用程序向用户查询月份和日期。您愿意进行多少次查询都可以。一旦返回了一个有效应答,这个有效应答将被打印到屏幕。您将注意到,您可能没有让所有的应答都按照和发送时相同的顺序返回。输出显示在
清单 5
中。
清单 5. 客户机输出
[molson@penny src]$ python client.py
Starting Client Server
Year of request(Return to exit): 2003
Month of request: 1
Sending out message ID: 2
Year of request(Return to exit): 2004
Month of request: 1
Sending out message ID: 3
Year of request(Return to exit): 2005
Month of request: 1
Sending out message ID: 4
Year of request(Return to exit):
Results for ID: 2
January 2003
Mo Tu We Th Fr Sa Su
       1  2  3  4  5
6  7  8  9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Results for ID: 3
January 2004
Mo Tu We Th Fr Sa Su
          1  2  3  4
5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Results for ID: 4
January 2005
Mo Tu We Th Fr Sa Su
                1  2
3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31


回页首
下一篇专栏文章
在本专栏的下一篇文章中,我们将研究一下与 Google 的 SOAP API 进行交互都需要些什么。


回页首
参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的
    英文原文
    .
  • 请参加关于本文的
    [url=javascript:void forumWindow()]讨论论坛[/url]
    。(您也可以单击本文顶部或底部的 讨论访问该论坛。)
  • 请下载
    zip 文件
    ,该文件包含本文所使用的示例。
  • 请阅读本系列中关于
    RSS for Python
    的前一篇专栏文章,或阅读所有
    该系列的 Python Web 服务开发者专栏文章

  • 请访问
    ZSI 下载页面

  • 请了解更多关于
    SOAP 电子邮件绑定
    的信息。
  • 请阅读了解
    RFC 821(SMTP)

  • 请了解更多有关
    RFC 1869(ESMTP)
    的信息。



回页首
作者简介


Mike Olson 是 Fourthought Inc. 的顾问兼合伙创始人,该公司是一家软件供应商并专门提供企业知识管理应用程序的 XML 解决方案方面的咨询。Fourthought 开发了用于 XML 中间件的开放源代码平台 4Suite 和 4Suite Server。您可以通过
mike.olson@fourthought.com
与 Olson 先生联系。


Uche Ogbuji 是 Fourthought Inc. 的顾问兼合伙创始人,该公司是一家软件供应商并专门提供企业知识管理应用程序的 XML 解决方案方面的咨询。Fourthought 开发了用于 XML 中间件的开放源代码平台 4Suite 和 4Suite Server。Ogbuji 先生是一位出生于尼日利亚的计算机工程师和作家,他现在美国科罗拉多州的博耳德(Boulder)生活和工作。您可以通过
uche.ogbuji@fourthought.com
与 Ogbuji 先生联系。


http://www-128.ibm.com/developerworks/cn/webservices/ws-pyth/part12/index.html#resources