4

这是我构建的第一个长轮询应用程序,也是 Twisted 的第二个项目,所以如果有人对我的代码中的任何内容提出任何反馈,我将不胜感激,因为我可能完全以错误的方式处理这件事。

我一直在拼凑各种示例,并且它几乎可以正常工作,但我似乎无法找到将数据返回到 Javascript 的方法。我有一个在 Twisted 上运行的 Django 站点,它似乎运行良好,所以我不会包含 Django 位,除非有人认为它很重要,而 Django 站点唯一做的就是主持聊天。我最初使用常规轮询设置它,但我被要求将其更改为长轮询并且我几乎就在那里(我希望)。

这是 HTML/JS (long.html):

<div class="chat-messages" style="width:300px;height:400px;border:1px solid black;overflow:scroll;" id="messages">
  </div><br/>
  <form action="javascript:sendMessage();" >
    <input type="text" id="chat_nickname" name="author"/>
    <input type="text" id="chat_input" name="message" class="chat-new"/>
    <button class="submit">Submit</button>
  </form>
 </body>

 <script type="text/javascript">
    // keep track of the last time data wes received
    var last_update = 0;

    // call getData when the document has loaded
    $(document).ready(function(){
        getData(last_update);
    });

    // execute ajax call to chat_server.py
    var getData = function(last_update){
        $.ajax({
            type: "GET",
            url: "http://"+ window.location.hostname + ":8081?last_update=" + last_update + "&callback=?",
            dataType: 'json',
            async: true,
            cache:false,
            timeout: 300000,
            success: function(response){
                // append the new message to the message list
                var messages = response.data.messages;
                console.log(response);
                for (i in messages){
                    $('<p><span class="time">[' + messages[i].time +']</span> - <span class="message">' + messages[i].message + '</span></p>').appendTo('#messages');
                    if (messages[i].time > last_update){
                        last_update = messages[i].time;
                    }
                }
                console.log("Last_update: " + last_update);
                // Keep div scrolled to bottom
                $("#messages").scrollTop($("#messages")[0].scrollHeight);
                // Check again in a second
                setTimeout('getData(' + last_update + ');', 1000);
            },
            error: function(XMLHttpRequest, textStatus, errorThrown){
                // Try again in 10 seconds
                setTimeout( "getData(" + last_update + ");", 10000);
            },
            failure: function(){ console.log('fail'); },
        });
    }
    // Add a contribution to the conversation
     function sendMessage(){
        var nickname = $('#chat_nickname').val();
        var message = $('#chat_input').val();
        $('#chat_input').val("");

        console.log( "nickname: " + nickname + "; message: " + message );

        $.ajax({
             type: 'POST',
             url: '/chat/post_message/',
             data: {
                nickname: nickname, 
                message:message
             },
             success: function(data, status, xml){
                console.log("Success! - " + status);
             },
             error: function(xml, status, error){
                console.log(error + " - Error! - " + status);
             },
             complete: function(xml, status){
                console.log("Complete! - " + status);
             }
        });

    }
</script> 

sendMessage将表单中的数据传递给 Django,然后 Django 将其放入数据库(并为其添加时间)。 getData指向:8081,Twisted 正在使用下一段代码(chat_server.py)的### Chat Server部分(后半部分)进行监听:

import datetime, json, sys, time, os, types
from twisted.web import client, resource, server, wsgi
from twisted.python import threadpool
from twisted.internet import defer, task, reactor
from twisted.application import internet, service
from twisted.enterprise import adbapi
from django.core.handlers.wsgi import WSGIHandler

## Django environment variables
sys.path.append("mydjangosite")
os.environ['DJANGO_SETTINGS_MODULE'] = 'mydjangosite.settings'

 ## Tying Django's WSGIHandler into Twisted
def wsgi_resource():
    pool = threadpool.ThreadPool()
    pool.start()

    # Allow Ctrl-C to get you out cleanly:
    reactor.addSystemEventTrigger('after', 'shutdown', pool.stop)

    wsgi_resource = wsgi.WSGIResource(reactor, pool, WSGIHandler())

    return wsgi_resource

