diff options
author | No'am Rosenthal <noam.rosenthal@nokia.com> | 2011-07-13 16:53:52 -0700 |
---|---|---|
committer | No'am Rosenthal <noam.rosenthal@nokia.com> | 2011-07-13 16:53:52 -0700 |
commit | e887e8f0130b9290157aac9629ec8cc83f48b9d0 (patch) | |
tree | 2cbb6f2e390cd2c36a6848db5e0f5a26e4bf13dd | |
parent | b49ccb384eb37f821c06c1e68dd3ca11837f54c6 (diff) | |
download | qtwebchannel-e887e8f0130b9290157aac9629ec8cc83f48b9d0.tar.gz |
Switch from WebSockets to Comet
-rw-r--r-- | examples/qmlapp/index.html | 8 | ||||
-rw-r--r-- | examples/qmlapp/qmlapp.qml | 20 | ||||
-rw-r--r-- | src/qwebchannel.cpp | 148 | ||||
-rw-r--r-- | src/qwebchannel.h | 23 | ||||
-rw-r--r-- | src/qwebchannel.js | 97 | ||||
-rw-r--r-- | src/qwebchannel_plugin.cpp | 1 | ||||
-rw-r--r-- | src/script-communicator.js | 86 | ||||
-rw-r--r-- | src/src.pro | 4 |
8 files changed, 295 insertions, 92 deletions
diff --git a/examples/qmlapp/index.html b/examples/qmlapp/index.html index 4004346..e1a4bf6 100644 --- a/examples/qmlapp/index.html +++ b/examples/qmlapp/index.html @@ -1,11 +1,13 @@ <html> <head> - <script src="../../src/qwebchannel.js"></script> + <script src="../../src/qwebchannel.js"></script> <script> function load() { - navigator.webChannel.init(); + navigator.webChannel.subscribe("incoming-call", + function(message) { debug(message); } + ); navigator.webChannel.exec({a:"This is a request from HTML"}, function(response) { - document.body.innerHTML = response.b; + debug(response.b); }); } </script> diff --git a/examples/qmlapp/qmlapp.qml b/examples/qmlapp/qmlapp.qml index 06f9cdc..9872c9d 100644 --- a/examples/qmlapp/qmlapp.qml +++ b/examples/qmlapp/qmlapp.qml @@ -8,8 +8,9 @@ Rectangle { WebChannel { id: webChannel useSecret: false + onRequest: { - var data = JSON.parse(request); + var data = JSON.parse(requestData); txt.text = data.a; response.send(JSON.stringify({b:'This is a response from QML'})); } @@ -21,11 +22,24 @@ Rectangle { height: 200 settings.localContentCanAccessRemoteUrls: true settings.developerExtrasEnabled: true - url: "index.html?baseUrl=" + webChannel.baseUrl + url: "index.html?webchannel_baseUrl=" + webChannel.baseUrl } + TextEdit { + width: 1000 + height: 100 + id: editor + anchors.top: parent.top + } Text { id: txt - anchors.top: parent.top + anchors.top: editor.bottom + text: "BLA" + MouseArea { + anchors.fill: parent + onClicked: { + webChannel.broadcast("incoming-call", JSON.stringify(editor.text)); + } + } } } diff --git a/src/qwebchannel.cpp b/src/qwebchannel.cpp index 7159879..242eff8 100644 --- a/src/qwebchannel.cpp +++ b/src/qwebchannel.cpp @@ -51,57 +51,46 @@ #include <QTimer> #include <QUuid> -class QWebChannelResponder : public QObject { - Q_OBJECT +QWebChannelResponder::QWebChannelResponder(QTcpSocket* s) + : QObject(0) + , socket(s) +{ + connect(socket.data(), SIGNAL(disconnected()), socket.data(), SLOT(deleteLater())); +} -public: - QWebChannelResponder(QTcpSocket* s, const QString& id) - : QObject(0) - , socket(s) - , uid(id) - { - connect(socket.data(), SIGNAL(disconnected()), socket.data(), SLOT(deleteLater())); - } +void QWebChannelResponder::open() +{ + if (!socket || !socket->isOpen()) + return; -public slots: - void open() - { - if (!socket || !socket->isOpen()) - return; - socket->write(QString("HTTP/1.1 200 OK\r\n" - "Content-Type: text/javascript\r\n" - "socket: Close\r\n" - "\r\n" - "navigator.webChannel.callback('%1',").arg(uid).toUtf8()); - } + socket->write("HTTP/1.1 200 OK\r\n" + "Content-Type: text/json\r\n" + "\r\n"); +} - void write(const QString& data) - { - if (!socket || !socket->isOpen()) - return; - socket->write(data.toUtf8()); - } +void QWebChannelResponder::write(const QString& data) +{ + if (!socket || !socket->isOpen()) + return; + socket->write(data.toUtf8()); +} - void close() - { - if (!socket) - return; - socket->write(")"); - deleteLater(); - socket->close(); - } +void QWebChannelResponder::close() +{ + if (!socket) + return; + deleteLater(); + socket->close(); +} - void send(const QString& data) - { - open(); - write(data); - close(); - } +void QWebChannelResponder::send(const QString& data) +{ + qWarning() << __func__ << data; + open(); + write(data); + close(); +} -private: - QPointer<QTcpSocket> socket; - QString uid; -}; class QWebChannelPrivate : public QObject { Q_OBJECT @@ -115,6 +104,8 @@ public: QTcpServer* server; QString secret; QStringList allowedOrigins; + typedef QMultiMap<QString, QPointer<QTcpSocket> > SubscriberMap; + SubscriberMap subscribers; bool starting; QWebChannelPrivate(QObject* parent) @@ -142,8 +133,9 @@ public: public slots: void init(); - void postMessage(const QString&); + void broadcast(const QString&, const QString&); void service(); + void onSocketDelete(QObject*); signals: void request(const QString&, QObject*); @@ -151,12 +143,44 @@ signals: void noPortAvailable(); }; +void QWebChannelPrivate::onSocketDelete(QObject * o) +{ + for (SubscriberMap::iterator it = subscribers.begin(); it != subscribers.end(); ++it) { + if (it.value() != o) + continue; + subscribers.erase(it); + return; + } +} + +void QWebChannelPrivate::broadcast(const QString& id, const QString& message) +{ + QList<QPointer<QTcpSocket> > sockets = subscribers.values(id); + + foreach(QPointer<QTcpSocket> socket, sockets) { + if (!socket) + continue; + socket->write("HTTP/1.1 200 OK\r\n" + "Content-Type: text/json\r\n" + "Connection: Close\r\n" + "Cache-Control: No-Cache\r\n" + "\r\n"); + socket->write(message.toUtf8()); + socket->close(); + } +} + void QWebChannelPrivate::service() { if (!server->hasPendingConnections()) return; + QTcpSocket* socket = server->nextPendingConnection(); - socket->waitForReadyRead(); + if (!socket->canReadLine()) + if (!socket->waitForReadyRead(1000)) + return; + + QString firstLine = socket->readLine(); QStringList firstLineValues = firstLine.split(' '); QString method = firstLineValues[0]; @@ -187,7 +211,10 @@ void QWebChannelPrivate::service() queryMap[q.left(idx)] = q.mid(idx + 1); } + while (!socket->canReadLine()) + socket->waitForReadyRead(); QString requestString = socket->readAll(); + qWarning() << requestString; QStringList headersAndContent = requestString.split("\r\n\r\n"); QStringList headerList = headersAndContent[0].split("\r\n"); QMap<QString, QString> headerMap; @@ -199,7 +226,7 @@ void QWebChannelPrivate::service() QStringList pathElements = path.split('/'); pathElements.removeFirst(); - if (pathElements.length() < 3 || (useSecret && pathElements[0] != secret)) { + if (pathElements.length() < 2 || (useSecret && pathElements[0] != secret)) { socket->write( "HTTP/1.1 401 Wrong Path\r\n" "Content-Type: text/html\r\n" @@ -211,12 +238,22 @@ void QWebChannelPrivate::service() } if (method == "GET") { - QString message = QStringList(pathElements.mid(2)).join("/"); - QWebChannelResponder* responder = new QWebChannelResponder(socket, pathElements[1]); - responder->moveToThread(thread); - emit request(QUrl::fromPercentEncoding(message.toUtf8()), responder); + QString type = pathElements[1]; + if (type == "exec") { + QString message = QStringList(pathElements.mid(2)).join("/"); + QWebChannelResponder* responder = new QWebChannelResponder(socket); + responder->moveToThread(thread); + QString msg = QUrl::fromPercentEncoding(message.toUtf8()); + qWarning() << __func__ << __LINE__ << msg << responder; + + emit request(msg, responder); + } else if (type == "subscribe") { + QString id = pathElements[2]; + connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); + connect(socket, SIGNAL(destroyed(QObject*)), this, SLOT(onSocketDelete(QObject*))); + subscribers.insert(id, socket); + } } - QTimer::singleShot(0, this, SLOT(service())); } void QWebChannelPrivate::init() @@ -320,5 +357,10 @@ void QWebChannel::onInitialized() emit baseUrlChanged(d->baseUrl); } +void QWebChannel::broadcast(const QString& id, const QString& data) +{ + d->broadcast(id, data); +} + #include <qwebchannel.moc> diff --git a/src/qwebchannel.h b/src/qwebchannel.h index 457a631..70c51ac 100644 --- a/src/qwebchannel.h +++ b/src/qwebchannel.h @@ -43,9 +43,26 @@ #define QWEBCHANNEL_H #include <QtDeclarative/QDeclarativeItem> +#include <QTcpSocket> +#include <QPointer> class QWebChannelPrivate; +class QWebChannelResponder : public QObject { + Q_OBJECT + +public: + QWebChannelResponder(QTcpSocket* s); + +public slots: + void open(); + void write(const QString& data); + void close(); + void send(const QString& data); +private: + QPointer<QTcpSocket> socket; +}; + class QWebChannel : public QDeclarativeItem { Q_OBJECT @@ -74,9 +91,13 @@ public: signals: void baseUrlChanged(const QUrl &); void scriptUrlChanged(const QUrl &); - void request(const QString& request, QObject* response); + void request(const QString& requestData, QObject* response); void noPortAvailable(); +public slots: + void broadcast(const QString& id, const QString& data); + void writeResponseData(const QString& responseID, const QString& data); + private slots: void onInitialized(); diff --git a/src/qwebchannel.js b/src/qwebchannel.js index 0c3714e..6bcfa2a 100644 --- a/src/qwebchannel.js +++ b/src/qwebchannel.js @@ -39,39 +39,74 @@ ** ****************************************************************************/ -navigator.webChannel = { - requests: {}, - id: "", - init: function() { - this.queryVariables = {}; - var search = location.search.substr(1).split("&"); - for (var i = 0; i < search.length; ++i) { - var s = search[i].split("="); - this.queryVariables[s[0]] = s[1]; - } - }, - exec: function(message, onSuccess, onFailure) { - var element = document.createElement("script"); - function S4() { - return (((1+Math.random())*0x10000)|0).toString(16).substring(1); - } - function guid() { - return (S4()+S4()+"."+S4()+"."+S4()+"."+S4()+"."+S4()+S4()+S4()); +function debug(x) { + document.body.innerHTML = document.body.innerHTML + "<p>" + x + "</p>"; +} + + +(function(){ + +var queryVariables = {}; +var requests = {}; +var subscribers = {}; +var baseUrl = ""; +var initialized = false; +function S4() { + return (((1+Math.random())*0x10000)|0).toString(16).substring(1); +} +function guid() { + return (S4()+S4()+"."+S4()+"."+S4()+"."+S4()+"."+S4()+S4()+S4()); +} + + +function sendRequest(url, onSuccess, onFailure) +{ + var req = new XMLHttpRequest(); + req.open("GET", url, true); + req.onreadystatechange = function() { + if (req.readyState != 4) + return; + if (req.status != 200 && req.status != 304) { + onFailure(); + return; } + onSuccess(JSON.parse(req.responseText)); + }; + req.send(null); +} - var id = guid(); - element.async = true; - element.src = this.queryVariables.baseUrl + "/"+ id + "/" + JSON.stringify(message); - document.body.innerHTML = element.src; - this.requests[id] = { element: element, onSuccess: onSuccess, onFailure: onFailure }; - document.head.appendChild(element); - }, +function poll(url, callback) +{ + setTimeout(function() { + sendRequest(url + "/" + guid(), + function(object) { + poll(url, callback); + callback(object); + }, function() { poll(url, callback); }); + }, 0); +} - callback: function(id, message) { - var req = this.requests[id]; - (req.onSuccess)(message); - var element = req.element; - setTimeout(function() { document.head.removeChild(element); }, 0); - delete this.requests[id]; +function init() { + if (initialized) + return; + initialized = true; + var search = location.search.substr(1).split("&"); + for (var i = 0; i < search.length; ++i) { + var s = search[i].split("="); + queryVariables[s[0]] = s[1]; } + baseUrl = queryVariables.webchannel_baseUrl; +} + +navigator.webChannel = { + exec: function(message, onSuccess, onFailure) { + init(); + sendRequest(baseUrl + "/exec/"+ JSON.stringify(message), onSuccess, onFailure); + }, + + subscribe: function(id, callback) { + init(); + poll(baseUrl + "/subscribe/" + id, callback); + }, }; +})(); diff --git a/src/qwebchannel_plugin.cpp b/src/qwebchannel_plugin.cpp index bcd4523..102dc2f 100644 --- a/src/qwebchannel_plugin.cpp +++ b/src/qwebchannel_plugin.cpp @@ -44,6 +44,7 @@ #include "qwebchannel.h" QML_DECLARE_TYPE(QWebChannel) +QML_DECLARE_TYPE(QWebChannelResponder) #include "qwebchannel_plugin.h" diff --git a/src/script-communicator.js b/src/script-communicator.js new file mode 100644 index 0000000..bcafe15 --- /dev/null +++ b/src/script-communicator.js @@ -0,0 +1,86 @@ +/* + * Implementation of script communication that: + * - uses script tags for communication, but can detect when a script isn't loaded (this is non-trivial to implement across browsers) + * - works across domains as long as you control the domains + * - works on IE 6, IE 7, IE 8, FF X, Safari, Chrome and Opera + * - small (80 lines of code) with no dependencies + * + * For more info and usage check out: + * http://amix.dk/blog/post/19489#ScriptCommunicator-implementing-comet-long-polling-for-all-browse + * + * Made by amix the lucky stiff - amix.dk + * Copyright Plurk 2010, released under BSD license + */ +ScriptCommunicator = { + + callback_called: false, + + /* + * Important: + * The JavaScript you source must do some kind of call back into your code + * and this call back has to set ScriptCommunicator.callback_called = true + * for this to work! + */ + sourceJavaScript: function(uri, on_success, on_error) { + ScriptCommunicator.callback_called = false; + + ScriptCommunicator._onSuccess = on_success; + ScriptCommunicator._onError = on_error; + + var loaded_text = 'if(!ScriptCommunicator.callback_called) {' + + 'ScriptCommunicator.onError();'+ + '}'+ + 'else { ' + + 'ScriptCommunicator.onSuccess();'+ + '}'; + + var agent = navigator.userAgent.toLowerCase(); + + if(agent.indexOf("khtml") != -1) { //Safari + document.writeln('<script type="text/javascript" src="'+uri+'" class="temp_script"><\/script>'); + document.writeln('<script type="text/javascript" class="temp_script">'+ loaded_text +'<\/script>'); + } + else { + var script_channel = document.createElement('script'); + script_channel.src = uri; + script_channel.type = "text/javascript"; + script_channel.className = 'temp_script'; + + var loaded = null; + if(agent.indexOf("msie") != -1) { //IE + script_channel.onreadystatechange = ScriptCommunicator.onSuccess; + } + else if(agent.indexOf('firefox/4.0')) { + script_channel.onload = function() { + eval(loaded_text); + } + } + else { + var loaded = document.createElement('script'); + loaded.type = "text/javascript"; + loaded.className = 'temp_script'; + loaded.text = loaded_text; + } + + var body = document.getElementsByTagName('body')[0]; + body.appendChild(script_channel); + if(loaded) + body.appendChild(loaded); + } + }, + + onSuccess: function() { + if(this.readyState == 'loaded' && !ScriptCommunicator.callback_called) { + return ScriptCommunicator.onError(); + } + + if(!this.readyState || this.readyState === "loaded" || this.readyState === "complete") { + return ScriptCommunicator._onSuccess(); + } + }, + + onError: function() { + return ScriptCommunicator._onError(); + } + +} diff --git a/src/src.pro b/src/src.pro index 9c424cf..3cf4129 100644 --- a/src/src.pro +++ b/src/src.pro @@ -12,7 +12,9 @@ TARGET = $$qtLibraryTarget($$TARGET) SOURCES += qwebchannel_plugin.cpp HEADERS += qwebchannel_plugin.h -OTHER_FILES = qmldir +OTHER_FILES = qmldir \ + qwebchannel.js \ + script-communicator.js !equals(_PRO_FILE_PWD_, $$OUT_PWD) { copy_qmldir.target = $$OUT_PWD/qmldir |