基于 Swoole 實作支援高并發的實時彈幕功能(下)
由 學院君 建立于1年前, 最後更新于 1年前
版本号 #1
12184 views
6 likes
0 collects
我們接着上篇教程來完成彈幕服務端以及用戶端與服務端互動的開發,首先來實作服務端 WebSocket 伺服器的編碼。
WebSocket 伺服器
我們參照之前的功能介紹教程《在 Laravel 中內建 Swoole 實作 WebSocket 伺服器》實作這個用于彈幕功能的 WebSocket 伺服器。
注:如果你還沒有在 Laravel 項目中安裝配置 LaravelS 擴充包,參考這篇教程:基于 Swoole 實作高性能 HTTP 伺服器。
WebSocketHandler
首先在 app/Handlers 目錄下建立一個實作了 WebSocketHandlerInterface 接口的 WebSocket 處理器類 WebSocketHandler,并編寫對應的業務代碼如下:
namespace App\Handlers;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Illuminate\Support\Facades\Log;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
class WebSocketHandler implements WebSocketHandlerInterface
{
public function __construct()
{
}
// 連接配接建立時觸發
public function onOpen(Server $server, Request $request)
{
Log::info('WebSocket 連接配接建立:' . $request->fd);
}
// 收到消息時觸發
public function onMessage(Server $server, Frame $frame)
{
// $frame->fd 是用戶端 id,$frame->data 是用戶端發送的資料
Log::info("從 {$frame->fd} 接收到的資料: {$frame->data}");
foreach($server->connections as $fd){
if (!$server->isEstablished($fd)) {
// 如果連接配接不可用則忽略
continue;
}
$server->push($fd , $frame->data); // 服務端通過 push 方法向所有連接配接的用戶端發送資料
}
}
// 連接配接關閉時觸發
public function onClose(Server $server, $fd, $reactorId)
{
Log::info('WebSocket 連接配接關閉:' . $fd);
}
}
很簡單,就是在建立、斷開 WebSocket 連接配接的時候列印下日志,然後在收到用戶端發送過來的彈幕消息時将其推送給所有已連接配接的 WebSocket 用戶端,達到「廣播」的效果,這樣,就不需要用戶端主動來拉資料了。當然,這裡是最簡單的推送邏輯,你可以根據需要将彈幕消息儲存到資料庫或其他儲存設備持久化存儲。
然後我們在 config/laravels.php 中配置這個 WebSocketHandler 使其生效:
'websocket' => [
'enable' => true,
'handler' => \App\Handlers\WebSocketHandler::class,
],
這樣一來服務端 WebSocket 處理器的編碼工作就完成了,很簡單吧,接下來,我們還要在 Nginx 中配置使其支援 WebSocket 通信。
在 Nginx 中配置支援 WebSocket 通信
假設我們這個項目對應的虛拟主機域名是 laravel58.test,接下來我們要到 Nginx 對應的虛拟主機配置中使其支援處理 WebSocket 連接配接和請求,由于 Swoole 的 WebSocket 伺服器基于 Swoole HTTP 伺服器實作,是以我們要同時開啟這兩個支援,修改 laravel58 應用對應虛拟主機配置檔案 laravel58.conf 内容如下:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream danmu {
# Connect IP:Port
server workspace:5200 weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
server {
listen 80;
server_name laravel58.test;
root /var/www/laravel58/public;
error_log /var/log/nginx/laravel58_error.log;
access_log /var/log/nginx/laravel58_access.log;
autoindex off;
index index.html index.htm;
# Nginx handles the static resources(recommend enabling gzip), LaravelS handles the dynamic resource.
location / {
try_files $uri @danmu;
}
# Response 404 directly when request the PHP file, to avoid exposing public/*.php
#location ~* \.php$ {
# return 404;
#}
# Http and WebSocket are concomitant, Nginx identifies them by "location"
# !!! The location of WebSocket is "/ws"
# Javascript: var ws = new WebSocket("ws://laravel58.test/ws");
# 處理 WebSocket 通信
location ^~ /ws/ {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout: Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds; At the same time, this close behavior is also affected by heartbeat setting of Swoole.
# proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://danmu;
}
location @danmu {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://danmu;
}
}
注:學院君本地基于 Laradock 作為開發環境,是以在 upstream danmu 配置塊中将 server 設定為 workspace:5200,如果你不是以 Laradock 作為開發環境,需要調整這裡的 IP 位址為實際 Swoole WebSocket 伺服器運作位址。
接下來,重新開機 Nginx 伺服器,然後我們在 laravel58 項目下啟動 HTTP/WebSocket 伺服器:
bin/laravels start
就可以基于 Nginx + Swoole HTTP/WebSocket 伺服器對外提供服務了。
Vue 元件與 WebSocket 伺服器通信
下面,我們回到上篇教程實作的 Vue 元件 DanmuComponent,編寫用戶端與 WebSocket 伺服器通信的 JavaScript 腳本代碼:
import { MESSAGE_TYPE } from 'vue-baberrage'
export default {
name: 'danmu',
data () {
return {
msg: '你好,學院君!',
position: 'top',
barrageIsShow: true,
currentId: 0,
barrageLoop: true,
websocket: null,
barrageList: []
}
},
created () {
// 初始化 websocket 并定義回調函數
this.websocket = new WebSocket("ws://laravel58.test/ws/");
this.websocket.onopen = function (event) {
console.log("已建立 WebSocket 連接配接");
};
let that = this;
this.websocket.onmessage = function (event) {
// 接收到 WebSocket 伺服器傳回消息時觸發
let data = JSON.parse(event.data);
that.addToList(data.position, data.message);
};
this.websocket.onerror = function (event) {
console.log("與 WebSocket 通信出錯");
};
this.websocket.onclose = function (event) {
console.log("斷開 WebSocket 連接配接");
};
},
destroyed () {
this.websocket.close();
},
methods: {
removeList () {
this.barrageList = []
},
addToList (position, message) {
if (position === 'top') {
this.barrageList.push({
id: ++this.currentId,
avatar: 'https://xueyuanjun.com/assets/avatars/numxwdxf8lrtrsol.jpg',
msg: message,
barrageStyle: 'top',
time: 8,
type: MESSAGE_TYPE.FROM_TOP,
position: 'top'
})
} else {
this.barrageList.push({
id: ++this.currentId,
avatar: 'https://xueyuanjun.com/assets/avatars/numxwdxf8lrtrsol.jpg',
msg: message,
time: 15,
type: MESSAGE_TYPE.NORMAL
})
}
},
sendMsg () {
// 發送消息到 WebSocket 伺服器
this.websocket.send('{"position":"' + this.position + '", "message":"' + this.msg + '"}');
},
}
}
我們在 Vue 元件建立期間初始化 WebSocket 連接配接并定義回調函數,在接收到 WebSocket 伺服器傳回的消息時調用 addToList 方法将其渲染到用戶端彈幕元件中,最後将模闆代碼中點選發送事件函數從之前的 addToList 調整為 sendMsg,即将消息推送給 WebSocket 伺服器:
:isShow= "barrageIsShow"
:barrageList = "barrageList"
:loop = "barrageLoop"
:maxWordCount = "60"
>
從上
從右
發送
至此,用戶端 Vue 元件就編寫好了,當然,這裡隻是一個小的功能示範,對于複雜系統,如果多處需要建立 WebSocket 通信,可以在公共元件中初始化 WebSocket 連接配接,然後通過 Vuex 來管理 WebSocket 伺服器傳回的消息,最後在各自的 Vue 元件中監聽 Vuex 資料變更進行本地資料渲染,關于這一部分的優化,我們在實時聊天室項目中會示範如何實作,這裡彈幕功能比較單一,就将代碼直接寫到一個 Vue 元件中了。
基于 WebSocket + Vue 用戶端的彈幕功能示範
重新編譯前端資源讓上述 Vue 元件變更生效:
npm run dev
然後打開浏覽器通路 http://laravel58.test/danmu,在開發者工具的控制台标簽頁可以看到 WebSocket 連接配接建立日志,在輸入框中輸入文字點選「發送」,可以看到彈幕效果:
這個時候展現的效果和上篇教程并無二緻,盡管這些彈幕消息已經是經過 WebSocket 伺服器處理後傳回的,而不是直接渲染的,這個通信過程可以在開發者工具的 Network->WS 标簽頁中看到:
為了展現 WebSocket 伺服器的優勢,新開一個浏覽器标簽頁,為了友善識别,我們把上面這個已經打開的标簽頁叫做 P1,把新開的标簽頁叫做 P2,重新加載 P1,重新整理之前的資料,然後點選「發送」按鈕,可以同時在 P1 和 P2 上看到彈幕消息,同理,在 P2 上更新輸入框預設資料,比如「Laravel學院」,然後點選發送按鈕,也可以在 P1 看到 P2 頁面發出的彈幕消息:
這樣,我們就完成了簡單的、後端基于 WebSocket 伺服器通信的、支援長連接配接和多并發的實時彈幕功能。不過真正用于實際項目的話還有一些地方需要優化,比如與使用者系統的關聯、視訊蒙版、以及彈幕重疊等,由于這些不是本教程探讨的重點,就不再展開了,感興趣的同學可以自行去研究和實作。