网络(Networking)

Qt5在C++中有丰富的网络相关的类。例如在http协议层上使用请求回答方式的高级封装类如QNetworkRequest,QNetworkReply,QNetworkAccessManageer。也有在TCP/IP或者UDP协议层封装的低级类如QTcpSocket,QTcpServer和QUdpSocket。还有一些额外的类用来管理代理,网络缓冲和系统网络配置。

这章将不再阐述关于C++网络方面的知识,这章是关于QtQuick与网络的知识。我们应该怎样连接QML/JS用户界面与网络服务,或者如何通过网络服务来为我们用户界面提供服务。已经有很好的教材和示例覆盖了关于Qt/C++的网络编程。然后你只需要阅读这章相关的C++集成来满足你的QtQuick就可以了。

通过HTTP服务UI(Serving UI via HTTP)

通过HTTP加载一个简单的用户界面,我们需要一个web服务器,它为UI文件服务。但是首先我们需要有用户界面,我们在项目里创建一个创建了红色矩形框的main.qml。

// main.qml
import QtQuick 2.0

Rectangle {
    width: 320
    height: 320
    color: '#ff0000'
}

我们加载一段python脚本来提供这个文件:

$ cd <PROJECT>
# python -m SimpleHTTPServer 8080

现在我们可以通过http://localhost:8000/main.qml来访问,你可以像下面这样测试:

$ curl http://localhost:8000/main.qml

或者你可以用浏览器来访问。浏览器无法识别QML,并且无法通过文档来渲染。我们需要创建一个可以浏览QML文档的浏览器。为了渲染文档,我们需要指出qmlscene的位置。不幸的是qmlscene只能读取本地文件。我们为了突破这个限制,我们可以使用自己写的qmlscene或者使用QML动态加载。我们选择动态加载的方式。我们选择一个加载元素来加载远程的文档。

// remote.qml
import QtQuick 2.0

Loader {
    id: root
    source: 'http://localhost:8080/main2.qml'
    onLoaded: {
        root.width = item.width
        root.height = item.height
    }
}

我们现在可以使用qmlscene来加载remote.qml文档。这里仍然有一个小问题。加载器将会调整加载项的大小。我们的qmlscene需要适配大小。可以使用–resize-to-root选项来运行qmlscene。

$ qmlscene --resize-to-root remote.qml

按照root元素调整大小,告诉qmlscene按照root元素的大小调它的窗口大小。remote现在从本地服务器加载main.qml,并且可以自动调整加载的用户界面。方便且简单。

注意

如果你不想使用一个本地服务器,你可以使用来自GitHub的gist服务。Gist是一个在线剪切板服务,就像PasteBin等等。可以在https://gist.github.com下使用。我创建了一个简单的gist例子,地址是https://gist.github.com/jryannel/7983492。这将会返回一个绿色矩形框。由于gist连接提供的是HTML代码,我们需要连接一个/raw来读取原始文件而不是HTML代码。

// remote.qml
import QtQuick 2.0

Loader {
    id: root
    source: 'https://gist.github.com/jryannel/7983492/raw'
    onLoaded: {
        root.width = item.width
        root.height = item.height
    }
}

从网络加载另一个文件,你只需要引用组件名。例如一个Button.qml,只要它们在同一个远程文件夹下就能够像正常一样访问。

11.1.1 网络组件(Networked Components)

我们做了一个小实验。我们在远程端添加一个按钮作为可以复用的组件。

- src/main.qml
- src/Button.qml

我们修改main.qml来使用button:

import QtQuick 2.0

Rectangle {
    width: 320
    height: 320
    color: '#ff0000'

    Button {
        anchors.centerIn: parent
        text: 'Click Me'
        onClicked: Qt.quit()
    }
}

再次加载我们的web服务器:

$ cd src
# python -m SimpleHTTPServer 8080

再次使用http加载远mainQML文件:

$ qmlscene --resize-to-root remote.qml

我们看到一个错误:

http://localhost:8080/main2.qml:11:5: Button is not a type

所以,在远程加载时,QML无法解决Button组件的问题。如果代码使用本地加载qmlscene src/main.qml,将不会有问题。Qt能够直接解析本地文件,并且检测哪些组件可用,但是使用http的远程访问没有“list-dir”函数。我们可以在main.qml中使用import声明来强制QML加载元素:

import "http://localhost:8080" as Remote

...

Remote.Button { ... }

