summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNo'am Rosenthal <noam.rosenthal@nokia.com>2011-07-13 16:53:52 -0700
committerNo'am Rosenthal <noam.rosenthal@nokia.com>2011-07-13 16:53:52 -0700
commite887e8f0130b9290157aac9629ec8cc83f48b9d0 (patch)
tree2cbb6f2e390cd2c36a6848db5e0f5a26e4bf13dd
parentb49ccb384eb37f821c06c1e68dd3ca11837f54c6 (diff)
downloadqtwebchannel-e887e8f0130b9290157aac9629ec8cc83f48b9d0.tar.gz
Switch from WebSockets to Comet
-rw-r--r--examples/qmlapp/index.html8
-rw-r--r--examples/qmlapp/qmlapp.qml20
-rw-r--r--src/qwebchannel.cpp148
-rw-r--r--src/qwebchannel.h23
-rw-r--r--src/qwebchannel.js97
-rw-r--r--src/qwebchannel_plugin.cpp1
-rw-r--r--src/script-communicator.js86
-rw-r--r--src/src.pro4
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