由於博主是個忠實的英雄聯盟粉絲,所以經常觀看一些明星大神的直播。而一談到直播,肯定會看到滿屏幕飄來飄去的彈幕。那么問題來了,這些視頻彈幕網站如何做到實時同步的?PHP如何開發一個類似的網站?
首先要搞定的是前端頁面,最起碼得有個框,讓彈幕飛起來吧。一想到前台,博主頭就大(畢竟我不喜歡去扣前端代碼,而且做出來的東西還巨丑)。那咱們就百度一下吧,看看有什么好用的彈幕插件,現在開源的東西那么多。
經過搜索,找到了一個jQuery.danmu.js的開源項目。看了一下star的人還挺多。https://github.com/chiruom/jquery.danmu.js
於是乎,管他三七二十一,先down下來再說。
git clone https://github.com/chiruom/jquery.danmu.js.git
- 1
大致一看目錄結構如下:

進入demo目錄,先運行一下例子看看結果唄。
果然,點開以后出現了一個高大上的頁面,略看一下功能還挺多。但是問題來了,為啥我點擊開始,一點反應也木有呢。
尋找原因ing。
原來是源文件中的jQuery插件的問題。在src目錄下,並沒有該文件
<script src="../src/jquery-2.1.4.min.js"></script>
- 1
算了還是調用百度的在線jQuery插件吧
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
- 1
再一刷新,不出預料,成功運行。

很有意思,有木有,很激動有木有。然而重點才剛剛開始。
后端,那就先來說說彈幕的原理吧。彈幕,就相當於一個公共聊天室,都是一個客戶端發送消息給服務端,服務端再將收到的消息廣播給其他的客戶端。
用傳統的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是基於彈幕運行時間的一個插件。那又要如何做到實時呢。開始博主想的是在服務器端規定一個時間(即其連接時間),當有客戶端連接時,返回服務器的當前時間戳,然后以此為依據開始計時。但是遇到的問題如下:
- 該彈幕插件是按十分之秒計時制度。
- 各瀏覽器上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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
上述代碼需要注意的是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();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
運行方法:
輸入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
如需轉載請明示。