再次运行qmlscene后,它将正常工作:

$ qmlscene --resize-to-root remote.qml

这是完整的代码:

// main2.qml
import QtQuick 2.0
import "http://localhost:8080" 1.0 as Remote

Rectangle {
    width: 320
    height: 320
    color: '#ff0000'

    Remote.Button {
        anchors.centerIn: parent
        text: 'Click Me'
        onClicked: Qt.quit()
    }
}

一个更好的选择是在服务器端使用qmldir文件来控制输出:

// qmldir
Button 1.0 Button.qml

然后更新main.qml:

import "http://localhost:8080" 1.0 as Remote

...

Remote.Button { ... }

当从本地文件系统使用组件时,它们的创建没有延迟。当组件通过网络加载时,它们的创建是异步的。创建时间的影响是未知的,当其它组件已经完成时,一个组件可能还没有完成加载。当通过网络加载组件时,需要考虑这些。

模板(Templating)

当使用HTML项目时,通常需要使用模板驱动开发。服务器使用模板机制生成代码在服务器端对一个HTML根进行扩展。例如一个照片列表的列表头将使用HTML编码,动态图片链表将会使用模板机制动态生成。通常这也可以使用QML解决,但是仍然有一些问题。

首先,HTML开发者这样做的原因是为了克服HTML后端的限制。在HTML中没有组件模型,动态机制方面不得不使用这些机制或者在客户端边使用javascript编程。很多的JS框架产生(jQuery,dojo,backbone,angular,…)可以用来解决这个问题,把更多的逻辑问题放在使用网络服务连接的客户端浏览器。客户端使用一个web服务的接口(例如JSON服务,或者XML数据服务)与服务器通信。这也适用于QML。

第二个问题是来自QML的组件缓冲。当QML访问一个组件时,缓冲渲染树(render-tree),并且只加载缓冲版本来渲染。磁盘上的修改版本或者远程的修改在没有重新启动客户端时不会被检测到。为了克服这个问题,我们需要跟踪。我们使用URL后缀来加载链接(例如http://localhost:8080/main.qml#1234),“#1234”就是后缀标识。HTTP服务器总是为相同的文档服务,但是QML将使用完整的链接来保存这个文档,包括链接标识。每次我们访问的这个链接的标识获得改变,QML缓冲无法获得这个信息。这个后缀标识可以是当前时间的毫秒或者一个随机数。

Loader {
    source: 'http://localhost:8080/main.qml#' + new Date().getTime()
}

总之,模板可以实现,但是不推荐,无法完整发挥QML的长处。一个更好的方法是使用web服务提供JSON或者XML数据服务。

HTTP请求(HTTP Requests)

从c++方面来看,Qt中完成http请求通常是使用QNetworkRequest和QNetworkReply,然后使用Qt/C++将响应推送到集成的QML。所以我们尝试使用QtQuick的工具给我们的网络信息尾部封装了小段信息,然后推送这些信息。为此我们使用一个帮助对象来构造http请求,和循环响应。它使用java脚本的XMLHttpRequest对象的格式。

XMLHttpRequest对象允许用户注册一个响应操作函数和一个链接。一个请求能够使用http动作来发送(如get,post,put,delete,等等)。当响应到达时,会调用注册的操作函数。操作函数会被调用多次。每次调用请求的状态都已经改变(例如信息头部已接收,或者响应完成)。

下面是一个简短的例子:

function request() {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
            print('HEADERS_RECEIVED');
        } else if(xhr.readyState === XMLHttpRequest.DONE) {
            print('DONE');
        }
    }
    xhr.open("GET", "http://example.com");
    xhr.send();
}

从一个响应中你可以获取XML格式的数据或者是原始文本。可以遍历XML结果但是通常使用原始文本来匹配JSON格式响应。使用JSON.parse(text)可以JSON文档将转换为JS对象使用。

...
} else if(xhr.readyState === XMLHttpRequest.DONE) {
    var object = JSON.parse(xhr.responseText.toString());
    print(JSON.stringify(object, null, 2));
}

在响应操作中,我们访问原始响应文本并且将它转换为一个javascript对象。JSON对象是一个可以使用的JS对象(在javascript中,一个对象可以是对象或者一个数组)。

注意

toString()转换似乎让代码更加稳定。在不使用显式的转换下我有几次都解析错误。不确定是什么问题引起的。

11.3.1 Flickr调用(Flickr Call)