## Twisted Application Framework
application = service.Application('twisted-django')

class Root(resource.Resource):
    def __init__(self, wsgi_resource = None):
        resource.Resource.__init__(self)
        if wsgi_resource != None:
            self.wsgi_resource = wsgi_resource

    def getChild(self, path, request):
        child_path = request.prepath.pop(0)
        request.postpath.insert(0, child_path)
        return self.wsgi_resource

    def render_GET(self, request):
        id = request.args.get('id', [""])[0]
        command = request.args.get('command', [""])[0]
        self.get_page(request, id)
        return server.NOT_DONE_YET

    @defer.inlineCallbacks
    def get_page(self, request, id):
        page = yield client.getPage("/chat/latest/%s" % id)
        request.write(page)
        request.finish()

## Create and attach the django site to the reactor
django_root = Root(wsgi_resource())
django_factory = server.Site(django_root)
reactor.listenTCP(8080, django_factory)





### Chat Server
class ChatServer(resource.Resource):
    isLeaf = True

    def __init__(self):
        # throttle in seconds
        self.throttle = 5
        # store client requests
        self.delayed_requests = []
        # setup a loop to process collected requests
        loopingCall = task.LoopingCall(self.processDelayedRequests)
        loopingCall.start(self.throttle, False)
        # Initialize
        resource.Resource.__init__(self)

    def render(self, request):
        """Handle a new request"""
        request.setHeader('Content-Type', 'applicaton/json')
        args = request.args
        # set jsonp callback handler name if it exists
        if 'callback' in args:
            request.jsonpcallback = args['callback'][0]
        # set last_update if it exists
        if 'last_update' in args:
            request.last_update = args ['last_update'][0]

        data = self.getData(request)
        if type(data) is not types.InstanceType and len(data) > 0:
            # send the requested messages back
            return self.__format_response(request, 1, data)
        else:
            # or put them in the delayed request list and keep the connection going
            self.delayed_requests.append(request)
            return server.NOT_DONE_YET

    def getData(self, request):
        data = {}
        dbpool = adbapi.ConnectionPool("sqlite3", database="/home/server/development/twisted_chat/twisted-wsgi-django/mydjangosite/site.db", check_same_thread=False)
        last_update = request.last_update
        print "LAST UPDATE: ", last_update
        new_messages = dbpool.runQuery("SELECT * FROM chat_message WHERE time > %r" % request.last_update )
        return new_messages.addCallback(self.gotRows, request )

    def gotRows(self, rows, request):
        if rows:
            data = {"messages": 
                                [{ 'author': row[1], 'message':row[2],'timestamp': row[3] } for row in rows] 
                        }
            print 'MESSAGES: ', data
            if  len(data) > 0:
                return self.__format_response(request, 1, data)
            return data

    def processDelayedRequests(self):
        for request in self.delayed_requests:
            data = self.getData(request)
            if type(data) is not types.InstanceType and len(data) > 0:
                try:
                    print "REQUEST DATA:", data
                    request.write(self.__format_response(request, 1, data))
                    request.finish()
                except:
                    print 'connection lost before complete.'
                finally:
                    self.delayed_requests.remove(request)

    def __format_response(self, request, status, data):
        response = json.dumps({ "status": status, "time": int(time.time()), "data": data })
        if hasattr(request, 'jsonpcallback'):
            return request.jsonpcallback + '(' + response + ')'
        else:
            return response

chat_server = ChatServer()
chat_factory = server.Site(chat_server)
reactor.listenTCP(8081, chat_factory)

在这里,render尝试getData(它可能永远不会?)并且当它不能将请求放入self.delayed_requests. getData使用 enterprise.adbapi 对 Django 的数据库进行查询,返回一个 Deferred 实例。 processedDelayedRequests遍历延迟的请求队列,如果查询完成,该数据将传递给gotRows,然后将其转换为我想要的格式并将__format_response其发送到 JS 将数据发送回可以处理的 JS。 无论如何,这就是理论 - 上一句是我认为我的问题所在

