在Tornado中嵌套Web服务调用(异步?)

4 投票
2 回答
2292 浏览
提问于 2025-04-17 14:04

我正在用tornado(还有一个第三方的tornadows模块)实现一个SOAP网络服务。我的服务里有一个操作需要调用另一个操作,所以我有了以下的流程:

  1. 外部请求(通过SOAPUI)到操作A
  2. 内部请求(通过requests模块)到操作B
  3. 从操作B得到的内部响应
  4. 从操作A得到的外部响应

因为所有操作都在一个服务里运行,所以在某个地方出现了阻塞。我对tornado的异步功能不太熟悉。

只有一个请求处理方法(post),因为所有请求都是通过同一个网址进来的,然后根据SOAPAction请求头的值调用具体的操作(处理的方法)。我在post方法上加了@tornado.web.asynchronous装饰器,并在最后调用了self.finish(),但还是不行。

tornado能处理这种情况吗?如果可以,我该怎么实现呢?

编辑(添加代码):

class SoapHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def post(self):
        """ Method post() to process of requests and responses SOAP messages """
        try:
            self._request = self._parseSoap(self.request.body)
            soapaction = self.request.headers['SOAPAction'].replace('"','')
            self.set_header('Content-Type','text/xml')
            for operations in dir(self):
                operation = getattr(self,operations)
                method = ''
                if callable(operation) and hasattr(operation,'_is_operation'):
                    num_methods = self._countOperations()
                    if hasattr(operation,'_operation') and soapaction.endswith(getattr(operation,'_operation')) and num_methods > 1:
                        method = getattr(operation,'_operation')
                        self._response = self._executeOperation(operation,method=method)
                        break
                    elif num_methods == 1:
                        self._response = self._executeOperation(operation,method='')
                        break
            soapmsg = self._response.getSoap().toprettyxml()
            self.write(soapmsg)
            self.finish()
        except Exception as detail:
            #traceback.print_exc(file=sys.stdout)
            wsdl_nameservice = self.request.uri.replace('/','').replace('?wsdl','').replace('?WSDL','')
            fault = soapfault('Error in web service : {fault}'.format(fault=detail), wsdl_nameservice)
            self.write(fault.getSoap().toxml())
            self.finish()

这是请求处理器的post方法。它来自我使用的网络服务模块(所以不是我的代码),但我加了异步装饰器和self.finish()。基本上,它就是根据请求中的SOAPAction调用正确的操作。

class CountryService(soaphandler.SoapHandler):
    @webservice(_params=GetCurrencyRequest, _returns=GetCurrencyResponse)
    def get_currency(self, input):
        result = db_query(input.country, 'currency')
        get_currency_response = GetCurrencyResponse()
        get_currency_response.currency = result
        headers = None
        return headers, get_currency_response

    @webservice(_params=GetTempRequest, _returns=GetTempResponse)
    def get_temp(self, input):
        get_temp_response = GetTempResponse()
        curr = self.make_curr_request(input.country)
        get_temp_response.temp = curr
        headers = None
        return headers, get_temp_response

    def make_curr_request(self, country):

        soap_request = """<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:coun='CountryService'>
   <soapenv:Header/>
   <soapenv:Body>
      <coun:GetCurrencyRequestget_currency>
         <country>{0}</country>
      </coun:GetCurrencyRequestget_currency>
   </soapenv:Body>
</soapenv:Envelope>""".format(country)

        headers = {'Content-Type': 'text/xml;charset=UTF-8', 'SOAPAction': '"http://localhost:8080/CountryService/get_currency"'}
        r = requests.post('http://localhost:8080/CountryService', data=soap_request, headers=headers)
        try:
            tree = etree.fromstring(r.content)
            currency = tree.xpath('//currency')
            message = currency[0].text
        except:
            message = "Failure"
        return message

这些是网络服务的两个操作(get_currency和get_temp)。所以SOAPUI请求get_temp,这个操作会通过make_curr_request和requests模块发起一个SOAP请求去获取get_currency。然后结果应该就这样返回,发回给SOAPUI。

这个服务的实际操作没有什么意义(请求温度却返回货币),但我只是想让功能正常工作,这就是我现在的操作。

2 个回答

0

这个问题从昨天开始就已经解决了。

拉取请求链接: https://github.com/rancavil/tornado-webservices/pull/23

举个例子:这里有一个简单的网络服务,它不需要任何参数,只返回版本信息。 请注意,你需要:

  • 方法声明:用 @gen.coroutine 来装饰这个方法
  • 返回结果:使用 raise gen.Return(data) 来返回数据

代码:

from tornado import gen
from tornadows.soaphandler import SoapHandler
...

class Example(SoapHandler):
    @gen.coroutine
    @webservice(_params=None, _returns=Version)
    def Version(self):
        _version = Version()
        # async stuff here, let's suppose you ask other rest service or resource for the version details.
        # ...
        # returns the result.
        raise gen.Return(_version)

谢谢!

7

我觉得你的soap模块或者请求模块并不是异步的。

我认为加上@asyncronous这个装饰器只是解决了一部分问题。现在你在函数里面并没有发起任何异步请求(每个请求都是阻塞的,这样会占用服务器,直到你的方法执行完毕)。

你可以通过使用tornado的AsynHttpClient来改变这种情况。这几乎可以完全替代请求模块。从文档的例子来看:

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        http.fetch("http://friendfeed-api.com/v2/feed/bret",
                   callback=self.on_response)

    def on_response(self, response):
        if response.error: raise tornado.web.HTTPError(500)
        json = tornado.escape.json_decode(response.body)
        self.write("Fetched " + str(len(json["entries"])) + " entries "
                   "from the FriendFeed API")
        self.finish()

他们的方法是用async装饰的,并且他们在发起异步的http请求。这时候流程会有点奇怪。当你使用AsyncHttpClient时,它不会锁住事件循环(请注意,我这周才开始用tornado,如果我的术语不太准确,请多包涵)。这让服务器可以自由处理进来的请求。当你的asynchttp请求完成后,回调方法会被执行,在这个例子中是on_response

在这里,你可以相对容易地用tornado的asynchttp客户端替换请求模块。不过,对于你的soap服务来说,事情可能会更复杂。你可以围绕你的soap客户端创建一个本地的web服务,并使用tornado的异步http客户端向它发起异步请求???

这会产生一些复杂的回调逻辑,可以通过使用gen装饰器来解决。

撰写回答