让我们看看更加真实的例子。一个典型的例子是使用网络相册服务来取得公共订阅中新上传的图片。我们可以使用http://api.flicker.com/services/feeds/photos_public.gne链接。不幸的是它默认返回XML流格式的数据,在qml中可以很方便的使用XmlListModel来解析。为了达到只关注JSON数据的目的,我们需要在请求中附加一些参数可以得到JSON响应:http://api.flickr.com/services/feeds/photo_public.gne?format=json&nojsoncallback=1。这将会返回一个没有JSON回调的JSON响应。

注意
一个JSON回调将JSON响应包装在一个函数调用中。这是一个HTML编程中的快捷方式,使用脚本标记来创建一个JSON请求。响应将触发本地定义的回调函数。在QML中没有JSON回调的工作机制。

使用curl来查看响应:

curl "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich"

响应如下:

{
    "title": "Recent Uploads tagged munich",
    ...
    "items": [
        {
        "title": "Candle lit dinner in Munich",
        "media": {"m":"http://farm8.staticflickr.com/7313/11444882743_2f5f87169f_m.jpg"},
        ...
        },{
        "title": "Munich after sunset: a train full of \"must haves\" =",
        "media": {"m":"http://farm8.staticflickr.com/7394/11443414206_a462c80e83_m.jpg"},
        ...
        }
    ]
    ...
}

JSON文档已经定义了结构体。一个对象包含一个标题和子项的属性。标题是一个字符串,子项是一组对象。当转换文本为一个JSON文档后,你可以单独访问这些条目,它们都是可用的JS对象或者结构体数组。

// JS code
obj = JSON.parse(response);
print(obj.title) // => "Recent Uploads tagged munich"
for(var i=0; i<obj.items.length; i++) {
    // iterate of the items array entries
    print(obj.items[i].title) // title of picture
    print(obj.items[i].media.m) // url of thumbnail
}

我们可以使用obj.items数组将JS数组作为链表视图的模型,试着完成这个操作。首先我们需要取得响应并且将它转换为可用的JS对象。然后设置response.items属性作为链表视图的模型。

function request() {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if(...) {
            ...
        } else if(xhr.readyState === XMLHttpRequest.DONE) {
            var response = JSON.parse(xhr.responseText.toString());
            // set JS object as model for listview
            view.model = response.items;
        }
    }
    xhr.open("GET", "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich");
    xhr.send();
}

下面是完整的源代码,当组件加载完成后,我们创建请求。然后使用请求的响应作为我们链表视图的模型。

import QtQuick 2.0

Rectangle {
    width: 320
    height: 480
    ListView {
        id: view
        anchors.fill: parent
        delegate: Thumbnail {
            width: view.width
            text: modelData.title
            iconSource: modelData.media.m
        }
    }

    function request() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
                print('HEADERS_RECEIVED')
            } else if(xhr.readyState === XMLHttpRequest.DONE) {
                print('DONE')
                var json = JSON.parse(xhr.responseText.toString())
                view.model = json.items
            }
        }
        xhr.open("GET", "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich");
        xhr.send();
    }

    Component.onCompleted: {
        request()
    }
}

当文档完整加载后(Component.onCompleted),我们从Flickr请求最新的订阅内容。我们解析JSON的响应并且设置item数组作为我们视图的模型。链表视图有一个代理可以在一行中显示图标缩略图和标题文本。

另一种方法是添加一个ListModel,并且将每个子项添加到链表模型中。为了支持更大的模型,需要支持分页和懒加载。

本地文件(Local files)

使用XMLHttpRequest也可以加载本地文件(XML/JSON)。例如加载一个本地名为“colors.json”的文件可以这样使用:

xhr.open("GET", "colors.json");

我们使用它读取一个颜色表并且使用表格来显示。从QtQuick这边无法修改文件。为了将源数据存储回去,我们需要一个基于HTTP服务器的REST服务支持或者一个用来访问文件的QtQuick扩展。

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360
    color: '#000'

    GridView {
        id: view
        anchors.fill: parent
        cellWidth: width/4
        cellHeight: cellWidth
        delegate: Rectangle {
            width: view.cellWidth
            height: view.cellHeight
            color: modelData.value
        }
    }

    function request() {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
                print('HEADERS_RECEIVED')
            } else if(xhr.readyState === XMLHttpRequest.DONE) {
                print('DONE');
                var obj = JSON.parse(xhr.responseText.toString());
                view.model = obj.colors
            }
        }
        xhr.open("GET", "colors.json");
        xhr.send();
    }

    Component.onCompleted: {
        request()
    }
}