print "LAST UPDATE: ", last_update总是打印 "LAST_UPDATE: 0" 但 last_update 是通过 JS 更新的,所以这不是错误。

print 'MESSAGES: ', data打印“{'messages': [{'timestamp': u'2013-08-10 16:59:07.909350', 'message': u'chat message', 'author': u'test'}, {'timestamp ': u'2013-08-10 17:11:56.893340', 'message': u'hello', 'author': u'pardon'}]}" 等新消息添加到数据库中。发布帖子时它会获取新数据,否则似乎工作得很好。

print "REQUEST DATA:", data根本不会触发...我认为这种方法是早期尝试使其正常工作时遗留下来的。

我得到了正确的输出,gotRows但不知道如何将该输出传递回客户端。我对自己对 Deferreds 的理解甚至没有半点信心,所以我认为这就是我的问题所在,但我不知道我能做些什么才能从这里继续前进。任何帮助将不胜感激。

4

1 回答 1

3

有时,扭曲应用程序中的函数可能有条件地返回数据,而在其他时候返回一个Deferred. 在这些情况下,您无法检查是否有数据;你可能不会,而且在你确实得到延期的情况下,再多的复查也不会改变这一点;您必须始终将此类函数转换为真正的 Deferred,使用maybeDeferred,然后将回调附加到结果。

也就是说,t.e.adbapi.ConnectionPool.runQuery()不是这样的功能。它总是返回一个延迟。处理该数据的唯一方法是附加回调。通常,您永远不会在进行初始调用的同一函数中看到扭曲应用程序中异步调用的结果。

这意味着,由于您想为每个长轮询请求运行查询,并且由于它们是无条件异步的(您必须render()在它们甚至可以开始之前从您的函数返回),您render()总是返回NOT_DONE_YET

def render(self, request):
    """Handle a new request"""
    request.setHeader('Content-Type', 'applicaton/json')
    self.getData(request)
    return server.NOT_DONE_YET

现在一切都需要在 getData 中正确发生。事实证明,对 deferred from 的处理runQuery很好;但 sql 本身有一个 相当大的问题。为了理解为什么,想象一个聪明的黑客试图访问

http://yoursite?last_update=5+and+"secret"+in+(select+password+from+users)

不过,修复很简单,不要进行字符串插值,使用绑定参数。在查询中切换%sfor a ,在函数调用本身中切换 for a。当我们这样做时,让我们将这个方法移出并移入,您不希望或不需要为每个请求的每次重试使用整个池。?%,ConnectionPool__init__

def getData(self, request):
    last_update = request.args['last_update']
    print "LAST UPDATE: ", last_update
    new_messages = self.dbpool.runQuery("SELECT *"
                                        " FROM chat_message"
                                        " WHERE time > ?", request.last_update)
    #                                                  ^ ^
    return new_messages.addCallback(self.gotRows, request)

附加到 deferred 返回的回调runQuery是返回格式化的结果;但是没有人可以退还给它;它需要自己完成所有工作。幸运的是,我们已经在request使用它,所以这并不太难。我们还需要处理没有数据返回的情况,因为另一端没有人将其添加到延迟请求列表中。

def gotRows(self, rows, request):
    if rows:
        # we have data to send back to the client! actually finish the
        # request here.
        data = {"messages": [{'author': row[1], 'message': row[2], 'timestamp': row[3]} for
                             row in rows]}
        request.write(self.__format_response(request, 1, data))
        request.finish()

    else:
        self.delayed_requests.append(self)

最后,我们需要processedDelayedRequests()render(). 它只能触发查询,它不能根据结果更新其状态,因为它没有它们。为了简化事情,我们只吃清单上的项目。

def processDelayedRequests(self):
    delayed_requests = self.delayed_requests
    self.delayed_requests = []
    while self.delayed_requests:
        # grab a request out of the "queue"
        request = self.delayed_requests.pop()

        # we can cause another attempt at getting data, but we'll never get
        # to see what hapened with it in this function.
        self.getData(request)
于 2013-08-14T18:25:44.190 回答