Libjingle学习
综合网上资料和个人理解,主要分为以下几大部分: 1. libjingle概念 2. libjingle 知识点
3. libjingle 编译(源代码的编译和一个例子myjingle的编译) 4. libjingle 程序创建(一个简要的过程) 5. libjingle 知识点扩展(一些重要概念) 6. libjingle底层的主要代码理解
文档还未完成,每一部分都有待进一步学习和研究后再修改。
Libjingle概念:
Libjingle是一个开发库,由c++的源码和文档组成,这些文档可以使你设计应用程序,通过一个网络,能够连接和交换数据。值得注意的是这个源码需要一些外部的依赖,例如: 声音的聊天例子依赖于Linphone或者GIPS,这取决于你自己的平台。这个源码包含网络和代理服务器的协商层,XML的分析层,一个STUN的服务器,所有的源码需要发起一个连接和交换数据在两台电脑之间。通过使用ICE(Internet Communication Engine)的机制,STUN服务器,以及交换UDP或者TCP的数据包,这些连接的源码能够强行穿越NAT(网络地址转换)和防火墙的装置。你可以使用提供的源码,或者扩展它,以适合你自己特定的需要,根据Berkeley-style的许可。
Jingle和libjingle的区别:
Libjingle大概和jingle XMPP 扩展在同一时间被建立。Libjingle的团队建立了他们自己的协议去处理回话协商,后来和使用标准化的jingle(基于XMPP的标准)一起工作。尽管,jingle和libjingle是非常相似的,但是它们是不一样的,而且不能共同使用。现在libjingle的源码版本依然使用原始的网络协议,跟以前的稍微有些不同,而且无法兼容jingle的规范。不过它还是足够的接近jingle,所以学习jingle的说明书是值得的。类似的“接近但不是一样”,libjingle的视频内容描述(早期的jingle的视频内容描述格式XEP-0167),ICE的传输描述(早期的jingle的ICE传输XEP-0176),以及流的UDP描述(早期的jingle流UDP的传输描述XEP-0177).
学习libjingle的条件:
首先得熟悉XMPP协议,普通的网络概念,和C++。除此以外,它能帮助熟悉jingle提出的扩展(XEP-0166),和其他的相关的扩展。
Libjingle 知识点:
1.Login handler是所有东西的缘起. 登陆服务器之后生成一个Login handler.
2. XmppPump Libjingle把XmppPump作为Login handler. XmppPump,注意Pump,顾名思义就是一整个系统的动力源.由这个pump源源不断地把Message送给等待接受这些Message
的地方.
3.XmppClient是在XmppPump的控制下,可以认为它是XmppPump的一部分.他负责从网上接受和发送Xmpp格式的消息.
4.XmppEngine是协助XmppClient工作的.可能是为它提供一些Utility API.diagram上面的三个圆环套圆环其实是包含关系.
5.stanza的原本的意思是英文诗里面的一段.在这里的意思是一个XMPP消息,有两种stanza,一种是正常的Message,一种是做negotiating用的.
6.task 是XMPP任务对象,由XmppPump管理,以Task结尾的类都是线程类。Task订阅了自己感兴趣的消息.来了消息就Pump到对应的Task里面去了.
7.PresencePushTask.首先解释一下Presence,这个单词是指这个账号目前的状态,是在线,还是下线还是离开.PresencePushTask主要是为了接收在线好友状态的线程.
8.PresenceOutTask是起把自己的状态提交出去的作用,里面有一个变量叫stanza_.它是用来存储将要发送的消息的指针的.Send函数实际上是把要发送的值赋上.
9.JingleInfoTask是专门管STUN服务器和中继传输服务器的.
10.SessionClient是沟通application和p2p模块的中间桥梁。比如,再共享文件的例子中。SessionClient就被派生成FileShareSessionClient。要传输的文件列表之类的东西都是由他控制.
11. Session 当一个新会话请求接收或发送连接请求,将会创建一个新的Session对象,Session用来监视着来自对方的请求,并调用Session::Accept或Session::Reject来接受或拒绝请求,Session会把stanza的到来事件提醒SessionClient,Session类型被定义在stanza的一个Unique ID中;每个Session都管理着多个Transport 对象。
12. SessionManager管理着Session,如果是进入的连接,SessionClient直接把这个连接给SessionManager,由SessionManager调用SessionClient::OnSessionCreate来创建一个新的Session;如果是出去的连接,session wrapper显示创建一个Session.在文件共享例子中这个session wrapper就是FileShareSession
13.Resource Allocator/Resource Classes是控制文件传输或者语音传输等等资源的。他们需要从Session中获得TransportChannel进行事实上的数据传输。
14. Channel,有两种channel,一种是negotiation channel,一种是transport channel。
15.P2PTransport里面持有多个P2PTransportChannel。这些都是候选Channel。按照ICT,
这些Channel事所有源IP和目的IP的组合。P2P传输是从这一堆Channel里面选出一个最好的。
16.PortAllocator事专门响应P2PTransport的请求生成所需要的端口的。
17.Thread,一种是signaling thread,也就是主管XMPP消息处理的主线程。另外一种是work thread,也叫channel thread。主要用来进行P2P数据传输的。libjingle都用talk_base::Thread来包含一个Thread;Thread中的Start()和Run(),Start是新起一个线程,马上开始运行,Run是在本身这个线程里面开始消息循环,是被阻塞的。
18.ThreadManager 管理着Thread,每个新建的Thread都要加进ThreadManager(在Thread构造中调用ThreadManager::add())
19.MessageQueue 要发消息的Thread要继承MessageQueue;
20.MessageQueueManage 管理着MassageQueue。
21.MessageHandler要收消息的对象要实现MessageHandler的OnMessage(),如果有消息传入,接受消息的对象要调用OnMessage()
22.connection一个connection对象有一个Port类,Port类是由PortAllocator分配的。
23.候选IP地址 有三种(1)本机IP地址。(2)NAT外面看到的地址和端口。(3)中继服务器的IP。
24.JingleInfoTask和google的STUN服务器联系主要接收Jingle相关的一些消息,比如外网地址阿,中继服务器地址阿之类的
25.buzz是XMPP相关的namespace.
26.cricket是P2P相关的namespace.
27.scoped_ptr是一个智能指针(smart pointer).里面的reset是重新给这个指针负值,并且把原来指向的内存释放掉.
28.Tls Lbjingle采用了Tls来记录线程信息.放在Tls里面的是Thread对象.这个对象记录了该线程的信息.
29.stanza被SessionManagerTask发送到SessionManager中.SessionManager又调用对应的Session来处理这些stanza.如果在Sesssion发现传进来的stanza是用来商议建立p2p连接的.就把这个stanza传送给P2PTransportChannel.其他的stanza就传送给订阅了这种stanza的Task,也就是其他消息处理线程.
Libjingle的编译
一、libjingle源码的编译
1. 首先下载
libjingle
的源代码,在如下地址:
https://sourceforge.net/projects/libjingle/,我下载的是libjingle-0[1].4.0版本 2. 然后安装Visual C++ 2005 Express Edition,具体下载地址,google一把,非常多的链接。
3. 安装好Visual C++ 2005 Express Edition后,需要安装platform SDK,在如下
地
址
中
下
载
:
http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdk-full.htm,按照提示说明安装platform SDK。
4. 在sourceforge.net上下载Expat XML Parser ,地址如下:http://sourceforge.net/project/downloading.php?group_id=10127&use_mirror=nchc&filename=expat_win32bin_2_0_0.exe&33064126,我下载的是2.0.0版本,安装完成后,需要到Visual C++ 2005 Express Edition版本中更新选项,Tools-》Options-》Projects and Solutions-》VC++ directories
Library files: C:\\Expat-2.0.0\\StaticLibs Include files: C:\\Expat-2.0.0\\Source\\Lib
Include files: C:\\Program Files\\Microsoft SDK\\include 5. 编译工程,仍旧有以下提示:
------ Build started: Project: libjingle, Configuration: Debug Win32 ------ Compiling...
gipslitemediaengine.cc
d:\\p2p\\xmpp\\libjingle\\libjingle-0[1].4.0\\libjingle-0.4.0\\talk\\session\\phone\\gipslitemediaengine.h(33) : fatal error C1083: Cannot open include file: ''talk/third_party/gips/Interface/GipsVoiceEngineLite.h'': No such file or directory
channelmanager.cc
d:\\p2p\\xmpp\\libjingle\\libjingle-0[1].4.0\\libjingle-0.4.0\\talk\\session\\phone\\gipslitemediaengine.h(33) : fatal error C1083: Cannot open include file: ''talk/third_party/gips/Interface/GipsVoiceEngineLite.h'': No such file or directory
winfirewall.cc
d:\\p2p\\xmpp\\libjingle\\libjingle-0[1].4.0\\libjingle-0.4.0\\talk\\base\\winfirewall.cc(29) : fatal error C1083: Cannot open include file: ''netfw.h'': No such file or directory
Generating Code... Build log
was
saved
at
\ldLog.htm\
libjingle - 3 error(s), 0 warning(s)
6. 前面两个编译错误,可以通过下载GipsVoiceEngineLite包完成,但是现在GipsVoiceEngineLite是需要许可才能下载,由于项目中暂时不需要语音功能,所以在项目
中去掉了session\\phone子项目,前面两个编译错误消失。
7. 在http://www.codeproject.com/KB/winsdk/WinXPSP2Firewall.aspx中下载WinXPSP2Firewall_demo文件包,将icftypes.h和netfw.h拷贝到\\talk\\base目录下,在base工程中加入icftypes.h文件。
8. 重新编译,libjingle - 0 error(s), 0 warning(s)
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========大功告成,libjingle编译成功。
二、myjingle的编译
1.MyJingle简介
MyJingle只是一个LibJingle的Win/MFC的DEMO.
简单来说,MyJingle利用LibJingle和一个叫Speex的音频编码,组成一个P2P的语音通讯软件.当然他还依赖XMPP作为服务端进行一些连接通讯.详细介绍可以上官方网站.
2.准备工作
安装VC 8(在VS2005里面).
安装Microsoft DirectX SDK (February 2006)
安装Microsoft Platform SDK for Windows Server 2003 R2 3.设置,编译
用VC打开MyJingle.sln,需要先设置两个lib和inclue 工具-选项-项目和解决方案, 选择\包含文件\添加
E:\\Microsoft DirectX SDK (February 2006)\\Include
E:\\Microsoft Platform SDK for Windows Server 2003 R2\\Include 选择\库文件\添加
E:\\Microsoft DirectX SDK (February 2006)\\Lib\\x86
E:\\Microsoft Platform SDK for Windows Server 2003 R2\\Lib
好了,现在可以编译通过了.
但是会提示LIBCMT库冲突.
然后在MyJingle项目右键-属性-配置属性-链接器-输入,\忽略特定库\那里输入LIBCMT
4.修改
MyJingle只能连上Gmail,其他XMPP的服务端连不上,需要修改一下代码.
修改 saslplainmechanism.h文件
virtual XmlElement * StartSaslAuth() { std::string sUser;
int i = user_jid_.Str().find(\ sUser = user_jid_.Str().substr(0,i);
// send initial request
XmlElement * el = new XmlElement(QN_SASL_AUTH, true); el->AddAttr(QN_MECHANISM, \
FormatXmppPassword credential; credential.Append(\
//credential.Append(user_jid_.Str()); credential.Append(sUser); credential.Append(\ credential.Append(&password_);
el->AddText(Base64EncodeFromArray(credential.GetData(), credential.GetLength())); return el; }
Libjingle应用程序的创建
一.登录服务器
http://code.google.com/apis/talk/libjingle/login.html
1.初始化 SSL ,如果需要的话。
2. 建信号线程。最简单的方案是创建一个 AutoThread 对象,它将获取当前操作系统 的线程并且在ThreadManager设置成当前线程。
3.从用户中获取登录信息。 使用XmppClientSettings将信息登进XMPP服务器,比如用户 名,密码和服务器地址。
4.创建一个XMPP 任务管理对象(task manager object).libjingle使用XmppPump,一个 辅助类,包装了XmppClient和处理登录进XMPP服务器,并且处理所有的XMPP任务(tasks ),例如发送和接收状态(presence)通知和登录进服务器。
4. 连接接收登录进度通知。连接上XmppPump对象的SignalStateChange信号。这个信号 将发送关于登录的进度的通知。当它发送STATE_OPEN状态时,你已经是登录进服务器了。
5. 登进服务器。 调用XmppPump::DoLogin来登录。例子代码会将Google Talk服务器设 置成XMPP服务器。你可以在buzz::XmppClientSettings更改其值,而buzz::XmppClientSettings将会传给 XmppPump::DoLogin. Dologin将要求一个运行中的信号线程;
Dologin在你调用信号线程Run之前会队列等待。
6. 调用信号线程的Run,以保证消息线程开始循环监听。或者,如果你有自己的方法令 程序长久地循环,你可以调用 Start 代替。
7. 监听成功或失败的消息。 如前所述,XmppPump 将发送 STATE_OPEN 当应用程序成功 登录。
8. 你将可以请求 STUN和 relay port信息,将它传给 PortAllocator对象。以便进行 下一环节的操作(Sending and Querying Presence)
一旦你已经登录,你将可以发送你的状态给服务器,并且为花名册注册状态通知,下一 节将细说。
二,发送和查询状态
http://code.google.com/apis/talk/libjingle/querying_presence.html
在登进XMPP服务器后,你的应用程序应该提交你的状态给服务器。并且请求好友状态信 息(buddy list)。服务器将回复这些信息给你(away,offline,and so on),例如当前 每个已经登录好友的JID。你可以使用这些JID和状态信息来发送连接请求。服务器将 持续发送状态通知直至你的连接结束。
libjingle 提供两个辅助的XmppTak对象来发送和请求状态: PresenceOutTask —发送你的状态给服务器。
PresencePushTask — 监听从服务器发来的状态信息。 你将调用PresencePu
shTalk::Start来启动它。你必须连接上这个类的 SignalStatusUpdate 信号,使它能 在收到一个状态流时发出警报(指被通知的意思). 将发送给好友中每个收到状态的人。
为了运行上面这几个对象(类似其它任务), 你必须实例化他们,并且传递父对象的名字 进去(一个基于Task的对象–比如 XmppClient),填满它所需要的信息,使用它们的Start 方法来启动他们。
(重要: Task对象必须用Start这个方法来启动。虽然Task对象有其它额外的方法是你 想运行的(例如,Send),但这些方法只是特别的动作,Task只能在Start调用后才真正 工作。
下面信息描述了怎样发送和接收状态信息(presence):
1. 登录以取得状态信息通知。 实例化并启动 PresencePushTask ,连接上
OnStatusUpdate 信号。有些服务器在收到状态流(presence stanza)时会返回花名称状态 通知。 避免丢失最初的通知,你必须在发送状态流时创建一个监听器(listener).
2. 用你的状态信息填满状态对象。这个Status对象描述普通的状态信息,比如: status, show, visible,和其它信息。
3. 发送你的状态到服务器。实例化并启动PresenceOutTask,用来发送你的状态 到服务器。服务器在收到你的状态信息后将会返回所有的好友成员的状态通知。
在发送和接收完状态流后,你可以处理会话管理,用于发送和接收会话请求。
三. 设置会话管理
http://code.google.com/apis/talk/libjingle/sending_stanzas.html
一旦你登录进服务器,并且发送和接收了关于花名册成员的状态信息,你应该需要 设置会话管理管道。这是会话管理集合,逻辑组件,用来监视进来的连接请求和响应 出去的连接请求。这是libjingle应用程序中最高可定制的部分,因为不同的应用程序 对于它们的发送或接收连接都会有不同的需求。例如,你是否需要创建或读取一系列文 件来共享呢? 你是否需要实例化一个额外的媒体引擎呢? 你是否需要生成一系列编码 来供给会话的提供者呢?
您需要发送什么样的连接请求,或响应什么样的连接请求,取决于您的应用程序。 最基本的步骤都会在(How libjingle applications work)里描述。但所有的应用程序 都需要实例化下面核心的libjingle对象:
1.实例化您的NetWorkManager, PorAllocator 子类,SessionManager对象。
以上必须在您想初始化或接收会话请求之前完成。例子代码中,直到成功登录服务器才 创建这些对象。这是为了避免登录失败从而引发的不必要的创建行为。然而,您可以 在更早的时候创建他们,只要您想。
2.创建一个新的线程对象来作为工作线程[可选]. 如果您的应用程序主持着单独 的工作线程,您可以使用talk_base::Thread对象来创建,并把它传给SessionManager 的构造函数。 否则,您创建的SessionManager中的线程将会被默认当成工作线程。 文件共享的例子程序使用单线程(创建来主持FileShareClient管理对象,创建于SessionManager),
声音例子程序创建了一个专注的线程,在CallClient::InitPhone中。确认调用Start, 不是Run, 在工作线程里,因为Run是阻塞的调用。
3.注册您的SessionClient子类到SessionManager. SessionManager 调用
SessionClient::OnSessionCreate 来创建一个新的会话(Session).(在外发和进入的请求 中将发送这个通知;信号中的一个参数指定了请求的管理). SessionManager 掌握着一个 ID/SessionClient的映射,这个ID是当前发送与接收代码中的唯一标识值。它将包含在 XMPP流中给各计算机,而SessionManager将在进入的流中找到它.
4.启动SessionManager监听并发送XMPP信息。SessionManagerTask启动SessionManager 的发送和接收XMPP消息功能。SessionManagerTask 是一个介于XMPP消息组件和P2P组件 的中间人。 发送的这些消息包含了会话请求,应答会话请求和候选人列表。
5.重置STUN和转播(relay)端口信息。libjingle默认使用Google的STUN和
中继服务器, 但你可以动态地设置这些信息,只要在HttpPortAllocator里设置这些值 就行了。 你可以动态地从JingleInfoTask对象中要求这些信息。
四.发出和接收连接
http://code.google.com/apis/talk/libjingle/make_receive_connections.html
一旦你已经登录服务器并广播了你的状态,还从XMPP里收到了返回的状态通知,您已经
可以建立连接(或者应答进来的连接请求)。某人发送一个连接请求给您是很重要的, 您可以获取一个异步的连接请求通知,但您的计算机将立即开始协商连接而没有等待你 的回应。取决于libjingle的代码运用,数据交换可能立即开始了。例如,在文件传输 例子中,发起者开始发送一个图像文件时,连接已经被协商好了。(接收者也许还没有 接受这个连接请求),虽然文件并没有开始传送,在对方明确表示接受连接时。同样地 ,语音例子程序中,声音字节会与连接被建立时同步发送。
发送连接请求:
1.发送连接请求。 发送一个连接请求意味着创建一个新的Session对
象,创建会话特别信息的描述(编码,文件名等等),并发送他们到其它的计算机上, 然后 Session::SignalState 将监视这些信息的请求。 具体连接会话(session)的类型 可以参考(声音例子代码和文件传输例子代码)。 Under the covers, Session::Initiate 用于输入接收者的JID,SessionDescription子类描述会话的细节(文件发送或请求,编 码可用性等等)
2.应答进入的连接请求。 当一个新会话请求接收,将会自动创建一
个新的Session对象。并且SessionClient::OnSessionCreate 将会被调用,将会有个 标识说明这是进入还是出去的请求。 对于进入的请求,您可以通知用户是接受还是拒 绝这个请求。 用户必须调用Session::Accept或Session::Reject来接受或拒绝请求。 详细的说明还是看两个例子代码吧。
3. 发送和接收数据。数据是通过TransportChannel对象来发送或接 收的,但它是怎样管理不同类型的会话的呢? 对于语音呼叫,MediaEngine调用SendPacket , 而TransportChannel调用ReadPacket来发送和接收数据。如果您自己的应用程序使用 StreamInterface来发送本地数据(比如MemoryStream),您则必须自己实现读和写的方法。 查看StreamInterface的说明来了解更多的事实。
Libjingle知识点扩展:
一、Signals:
libjingle 使用sigslot库 促进对象间的通信。sigslot是一种framework,它可以把呼叫方(calling member)和任意类实现的接收函数很容易地关联起来,工作方式就像这样:
1、 发出呼叫的类声明一个数据成员(被称作信号),声明方式使用一种很像模板的语法。这个信号数据成员定义了和接收函数一致的参数。(注:这个接收函数当然是属于某个类了)
2、 类中的接收函数在实现时,它的参数必须与它关联的信号的参数相同,这里的参数相同是指数量相同,类型相同和次序相同。这个接收函数有时被称作receiver或slot(注意:接收函数可以与信号数据成员同属一个类)。接收函数不能有返回值(可以是void)。它必须继承自sigslot::has_slots<>。
3、 通过呼叫信号数据成员的connect函数,使信号数据成员与接收函数关联起来,呼叫时传递两个参数:一个是接收函数所在类的对象指针,另一个是类中的接收函数的地址。
4、 呼叫方使用信号成员就像是调用它自己的函数一样,传递给与信号成员声明时一致的参数就可以了。如果调用信号成员成功,则所有与此信号成员关联的任意类中的接收函数都会被调用。
我们可以把任意数量的信号成员与一个接收函数关系起来。libjingle有时就是把多个信号成员与一个接收函数关联起来,达到统一处理消息之目的。相反,一些类对象声明一个信号对象,是为了从一个“信号点”广播消息(“信号点”语意上讲就是一个信号成员对象,此对象关联了众多的接收函数,当此信号成员被调用时,这些接收函数都能接收到消息,即这些接收函数都被调用)。当对象(包括信号成员所在对象和传递给connect函数的接收函数所属类对象)被销毁时,sigslot库会小心处理取消关联和引用关系。
下面的代码示范了sigslot库的使用方法: // Class that sends the notification. class Sender {
// The signal declaration.
// The '2' in the name indicates the number of parameters. Parameter //types
// are declared in the template parameter list.
sigslot::signal2
// When anyone calls Panic(), we will send the SignalDanger signal.
void Panic(){
SignalDanger(\ }
// Listening class. It must inherit sigslot.
class Receiver : public sigslot::has_slots<>{
// Receiver registers to get SignalDanger signals.
// When SignalDanger is sent, it is caught by OnDanger().
// Second parameter gives address of the listener function class definition.
// First parameter points to instance of this class to receive notifications.
Receiver(Sender sender){
sender->SignalDanger.connect(this, &Receiver.OnDanger); }v
// When anyone calls Panic(), Receiver::OnDanger gets the message.
// Notice that the number and type of parameters match
// those in Sender::SignalDanger, and that it doesn't return a value.
void Receiver::OnDanger(string message, std::time_t time){
if(message == \{
// Call the police ... } } ...
}
Sender 类声明了一个信号数据成员:
sigslot::signal2
语句中的“
从Sender类的成员数void Panic()实现中可以看到,使用信号成员的形式就像是在调用一个与信号成员同名的函数SignalDanger(\,参数类型就是声明信号成员时指定的参数。
Receiver类继承自sigslot::has_slots<>,它的成员函数就具有了成为“接收函数”的“潜质”。
从Receiver的构造函数可以看出,当Receiver对象创建时,必须向它指定一个信号类(即声明了信号成员的类)对象作为构建造函数的参数,当然此信号类必须有Receiver定义的操作用到的信号成员的样式。
一旦Receiver类对象被创建,Sender类中的信号成员就与Receiver类中的OnDanger()函数关联起来了,只要Sender对象的Panic()被调用,Receiver类对象的OnDanger()就被调用,即接收到来自Sender对象的消息,从而进行处理。 如:
Sender sender;
Receiver receiver(sender);
如果 运行: sender.Panic(); 则
receiver.OnDanger();
被自动调用,在此函数的内部就可以处理来自sender的消息。 实现了信号类与接收类之间的松偶合性。
libjingle库中的一些类,发送信号给接收函数(即listeners 监听者,可理解为某个类的接收函数),用来传递一些重要事件。比如:当你发出或收到一个链接尝试时,Call::SignalSessionState就会发出通知信号。在应用程序中应该有接收函数与这些信号关联起来,并且做出适当的行为。
按照libjingle中的约定,在声明信号数据成员时,名字被冠以“Signal”字符,比如:SignalStateChange,SignalSessionState,SignalSessionCreate。
与这些信号关联的函数名被冠以“On”,比如:OnPortDestropyed(),OnOutgoingMessage(),OnSendPacket(); 关于 sigslot库的更多内容,请查看sigslot文档。
二、Thread
libjingle 考虑到使用到此库的应用程序的性能,libjingle内部支持多线程。其内组件使用1或2个全局线程:
● signaling thread 被用作创建底层(基础)组件,
例如:Session Management,Control,XMPP Messaging组件。
● worker thread ( 有时称作channel thread)用来集中处理p2p组件中的对象提交过来的大量资源,例如:数据流。之所以这样用另外的线程单独处理,是为了避免数据流阻塞或被XMPP/用户界面组件阻塞。使用 worker thread的类包括ChannelManage,SocketMonitor,P2PTransportChannel 和 属于Port类的对象。
若起用worker thread,使之工作,在应用中必须创建一个Thread类对象,并把此对象当作SessionManager的构造函数的参数。(如果SessionManager类对象在创建时,没有传递给它Thread对象,则SessionManager类将在内部创建一个线程,当作worker thread)。CallClient::InitPhone示范了如何为底层组件(low-level components)创建一个worker thread方法。
另外、libjingle提供了一个基类SignalThread。扩展此类可以让一个扩展类对象存在于它自身代表的线程,此扩展类对象可以被实例化,启动,单独离开,结束时自释放。更多信息请查看signalthread.h/cc。
注意:尽管libjingle支持多线程,但是只有几个函数通过呼叫方线程的验证来支持线程安全,并且极少函数做了线程锁定。下面的片断示范了在函数中如何安全地呼叫线程(或线程安全地被呼叫):
// Check that being called from the channel (e.g., worker) thread. ASSERT(talk_base::Thread::Current() == channel_thread_); channel_thread_->Clear(this);
libjingle中用到的所有线程,signaling thread,worker thread,其它的一些线程,都是talk_base::Thread的对象(或子类的对象)。所有的Thread对象都被ThreadManager管理,当被请求时,ThreadManager会返回这些Thread对象。SessionManager被创建时通过调用ThreadManager::CurrentThread得到一个signal thread(当无worker thread 传递给SessionManager构造函数时,同时得到一个work thread)。XmppPump类把当前线程当作它的signal thread来用(XmppPump uses the current thread for its signaling thread)。所以,应用程序必须为signal thread创建一个Thread 对象(或其子类对象),并在SessionManager对象创建之前或在XmppPump工作之前,把此对象放进ThreadManager的线程池里。(Signing In to a Server(登录服务器) 有示例)有两种方法创建一个Thread对象:
AutoThread 这种方式就是libjingle用Thread对象包装一个操作系统中的线程,并把它当作ThreadManager线程池里的当前线程(当然,Thread::CurrentThread()被调用时,此线程会被提取出来)。
Thread 这种方式将创建一个新线程并用Thread类包装,比较典型就是的创建worker thread。使此线程发生作用,应用程序必须新创建一个Thread对象,调用ThreadManager::Add()或ThreadManager::SetCurrent()把它丢进线程池里,并且调用Run()使之在阻塞状态下运行或调用Start()使之处于监听状态。
线程为对象间或对象内部的消息沟通提供了“管道”()。例如:SocketManager可以通过其它线程向自己发送销毁一个套接字的消息,或当链接候选被产生时向SessionManager发送消息。Thread继承自MessageQueue,所以Thread的对象具有了Send,Post,和一些同步或异步发送消息的函数。如果要使一个对象能够接收到MessageQueue送出的消息,那么此对象必须继承和实现MessageHandler。MessageHandler定义了一个OnMessage函数,此函数在MessageQueue送出消息时被调用,用来接收MessageQueue送出的消息。
你可以通过任何线程向继承自talk_base::MessageHandler的任何对象发送消息。尽管能够做到,如果你发出的消息是为了集中处理大量的数据,应用程序应该通过worker thread。调用SessionManager::worker_thread()可以得到worker thread的句柄。
调用Session::Manager::signaling_thread()可以得到 signaling thrread的句柄。
对象使用一个指定的线程有如下几种方式:
对象要求一个线程指针作输入参数,并储存这个指针。
对象在创建时取得当前线程(构造函数中调用ThreadManager::CurrentThread()
取得),把取得的线程存进对象内部成员变量引用它,一般应用于获取特定的线程。(it can assume that the current thread when it is created (accessed by ThreadManager::CurrentThread in its constructor) is a particular thread and cache a member pointer to it)
因为一个对象可以被任意线程使用,对象可能需要验证当前调用是来自哪个线程的方法。应用可以调用Thread::Current()得到当前线程的句柄,然后与对象内部保存线程的数据成员进行比较,此数据成员的值可以是从SessionManager中暴露在外面的线程,或是对象在创建时通过构造函数传进去的初始化值。
这是一个对象通过其它线程调用自身函数时而广范使用的范例: // Note that worker_thread_ is not initialized until someone // calls PseudoTcpChannel::Connect
// Also note that this method *is* thread-safe.
bool PseudoTcpChannel::Connect(const std::string& channel_name) { ASSERT(signal_thread_->IsCurrent()); CritScope lock(&cs_); if (channel_) return false;
ASSERT(session_ != NULL);
worker_thread_ = session_->session_manager()->worker_thread(); ... }
void PseudoTcpChannel::SomeFunction(){ ...
// Post a message to yourself over the worker thread. worker_thread_->Post(this, MSG_PING); // <- Goes in here.... ...
对象调用SessionManger::signal_thread() 或 SessionManager::worker_threa以上三种方法,libjingle均有用到。
d()获取线程。
}
// Handle queued requests.
void PseudoTcpChannel::OnMessage(Message *pmsg) { if (pmsg->message_id == MSG_SORT) OnSort();
else if (pmsg->message_id == MSG_PING) // -> And comes out here! // Check that we're in the worker thread before proceding. ASSERT(worker_thread_->IsCurrent()); OnPing();
else if (pmsg->message_id == MSG_ALLOCATE) OnAllocate(); else
assert(false); }
三、Naming Conventions(命名约定)
libjingle有一些命名约定,比较有用;
OnSomeMethod 凡是以“On”开头的函数,大多和一个信号成员关联起来了,不是在本身对象内就是在其它对象内实现了这种关联。如果此函数被所在对象调用,大有可能是在不同的线程内调用的(即对象通过另一个线程调用自身的函数)。
SomeMethod_w 在worker thread中(“worker thread”,加了引号),以“_w”结尾的函数,是被其它线程调用的。
SignalSomeName 是向回调函数(callbakc methods,即接收函数)发送消息的信号成员。
四、SSL
libjingle 支持两种类型的SSL: ● OpenSSL (for UNIX) ● SChannel (for Windows)
使用SSL,应用程序必须执行如下步骤:
1、#define FEATURE_ENABLE_SSL (如果编译器是Visual Studio,这个设置是在工程设置中定义的,而不是在代码中)。
2、确定SSL_USE_OPENSSL 或SSL_USE_SCHANNEL 在ssladapter.cc中被定义,具体是哪一个视应用将来运行的操作系统而定。
3、调用InitializeSSL初始化一些必须的组件。这个函数在ssladapter.cc中定义。当应用程序退出时,应该调用 CleanupSSL。应用程序不必调用InitializeSSLThread(在InitializeSSL中己经被调用过了)。
五、Connections(链接)
一个p2p的链接实际上由两个通道组成。
● session negotiation channel(也称作signaling channel),会话协商通道。是为数据链接服务的沟通通道。这个通道被用来回应取得一个链接的请求,交换候选,和协商会话的细节(比如:套接字地址,需要的编码方案,交换的文件,链接改变请求,终止请求)。这个通道是两个计算机之间建立的第一个链接,也只有这个链接成功之后,两个计算机之间的数据链接才能被建立。libjingle通过发送一个指定的前导协议节发出一次响铃并收到一个回应,数据链接则被建立(see Jingle and libjingle)。这个通道发送协议节是通过XMPP 服务器这一中间机构进行的,例子中的代码是把Google Talk服务器当作中间机构用的。
● data channel (娄据通道,数据链接)这个通道传送的是p2p两端真正交换的数据(语音,视频,文件等),数据通道里的数据被TCP或UDP包封装,到底是TCP还是UDP这要视协商的传送方式,这些包并没有经过XMPP服务器。
会话协商通道首先被建立,它作为计算机间协商建立数据通道细节的通道。数据通道被成功建立之后,在这个通道上将发生许多数据活动,除非碰到改变编码请求,新文件请求,重传请求,或终止请求。
下面的图演示了这两种数据路径。尽管只有一个路径处于活动态,图中还列出了两个路径的交替使用态。因为路径可以是直接链接(92%的链接尝试都可以转换成直联)或服务器中转(8%的链接尝试需要中间服务器的中转)。第三种数据路径没有列出,它是没有防火墙的网络中从一台计算机直接链接另一台计算机。
1、libjingle不时地发送出心跳包(STUN),来维持一个链接可写入,保持防火墙和NAT地址绑定处于活动态,并且还可用来检查潜在的链接。
2、linjingle向链接端口分配用户名和密码。此举用来确定当前链接的数据通道就是在会话协商通道上协商好的数据通道。因为用户名和密码是被XMPP发出的,也许没有经过TLS的加密,心跳包中的用户名和密码只是身份的标识,并没有加密验证。
运行 file share 例子程序,可以看到发出的真实协议节。
六、Transports, Channels, and Connections
每个p2pTransportChannel代表了本地机与远程机的数据通道。这个通道实际上包含着(隐藏着)一个设计复杂而健壮的体系。P2PTransportChannel管理着大量的不同的Connection对象,每个对象代表了一个不同种类的链接(UDP,TCP等)。一个Connection对象实际上封装了一对对象:一个Port子类,代表了本地链接,和一个代表远程机的地址。如果一个链接无效,P2pTransportChannel会立即切换到下个最佳的链接上(即从候选链接中选一个最佳的)。
下面的图是数据路径在p2p组件内部的高层抽象。
当libjingle和远程机协商一个链接时,libjingle就会在本地创建一个链表,用来储存所有潜在链接点,这潜在的链接点称作“candidates”(候选),本地的每个候选被一个Port对象封装,此Port对象被PortAllocator子类分配。本地的Port对象是在发起方在发送链接请求之前被创建,或接收方收到一个链接请求后被创建(如果Port对象链表己经被生成,不论这个链表是怎么创建的,只要有就行)。当P2PTransportChannel收到另外一个计算机的链接请求,它就会创建一个Connection对象来封装每个“远程候选/本地Port对象”对。
libjingle还定义了一个RawTransport类,此类支持UDP两端的直接链接(在不使用ICE的情况下)。这样的传输方式(链接方式)也许在能够创建UDP直连条件下或UDP的任何一方不支持ICE机制下被使用。
P2PTtransportChannel 创建和管理多个Connection对象。P2PtransportChannel根据可写入性和优先级(比如:UDP的优级先比中转链接的高)来选择最佳的Connection对象使用。
当链接异常中断或性能低下时,上述情况就会随之发生,P2PTransportChannel立刻会切换到下一个Connection对象上,并且这种切换对上层是透明的。
P2PTransport(图中未显示)是p2p数据体系上的抽象度较高的创建者和管理者。它创建和管理P2PTransportChannel,并且监视它的性能,但是实际上P2PTransport不处理数据;数据的真正进入点是P2PTransportChannel。VoiceChannel和PseudoTcpChannel都关联到了P2PTransportChannel并进行数据的读取和写入。
Session对象掌管着P2PTransport对象,所以可以要求P2PTransport创建数据通道。尽管Session对象有掌管多个Transport和Transport子类实例的潜力,但是当前版本的代码只定义和使用了一个P2PTransport子类的实例。
七、Candidates(候选)
libjingle一个主要的好处就是它可以穿透防火墙和NAT设备进行链接协商。libjingle使用ICE机制穿透防火墙。libjingle应用程序第一步要做的就是在试图协商一个链接时为其它计算机的链入创建一个潜在的本地端口地址链表。链表中的每个潜在的地址就称作一个候选。候选就是“IP地址:端口”对应对,这些对应对使应用程序和其它计算机互联(技术上,这些对应对只在本地机上监听)。libjingle提供了强壮的机制在本地链接上发现候选供其它计算机进入,甚至穿透防火墙或NAT设备。
为了向其它计算机提供尽可能多的候选链接地址,libjingle生成三种本地候选: ● Local IP addresses 一种候选是计算机上的本地IP地址。与它共同在一个网络中的其它计算机能够通过这个候选进入。
● Global addresses第二种候选是两个计算机之间的NAT或防火墙设备的对外地址。如果这个候选是NAT设备对外地址,libjingle使用心跳包使NAT端口与本地机绑定起来并对外公布这个全局地址。这个全局地址被作为从NAT对外地址链接进入的候选。
(A second candidate is an external address on a NAT or firewall device between the two computers. If this
is outside a NAT device, libjingle uses STUN to cause the NAT to bind to your computer and expose a global address.
This address is used as a candidate to connect from outside the NAT device.)
● Relay server addresses 大约有8%的链接尝试在穿越防火墙时,上述方法失败。第三种方法就是在两个防火墙之间进行服务器中转。尽管libjingle有能力使用中转服务器,但是没有提供中转服务器的URI。libjingle包含中转服务的代码(relayserver.h)。应用程序可以自己创建和运行这个服务,使用方法是以中转服务器的IP地址做为BasicPortAllocator的构造函数的第三个参数。
下面的图演示了两台计算机之间生成的 local addrress 候选(C1),external NAT 候选(C2),Relay server候选(C3)。
libjingle以链表的形式储存着全部的候选,这样可以做到在链接建立后,libjingle能够在当前链接迟缓或中断后很快地切换到新的链接上。
libjingle包支持多种传输方式,实现了Jingle的
八、Data Packet(数据包)
计算机间的P2P数据被多重协议封装,就像下图演示的,具体被什么协议封装依懒于实现的应用程序。并不是每个应用程序都能用到所有的协议,例如:文件传输应用使用了pseudo-tcp协议,但是语音聊天应用却没有使用。
九、异步socket(asyncsocket)
异步发送数据以后不等对方确认,函数就退出了 同步则要等数据发完以后才能退出,运行下面的代码
以接收一条消息来说明这个问题。首先,程序向系统投递一个接收数据的请求,并为其指定一个数据缓冲区和回调函数,回调函数用来指示当数据到达后将如何处理,然后我们的程序继续执行下去,当有数据到达的时候,系统将数据读入缓冲区,并执行回调函数,处理这条消息。我们并不需要关心这条消息何时到达。
二、什么情况下我们用异步Socket:
有些人认为,异步Socket的性能比同步Socket的性能高很多,应该在各种环境下都用异步Socket,其实不然。在某些环境下面。异步反到比同步的性能低,那么在哪些情况下会这样呢?
1、 客户端Socket。
2、 服务端连接数比较少。
3、 连接数很多,但都是短连接。
在这些环境下,我们用同步Socket不但可以简化代码,而且性能并不会比异步Socket低。但在服务端连接比较多而且是长连接的情况下,我们就要使用异步Socket。
现在我们来看看如何用异步Socket编程。
首先,我们要建立一个Socket用来监听:
Socket _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint localEP = new IPEndPoint(_address, _port); _listener.Bind(localEP); _listener.Listen(100);
然后创建一个线程来处理客户端连接请求:
Thread _acceptWorkThread = new Thread(AcceptWorkThread); _acceptWorkThread.Start();
private void AcceptWorkThread() {
while (_isListener)
{
UserInfo info = new UserInfo();//这个UserInfo是用来保存客户信息的。 info.socket = socket;
Socket socket = _listener.Accept(); //这里进行其它处理。
socket.BeginReceive(info.Buffer,
0,
info.Buffer.Length,
SocketFlags.None,
ReceiveCallBack, info);//这里向系统投递一个接收信息的请求,并为其指定ReceiveCallBack做为回调函数 }
} 我们再来看看回调函数的定义:
private void ReceiveCallBack(IAsyncResult ar) {
UserInfo info = (UserInfo)ar.AsyncState; Socket handler = info.socket; int readCount = 0; try
{
readCount = handler.EndReceive(ar);//调用这个函数来结束本次接收并返回接收到的数据长度。 }
catch (SocketException)//出现Socket异常就关闭连接 {
CloseSocket(info);//这个函数用来关闭客户端连接
return; }
catch { }
if (readCount > 0) {
byte[] buffer = new byte[readCount];
Buffer.BlockCopy(info.Buffer, 0, buffer, 0, readCount); Analyzer(info, buffer);//这个函数用来处理接收到的信息。 try {
handler.BeginReceive(info.Buffer, 0, info.Buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallBack), info);//向系统投递下一个接收请求 }
catch (SocketException) //出现Socket异常就关闭连接 {
CloseSocket(info); } catch {
} }
else //如果接收到0字节的数据说明客户端关闭了Socket,那我们也要关闭Socket {
CloseSocket(info); }
}
接下来我们看看如何发送数据给客户端:
public void Send(Socket socket, byte message) { try
{
info.socket.BeginSend(message, 个回调函数。 }
catch (SocketException ex) {
CloseSocket(info); }
catch
0,
_byte.Length,
SocketFlags.None,
new
AsyncCallback(SendCallBack), info);//这里向系统投递一个发送数据的请求,并指定一
{
} }
定义发送回调函数:
private void SendCallBack(IAsyncResult ar) {
UserInfo info = (UserInfo)ar.AsyncState; try {
info.socket.EndSend(ar);//调用这个函数来结束本次发送。 } catch { } }
好了,整个监听、接收、发送的过程就完成了,很简单吧。现在需要说明的是,我在这里接收客户端连接的Accept是用的同步的,我个人认为在这里用同步的会比用异步好一些。因为这样代码简单而且没有性能上的损失。
Libjinlge 底层主要代码分析
一、socket(socket.h)
1.主要给出socket接口,该接口插槽实现各种网络,符合windows和unix标准。 2.socket接口中的主要函数: (大部分能通过函数名知道功能)
virtual SocketAddress GetLocalAddress() const = 0; virtual SocketAddress GetRemoteAddress() const = 0; virtual int Bind(const SocketAddress& addr) = 0; virtual int Connect(const SocketAddress& addr) = 0; virtual int Send(const void *pv, size_t cb) = 0;
virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr) = 0; virtual int Recv(void *pv, size_t cb) = 0;
virtual int RecvFrom(void *pv, size_t cb, SocketAddress *paddr) = 0; virtual int Listen(int backlog) = 0;
virtual Socket *Accept(SocketAddress *paddr) = 0; virtual int Close() = 0;
virtual int GetError() const = 0;
virtual void SetError(int error) = 0;
inline bool IsBlocking() const { return IsBlockingError(GetError()); }
enum ConnState { CS_CLOSED, CS_CONNECTING, CS_CONNECTED };
virtual ConnState GetState() const = 0;
二、asyncsocket(asyncsocket.h)
1.asyncsocket接口继承自socket和sigslot::has_slots<>,主要给出异步socket接口所要发出和接收的信号。
2.asyncsocket接口中的信号
sigslot::signal1
3. asyncsocketadapter 继承自asyncsocket,异步socket适配器主要用来绑定信号到事件,并指定一个回调函数来处理事件
4.asyncsocketadapter中的主要函数 绑定:
socket->SignalConnectEvent.connect(this, &AsyncSocketAdapter::OnConnectEvent); socket->SignalReadEvent.connect(this, &AsyncSocketAdapter::OnReadEvent); socket->SignalWriteEvent.connect(this, &AsyncSocketAdapter::OnWriteEvent);
socket->SignalCloseEvent.connect(this, &AsyncSocketAdapter::OnCloseEvent); 指定函数:
virtual void OnConnectEvent(AsyncSocket * socket) { SignalConnectEvent(this); } virtual void OnReadEvent(AsyncSocket * socket) { SignalReadEvent(this); } virtual void OnWriteEvent(AsyncSocket * socket) { SignalWriteEvent(this); }
virtual void OnCloseEvent(AsyncSocket * socket, int err) { SignalCloseEvent(this, err); }
三、asyncpacksocket(asyncpacksocket.h和asyncpacksocket.cpp)
1.封装数据包,实现数据包的不同步接收。实例化时要传入一个asyncskcket指针 2.主要函数:
要用到的套接字方法:
virtual SocketAddress GetLocalAddress() const; virtual SocketAddress GetRemoteAddress() const; virtual int Bind(const SocketAddress& addr); virtual int Connect(const SocketAddress& addr); virtual int Send(const void *pv, size_t cb);
virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr); virtual int Close();
virtual int SetOption(Socket::Option opt, int value);
virtual int GetError() const; virtual void SetError(int error); 发出的数据包是用来读取的: sigslot::signal4 char*, size_t, const SocketAddress&, AsyncPacketSocket*> 四、asynctcpsocket和asyncudpsocket(.h 和.cpp) 1.这两个类继承自asyncpacksocket,分别实现了TCP数据包和UDP数据包的封装及发送 2. TCP包的主要函数。(对于TCP与UDP连接的不同之处可以上网查资料) virtual int Send(const void *pv, size_t cb); SenDTo函数中调用Send函数 virtual int SendTo(const void *pv, size_t cb, const SocketAddress& addr); 五、physicalsocketserver(physicalsocket.h和physicalsocket.cpp) physicalsocketserver包含了有关socket操作的真正实现的类: 1. 2.在physicalsocke继承自asyncsocket,是套接字服务器,提供了真正的接口插槽底层操作的实现。 未完待续 talk_base::PhysicalSocketServer ss; talk_base::Thread main_thread(ss); talk_base::ThreadManager::SetCurrent(&main_thread); CLoginMessageHandler handler(pump.client()); pump.DoLogin(xcs, new XmppSocket(true), 0); main_thread.Run(); talk_base::Thread main_thread(NULL); talk_base::ThreadManager::SetCurrent(&main_thread); CLoginMessageHandler handler(pump.client()); pump.DoLogin(xcs, new XmppSocket(true), 0); main_thread.Run();