RTMFP(Real Time Media Flow Protocol)协议使用一个服务端点达成客户端之间的P2P连接。所有的媒体信息直接在客户端之间进行传输并不需要通过服务器进行中转,提供高扩展的部署依赖。由于CPU计算和内存限制,单独服务器提供的能力只能满足一定数额的客户端进行媒体信息的交流,负载会变得越来越大。
为了解决这个问题,MonaServer提供一个完整的框架满足多个Server实例间的通信交流,框架可以检测服务的连接与关闭,管理服务之间的数据交换,管理客户端重定向的负载均衡,提供用户信息同步rendez-vous服务和RTMFP组服务选项,服务器之间的所有通信都采用原始的TCP协议方式进行。主要思路很简单,默认情况下,每个实例都是独立的服务,相互之间不共享任何信息,由你来决定在服务之间共享资源的内容。
我们通过一些简单代码的例子和内容来描述这个框架的每一个功能。而API(增加页面链接)页列出了所有的功能,但没有代码例子及上下文内容。下面脚本代码只说明了如何使用它,至于如何创建一个应用服务器还请请参阅服务器应用程序页面。
Configuration 配置
为了确保多个MonaServer服务之间的正常通信,我们必须配置每个服务实例,下面列出的三个参数即为多服务模式:
- host 配置客户端直接访问的公共地址
- servers.port 配置接收传入的服务器连接的端口号
- servers.targets 配置远程MonaServer实例尝试连接的地址
如图所示:
说明:服务器之间的数据通过未加密的TCP进行传输,为了避免内部端口B的攻击,B应该通过防火墙的配置,保护servers.port端口仅仅允许指定服务的连接。
如下的脚本应包含在根目录下的main.lua,便于加载时启动。
A以B为目标,在初始化的时候连接B。
-- Server application on A side
function onServerConnection(server)
if server.isTarget then
NOTE("Target gotten : ", server.address, " (", server.host, " for clients)")
-- displays "Target gotten : 192.168.0.2 (www.hostB.com for clients)"
end
end
B配置1936的接收端口,接受A的连接,B以A为发起者。
-- Server application on B side
function onServerConnection(server)
NOTE(server.isTarget) -- displays "false"
end
注意:如果A和B互相以对方为目标,那么就会创建2个TCP连接,造成服务器信息交换混乱。如图:
配置系统支持在不重启服务器的情况下水平调整存在系统功能,实际上,第一个系统在启动的时配置接收服务端口(services.port),没有target。当新的服务启动的时会自动扩展把第一个系统的地址放到自己的services.targets中。当然也支持多个系统实例的配置,参见配置(增加连接)
;MonaServer.ini
host = www.myhost.com:1935
[servers]
targets = 192.168.0.2:1936?type=master;192.168.0.3:1936
function onServerConnection(server)
if server.type=="master" then -- true here just for 192.168.0.2:1936 server
NOTE("Master server connected")
end
end
function onServerDisconnection(server)
if server.type=="master" then -- true here just for 192.168.0.2:1936 server
NOTE("Master server disconnected")
end
end
说明:加载同一个应用(www/myGame)的服务A与服务B可以进行同步,但只能在连接客户端的时候加载,就是说如果编译更改服务A的www/myGame/main.lua,当有新的客户端连接A的时候,main.lua就会重新构建,并会尝试重构服务B上的文件(当然,如果B上的文件也发生了变化,重构就是有效的)。相反的,如果编译更改服务B,而客户端总是立即连接A的话,那就必须重新编译服务A以便于获得客户端对B文件的更新。
可以在onServerConnection 事件中加上拒绝某个连接的错误信息:
function onServerConnection(server)
-- Reject all connections not comming from localhost
if server.address is not "127.0.0.1" then
error(server.address, " is trying to connect to the server => rejected")
end
end
Exchange data and resources 交换数据和资源
服务之间的数据交换,需要在发送端调用server:send方法,在接收端定义接收函数。
function onServerConnection(server)
-- RPC function declaration, to receive data from one other server
function server:onHello(name)
self.name = name
end
-- send my name to the incoming server (it will receive it on its "onHello" method)
server:send("onHello","MonaServer A")
end
-- now you can find the name of each server everywhere
for index,server in mona.servers:ipairs() do
NOTE("Server '"..server.name.."' at address "..server.address)
end
说明:onHello函数中执行的self.name=name 语句会在server对象上创建name属性,name属性是和其它的Server Application共享的对象属性,因此很容易被覆盖,可以加上当前Server的前缀用以区分。
交换机制的主要目的是为了在服务实例之间共享资源。举个例子说明,如果使用Mona传输strean流给许多订阅者(非P2P方式),通常就会有很少部分的发布者及大量的订阅者,服务支持发布者加载,但是会随着监听者的增多达到饱和,很好的解决办法之一就是水平调整每个服务的作用来共享订阅者。如图:
如下是三个服务的配置例子,可动态增加许多服务实例,负载均衡以DNS的方式进行管理,但所有的服务实例之间必须共享publications对象,否则存在某个订阅者就会找不到自己的publication现象,看下面的代码:
-- following server (horizontal scaling)
_nextServer = nil
-- number of subscribers (listeners) for this server
_subscribers = 0
function onConnection(client,...)
INFO("Connection of a new client on ", mona.configs["host"])
function client:onPublish(publication)
-- informs the following server about this publication
if _nextServer then _nextServer:send("publish", publication.name) end
function publication:onVideo(time, packet)
if not _nextServer then return end
-- forward the video packet to the following server
_nextServer:send("video", publication.name, time, packet)
end
function publication:onAudio(time, packet)
if not _nextServer then return end
-- forward the audio packet to the following server
_nextServer:send("audio", publication.name, time, packet)
end
function publication:onData(name, packet)
INFO("onData : ", name, " - ", packet)
if not _nextServer then return end
-- forward the data packet to the following server
_nextServer:send("data", publication.name, name, packet)
end
end
function client:onUnpublish(publication)
-- informs the following server about this unpublication
if _nextServer then _nextServer:send("unpublish",publication.name) end
end
function client:onSubscribe(listener)
-- if a following server exist, and if this server has more than 400 subscribers
-- redirect the client to the following server:
-- I send an error with the redirection server address in its description
INFO("Subscription of client ", client.address, " (_subscribers=", _subscribers, ")")
if _nextServer and _subscribers>=400 then error(_nextServer.host) end
_subscribers = _subscribers + 1
end
function client:onUnsubscribe(listener)
_subscribers = _subscribers - 1
end
end
function onServerConnection(server)
if server.isTarget then
-- incoming server is a following server!
if _nextServer then error("following server already connected") end
_nextServer = server
-- informs the following server about my publications
for id,publication in pairs(mona.publications) do
_nextServer:send("publish",publication.name)
end
else
-- incoming server is a previous server, we have to create RPC function to receive
-- its publication informations
server.publications = {}
function server:publish(name)
-- publication creation
self.publications[name] = mona:publish(name)
end
function server:unpublish(name)
-- publication suppression
local publication = self.publications[name]
if publication then publication:close() end
self.publications[name] = nil
end
function server:video(name, time, packet)
local publication = self.publications[name]
-- give the video packet to our publication copy
if publication then publication:pushVideo(packet, time) end
end
function server:audio(name, time, packet)
local publication = self.publications[name]
-- give the audio packet to our publication copy
if publication then publication:pushAudio(packet, time) end
end
function server:data(name, dataname, packet)
local publication = self.publications[name]
-- give the data packet to our publication copy
if publication then publication:pushData(packet) end
end
end
end
function onServerDisconnection(server)
if server.isTarget then
-- disconnected server was a following server!
_nextServer = nil
return
end
-- disconnected server was a previous server, close its publications
for id,publication in pairs(server.publications) do
publication:close()
end
end
代码段:
f _nextServer and _subscribers>=400 then error(_nextServer.host) end
需要指定的客户端代码才能起作用,将新用户重新定向到新服务器。
function onStatusEvent(event:NetStatusEvent):void {
switch(event.info.code) {
case "NetStream.Play.Failed":
var error:Array = event.info.description.split(" ");
if (error.length > 0) {
var host:String = "rtmfp://" + error[error.length-1];
_netConnection.close();
_netConnection.connect(host);
}
break;
}
}
Load balancing and rendezvous service 负载均衡和rendezvous服务
通常,负载均衡的解决办法是通过硬件DNS方式返回动态的IP地址方式,这里,我们以软件的方式使用函数onHandshake(address,path,properties,attempts) 事件解决。
-- index incremented to redirect client equally to each server
index=0
function onHandshake(address,path,properties,attempts)
index=index+1
if index > mona.servers.count then index=1 end -- not exceed the number of server available
return mona.servers(index) -- load-balacing system!
end
服务不接受任何客户端的连接,它通过握手的方式重定向客户端,与硬件解决方案相比没有实际好处。对于并发的rtmfp连接请求返回多个可用的服务地址会有好处。
function onHandshake(address,path,properties,attempts)
return mona.servers
end
实际上,客户端会收到多个服务器地址,上面的情况,RTMFP会并发启动多个连接,仅仅保持连接最快的一个,这就是另一种的负载均衡,优胜劣汰。
关于P2P的Mona rendezvous 服务,在多个服务存在的环境下,假如连接MonaServerA的PeerA请求连接MonaServerB的PeerB,A是不可能获取B的任何信息的,我们需要使用 onRendezVousUnknown(protocol, peerId) 事件。
function onRendezVousUnknown(protocol, peerId)
return mona.servers -- redirect to all the connected servers
end
使用以上的代码,当连接其他服务器失败的时候,我们就可以发出重定向请求。但是在组信息同步的时候就不能用此办法了,假如ServerA上有GroupA包含PeerA,同样的GroupA可以存在ServerB上包含PeerB,但是PeerA与PeerB都不知道对方的存在,那我们就需使用groups:join方法同步信息。
思路很简单,就是在每个服务器之间共享每个group包含的信息。
function onRendezVousUnknown(protocol, peerId)
return mona.servers -- redirect to all the connected servers
end
function onConnection(client)
function client:onJoinGroup(group)
-- inform other servers of this joining operation
mona.servers:broadcast("join",group.rawId,client.rawId)
end
function client:onUnjoinGroup(group)
-- inform other servers of this unjoining operation
mona.servers:broadcast("unjoin",group.rawId,client.rawId)
end
end
function onServerConnection(server)
-- inform this new incoming server of my group/client relations existing
for id,group in pairs(mona.groups) do
for i,client in ipairs(mona.groups) do
server:send("join",group.rawId,client.rawId)
end
end
server.groups = {}
-- RPC server functions to receive joining/unjoining operation
function server:join(groupId,clientId)
-- creation of a virtual member for this group
local member = mona:joinGroup(clientId,groupId)
if not member then return end -- join operation has failed
-- We have to attach this member object to its server
-- to avoid its destruction by the LUA garbage collector
local group = self.groups[groupId]
if not group then group = {size=0}; self.groups[groupId] = group end
group.size = group.size + 1
group[clientId] = member
end
function server:unjoin(groupId,clientId)
-- suppression of a possible virtual member of group
if not group then return end
local member = group[clientId]
if member then
member:release() -- detach of its group
group[clientId] = nil
group.size = group.size - 1
end
-- erase the group object if it's empty now
if group.size==0 then self.groups[groupId]=nil end
end
end
function onServerDisconnection(server)
-- suppression of possible virtual members attached to this server
for id,group in pairs(server.groups) do
for id,member in pairs(group) do
if id ~= "size" then member:release() end
end
end
end
转载请注明出处:http://blog.youkuaiyun.com/sotower/article/details/48494791
欢迎留言讨论交流,一起成长。