視頻在線直播系統:www.hixiu.com;在線聊天系統demo:www.liaofuwu.com |
|
核心系統框架
視頻直播核心系統架構主要包括Web端架構、聊天系統架構、視頻直播、用戶狀態同步架構等。
Web端框架
由Nginx組成的前端負載集羣,後端由IIS、FPM服務器進行解析。前端將由Nginx集羣處理已靜態化頁面及向後端提交未靜態或不做靜態化要求的請求,後端Cached爲應用緩存,主要減少對數據庫無意義請求造成的壓力,數據庫架構由一主一備組成(目前暫無備庫)。
聊天系統框架
聊天系統分爲聊天室(ChatRoom)、消息同步中心(CastServer)、用戶列表(UserList Server)、系統廣播(SystemCast Server)及監控報表(Report)組成。同房間不同服務器之間用戶的消息系統廣播、及用戶列表則通過消息同步中心進行傳遞。用戶連接房間之前由調度中心分配聊天室服務器進行連接。該架構特點是同時在線人數易擴展、可實現負載均衡。
用戶狀態同步機制
用戶狀態(Session)的同步機制之所以列爲核心,是考慮到用戶消費、充值等行爲與非用戶行爲造成用戶屬性變更能及時反饋給用戶,此舉能大大提高產品的用戶體驗。該機制主要實現不同框架、不同服務器、不同站點(域名)之間用戶狀態的同步。同步之主要工作由SessionComponents組件完成。
事件 |
觸發 |
用戶端反應 |
是否重發 |
用戶充值 |
系統 |
用戶成功充值後由系統發起系統廣播,通知在線用戶充值成功,並無需刷新帳號金額自動同步。 |
否 |
運營發放庫存及運營幣 |
系統 |
無論用戶呆在哪個房間,都能收到該系統消息(目前如果用戶不在線,不能成功接收) |
否 |
… |
… |
… |
… |
服務器規劃
用途 |
參考配置 |
數量 |
說明 |
聊天 |
CPU:4核,內存8G |
3-6 |
含:聊天室、用戶列表、系統廣播(注:服務器要求雙線機房) |
Web |
CPU:4核,內存:8G |
2-4臺 |
站點、充值、接口、資源、活動等服務器 |
前端 |
CPU:4核,內存:8G |
1-2 |
Nginx服務器 |
分步式緩存 |
CPU:4核,內存:8G |
2 |
Memcached服務器(應用緩存與用戶Session狀態) |
數據庫 |
CPU:8核,內存:16G |
2 |
數據庫服務器,一主一備 |
流媒體 |
CPU:8核,內存:16G |
1-N |
Rtmp服務器 |
視頻系統
視頻系統主要包含客戶端推流、服務端處理、客戶端接收流三大部份。視頻流暢效果取決於推流端與接收端兩端帶寬,任何一方網絡帶寬問題都會將降低觀看者效果,其中推流端由爲重要,其將影響所有人觀看。
視頻插件
高清插件由Flash調用的ActiveX控件,只支持以IE爲內核的瀏覽器調用,適用於主播端。高清插件主要功能有兩個,1、加水印;2、進行高清編碼。
插件代碼片斷
void RtmpLiveScreen::Run()
{
//連接rtmp server,完成握手等協議
librtmp_->Open(rtmp_url_.c_str());
//發送metadata包¹
SendMetadataPacket();
//開始捕獲音視頻
ds_capture_->StartAudio();
ds_capture_->StartVideo();
while (true)
{
if(SimpleThread::IsStop()) break;
// 從隊列中取出音頻或視頻數據
std::deque<RtmpDataBuffer>rtmp_datas;
{
base::AutoLock alock(queue_lock_);
if(false == process_buf_queue_.empty())
{
rtmp_datas = process_buf_queue_;
process_buf_queue_.clear();
}
}
// 加上時戳發送給rtmp server
for (std::deque<RtmpDataBuffer>::iterator it =rtmp_datas.begin();
it!= rtmp_datas.end(); ++it)
{
RtmpDataBuffer& rtmp_data = *it;
if (rtmp_data.data != NULL)
{
if (rtmp_data.type ==FLV_TAG_TYPE_AUDIO)
SendAudioDataPacket(rtmp_data.data, rtmp_data.timestamp);
else
SendVideoDataPacket(rtmp_data.data, rtmp_data.timestamp,rtmp_data.is_keyframe);
}
}
Sleep(1);
}
ds_capture_->StopVideo();
ds_capture_->StopAudio();
librtmp_->Close();
}
流媒體服務
視頻流完全可以不經過流媒體服務,由於各家CDN所覆蓋區域偏重點不一致,故系統採用流媒體服務實現一對多的推流是爲了更好的適應不同區域客戶端的網絡情況。如下圖所示:
CDN
接口說明 |
接口地址 |
說明 |
推流地址 |
http://up.v.XXX.cn/live/ROOMID |
live爲發佈點名稱,與CDN商量協定,ROOMID爲房間ID |
拉流地址 |
http://down.v.XXX.cn/live/ROOMID |
同上 |
|
|
|
聊天系統
聊天服務由底層採用Socket中的完成端口方式實現,該系統單臺服務器支持10000以上的連接數,雖然所有Socket連接方式的客戶端都被接受,但在服務端有身份驗證機制防止惡意連接。目前實現對接的客戶端有Flash、C++。服務端與客戶端通訊由固定協議(包頭+包體)完成,包體大都採用JSON進行序列化及反序列化。
通訊協議
每條完整的消息由包頭和包體組成(當然包體可以爲空,也就是消息僅含包頭的情況),包頭共20字節,由5組int類型數據組成,分別代表指令ID、房間ID、用戶ID、時間及描述包體長度。
Header/Body |
Header |
Body |
|||||||||||||||||||
字節 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
JSON |
說明 |
COMMAND |
ROOMID |
USERID |
TIME |
BODYLENGTH |
||||||||||||||||
數值 |
10001 |
10092 |
4697995 |
1706 |
256 |
||||||||||||||||
類型 |
Int |
Int |
Int |
Int |
Int |
String |
指令說明
協議 |
指令 |
指令JSON格式(示例) |
指令描述 |
10001 |
COMMAND_SYSTEM_SYN_MESSAGE |
|
同步系統消息 |
10002 |
COMMAND_SYSTEM_SYN_USERMESSAGE |
|
同步用戶消息 |
10003 |
COMMAND_SYSTEM_MESSAGE |
|
系統公告 |
10004 |
COMMAND_SYSTEM_SYN_USEREXITSTATUS |
|
用戶退出消息 |
10005 |
COMMAND_SYSTEM_SYN_USERJOINSTATUS |
|
用戶進入房間消息 |
10006 |
COMMAND_SYSTEM_USERINFOCHANGE |
|
用戶餘額變更 |
10007 |
COMMAND_SYSTEM_SYN_ROOMINFOREPORTMESSAGE |
|
同步統計房間用戶信息 |
10008 |
COMMAND_SYSTEM_CASTROOMMESSAGE |
|
廣播 |
10009 |
COMMAND_SYSTEM_WORLDCASTROOMMESSAGE |
|
世界廣播 |
10010 |
COMMAND_SYSTEM_CAST_SUNNY |
|
陽光普照 |
20001 |
COMMAND_USER_GIFTS |
|
用戶送禮 |
20002 |
COMMAND_USER_MESSAGE |
|
用戶聊天消息 |
20003 |
COMMAND_USER_MANAGER_MESSAGE |
|
用戶管理消息 |
20004 |
COMMAND_USER_OPERATE_EXITMESSAGE |
|
用戶主動退出消息 |
20005 |
COMMAND_USER_ANCHOR_LIVEIN |
|
主播正在直播消息 |
20006 |
COMMAND_CLIENT_LOGININFO |
|
用戶登錄消息 |
20007 |
COMMAND_CLIENT_USERLIST |
|
用戶列表通知消息 |
20008 |
COMMAND_CLIENT_LOGINOTHERUSER |
|
用戶登錄後通知其它用戶的消息 |
20009 |
COMMAND_CLIENT_LOGINOUTOTHERUSER |
|
用戶退出後通知其它用戶的消息 |
20010 |
COMMAND_CLIENT_USERROLELIST |
|
用戶權限列表 |
20011 |
COMMAND_CLIENT_APPENDUSERLIST |
|
追加用戶列表 |
20012 |
COMMAND_CLIENT_SENDMESSAGESOFAST |
|
警告發送過快消息 |
20013 |
COMMAND_CLIENT_REQUESTSENDUSERLIST |
|
請求加載用戶列表消息 |
30001 |
COMMAND_RETURN_CONNECTED |
|
客戶端連接成功消息 |
30002 |
COMMAND_RETURN_CANNOTSPEAK |
|
用戶被禁言消息 |
30005 |
COMMAND_RETURN_OPERATE_CANNOTSPEAK_SUCCESS |
|
禁言用戶成功消息 |
30006 |
COMMAND_RETURN_OPERATE_CANNOTSPEAK_FAILURE |
|
禁言用戶失敗消息 |
30007 |
COMMAND_RETURN_OPERATE_KICKUSER_SUCCESS |
|
踢出用戶成功消息 |
30008 |
COMMAND_RETURN_OPERATE_KICKUSER_FAILURE |
|
踢出用戶失敗消息 |
30009 |
COMMAND_RETURN_OPERATE_BANIPUSER_SUCCESS |
|
禁IP成功消息 |
30010 |
COMMAND_RETURN_OPERATE_BANIPUSER_FAILURE |
|
禁IP失敗消息 |
30011 |
COMMAND_RETURN_OPERATE_RELIEVE_SUCCESS |
|
取消禁言成功消息 |
30012 |
COMMAND_RETURN_OPERATE_RELIEVE_FAILURE |
|
取消禁言失敗消息 |
30013 |
COMMAND_RETURN_OPERATE_SETADMIN_SUCCESS |
|
設置用戶爲管理員成功消息 |
30014 |
COMMAND_RETURN_OPERATE_SETADMIN_FAILURE |
|
設置用戶爲管理員失敗消息 |
30015 |
COMMAND_RETURN_OPERATE_RMADMIN_SUCCESS |
|
刪除用戶管理員成功消息 |
30016 |
COMMAND_RETURN_OPERATE_RMADMIN_FAILURE |
|
刪除用戶管理員失敗消息 |
30017 |
COMMAND_RETURN_USERVERIFY_FAILURE_KICK |
|
用戶驗證失敗(被踢出了) |
31001 |
COMMAND_RETURN_USERVERIFY_FAILURE |
|
用戶驗證失敗 |
32001 |
COMMAND_RETURN_USER_NOTWORTHBALANCE |
|
用戶餘額不足 |
32002 |
COMMAND_RETURN_DEDUCTIONS_FAILURE |
|
扣費驗證失敗 |
40001 |
COMMAND_SCENE_MANAGER_BEGIN |
|
直播開場 |
40002 |
COMMAND_SCENE_MANAGER_END |
|
直播關場 |
40003 |
COMMAND_SCENE_MANAGER_RETURN |
|
操作失敗消息 |
40004 |
COMMAND_SCENE_MANAGER_SEATUSERLIT |
|
座位列表消息 |
40005 |
COMMAND_SCENE_MANAGER_ENCORE |
|
安可的開啓與關閉 |
40006 |
COMMAND_SCENE_MANAGER_ENCOREINFO |
|
安可信息的發送 |
50001 |
COMMAND_CLIENT_SENDTOCLIENTSTATUS |
|
客戶端向服務器報告連接狀態 |
60001 |
COMMAND_FACETIME_REQUEST |
|
聯麥申請 |
60002 |
COMMAND_FACETIME_UPDATE |
|
聯麥信息更新 |
60003 |
COMMAND_FACETIME_USER_COMPLETE |
|
聯麥用戶 |
60004 |
COMMAND_FACETIME_USERLIST |
|
聯麥用戶列表 |
60005 |
COMMAND_FACETIME_ADDFACETIMES |
|
增加聯麥時間 |
60006 |
COMMAND_FACETIME_REQUESTOUTER |
|
申請退出聯麥 |
60007 |
COMMAND_FACETIME_REQUESTCASTUSERLIST |
|
申請廣播聯麥用戶列表 |
2677001 |
SERVER_CHATROOM_USERJOIN |
|
用戶進入(同步至用戶列表服務器) |
2677002 |
SERVER_CHATROOM_USEROUTER |
|
用戶退出(同步至用戶列表服務器) |
2677003 |
SERVER_CHATROOM_CASTUSERLIST |
|
廣播用戶列表(用戶列表服務器) |
2677004 |
SERVER_CHATROOM_USERSTAT |
|
發送房間統計數據 |
2677005 |
SERVER_CHATROOM_USERLIST_RESET |
|
清除用戶列表服務器上用戶(當聊天服務器重啓時) |
2677006 |
SERVER_CHATROOM_USERINFO_UPDATE |
|
房間用戶信息變更(同步更新至用戶列表服務器) |
2677101 |
SERVER_CHATROOM_SYNC_SERVERSTATUS |
|
子服務狀態同步 |
2677102 |
SERVER_CHATROOM_SYNC_CASTMESSAGE |
|
廣播消息同步 |
2677103 |
SERVER_CHATROOM_SYNC_CONVEYMESSAGE |
|
廣播消息中轉 |
88482677 |
COMMAND_SYSTEM_OPERATE_GETROOMINFO |
|
獲取房間日誌 |
Flash的特殊處理
Flash通過Socket連接聊天服務端會出現安全策略問題,通常頁面上加載Flash時會默認請求當前域下的crossdomain.xml和這裏的情況一樣。所以,在服務端監聽時對請求做特殊的處理,將策略文件內容通過連接發送回去。一般策略文件請求在連接端口的Socket中收到,但在這之前Flash默認的會先對843端口提交請求,只有失敗後或超時(約3秒)纔會向連接端口重新請求,所以在843端口另啓監聽線程專門來應對策略文件的請求會比一般的處理速度更快。
代碼片斷
消息處理分線程
bool _iocpSocket_OnRecv(RecvMessage recv)
{
try
{
Message message = recv.Message;
if (null == message) return false;
ChatRoom chatRoom =_chatRooms.GetChatRoom(message.Head.RoomId, true);
DealWithMessageThread dealWith = null;
switch ((Command)message.Head.Command)
{
case Command.COMMAND_CLIENT_LOGININFO:
chatRoom.messageDealWithNew(recv.SO,message);
Log.WriteErrorLog("SocketServer::_iocpSocket_OnRecv",message.ToString());
break;
case Command.COMMAND_USER_GIFTS:
dealWith= _dealWithGiftsThreadPool.Get();
dealWith.Add(newMessageParams(0,recv.SO, recv.Message, chatRoom,null));
break;
default:
dealWith= _dealWithMessageThreadPool.Get();
dealWith.Add(newMessageParams(0,recv.SO, recv.Message, chatRoom,null));
break;
}
return true;
}
catch (Exceptionex)
{
Log.WriteErrorLog("SocketServer::_iocpSocket_OnRecv",ex.Message);
}
returnfalse;
}
各服務器間的消息同步
privatevoid recvMessageDealWith(Messagemessage,EndPoint endPoint)
{
try
{
_tmpMessage.Add(String.Format("[{0}]來自{1}的消息{2}",DateTime.Now,endPoint,JsonConvert.SerializeObject(message.Head)));
if (_tmpMessage.Count > 50)
{
_tmpMessage.RemoveAt(0);
}
string ip = Convert.ToString(endPoint).Split(':')[0];
//string port = Convert.ToString(endPoint).Split(':')[1];
switch ((Command)message.Head.Command)
{
///服務器狀態同步
case Command.SERVER_CHATROOM_SYNC_SERVERSTATUS:
ServerInfo info = ByteConverter.BytesToObject(message.Body)asServerInfo;
ChildServer _s = loadServer(ip, info);
_s.Reset(info);
if (_s.IsNode && null== _nodeServer)
{
_nodeServer= _s;
}
break;
default:
sysCast(message,ip);
break;
}
}
catch (Exceptionex)
{
Log.WriteErrorLog("ServerControl::recvMessageDealWith","{0}",Encoding.UTF8.GetString(message.Body));
}
}
策略文件監聽
_crossDomainServer = new CrossDomainServer();
bool ret =_crossDomainServer.StartListen(_helper.GetCrossDomainPolicyPort(),_helper.GetCrossDomainPolicyData());
if (ret)
{
Log.WriteSystemLog("SocketServer::Listen","服務器[{0}]成功開啓安全策略請求的監聽器,端口爲[{1}]", _helper.GetLocalServerId(), _helper.GetCrossDomainPolicyPort());
}
else
{
Log.WriteSystemLog("SocketServer::Listen","警告警告服務器 [{0}]開啓安全策略請求的監聽器,端口爲 [{1}]失敗”,_helper.GetLocalServerId(), _helper.GetCrossDomainPolicyPort());
}
策略文件處理
if (null!= client)
{
SendMessage sm = newSendMessage(client, _crossDomainXmlBytes);
client.BeginSend(sm.SendBuffer,sm.SendIndex, sm.SendCount
,SocketFlags.Partial,newAsyncCallback(clientSendCallback),sm);
}
Flash端連接處理
var tryCount:int = 0;//重連次數
var reconndelay:int = 3000;//重連間隔
var socket = new Socket();
socket.addEventListener(Event.CONNECT,connectOk);
socket.addEventListener(Event.CLOSE,closed);
socket.addEventListener(IOErrorEvent.IO_ERROR,ioErrored);
socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR,securityErrored);
connectToServer();
private function connectToServer():void
{
woo.debug.debug.trace("socket","正在連接到"+HOST+":"+PORT);
setTimeout(function():void{socket.connect(HOST,PORT);},reconndelay*tryCount);
tryCount ++;
}
private function securityErrored(eve:SecurityErrorEvent):void
{
//安全策略錯誤處理
}
private function ioErrored (eve:SecurityErrorEvent):void
{
//IOERROR處理
}
private function closed (eve:SecurityErrorEvent):void
{
connectToServer();//重連
}
Flash端消息發送
socket.addEventListener(ProgressEvent.SOCKET_DATA,receiveData);
private function receiveData(eve:ProgressEvent =null):void
{
if(this.DATA.CMD==0)
{
if(socket.bytesAvailable>=20)
{
varhead:ByteArray = new ByteArray();
socket.readBytes(head,0,20);
varbyte0:ByteArray = new ByteArray();
varbyte1:ByteArray = new ByteArray();
varbyte2:ByteArray = new ByteArray();
varbyte3:ByteArray = new ByteArray();
varbyte4:ByteArray = new ByteArray();
byte0.position= 0;
byte1.position= 0;
byte2.position= 0;
byte3.position= 0;
byte4.position= 0;
head.readBytes(byte0,0,4);
head.readBytes(byte1,0,4);
head.readBytes(byte2,0,4);
head.readBytes(byte3,0,4);
head.readBytes(byte4,0,4);
DATA.CMD= byteToInt(byte0);
DATA.ROOMID= byteToInt(byte1);
DATA.USERID= byteToInt(byte2);
DATA.TIME= byteToInt(byte3);
DATA.BODYLENGTH= byteToInt(byte4);
}
else
{return;}
}
if(socket.bytesAvailable>=this.DATA.BODYLENGTH)
{
varbody:String = socket.readUTFBytes(DATA.BODYLENGTH);
DATA.BODY =body;
this.sendNotification(Notifications.M_SOCKET_RECEIVE_MSG,this.DATA);
data = null;
data = newSocketMsgVo();//完整消息取完,清空數據對象
if(20<=socket.bytesAvailable)
{
arguments.callee();//再執行當前方法
}
else
{return;}
}
}
Flash消息的發送
public function sendMsg(vo:SocketMsgVo):void
{
if(socket.connected)
{
socket.writeBytes(vo.bytes,0,vo.bytes.length);
socket.flush();
}
else
{
socket.connect(HOST,PORT);
}
}
充值
充值採用第三方充值平臺實現,可接入支付寶、易寶等第三方開發平臺,原理簡單,不做詳細介紹。
虛擬禮物
虛擬禮物的實現主要分爲兩塊,用戶提交送禮請求由扣費服務驗證,成功後廣播到房間所有用戶在前端展示禮物效果。
送禮詳細流程
扣費代碼片斷
禮物展示代碼片斷
//播放禮物
publicfunction ShowGiftInter(id:int, num:int,type:int,w:int,h:int, msg:String ="") {
this.setStageSize(w,h);
_gift.showGiftInter(id, num, type,msg);
trace("123123");
}
//設置播放區域
publicfunction setStageSize(wid:int, het:int) {
_gift.resetGroup(wid - w,het - h);
w = wid;
h = het;
_fs.setStageSize(wid, het);
_gift.setStageSize(wid, het);
}
//播放飛屏
publicfunction showFlyText(str:String,w:int,h:int,speed:int = 4) {
setStageSize(w,h);
_fs.setStaticText(str, speed);
}
//播放進場動畫
publicfunction ShowCarInter(id:int,w:int,h:int,_no:int,_nick:String,_sex:int)
{
this.setStageSize(w,h);
_gift.showGiftInter(id,1,5,{liangNo:_no,nickName:_nick,sex:_sex});
}