基於C#分步式聊天系統的在線視頻直播系統設計

視頻在線直播系統: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});

        }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章