也可以使用XmlListModel来替代XMLHttpRequest访问本地文件。

import QtQuick.XmlListModel 2.0

XmlListModel {
    source: "http://localhost:8080/colors.xml"
    query: "/colors"
    XmlRole { name: 'color'; query: 'name/string()' }
    XmlRole { name: 'value'; query: 'value/string()' }
}

XmlListModel只能用来读取XML文件,不能读取JSON文件。

REST接口(REST API)

为了使用web服务,我们首先需要创建它。我们使用Flask(http://flask.pocoo.org),一个基于python创建简单的颜色web服务的HTTP服务器应用。你也可以使用其它的web服务器,只要它接收和返回JSON数据。通过web服务来管理一组已经命名的颜色。在这个例子中,管理意味着CRUD(创建-读取-更新-删除)。

在Flask中一个简单的web服务可以写入一个文件。我们使用一个空的服务器.py文件开始,在这个文件中我们创建一些规则并且从额外的JSON文件中加载初始颜色。你可以查看Flask文档获取更多的帮助。

// 当你运行这个脚本后,它会在http://localhost:5000。
from flask import Flask, jsonify, request
import json

colors = json.load(file('colors.json', 'r'))

app = Flask(__name__)

# ... service calls go here

if __name__ == '__main__':
    app.run(debug = True)

我们开始添加我们的CRUD(创建,读取,更新,删除)到我们的web服务。

11.5.1 读取请求(Read Request)

从web服务读取数据,我们提供GET方法来读取所有的颜色。

@app.route('/colors', methods = ['GET'])
def get_colors():
    return jsonify( { "colors" :  colors })

这将会返回‘/colors’下的颜色。我们使用curl来创建一个http请求测试。

curl -i -GET http://localhost:5000/colors

这将会返回给我们JSON数据的颜色链表。

11.5.2 读取接口(Read Entry)

为了通过名字读取颜色,我们提供更加详细的后缀,定位在‘/colors/‘下。名称是后缀的参数,用来识别一个独立的颜色。

@app.route('/colors/<name>', methods = ['GET'])
def get_color(name):
    for color in colors:
        if color["name"] == name:
            return jsonify( color )

我们再次使用curl测试,例如获取一个红色的接口。

curl -i -GET http://localhost:5000/colors/red

这将返回一个JSON数据的颜色。

11.5.3 创建接口(Create Entry)

目前我们仅仅使用了HTTP GET方法。为了在服务器端创建一个接口,我们使用POST方法,并且将新的颜色信息发使用POST数据发送。后缀与获取所有颜色相同,但是我们需要使用一个POST请求。

@app.route('/colors', methods= ['POST'])
def create_color():
    color = {
        'name': request.json['name'],
        'value': request.json['value']
    }
    colors.append(color)
    return jsonify( color ), 201

curl非常灵活,允许我们使用JSON数据作为新的接口包含在POST请求中。

curl -i -H "Content-Type: application/json" -X POST -d '{"name":"gray1","value":"#333"}' http://localhost:5000/colors

11.5.4 更新接口(Update Entry)

我们使用PUT HTTP方法来添加新的update接口。后缀与取得一个颜色接口相同。当颜色更新后,我们获取更新后JSON数据的颜色。

@app.route('/colors/<name>', methods= ['PUT'])
def update_color(name):
    for color in colors:
        if color["name"] == name:
            color['value'] = request.json.get('value', color['value'])
            return jsonify( color )

在curl请求中,我们用JSON数据来定义更新值,后缀名用来识别哪个颜色需要更新。

curl -i -H "Content-Type: application/json" -X PUT -d '{"value":"#666"}' http://localhost:5000/colors/red

11.5.5 删除接口(Delete Entry)

使用DELETE HTTP来完成删除接口。使用与颜色相同的后缀,但是使用DELETE HTTP方法。

@app.route('/colors/<name>', methods=['DELETE'])
def delete_color(name):
    success = False
    for color in colors:
        if color["name"] == name:
            colors.remove(color)
            success = True
    return jsonify( { 'result' : success } )

这个请求看起来与GET请求一个颜色类似。

curl -i -X DELETE http://localhost:5000/colors/red

现在我们能够读取所有颜色,读取指定颜色,创建新的颜色,更新颜色和删除颜色。我们知道使用HTTP后缀来访问我们的接口。

动作 HTTP协议 后缀
读取所有 GET http://localhost:5000/colors
创建接口 POST http://localhost:5000/colors
读取接口 GET http://localhost:5000/colors/name
更新接口 PUT http://localhost:5000/colors/name
删除接口 DELETE http://localhost:500/colors/name

REST服务已经完成,我们现在只需要关注QML和客户端。为了创建一个简单好用的接口,我们需要映射每个动作为一个独立的HTTP请求,并且给我们的用户提供一个简单的接口。

11.5.6 REST客户端(REST Client)

为了展示REST客户端,我们写了一个小的颜色表格。这个颜色表格显示了通过HTTP请求从web服务取得的颜色。我们的用户界面提供以下命令:

  • 获取颜色链表

  • 创建颜色

  • 读取最后的颜色

  • 更新最后的颜色

  • 删除最后的颜色

我们将我们的接口包装在一个JS文件中,叫做colorservice.js,并将它导入到我们的UI中作为服务(Service)。在服务模块中,我们创建了帮助函数来为我们构造HTTP请求:

// colorservice.js
function request(verb, endpoint, obj, cb) {
    print('request: ' + verb + ' ' + BASE + (endpoint?'/' + endpoint:''))
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        print('xhr: on ready state change: ' + xhr.readyState)
        if(xhr.readyState === XMLHttpRequest.DONE) {
            if(cb) {
                var res = JSON.parse(xhr.responseText.toString())
                cb(res);
            }
        }
    }
    xhr.open(verb, BASE + (endpoint?'/' + endpoint:''));
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.setRequestHeader('Accept', 'application/json');
    var data = obj?JSON.stringify(obj):''
    xhr.send(data)
}

