文章目录
  1. 1. 概述
  2. 2. 为什么要有CM
  3. 3. BOSH实现长连接
    1. 3.1. BOSH概述
    2. 3.2. 原理
    3. 3.3.
    4. 3.4. 传输信息的格式
    5. 3.5. strophe.js
      1. 3.5.1. 结构图:
      2. 3.5.2. 例子
      3. 3.5.3. 查看源码
    6. 3.6. converse.js
  4. 4. XMPP
    1. 4.1. JID
    2. 4.2. 架构
    3. 4.3. 绑定到TCP的持久化连接
    4. 4.4. XML流
  5. 5. Websocket
  6. 6. 参考文献

概述

first.png

上图是WebChat的连接示意图,图中的CM是指连接管理器(Connection Manager),服务器端与CM之间是XMPP协议的连接,CM与客户端之间是Http协议的连接。

为什么要有CM

因为WebChat要部署在浏览器中,浏览器与服务器之间的通信我们最常用的就是Http的请求和响应,浏览器不支持XMPP协议的连接,JS又不能操作TCP建立一个持久连接,所以采用了一个折衷的方案,就是在浏览器与客户端之间建立一个CM服务器,它的作用就是用Http协议模拟与客户端之间的双向的持久化连接,与服务器之间直接用XMPP协议通信,XMPP的规范化组织叫这技术为BOSH。我先介绍前端最关注的BOSH技术,引出它们的实现strophe.jsconverse.js,再介绍一下服务器与CM之间的通信协议XMPP,主要包括怎样找到消息的接收者,怎样传输消息等。

BOSH实现长连接

BOSH概述

BOSH是一种传输协议,它可以利用HTTP协议模拟客户端与服务器之间的双向流传输,它比普通的轮询技术节省了系统开销与网络资源,它主要用于Jabber/XMPP客户端-服务器之间的数据传输,然而BOSH并非为XMPP定制,它也可以用于别的传输。

原理

大部分BOSH技术的实现都是在服务器与客户端之间架设一个连接管理器(CM)来处理HTTP连接。

CM有以下两个作用:

  1. 模拟CM与客户端之间的双向通信。
  2. 将消息体用包裹起来。
cm-http.png

客户端与CM之间的通信遵守如下规则:

  1. 当客户端发送一个request给CM,CM不会立刻返回一个response给客户端,相反它会保持这个request的open状态,直到CM从服务器接收到一个新的数据它才会将数据response给客户端。收到response后,客户端会立即发送一个新的request给CM,保持客户端与CM总是处于连接状态。
  2. 当在一定时间之后(通常为几秒钟),CM如果始终没有新的数据response给客户端,CM会发送一个空的response给客户端,这样做的目的是检测CM与客户端是否处于连接状态,因为诸如防火墙之类的程序会断开很久保持静默的连接。
  3. 在HTTP1.1协议中默认开启了持久化连接(就是Connection: keep-alive),CM与客户端一个request和response之后仍然保持连接状态,等待客户端发起发起下一个请求,这样做减小了套接字创建的开销。
  4. 当客户端有数据要发送给服务器的时候,客户端先发送请求给CM,这时CM会先立即响应先前的request,发送response给客户端,也就是说它会先立即处理完已经存在的连接,再腾出手来处理另一个套接字发来的请求。这样CM会遵守规则1,收到request后不立即返回response给客户端,等到有真正的数据从服务器传来的时候CM再response给客户端。

连接示意图如下图所示:

具体作用.png

如上图所示,当浏览器发送一个request给CM后,CM不立即返回response,当浏览器又发送一个request后,CM先立即返回上一个response,当CM有消息要发送给浏览器时,再response给浏览器,浏览器收到一个response之后,又发送一个新的request给CM,保持连接状态。

body.png

每一个request或者response的消息体(body)都会被一个命名空间为httpbind<body>元素包裹起来。具体的作用参考:xep-0206

传输信息的格式

