因爲博主是個忠實的英雄聯盟粉絲,因此常常觀看一些明星大神的直播。而一談到直播,確定會看到滿屏幕飄來飄去的彈幕。那麼問題來了,這些視頻彈幕網站如何作到實時同步的?PHP如何開發一個相似的網站?javascript
首先要搞定的是前端頁面,最起碼得有個框,讓彈幕飛起來吧。一想到前臺,博主頭就大(畢竟我不喜歡去扣前端代碼,並且作出來的東西還巨醜)。那我們就百度一下吧,看看有什麼好用的彈幕插件,如今開源的東西那麼多。php
通過搜索,找到了一個jQuery.danmu.js的開源項目。看了一下star的人還挺多。https://github.com/chiruom/jquery.danmu.jscss
因而乎,管他三七二十一,先down下來再說。html
git clone https://github.com/chiruom/jquery.danmu.js.git
大體一看目錄結構以下:前端
進入demo目錄,先運行一下例子看看結果唄。java
果真,點開之後出現了一個高大上的頁面,略看一下功能還挺多。可是問題來了,爲啥我點擊開始,一點反應也木有呢。jquery
尋找緣由ing。git
原來是源文件中的jQuery插件的問題。在src目錄下,並無該文件github
<script src="../src/jquery-2.1.4.min.js"></script>
算了仍是調用百度的在線jQuery插件吧web
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
再一刷新,不出預料,成功運行。
頗有意思,有木有,很激動有木有。然而重點纔剛剛開始。
後端,那就先來講說彈幕的原理吧。彈幕,就至關於一個公共聊天室,都是一個客戶端發送消息給服務端,服務端再將收到的消息廣播給其餘的客戶端。
用傳統的ajax輪詢嗎?不行,這樣效率過低,想一想各大火爆的直播平臺都是同一時間幾萬人在線,幾千人同時發彈幕,若是靠ajax輪詢一個PHP接口的話服務器會吃不消的。且彈幕消息存儲方案略顯複雜,有人問爲何要存儲呢?由於ajax使用的HTTP協議是無狀態協議,A客戶端和B客戶端之間對於服務器來講沒有任何標誌,若是服務器要確保A客戶端和B客戶端分別在兩次請求的時候服務器只返回這兩個客戶端沒有獲取過的彈幕消息,那麼服務器端就必須使用一個緩存來標識某某客戶端看過哪條彈幕消息。綜上所述ajax能夠實現小規模的彈幕通訊方案,可是很麻煩。
好在最新的HTML5中加入了WebSocket協議,咱們能夠通WebSocket這種基於HTTP協議之上的即時通訊協議來替代ajax這種傳統的我問你答的老舊通訊模式。而咱們是PHPer,對於咱們這種只懂PHP的人該如何編寫WebSocket服務端呢?好在咱們又得知PHP有一個Swoole擴展,咱們在PHP語言中使用它能夠很方便的構建一個WebSocket服務端。
關於Swoole,下面這段是其官網上的話:
PHP的異步、並行、高性能網絡通訊引擎,使用純C語言編寫,提供了PHP語言的異步多線程服務器,異步TCP/UDP網絡客戶端,異步MySQL,異步Redis,數據庫鏈接池,AsyncTask,消息隊列,毫秒定時器,異步文件讀寫,異步DNS查詢。 Swoole內置了Http/WebSocket服務器端/客戶端、Http2.0服務器端。
Swoole能夠普遍應用於互聯網、移動通訊、企業軟件、雲計算、網絡遊戲、物聯網(IOT)、車聯網、智能家居等領域。 使用PHP+Swoole做爲網絡通訊框架,可使企業IT研發團隊的效率大大提高,更加專一於開發創新產品。
跟詳細的東西請自行參考官網文檔。這裏就不在廢話了。
http://wiki.swoole.com/wiki/page/479.html
還有一個問題須要解決,那就是,這個jquery.danmu.js是基於彈幕運行時間的一個插件。那又要如何作到實時呢。開始博主想的是在服務器端規定一個時間(即其鏈接時間),當有客戶端鏈接時,返回服務器的當前時間戳,而後以此爲依據開始計時。可是遇到的問題以下:
好吧,博主走彎路子了(沒作過這方面的東西,缺少經驗)。這個時候,就須要轉變一種思路了。
websocket是實時通訊的,哎,那全部客戶端的時間,不一致就不一致吧,彈幕發的時間根據各個客戶端的爲準唄,都以當前各個客戶端的時間來發,websocket只傳遞不包含時間的數據(好吧有點繞,我本身都感受說饒了),我們直接來上代碼吧。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>彈幕made by diligentyang</title> <style> body { font-family: "Microsoft YaHei" ! important; font-color:#222; } pre { line-height: 2em; font-family: "Microsoft YaHei" ! important; } h4 { line-height: 2em; } #danmuarea { position: relative; background: #222; width:800px; height: 445px; margin-left: auto; margin-right: auto; } .center { text-align: center; } .ctr { font-size: 1em; line-height: 2em; } </style> <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script> <script src="../dist/jquery.danmu.min.js"></script> </head> <body class="center"> Demo<br><br> <!--黑背景和彈幕區--> <div id="danmuarea"> <div id="danmu" > </div> </div> <!--控制區--> <div class="ctr" > <button type="button" onclick="pauser()">彈幕暫停</button> <button type="button" onclick="resumer() ">彈幕繼續</button> 顯示彈幕:<input type='checkbox' checked='checked' id='ishide' value='is' onchange='changehide()'> 彈幕透明度: <input type="range" name="op" id="op" onchange="op()" value="100"> <br> 當前彈幕運行時間(秒):<span id="time"></span> <!--設置當前彈幕時間(秒): <input type="text" id="set_time" max=20 /> <button type="button" onclick="settime()">設置</button>--> <br> 發彈幕: <select name="color" id="color" > <option value="white">白色</option> <option value="red">紅色</option> <option value="green">綠色</option> <option value="blue">藍色</option> <option value="yellow">黃色</option> </select> <select name="size" id="text_size" > <option value="1">大文字</option> <option value="0">小文字</option> </select> <select name="position" id="position" > <option value="0">滾動</option> <option value="1">頂端</option> <option value="2">底端</option> </select> <input type="textarea" id="text" max=300 /> <button type="button" onclick="send()">發送</button> </div> <script> //WebSocket var wsServer = 'ws://123.206.61.229:9505'; var websocket= new WebSocket(wsServer); websocket.onopen = function (evt) { console.log("Connected to WebSocket server."); /*websocket.send("gaga");*/ //連上以後就打開彈幕 $('#danmu').danmu('danmuResume'); }; websocket.onclose = function (evt) { console.log("Disconnected"); }; websocket.onmessage = function (evt) { console.log('Retrieved data from server: ' + evt.data); var time = $('#danmu').data("nowTime")+1; var text_obj= evt.data +',"time":'+time+'}';//獲取加上當前時間 console.log(text_obj); var new_obj=eval('('+text_obj+')'); $('#danmu').danmu("addDanmu",new_obj);//添加彈幕 }; websocket.onerror = function (evt, e) { console.log('Error occured: ' + evt.data); }; //初始化 $("#danmu").danmu({ left:0, top:0, height:"100%", width:"100%", speed:20000, opacity:1, font_size_small:16, font_size_big:24, top_botton_danmu_time:6000 }); //一個定時器,監視彈幕時間並更新到頁面上 function timedCount(){ $("#time").text($('#danmu').data("nowTime")); t=setTimeout("timedCount()",50) } timedCount(); function starter(){ $('#danmu').danmu('danmuStart'); } function pauser(){ $('#danmu').danmu('danmuPause'); } function resumer(){ $('#danmu').danmu('danmuResume'); } function stoper(){ $('#danmu').danmu('danmuStop'); } function getime(){ alert($('#danmu').data("nowTime")); } function getpaused(){ alert($('#danmu').data("paused")); } //發送彈幕,使用了文檔README.md第7節中推薦的方法 function send(){ var text = document.getElementById('text').value; var color = document.getElementById('color').value; var position = document.getElementById('position').value; //var time = $('#danmu').data("nowTime")+1; var size =document.getElementById('text_size').value; //var text_obj='{ "text":"'+text+'","color":"'+color+'","size":"'+size+'","position":"'+position+'","time":'+time+'}'; //爲了處理簡單,方便後續加time,和isnew,就先醬紫發一半吧。 //注:time爲彈幕出來的時間,isnew爲是否加邊框,本身發的彈幕,常理上來講是有邊框的。 var text_obj='{ "text":"'+text+'","color":"'+color+'","size":"'+size+'","position":"'+position+'"'; //利用websocket發送 websocket.send(text_obj); //清空相應的內容 document.getElementById('text').value=''; } //調整透明度函數 function op(){ var op=document.getElementById('op').value; $('#danmu').danmu("setOpacity",op/100); } //調隱藏 顯示 function changehide() { var op = document.getElementById('op').value; op = op / 100; if (document.getElementById("ishide").checked) { $("#danmu").danmu("setOpacity",1) } else { $("#danmu").danmu("setOpacity",0) } } //設置彈幕時間 function settime(){ var t=document.getElementById("set_time").value; t=parseInt(t) $('#danmu').danmu("setTime",t); } </script> </body> </html>
上述代碼須要注意的是websocket的創建和接收,以及send方法中對彈幕的處理。
ws_server.php
<?php //建立websocket服務器對象,監聽0.0.0.0:9505端口 $ws = new swoole_websocket_server("0.0.0.0", 9505); //監聽WebSocket鏈接打開事件 $ws->on('open', function ($ws, $request) { //var_dump($request->fd, $request->get, $request->server); //至關於記錄一個日誌吧,有鏈接時間和鏈接ip echo $request->fd.'-----time:'.date("Y-m-d H:i:s",$request->server['request_time']).'--IP--'.$request->server['remote_addr'].'-----'; }); //監聽WebSocket消息事件 $ws->on('message', function ($ws, $frame) { //記錄收到的消息,能夠寫到日誌文件中 echo "Message: {$frame->data}\n"; //遍歷全部鏈接,循環廣播 foreach($ws->connections as $fd){ //若是是某個客戶端,本身發的則加上isnew屬性,不然不加 if($frame->fd == $fd){ $ws->push($frame->fd, $frame->data.',"isnew":""'); }else{ $ws->push($fd, "{$frame->data}"); } } }); //監聽WebSocket鏈接關閉事件 $ws->on('close', function ($ws, $fd) { echo "client-{$fd} is closed\n"; }); $ws->start();
運行方法:
輸入php ws_server.php
先啓動服務器端的websocket。若是要後臺運行,且不隨用戶終端關閉而斷開,須要建立一個log.txt用於存取上述輸出的東西,而後輸入nohup php ws_server.php > log.txt &
便可。
而後,
注,若是要用此項目,須要自行修改本身的服務器ip地址。只須要修改var wsServer = 'ws://123.206.61.229:9505';
處便可,後臺代碼不須要作任何處理。
github地址:https://github.com/diligentyang/danmu
原文博主:http://blog.csdn.net/qq_28602957
如需轉載請明示。