包含四个参数。verb,定义了使用HTTP的动作(GET,POST,PUT,DELETE)。第二个参数是作为基础地址的后缀(例如’http://localhost:5000/colors‘)。第三个参数是可选对象,作为JSON数据发送给服务的数据。最后一个选项是定义当响应返回时的回调。回调接收一个响应数据的响应对象。

在我们发送请求前,我们需要明确我们发送和接收的JSON数据修改的请求头。

// colorservice.js
function get_colors(cb) {
    // GET http://localhost:5000/colors
    request('GET', null, null, cb)
}

function create_color(entry, cb) {
    // POST http://localhost:5000/colors
    request('POST', null, entry, cb)
}

function get_color(name, cb) {
    // GET http://localhost:5000/colors/<name>
    request('GET', name, null, cb)
}

function update_color(name, entry, cb) {
    // PUT http://localhost:5000/colors/<name>
    request('PUT', name, entry, cb)
}

function delete_color(name, cb) {
    // DELETE http://localhost:5000/colors/<name>
    request('DELETE', name, null, cb)
}

这些代码在服务实现中。在UI中我们使用服务来实现我们的命令。我们有一个存储id的ListModel和存储数据的gridModel为GridView提供数据。命令使用Button元素来发送。

读取服务器颜色链表。

// rest.qml
import "colorservice.js" as Service
...
// read colors command
Button {
    text: 'Read Colors';
    onClicked: {
        Service.get_colors( function(resp) {
            print('handle get colors resp: ' + JSON.stringify(resp));
            gridModel.clear();
            var entries = resp.data;
            for(var i=0; i<entries.length; i++) {
                gridModel.append(entries[i]);
            }
        });
    }
}

在服务器上创建一个新的颜色。

// rest.qml
import "colorservice.js" as Service
...
// create new color command
Button {
    text: 'Create New';
    onClicked: {
        var index = gridModel.count-1
        var entry = {
            name: 'color-' + index,
            value: Qt.hsla(Math.random(), 0.5, 0.5, 1.0).toString()
        }
        Service.create_color(entry, function(resp) {
            print('handle create color resp: ' + JSON.stringify(resp))
            gridModel.append(resp)
        });
    }
}

基于名称读取一个颜色。

// rest.qml
import "colorservice.js" as Service
...
// read last color command
Button {
    text: 'Read Last Color';
    onClicked: {
        var index = gridModel.count-1
        var name = gridModel.get(index).name
        Service.get_color(name, function(resp) {
            print('handle get color resp:' + JSON.stringify(resp))
            message.text = resp.value
        });
    }
}

基于颜色名称更新服务器上的一个颜色。

// rest.qml
import "colorservice.js" as Service
...
// update color command
Button {
    text: 'Update Last Color'
    onClicked: {
        var index = gridModel.count-1
        var name = gridModel.get(index).name
        var entry = {
            value: Qt.hsla(Math.random(), 0.5, 0.5, 1.0).toString()
        }
        Service.update_color(name, entry, function(resp) {
            print('handle update color resp: ' + JSON.stringify(resp))
            var index = gridModel.count-1
            gridModel.setProperty(index, 'value', resp.value)
        });
    }
}

基于颜色名称删除一个颜色。

// rest.qml
import "colorservice.js" as Service
...
// delete color command
Button {
    text: 'Delete Last Color'
    onClicked: {
        var index = gridModel.count-1
        var name = gridModel.get(index).name
        Service.delete_color(name)
        gridModel.remove(index, 1)
    }
}

在CRUD(创建,读取,更新,删除)操作使用REST接口。也可以使用其它的方法来创建web服务接口。可以基于模块,每个模块都有自己的后缀。可以使用JSON RPC(http://www.jsonrpc.org/)来定义接口。当然基于XML的接口也可以使用,但是JSON在作为JavaScript部分解析进QML/JS中更有优势。

使用开放授权登陆验证(Authentication using OAuth)

OAuth是一个开放协议,允许简单的安全验证,是来自web的典型方法,用于移动和桌面应用程序。使用OAuth对通常的web服务的客户端进行身份验证,例如Google,Facebook和Twitter。

注意

对于自定义的web服务,你也可以使用典型的HTTP身份验证,例如使用XMLHttpRequest的用户名和密码的获取方法(比如xhr.open(verb,url,true,username,password))。

Auth目前不是QML/JS的接口,你需要写一些C++代码并且将身份验证导入到QML/JS中。另一个问题是安全的存储访问密码。

下面这些是我找到的有用的连接:

云服务(Engine IO)

Engine IO是DIGIA运行的一个web服务。它允许Qt/QML应用程序访问来自Engin.IO的NoSQL存储。这是一个基于云存储对象的Qt/QML接口和一个管理平台。如果你想存储一个QML应用程序的数据到云存储中,它可以提供非常方便的QML/JS的接口。

查看EnginIO的文档获得更多的帮助。

Web Sockets

webSockets不是Qt提供的。将WebSockets加入到Qt/QML中需要花费一些工作。从作者的角度来看WebSockets有巨大的潜力来添加HTTP服务缺少的功能-通知。HTTP给了我们get和post的功能,但是post还不是一个通知。目前客户端轮询服务器来获得应用程序的服务,服务器也需要能通知客户端变化和事件。你可以与QML接口比较:属性,函数,信号。也可以叫做获取/设置/调用和通知。

QML WebSocket插件将会在Qt5中加入。你可以试试来自qt playground的web sockets插件。为了测试,我们使用一个现有的web socket服务实现了echo server。

首先确保你使用的Qt5.2.x。

$ qmake --version
... Using Qt version 5.2.0 ...

然后你需要克隆web socket的代码库,并且编译它。

$ git clone git@gitorious.org:qtplayground/websockets.git
$ cd websockets
$ qmake
$ make
$ make install

现在你可以在qml模块中使用web socket。

import Qt.WebSockets 1.0

WebSocket {
    id: socket
}

测试你的web socket,我们使用来自http://websocket.org的echo server 。

import QtQuick 2.0
import Qt.WebSockets 1.0

Text {
    width: 480
    height: 48

    horizontalAlignment: Text.AlignHCenter
    verticalAlignment: Text.AlignVCenter

    WebSocket {
        id: socket
        url: "ws://echo.websocket.org"
        active: true
        onTextMessageReceived: {
            text = message
        }
        onStatusChanged: {
            if (socket.status == WebSocket.Error) {
                console.log("Error: " + socket.errorString)
            } else if (socket.status == WebSocket.Open) {
                socket.sendTextMessage("ping")
            } else if (socket.status == WebSocket.Closed) {
                text += "\nSocket closed"
            }
        }
    }
}

你可以看到我们使用socket.sendTextMessage(“ping”)作为响应在文本区域中。

11.8.1 WS Server

你可以使用Qt WebSocket的C++部分来创建你自己的WS Server或者使用一个不同的WS实现。它非常有趣,是因为它允许连接使用大量扩展的web应用程序服务的高质量渲染的QML。在这个例子中,我们将使用基于web socket的ws模块的Node JS。你首先需要安装node.js。然后创建一个ws_server文件夹,使用node package manager(npm)安装ws包。

$ cd ws_server
$ npm install ws

npm工具下载并安装了ws包到你的本地依赖文件夹中。

一个server.js文件是我们服务器的实现。服务器代码将在端口3000创建一个web socket服务并监听连接。在一个连接加入后,它将会发送一个欢迎并等待客户端信息。每个客户端发送到socket信息都会发送回客户端。

var WebSocketServer = require('ws').Server;

var server = new WebSocketServer({ port : 3000 });

server.on('connection', function(socket) {
    console.log('client connected');
    socket.on('message', function(msg) {
        console.log('Message: %s', msg);
        socket.send(msg);
    });
    socket.send('Welcome to Awesome Chat');
});

console.log('listening on port ' + server.options.port);

你需要获取使用的JavaScript标记和回调函数。

11.8.2 WS Client

在客户端我们需要一个链表视图来显示信息,和一个文本输入来输入新的聊天信息。

在例子中我们使用一个白色的标签。

// Label.qml
import QtQuick 2.0

Text {
    color: '#fff'
    horizontalAlignment: Text.AlignLeft
    verticalAlignment: Text.AlignVCenter
}

我们的聊天视图是一个链表视图,文本被加入到链表模型中。每个条目显示使用行前缀和信息标签。我们使用单元将它分为24列。

// ChatView.qml
import QtQuick 2.0

ListView {
    id: root
    width: 100
    height: 62

    model: ListModel {}

    function append(prefix, message) {
        model.append({prefix: prefix, message: message})
    }

    delegate: Row {
        width: root.width
        height: 18
        property real cw: width/24
        Label {
            width: cw*1
            height: parent.height
            text: model.prefix
        }
        Label {
            width: cw*23
            height: parent.height
            text: model.message
        }
    }
}

聊天输入框是一个简单的使用颜色包裹边界的文本输入。

// ChatInput.qml
import QtQuick 2.0

FocusScope {
    id: root
    width: 240
    height: 32
    Rectangle {
        anchors.fill: parent
        color: '#000'
        border.color: '#fff'
        border.width: 2
    }

    property alias text: input.text

    signal accepted(string text)

    TextInput {
        id: input
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.verticalCenter: parent.verticalCenter
        anchors.leftMargin: 4
        anchors.rightMargin: 4
        onAccepted: root.accepted(text)
        color: '#fff'
        focus: true
    }
}

当web socket返回一个信息后,它将会把信息添加到聊天视图中。这也同样适用于状态改变。也可以当用户输入一个聊天信息,将聊天信息拷贝添加到客户端的聊天视图中,并将信息发送给服务器。

// ws_client.qml
import QtQuick 2.0
import Qt.WebSockets 1.0

Rectangle {
    width: 360
    height: 360
    color: '#000'

    ChatView {
        id: box
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.top: parent.top
        anchors.bottom: input.top
    }
    ChatInput {
        id: input
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        focus: true
        onAccepted: {
            print('send message: ' + text)
            socket.sendTextMessage(text)
            box.append('>', text)
            text = ''
        }
    }
    WebSocket {
        id: socket

        url: "ws://localhost:3000"
        active: true
        onTextMessageReceived: {
            box.append('<', message)
        }
        onStatusChanged: {
            if (socket.status == WebSocket.Error) {
                box.append('#', 'socket error ' + socket.errorString)
            } else if (socket.status == WebSocket.Open) {
                box.append('#', 'socket open')
            } else if (socket.status == WebSocket.Closed) {
                box.append('#', 'socket closed')
            }
        }
    }
}

你首先需要运行服务器,然后是客户端。在我们简单例子中没有客户端重连的机制。

运行服务器

$ cd ws_server
$ node server.js

运行客户端

$ cd ws_client
$ qmlscene ws_client.qml

当输入文本并点击发送后,你可以看到类似下面这样。

总结(Summary)

这章我们讨论了关于QML的网络应用。请记住Qt已在本地端提供了丰富的网络接口可以在QML中使用。但是这一章的我们是想推动QML的网络运用和如何与云服务集成。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2291184112@qq.com

×

喜欢就点赞,疼爱就打赏