信息传递的格式都是XML,之所以用XML是因为XMPP本身就是一种XML流技术。这里有三种传输信息的格式,分别代表不同的用途:

  1. <message>
message.png

聊天信息的发送和接收是通过message节来实现。例如xxg1@host发送一条信息”你好“给xxg2@host,xxg1@host客户端会将下面的这段XML发送到XMPP服务器,服务器再推送给xxg2@host客户端。其中<message>的from属性是发送者,to属性是接收者,<body>子元素的内容就是聊天信息。

关于type的种类:

  • normal:单个消息,回应可能会或者可能不会很快到来。
  • chat:代表这类消息是私聊,两个用户间的对话。
  • groupchat:代表这类消息是多用户聊天室中的消息。
  • headline:发送的警告或通告,并不期望有回应

每个用户都需要一个地址,以便服务器可以在网络上定位我们,就相当于IP地址,称为JID。

  1. <iq>

iq即info/Query,发送的iq节消息必须总是收到一个回复,其采用“请求-响应”机制,相当于一个Ajax请求。下面的例子是客户端通过<iq>请求获取联系人,XMPP服务器将结果返回:

客户端请求获取联系人:
getMessage.png
服务器返回结果:
getMessageResponse.png

可用于表明用户的状态,例如用户状态改变成“Do not disturb”(“请勿打扰”),会向服务器发送:
userStatus.png

strophe.js

strophe.js是一个XMPP javascript库,作用

  1. 操作XML节点
  2. 管理连接
    API Document:strophe-js

结构图:

结构图.png
  • Builder用于操作XML节点,Handler相当于我们常用的event.js,管理回调方法;Connection是最重要的类,用于管理向XMPP服务器发送的连接。
  • 在strophe.js中持久化的连接有两种实现方法,一种是Bosh一种是websocket,request用于新建xmlHttprequest请求。

例子

以下是一个利用strophe.js实现Web私聊的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// XMPP服务器BOSH地址
var BOSH_SERVICE = 'http://host:5280';

// XMPP连接
var connection = null;

// 当前状态是否连接
var connected = false;

// 当前登录的JID
var jid = "";

// 连接状态改变的事件
function onConnect(status) {
console.log(status)
//Strophe.Status查询连接状态
if (status == Strophe.Status.CONNFAIL) {
alert("连接失败!");
} else if (status == Strophe.Status.AUTHFAIL) {
alert("登录失败!");
} else if (status == Strophe.Status.DISCONNECTED) {
alert("连接断开!");
connected = false;
} else if (status == Strophe.Status.CONNECTED) {
alert("连接成功,可以开始聊天了!");
connected = true;

// 当接收到<message>节点,调用onMessage回调函数
connection.addHandler(onMessage, null, 'message', null, null, null);

}
}

// 接收到<message>
function onMessage(msg) {

// 解析出<message>的from、type属性,以及body子元素
var from = msg.getAttribute('from');
var type = msg.getAttribute('type');
var elems = msg.getElementsByTagName('body');

if (type == "chat" && elems.length > 0) {
var body = elems[0];
$("#msg").append(from + ":<br>" + Strophe.getText(body) + "<br>");//Strophe.getText得到body中的text节点
}
return true;
}

$(document).ready(function() {

// 通过BOSH连接XMPP服务器
$('#btn-login').click(function() {
if(!connected) {
connection = new Strophe.Connection(BOSH_SERVICE);
connection.connect($("#input-jid").val(), $("#input-pwd").val(), onConnect);
jid = $("#input-jid").val();
}
});

// 发送消息
$("#btn-send").click(function() {
if(connected) {
if($("#input-contacts").val() == '') {
alert("请输入联系人!");
return;
}

// 创建一个<message>元素并发送
//c方法是增加一个子节点给当前的元素
var msg = $msg({
to: $("#input-contacts").val(),
from: jid,
type: 'chat'
}).c("body", null, $("#input-msg").val());
connection.send(msg.tree());

$("#msg").append(jid + ":<br>" + $("#input-msg").val() + "<br>");
$("#input-msg").val('');
} else {
alert("请先登录!");
}
});
});

