WebSocket
WebSockets是一种通信标准, 它支持服务器和客户端的双向通信, 被浏览器广泛支持.
HTTP的每次请求都要建立连接和断开连接, 不支持长时间的连接.
另一方面, 在TCP连接成功建立后WebSocket保持连接. 在持续连接期间, 信息可以从服务器端和客户端发送. 因为它假设连接是持续的, 叫做乒乓Ping/Pong的数据帧用来确认对方的生存和死亡.
此外, 因为连接是有状态的(维持一个建立的连接), 你不需要使用cookie返回会话的ID.
概要:WebSocket是一个有状态的双向通信
客户端是浏览器时, 如果跳到另外一个页面或者页面上建立的WebSocket连接关闭时, 连接会丢失.
因为WebSocket在浏览器端是用JavaScript实现的, 本质上,如果WebSocket的环境(对象)消失了, WebSocket连接将会丢失, 因此可以这样认为连接的时间和页面的切换是相联系的. 在实践中认为不会有很多实际上长期的连接的例子.
现在, 让我们准备一个浏览器兼容的WebSocket, 然写一些脚本.
首先, 我们将生成一个WebSocket对象. 传递ws://或者wss://起头的URL作为参数.
连接的处理在创建的时刻进行.
可以为WebSocket对象注册下面的事件处理器(event handers):
名称 | 描述 |
---|---|
onopen | open处理器 |
onclose | close处理器 |
onmessage | message处理器 |
onerror | error 处理器 |
下面是WebSocket对象的方法:
方法 | 描述 |
---|---|
send(msg) | 发送消息 |
close(code) | 断开连接 |
更多详细信息,请看 http://www.w3.org/TR/websockets/.
创建一个聊天程序
让我们基于这些处理器(handlers)生成一个聊天程序.
这个程序的主要功能就是: 用户可以输入他的名字和一个i奥信息到一个非常基本的HTML表单. 在用户点击”send”按键后, 应用将发送数据到服务器并将它保存在数据库中. 任何程序的访问者访问网页时, 举例, 最近30条信息将发送给访问者并显示在聊天窗口中.
我们将说一下如何将信息发送给订阅sbuscribed了特定主题(topic)的用户.
现在, 让我们开始实现它们, 先从客户端开始.说明: 下面的例子使用了 jQuery.
HTML (节选)
保存为public/index.html.
<!-- 消息显示区域 -->
<div id="log" style="max-width: 900px; max-height: 480px; overflow: auto;"></div>
<!-- 输入区域 -->
Name <input type="text" id="name" />
<input type="button" value="Write" onclick="sendMessage()" /><br>
<textarea id="msg" rows="4"></textarea>
这里是JavaScript的代码.
$(function(){
// 创建'cat'端点的WebSocket
ws = new WebSocket("ws://" + location.host + "/chat");
// 接收到信息
ws.onmessage = function(message){
var msg = escapeHtml(message.data);
$("#log").append("<p>" + msg + "</p>");
}
// 错误事件
ws.onerror = function(){
$("#log").append("[ Error occurred. Try reloading. ]");
}
// 断开事件
ws.onclose = function(){
$("#log").append("[ Connection closed. Try reloading. ]");
}
});
// 发送包含 '名字Name' 和 '时间Time'的信息
function sendMessage() {
if ($('#msg').val() != '') {
var name = $.trim($('#name').val());
if (name == '') {
name = "(I'm John Doe)";
}
name += ' : ' + (new Date()).toISOString() + '\n';
ws.send(name + $('#msg').val());
$('#msg').val(''); // 清除信息
}
}
接下来,用骨架实现服务器端.
$ tspawn new chatapp
created chatapp
created chatapp/controllers
created chatapp/models
:
为WebSocket互动创建端点
这里使用的名字(此例为’chat’)和生成JavaScript WebSocket对象时设置URL的路径一样. 否则它不会工作.
$ cd chatapp
$ tspawn websocket chat
created controllers/applicationendpoint.h
updated controllers/ controllers. pro
created controllers/applicationendpoint.cpp
:
生成的chatendpoint.h看起来像这样.
不需要进行什么特殊的修改.
class T_CONTROLLER_EXPORT ChatEndpoint : public ApplicationEndpoint
{
public:
ChatEndpoint() { }
ChatEndpoint(const ChatEndpoint &other);
protected:
bool onOpen(const TSession &httpSession); // open 处理器
void onClose(int closeCode); // close 处理器
void onTextReceived(const QString &text); // text receive 处理器
void onBinaryReceived(const QByteArray &binary); // binary receive 处理器
};
解释 onOpen() 处理器:
HTTP会话对象那个时刻被传递到httpSession参数中. 端点是只读的并且它的内容不能被更改(我可以在将来处理它).
我们将使用WebSocketSession对象代替来保存信息. 在endpoint类的每个方法中, 可以使用session()方法获取信息.顺便说一下, 因为信息是保存在内存里的, 如果数据量大, 在连接负载增加时内存会被压缩.
还有, 如果onOpen()返回false, WebSocket连接会被拒绝. 如果不想接受所有的连接请求, 可以实现一些分类的秘密的值存储在HTTP会话中. 例如, 仅在它们正确的情况下才接受.
接下来是chatendpoint.cpp.
我们希望发送收到的文字给所有的订阅者. 这个可以使用publication/subscription(Pub/Sub)方法.
首先, 接受方需要订阅某个”主题(tipic)”. 当某人发送信息给那个”主题(tipic)”时, 那条消息将会发送给所有的订阅者.
这种行为的代码看起来像这样:
#define TOPIC_NAME "foo"
ChatEndpoint::ChatEndpoint(const ChatEndpoint &) : ApplicationEndpoint()
{ }
bool ChatEndpoint::onOpen(const TSession &)
{
subscribe(TOPIC_NAME); // 开始订阅
publish(TOPIC_NAME, QString(" [ New person joined ]\n"));
return true;
}
void ChatEndpoint::onClose(int)
{
unsubscribe(TOPIC_NAME); // 停止订阅
publish(TOPIC_NAME, QString(" [ A person left ]\n"));
}
void ChatEndpoint::onTextReceived(const QString &text)
{
publish(TOPIC_NAME, text); // 发送信息
}
void ChatEndpoint::onBinaryReceived(const QByteArray &)
{ }
构建
此例中, 我将不使用VIEW, 所以我将会从构建中删掉它. 编辑chatapp.pro如下并保存它.
TEMPLATE = subdirs
CONFIG += ordered
SUBDIRS = helpers models controllers
构建命令:
$ qmake -r
$ make (nmake on Windows)
$ treefrog -d
(停止命令)
$ treefrog- k stop
让我们启动浏览器并访问http://(host):8800/index.html.
它正常工作了吗?
我们现在将我们刚才实现的发布, 所以这个结果应该看起来像这样http://chatsample.treefrogframework.org/
下面的这些功能已经添加到上面的例子中:
- 数据库中最近的30条信息.
- 在连接(onOpen)后信息立即被发送
- 增加了一些样式表使应用看起来漂亮一些
保持在线(keep alive)
当TCP会话长时间持续在无通讯状态, 通信设备例如路由器, 将会停止路由. 这种情况下, 即使你从服务器发送一个消息, 它再也到达不了客户端了.
要避免这种情况, 需要通过定期通信来保持在线(keep alive). 保持在线(keep alive)在WebSocket中是通过发送和接收乒乓Ping/Pong*数据帧实现的.
在Treefrog中, 它是通过endpoint类的keepAliveInterval()的返回值设置的. 时间单位是秒.
int keepAliveInterval() const { return 300; }
如果值为0, 保持在线功能将不会工作.默认值是0(不工作).
通过保存在线, 你不仅可以检查是否通信线路是通畅的, 还可以检查主机软件是否已经关闭. 然而, Treefrog目前(2015/6)还没有实现检测当前是否可用的API.
参考
tspawn HELP
$ tspawn -h
usage: tspawn <subcommand> [args]
Type 'tspawn --show-drivers' to show all the available database drivers for Qt.
Type 'tspawn --show-driver-path' to show the path of database drivers for Qt.
Type 'tspawn --show-tables' to show all tables to user in the setting of 'dev'.
Type 'tspawn --show-collections' to show all collections in the MongoDB.
Available subcommands:
new (n) <application-name>
scaffold (s) <table-name> [model-name]
controller (c) <controller-name> action [action ...]
model (m) <table-name> [model-name]
usermodel (u) <table-name> [username password [model-name]]
sqlobject (o) <table-name> [model-name]
mongoscaffold (ms) <model-name>
mongomodel (mm) <model-name>
websocket (w) <endpoint-name>
validator (v) <name>
mailer (l) <mailer-name> action [action ...]
delete (d) <table-name or validator-name>