1. 直播表设计;
表的设计
球队表
球员表
球员分数表(得分、助攻等)
直播赛事表
赛事战况表
用户聊天室表
CREATE TABLE ` live_team` (
` id` tinyint ( 1 ) unsigned NOT NULL auto_increment ,
` name` varchar ( 32 ) NOT NULL DEFAULT '' ,
` image` varchar ( 64 ) NOT NULL DEFAULT '' ,
` type ` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` create_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
` update_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
PRIMARY KEY ( ` id` )
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT charset = utf8;
CREATE TABLE ` live_player` (
` id` int ( 10 ) unsigned NOT NULL auto_increment ,
` name` varchar ( 32 ) NOT NULL DEFAULT '' ,
` image` varchar ( 64 ) NOT NULL DEFAULT '' ,
` age` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` position` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` status ` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` create_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
` update_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
PRIMARY KEY ( ` id` )
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT charset = utf8;
CREATE TABLE ` live_game` (
` id` int ( 10 ) unsigned NOT NULL auto_increment ,
` a_id` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` b_id` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` a_score` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` b_score` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` narrator` varchar ( 32 ) NOT NULL DEFAULT '' ,
` name` varchar ( 32 ) NOT NULL DEFAULT '' ,
` image` varchar ( 64 ) NOT NULL DEFAULT '' ,
` start_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
` status ` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` create_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
` update_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
PRIMARY KEY ( ` id` )
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT charset = utf8;
CREATE TABLE ` live_outs` (
` id` int ( 10 ) unsigned NOT NULL auto_increment ,
` game_id` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
` team_id` tiny_int( 1 ) unsigned NOT NULL DEFAULT 0 ,
` content` varchar ( 256 ) NOT NULL DEFAULT '' ,
` image` varchar ( 64 ) NOT NULL DEFAULT '' ,
` type ` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` status ` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` create_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
PRIMARY KEY ( ` id` )
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT charset = utf8;
CREATE TABLE ` live_chart` (
` id` int ( 10 ) unsigned NOT NULL auto_increment ,
` game_id` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
` user_id` tiny_int( 1 ) unsigned NOT NULL DEFAULT 0 ,
` content` varchar ( 256 ) NOT NULL DEFAULT '' ,
` status ` tinyint ( 1 ) unsigned NOT NULL DEFAULT 0 ,
` create_time` int ( 10 ) unsigned NOT NULL DEFAULT 0 ,
PRIMARY KEY ( ` id` )
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT charset = utf8;
2. WebSocket 服务器搭建并支持 Http 服务;
WebSocket 机制是基于 Swoole 里的 Http Server,所以如果建立一个 WebSocket 脚本,是可以拥有 Http Server 的特性 实例:新建 ws.php
cd /data/project/test/swoole/demo/server
vim ws.php
写入以下内容(基于 http_server 基础上增加)
<?php
class Ws {
CONST HOST = "0.0.0.0" ;
CONST PORT = 8811 ;
public $ws = null ;
public function __construct ( ) {
$this - > ws = new swoole_websocket_server ( self: : HOST , self: : PORT ) ;
$this - > ws - > set ( [
'worker_num' = > 4 ,
'task_worker_num' = > 4 ,
'enable_static_handler' = > true ,
'document_root' = > "/data/project/test/swoole/tp5/public/static" ,
] ) ;
$this - > ws - > on ( "open" , [ $this , 'onOpen' ] ) ;
$this - > ws - > on ( "message" , [ $this , 'onMessage' ] ) ;
$this - > ws - > on ( "workerStart" , [ $this , 'onWorkerStart' ] ) ;
$this - > ws - > on ( "request" , [ $this , 'onRequest' ] ) ;
$this - > ws - > on ( "task" , [ $this , 'onTask' ] ) ;
$this - > ws - > on ( "finish" , [ $this , 'onFinish' ] ) ;
$this - > ws - > on ( "close" , [ $this , 'onClose' ] ) ;
$this - > ws - > start ( ) ;
}
public function onWorkerStart ( $server , $worker_id ) {
define ( 'APP_PATH' , __DIR__ . '/../application/' ) ;
require __DIR__ . '/../thinkphp/start.php' ;
}
public function onRequest ( $request , $response ) {
$_SERVER = [ ] ;
if ( isset ( $request - > server ) ) {
foreach ( $request - > server as $k = > $v ) {
$_SERVER [ strtoupper ( $k ) ] = $v ;
}
}
if ( isset ( $request - > header ) ) {
foreach ( $request - > header as $k = > $v ) {
$_SERVER [ strtoupper ( $k ) ] = $v ;
}
}
$_FILES = [ ] ;
if ( isset ( $request - > files ) ) {
foreach ( $request - > files as $k = > $v ) {
$_FILES [ $k ] = $v ;
}
}
$_GET = [ ] ;
if ( isset ( $request - > get ) ) {
foreach ( $request - > get as $k = > $v ) {
$_GET [ $k ] = $v ;
}
}
$_POST = [ ] ;
if ( isset ( $request - > post ) ) {
foreach ( $request - > post as $k = > $v ) {
$_POST [ $k ] = $v ;
}
}
$_POST [ 'http_server' ] = $this - > ws ;
ob_start ( ) ;
try {
think\Container : : get ( 'app' , [ APP_PATH ] ) - > run ( ) - > send ( ) ;
} catch ( \Exception $e ) {
}
$res = ob_get_contents ( ) ;
ob_end_clean ( ) ;
$response - > end ( $res ) ;
}
public function onTask ( $serv , $task_id , $workerId , $data ) {
$obj = new app\ common\ lib\ task\ Task ;
$method = $data [ 'method' ] ;
$flag = $obj - > $method ( $data [ 'data' ] ) ;
return $flag ;
}
public function onFinish ( $serv , $taskId , $data ) {
echo "taskId:{ $taskId } \n" ;
echo "finish-data-success:{ $data } \n" ;
}
public function onOpen ( $ws , $request ) {
var_dump ( $request - > fd ) ;
}
public function onMessage ( $ws , $frame ) {
echo "server-push-message:{ $frame - > data } \n" ;
}
public function onClose ( $ws , $fd ) {
echo "closed - clientId: { $fd } \n" ;
}
}
new Ws ( ) ;
3. 直播页面搭建;
赛事直播流程图
直播员去后台录入一些直播数据,然后把数据发到服务器(ws.php)
数据会进行两部分的操作。第一,数据会录入 MySQL 表里。第二,数据会推送给互联网用户(比如 10 万个用户打开了直播页面,就要把数据推送给这 10 万个用户)。
互联网用户观看直播(赛况直播页面)的时候,直播页面就找到 Http WebSocket 服务器,服务器把静态页面返回给用户。直播页面和服务器是通过 WebSocket 机制连接服务器。
赛事直播员解说页面(后台)搭建:public/static/admin/live.html
<!DOCTYPE html>
< html>
< head>
< meta charset = " UTF-8" >
< title> 赛事直播-主持人页面</ title>
< meta name = " renderer" content = " webkit" >
< meta http-equiv = " X-UA-Compatible" content = " IE=edge,chrome=1" >
< meta name = " viewport" content = " width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi" />
< link rel = " shortcut icon" href = " /favicon.ico" type = " image/x-icon" />
< link rel = " stylesheet" href = " ./css/font.css" >
< link rel = " stylesheet" href = " ./css/xadmin.css" >
< link rel = " stylesheet" type = " text/css" href = " ../webuploader/webuploader.css" >
< script type = " text/javascript" src = " https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js" > </ script>
< script type = " text/javascript" src = " ./lib/layui/layui.js" charset = " utf-8" > </ script>
< script type = " text/javascript" src = " ./js/xadmin.js" > </ script>
< script type = " text/javascript" src = " ../webuploader/webuploader.js" > </ script>
</ head>
< body>
< div class = " x-body" >
< form class = " layui-form" >
< div class = " layui-form-item" >
< label for = " username" class = " layui-form-label" >
< span class = " x-red" > *</ span> 第几节
</ label>
< div class = " layui-input-inline" >
< select id = " type" name = " type" class = " valid" >
< option value = " 1" > 第一节</ option>
< option value = " 2" > 第二节</ option>
< option value = " 3" > 第三节</ option>
< option value = " 4" > 第四节</ option>
</ select>
</ div>
</ div>
< div class = " layui-form-item" >
< label for = " username" class = " layui-form-label" >
< span class = " x-red" > *</ span> 球队
</ label>
< div class = " layui-input-inline" >
< select id = " team_id" name = " team_id" class = " valid" >
< option value = " 0" > 请选择</ option>
< option value = " 1" > 马刺</ option>
< option value = " 4" > 火箭</ option>
</ select>
</ div>
</ div>
< div class = " layui-form-item layui-form-text" >
< label for = " desc" class = " layui-form-label" >
赛况内容
</ label>
< div class = " layui-input-block" >
< textarea placeholder = " 请输入内容" id = " content" name = " content" class = " layui-textarea" > </ textarea>
</ div>
</ div>
< div class = " layui-form-item layui-form-text" >
< label for = " desc" class = " layui-form-label" >
赛况图
</ label>
< div id = " uploader-demo" >
< div id = " fileList" class = " uploader-list" > </ div>
< div id = " filePicker" > 选择图片</ div>
</ div>
</ div>
< div class = " layui-form-item" >
< label for = " L_repass" class = " layui-form-label" >
</ label>
< button type = " submit" class = " layui-btn" lay-filter = " add" id = " submit-btn" lay-submit = " " >
增加
</ button>
</ div>
</ form>
</ div>
< script>
var $ = jQuery,
$list = $ ( '#fileList' ) ,
ratio = window. devicePixelRatio || 1 ,
thumbnailWidth = 100 * ratio,
thumbnailHeight = 100 * ratio,
uploader;
var uploader = WebUploader. create ( {
auto: true ,
swf: 'http://192.168.2.214:8811/webuploader/Uploader.swf' ,
server: 'http://192.168.2.214:8811/?s=admin/image/index' ,
pick: '#filePicker' ,
accept: {
title: 'Images' ,
extensions: 'gif,jpg,jpeg,bmp,png' ,
mimeTypes: 'image/*'
}
} ) ;
/ / PS : 这里得到的是Data URL 数据,IE6 、IE7 不支持直接预览。可以借助FLASH 或者服务端来完成预览。
/ / 当有文件添加进来的时候
uploader. on ( 'fileQueued' , function ( file ) {
var $li = $ (
'<div id="' + file. id + '" class="file-item thumbnail">' +
'<img>' + '</div>'
) ,
$img = $li. find ( 'img' ) ;
/ / $list为容器jQuery实例
$list. append ( $li ) ;
/ / 创建缩略图
/ / 如果为非图片文件,可以不用调用此方法。
/ / thumbnailWidth x thumbnailHeight 为 100 x 100
uploader. makeThumb ( file, function ( error, src ) {
if ( error ) {
$img. replaceWith ( '<span>不能预览</span>' ) ;
return ;
}
$img. attr ( 'src' , src ) ;
} , thumbnailWidth, thumbnailHeight ) ;
} ) ;
/ / 然后剩下的就是上传状态提示了,
/ / 当文件上传过程中, 上传成功,上传失败,
/ / 上传完成都分别对应uploadProgress, uploadSuccess, uploadError, uploadComplete事件。
/ / 文件上传过程中创建进度条实时显示。
uploader. on ( 'uploadProgress' , function ( file, percentage ) {
var $li = $ ( '#' + file. id ) ,
$percent = $li. find ( '.progress span' ) ;
/ / 避免重复创建
if ( ! $percent. length ) {
$percent = $ ( '<p class="progress"><span></span></p>' )
. appendTo ( $li )
. find ( 'span' ) ;
}
$percent. css ( 'width' , percentage * 100 + '%' ) ;
} ) ;
/ / 文件上传成功,给item添加成功class , 用样式标记上传成功。
uploader. on ( 'uploadSuccess' , function ( file, response ) {
if ( response. status == 1 ) {
$ ( '#' + file. id ) . append ( '<input type="hidden" name="image" value="' + response. data. image + '">' ) ;
}
$ ( '#' + file. id ) . addClass ( 'upload-state-done' ) ;
} ) ;
/ / 文件上传失败,显示上传出错
uploader. on ( 'uploadError' , function ( file ) {
var $li = $ ( '#' + file. id ) ,
$error = $li. find ( 'div.error' ) ;
/ / 避免重复创建
if ( ! $error. length ) {
$error = $ ( '<div class="error"></div>' ) . appendTo ( $li ) ;
}
$error. text ( '上传失败' ) ;
} ) ;
/ / 完成上传完了,成功或者失败,先删除进度条。
uploader. on ( 'uploadComplete' , function ( file ) {
$ ( '#' + file. id ) . find ( '.progress' ) . remove ( ) ;
} ) ;
/ / 最后提交信息
var $submitBtn = $ ( '#submit-btn' ) ;
/ / 提交表单
$submitBtn. click ( function ( event) {
event. preventDefault ( ) ;
var formData = $ ( 'form' ) . serialize ( ) ;
/ / TODO : 请求后台接口跳转界面,前端跳转或者后台跳
$. get ( "http://192.168.2.214:8811/?s=admin/live/push&" + formData, function ( data) {
if ( data. status == 1 ) {
/ / 登录成功
}
/ / location. href= 'index.html' ;
} , 'json' ) ;
} ) ;
</ script>
</ body>
</ html>
图片 host 配置文件:新增 config/live.php
<?php
return [
'host' = > 'http://192.168.2.214:8811'
] ;
图片上传接口:application/admin/controller/Image.php
<?php
namespace app\ admin\ controller ;
use app\ common\ lib\ Util ;
class Image {
public function index ( ) {
$file = request ( ) - > file ( 'file' ) ;
$info = $file - > move ( '../public/static/upload' ) ;
if ( $info ) {
$data = [
'image' = > config ( 'live.host' ) . "/upload/" . $info - > getSaveName ( ) ,
] ;
return Util: : show ( config ( 'code.success' ) , 'ok' , $data ) ;
} else {
return Util: : show ( config ( 'code.error' ) , 'error' ) ;
}
}
}
数据入库、push 到直播页面接口:application/admin/controller/Live.php
<?php
namespace app\ admin\ controller ;
use app\ common\ lib\ Util ;
class Live {
public function push ( ) {
print_r ( $_GET ) ;
$_POST [ 'http_server' ] - > push ( 7 , 'hello-push-data' ) ;
}
}
直播页面引入的 js 文件:public/static/live/js/live.js
var wsUrl = "ws://192.168.2.214:8811" ;
var websocket = new WebSocket ( wsUrl) ;
websocket. onopen = function ( evt) {
console. log ( "connected-swoole-success" ) ;
}
websocket. onmessage = function ( evt) {
console. log ( "ws-server-return-data:" + evt. data) ;
}
websocket. onclose = function ( evt) {
console. log ( "close" ) ;
}
websocket. onerror = function ( evt, e) {
console. log ( "error" + evt. data) ;
}
4. 赛事直播在线用户处理 - redis 方案;
'live_game_key' = > 'live_game_key' ,
修改 application/common/lib/redis/Predis.php
public function sAdd ( $key , $value ) {
return $this - > redis - > sAdd ( $key , $value ) ;
}
public function sRem ( $key , $value ) {
return $this - > redis - > sRem ( $key , $value ) ;
}
public function sMembers ( $key ) {
return $this - > redis - > sMembers ( $key ) ;
}
public function __call ( $name , $arguments ) {
if ( count ( $arguments ) != 2 ) {
return '' ;
}
return $this - > redis - > name ( $arguments [ 0 ] , $arguments [ 1 ] ) ;
}
public function onOpen ( $ws , $request ) {
\app\ common\ lib\ redis\ Predis : : getInstance ( ) - > sAdd ( config ( 'redis.live_game_key' ) , $request - > fd ) ;
var_dump ( $request - > fd ) ;
}
public function onClose ( $ws , $fd ) {
\app\ common\ lib\ redis\ Predis : : getInstance ( ) - > sRem ( config ( 'redis.live_game_key' ) , $fd ) ;
echo "closed - clientId: { $fd } \n" ;
}
修改 application/admin/controller/Live.php
public function push ( ) {
print_r ( $_GET ) ;
$clients = Predis: : getInstance ( ) - > sMembers ( config ( 'redis.live_game_key' ) ) ;
foreach ( $clients as $fd ) {
$_POST [ 'http_server' ] - > push ( $fd , 'hello-push-data-' . $fd ) ;
}
}
5. 赛事直播功能逻辑开发。
完善推送接口:application/admin/controller/Live.php
<?php
namespace app\ admin\ controller ;
use app\ common\ lib\ Util ;
use app\ common\ lib\ redis\ Predis ;
class Live {
public function push ( ) {
if ( empty ( $_GET ) ) {
return Util: : show ( config ( 'code.error' ) , 'error' ) ;
}
$teams = [
1 = > [ 'name' = > '马刺' , 'logo' = > './imgs/team1.png' ] ,
4 = > [ 'name' = > '火箭' , 'logo' = > './imgs/team2.png' ]
] ;
$data = [
'type' = > intval ( $_GET [ 'type' ] ) ,
'title' = > ! empty ( $teams [ $_GET [ 'team_id' ] ] [ 'name' ] ) ? $teams [ $_GET [ 'team_id' ] ] [ 'name' ] : '直播员' ,
'logo' = > ! empty ( $teams [ $_GET [ 'team_id' ] ] [ 'logo' ] ) ? $teams [ $_GET [ 'team_id' ] ] [ 'logo' ] : '' ,
'content' = > ! empty ( $_GET [ 'content' ] ) ? $_GET [ 'content' ] : '' ,
'image' = > ! empty ( $_GET [ 'image' ] ) ? $_GET [ 'image' ] : ''
] ;
$taskData = [
'method' = > 'pushLive' ,
'data' = > $data
] ;
$_POST [ 'http_server' ] - > task ( $taskData ) ;
return Util: : show ( config ( 'code.success' ) , $code ) ;
}
}
修改:public/static/live/js/live.js
var wsUrl = "ws://192.168.2.214:8811" ;
var websocket = new WebSocket ( wsUrl) ;
websocket. onopen = function ( evt) {
console. log ( "connected-swoole-success" ) ;
}
websocket. onmessage = function ( evt) {
console. log ( "ws-server-return-data:" + evt. data) ;
push ( evt. data)
}
websocket. onclose = function ( evt) {
console. log ( "close" ) ;
}
websocket. onerror = function ( evt, e) {
console. log ( "error" + evt. data) ;
}
function push ( data) {
data = JSON . parse ( data) ;
html = '<div class="frame">' ;
html += '<h3 class="frame-header">' ;
html += '<i class="icon iconfont icon-shijian"></i>第' + data. type+ '节 01:30' ;
html += '</h3>' ;
html += '<div class="frame-item">' ;
html += '<span class="frame-dot"></span>' ;
html += '<div class="frame-item-author">' ;
if ( data. logo) {
html += '<img src="' + data. logo+ '" width="20px" height="20px" />' ;
}
html += data. title;
html += '</div>' ;
html += '<p>' + data. content+ '</p>' ;
html += '</div>' ;
$ ( '#match-result' ) . prepend ( html) ;
}
public function onTask ( $serv , $task_id , $workerId , $data ) {
$obj = new app\ common\ lib\ task\ Task ;
$method = $data [ 'method' ] ;
$flag = $obj - > $method ( $data [ 'data' ] , $serv ) ;
return $flag ;
}
修改 Task 类:application/common/lib/task/Task.php
<?php
namespace app\ common\ lib\ task ;
use app\ common\ lib\ ali\ Sms ;
use app\ common\ lib\ Redis ;
use app\ common\ lib\ redis\ Predis ;
class Task {
public function sendSms ( $data , $serv ) {
print_R ( $data ) ;
try {
Predis: : getInstance ( ) - > set ( Redis: : smsKey ( $data [ 'phone' ] ) , $data [ 'code' ] , config ( 'redis.out_time' ) ) ;
} catch ( \ Exception $e ) {
echo $e - > getMessage ( ) ;
}
}
public function pushLive ( $data , $serv ) {
$clients = Predis: : getInstance ( ) - > sMembers ( config ( "redis.live_game_key" ) ) ;
foreach ( $clients as $fd ) {
$a = $serv - > connection_info ( $fd ) ;
if ( $a && isset ( $a [ "websocket_status" ] ) && intval ( $a [ "websocket_status" ] ) > 0 ) {
$serv - > push ( $fd , json_encode ( $data ) ) ;
}
}
}
}