查看源码

  • strophe.builder 1649行
  • 建立Connection 2171行
  • 建立连接 4302行
  • 发送请求 4747行

converse.js

converse.js是一个开源的XMPP聊天客户端,它使用strophe.js作为它实现XMPP通信功能的库。

XMPP

JID

XMPP是一种基于XML流技术的通信协议,像Email地址一样,XMPP实体也有一个网络中唯一的地址,用user@domain/resource表示,例如WebChat中我的地址就是iyis2569@ejabhost2/web-58531850,User为用户名,Domain表示服务器地址,WebChat可以在不同的地方同时登录,resource就可以区分同一账号在多处登陆,QQ、MSN都不能实现这种功能。

架构

架构.png

在实践中,XMPP是一个包含了很多互相通信的客户端和服务器的网络,因此如图客户端1与客户端2可以通过服务器1与服务器2交互信息,这种分布式的网络很常见,例如Email网络。
在XMPP端到端的通信中逻辑上是点到点,物理则是客户端到服务器再到服务器再到客户端。

客户端与服务器、服务器与服务器连接的流程如下:

  1. 确定要连接的服务器的IP地址和端口号,也就是域名解析,类DNS解析
  2. 打开一个TCP连接
  3. 通过TCP打开一个XML流
  4. 传输XML节点
  5. 关闭XML流
  6. 关闭TCP连接

绑定到TCP的持久化连接

XMPP协议的重点就是将一个JID的消息发送到另一个JID去,这很像Email协议,但XMPP更强调实时性。之所以XMPP能做到即时通信就是因为它是一种持久化的连接。每个点对点都建立了基于TCP长连接的持久XML流来保持可通讯状态,这样可以双方进行实时的信息交互。

XML流

定义:XML流是一个容器,用于任何两个实体间通过网络进行XML元素的交换。XML流以<stream>标签开始,以</stream>标签结束。在XML流处于活动状态期间,可以通过这个流发送不限数量的XML元素。
XML节:
打开流:

上面说的打开一个TCP连接后,客户端向服务器发送一段XML流:

1
2
3
4
5
6
7
8
<?xml version='1.0'?>
<stream:stream
from='juliet@im.example.com'
to='im.example.com'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>

这时服务器会发送一段stream来回应客户端

1
2
3
4
5
6
7
8
9
<?xml version='1.0'?>
<stream:stream
from='im.example.com'
id='++TR84Sm6A3hnt3Q065SnAbbk3Y='
to='juliet@im.example.com'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>

这时就打开了一段流,服务器与客户端就可以交互XML节了。

Websocket

参考文献

1、BOSH:http://xmpp.org/extensions/xep-0124.html

2、XMPP OVER BOSH:http://xmpp.org/extensions/xep-0206.html

3、百度百科

4、strophe.js:http://strophe.im/strophejs/doc/1.2.3/files/strophe-js.html#Strophe.Connection.Strophe.Connection

5、converse.js:https://conversejs.org/

6、xmpp博客:http://www.jianshu.com/p/a94749385755

7、xmpp Core:http://xmpp.org/rfcs/rfc6120.html

文章目录
  1. 1. 概述
  2. 2. 为什么要有CM
  3. 3. BOSH实现长连接
    1. 3.1. BOSH概述
    2. 3.2. 原理
    3. 3.3.
    4. 3.4. 传输信息的格式
    5. 3.5. strophe.js
      1. 3.5.1. 结构图:
      2. 3.5.2. 例子
      3. 3.5.3. 查看源码
    6. 3.6. converse.js
  4. 4. XMPP
    1. 4.1. JID
    2. 4.2. 架构
    3. 4.3. 绑定到TCP的持久化连接
    4. 4.4. XML流
  5. 5. Websocket
  6. 6. 参考文献