This commit is contained in:
tao 2022-08-16 15:19:21 +08:00
parent 7b24ce1097
commit 6cf7c3c589
82 changed files with 15463 additions and 260 deletions

View File

@ -13,6 +13,8 @@ use think\facade\Request;
use think\facade\Lang;
use think\facade\View;
use think\facade\Route;
use taoser\SetArr;
use app\common\lib\Uploads;
/**
* 控制器基础类
@ -250,11 +252,12 @@ abstract class BaseController
$indexUrl = $this->getIndexUrl();
if(config('taoler.url_rewrite.article_as') == '<ename>/'){
// 分类可变路由
$artUrl = (string) Route::buildUrl('article_detail', ['id' => $aid, 'ename'=> $ename]);
$artUrl = (string) url('article_detail', ['id' => (int) $aid, 'ename'=> $ename]);
//$artUrl = (string) Route::buildUrl('article_detail', ['id' => $aid, 'ename'=> $ename]);
} else {
$artUrl = (string) url('article_detail', ['id' => $aid]);
}
//dump($artUrl);
// 判断是否开启绑定
//$domain_bind = array_key_exists('domain_bind',config('app'));
@ -288,6 +291,191 @@ abstract class BaseController
return $url;
}
/**
* 关键词
*
* @return void
*/
public function setTags($data)
{
$tags = [];
if($data['flag'] == 'on') {
// 百度分词自动生成关键词
if(!empty(config('taoler.baidu.client_id')) == true) {
$url = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer?charset=UTF-8&access_token='.config('taoler.baidu.access_token');
//headers数组内的格式
$headers = array();
$headers[] = "Content-Type:application/json";
$body = array(
"text" => $data['tags']
);
$postBody = json_encode($body);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);//设置请求头
curl_setopt($curl, CURLOPT_POSTFIELDS, $postBody);//设置请求体
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');//使用一个自定义的请求信息来代替"GET"或"HEAD"作为HTTP请求。(这个加不加没啥影响)
$datas = curl_exec($curl);
if($datas == false) {
echo '接口无法链接';
} else {
$res = stripos($datas,'error_code');
// 接收返回的数据
$dataItem = json_decode($datas);
if($res == false) {
// 数据正常
$items = $dataItem->items;
foreach($items as $item) {
if($item->pos == 'n' && !in_array($item->item,$tags)){
$tags[] = $item->item;
}
}
} else {
// 接口正常但获取数据失败可能参数错误重新获取token
$url = 'https://aip.baidubce.com/oauth/2.0/token';
$post_data['grant_type'] = config('taoler.baidu.grant_type');;
$post_data['client_id'] = config('taoler.baidu.client_id');
$post_data['client_secret'] = config('taoler.baidu.client_secret');
$o = "";
foreach ( $post_data as $k => $v )
{
$o.= "$k=" . urlencode( $v ). "&" ;
}
$post_data = substr($o,0,-1);
$res = $this->request_post($url, $post_data);
// 写入token
SetArr::name('taoler')->edit([
'baidu'=> [
'access_token' => json_decode($res)->access_token,
]
]);
echo 'api接口数据错误 - ';
echo $dataItem->error_msg;
}
}
}
} else {
// 手动添加关键词
// 中文一个或者多个空格转换为英文空格
$str = preg_replace('/\s+/',' ',$data['tags']);
$att = explode(' ', $str);
foreach($att as $v){
if ($v !='') {
$tags[] = $v;
}
}
}
return json(['code'=>0,'data'=>$tags]);
}
// api_post接口
function request_post($url = '', $param = '')
{
if (empty($url) || empty($param)) {
return false;
}
$postUrl = $url;
$curlPost = $param;
$curl = curl_init();//初始化curl
curl_setopt($curl, CURLOPT_URL,$postUrl);//抓取指定网页
curl_setopt($curl, CURLOPT_HEADER, 0);//设置header
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);//要求结果为字符串且输出到屏幕上
curl_setopt($curl, CURLOPT_POST, 1);//post提交方式
curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
$data = curl_exec($curl);//运行curl
curl_close($curl);
return $data;
}
/**
* 标题调用百度关键词词条
*
* @return void
*/
public function getBdiduSearchWordList($words)
{
if(empty($words)) return json(['code'=>-1,'msg'=>'null']);
$url = 'https://www.baidu.com/sugrec?prod=pc&from=pc_web&wd='.$words;
//$result = Api::urlGet($url);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$datas = curl_exec($curl);
curl_close($curl);
$data = json_decode($datas,true);
if(isset($data['g'])) {
return json(['code'=>0,'msg'=>'success','data'=>$data['g']]);
} else {
return json(['code'=>-1,'msg'=>'null']);
}
}
/**
* baidu push api
*
* @param string $link
* @return void
*/
protected function baiduPushUrl(string $link)
{
// baidu 接口
$api = config('taoler.baidu.push_api');
if(!empty($api)) {
$url[] = $link;
$ch = curl_init();
$options = array(
CURLOPT_URL => $api,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", $url),
CURLOPT_HTTPHEADER => array('Content-Type: text/plain'),
);
curl_setopt_array($ch, $options);
curl_exec($ch);
curl_close($ch);
}
}
/**
* 上传接口
*
* @return void
*/
public function uploadFiles($type)
{
$type = Request::param('type');
$uploads = new Uploads();
switch ($type){
case 'image':
$upRes = $uploads->put('file','article_pic',1024,'image');
break;
case 'zip':
$upRes = $uploads->put('file','article_zip',1024,'application|image');
break;
case 'video':
$upRes = $uploads->put('file','article_video',102400,'video|audio');
break;
case 'audio':
$upRes = $uploads->put('file','article_audio',102400,'audio');
break;
default:
$upRes = $uploads->put('file','article_file',1024,'image');
break;
}
return $upRes;
}
}

View File

@ -11,6 +11,7 @@ use think\facade\Request;
use think\facade\Db;
use think\facade\Cache;
use taoler\com\Files;
use app\common\lib\Msgres;
class Forum extends AdminController
{
@ -326,4 +327,188 @@ class Forum extends AdminController
}
return true;
}
/**
* 添加帖子文章
* @return string|\think\Response|\think\response\Json|void
*/
public function add()
{
if (Request::isAjax()) {
$data = Request::only(['cate_id', 'title', 'title_color', 'tiny_content', 'content', 'upzip', 'tags', 'description', 'captcha']);
$data['user_id'] = 1; //管理员ID
// 调用验证器
$validate = new \app\common\validate\Article;
$result = $validate->scene('Artadd')->check($data);
if (true !== $result) {
return Msgres::error($validate->getError());
}
// 获取内容图片音视频标识
$iva= $this->hasIva($data['content']);
$data = array_merge($data,$iva);
// 获取分类ename
$cate_ename = Db::name('cate')->where('id',$data['cate_id'])->value('ename');
$article = new Article();
$result = $article->add($data);
if ($result['code'] == 1) {
// 获取到的最新ID
$aid = Db::name('article')->max('id');
// 清除文章tag缓存
Cache::tag('tagArtDetail')->clear();
$link = $this->getRouteUrl((int)$aid, $cate_ename);
// 推送给百度收录接口
$this->baiduPushUrl($link);
$url = $result['data']['status'] ? $link : (string)url('index/');
$res = Msgres::success($result['msg'], $url);
} else {
$res = Msgres::error('add_error');
}
return $res;
}
//1.查询分类表获取所有分类
$cateList = Db::name('cate')->where(['status'=>1,'delete_time'=>0])->order('sort','asc')->cache('catename',3600)->select();
//2.将catelist变量赋给模板 公共模板nav.html
View::assign('cateList',$cateList);
return View::fetch('add');
}
/**
* 编辑文章
* @param $id
* @return string|\think\Response|\think\response\Json|void
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function edit($id)
{
$article = Article::find($id);
if(Request::isAjax()){
$data = Request::only(['id','cate_id','title','title_color','content','upzip','tags','description','captcha']);
$data['user_id'] = 1;
//调用验证器
$validate = new \app\common\validate\Article();
$res = $validate->scene('Artadd')->check($data);
if(true !== $res){
return Msgres::error($validate->getError());
} else {
//获取内容图片音视频标识
$iva= $this->hasIva($data['content']);
$data = array_merge($data,$iva);
$result = $article->edit($data);
if($result == 1) {
//删除原有缓存显示编辑后内容
Cache::delete('article_'.$id);
$link = $this->getRouteUrl((int) $id, $article->cate->ename);
// 推送给百度收录接口
$this->baiduPushUrl($link);
$editRes = Msgres::success('edit_success',$link);
} else {
$editRes = Msgres::error($result);
}
return $editRes;
}
}
// 查询标签
$tag = $article->tags;
$tags = [];
if(!is_null($tag)) {
$attr = explode(',',$tag);
foreach($attr as $key => $v){
if ($v !='') {
$tags[] = $v;
}
}
}
View::assign(['article'=>$article,'tags'=>$tags]);
//1.查询分类表获取所有分类
$cateList = Db::name('cate')->where(['status'=>1,'delete_time'=>0])->order('sort','asc')->cache('catename',3600)->select();
//2.将catelist变量赋给模板 公共模板nav.html
View::assign('cateList',$cateList);
return View::fetch();
}
/**
* 调用百度关键词
*
* @return void
*/
public function tagWord()
{
$data = Request::only(['tags','flag']);
return $this->setTags($data);
}
/**
* 标题调用百度关键词词条
*
* @return void
*/
public function getWordList()
{
$title = input('title');
return $this->getBdiduSearchWordList($title);
}
/**
* 内容中是否有图片视频音频插入
*
* @param [type] $content
* @return boolean
*/
public function hasIva($content)
{
//判断是否插入图片
$isHasImg = strpos($content,'img[');
$data['has_img'] = is_int($isHasImg) ? 1 : 0;
//判断是否插入视频
$isHasVideo = strpos($content,'video(');
$data['has_video'] = is_int($isHasVideo) ? 1 : 0;
//判断是否插入音频
$isHasAudio = strpos($content,'audio[');
$data['has_audio'] = is_int($isHasAudio) ? 1 : 0;
return $data;
}
/**
* 获取描述过滤html
*
* @return void
*/
public function getDescription()
{
$data = Request::only(['content']);
$description = getArtContent($data['content']);
return json(['code'=>0,'data'=>$description]);
}
/**
* 上传接口
*
* @return void
*/
public function uploads()
{
$type = Request::param('type');
return $this->uploadFiles($type);
}
}

View File

@ -2,10 +2,10 @@
/*
* @Author: TaoLer <alipay_tao@qq.com>
* @Date: 2021-12-06 16:04:50
* @LastEditTime: 2022-06-29 15:29:13
* @LastEditTime: 2022-07-29 17:17:41
* @LastEditors: TaoLer
* @Description: admin路由配置
* @FilePath: \TaoLer\app\admin\route\route.php
* @FilePath: \github\TaoLer\app\admin\route\route.php
* Copyright (c) 2020~2022 https://www.aieok.com All rights reserved.
*/
use think\facade\Route;

View File

@ -0,0 +1,291 @@
{extend name="public/base" /}
{block name="body"}
<div class="layui-form layui-form-pane" lay-filter="layuiadmin-form-addforum" id="layuiadmin-form-addforum">
<div class="layui-tab layui-tab-brief" lay-filter="user">
<div class="layui-tab-content" id="LAY_ucm" style="padding: 20px 0">
<div class="layui-tab-item layui-show">
<div class="layui-row layui-col-space15 layui-form-item">
<div class="layui-col-md3">
<label class="layui-form-label">{:lang('special column')}</label>
<div class="layui-input-block">
<select lay-verify="required" name="cate_id" lay-filter="column">
<option></option>
{volist name="cateList" id="cate"}
<option value="{$cate.id}" {if($Request.param.cate == $cate.ename)} selected {/if}>{:cookie('think_lang') == 'en-us' ? $cate.ename : $cate.catename}</option>
{/volist}
</select>
</div>
</div>
<div class="layui-col-md8">
<label for="L_title" class="layui-form-label">{:lang('title')}</label>
<div class="layui-input-block">
<input type="text" id="L_title" name="title" required lay-verify="required" autocomplete="off" class="layui-input" style="position:relative;" value=""/>
<input type="hidden" id="L_title_color" name="title_color" autocomplete="off" class="layui-input" />
<div class="layui-input bdsug layui-hide">
<ul class="wordlist">
</ul>
</div>
</div>
</div>
<div class="layui-col-md1">
<div id="color"></div>
</div>
</div>
<div class="layui-form-item layui-form-text">
<div class="layui-input-block">
<textarea
id="L_content"
name="content"
required
lay-verify="required"
placeholder="{:lang('please input the content')}"
class="layui-textarea fly-editor"
style="height: 260px">
</textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{:lang('enclosure')}</label>
<div class="layui-input-inline" style="width: 190px">
<input type="text" class="layui-input" name="upzip" value="" placeholder="zip,image文件" title="上传附件" />
</div>
<button type="button" class="layui-btn" id="zip-button"><i class="layui-icon"></i>{:lang('uploads')}</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{:lang('描述')}</label>
<div class="layui-input-block">
<textarea name="description" class="layui-textarea" placeholder="SEO描述"></textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{:lang('add tags')}</label>
<div class="layui-input-inline" style="width: 190px">
<input type="text" class="layui-input" name="tags" value="" placeholder="多个用空格隔开" title="添加标签" />
</div>
<button type="button" class="layui-btn" id="article-tags-button">{:lang('add')}</button>
</div>
</div>
<div class="layui-form-item">
<div class="layui-btn-container"></div>
</div>
<div class="layui-form-item layui-hide">
<button type="submit" class="layui-btn" lay-filter="article-add" lay-submit id="article-add">{:lang('post now')}</button>
</div>
</div>
</div>
</div>
</div>
{/block}
{block name="js"}
<script src="/static/jquery-3.6.0.min.js"></script>
<script src="/addons/taonyeditor/tinymce/tinymce.min.js"></script>
<script src="/addons/taonyeditor/tinymce/tinymce-jquery.min.js"></script>
<script>
//定义选择器
var mytextareaid = 'textarea#L_content';
var imagePrependUrl = "{$domain}";
//定义文件上传接口接口
var taonyUploadUrl = "{:url('forum/uploads')}",
taonyUploadImgage = "{:url('forum/uploads')}?type=image",
taonyUploadVideo = "{:url('forum/uploads')}?type=video";
taonyUploadZip = "{:url('forum/uploads')}?type=zip";
taonyUploadAudio = "{:url('forum/uploads')}?type=audio";
$(mytextareaid).removeClass();
</script>
<script src="/addons/taonyeditor/js/taonyeditor.js"></script>
<script>
layui.config({
base: '/static/admin/' //静态资源所在路径
}).extend({
index: 'lib/index' //主入口模块
}).use(["form", "colorpicker", "upload", 'editor'], function () {
var $ = layui.jquery,
form = layui.form,
colorpicker = layui.colorpicker,
upload = layui.upload;
var editor = layui.editor;
// 从详情页自动调用端口过滤,获取描述信息
tinymce.get('L_content').on('mouseleave', function() {
var content = tinymce.get('L_content').getContent({format: 'text'});
content = content.replace(/[\r\n]/g,"").replace(/\n/g, '').replace(/\s/g, '').replace(/\t/g, '');
if(content.length >200) {
content = content.substring(0,200);
}
// var test = tinymce.activeEditor.getContent({format: 'text'});
$('[name="description"]').val(content);
});
// 改变标题颜色
colorpicker.render({
elem: "#color",
color: "#393d49",
predefine: true, // 开启预定义颜色
done: function (color) {
//譬如你可以在回调中把得到的 color 赋值给表单
$("#L_title_color").val(color);
$("#L_title").css("color", color);
},
});
//上传附件
upload.render({
elem: "#zip-button",
url: "{:url('forum/uploads')}", //改成您自己的上传接口
data: { type: "zip" },
accept: "file", //普通文件
done: function (res) {
if (res.status == 0) {
$('input[name="upzip"]').val(res.url);
layer.msg("上传成功");
} else {
layer.msg(res.msg);
}
},
});
// 发布文章
// form.on("submit(article-add)", function (data) {
// var field = data.field;
// var numArr = new Array();
// $(".layui-btn-container").children("button").each(function () {
// numArr.push($(this).val()); //添加至数组
// });
// tags = numArr.lenth ? "" : numArr.join(",");
// var index = layer.load(1);
// $.ajax({
// type: "post",
// url: "{:url('Forum/add')}",
// data: field,
// dataType: "json",
// success: function (data) {
// if (data.code == 0) {
// layer.msg(data.msg, { icon: 6, time: 2000 }, function () {
// location.href = data.url;
// });
// } else {
// layer.open({ title: "发布失败", content: data.msg, icon: 5, anim: 6 });
// }
// layer.close(index);
// },
// });
// return false;
// });
// 手动添加tags
$("#article-tags-button").on("click", function () {
var tags = $("input[name='tags']").val();
var flag = "off";
getTags(tags, flag);
});
// 通过接口自动获取tag的内容
var conf = "{:empty(config('taoler.baidu.client_id'))}";
if (conf !== "1") {
$("#L_title").on("blur", function () {
var title = $(this).val();
var flag = "on";
// 清空上次生成的tag button
$(".layui-btn-container").children("button").each(function () {
$(this).remove();
});
getTags(title, flag);
});
}
// 百度词条
var baidu_title_switch = "{:config('taoler.config.baidu_title_switch')}";
if(baidu_title_switch == 1) {
$("#L_title").bind('input propertychange',function () {
var title = $(this).val();
var str = '';
if(title.length > 0 ) {
$.post("{:url('forum/getWordList')}",{title:title},function(res){
// 动态生成ur>li内容
if (res.code == 0) {
// 显示动态框
$(".bdsug").removeClass('layui-hide');
for (var i = 0; i < res.data.length; i++) {
//str += '<li data-key=' + res.data[i].q + '><b>' + res.data[i].q.replace(title,'') + '</b></li>';
str += '<li data-key=' + res.data[i].q + '><b>' + res.data[i].q + '</b></li>';
}
// 清空ul并追加li
$('.wordlist').empty().append(str);
// 点击李获取li值并复制给#L_title input的value
$(".bdsug li").on('click',function(){
var word = $(this).attr('data-key');
var words = title + '(' + word + ')';
$("#L_title").val(words);
// 关闭动态框
$(".bdsug").addClass('layui-hide');
});
} else {
$(".bdsug").addClass('layui-hide');
}
});
} else {
$(".bdsug").addClass('layui-hide');
}
});
}
// 添加关键词button
function getTags(tags, flag) {
if (tags == "") {
layer.msg("tag不能为空");
return false;
}
//把得到的tags放进数组
var numArr = new Array();
$(".layui-btn-container")
.children("button")
.each(function () {
numArr.push($(this).val()); //添加至数组
});
$.ajax({
type: "post",
url: "{:url('Forum/tagWord')}",
data: { tags: tags, flag: flag },
daType: "json",
success: function (data) {
if (data.code == 0) {
for (var i = 0; i < data.data.length; i++) {
if ($.inArray(data.data[i], numArr) < 0) {
$(".layui-btn-container").append(
'<button type="button" class="layui-btn layui-btn-sm layui-btn-danger" value=' + data.data[i] + ">" + data.data[i] + " x" + "</button>"
);
}
}
$("input[name='tags']").val("");
}
},
});
}
// 删除tag
$(document).ready(function () {
$(".layui-btn-container").on("click", "button", function () {
$(this).remove();
});
});
});
</script>
{/block}

View File

@ -0,0 +1,255 @@
{extend name="public/base" /}
{block name="body"}
<div class="layui-form layui-form-pane">
<div class="layui-tab layui-tab-brief" lay-filter="user">
<div class="layui-form layui-tab-content" id="LAY_ucm" style="padding: 20px 0;">
<div class="layui-tab-item layui-show">
<input type="hidden" name="id" value="{$article.id}">
<div class="layui-row layui-col-space15 layui-form-item">
<div class="layui-col-md3">
<label class="layui-form-label">{:lang('special column')}</label>
<div class="layui-input-block">
<select lay-verify="required" name="cate_id" lay-filter="column">
<option></option>
{volist name="cateList" id="cate"}
<option value="{$cate.id}" {if $article.cate_id == $cate.id}selected{/if}>{:cookie('think_lang') == 'en-us' ? $cate.ename : $cate.catename}</option>
{/volist}
</select>
</div>
</div>
<div class="layui-col-md8">
<label for="L_title" class="layui-form-label">{:lang('title')}</label>
<div class="layui-input-block">
<input type="text" id="L_title" name="title" required lay-verify="required" autocomplete="off" class="layui-input" value="{$article.title}">
<input type="hidden" id="L_title_color" name="title_color" autocomplete="off" class="layui-input" value="{$article.title_color ?? '#333'}">
<input type="hidden" name="user_id" value="{$article.user_id}">
</div>
</div>
<div class="layui-col-md1">
<div id="color"></div>
<div id="test9" style="margin-left: 30px;"></div>
</div>
</div>
<div class="layui-form-item layui-form-text">
<div class="layui-input-block">
<textarea id="L_content" name="content" required lay-verify="required" placeholder="详细内容" class="layui-textarea fly-editor" style="height: 260px;">{$article.content}</textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{:lang('enclosure')}</label>
<div class="layui-input-inline" style="width: 190px;">
<input type="text" class="layui-input" name="upzip" value="{$article.upzip}" placeholder="zip,jpg格式" title="上传附件"/>
</div>
<button type="button" class="layui-btn" id="zip-button"><i class="layui-icon"></i>上传文件</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{:lang('描述')}</label>
<div class="layui-input-block">
<textarea name="description" class="layui-textarea" placeholder="SEO描述">{$article.description}</textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">{:lang('add tags')}</label>
<div class="layui-input-inline" style="width: 190px;">
<input type="text" class="layui-input" name="tags" placeholder="多个用空格隔开" title="添加标签"/>
</div>
<button type="button" class="layui-btn" id="article-tags-button">{:lang('add')}</button>
</div>
</div>
<div class="layui-form-item">
<div class="layui-btn-container">
{volist name="tags" id="vo" }
<button type="button" class="layui-btn layui-btn-sm layui-btn-danger" value="{$vo}">{$vo} x</button>
{/volist}
</div>
</div>
<div class="layui-form-item layui-hide">
<button type="submit" class="layui-btn" lay-filter="article-edit" lay-submit id="article-edit">{:lang('post now')}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/block}
{block name="js"}
<script src="/static/jquery-3.6.0.min.js"></script>
<script src="/addons/taonyeditor/tinymce/tinymce.min.js"></script>
<script src="/addons/taonyeditor/tinymce/tinymce-jquery.min.js"></script>
<script>
//定义选择器
var mytextareaid = 'textarea#L_content';
var imagePrependUrl = "{domain}";
//定义文件上传接口接口
var taonyUploadUrl = "{:url('forum/uploads')}",
taonyUploadImgage = "{:url('forum/uploads')}?type=image",
taonyUploadVideo = "{:url('forum/uploads')}?type=video";
taonyUploadZip = "{:url('forum/uploads')}?type=zip";
taonyUploadAudio = "{:url('forum/uploads')}?type=audio";
$(mytextareaid).removeClass();
</script>
<script src="/addons/taonyeditor/js/taonyeditor.js"></script>
<script>
layui.use(['colorpicker','form','upload', 'editor'], function(){
var $ = layui.jquery
,colorpicker = layui.colorpicker
,form = layui.form
,upload = layui.upload;
var editor = layui.editor;
// 从详情页自动调用端口过滤,获取描述信息
tinymce.get('L_content').on('mouseleave', function() {
var content = tinymce.get('L_content').getContent({format: 'text'});
content = content.replace(/[\r\n]/g,"").replace(/\n/g, '').replace(/\s/g, '').replace(/\t/g, '');
if(content.length >200) {
content = content.substring(0,200);
}
// var test = tinymce.activeEditor.getContent({format: 'text'});
$('[name="description"]').val(content);
});
//预定义颜色项
colorpicker.render({
elem: '#color'
,color: "{$article.title_color ?? '#333'}"
,predefine: true // 开启预定义颜色
,done: function(color){
//譬如你可以在回调中把得到的 color 赋值给表单
$('#L_title_color').val(color);
//改变标题颜色
$('#L_title').css("color", color);
}
});
//指定允许上传的文件类型
upload.render({
elem: '#zip-button'
,url: "{:url('forum/uploads')}" //改成您自己的上传接口
,data: {type:'zip'}
,accept: 'file' //普通文件
,done: function(res){
if(res.status == 0){
$('input[name="upzip"]').val(res.url);
layer.msg('上传成功');
} else {
layer.msg(res.msg);
}
}
});
//编辑文章
form.on('submit(article-edit)', function(data){
var field = data.field;
var numArr = new Array();
$('.layui-btn-container').children('button').each(function(){
numArr.push($(this).val());//添加至数组
});
tags = numArr.lenth ? '' : numArr.join(',');
$.ajax({
type:"post",
url:"{:url('Forum/edit')}",
data: field,
daType:"json",
success:function (data){
if (data.code == 0) {
layer.msg(data.msg,{icon:6,time:2000}, function(){
location.href = data.url;
});
} else {
layer.open({title:'编辑失败',content:data.msg,icon:5,anim:6});
};
}
});
return false;
});
// 获取描述的内容
$("#L_content").bind('input propertychange', function(){
var content = $(this).val()
$.ajax({
type:"post",
url:"{:url('Forum/getDescription')}",
data:{"content":content},
daType:"json",
success:function (data){
if (data.code == 0) {
$('[name="description"]').val(data.data);
}
}
});
return false;
})
// 手动添加tags
$('#article-tags-button').on('click',function(){
var tags = $("input[name='tags']").val();
var flag = 'off';
getTags(tags,flag);
});
// 获取tag的内容
var conf = "{:empty(config('taoler.baidu.client_id'))}";
if(conf !== '1'){
$("#L_title").on('blur', function(){
var title = $(this).val();
var flag = 'on';
// 清空上次生成的tag button
$('.layui-btn-container').children('button').each(function(){
$(this).remove();
});
getTags(title,flag);
})
}
// 循环添加button
function getTags(tags,flag)
{
if(tags == ''){
layer.msg('不能为空');
return false;
}
//把得到的tags放进数组
var numArr = new Array();
$('.layui-btn-container').children('button').each(function(){
numArr.push($(this).val());//添加至数组
});
$.ajax({
type:"post",
url:"{:url('Forum/tagWord')}",
data:{"tags":tags,"flag":flag},
daType:"json",
success:function (data){
if (data.code == 0) {
for(var i=0; i<data.data.length; i++){
if($.inArray(data.data[i],numArr) < 0){
$('.layui-btn-container').append('<button type="button" class="layui-btn layui-btn-sm layui-btn-danger" value='+data.data[i]+'>'+data.data[i]+' x'+'</button>');
}
}
$("input[name='tags']").val("");
}
}
});
return false;
}
//删除tag
$(document).ready(function(){
$('.layui-btn-container').on('click','button',function(){
$(this).remove();
});
});
});
</script>
{/block}

View File

@ -47,7 +47,8 @@
<div class="layui-card-body">
<div style="padding-bottom: 10px;">
<button class="layui-btn layuiadmin-btn-forum-list" data-type="batchdel">删除</button>
<button class="layui-btn layuiadmin-btn-forum-list" data-type="add">添加</button>
<button class="layui-btn layuiadmin-btn-forum-list" data-type="batchdel">删除</button>
</div>
<table id="LAY-app-forum-list" lay-filter="LAY-app-forum-list" ></table>
<script type="text/html" id="avatarTpl">
@ -66,8 +67,8 @@
<input type="checkbox" name="status" value="{{d.id}}" lay-skin="switch" lay-filter="artStatus" lay-text="通过|{{ d.check == 0 ? '待审' : '禁止' }}" {{ d.check == 1 ? 'checked' : '' }}>
</script>
<script type="text/html" id="table-forum-list">
<a class="layui-btn layui-btn-xs" lay-event="edit" ><i class="layui-icon layui-icon-edit"></i></a>
{if condition="checkRuleButton('forum/listdel')"}
<!--a class="layui-btn layui-btn-disabled layui-btn-xs" lay-event="edit" ><i class="layui-icon layui-icon-edit"></i>编辑</a-->
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del"><i class="layui-icon layui-icon-delete"></i></a>
{else /}<a class="layui-btn layui-btn-danger layui-btn-xs layui-btn-disabled"><i class="layui-icon layui-icon-delete"></i></a>{/if}
</script>
@ -78,6 +79,7 @@
{/block}
{block name="js"}
<script>
var forumList = "{:url('Forum/list')}",
forumListdel = "{:url('Forum/listdel')}",
@ -88,6 +90,7 @@ var forumList = "{:url('Forum/list')}",
forumTags = "{:url('Forum/tags')}",
forumTagsDelete = "{:url('Forum/tagsdelete')}",
forumTagsForm = "{:url('Forum/tagsform')}";
forumEdit = "{:url('Forum/edit')}";
layui.config({
base: '/static/admin/' //静态资源所在路径
}).extend({
@ -173,12 +176,66 @@ layui.config({
layer.msg('已删除');
});
}
,add: function(){
layer.open({
type: 2
,title: '添加'
,content: 'add.html'
,maxmin: true
,area: ['100%', '100%']
,btn: ['确定', '取消']
,yes: function(index, layero){
var iframeWindow = window['layui-layer-iframe'+ index]
,submitID = 'article-add'
,submit = layero.find('iframe').contents().find('#'+ submitID);
//监听提交
iframeWindow.layui.form.on('submit('+ submitID +')', function(data){
var field = data.field; //获取提交的字段
//提交 Ajax 成功后,静态更新表格中的数据
$.ajax({
type:"post",
url:"{:url('Forum/add')}",
data: field,
daType:"json",
success:function (data){
if (data.code == 0) {
layer.msg(data.msg,{
icon:6,
time:2000
});
} else {
layer.open({
tiele:'添加失败',
content:data.msg,
icon:5,
anim:6
});
}
}
});
table.reload('LAY-app-forum-list'); //数据刷新
layer.close(index); //关闭弹层
});
submit.trigger('click');
}
});
}
}
$('.layui-btn.layuiadmin-btn-forum-list').on('click', function(){
var type = $(this).data('type');
active[type] ? active[type].call(this) : '';
});
// $(document).on('focusin', function(e) {
// if ($(e.target).closest(".tox-tinymce, .tox-tinymce-aux, .moxman-window, .tam-assetmanager-root").length) {
// e.stopImmediatePropagation();
// }
// });
});
</script>
{/block}

View File

@ -178,26 +178,26 @@
//提交 Ajax 成功后,静态更新表格中的数据
$.ajax({
type:"post",
url:"{:url('User/userform')}",
data:{"name":field.username,"phone":field.phone,"email":field.email,"user_img":field.avatar,"sex":field.sex},
daType:"json",
success:function (data){
if (data.code == 0) {
layer.msg(data.msg,{
icon:6,
time:2000
});
} else {
layer.open({
tiele:'添加失败',
content:data.msg,
icon:5,
anim:6
});
}
}
});
type:"post",
url:"{:url('User/userform')}",
data:{"name":field.username,"phone":field.phone,"email":field.email,"user_img":field.avatar,"sex":field.sex},
daType:"json",
success:function (data){
if (data.code == 0) {
layer.msg(data.msg,{
icon:6,
time:2000
});
} else {
layer.open({
tiele:'添加失败',
content:data.msg,
icon:5,
anim:6
});
}
}
});
table.reload('LAY-user-manage'); //数据刷新
layer.close(index); //关闭弹层
});

View File

@ -269,7 +269,7 @@ class Article extends BaseController
$link = $this->getRouteUrl((int)$aid, $cate_ename);
// 推送给百度收录接口
$this->baiduPush($link);
$this->baiduPushUrl($link);
$url = $result['data']['status'] ? $link : (string)url('index/');
$res = Msgres::success($result['msg'], $url);
@ -328,7 +328,7 @@ class Article extends BaseController
Cache::delete('article_'.$id);
$link = $this->getRouteUrl((int) $id, $article->cate->ename);
// 推送给百度收录接口
$this->baiduPush($link);
$this->baiduPushUrl($link);
$editRes = Msgres::success('edit_success',$link);
} else {
$editRes = Msgres::error($result);
@ -386,25 +386,7 @@ class Article extends BaseController
public function uploads()
{
$type = Request::param('type');
$uploads = new Uploads();
switch ($type){
case 'image':
$upRes = $uploads->put('file','article_pic',1024,'image');
break;
case 'zip':
$upRes = $uploads->put('file','article_zip',1024,'application|image');
break;
case 'video':
$upRes = $uploads->put('file','article_video',102400,'video|audio');
break;
case 'audio':
$upRes = $uploads->put('file','article_audio',102400,'audio');
break;
default:
$upRes = $uploads->put('file','article_file',1024,'image');
break;
}
return $upRes;
return $this->uploadFiles($type);
}
/**
@ -443,23 +425,7 @@ class Article extends BaseController
public function getWordList()
{
$title = input('title');
if(empty($title)) return json(['code'=>-1,'msg'=>'null']);
$url = 'https://www.baidu.com/sugrec?prod=pc&from=pc_web&wd='.$title;
//$result = Api::urlGet($url);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$datas = curl_exec($curl);
curl_close($curl);
$data = json_decode($datas,true);
if(isset($data['g'])) {
return json(['code'=>0,'msg'=>'success','data'=>$data['g']]);
} else {
return json(['code'=>-1,'msg'=>'null']);
}
return $this->getBdiduSearchWordList($title);
}
/**
@ -470,100 +436,7 @@ class Article extends BaseController
public function tags()
{
$data = Request::only(['tags','flag']);
$tags = [];
if($data['flag'] == 'on') {
// 百度分词自动生成关键词
if(!empty(config('taoler.baidu.client_id')) == true) {
$url = 'https://aip.baidubce.com/rpc/2.0/nlp/v1/lexer?charset=UTF-8&access_token='.config('taoler.baidu.access_token');
//headers数组内的格式
$headers = array();
$headers[] = "Content-Type:application/json";
$body = array(
"text" => $data['tags']
);
$postBody = json_encode($body);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);//设置请求头
curl_setopt($curl, CURLOPT_POSTFIELDS, $postBody);//设置请求体
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');//使用一个自定义的请求信息来代替"GET"或"HEAD"作为HTTP请求。(这个加不加没啥影响)
$datas = curl_exec($curl);
if($datas == false) {
echo '接口无法链接';
} else {
$res = stripos($datas,'error_code');
// 接收返回的数据
$dataItem = json_decode($datas);
if($res == false) {
// 数据正常
$items = $dataItem->items;
foreach($items as $item) {
if($item->pos == 'n' && !in_array($item->item,$tags)){
$tags[] = $item->item;
}
}
} else {
// 接口正常但获取数据失败可能参数错误重新获取token
$url = 'https://aip.baidubce.com/oauth/2.0/token';
$post_data['grant_type'] = config('taoler.baidu.grant_type');;
$post_data['client_id'] = config('taoler.baidu.client_id');
$post_data['client_secret'] = config('taoler.baidu.client_secret');
$o = "";
foreach ( $post_data as $k => $v )
{
$o.= "$k=" . urlencode( $v ). "&" ;
}
$post_data = substr($o,0,-1);
$res = $this->request_post($url, $post_data);
// 写入token
SetArr::name('taoler')->edit([
'baidu'=> [
'access_token' => json_decode($res)->access_token,
]
]);
echo 'api接口数据错误 - ';
echo $dataItem->error_msg;
}
}
}
} else {
// 手动添加关键词
// 中文一个或者多个空格转换为英文空格
$str = preg_replace('/\s+/',' ',$data['tags']);
$att = explode(' ', $str);
foreach($att as $v){
if ($v !='') {
$tags[] = $v;
}
}
}
return json(['code'=>0,'data'=>$tags]);
}
// api_post接口
function request_post($url = '', $param = '')
{
if (empty($url) || empty($param)) {
return false;
}
$postUrl = $url;
$curlPost = $param;
$curl = curl_init();//初始化curl
curl_setopt($curl, CURLOPT_URL,$postUrl);//抓取指定网页
curl_setopt($curl, CURLOPT_HEADER, 0);//设置header
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);//要求结果为字符串且输出到屏幕上
curl_setopt($curl, CURLOPT_POST, 1);//post提交方式
curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
$data = curl_exec($curl);//运行curl
curl_close($curl);
return $data;
return $this->setTags($data);
}
// 文章置顶、加精、评论状态
@ -666,33 +539,6 @@ class Article extends BaseController
return $content;
}
/**
* baidu push api
*
* @param string $link
* @return void
*/
protected function baiduPush(string $link)
{
// baidu 接口
$api = config('taoler.baidu.push_api');
if(!empty($api)) {
$url[] = $link;
$ch = curl_init();
$options = array(
CURLOPT_URL => $api,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", $url),
CURLOPT_HTTPHEADER => array('Content-Type: text/plain'),
);
curl_setopt_array($ch, $options);
curl_exec($ch);
curl_close($ch);
}
}
public function userZanArticle()
{
//

View File

@ -2,7 +2,7 @@
/*
* @Author: TaoLer <alipey_tao@qq.com>
* @Date: 2021-12-06 16:04:50
* @LastEditTime: 2022-07-26 13:58:17
* @LastEditTime: 2022-07-30 08:58:51
* @LastEditors: TaoLer
* @Description: 前端路由设置
* @FilePath: \github\TaoLer\app\index\route\route.php
@ -18,16 +18,16 @@ $cate_as = config('taoler.url_rewrite.cate_as');
Route::get('captcha/[:config]','\\think\\captcha\\CaptchaController@index');
Route::rule('/', 'index'); // 首页访问路由
Route::get('index/reply','index/reply')->name('user_reply');
Route::get('index/reply$','index/reply')->name('user_reply');
Route::rule('search','Search/getSearch')->name('user_search');
Route::get('message/nums','message/nums')->name('user_message');
Route::get('message/nums$','message/nums')->name('user_message');
Route::get('tag/:tag', 'Tag/list')->name('tag_list');
// 用户中心
Route::group(function () {
Route::get('u/:id$', 'user/home')->name('user_home');
Route::get('user/index', 'user/index');
Route::get('user/set', 'user/set');
Route::get('user/message', 'user/message');
Route::get('user/message$', 'user/message');
Route::get('user/post', 'user/post');
Route::get('user/article','user/artList');
Route::get('user/coll','user/collList');
@ -41,11 +41,11 @@ Route::group(function () {
// 登录注册
Route::group(function () {
Route::rule('login','login/index')->name('user_login');
Route::rule('forget','login/forget')->name('user_forget');
Route::rule('postcode','login/postcode');
Route::rule('sentemailcode','login/sentMailCode');
Route::rule('respass','login/respass');
Route::rule('login$','login/index')->name('user_login');
Route::rule('forget$','login/forget')->name('user_forget');
Route::rule('postcode$','login/postcode');
Route::rule('sentemailcode$','login/sentMailCode');
Route::rule('respass$','login/respass');
Route::rule('reg$','Login/reg')->name('user_reg')
->middleware(\app\middleware\CheckRegister::class);
});
@ -53,16 +53,18 @@ Route::group(function () {
// article
Route::group(function () use($detail_as,$cate_as){
Route::group('art',function () use($detail_as,$cate_as){
Route::rule('add/[:cate]','Article/add')->name('add_article');
Route::rule('delete/[:id]','Article/delete');
Route::rule('tags','Article/tags')->allowCrossDomain();
Route::rule('edit/[:id]','Article/edit');
});
Route::group(function () use($detail_as,$cate_as){
// 动态路径路由会影响下面的路由,所以动态路由放下面
Route::get($detail_as . ':id$', 'article/detail')->name('article_detail');
Route::get($cate_as . '<ename>$','article/cate')->name('cate');
Route::get($cate_as . '<ename>/<type>$', 'article/cate')->name('cate_type');
Route::get($cate_as . '<ename>/<type>/<page>', 'article/cate')->name('cate_page');
Route::get($cate_as . '<ename>/<type>/<page>$', 'article/cate')->name('cate_page');
})->pattern([
'ename' => '\w+',
'type' => '\w+',

View File

@ -36,7 +36,8 @@
"overtrue/pinyin": "^4.0",
"yansongda/pay": "~3.1.0",
"guzzlehttp/guzzle": "7.0",
"php-di/php-di": "^6.4"
"php-di/php-di": "^6.4",
"workerman/phpsocket.io": "^1.1"
},
"require-dev": {
"symfony/var-dumper": "^4.2",

84
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fe8d6bf6748143aa416b16c9a3f5db98",
"content-hash": "8e024ab98b5bfe9d89eb50e675b21b20",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -2259,6 +2259,88 @@
},
"time": "2020-02-15T13:04:16+00:00"
},
{
"name": "workerman/channel",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/walkor/channel.git",
"reference": "3df772d0d20d4cebfcfd621c33d1a1ab732db523"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/channel/zipball/3df772d0d20d4cebfcfd621c33d1a1ab732db523",
"reference": "3df772d0d20d4cebfcfd621c33d1a1ab732db523",
"shasum": ""
},
"require": {
"workerman/workerman": ">=4.0.12"
},
"type": "library",
"autoload": {
"psr-4": {
"Channel\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"support": {
"issues": "https://github.com/walkor/channel/issues",
"source": "https://github.com/walkor/channel/tree/v1.1.0"
},
"time": "2021-02-08T02:45:42+00:00"
},
{
"name": "workerman/phpsocket.io",
"version": "v1.1.14",
"source": {
"type": "git",
"url": "https://github.com/walkor/phpsocket.io.git",
"reference": "a5758da4d55b4744a4cc9c956816d88ce385601e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/phpsocket.io/zipball/a5758da4d55b4744a4cc9c956816d88ce385601e",
"reference": "a5758da4d55b4744a4cc9c956816d88ce385601e",
"shasum": ""
},
"require": {
"workerman/channel": ">=1.0.0",
"workerman/workerman": ">=3.5.16"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPSocketIO\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"Socket.io"
],
"support": {
"issues": "https://github.com/walkor/phpsocket.io/issues",
"source": "https://github.com/walkor/phpsocket.io/tree/v1.1.14"
},
"funding": [
{
"url": "https://opencollective.com/walkor",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/walkor",
"type": "patreon"
}
],
"time": "2022-02-24T03:33:45+00:00"
},
{
"name": "workerman/workerman",
"version": "v4.0.41",

View File

@ -16,7 +16,7 @@ return [
// 应用名,此项不可更改
'appname' => 'TaoLer',
// 版本配置
'version' => '1.9.20',
'version' => '1.9.21',
// 加盐
'salt' => 'taoler',
// 数据库备份目录

View File

@ -1,3 +1,12 @@
/*
* @Author: TaoLer <317927823@qq.com>
* @Date: 2021-12-06 16:04:50
* @LastEditTime: 2022-07-29 16:22:20
* @LastEditors: TaoLer
* @Description: 优化版
* @FilePath: \github\TaoLer\public\static\admin\lib\index.js
* Copyright (c) 2020~2022 https://www.aieok.com All rights reserved.
*/
/**
@NamelayuiAdmin iframe版主入口
@ -11,13 +20,14 @@ layui.extend({
setter: 'config' //配置模块
,admin: 'lib/admin' //核心模块
,view: 'lib/view' //视图渲染模块
,editor: '{/}/addons/taonyeditor/js/taonyeditor'
}).define(['setter', 'admin'], function(exports){
var setter = layui.setter
,element = layui.element
,admin = layui.admin
,tabsPage = admin.tabsPage
,view = layui.view
,editor = layui.editor
//打开标签页
,openTabsPage = function(url, text){
//遍历页签选项卡

View File

@ -20,7 +20,7 @@ var forms = table.render({
,{field: 'hot', title: '加精', templet: '#buttonHot', width: 80, align: 'center'}
,{field: 'reply', title: '禁评', templet: '#buttonReply', width: 80, align: 'center'}
,{field: 'check', title: '审帖', templet: '#buttonCheck', width: 95, align: 'center'}
,{title: '操作', width: 60, align: 'center', toolbar: '#table-forum-list'}
,{title: '操作', width: 110, align: 'center', toolbar: '#table-forum-list'}
]]
,page: true
,limit: 15
@ -65,45 +65,30 @@ var forms = table.render({
layer.open({
type: 2
,title: '编辑帖子'
,content: '/admin/Forum/listform?id='+ data.id
,area: ['550px', '400px']
,content: forumEdit + '?id='+ data.id
,area: ['100%', '100%']
,btn: ['确定', '取消']
,resize: false
,yes: function(index, layero){
var iframeWindow = window['layui-layer-iframe'+ index]
,submitID = 'LAY-app-forum-submit'
,submit = layero.find('iframe').contents().find("#layuiadmin-form-list")
,poster = submit.find('input[name="poster"]').val()
,content = submit.find('input[name="content"]').val()
,avatar = submit.find('input[name="avatar"]').val();
,submitID = 'article-edit'
,submit = layero.find('iframe').contents().find('#'+ submitID)
//监听提交
iframeWindow.layui.form.on('submit('+ submitID +')', function(data){
var field = data.field; //获取提交的字段
//提交 Ajax 成功后,静态更新表格中的数据
//$.ajax({});
$.ajax({
type:"post",
url:"/admin/Forum/listform",
data:{"id":data.id,"poster":name,"sort":sort,"ename":ename},
url: forumEdit,
data: field,
daType:"json",
success:function (data){
if (data.code == 0) {
layer.msg(data.msg,{
icon:6,
time:2000
}, function(){
location.reload();
});
layer.msg(data.msg,{icon:6,time:2000});
} else {
layer.open({
tiele:'修改失败',
content:data.msg,
icon:5,
anim:6
});
}
layer.open({title:'编辑失败',content:data.msg,icon:5,anim:6});
};
}
});
@ -113,9 +98,6 @@ var forms = table.render({
submit.trigger('click');
}
,success: function(layero, index){
}
});
}
});
@ -264,8 +246,8 @@ var forms = table.render({
,sort = othis.find('input[name="sort"]').val()
,tags = othis.find('input[name="tags"]').val()
,ename = othis.find('input[name="ename"]').val()
,detpl = othis.find('select[name="detpl"]').val()
,icon = othis.find('input[name="icon"]').val()
,detpl = othis.find('select[name="detpl"]').val()
,icon = othis.find('input[name="icon"]').val()
,desc = othis.find('input[name="desc"]').val();
if(!tags.replace(/\s/g, '')) return;

View File

@ -539,3 +539,8 @@ html{background-color: #f2f2f2; color: #666;}
.xm-body .xm-option.hide-icon.selected {
background-color: #1890ff !important;
}
/* 发文章标题下百度词条 */
.bdsug{height: auto; position: absolute; left: 0; top: 30px; z-index: 100; background: #fff; border-radius: 0 0 10px 10px; border: 1px solid #dadade!important; border-top: 0!important; box-shadow: none;}
.bdsug ul{display: block;margin: 5px 2px 0; padding: 5px 0 7px; background: 0 0; border-top: 0px solid #f5f5f6;}
.bdsug ul>li{margin-top: 0;height:30px;line-height: 25px;}

View File

@ -12,7 +12,6 @@ i{font-style: normal;}
/* 布局 */
.layui-mm{position: fixed; top: 100px; bottom: 0;}
.site-menu,
.site-menu{box-shadow: none; border-left: none;}
/* 文档 */

View File

@ -995,6 +995,7 @@ layui.define(['layer', 'laytpl', 'form', 'element', 'upload', 'util', 'imgcom'],
util.fixbar({
bar1: '&#xe642;'
,bgcolor: '#009688'
,css: {right: 10, bottom: 100}
,click: function(type){
//添加文章
if(type === 'bar1'){

View File

@ -35,6 +35,7 @@ return array(
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpDocReader\\' => array($vendorDir . '/php-di/phpdoc-reader/src/PhpDocReader'),
'Phinx\\' => array($vendorDir . '/topthink/think-migration/phinx/src/Phinx'),
'PHPSocketIO\\' => array($vendorDir . '/workerman/phpsocket.io/src'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'Overtrue\\Pinyin\\' => array($vendorDir . '/overtrue/pinyin/src'),
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
@ -49,5 +50,6 @@ return array(
'Endroid\\QrCode\\' => array($vendorDir . '/endroid/qr-code/src'),
'DI\\' => array($vendorDir . '/php-di/php-di/src'),
'DASPRiD\\Enum\\' => array($vendorDir . '/dasprid/enum/src'),
'Channel\\' => array($vendorDir . '/workerman/channel/src'),
'BaconQrCode\\' => array($vendorDir . '/bacon/bacon-qr-code/src'),
);

View File

@ -82,6 +82,7 @@ class ComposerStaticInit1b32198725235c8d6500c87262ef30c2
'Psr\\Cache\\' => 10,
'PhpDocReader\\' => 13,
'Phinx\\' => 6,
'PHPSocketIO\\' => 12,
'PHPMailer\\PHPMailer\\' => 20,
),
'O' =>
@ -118,6 +119,10 @@ class ComposerStaticInit1b32198725235c8d6500c87262ef30c2
'DI\\' => 3,
'DASPRiD\\Enum\\' => 13,
),
'C' =>
array (
'Channel\\' => 8,
),
'B' =>
array (
'BaconQrCode\\' => 12,
@ -245,6 +250,10 @@ class ComposerStaticInit1b32198725235c8d6500c87262ef30c2
array (
0 => __DIR__ . '/..' . '/topthink/think-migration/phinx/src/Phinx',
),
'PHPSocketIO\\' =>
array (
0 => __DIR__ . '/..' . '/workerman/phpsocket.io/src',
),
'PHPMailer\\PHPMailer\\' =>
array (
0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
@ -301,6 +310,10 @@ class ComposerStaticInit1b32198725235c8d6500c87262ef30c2
array (
0 => __DIR__ . '/..' . '/dasprid/enum/src',
),
'Channel\\' =>
array (
0 => __DIR__ . '/..' . '/workerman/channel/src',
),
'BaconQrCode\\' =>
array (
0 => __DIR__ . '/..' . '/bacon/bacon-qr-code/src',

View File

@ -2849,6 +2849,94 @@
},
"install-path": "../wamkj/thinkphp6.0-databackup"
},
{
"name": "workerman/channel",
"version": "v1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/walkor/channel.git",
"reference": "3df772d0d20d4cebfcfd621c33d1a1ab732db523"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/channel/zipball/3df772d0d20d4cebfcfd621c33d1a1ab732db523",
"reference": "3df772d0d20d4cebfcfd621c33d1a1ab732db523",
"shasum": ""
},
"require": {
"workerman/workerman": ">=4.0.12"
},
"time": "2021-02-08T02:45:42+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Channel\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"support": {
"issues": "https://github.com/walkor/channel/issues",
"source": "https://github.com/walkor/channel/tree/v1.1.0"
},
"install-path": "../workerman/channel"
},
{
"name": "workerman/phpsocket.io",
"version": "v1.1.14",
"version_normalized": "1.1.14.0",
"source": {
"type": "git",
"url": "https://github.com/walkor/phpsocket.io.git",
"reference": "a5758da4d55b4744a4cc9c956816d88ce385601e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/phpsocket.io/zipball/a5758da4d55b4744a4cc9c956816d88ce385601e",
"reference": "a5758da4d55b4744a4cc9c956816d88ce385601e",
"shasum": ""
},
"require": {
"workerman/channel": ">=1.0.0",
"workerman/workerman": ">=3.5.16"
},
"time": "2022-02-24T03:33:45+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"PHPSocketIO\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"Socket.io"
],
"support": {
"issues": "https://github.com/walkor/phpsocket.io/issues",
"source": "https://github.com/walkor/phpsocket.io/tree/v1.1.14"
},
"funding": [
{
"url": "https://opencollective.com/walkor",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/walkor",
"type": "patreon"
}
],
"install-path": "../workerman/phpsocket.io"
},
{
"name": "workerman/workerman",
"version": "v4.0.41",

View File

@ -3,7 +3,7 @@
'name' => 'taoser/taoler',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '13f9cb05575d0a81b1392ce7fc500a595ca7290a',
'reference' => '9c56fe07e9c108768ac6d8a254dc3e838186eab1',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -301,7 +301,7 @@
'taoser/taoler' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '13f9cb05575d0a81b1392ce7fc500a595ca7290a',
'reference' => '9c56fe07e9c108768ac6d8a254dc3e838186eab1',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -433,6 +433,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
'workerman/channel' => array(
'pretty_version' => 'v1.1.0',
'version' => '1.1.0.0',
'reference' => '3df772d0d20d4cebfcfd621c33d1a1ab732db523',
'type' => 'library',
'install_path' => __DIR__ . '/../workerman/channel',
'aliases' => array(),
'dev_requirement' => false,
),
'workerman/phpsocket.io' => array(
'pretty_version' => 'v1.1.14',
'version' => '1.1.14.0',
'reference' => 'a5758da4d55b4744a4cc9c956816d88ce385601e',
'type' => 'library',
'install_path' => __DIR__ . '/../workerman/phpsocket.io',
'aliases' => array(),
'dev_requirement' => false,
),
'workerman/workerman' => array(
'pretty_version' => 'v4.0.41',
'version' => '4.0.41.0',

2
vendor/services.php vendored
View File

@ -1,5 +1,5 @@
<?php
// This file is automatically generated at:2022-07-26 15:34:50
// This file is automatically generated at:2022-07-29 21:13:15
declare (strict_types = 1);
return array (
0 => 'taoser\\addons\\Service',

101
vendor/workerman/channel/README.md vendored Normal file
View File

@ -0,0 +1,101 @@
# Channel
基于订阅的多进程通讯组件用于workerman进程间通讯或者服务器集群通讯类似redis订阅发布机制。基于workerman开发。
Channel 提供两种通讯形式,分别是发布订阅的事件机制和消息队列机制。
它们的主要区别是:
- 事件机制是消息发出后,所有订阅该事件的客户端都能收到消息。
- 消息队列机制是消息发出后,所有订阅该消息的客户端只有一个会收到消息,如果客户端忙消息会进行排队直到有客户端闲置后重新取到消息。
- 需要注意的是 Channel 只是提供一种通讯方式,本身并不提供消息确认、重试、延迟、持久化等功能,请根据实际情况合理使用。
# 手册地址
[Channel手册](http://doc.workerman.net/components/channel.html)
# 服务端
```php
use Workerman\Worker;
//Tcp 通讯方式
$channel_server = new Channel\Server('0.0.0.0', 2206);
//Unix Domain Socket 通讯方式
//$channel_server = new Channel\Server('unix:///tmp/workerman-channel.sock');
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}
```
# 客户端
```php
use Workerman\Worker;
$worker = new Worker();
$worker->onWorkerStart = function()
{
// Channel客户端连接到Channel服务端
Channel\Client::connect('<Channel服务端ip>', 2206);
// 使用 Unix Domain Socket 通讯
//Channel\Client::connect('unix:///tmp/workerman-channel.sock');
// 要订阅的事件名称(名称可以为任意的数字和字符串组合)
$event_name = 'event_xxxx';
// 订阅某个自定义事件并注册回调,收到事件后会自动触发此回调
Channel\Client::on($event_name, function($event_data){
var_dump($event_data);
});
};
$worker->onMessage = function($connection, $data)
{
// 要发布的事件名称
$event_name = 'event_xxxx';
// 事件数据(数据格式可以为数字、字符串、数组),会传递给客户端回调函数作为参数
$event_data = array('some data.', 'some data..');
// 发布某个自定义事件,订阅这个事件的客户端会收到事件数据,并触发客户端对应的事件回调
Channel\Client::publish($event_name, $event_data);
};
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}
````
## 消息队列示例
```php
use Workerman\Worker;
use Workerman\Timer;
$worker = new Worker();
$worker->name = 'Producer';
$worker->onWorkerStart = function()
{
Client::connect();
$count = 0;
Timer::add(1, function() {
Client::enqueue('queue', 'Hello World '.time());
});
};
$mq = new Worker();
$mq->name = 'Consumer';
$mq->count = 4;
$mq->onWorkerStart = function($worker) {
Client::connect();
//订阅消息 queue
Client::watch('queue', function($data) use ($worker) {
echo "Worker {$worker->id} get queue: $data\n";
});
//10 秒后取消订阅该消息
Timer::add(10, function() {
Client::unwatch('queue');
}, [], false);
};
Worker::runAll();
```

12
vendor/workerman/channel/composer.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"name" : "workerman/channel",
"type" : "library",
"homepage": "http://www.workerman.net",
"license" : "MIT",
"require": {
"workerman/workerman" : ">=4.0.12"
},
"autoload": {
"psr-4": {"Channel\\": "./src"}
}
}

391
vendor/workerman/channel/src/Client.php vendored Normal file
View File

@ -0,0 +1,391 @@
<?php
namespace Channel;
use Workerman\Connection\AsyncTcpConnection;
use Workerman\Lib\Timer;
use Workerman\Protocols\Frame;
/**
* Channel/Client
* @version 1.0.7
*/
class Client
{
/**
* onMessage.
* @var callback
*/
public static $onMessage = null;
/**
* onConnect
* @var callback
*/
public static $onConnect = null;
/**
* onClose
* @var callback
*/
public static $onClose = null;
/**
* Connction to channel server.
* @var \Workerman\Connection\TcpConnection
*/
protected static $_remoteConnection = null;
/**
* Channel server ip.
* @var string
*/
protected static $_remoteIp = null;
/**
* Channel server port.
* @var int
*/
protected static $_remotePort = null;
/**
* Reconnect timer.
* @var Timer
*/
protected static $_reconnectTimer = null;
/**
* Ping timer.
* @var Timer
*/
protected static $_pingTimer = null;
/**
* All event callback.
* @var array
*/
protected static $_events = array();
/**
* All queue callback.
* @var callable
*/
protected static $_queues = array();
/**
* @var bool
*/
protected static $_isWorkermanEnv = true;
/**
* Ping interval.
* @var int
*/
public static $pingInterval = 25;
/**
* Connect to channel server
* @param string $ip Channel server ip address or unix domain socket address
* Ip like (TCP): 192.168.1.100
* Unix domain socket like: unix:///tmp/workerman-channel.sock
* @param int $port Port to connect when use tcp
*/
public static function connect($ip = '127.0.0.1', $port = 2206)
{
if (self::$_remoteConnection) {
return;
}
self::$_remoteIp = $ip;
self::$_remotePort = $port;
if (PHP_SAPI !== 'cli' || !class_exists('Workerman\Worker', false)) {
self::$_isWorkermanEnv = false;
}
// For workerman environment.
if (self::$_isWorkermanEnv) {
if (strpos($ip, 'unix://') === false) {
$conn = new AsyncTcpConnection('frame://' . self::$_remoteIp . ':' . self::$_remotePort);
} else {
$conn = new AsyncTcpConnection($ip);
$conn->protocol = Frame::class;
}
$conn->onClose = [self::class, 'onRemoteClose'];
$conn->onConnect = [self::class, 'onRemoteConnect'];
$conn->onMessage = [self::class , 'onRemoteMessage'];
$conn->connect();
if (empty(self::$_pingTimer)) {
self::$_pingTimer = Timer::add(self::$pingInterval, 'Channel\Client::ping');
}
// Not workerman environment.
} else {
$remote = strpos($ip, 'unix://') === false ? 'tcp://'.self::$_remoteIp.':'.self::$_remotePort : $ip;
$conn = stream_socket_client($remote, $code, $message, 5);
if (!$conn) {
throw new \Exception($message);
}
}
self::$_remoteConnection = $conn;
}
/**
* onRemoteMessage.
* @param \Workerman\Connection\TcpConnection $connection
* @param string $data
* @throws \Exception
*/
public static function onRemoteMessage($connection, $data)
{
$data = unserialize($data);
$type = $data['type'];
$event = $data['channel'];
$event_data = $data['data'];
$callback = null;
if ($type == 'event') {
if (!empty(self::$_events[$event])) {
call_user_func(self::$_events[$event], $event_data);
} elseif (!empty(Client::$onMessage)) {
call_user_func(Client::$onMessage, $event, $event_data);
} else {
throw new \Exception("event:$event have not callback");
}
} else {
if (isset(self::$_queues[$event])) {
call_user_func(self::$_queues[$event], $event_data);
} else {
throw new \Exception("queue:$event have not callback");
}
}
}
/**
* Ping.
* @return void
*/
public static function ping()
{
if(self::$_remoteConnection)
{
self::$_remoteConnection->send('');
}
}
/**
* onRemoteClose.
* @return void
*/
public static function onRemoteClose()
{
echo "Waring channel connection closed and try to reconnect\n";
self::$_remoteConnection = null;
self::clearTimer();
self::$_reconnectTimer = Timer::add(1, 'Channel\Client::connect', array(self::$_remoteIp, self::$_remotePort));
if (self::$onClose) {
call_user_func(Client::$onClose);
}
}
/**
* onRemoteConnect.
* @return void
*/
public static function onRemoteConnect()
{
$all_event_names = array_keys(self::$_events);
if($all_event_names)
{
self::subscribe($all_event_names);
}
self::clearTimer();
if (self::$onConnect) {
call_user_func(Client::$onConnect);
}
}
/**
* clearTimer.
* @return void
*/
public static function clearTimer()
{
if (!self::$_isWorkermanEnv) {
throw new \Exception('Channel\\Client not support clearTimer method when it is not in the workerman environment.');
}
if(self::$_reconnectTimer)
{
Timer::del(self::$_reconnectTimer);
self::$_reconnectTimer = null;
}
}
/**
* On.
* @param string $event
* @param callback $callback
* @throws \Exception
*/
public static function on($event, $callback)
{
if (!is_callable($callback)) {
throw new \Exception('callback is not callable for event.');
}
self::$_events[$event] = $callback;
self::subscribe($event);
}
/**
* Subscribe.
* @param string $events
* @return void
*/
public static function subscribe($events)
{
$events = (array)$events;
self::send(array('type' => 'subscribe', 'channels'=>$events));
foreach ($events as $event) {
if(!isset(self::$_events[$event])) {
self::$_events[$event] = null;
}
}
}
/**
* Unsubscribe.
* @param string $events
* @return void
*/
public static function unsubscribe($events)
{
$events = (array)$events;
self::send(array('type' => 'unsubscribe', 'channels'=>$events));
foreach($events as $event) {
unset(self::$_events[$event]);
}
}
/**
* Publish.
* @param string $events
* @param mixed $data
*/
public static function publish($events, $data)
{
self::sendAnyway(array('type' => 'publish', 'channels' => (array)$events, 'data' => $data));
}
/**
* Watch a channel of queue
* @param string|array $channels
* @param callable $callback
* @param boolean $autoReserve Auto reserve after callback finished.
* But sometime you may don't want reserve immediately, or in some asynchronous job,
* you want reserve in finished callback, so you should set $autoReserve to false
* and call Client::reserve() after watch() and in finish callback manually.
* @throws \Exception
*/
public static function watch($channels, $callback, $autoReserve=true)
{
if (!is_callable($callback)) {
throw new \Exception('callback is not callable for watch.');
}
if ($autoReserve) {
$callback = static function($data) use ($callback) {
try {
call_user_func($callback, $data);
} catch (\Exception $e) {
throw $e;
} catch (\Error $e) {
throw $e;
} finally {
self::reserve();
}
};
}
$channels = (array)$channels;
self::send(array('type' => 'watch', 'channels'=>$channels));
foreach ($channels as $channel) {
self::$_queues[$channel] = $callback;
}
if ($autoReserve) {
self::reserve();
}
}
/**
* Unwatch a channel of queue
* @param string $channel
* @throws \Exception
*/
public static function unwatch($channels)
{
$channels = (array)$channels;
self::send(array('type' => 'unwatch', 'channels'=>$channels));
foreach ($channels as $channel) {
if (isset(self::$_queues[$channel])) {
unset(self::$_queues[$channel]);
}
}
}
/**
* Put data to queue
* @param string|array $channels
* @param mixed $data
* @throws \Exception
*/
public static function enqueue($channels, $data)
{
self::sendAnyway(array('type' => 'enqueue', 'channels' => (array)$channels, 'data' => $data));
}
/**
* Start reserve queue manual
* @throws \Exception
*/
public static function reserve()
{
self::send(array('type' => 'reserve'));
}
/**
* Send through workerman environment
* @param $data
* @throws \Exception
*/
protected static function send($data)
{
if (!self::$_isWorkermanEnv) {
throw new \Exception("Channel\\Client not support {$data['type']} method when it is not in the workerman environment.");
}
self::connect(self::$_remoteIp, self::$_remotePort);
self::$_remoteConnection->send(serialize($data));
}
/**
* Send from any environment
* @param $data
* @throws \Exception
*/
protected static function sendAnyway($data)
{
self::connect(self::$_remoteIp, self::$_remotePort);
$body = serialize($data);
if (self::$_isWorkermanEnv) {
self::$_remoteConnection->send($body);
} else {
$buffer = pack('N', 4+strlen($body)) . $body;
fwrite(self::$_remoteConnection, $buffer);
}
}
}

89
vendor/workerman/channel/src/Queue.php vendored Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace Channel;
use Workerman\Connection\TcpConnection;
class Queue
{
public $name = 'default';
public $watcher = array();
public $consumer = array();
protected $queue = null;
public function __construct($name)
{
$this->name = $name;
$this->queue = new \SplQueue();
}
/**
* @param TcpConnection $connection
*/
public function addWatch($connection)
{
if (!isset($this->watcher[$connection->id])) {
$this->watcher[$connection->id] = $connection;
$connection->watchs[] = $this->name;
}
}
/**
* @param TcpConnection $connection
*/
public function removeWatch($connection)
{
if (isset($connection->watchs) && in_array($this->name, $connection->watchs)) {
$idx = array_search($this->name, $connection->watchs);
unset($connection->watchs[$idx]);
}
if (isset($this->watcher[$connection->id])) {
unset($this->watcher[$connection->id]);
}
if (isset($this->consumer[$connection->id])) {
unset($this->consumer[$connection->id]);
}
}
/**
* @param TcpConnection $connection
*/
public function addConsumer($connection)
{
if (isset($this->watcher[$connection->id]) && !isset($this->consumer[$connection->id])) {
$this->consumer[$connection->id] = $connection;
}
$this->dispatch();
}
public function enqueue($data)
{
$this->queue->enqueue($data);
$this->dispatch();
}
private function dispatch()
{
if ($this->queue->isEmpty() || count($this->consumer) == 0) {
return;
}
while (!$this->queue->isEmpty()) {
$data = $this->queue->dequeue();
$idx = key($this->consumer);
$connection = $this->consumer[$idx];
unset($this->consumer[$idx]);
$connection->send(serialize(array('type'=>'queue', 'channel'=>$this->name, 'data' => $data)));
if (count($this->consumer) == 0) {
break;
}
}
}
public function isEmpty()
{
return empty($this->watcher) && $this->queue->isEmpty();
}
}

163
vendor/workerman/channel/src/Server.php vendored Normal file
View File

@ -0,0 +1,163 @@
<?php
namespace Channel;
use Workerman\Protocols\Frame;
use Workerman\Worker;
/**
* Channel server.
*/
class Server
{
/**
* Worker instance.
* @var Worker
*/
protected $_worker = null;
/**
* Queues
* @var Queue[]
*/
protected $_queues = array();
private $ip;
/**
* Construct.
* @param string $ip Bind ip address or unix domain socket.
* Bind unix domain socket use 'unix:///tmp/channel.sock'
* @param int $port Tcp port to bind, only used when listen on tcp.
*/
public function __construct($ip = '0.0.0.0', $port = 2206)
{
if (strpos($ip, 'unix:') === false) {
$worker = new Worker("frame://$ip:$port");
} else {
$worker = new Worker($ip);
$worker->protocol = Frame::class;
}
$this->ip = $ip;
$worker->count = 1;
$worker->name = 'ChannelServer';
$worker->channels = array();
$worker->onMessage = array($this, 'onMessage') ;
$worker->onClose = array($this, 'onClose');
$this->_worker = $worker;
}
/**
* onClose
* @return void
*/
public function onClose($connection)
{
if (!empty($connection->channels)) {
foreach ($connection->channels as $channel) {
unset($this->_worker->channels[$channel][$connection->id]);
if (empty($this->_worker->channels[$channel])) {
unset($this->_worker->channels[$channel]);
}
}
}
if (!empty($connection->watchs)) {
foreach ($connection->watchs as $channel) {
if (isset($this->_queues[$channel])) {
$this->_queues[$channel]->removeWatch($connection);
if ($this->_queues[$channel]->isEmpty()) {
unset($this->_queues[$channel]);
}
}
}
}
}
/**
* onMessage.
* @param \Workerman\Connection\TcpConnection $connection
* @param string $data
*/
public function onMessage($connection, $data)
{
if(!$data)
{
return;
}
$worker = $this->_worker;
$data = unserialize($data);
$type = $data['type'];
switch($type)
{
case 'subscribe':
foreach($data['channels'] as $channel)
{
$connection->channels[$channel] = $channel;
$worker->channels[$channel][$connection->id] = $connection;
}
break;
case 'unsubscribe':
foreach($data['channels'] as $channel) {
if (isset($connection->channels[$channel])) {
unset($connection->channels[$channel]);
}
if (isset($worker->channels[$channel][$connection->id])) {
unset($worker->channels[$channel][$connection->id]);
if (empty($worker->channels[$channel])) {
unset($worker->channels[$channel]);
}
}
}
break;
case 'publish':
foreach ($data['channels'] as $channel) {
if (empty($worker->channels[$channel])) {
continue;
}
$buffer = serialize(array('type' => 'event', 'channel' => $channel, 'data' => $data['data']))."\n";
foreach ($worker->channels[$channel] as $connection) {
$connection->send($buffer);
}
}
break;
case 'watch':
foreach ($data['channels'] as $channel) {
$this->getQueue($channel)->addWatch($connection);
}
break;
case 'unwatch':
foreach ($data['channels'] as $channel) {
if (isset($this->_queues[$channel])) {
$this->_queues[$channel]->removeWatch($connection);
if ($this->_queues[$channel]->isEmpty()) {
unset($this->_queues[$channel]);
}
}
}
break;
case 'enqueue':
foreach ($data['channels'] as $channel) {
$this->getQueue($channel)->enqueue($data['data']);
}
break;
case 'reserve':
if (isset($connection->watchs)) {
foreach ($connection->watchs as $channel) {
if (isset($this->_queues[$channel])) {
$this->_queues[$channel]->addConsumer($connection);
}
}
}
break;
}
}
private function getQueue($channel)
{
if (isset($this->_queues[$channel])) {
return $this->_queues[$channel];
}
return ($this->_queues[$channel] = new Queue($channel));
}
}

53
vendor/workerman/channel/test/queue.php vendored Normal file
View File

@ -0,0 +1,53 @@
<?php
use Channel\Client;
use Channel\Server;
use Workerman\Worker;
use Workerman\Lib\Timer;
// composer autoload
include __DIR__ . '/../vendor/autoload.php';
$channel_server = new Server();
$worker = new Worker();
$worker->name = 'Event';
$worker->onWorkerStart = function()
{
Client::connect();
$count = 0;
$timerId = Timer::add(0.01, function() use (&$timerId, &$count) {
Client::publish('test event', 'some data');
$count++;
Client::enqueue('task-queue', time());
if ($count == 1000) {
Timer::del($timerId);
}
});
Timer::add(10, function() {
Client::enqueue('task-queue', 'hello every 10 seconds');
});
};
$mq = new Worker();
$mq->name = 'Queue';
$mq->count = 4;
$mq->onWorkerStart = function($worker) {
Client::connect();
$countDown = 20;
$id = 1;
Client::watch('task-queue', function($data) use ($worker, &$countDown, &$id) {
echo "[$id] Worker {$worker->id} get queue: $data\n";
sleep(0.2);
$countDown--;
$id++;
if ($worker->id > 1 && $countDown == 0) {
Client::unwatch('task-queue');
}
Timer::add(1, [Client::class, 'reserve'], [], false);
});
};
Worker::runAll();

View File

@ -0,0 +1,28 @@
<?php
use Channel\Client;
use Channel\Server;
use Workerman\Worker;
use Workerman\Lib\Timer;
// composer autoload
include __DIR__ . '/../vendor/autoload.php';
$channel_server = new Server();
$worker = new Worker();
$worker->onWorkerStart = function()
{
Client::connect();
Client::on('test event', function($event_data){
echo 'test event triggered event_data :';
var_dump($event_data);
});
Timer::add(2, function(){
Client::publish('test event', 'some data');
});
};
Worker::runAll();

View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
open_collective: walkor
patreon: walkor

View File

@ -0,0 +1,6 @@
.buildpath
.project
.settings/org.eclipse.php.core.prefs
vendor
examples/vendor
composer.lock

162
vendor/workerman/phpsocket.io/README.md vendored Normal file
View File

@ -0,0 +1,162 @@
# phpsocket.io
A server side alternative implementation of [socket.io](https://github.com/socketio/socket.io) in PHP based on [Workerman](https://github.com/walkor/Workerman).<br>
# Notice
Only support socket.io v1.3.0 or greater. <br>
This project is just translate socket.io by [workerman](https://github.com/walkor/Workerman).<br>
More api just see http://socket.io/docs/server-api/
# Install
composer require workerman/phpsocket.io
# Examples
## Simple chat
start.php
```php
use Workerman\Worker;
use PHPSocketIO\SocketIO;
require_once __DIR__ . '/vendor/autoload.php';
// Listen port 2021 for socket.io client
$io = new SocketIO(2021);
$io->on('connection', function ($socket) use ($io) {
$socket->on('chat message', function ($msg) use ($io) {
$io->emit('chat message', $msg);
});
});
Worker::runAll();
```
## Another chat demo
https://github.com/walkor/phpsocket.io/blob/master/examples/chat/start_io.php
```php
use Workerman\Worker;
use PHPSocketIO\SocketIO;
require_once __DIR__ . '/vendor/autoload.php';
// Listen port 2020 for socket.io client
$io = new SocketIO(2020);
$io->on('connection', function ($socket) {
$socket->addedUser = false;
// When the client emits 'new message', this listens and executes
$socket->on('new message', function ($data) use ($socket) {
// We tell the client to execute 'new message'
$socket->broadcast->emit('new message', array(
'username' => $socket->username,
'message' => $data
));
});
// When the client emits 'add user', this listens and executes
$socket->on('add user', function ($username) use ($socket) {
global $usernames, $numUsers;
// We store the username in the socket session for this client
$socket->username = $username;
// Add the client's username to the global list
$usernames[$username] = $username;
++$numUsers;
$socket->addedUser = true;
$socket->emit('login', array(
'numUsers' => $numUsers
));
// echo globally (all clients) that a person has connected
$socket->broadcast->emit('user joined', array(
'username' => $socket->username,
'numUsers' => $numUsers
));
});
// When the client emits 'typing', we broadcast it to others
$socket->on('typing', function () use ($socket) {
$socket->broadcast->emit('typing', array(
'username' => $socket->username
));
});
// When the client emits 'stop typing', we broadcast it to others
$socket->on('stop typing', function () use ($socket) {
$socket->broadcast->emit('stop typing', array(
'username' => $socket->username
));
});
// When the user disconnects, perform this
$socket->on('disconnect', function () use ($socket) {
global $usernames, $numUsers;
// Remove the username from global usernames list
if ($socket->addedUser) {
unset($usernames[$socket->username]);
--$numUsers;
// echo globally that this client has left
$socket->broadcast->emit('user left', array(
'username' => $socket->username,
'numUsers' => $numUsers
));
}
});
});
Worker::runAll();
```
## Enable SSL for https
**```(phpsocket.io>=1.1.1 && workerman>=3.3.7 required)```**
start.php
```php
<?php
use Workerman\Worker;
use PHPSocketIO\SocketIO;
require_once __DIR__ . '/vendor/autoload.php';
// SSL context
$context = array(
'ssl' => array(
'local_cert' => '/your/path/of/server.pem',
'local_pk' => '/your/path/of/server.key',
'verify_peer' => false
)
);
$io = new SocketIO(2021, $context);
$io->on('connection', function ($connection) use ($io) {
echo "New connection coming\n";
});
Worker::runAll();
```
# 手册
[中文手册](https://github.com/walkor/phpsocket.io/tree/master/docs/zh)
# Livedemo
[chat demo](http://demos.workerman.net/phpsocketio-chat/)
# Run chat example
cd examples/chat
## Start
```php start.php start``` for debug mode
```php start.php start -d ``` for daemon mode
## Stop
```php start.php stop```
## Status
```php start.php status```
# License
MIT

View File

@ -0,0 +1,14 @@
{
"name" : "workerman/phpsocket.io",
"type" : "library",
"keywords": ["socket.io"],
"homepage": "http://www.workerman.net",
"license" : "MIT",
"require": {
"workerman/workerman" : ">=3.5.16",
"workerman/channel" : ">=1.0.0"
},
"autoload": {
"psr-4": {"PHPSocketIO\\": "./src"}
}
}

View File

@ -0,0 +1,3 @@
## Documentation
[中文](./zh/)

View File

@ -0,0 +1,259 @@
# phpsocket.io手册
## 安装
请使用composer集成phpsocket.io。
脚本中引用vendor中的autoload.php实现SocketIO相关类的加载。例如
```php
require_once '/你的vendor路径/autoload.php';
```
## 服务端和客户端连接
**创建一个SocketIO服务端**
```php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use PHPSocketIO\SocketIO;
// 创建socket.io服务端监听3120端口
$io = new SocketIO(3120);
// 当有客户端连接时打印一行文字
$io->on('connection', function($socket)use($io){
echo "new connection coming\n";
});
Worker::runAll();
```
**客户端**
```javascript
<script src='https://cdn.bootcss.com/socket.io/2.0.3/socket.io.js'></script>
<script>
// 如果服务端不在本机请把127.0.0.1改成服务端ip
var socket = io('http://127.0.0.1:3120');
// 当连接服务端成功时触发connect默认事件
socket.on('connect', function(){
console.log('connect success');
});
</script>
```
## 自定义事件
socket.io主要是通过事件来进行通讯交互的。
socket连接除了自带的connectmessagedisconnect三个事件以外在服务端和客户端开发者可以自定义其它事件。
服务端和客户端都通过emit方法触发对端的事件。
例如下面的代码在服务端定义了一个```chat message```事件,事件参数为```$msg```。
```php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use PHPSocketIO\SocketIO;
$io = new SocketIO(3120);
// 当有客户端连接时
$io->on('connection', function($socket)use($io){
// 定义chat message事件回调函数
$socket->on('chat message', function($msg)use($io){
// 触发所有客户端定义的chat message from server事件
$io->emit('chat message from server', $msg);
});
});
Worker::runAll();
```
客户端通过下面的方法触发服务端的chat message事件。
```javascript
<script src='//cdn.bootcss.com/socket.io/1.3.7/socket.io.js'></script>
<script>
// 连接服务端
var socket = io('http://127.0.0.1:3120');
// 触发服务端的chat message事件
socket.emit('chat message', '这个是消息内容...');
// 服务端通过emit('chat message from server', $msg)触发客户端的chat message from server事件
socket.on('chat message from server', function(msg){
console.log('get message:' + msg + ' from server');
});
</script>
```
## workerStart事件
phpsocket.io提供了workerStart事件回调也就是当进程启动后准备好接受客户端链接时触发的回调。
一个进程生命周期只会触发一次。可以在这里设置一些全局的事情比如开一个新的Worker端口等等。
```php
require_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use PHPSocketIO\SocketIO;
$io = new SocketIO(9120);
// 监听一个http端口通过http协议访问这个端口可以向所有客户端推送数据(url类似http://ip:9191?msg=xxxx)
$io->on('workerStart', function()use($io) {
$inner_http_worker = new Worker('http://0.0.0.0:9191');
$inner_http_worker->onMessage = function($http_connection, $data)use($io){
if(!isset($_GET['msg'])) {
return $http_connection->send('fail, $_GET["msg"] not found');
}
$io->emit('chat message', $_GET['msg']);
$http_connection->send('ok');
};
$inner_http_worker->listen();
});
// 当有客户端连接时
$io->on('connection', function($socket)use($io){
// 定义chat message事件回调函数
$socket->on('chat message', function($msg)use($io){
// 触发所有客户端定义的chat message from server事件
$io->emit('chat message from server', $msg);
});
});
Worker::runAll();
```
phpsocket.io启动后开内部http端口通过phpsocket.io向客户端推送数据参考 [web-msg-sender](http://www.workerman.net/web-sender)。
## 分组
socket.io提供分组功能允许向某个分组发送事件例如向某个房间广播数据。
1、加入分组一个连接可以加入多个分组
```php
$socket->join('group name');
```
2、离开分组连接断开时会自动从分组中离开
```php
$socket->leave('group name');
```
## 向客户端发送事件的各种方法
$io是SocketIO对象。$socket是客户端连接
$data可以是数字和字符串也可以是数组。当$data是数组时客户端会自动转换为javascript对象。
同理如果客户端向服务端emit某个事件传递的是一个javascript对象在服务端接收时会自动转换为php数组。
1、向当前客户端发送事件
```php
$socket->emit('event name', $data);
```
2、向所有客户端发送事件
```php
$io->emit('event name', $data);
```
3、向所有客户端发送事件但不包括当前连接。
```php
$socket->broadcast->emit('event name', $data);
```
4、向某个分组的所有客户端发送事件
```php
$io->to('group name')->emit('event name', $data);
```
## 获取客户端ip
```php
$io->on('connection', function($socket)use($io){
var_dump($socket->conn->remoteAddress);
});
```
## 关闭链接
```php
$socket->disconnect();
```
## 限制连接域名
当我们想指定特定域名的页面才能连接,可以用$io->origins方法来设置域名白名单。
```php
$io = new SocketIO(2020);
$io->origins('http://example.com:8080');
```
多个域名时用空格分隔,类似
```php
$io = new SocketIO(2020);
$io->origins('http://workerman.net http://www.workerman.net');
```
## 支持SSL(https wss)
SSL支持有两种方法workerman原生和nginx代理
### workerman原生支持
SSL 要求workerman>=3.3.7 phpsocket.io>=1.1.1
```php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use PHPSocketIO\SocketIO;
// 传入ssl选项包含证书的路径
$context = array(
'ssl' => array(
'local_cert' => '/your/path/of/server.pem',
'local_pk' => '/your/path/of/server.key',
'verify_peer' => false,
)
);
$io = new SocketIO(2120, $context);
$io->on('connection', function($socket)use($io){
echo "new connection coming\n";
});
Worker::runAll();
```
**注意:**<br>
1、证书是要验证域名的所以客户端链接时要指定域名才能顺利的建立链接。<br>
2、客户端连接时不能再用http方式要改成https类似下面这样。
```javascript
<script>
var socket = io('https://yoursite.com:2120');
//.....
</script>
```
### nginx代理SSL
**前提条件及准备工作:**
1、已经安装nginx版本不低于1.3
2、假设phpsocket.io监听的是2120端口
3、已经申请了证书pem/crt文件及key文件放在了/etc/nginx/conf.d/ssl下
4、打算利用nginx开启443端口对外提供ssl代理服务端口可以根据需要修改
**nginx配置类似如下**
```
server {
listen 443;
ssl on;
ssl_certificate /etc/ssl/server.pem;
ssl_certificate_key /etc/ssl/server.key;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
location /socket.io
{
proxy_pass http://127.0.0.1:2120;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Real-IP $remote_addr;
}
# location / {} 站点的其它配置...
}
```
**注意:**<br>
1、证书是要验证域名的所以客户端链接时要指定域名才能顺利的建立链接。<br>
2、客户端连接时不能再用http方式要改成https类似下面这样。
```javascript
<script>
var socket = io('https://yoursite.com');
//.....
</scrip

View File

@ -0,0 +1,20 @@
# For chat demo
## start
```php start.php start``` for debug mode
```php start.php start -d``` for daemon mode
## stop
```php start.php stop```
## satus
```php start.php status```
## restart
``` php start.php restart```
## reload
``` php start.php reload```
## connections
``` php start.php connections```

View File

@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO Chat Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul class="pages">
<li class="chat page">
<div class="chatArea">
<ul class="messages"></ul>
</div>
<input class="inputMessage" placeholder="Type here..."/>
</li>
<li class="login page">
<div class="form">
<h3 class="title">What's your nickname?</h3>
<input class="usernameInput" type="text" maxlength="14" />
</div>
</li>
</ul>
<script src="/jquery.min.js"></script>
<script src="/socket.io-client/socket.io.js"></script>
<script src="/main.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,282 @@
$(function() {
var FADE_TIME = 150; // ms
var TYPING_TIMER_LENGTH = 400; // ms
var COLORS = [
'#e21400', '#91580f', '#f8a700', '#f78b00',
'#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
'#3b88eb', '#3824aa', '#a700ff', '#d300e7'
];
// Initialize variables
var $window = $(window);
var $usernameInput = $('.usernameInput'); // Input for username
var $messages = $('.messages'); // Messages area
var $inputMessage = $('.inputMessage'); // Input message input box
var $loginPage = $('.login.page'); // The login page
var $chatPage = $('.chat.page'); // The chatroom page
// Prompt for setting a username
var username;
var connected = false;
var typing = false;
var lastTypingTime;
var $currentInput = $usernameInput.focus();
var socket = io('http://'+document.domain+':2020');
const addParticipantsMessage = (data) => {
var message = '';
if (data.numUsers === 1) {
message += "there's 1 participant";
} else {
message += "there are " + data.numUsers + " participants";
}
log(message);
}
// Sets the client's username
const setUsername = () => {
username = cleanInput($usernameInput.val().trim());
// If the username is valid
if (username) {
$loginPage.fadeOut();
$chatPage.show();
$loginPage.off('click');
$currentInput = $inputMessage.focus();
// Tell the server your username
socket.emit('add user', username);
}
}
// Sends a chat message
const sendMessage = () => {
var message = $inputMessage.val();
// Prevent markup from being injected into the message
message = cleanInput(message);
// if there is a non-empty message and a socket connection
if (message && connected) {
$inputMessage.val('');
addChatMessage({
username: username,
message: message
});
// tell server to execute 'new message' and send along one parameter
socket.emit('new message', message);
}
}
// Log a message
const log = (message, options) => {
var $el = $('<li>').addClass('log').text(message);
addMessageElement($el, options);
}
// Adds the visual chat message to the message list
const addChatMessage = (data, options) => {
// Don't fade the message in if there is an 'X was typing'
var $typingMessages = getTypingMessages(data);
options = options || {};
if ($typingMessages.length !== 0) {
options.fade = false;
$typingMessages.remove();
}
var $usernameDiv = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
.text(data.message);
var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message"/>')
.data('username', data.username)
.addClass(typingClass)
.append($usernameDiv, $messageBodyDiv);
addMessageElement($messageDiv, options);
}
// Adds the visual chat typing message
const addChatTyping = (data) => {
data.typing = true;
data.message = 'is typing';
addChatMessage(data);
}
// Removes the visual chat typing message
const removeChatTyping = (data) => {
getTypingMessages(data).fadeOut(function () {
$(this).remove();
});
}
// Adds a message element to the messages and scrolls to the bottom
// el - The element to add as a message
// options.fade - If the element should fade-in (default = true)
// options.prepend - If the element should prepend
// all other messages (default = false)
const addMessageElement = (el, options) => {
var $el = $(el);
// Setup default options
if (!options) {
options = {};
}
if (typeof options.fade === 'undefined') {
options.fade = true;
}
if (typeof options.prepend === 'undefined') {
options.prepend = false;
}
// Apply options
if (options.fade) {
$el.hide().fadeIn(FADE_TIME);
}
if (options.prepend) {
$messages.prepend($el);
} else {
$messages.append($el);
}
$messages[0].scrollTop = $messages[0].scrollHeight;
}
// Prevents input from having injected markup
const cleanInput = (input) => {
return $('<div/>').text(input).html();
}
// Updates the typing event
const updateTyping = () => {
if (connected) {
if (!typing) {
typing = true;
socket.emit('typing');
}
lastTypingTime = (new Date()).getTime();
setTimeout(() => {
var typingTimer = (new Date()).getTime();
var timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
socket.emit('stop typing');
typing = false;
}
}, TYPING_TIMER_LENGTH);
}
}
// Gets the 'X is typing' messages of a user
const getTypingMessages = (data) => {
return $('.typing.message').filter(function (i) {
return $(this).data('username') === data.username;
});
}
// Gets the color of a username through our hash function
const getUsernameColor = (username) => {
// Compute hash code
var hash = 7;
for (var i = 0; i < username.length; i++) {
hash = username.charCodeAt(i) + (hash << 5) - hash;
}
// Calculate color
var index = Math.abs(hash % COLORS.length);
return COLORS[index];
}
// Keyboard events
$window.keydown(event => {
// Auto-focus the current input when a key is typed
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
$currentInput.focus();
}
// When the client hits ENTER on their keyboard
if (event.which === 13) {
if (username) {
sendMessage();
socket.emit('stop typing');
typing = false;
} else {
setUsername();
}
}
});
$inputMessage.on('input', () => {
updateTyping();
});
// Click events
// Focus input when clicking anywhere on login page
$loginPage.click(() => {
$currentInput.focus();
});
// Focus input when clicking on the message input's border
$inputMessage.click(() => {
$inputMessage.focus();
});
// Socket events
// Whenever the server emits 'login', log the login message
socket.on('login', (data) => {
connected = true;
// Display the welcome message
var message = "Welcome to Socket.IO Chat ";
log(message, {
prepend: true
});
addParticipantsMessage(data);
});
// Whenever the server emits 'new message', update the chat body
socket.on('new message', (data) => {
addChatMessage(data);
});
// Whenever the server emits 'user joined', log it in the chat body
socket.on('user joined', (data) => {
log(data.username + ' joined');
addParticipantsMessage(data);
});
// Whenever the server emits 'user left', log it in the chat body
socket.on('user left', (data) => {
log(data.username + ' left');
addParticipantsMessage(data);
removeChatTyping(data);
});
// Whenever the server emits 'typing', show the typing message
socket.on('typing', (data) => {
addChatTyping(data);
});
// Whenever the server emits 'stop typing', kill the typing message
socket.on('stop typing', (data) => {
removeChatTyping(data);
});
socket.on('disconnect', () => {
log('you have been disconnected');
});
socket.on('reconnect', () => {
log('you have been reconnected');
if (username) {
socket.emit('add user', username);
}
});
socket.on('reconnect_error', () => {
log('attempt to reconnect has failed');
});
});

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Guillermo Rauch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,92 @@
/**
* Module dependencies.
*/
var url = require('./url');
var parser = require('socket.io-parser');
var Manager = require('./manager');
var debug = require('debug')('socket.io-client');
/**
* Module exports.
*/
module.exports = exports = lookup;
/**
* Managers cache.
*/
var cache = exports.managers = {};
/**
* Looks up an existing `Manager` for multiplexing.
* If the user summons:
*
* `io('http://localhost/a');`
* `io('http://localhost/b');`
*
* We reuse the existing instance based on same scheme/port/host,
* and we initialize sockets for each namespace.
*
* @api public
*/
function lookup(uri, opts) {
if (typeof uri == 'object') {
opts = uri;
uri = undefined;
}
opts = opts || {};
var parsed = url(uri);
var source = parsed.source;
var id = parsed.id;
var path = parsed.path;
var sameNamespace = cache[id] && path in cache[id].nsps;
var newConnection = opts.forceNew || opts['force new connection'] ||
false === opts.multiplex || sameNamespace;
var io;
if (newConnection) {
debug('ignoring socket cache for %s', source);
io = Manager(source, opts);
} else {
if (!cache[id]) {
debug('new io instance for %s', source);
cache[id] = Manager(source, opts);
}
io = cache[id];
}
return io.socket(parsed.path);
}
/**
* Protocol version.
*
* @api public
*/
exports.protocol = parser.protocol;
/**
* `connect`.
*
* @param {String} uri
* @api public
*/
exports.connect = lookup;
/**
* Expose constructors for standalone build.
*
* @api public
*/
exports.Manager = require('./manager');
exports.Socket = require('./socket');

View File

@ -0,0 +1,539 @@
/**
* Module dependencies.
*/
var url = require('./url');
var eio = require('engine.io-client');
var Socket = require('./socket');
var Emitter = require('component-emitter');
var parser = require('socket.io-parser');
var on = require('./on');
var bind = require('component-bind');
var object = require('object-component');
var debug = require('debug')('socket.io-client:manager');
var indexOf = require('indexof');
var Backoff = require('backo2');
/**
* Module exports
*/
module.exports = Manager;
/**
* `Manager` constructor.
*
* @param {String} engine instance or engine uri/opts
* @param {Object} options
* @api public
*/
function Manager(uri, opts){
if (!(this instanceof Manager)) return new Manager(uri, opts);
if (uri && ('object' == typeof uri)) {
opts = uri;
uri = undefined;
}
opts = opts || {};
opts.path = opts.path || '/socket.io';
this.nsps = {};
this.subs = [];
this.opts = opts;
this.reconnection(opts.reconnection !== false);
this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);
this.reconnectionDelay(opts.reconnectionDelay || 1000);
this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);
this.randomizationFactor(opts.randomizationFactor || 0.5);
this.backoff = new Backoff({
min: this.reconnectionDelay(),
max: this.reconnectionDelayMax(),
jitter: this.randomizationFactor()
});
this.timeout(null == opts.timeout ? 20000 : opts.timeout);
this.readyState = 'closed';
this.uri = uri;
this.connected = [];
this.lastPing = null;
this.encoding = false;
this.packetBuffer = [];
this.encoder = new parser.Encoder();
this.decoder = new parser.Decoder();
this.autoConnect = opts.autoConnect !== false;
if (this.autoConnect) this.open();
}
/**
* Propagate given event to sockets and emit on `this`
*
* @api private
*/
Manager.prototype.emitAll = function() {
this.emit.apply(this, arguments);
for (var nsp in this.nsps) {
this.nsps[nsp].emit.apply(this.nsps[nsp], arguments);
}
};
/**
* Update `socket.id` of all sockets
*
* @api private
*/
Manager.prototype.updateSocketIds = function(){
for (var nsp in this.nsps) {
this.nsps[nsp].id = this.engine.id;
}
};
/**
* Mix in `Emitter`.
*/
Emitter(Manager.prototype);
/**
* Sets the `reconnection` config.
*
* @param {Boolean} true/false if it should automatically reconnect
* @return {Manager} self or value
* @api public
*/
Manager.prototype.reconnection = function(v){
if (!arguments.length) return this._reconnection;
this._reconnection = !!v;
return this;
};
/**
* Sets the reconnection attempts config.
*
* @param {Number} max reconnection attempts before giving up
* @return {Manager} self or value
* @api public
*/
Manager.prototype.reconnectionAttempts = function(v){
if (!arguments.length) return this._reconnectionAttempts;
this._reconnectionAttempts = v;
return this;
};
/**
* Sets the delay between reconnections.
*
* @param {Number} delay
* @return {Manager} self or value
* @api public
*/
Manager.prototype.reconnectionDelay = function(v){
if (!arguments.length) return this._reconnectionDelay;
this._reconnectionDelay = v;
this.backoff && this.backoff.setMin(v);
return this;
};
Manager.prototype.randomizationFactor = function(v){
if (!arguments.length) return this._randomizationFactor;
this._randomizationFactor = v;
this.backoff && this.backoff.setJitter(v);
return this;
};
/**
* Sets the maximum delay between reconnections.
*
* @param {Number} delay
* @return {Manager} self or value
* @api public
*/
Manager.prototype.reconnectionDelayMax = function(v){
if (!arguments.length) return this._reconnectionDelayMax;
this._reconnectionDelayMax = v;
this.backoff && this.backoff.setMax(v);
return this;
};
/**
* Sets the connection timeout. `false` to disable
*
* @return {Manager} self or value
* @api public
*/
Manager.prototype.timeout = function(v){
if (!arguments.length) return this._timeout;
this._timeout = v;
return this;
};
/**
* Starts trying to reconnect if reconnection is enabled and we have not
* started reconnecting yet
*
* @api private
*/
Manager.prototype.maybeReconnectOnOpen = function() {
// Only try to reconnect if it's the first time we're connecting
if (!this.reconnecting && this._reconnection && this.backoff.attempts === 0) {
// keeps reconnection from firing twice for the same reconnection loop
this.reconnect();
}
};
/**
* Sets the current transport `socket`.
*
* @param {Function} optional, callback
* @return {Manager} self
* @api public
*/
Manager.prototype.open =
Manager.prototype.connect = function(fn){
debug('readyState %s', this.readyState);
if (~this.readyState.indexOf('open')) return this;
debug('opening %s', this.uri);
this.engine = eio(this.uri, this.opts);
var socket = this.engine;
var self = this;
this.readyState = 'opening';
this.skipReconnect = false;
// emit `open`
var openSub = on(socket, 'open', function() {
self.onopen();
fn && fn();
});
// emit `connect_error`
var errorSub = on(socket, 'error', function(data){
debug('connect_error');
self.cleanup();
self.readyState = 'closed';
self.emitAll('connect_error', data);
if (fn) {
var err = new Error('Connection error');
err.data = data;
fn(err);
} else {
// Only do this if there is no fn to handle the error
self.maybeReconnectOnOpen();
}
});
// emit `connect_timeout`
if (false !== this._timeout) {
var timeout = this._timeout;
debug('connect attempt will timeout after %d', timeout);
// set timer
var timer = setTimeout(function(){
debug('connect attempt timed out after %d', timeout);
openSub.destroy();
socket.close();
socket.emit('error', 'timeout');
self.emitAll('connect_timeout', timeout);
}, timeout);
this.subs.push({
destroy: function(){
clearTimeout(timer);
}
});
}
this.subs.push(openSub);
this.subs.push(errorSub);
return this;
};
/**
* Called upon transport open.
*
* @api private
*/
Manager.prototype.onopen = function(){
debug('open');
// clear old subs
this.cleanup();
// mark as open
this.readyState = 'open';
this.emit('open');
// add new subs
var socket = this.engine;
this.subs.push(on(socket, 'data', bind(this, 'ondata')));
this.subs.push(on(socket, 'ping', bind(this, 'onping')));
this.subs.push(on(socket, 'pong', bind(this, 'onpong')));
this.subs.push(on(socket, 'error', bind(this, 'onerror')));
this.subs.push(on(socket, 'close', bind(this, 'onclose')));
this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded')));
};
/**
* Called upon a ping.
*
* @api private
*/
Manager.prototype.onping = function(){
this.lastPing = new Date;
this.emitAll('ping');
};
/**
* Called upon a packet.
*
* @api private
*/
Manager.prototype.onpong = function(){
this.emitAll('pong', new Date - this.lastPing);
};
/**
* Called with data.
*
* @api private
*/
Manager.prototype.ondata = function(data){
this.decoder.add(data);
};
/**
* Called when parser fully decodes a packet.
*
* @api private
*/
Manager.prototype.ondecoded = function(packet) {
this.emit('packet', packet);
};
/**
* Called upon socket error.
*
* @api private
*/
Manager.prototype.onerror = function(err){
debug('error', err);
this.emitAll('error', err);
};
/**
* Creates a new socket for the given `nsp`.
*
* @return {Socket}
* @api public
*/
Manager.prototype.socket = function(nsp){
var socket = this.nsps[nsp];
if (!socket) {
socket = new Socket(this, nsp);
this.nsps[nsp] = socket;
var self = this;
socket.on('connect', function(){
socket.id = self.engine.id;
if (!~indexOf(self.connected, socket)) {
self.connected.push(socket);
}
});
}
return socket;
};
/**
* Called upon a socket close.
*
* @param {Socket} socket
*/
Manager.prototype.destroy = function(socket){
var index = indexOf(this.connected, socket);
if (~index) this.connected.splice(index, 1);
if (this.connected.length) return;
this.close();
};
/**
* Writes a packet.
*
* @param {Object} packet
* @api private
*/
Manager.prototype.packet = function(packet){
debug('writing packet %j', packet);
var self = this;
if (!self.encoding) {
// encode, then write to engine with result
self.encoding = true;
this.encoder.encode(packet, function(encodedPackets) {
for (var i = 0; i < encodedPackets.length; i++) {
self.engine.write(encodedPackets[i], packet.options);
}
self.encoding = false;
self.processPacketQueue();
});
} else { // add packet to the queue
self.packetBuffer.push(packet);
}
};
/**
* If packet buffer is non-empty, begins encoding the
* next packet in line.
*
* @api private
*/
Manager.prototype.processPacketQueue = function() {
if (this.packetBuffer.length > 0 && !this.encoding) {
var pack = this.packetBuffer.shift();
this.packet(pack);
}
};
/**
* Clean up transport subscriptions and packet buffer.
*
* @api private
*/
Manager.prototype.cleanup = function(){
debug('cleanup');
var sub;
while (sub = this.subs.shift()) sub.destroy();
this.packetBuffer = [];
this.encoding = false;
this.lastPing = null;
this.decoder.destroy();
};
/**
* Close the current socket.
*
* @api private
*/
Manager.prototype.close =
Manager.prototype.disconnect = function(){
debug('disconnect');
this.skipReconnect = true;
this.reconnecting = false;
if ('opening' == this.readyState) {
// `onclose` will not fire because
// an open event never happened
this.cleanup();
}
this.backoff.reset();
this.readyState = 'closed';
if (this.engine) this.engine.close();
};
/**
* Called upon engine close.
*
* @api private
*/
Manager.prototype.onclose = function(reason){
debug('onclose');
this.cleanup();
this.backoff.reset();
this.readyState = 'closed';
this.emit('close', reason);
if (this._reconnection && !this.skipReconnect) {
this.reconnect();
}
};
/**
* Attempt a reconnection.
*
* @api private
*/
Manager.prototype.reconnect = function(){
if (this.reconnecting || this.skipReconnect) return this;
var self = this;
if (this.backoff.attempts >= this._reconnectionAttempts) {
debug('reconnect failed');
this.backoff.reset();
this.emitAll('reconnect_failed');
this.reconnecting = false;
} else {
var delay = this.backoff.duration();
debug('will wait %dms before reconnect attempt', delay);
this.reconnecting = true;
var timer = setTimeout(function(){
if (self.skipReconnect) return;
debug('attempting reconnect');
self.emitAll('reconnect_attempt', self.backoff.attempts);
self.emitAll('reconnecting', self.backoff.attempts);
// check again for the case socket closed in above events
if (self.skipReconnect) return;
self.open(function(err){
if (err) {
debug('reconnect attempt error');
self.reconnecting = false;
self.reconnect();
self.emitAll('reconnect_error', err.data);
} else {
debug('reconnect success');
self.onreconnect();
}
});
}, delay);
this.subs.push({
destroy: function(){
clearTimeout(timer);
}
});
}
};
/**
* Called upon successful reconnect.
*
* @api private
*/
Manager.prototype.onreconnect = function(){
var attempt = this.backoff.attempts;
this.reconnecting = false;
this.backoff.reset();
this.updateSocketIds();
this.emitAll('reconnect', attempt);
};

View File

@ -0,0 +1,24 @@
/**
* Module exports.
*/
module.exports = on;
/**
* Helper for subscriptions.
*
* @param {Object|EventEmitter} obj with `Emitter` mixin or `EventEmitter`
* @param {String} event name
* @param {Function} callback
* @api public
*/
function on(obj, ev, fn) {
obj.on(ev, fn);
return {
destroy: function(){
obj.removeListener(ev, fn);
}
};
}

View File

@ -0,0 +1,411 @@
/**
* Module dependencies.
*/
var parser = require('socket.io-parser');
var Emitter = require('component-emitter');
var toArray = require('to-array');
var on = require('./on');
var bind = require('component-bind');
var debug = require('debug')('socket.io-client:socket');
var hasBin = require('has-binary');
/**
* Module exports.
*/
module.exports = exports = Socket;
/**
* Internal events (blacklisted).
* These events can't be emitted by the user.
*
* @api private
*/
var events = {
connect: 1,
connect_error: 1,
connect_timeout: 1,
disconnect: 1,
error: 1,
reconnect: 1,
reconnect_attempt: 1,
reconnect_failed: 1,
reconnect_error: 1,
reconnecting: 1,
ping: 1,
pong: 1
};
/**
* Shortcut to `Emitter#emit`.
*/
var emit = Emitter.prototype.emit;
/**
* `Socket` constructor.
*
* @api public
*/
function Socket(io, nsp){
this.io = io;
this.nsp = nsp;
this.json = this; // compat
this.ids = 0;
this.acks = {};
if (this.io.autoConnect) this.open();
this.receiveBuffer = [];
this.sendBuffer = [];
this.connected = false;
this.disconnected = true;
}
/**
* Mix in `Emitter`.
*/
Emitter(Socket.prototype);
/**
* Subscribe to open, close and packet events
*
* @api private
*/
Socket.prototype.subEvents = function() {
if (this.subs) return;
var io = this.io;
this.subs = [
on(io, 'open', bind(this, 'onopen')),
on(io, 'packet', bind(this, 'onpacket')),
on(io, 'close', bind(this, 'onclose'))
];
};
/**
* "Opens" the socket.
*
* @api public
*/
Socket.prototype.open =
Socket.prototype.connect = function(){
if (this.connected) return this;
this.subEvents();
this.io.open(); // ensure open
if ('open' == this.io.readyState) this.onopen();
return this;
};
/**
* Sends a `message` event.
*
* @return {Socket} self
* @api public
*/
Socket.prototype.send = function(){
var args = toArray(arguments);
args.unshift('message');
this.emit.apply(this, args);
return this;
};
/**
* Override `emit`.
* If the event is in `events`, it's emitted normally.
*
* @param {String} event name
* @return {Socket} self
* @api public
*/
Socket.prototype.emit = function(ev){
if (events.hasOwnProperty(ev)) {
emit.apply(this, arguments);
return this;
}
var args = toArray(arguments);
var parserType = parser.EVENT; // default
if (hasBin(args)) { parserType = parser.BINARY_EVENT; } // binary
var packet = { type: parserType, data: args };
packet.options = {};
packet.options.compress = !this.flags || false !== this.flags.compress;
// event ack callback
if ('function' == typeof args[args.length - 1]) {
debug('emitting packet with ack id %d', this.ids);
this.acks[this.ids] = args.pop();
packet.id = this.ids++;
}
if (this.connected) {
this.packet(packet);
} else {
this.sendBuffer.push(packet);
}
delete this.flags;
return this;
};
/**
* Sends a packet.
*
* @param {Object} packet
* @api private
*/
Socket.prototype.packet = function(packet){
packet.nsp = this.nsp;
this.io.packet(packet);
};
/**
* Called upon engine `open`.
*
* @api private
*/
Socket.prototype.onopen = function(){
debug('transport is open - connecting');
// write connect packet if necessary
if ('/' != this.nsp) {
this.packet({ type: parser.CONNECT, options: { compress: true } });
}
};
/**
* Called upon engine `close`.
*
* @param {String} reason
* @api private
*/
Socket.prototype.onclose = function(reason){
debug('close (%s)', reason);
this.connected = false;
this.disconnected = true;
delete this.id;
this.emit('disconnect', reason);
};
/**
* Called with socket packet.
*
* @param {Object} packet
* @api private
*/
Socket.prototype.onpacket = function(packet){
if (packet.nsp != this.nsp) return;
switch (packet.type) {
case parser.CONNECT:
this.onconnect();
break;
case parser.EVENT:
this.onevent(packet);
break;
case parser.BINARY_EVENT:
this.onevent(packet);
break;
case parser.ACK:
this.onack(packet);
break;
case parser.BINARY_ACK:
this.onack(packet);
break;
case parser.DISCONNECT:
this.ondisconnect();
break;
case parser.ERROR:
this.emit('error', packet.data);
break;
}
};
/**
* Called upon a server event.
*
* @param {Object} packet
* @api private
*/
Socket.prototype.onevent = function(packet){
var args = packet.data || [];
debug('emitting event %j', args);
if (null != packet.id) {
debug('attaching ack callback to event');
args.push(this.ack(packet.id));
}
if (this.connected) {
emit.apply(this, args);
} else {
this.receiveBuffer.push(args);
}
};
/**
* Produces an ack callback to emit with an event.
*
* @api private
*/
Socket.prototype.ack = function(id){
var self = this;
var sent = false;
return function(){
// prevent double callbacks
if (sent) return;
sent = true;
var args = toArray(arguments);
debug('sending ack %j', args);
var type = hasBin(args) ? parser.BINARY_ACK : parser.ACK;
self.packet({
type: type,
id: id,
data: args,
options: { compress: true }
});
};
};
/**
* Called upon a server acknowlegement.
*
* @param {Object} packet
* @api private
*/
Socket.prototype.onack = function(packet){
var ack = this.acks[packet.id];
if ('function' == typeof ack) {
debug('calling ack %s with %j', packet.id, packet.data);
ack.apply(this, packet.data);
delete this.acks[packet.id];
} else {
debug('bad ack %s', packet.id);
}
};
/**
* Called upon server connect.
*
* @api private
*/
Socket.prototype.onconnect = function(){
this.connected = true;
this.disconnected = false;
this.emit('connect');
this.emitBuffered();
};
/**
* Emit buffered events (received and emitted).
*
* @api private
*/
Socket.prototype.emitBuffered = function(){
var i;
for (i = 0; i < this.receiveBuffer.length; i++) {
emit.apply(this, this.receiveBuffer[i]);
}
this.receiveBuffer = [];
for (i = 0; i < this.sendBuffer.length; i++) {
this.packet(this.sendBuffer[i]);
}
this.sendBuffer = [];
};
/**
* Called upon server disconnect.
*
* @api private
*/
Socket.prototype.ondisconnect = function(){
debug('server disconnect (%s)', this.nsp);
this.destroy();
this.onclose('io server disconnect');
};
/**
* Called upon forced client/server side disconnections,
* this method ensures the manager stops tracking us and
* that reconnections don't get triggered for this.
*
* @api private.
*/
Socket.prototype.destroy = function(){
if (this.subs) {
// clean subscriptions to avoid reconnections
for (var i = 0; i < this.subs.length; i++) {
this.subs[i].destroy();
}
this.subs = null;
}
this.io.destroy(this);
};
/**
* Disconnects the socket manually.
*
* @return {Socket} self
* @api public
*/
Socket.prototype.close =
Socket.prototype.disconnect = function(){
if (this.connected) {
debug('performing disconnect (%s)', this.nsp);
this.packet({ type: parser.DISCONNECT, options: { compress: true } });
}
// remove socket from pool
this.destroy();
if (this.connected) {
// fire events
this.onclose('io client disconnect');
}
return this;
};
/**
* Sets the compress flag.
*
* @param {Boolean} if `true`, compresses the sending data
* @return {Socket} self
* @api public
*/
Socket.prototype.compress = function(compress){
this.flags = this.flags || {};
this.flags.compress = compress;
return this;
};

View File

@ -0,0 +1,73 @@
/**
* Module dependencies.
*/
var parseuri = require('parseuri');
var debug = require('debug')('socket.io-client:url');
/**
* Module exports.
*/
module.exports = url;
/**
* URL parser.
*
* @param {String} url
* @param {Object} An object meant to mimic window.location.
* Defaults to window.location.
* @api public
*/
function url(uri, loc){
var obj = uri;
// default to window.location
var loc = loc || global.location;
if (null == uri) uri = loc.protocol + '//' + loc.host;
// relative path support
if ('string' == typeof uri) {
if ('/' == uri.charAt(0)) {
if ('/' == uri.charAt(1)) {
uri = loc.protocol + uri;
} else {
uri = loc.host + uri;
}
}
if (!/^(https?|wss?):\/\//.test(uri)) {
debug('protocol-less url %s', uri);
if ('undefined' != typeof loc) {
uri = loc.protocol + '//' + uri;
} else {
uri = 'https://' + uri;
}
}
// parse
debug('parse %s', uri);
obj = parseuri(uri);
}
// make sure we treat `localhost:80` and `localhost` equally
if (!obj.port) {
if (/^(http|ws)$/.test(obj.protocol)) {
obj.port = '80';
}
else if (/^(http|ws)s$/.test(obj.protocol)) {
obj.port = '443';
}
}
obj.path = obj.path || '/';
// define unique id
obj.id = obj.protocol + '://' + obj.host + ':' + obj.port;
// define href
obj.href = obj.protocol + '://' + obj.host + (loc && loc.port == obj.port ? '' : (':' + obj.port));
return obj;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,149 @@
/* Fix user-agent */
* {
box-sizing: border-box;
}
html {
font-weight: 300;
-webkit-font-smoothing: antialiased;
}
html, input {
font-family:
"HelveticaNeue-Light",
"Helvetica Neue Light",
"Helvetica Neue",
Helvetica,
Arial,
"Lucida Grande",
sans-serif;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
ul {
list-style: none;
word-wrap: break-word;
}
/* Pages */
.pages {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
}
.page {
height: 100%;
position: absolute;
width: 100%;
}
/* Login Page */
.login.page {
background-color: #000;
}
.login.page .form {
height: 100px;
margin-top: -100px;
position: absolute;
text-align: center;
top: 50%;
width: 100%;
}
.login.page .form .usernameInput {
background-color: transparent;
border: none;
border-bottom: 2px solid #fff;
outline: none;
padding-bottom: 15px;
text-align: center;
width: 400px;
}
.login.page .title {
font-size: 200%;
}
.login.page .usernameInput {
font-size: 200%;
letter-spacing: 3px;
}
.login.page .title, .login.page .usernameInput {
color: #fff;
font-weight: 100;
}
/* Chat page */
.chat.page {
display: none;
}
/* Font */
.messages {
font-size: 150%;
}
.inputMessage {
font-size: 100%;
}
.log {
color: gray;
font-size: 70%;
margin: 5px;
text-align: center;
}
/* Messages */
.chatArea {
height: 100%;
padding-bottom: 60px;
}
.messages {
height: 100%;
margin: 0;
overflow-y: scroll;
padding: 10px 20px 10px 20px;
}
.message.typing .messageBody {
color: gray;
}
.username {
font-weight: 700;
overflow: hidden;
padding-right: 15px;
text-align: right;
}
/* Input */
.inputMessage {
border: 10px solid #000;
bottom: 0;
height: 60px;
left: 0;
outline: none;
padding-left: 10px;
position: absolute;
right: 0;
width: 100%;
}

View File

@ -0,0 +1,12 @@
<?php
use Workerman\Worker;
use Workerman\WebServer;
use Workerman\Autoloader;
use PHPSocketIO\SocketIO;
define('GLOBAL_START', true);
require_once __DIR__ . '/start_web.php';
require_once __DIR__ . '/start_io.php';
Worker::runAll();

View File

@ -0,0 +1,73 @@
<?php
use Workerman\Worker;
use Workerman\WebServer;
use Workerman\Autoloader;
use PHPSocketIO\SocketIO;
// composer autoload
require_once join(DIRECTORY_SEPARATOR, array(__DIR__, "..", "..", "vendor", "autoload.php"));
$io = new SocketIO(2020);
$io->on('connection', function($socket){
$socket->addedUser = false;
// when the client emits 'new message', this listens and executes
$socket->on('new message', function ($data)use($socket){
// we tell the client to execute 'new message'
$socket->broadcast->emit('new message', array(
'username'=> $socket->username,
'message'=> $data
));
});
// when the client emits 'add user', this listens and executes
$socket->on('add user', function ($username) use($socket){
if ($socket->addedUser)
return;
global $usernames, $numUsers;
// we store the username in the socket session for this client
$socket->username = $username;
++$numUsers;
$socket->addedUser = true;
$socket->emit('login', array(
'numUsers' => $numUsers
));
// echo globally (all clients) that a person has connected
$socket->broadcast->emit('user joined', array(
'username' => $socket->username,
'numUsers' => $numUsers
));
});
// when the client emits 'typing', we broadcast it to others
$socket->on('typing', function () use($socket) {
$socket->broadcast->emit('typing', array(
'username' => $socket->username
));
});
// when the client emits 'stop typing', we broadcast it to others
$socket->on('stop typing', function () use($socket) {
$socket->broadcast->emit('stop typing', array(
'username' => $socket->username
));
});
// when the user disconnects.. perform this
$socket->on('disconnect', function () use($socket) {
global $usernames, $numUsers;
if($socket->addedUser) {
--$numUsers;
// echo globally that this client has left
$socket->broadcast->emit('user left', array(
'username' => $socket->username,
'numUsers' => $numUsers
));
}
});
});
if (!defined('GLOBAL_START')) {
Worker::runAll();
}

View File

@ -0,0 +1,62 @@
<?php
use Workerman\Worker;
use Workerman\Protocols\Http\Request;
use Workerman\Protocols\Http\Response;
use Workerman\Connection\TcpConnection;
// composer autoload
require_once join(DIRECTORY_SEPARATOR, array(__DIR__, "..", "..", "vendor", "autoload.php"));
$web = new Worker('http://0.0.0.0:2022');
$web->name = 'web';
define('WEBROOT', __DIR__ . DIRECTORY_SEPARATOR . 'public');
$web->onMessage = function (TcpConnection $connection, Request $request) {
$path = $request->path();
if ($path === '/') {
$connection->send(exec_php_file(WEBROOT.'/index.html'));
return;
}
$file = realpath(WEBROOT. $path);
if (false === $file) {
$connection->send(new Response(404, array(), '<h3>404 Not Found</h3>'));
return;
}
// Security check! Very important!!!
if (strpos($file, WEBROOT) !== 0) {
$connection->send(new Response(400));
return;
}
if (\pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$connection->send(exec_php_file($file));
return;
}
$if_modified_since = $request->header('if-modified-since');
if (!empty($if_modified_since)) {
// Check 304.
$info = \stat($file);
$modified_time = $info ? \date('D, d M Y H:i:s', $info['mtime']) . ' ' . \date_default_timezone_get() : '';
if ($modified_time === $if_modified_since) {
$connection->send(new Response(304));
return;
}
}
$connection->send((new Response())->withFile($file));
};
function exec_php_file($file) {
\ob_start();
// Try to include php file.
try {
include $file;
} catch (\Exception $e) {
echo $e;
}
return \ob_get_clean();
}
if (!defined('GLOBAL_START')) {
Worker::runAll();
}

View File

@ -0,0 +1,124 @@
<?php
namespace PHPSocketIO;
class ChannelAdapter extends DefaultAdapter
{
protected $_channelId = null;
public static $ip = '127.0.0.1';
public static $port = 2206;
public function __construct($nsp)
{
parent::__construct($nsp);
$this->_channelId = (function_exists('random_int') ? random_int(1, 10000000): rand(1, 10000000)) . "-" . (function_exists('posix_getpid') ? posix_getpid(): 1);
\Channel\Client::connect(self::$ip, self::$port);
\Channel\Client::$onMessage = array($this, 'onChannelMessage');
\Channel\Client::subscribe("socket.io#/#");
Debug::debug('ChannelAdapter __construct');
}
public function __destruct()
{
Debug::debug('ChannelAdapter __destruct');
}
public function add($id ,$room)
{
$this->sids[$id][$room] = true;
$this->rooms[$room][$id] = true;
$channel = "socket.io#/#$room#";
\Channel\Client::subscribe($channel);
}
public function del($id, $room)
{
unset($this->sids[$id][$room]);
unset($this->rooms[$room][$id]);
if(empty($this->rooms[$room]))
{
unset($this->rooms[$room]);
$channel = "socket.io#/#$room#";
\Channel\Client::unsubscribe($channel);
}
}
public function delAll($id)
{
$rooms = isset($this->sids[$id]) ? array_keys($this->sids[$id]) : array();
if($rooms)
{
foreach($rooms as $room)
{
if(isset($this->rooms[$room][$id]))
{
unset($this->rooms[$room][$id]);
$channel = "socket.io#/#$room#";
\Channel\Client::unsubscribe($channel);
}
if(isset($this->rooms[$room]) && empty($this->rooms[$room]))
{
unset($this->rooms[$room]);
}
}
}
unset($this->sids[$id]);
}
public function onChannelMessage($channel, $msg)
{
if($this->_channelId === array_shift($msg))
{
//echo "ignore same channel_id \n";
return;
}
$packet = $msg[0];
$opts = $msg[1];
if(!$packet)
{
echo "invalid channel:$channel packet \n";
return;
}
if(empty($packet['nsp']))
{
$packet['nsp'] = '/';
}
if($packet['nsp'] != $this->nsp->name)
{
echo "ignore different namespace {$packet['nsp']} != {$this->nsp->name}\n";
return;
}
$this->broadcast($packet, $opts, true);
}
public function broadcast($packet, $opts, $remote = false)
{
parent::broadcast($packet, $opts);
if (!$remote)
{
$packet['nsp'] = '/';
if(!empty($opts['rooms']))
{
foreach($opts['rooms'] as $room)
{
$chn = "socket.io#/#$room#";
$msg = array($this->_channelId, $packet, $opts);
\Channel\Client::publish($chn, $msg);
}
}
else
{
$chn = "socket.io#/#";
$msg = array($this->_channelId, $packet, $opts);
\Channel\Client::publish($chn, $msg);
}
}
}
}

View File

@ -0,0 +1,260 @@
<?php
namespace PHPSocketIO;
use PHPSocketIO\Parser\Parser;
class Client
{
public $server = null;
public $conn = null;
public $encoder = null;
public $decoder = null;
public $id = null;
public $request = null;
public $nsps = array();
public $connectBuffer = array();
public function __construct($server, $conn)
{
$this->server = $server;
$this->conn = $conn;
$this->encoder = new \PHPSocketIO\Parser\Encoder();
$this->decoder = new \PHPSocketIO\Parser\Decoder();
$this->id = $conn->id;
$this->request = $conn->request;
$this->setup();
Debug::debug('Client __construct');
}
public function __destruct()
{
Debug::debug('Client __destruct');
}
/**
* Sets up event listeners.
*
* @api private
*/
public function setup(){
$this->decoder->on('decoded', array($this,'ondecoded'));
$this->conn->on('data', array($this,'ondata'));
$this->conn->on('error', array($this, 'onerror'));
$this->conn->on('close' ,array($this, 'onclose'));
}
/**
* Connects a client to a namespace.
*
* @param {String} namespace name
* @api private
*/
public function connect($name){
if (!isset($this->server->nsps[$name]))
{
$this->packet(array('type'=> Parser::ERROR, 'nsp'=> $name, 'data'=> 'Invalid namespace'));
return;
}
$nsp = $this->server->of($name);
if ('/' !== $name && !isset($this->nsps['/']))
{
$this->connectBuffer[$name] = $name;
return;
}
$nsp->add($this, $nsp, array($this, 'nspAdd'));
}
public function nspAdd($socket, $nsp)
{
$this->sockets[$socket->id] = $socket;
$this->nsps[$nsp->name] = $socket;
if ('/' === $nsp->name && $this->connectBuffer)
{
foreach($this->connectBuffer as $name)
{
$this->connect($name);
}
$this->connectBuffer = array();
}
}
/**
* Disconnects from all namespaces and closes transport.
*
* @api private
*/
public function disconnect()
{
foreach($this->sockets as $socket)
{
$socket->disconnect();
}
$this->sockets = array();
$this->close();
}
/**
* Removes a socket. Called by each `Socket`.
*
* @api private
*/
public function remove($socket)
{
if(isset($this->sockets[$socket->id]))
{
$nsp = $this->sockets[$socket->id]->nsp->name;
unset($this->sockets[$socket->id]);
unset($this->nsps[$nsp]);
} else {
//echo('ignoring remove for '. $socket->id);
}
}
/**
* Closes the underlying connection.
*
* @api private
*/
public function close()
{
if (empty($this->conn)) return;
if('open' === $this->conn->readyState)
{
//echo('forcing transport close');
$this->conn->close();
$this->onclose('forced server close');
}
}
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Object} options
* @api private
*/
public function packet($packet, $preEncoded = false, $volatile = false)
{
if(!empty($this->conn) && 'open' === $this->conn->readyState)
{
if (!$preEncoded)
{
// not broadcasting, need to encode
$encodedPackets = $this->encoder->encode($packet);
$this->writeToEngine($encodedPackets, $volatile);
} else { // a broadcast pre-encodes a packet
$this->writeToEngine($packet);
}
} else {
// todo check
// echo('ignoring packet write ' . var_export($packet, true));
}
}
public function writeToEngine($encodedPackets, $volatile = false)
{
if($volatile)echo new \Exception('volatile');
if ($volatile && !$this->conn->transport->writable) return;
// todo check
if(isset($encodedPackets['nsp']))unset($encodedPackets['nsp']);
foreach($encodedPackets as $packet)
{
$this->conn->write($packet);
}
}
/**
* Called with incoming transport data.
*
* @api private
*/
public function ondata($data)
{
try {
// todo chek '2["chat message","2"]' . "\0" . ''
$this->decoder->add(trim($data));
} catch(\Exception $e) {
$this->onerror($e);
}
}
/**
* Called when parser fully decodes a packet.
*
* @api private
*/
public function ondecoded($packet)
{
if(Parser::CONNECT == $packet['type'])
{
$this->connect($packet['nsp']);
} else {
if(isset($this->nsps[$packet['nsp']]))
{
$this->nsps[$packet['nsp']]->onpacket($packet);
} else {
//echo('no socket for namespace ' . $packet['nsp']);
}
}
}
/**
* Handles an error.
*
* @param {Objcet} error object
* @api private
*/
public function onerror($err)
{
foreach($this->sockets as $socket)
{
$socket->onerror($err);
}
$this->onclose('client error');
}
/**
* Called upon transport close.
*
* @param {String} reason
* @api private
*/
public function onclose($reason)
{
if (empty($this->conn)) return;
// ignore a potential subsequent `close` event
$this->destroy();
// `nsps` and `sockets` are cleaned up seamlessly
foreach($this->sockets as $socket)
{
$socket->onclose($reason);
}
$this->sockets = null;
}
/**
* Cleans up event listeners.
*
* @api private
*/
public function destroy()
{
if (!$this->conn) return;
$this->conn->removeAllListeners();
$this->decoder->removeAllListeners();
$this->encoder->removeAllListeners();
$this->server = $this->conn = $this->encoder = $this->decoder = $this->request = $this->nsps = null;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace PHPSocketIO;
class Debug
{
public static function debug($var)
{
global $debug;
if($debug)
echo var_export($var, true)."\n";
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace PHPSocketIO;
class DefaultAdapter
{
public $nsp = null;
public $rooms = array();
public $sids = array();
public $encoder = null;
public function __construct($nsp)
{
$this->nsp = $nsp;
$this->encoder = new Parser\Encoder();
Debug::debug('DefaultAdapter __construct');
}
public function __destruct()
{
Debug::debug('DefaultAdapter __destruct');
}
public function add($id, $room)
{
$this->sids[$id][$room] = true;
$this->rooms[$room][$id] = true;
}
public function del($id, $room)
{
unset($this->sids[$id][$room]);
unset($this->rooms[$room][$id]);
if(empty($this->rooms[$room]))
{
unset($this->rooms[$room]);
}
}
public function delAll($id)
{
$rooms = array_keys(isset($this->sids[$id]) ? $this->sids[$id] : array());
foreach($rooms as $room)
{
$this->del($id, $room);
}
unset($this->sids[$id]);
}
public function broadcast($packet, $opts, $remote = false)
{
$rooms = isset($opts['rooms']) ? $opts['rooms'] : array();
$except = isset($opts['except']) ? $opts['except'] : array();
$flags = isset($opts['flags']) ? $opts['flags'] : array();
$packetOpts = array(
'preEncoded' => true,
'volatile' => isset($flags['volatile']) ? $flags['volatile'] : null,
'compress' => isset($flags['compress']) ? $flags['compress'] : null
);
$packet['nsp'] = $this->nsp->name;
$encodedPackets = $this->encoder->encode($packet);
if($rooms)
{
$ids = array();
foreach($rooms as $i=>$room)
{
if(!isset($this->rooms[$room]))
{
continue;
}
$room = $this->rooms[$room];
foreach($room as $id=>$item)
{
if(isset($ids[$id]) || isset($except[$id]))
{
continue;
}
if(isset($this->nsp->connected[$id]))
{
$ids[$id] = true;
$this->nsp->connected[$id]->packet($encodedPackets, $packetOpts);
}
}
}
} else {
foreach($this->sids as $id=>$sid)
{
if(isset($except[$id])) continue;
if(isset($this->nsp->connected[$id]))
{
$socket = $this->nsp->connected[$id];
$volatile = isset($flags['volatile']) ? $flags['volatile'] : null;
$socket->packet($encodedPackets, true, $volatile);
}
}
}
}
public function clients($rooms, $fn) {
$sids = array();
foreach ($rooms as $room) {
$sids = array_merge($sids, $this->rooms[$room]);
}
$fn();
}
}

View File

@ -0,0 +1,309 @@
<?php
namespace PHPSocketIO\Engine;
use \PHPSocketIO\Engine\Transports\Polling;
use \PHPSocketIO\Engine\Transports\PollingXHR;
use \PHPSocketIO\Engine\Transports\WebSocket;
use \PHPSocketIO\Event\Emitter;
use \PHPSocketIO\Debug;
class Engine extends Emitter
{
public $pingTimeout = 60;
public $pingInterval = 25;
public $upgradeTimeout = 5;
public $transports = array();
public $allowUpgrades = array();
public $allowRequest = array();
public $clients = array();
public $origins = '*:*';
public static $allowTransports = array(
'polling' => 'polling',
'websocket' => 'websocket'
);
public static $errorMessages = array(
'Transport unknown',
'Session ID unknown',
'Bad handshake method',
'Bad request'
);
const ERROR_UNKNOWN_TRANSPORT = 0;
const ERROR_UNKNOWN_SID = 1;
const ERROR_BAD_HANDSHAKE_METHOD = 2;
const ERROR_BAD_REQUEST = 3;
public function __construct($opts = array())
{
$ops_map = array(
'pingTimeout',
'pingInterval',
'upgradeTimeout',
'transports',
'allowUpgrades',
'allowRequest'
);
foreach($ops_map as $key)
{
if(isset($opts[$key]))
{
$this->$key = $opts[$key];
}
}
Debug::debug('Engine __construct');
}
public function __destruct()
{
Debug::debug('Engine __destruct');
}
public function handleRequest($req, $res)
{
$this->prepare($req);
$req->res = $res;
$this->verify($req, $res, false, array($this, 'dealRequest'));
}
public function dealRequest($err, $success, $req)
{
if (!$success)
{
self::sendErrorMessage($req, $req->res, $err);
return;
}
if(isset($req->_query['sid']))
{
$this->clients[$req->_query['sid']]->transport->onRequest($req);
}
else
{
$this->handshake($req->_query['transport'], $req);
}
}
protected function sendErrorMessage($req, $res, $code)
{
$headers = array('Content-Type'=> 'application/json');
if(isset($req->headers['origin']))
{
$headers['Access-Control-Allow-Credentials'] = 'true';
$headers['Access-Control-Allow-Origin'] = $req->headers['origin'];
}
else
{
$headers['Access-Control-Allow-Origin'] = '*';
}
$res->writeHead(403, '', $headers);
$res->end(json_encode(array(
'code' => $code,
'message' => isset(self::$errorMessages[$code]) ? self::$errorMessages[$code] : $code
)));
}
protected function verify($req, $res, $upgrade, $fn)
{
if(!isset($req->_query['transport']) || !isset(self::$allowTransports[$req->_query['transport']]))
{
return call_user_func($fn, self::ERROR_UNKNOWN_TRANSPORT, false, $req, $res);
}
$transport = $req->_query['transport'];
$sid = isset($req->_query['sid']) ? $req->_query['sid'] : '';
/*if ($transport === 'websocket' && empty($sid)) {
return call_user_func($fn, self::ERROR_UNKNOWN_TRANSPORT, false, $req, $res);
}*/
if($sid)
{
if(!isset($this->clients[$sid]))
{
return call_user_func($fn, self::ERROR_UNKNOWN_SID, false, $req, $res);
}
if(!$upgrade && $this->clients[$sid]->transport->name !== $transport)
{
return call_user_func($fn, self::ERROR_BAD_REQUEST, false, $req, $res);
}
}
else
{
if('GET' !== $req->method)
{
return call_user_func($fn, self::ERROR_BAD_HANDSHAKE_METHOD, false, $req, $res);
}
return $this->checkRequest($req, $res, $fn);
}
call_user_func($fn, null, true, $req, $res);
}
public function checkRequest($req, $res, $fn)
{
if ($this->origins === "*:*" || empty($this->origins))
{
return call_user_func($fn, null, true, $req, $res);
}
$origin = null;
if (isset($req->headers['origin']))
{
$origin = $req->headers['origin'];
}
else if(isset($req->headers['referer']))
{
$origin = $req->headers['referer'];
}
// file:// URLs produce a null Origin which can't be authorized via echo-back
if ('null' === $origin || null === $origin) {
return call_user_func($fn, null, true, $req, $res);
}
if ($origin)
{
$parts = parse_url($origin);
$defaultPort = 'https:' === $parts['scheme'] ? 443 : 80;
$parts['port'] = isset($parts['port']) ? $parts['port'] : $defaultPort;
$allowed_origins = explode(' ', $this->origins);
foreach( $allowed_origins as $allow_origin ){
$ok =
$allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'] ||
$allow_origin === $parts['scheme'] . '://' . $parts['host'] ||
$allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':*' ||
$allow_origin === '*:' . $parts['port'];
if($ok){
# 只需要有一个白名单通过,则都通过
return call_user_func($fn, null, $ok, $req, $res);
}
}
}
call_user_func($fn, null, false, $req, $res);
}
protected function prepare($req)
{
if(!isset($req->_query))
{
$info = parse_url($req->url);
if(isset($info['query']))
{
parse_str($info['query'], $req->_query);
}
}
}
public function handshake($transport, $req)
{
$id = bin2hex(pack('d', microtime(true)).pack('N', function_exists('random_int') ? random_int(1, 100000000): rand(1, 100000000)));
if ($transport == 'websocket') {
$transport = '\\PHPSocketIO\\Engine\\Transports\\WebSocket';
}
elseif (isset($req->_query['j']))
{
$transport = '\\PHPSocketIO\\Engine\\Transports\\PollingJsonp';
}
else
{
$transport = '\\PHPSocketIO\\Engine\\Transports\\PollingXHR';
}
$transport = new $transport($req);
$transport->supportsBinary = !isset($req->_query['b64']);
$socket = new Socket($id, $this, $transport, $req);
/* $transport->on('headers', function(&$headers)use($id)
{
$headers['Set-Cookie'] = "io=$id";
}); */
$transport->onRequest($req);
$this->clients[$id] = $socket;
$socket->once('close', array($this, 'onSocketClose'));
$this->emit('connection', $socket);
}
public function onSocketClose($id)
{
unset($this->clients[$id]);
}
public function attach($worker)
{
$this->server = $worker;
$worker->onConnect = array($this, 'onConnect');
}
public function onConnect($connection)
{
$connection->onRequest = array($this, 'handleRequest');
$connection->onWebSocketConnect = array($this, 'onWebSocketConnect');
// clean
$connection->onClose = function($connection)
{
if(!empty($connection->httpRequest))
{
$connection->httpRequest->destroy();
$connection->httpRequest = null;
}
if(!empty($connection->httpResponse))
{
$connection->httpResponse->destroy();
$connection->httpResponse = null;
}
if(!empty($connection->onRequest))
{
$connection->onRequest = null;
}
if(!empty($connection->onWebSocketConnect))
{
$connection->onWebSocketConnect = null;
}
};
}
public function onWebSocketConnect($connection, $req, $res)
{
$this->prepare($req);
$this->verify($req, $res, true, array($this, 'dealWebSocketConnect'));
}
public function dealWebSocketConnect($err, $success, $req, $res)
{
if (!$success)
{
self::sendErrorMessage($req, $res, $err);
return;
}
if(isset($req->_query['sid']))
{
if(!isset($this->clients[$req->_query['sid']]))
{
self::sendErrorMessage($req, $res, 'upgrade attempt for closed client');
return;
}
$client = $this->clients[$req->_query['sid']];
if($client->upgrading)
{
self::sendErrorMessage($req, $res, 'transport has already been trying to upgrade');
return;
}
if($client->upgraded)
{
self::sendErrorMessage($req, $res, 'transport had already been upgraded');
return;
}
$transport = new WebSocket($req);
$client->maybeUpgrade($transport);
}
else
{
$this->handshake($req->_query['transport'], $req);
}
}
}

View File

@ -0,0 +1,303 @@
<?php
namespace PHPSocketIO\Engine;
use \PHPSocketIO\Debug;
class Parser
{
public function __construct()
{
Debug::debug('Engine/Parser __construct');
}
public static $packets=array(
'open'=> 0 // non-ws
, 'close'=> 1 // non-ws
, 'ping'=> 2
, 'pong'=> 3
, 'message'=> 4
, 'upgrade'=> 5
, 'noop'=> 6
);
public static $packetsList = array(
'open',
'close',
'ping',
'pong',
'message',
'upgrade',
'noop'
);
public static $err = array(
'type' => 'error',
'data' => 'parser error'
);
public static function encodePacket($packet)
{
$data = !isset($packet['data']) ? '' : $packet['data'];
return self::$packets[$packet['type']].$data;
}
/**
* Encodes a packet with binary data in a base64 string
*
* @param {Object} packet, has `type` and `data`
* @return {String} base64 encoded message
*/
public static function encodeBase64Packet($packet)
{
$data = isset($packet['data']) ? '' : $packet['data'];
return $message = 'b' . self::$packets[$packet['type']] . base64_encode($packet['data']);
}
/**
* Decodes a packet. Data also available as an ArrayBuffer if requested.
*
* @return {Object} with `type` and `data` (if any)
* @api private
*/
public static function decodePacket($data, $binaryType = null, $utf8decode = true)
{
// String data todo check if (typeof data == 'string' || data === undefined)
if ($data[0] === 'b')
{
return self::decodeBase64Packet(substr($data, 1), $binaryType);
}
$type = $data[0];
if (!isset(self::$packetsList[$type]))
{
return self::$err;
}
if (isset($data[1]))
{
return array('type'=> self::$packetsList[$type], 'data'=> substr($data, 1));
}
else
{
return array('type'=> self::$packetsList[$type]);
}
}
/**
* Decodes a packet encoded in a base64 string.
*
* @param {String} base64 encoded message
* @return {Object} with `type` and `data` (if any)
*/
public static function decodeBase64Packet($msg, $binaryType)
{
$type = self::$packetsList[$msg[0]];
$data = base64_decode(substr($data, 1));
return array('type'=> $type, 'data'=> $data);
}
/**
* Encodes multiple messages (payload).
*
* <length>:data
*
* Example:
*
* 11:hello world2:hi
*
* If any contents are binary, they will be encoded as base64 strings. Base64
* encoded strings are marked with a b before the length specifier
*
* @param {Array} packets
* @api private
*/
public static function encodePayload($packets, $supportsBinary = null)
{
if ($supportsBinary)
{
return self::encodePayloadAsBinary($packets);
}
if (!$packets)
{
return '0:';
}
$results = '';
foreach($packets as $msg)
{
$results .= self::encodeOne($msg, $supportsBinary);
}
return $results;
}
public static function encodeOne($packet, $supportsBinary = null, $result = null)
{
$message = self::encodePacket($packet, $supportsBinary, true);
return strlen($message) . ':' . $message;
}
/*
* Decodes data when a payload is maybe expected. Possible binary contents are
* decoded from their base64 representation
*
* @api public
*/
public static function decodePayload($data, $binaryType = null)
{
if(!preg_match('/^\d+:\d/',$data))
{
return self::decodePayloadAsBinary($data, $binaryType);
}
if ($data === '')
{
// parser error - ignoring payload
return self::$err;
}
$length = '';//, n, msg;
for ($i = 0, $l = strlen($data); $i < $l; $i++)
{
$chr = $data[$i];
if (':' != $chr)
{
$length .= $chr;
}
else
{
if ('' == $length || ($length != ($n = intval($length))))
{
// parser error - ignoring payload
return self::$err;
}
$msg = substr($data, $i + 1/*, $n*/);
/*if ($length != strlen($msg))
{
// parser error - ignoring payload
return self::$err;
}*/
if (isset($msg[0]))
{
$packet = self::decodePacket($msg, $binaryType, true);
if (self::$err['type'] == $packet['type'] && self::$err['data'] == $packet['data'])
{
// parser error in individual packet - ignoring payload
return self::$err;
}
return $packet;
}
// advance cursor
$i += $n;
$length = '';
}
}
if ($length !== '')
{
// parser error - ignoring payload
echo new \Exception('parser error');
return self::$err;
}
}
/**
* Encodes multiple messages (payload) as binary.
*
* <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
* 255><data>
*
* Example:
* 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
*
* @param {Array} packets
* @return {Buffer} encoded payload
* @api private
*/
public static function encodePayloadAsBinary($packets)
{
$results = '';
foreach($packets as $msg)
{
$results .= self::encodeOneAsBinary($msg);
}
return $results;
}
public static function encodeOneAsBinary($p)
{
// todo is string or arraybuf
$packet = self::encodePacket($p, true, true);
$encodingLength = ''.strlen($packet);
$sizeBuffer = chr(0);
for ($i = 0; $i < strlen($encodingLength); $i++)
{
$sizeBuffer .= chr($encodingLength[$i]);
}
$sizeBuffer .= chr(255);
return $sizeBuffer.$packet;
}
/*
* Decodes data when a payload is maybe expected. Strings are decoded by
* interpreting each byte as a key code for entries marked to start with 0. See
* description of encodePayloadAsBinary
* @api public
*/
public static function decodePayloadAsBinary($data, $binaryType = null)
{
$bufferTail = $data;
$buffers = array();
while (strlen($bufferTail) > 0)
{
$strLen = '';
$isString = $bufferTail[0] == 0;
$numberTooLong = false;
for ($i = 1; ; $i++)
{
$tail = ord($bufferTail[$i]);
if ($tail === 255) break;
// 310 = char length of Number.MAX_VALUE
if (strlen($strLen) > 310)
{
$numberTooLong = true;
break;
}
$strLen .= $tail;
}
if($numberTooLong) return self::$err;
$bufferTail = substr($bufferTail, strlen($strLen) + 1);
$msgLength = intval($strLen, 10);
$msg = substr($bufferTail, 1, $msgLength + 1);
$buffers[] = $msg;
$bufferTail = substr($bufferTail, $msgLength + 1);
}
$total = count($buffers);
$packets = array();
foreach($buffers as $i => $buffer)
{
$packets[] = self::decodePacket($buffer, $binaryType, true);
}
return $packets;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace PHPSocketIO\Engine\Protocols\Http;
class Request
{
public $onData = null;
public $onEnd = null;
public $httpVersion = null;
public $headers = array();
public $rawHeaders = null;
public $method = null;
public $url = null;
public $connection = null;
public function __construct($connection, $raw_head)
{
$this->connection = $connection;
$this->parseHead($raw_head);
}
public function parseHead($raw_head)
{
$header_data = explode("\r\n", $raw_head);
list($this->method, $this->url, $protocol) = explode(' ', $header_data[0]);
list($null, $this->httpVersion) = explode('/', $protocol);
unset($header_data[0]);
foreach($header_data as $content)
{
if(empty($content))
{
continue;
}
$this->rawHeaders[] = $content;
list($key, $value) = explode(':', $content, 2);
$this->headers[strtolower($key)] = trim($value);
}
}
public function destroy()
{
$this->onData = $this->onEnd = $this->onClose = null;
$this->connection = null;
}
}

View File

@ -0,0 +1,206 @@
<?php
namespace PHPSocketIO\Engine\Protocols\Http;
class Response
{
public $statusCode = 200;
protected $_statusPhrase = null;
protected $_connection = null;
protected $_headers = array();
public $headersSent = false;
public $writable = true;
protected $_buffer = '';
public function __construct($connection)
{
$this->_connection = $connection;
}
protected function initHeader()
{
$this->_headers['Connection'] = 'keep-alive';
$this->_headers['Content-Type'] = 'Content-Type: text/html;charset=utf-8';
}
public function writeHead($status_code, $reason_phrase = '', $headers = null)
{
if($this->headersSent)
{
echo "header has already send\n";
return false;
}
$this->statusCode = $status_code;
if($reason_phrase)
{
$this->_statusPhrase = $reason_phrase;
}
if($headers)
{
foreach($headers as $key=>$val)
{
$this->_headers[$key] = $val;
}
}
$this->_buffer = $this->getHeadBuffer();
$this->headersSent = true;
}
public function getHeadBuffer()
{
if(!$this->_statusPhrase)
{
$this->_statusPhrase = isset(self::$codes[$this->statusCode]) ? self::$codes[$this->statusCode] : '';
}
$head_buffer = "HTTP/1.1 $this->statusCode $this->_statusPhrase\r\n";
if(!isset($this->_headers['Content-Length']) && !isset($this->_headers['Transfer-Encoding']))
{
$head_buffer .= "Transfer-Encoding: chunked\r\n";
}
if(!isset($this->_headers['Connection']))
{
$head_buffer .= "Connection: keep-alive\r\n";
}
foreach($this->_headers as $key=>$val)
{
if($key === 'Set-Cookie' && is_array($val))
{
foreach($val as $v)
{
$head_buffer .= "Set-Cookie: $v\r\n";
}
continue;
}
$head_buffer .= "$key: $val\r\n";
}
return $head_buffer."\r\n";
}
public function setHeader($key, $val)
{
$this->_headers[$key] = $val;
}
public function getHeader($name)
{
return isset($this->_headers[$name]) ? $this->_headers[$name] : '';
}
public function removeHeader($name)
{
unset($this->_headers[$name]);
}
public function write($chunk)
{
if(!isset($this->_headers['Content-Length']))
{
$chunk = dechex(strlen($chunk)) . "\r\n" . $chunk . "\r\n";
}
if(!$this->headersSent)
{
$head_buffer = $this->getHeadBuffer();
$this->_buffer = $head_buffer . $chunk;
$this->headersSent = true;
}
else
{
$this->_buffer .= $chunk;
}
}
public function end($data = null)
{
if(!$this->writable)
{
echo new \Exception('unwirtable');
return false;
}
if($data !== null)
{
$this->write($data);
}
if(!$this->headersSent)
{
$head_buffer = $this->getHeadBuffer();
$this->_buffer = $head_buffer;
$this->headersSent = true;
}
if(!isset($this->_headers['Content-Length']))
{
$ret = $this->_connection->send($this->_buffer . "0\r\n\r\n", true);
$this->destroy();
return $ret;
}
$ret = $this->_connection->send($this->_buffer, true);
$this->destroy();
return $ret;
}
public function destroy()
{
if(!empty($this->_connection->httpRequest))
{
$this->_connection->httpRequest->destroy();
}
if(!empty($this->_connection))
{
$this->_connection->httpResponse = $this->_connection->httpRequest = null;
}
$this->_connection = null;
$this->writable = false;
}
public static $codes = array(
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => '(Unused)',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
423 => 'Locked',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
);
}

View File

@ -0,0 +1,209 @@
<?php
namespace PHPSocketIO\Engine\Protocols;
use \PHPSocketIO\Engine\Protocols\WebSocket;
use \PHPSocketIO\Engine\Protocols\Http\Request;
use \PHPSocketIO\Engine\Protocols\Http\Response;
use \Workerman\Connection\TcpConnection;
class SocketIO
{
public static function input($http_buffer, $connection)
{
if(!empty($connection->hasReadedHead))
{
return strlen($http_buffer);
}
$pos = strpos($http_buffer, "\r\n\r\n");
if(!$pos)
{
if(strlen($http_buffer) >= $connection->maxPackageSize)
{
$connection->close("HTTP/1.1 400 bad request\r\n\r\nheader too long");
return 0;
}
return 0;
}
$head_len = $pos + 4;
$raw_head = substr($http_buffer, 0, $head_len);
$raw_body = substr($http_buffer, $head_len);
$req = new Request($connection, $raw_head);
$res = new Response($connection);
$connection->httpRequest = $req;
$connection->httpResponse = $res;
$connection->hasReadedHead = true;
TcpConnection::$statistics['total_request']++;
$connection->onClose = '\PHPSocketIO\Engine\Protocols\SocketIO::emitClose';
if(isset($req->headers['upgrade']) && strtolower($req->headers['upgrade']) === 'websocket')
{
$connection->consumeRecvBuffer(strlen($http_buffer));
WebSocket::dealHandshake($connection, $req, $res);
self::cleanup($connection);
return 0;
}
if(!empty($connection->onRequest))
{
$connection->consumeRecvBuffer(strlen($http_buffer));
self::emitRequest($connection, $req, $res);
if($req->method === 'GET' || $req->method === 'OPTIONS')
{
self::emitEnd($connection, $req);
return 0;
}
// POST
if('\PHPSocketIO\Engine\Protocols\SocketIO::onData' !== $connection->onMessage)
{
$connection->onMessage = '\PHPSocketIO\Engine\Protocols\SocketIO::onData';
}
if(!$raw_body)
{
return 0;
}
self::onData($connection, $raw_body);
return 0;
}
else
{
if($req->method === 'GET')
{
return $pos + 4;
}
elseif(isset($req->headers['content-length']))
{
return $req->headers['content-length'];
}
else
{
$connection->close("HTTP/1.1 400 bad request\r\n\r\ntrunk not support");
return 0;
}
}
}
public static function onData($connection, $data)
{
$req = $connection->httpRequest;
self::emitData($connection, $req, $data);
if((isset($req->headers['content-length']) && $req->headers['content-length'] <= strlen($data))
|| substr($data, -5) === "0\r\n\r\n")
{
self::emitEnd($connection, $req);
}
}
protected static function emitRequest($connection, $req, $res)
{
try
{
call_user_func($connection->onRequest, $req, $res);
}
catch(\Exception $e)
{
echo $e;
}
}
public static function emitClose($connection)
{
$req = $connection->httpRequest;
if(isset($req->onClose))
{
try
{
call_user_func($req->onClose, $req);
}
catch(\Exception $e)
{
echo $e;
}
}
$res = $connection->httpResponse;
if(isset($res->onClose))
{
try
{
call_user_func($res->onClose, $res);
}
catch(\Exception $e)
{
echo $e;
}
}
self::cleanup($connection);
}
public static function cleanup($connection)
{
if(!empty($connection->onRequest))
{
$connection->onRequest = null;
}
if(!empty($connection->onWebSocketConnect))
{
$connection->onWebSocketConnect = null;
}
if(!empty($connection->httpRequest))
{
$connection->httpRequest->destroy();
$connection->httpRequest = null;
}
if(!empty($connection->httpResponse))
{
$connection->httpResponse->destroy();
$connection->httpResponse = null;
}
}
public static function emitData($connection, $req, $data)
{
if(isset($req->onData))
{
try
{
call_user_func($req->onData, $req, $data);
}
catch(\Exception $e)
{
echo $e;
}
}
}
public static function emitEnd($connection, $req)
{
if(isset($req->onEnd))
{
try
{
call_user_func($req->onEnd, $req);
}
catch(\Exception $e)
{
echo $e;
}
}
$connection->hasReadedHead = false;
}
public static function encode($buffer, $connection)
{
if(!isset($connection->onRequest))
{
$connection->httpResponse->setHeader('Content-Length', strlen($buffer));
return $connection->httpResponse->getHeadBuffer() . $buffer;
}
return $buffer;
}
public static function decode($http_buffer, $connection)
{
if(isset($connection->onRequest))
{
return $http_buffer;
}
else
{
list($head, $body) = explode("\r\n\r\n", $http_buffer, 2);
return $body;
}
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace PHPSocketIO\Engine\Protocols;
use \PHPSocketIO\Engine\Protocols\Http\Request;
use \PHPSocketIO\Engine\Protocols\Http\Response;
use \PHPSocketIO\Engine\Protocols\WebSocket\RFC6455;
use \Workerman\Connection\TcpConnection;
/**
* WebSocket 协议服务端解包和打包
*/
class WebSocket
{
/**
* 最小包头
* @var int
*/
const MIN_HEAD_LEN = 7;
/**
* 检查包的完整性
* @param string $buffer
*/
public static function input($buffer, $connection)
{
if(strlen($buffer) < self::MIN_HEAD_LEN)
{
return 0;
}
// flash policy file
if(0 === strpos($buffer,'<policy'))
{
$policy_xml = '<?xml version="1.0"?><cross-domain-policy><site-control permitted-cross-domain-policies="all"/><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'."\0";
$connection->send($policy_xml, true);
$connection->consumeRecvBuffer(strlen($buffer));
return 0;
}
// http head
$pos = strpos($buffer, "\r\n\r\n");
if(!$pos)
{
if(strlen($buffer)>=TcpConnection::$maxPackageSize)
{
$connection->close("HTTP/1.1 400 bad request\r\n\r\nheader too long");
return 0;
}
return 0;
}
$req = new Request($connection, $buffer);
$res = new Response($connection);
$connection->consumeRecvBuffer(strlen($buffer));
return self::dealHandshake($connection, $req, $res);
$connection->consumeRecvBuffer($pos+4);
return 0;
}
/**
* 处理websocket握手
* @param string $buffer
* @param TcpConnection $connection
* @return int
*/
public static function dealHandshake($connection, $req, $res)
{
if(isset($req->headers['sec-websocket-key1']))
{
$res->writeHead(400);
$res->end("Not support");
return 0;
}
$connection->protocol = 'PHPSocketIO\Engine\Protocols\WebSocket\RFC6455';
return RFC6455::dealHandshake($connection, $req, $res);
}
}

View File

@ -0,0 +1,335 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace PHPSocketIO\Engine\Protocols\WebSocket;
use Workerman\Connection\ConnectionInterface;
/**
* WebSocket 协议服务端解包和打包
*/
class RFC6455 implements \Workerman\Protocols\ProtocolInterface
{
/**
* websocket头部最小长度
* @var int
*/
const MIN_HEAD_LEN = 6;
/**
* websocket blob类型
* @var char
*/
const BINARY_TYPE_BLOB = "\x81";
/**
* websocket arraybuffer类型
* @var char
*/
const BINARY_TYPE_ARRAYBUFFER = "\x82";
/**
* 检查包的完整性
* @param string $buffer
*/
public static function input($buffer, ConnectionInterface $connection)
{
// 数据长度
$recv_len = strlen($buffer);
// 长度不够
if($recv_len < self::MIN_HEAD_LEN)
{
return 0;
}
// $connection->websocketCurrentFrameLength有值说明当前fin为0则缓冲websocket帧数据
if($connection->websocketCurrentFrameLength)
{
// 如果当前帧数据未收全,则继续收
if($connection->websocketCurrentFrameLength > $recv_len)
{
// 返回0因为不清楚完整的数据包长度需要等待fin=1的帧
return 0;
}
}
else
{
$data_len = ord($buffer[1]) & 127;
$firstbyte = ord($buffer[0]);
$is_fin_frame = $firstbyte>>7;
$opcode = $firstbyte & 0xf;
switch($opcode)
{
// 附加数据帧 @todo 实现附加数据帧
case 0x0:
break;
// 文本数据帧
case 0x1:
break;
// 二进制数据帧
case 0x2:
break;
// 关闭的包
case 0x8:
// 如果有设置onWebSocketClose回调尝试执行
if(isset($connection->onWebSocketClose))
{
call_user_func($connection->onWebSocketClose, $connection);
}
// 默认行为是关闭连接
else
{
$connection->close();
}
return 0;
// ping的包
case 0x9:
// 如果有设置onWebSocketPing回调尝试执行
if(isset($connection->onWebSocketPing))
{
call_user_func($connection->onWebSocketPing, $connection);
}
// 默认发送pong
else
{
$connection->send(pack('H*', '8a00'), true);
}
// 从接受缓冲区中消费掉该数据包
if(!$data_len)
{
$connection->consumeRecvBuffer(self::MIN_HEAD_LEN);
return 0;
}
break;
// pong的包
case 0xa:
// 如果有设置onWebSocketPong回调尝试执行
if(isset($connection->onWebSocketPong))
{
call_user_func($connection->onWebSocketPong, $connection);
}
// 从接受缓冲区中消费掉该数据包
if(!$data_len)
{
$connection->consumeRecvBuffer(self::MIN_HEAD_LEN);
return 0;
}
break;
// 错误的opcode
default :
echo "error opcode $opcode and close websocket connection\n";
$connection->close();
return 0;
}
// websocket二进制数据
$head_len = self::MIN_HEAD_LEN;
if ($data_len === 126) {
$head_len = 8;
if($head_len > $recv_len)
{
return 0;
}
$pack = unpack('ntotal_len', substr($buffer, 2, 2));
$data_len = $pack['total_len'];
} else if ($data_len === 127) {
$head_len = 14;
if($head_len > $recv_len)
{
return 0;
}
$arr = unpack('N2', substr($buffer, 2, 8));
$data_len = $arr[1]*4294967296 + $arr[2];
}
$current_frame_length = $head_len + $data_len;
if($is_fin_frame)
{
return $current_frame_length;
}
else
{
$connection->websocketCurrentFrameLength = $current_frame_length;
}
}
// 收到的数据刚好是一个frame
if($connection->websocketCurrentFrameLength == $recv_len)
{
self::decode($buffer, $connection);
$connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
$connection->websocketCurrentFrameLength = 0;
return 0;
}
// 收到的数据大于一个frame
elseif($connection->websocketCurrentFrameLength < $recv_len)
{
self::decode(substr($buffer, 0, $connection->websocketCurrentFrameLength), $connection);
$connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
$current_frame_length = $connection->websocketCurrentFrameLength;
$connection->websocketCurrentFrameLength = 0;
// 继续读取下一个frame
return self::input(substr($buffer, $current_frame_length), $connection);
}
// 收到的数据不足一个frame
else
{
return 0;
}
}
/**
* 打包
* @param string $buffer
* @return string
*/
public static function encode($buffer, ConnectionInterface $connection)
{
$len = strlen($buffer);
if(empty($connection->websocketHandshake))
{
// 默认是utf8文本格式
$connection->websocketType = self::BINARY_TYPE_BLOB;
}
$first_byte = $connection->websocketType;
if($len<=125)
{
$encode_buffer = $first_byte.chr($len).$buffer;
}
else if($len<=65535)
{
$encode_buffer = $first_byte.chr(126).pack("n", $len).$buffer;
}
else
{
$encode_buffer = $first_byte.chr(127).pack("xxxxN", $len).$buffer;
}
// 还没握手不能发数据,先将数据缓冲起来,等握手完毕后发送
if(empty($connection->websocketHandshake))
{
if(empty($connection->websocketTmpData))
{
// 临时数据缓冲
$connection->websocketTmpData = '';
}
$connection->websocketTmpData .= $encode_buffer;
// 返回空,阻止发送
return '';
}
return $encode_buffer;
}
/**
* 解包
* @param string $buffer
* @return string
*/
public static function decode($buffer, ConnectionInterface $connection)
{
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
if($connection->websocketCurrentFrameLength)
{
$connection->websocketDataBuffer .= $decoded;
return $connection->websocketDataBuffer;
}
else
{
$decoded = $connection->websocketDataBuffer . $decoded;
$connection->websocketDataBuffer = '';
return $decoded;
}
}
/**
* 处理websocket握手
* @param string $buffer
* @param TcpConnection $connection
* @return int
*/
public static function dealHandshake($connection, $req, $res)
{
$headers = array();
if(isset($connection->onWebSocketConnect))
{
try
{
call_user_func_array($connection->onWebSocketConnect, array($connection, $req, $res));
}
catch (\Exception $e)
{
echo $e;
}
if(!$res->writable)
{
return false;
}
}
if(isset($req->headers['sec-websocket-key']))
{
$sec_websocket_key = $req->headers['sec-websocket-key'];
}
else
{
$res->writeHead(400);
$res->end('<b>400 Bad Request</b><br>Upgrade to websocket but Sec-WebSocket-Key not found.');
return 0;
}
// 标记已经握手
$connection->websocketHandshake = true;
// 缓冲fin为0的包直到fin为1
$connection->websocketDataBuffer = '';
// 当前数据帧的长度可能是fin为0的帧也可能是fin为1的帧
$connection->websocketCurrentFrameLength = 0;
// 当前帧的数据缓冲
$connection->websocketCurrentFrameBuffer = '';
// blob or arraybuffer
$connection->websocketType = self::BINARY_TYPE_BLOB;
$sec_websocket_accept = base64_encode(sha1($sec_websocket_key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));
$headers['Content-Length'] = 0;
$headers['Upgrade'] = 'websocket';
$headers['Sec-WebSocket-Version'] = 13;
$headers['Connection'] = 'Upgrade';
$headers['Sec-WebSocket-Accept'] = $sec_websocket_accept;
$res->writeHead(101, '', $headers);
$res->end();
// 握手后有数据要发送
if(!empty($connection->websocketTmpData))
{
$connection->send($connection->websocketTmpData, true);
$connection->websocketTmpData = '';
}
return 0;
}
}

View File

@ -0,0 +1,380 @@
<?php
namespace PHPSocketIO\Engine;
use \PHPSocketIO\Event\Emitter;
use \Workerman\Lib\Timer;
use \PHPSocketIO\Debug;
class Socket extends Emitter
{
public $id = 0;
public $server = null;
public $upgrading = false;
public $upgraded = false;
public $readyState = 'opening';
public $writeBuffer = array();
public $packetsFn = array();
public $sentCallbackFn = array();
public $request = null;
public $remoteAddress = '';
public $checkIntervalTimer = null;
public $upgradeTimeoutTimer = null;
public $pingTimeoutTimer = null;
public function __construct($id, $server, $transport, $req)
{
$this->id = $id;
$this->server = $server;
$this->request = $req;
$this->remoteAddress = $req->connection->getRemoteIp().':'.$req->connection->getRemotePort();
$this->setTransport($transport);
$this->onOpen();
Debug::debug('Engine/Socket __construct');
}
public function __destruct()
{
Debug::debug('Engine/Socket __destruct');
}
public function maybeUpgrade($transport)
{
$this->upgrading = true;
$this->upgradeTimeoutTimer = Timer::add(
$this->server->upgradeTimeout,
array($this, 'upgradeTimeoutCallback'),
array($transport), false
);
$this->upgradeTransport = $transport;
$transport->on('packet', array($this, 'onUpgradePacket'));
$transport->once('close', array($this, 'onUpgradeTransportClose'));
$transport->once('error', array($this, 'onUpgradeTransportError'));
$this->once('close', array($this, 'onUpgradeTransportClose'));
}
public function onUpgradePacket($packet)
{
if(empty($this->upgradeTransport))
{
$this->onError('upgradeTransport empty');
return;
}
if('ping' === $packet['type'] && (isset($packet['data']) && 'probe' === $packet['data']))
{
$this->upgradeTransport->send(array(array('type'=> 'pong', 'data'=> 'probe')));
//$this->transport->shouldClose = function(){};
if ($this->checkIntervalTimer) {
Timer::del($this->checkIntervalTimer);
}
$this->checkIntervalTimer = Timer::add(0.5, array($this, 'check'));
}
else if('upgrade' === $packet['type'] && $this->readyState !== 'closed')
{
$this->upgradeCleanup();
$this->upgraded = true;
$this->clearTransport();
$this->transport->destroy();
$this->setTransport($this->upgradeTransport);
$this->emit('upgrade', $this->upgradeTransport);
$this->upgradeTransport = null;
$this->setPingTimeout();
$this->flush();
if($this->readyState === 'closing')
{
$this->transport->close(array($this, 'onClose'));
}
}
else
{
if(!empty($this->upgradeTransport))
{
$this->upgradeCleanup();
$this->upgradeTransport->close();
$this->upgradeTransport = null;
}
}
}
public function upgradeCleanup()
{
$this->upgrading = false;
Timer::del($this->checkIntervalTimer);
Timer::del($this->upgradeTimeoutTimer);
if(!empty($this->upgradeTransport))
{
$this->upgradeTransport->removeListener('packet', array($this, 'onUpgradePacket'));
$this->upgradeTransport->removeListener('close', array($this, 'onUpgradeTransportClose'));
$this->upgradeTransport->removeListener('error', array($this, 'onUpgradeTransportError'));
}
$this->removeListener('close', array($this, 'onUpgradeTransportClose'));
}
public function onUpgradeTransportClose()
{
$this->onUpgradeTransportError('transport closed');
}
public function onUpgradeTransportError($err)
{
//echo $err;
$this->upgradeCleanup();
if($this->upgradeTransport)
{
$this->upgradeTransport->close();
$this->upgradeTransport = null;
}
}
public function upgradeTimeoutCallback($transport)
{
//echo("client did not complete upgrade - closing transport\n");
$this->upgradeCleanup();
if('open' === $transport->readyState)
{
$transport->close();
}
}
public function setTransport($transport)
{
$this->transport = $transport;
$this->transport->once('error', array($this, 'onError'));
$this->transport->on('packet', array($this, 'onPacket'));
$this->transport->on('drain', array($this, 'flush'));
$this->transport->once('close', array($this, 'onClose'));
//this function will manage packet events (also message callbacks)
$this->setupSendCallback();
}
public function onOpen()
{
$this->readyState = 'open';
// sends an `open` packet
$this->transport->sid = $this->id;
$this->sendPacket('open', json_encode(array(
'sid'=> $this->id
, 'upgrades' => $this->getAvailableUpgrades()
, 'pingInterval'=> $this->server->pingInterval*1000
, 'pingTimeout'=> $this->server->pingTimeout*1000
)));
$this->emit('open');
$this->setPingTimeout();
}
public function onPacket($packet)
{
if ('open' === $this->readyState) {
// export packet event
$this->emit('packet', $packet);
// Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness
$this->setPingTimeout();
switch ($packet['type']) {
case 'ping':
$this->sendPacket('pong');
$this->emit('heartbeat');
break;
case 'error':
$this->onClose('parse error');
break;
case 'message':
$this->emit('data', $packet['data']);
$this->emit('message', $packet['data']);
break;
}
}
else
{
echo('packet received with closed socket');
}
}
public function check()
{
if('polling' == $this->transport->name && $this->transport->writable)
{
$this->transport->send(array(array('type' => 'noop')));
}
}
public function onError($err)
{
$this->onClose('transport error', $err);
}
public function setPingTimeout()
{
if ($this->pingTimeoutTimer) {
Timer::del($this->pingTimeoutTimer);
}
$this->pingTimeoutTimer = Timer::add(
$this->server->pingInterval + $this->server->pingTimeout ,
array($this, 'pingTimeoutCallback'), null, false);
}
public function pingTimeoutCallback()
{
$this->transport->close();
$this->onClose('ping timeout');
}
public function clearTransport()
{
$this->transport->close();
Timer::del($this->pingTimeoutTimer);
}
public function onClose($reason = '', $description = null)
{
if ('closed' !== $this->readyState)
{
Timer::del($this->pingTimeoutTimer);
Timer::del($this->checkIntervalTimer);
$this->checkIntervalTimer = null;
Timer::del($this->upgradeTimeoutTimer);
// clean writeBuffer in next tick, so developers can still
// grab the writeBuffer on 'close' event
$this->writeBuffer = array();
$this->packetsFn = array();
$this->sentCallbackFn = array();
$this->clearTransport();
$this->readyState = 'closed';
$this->emit('close', $this->id, $reason, $description);
$this->server = null;
$this->request = null;
$this->upgradeTransport = null;
$this->removeAllListeners();
if(!empty($this->transport))
{
$this->transport->removeAllListeners();
$this->transport = null;
}
}
}
public function send($data, $options, $callback)
{
$this->sendPacket('message', $data, $callback);
return $this;
}
public function write($data, $options = array(), $callback = null)
{
return $this->send($data, $options, $callback);
}
public function sendPacket($type, $data = null, $callback = null)
{
if('closing' !== $this->readyState)
{
$packet = array(
'type'=> $type
);
if($data !== null)
{
$packet['data'] = $data;
}
// exports packetCreate event
$this->emit('packetCreate', $packet);
$this->writeBuffer[] = $packet;
//add send callback to object
if($callback)
{
$this->packetsFn[] = $callback;
}
$this->flush();
}
}
public function flush()
{
if ('closed' !== $this->readyState && $this->transport->writable
&& $this->writeBuffer)
{
$this->emit('flush', $this->writeBuffer);
$this->server->emit('flush', $this, $this->writeBuffer);
$wbuf = $this->writeBuffer;
$this->writeBuffer = array();
if($this->packetsFn)
{
if(!empty($this->transport->supportsFraming))
{
$this->sentCallbackFn[] = $this->packetsFn;
}
else
{
// @todo check
$this->sentCallbackFn[]=$this->packetsFn;
}
}
$this->packetsFn = array();
$this->transport->send($wbuf);
$this->emit('drain');
if($this->server)
{
$this->server->emit('drain', $this);
}
}
}
public function getAvailableUpgrades()
{
return array('websocket');
}
public function close()
{
if ('open' !== $this->readyState)
{
return;
}
$this->readyState = 'closing';
if ($this->writeBuffer) {
$this->once('drain', array($this, 'closeTransport'));
return;
}
$this->closeTransport();
}
public function closeTransport()
{
//todo onClose.bind(this, 'forced close'));
$this->transport->close(array($this, 'onClose'));
}
public function setupSendCallback()
{
$self = $this;
//the message was sent successfully, execute the callback
$this->transport->on('drain', array($this, 'onDrainCallback'));
}
public function onDrainCallback()
{
if ($this->sentCallbackFn)
{
$seqFn = array_shift($this->sentCallbackFn);
if(is_callable($seqFn))
{
echo('executing send callback');
call_user_func($seqFn, $this->transport);
}else if (is_array($seqFn)) {
echo('executing batch send callback');
foreach($seqFn as $fn)
{
call_user_func($fn, $this->transport);
}
}
}
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace PHPSocketIO\Engine;
use \PHPSocketIO\Event\Emitter;
use \PHPSocketIO\Debug;
class Transport extends Emitter
{
public $readyState = 'opening';
public $req = null;
public $res = null;
public function __construct()
{
Debug::debug('Transport __construct no access !!!!');
}
public function __destruct()
{
Debug::debug('Transport __destruct');
}
public function noop()
{
}
public function onRequest($req)
{
$this->req = $req;
}
public function close($fn = null)
{
$this->readyState = 'closing';
$fn = $fn ? $fn : array($this, 'noop');
$this->doClose($fn);
}
public function onError($msg, $desc = '')
{
if ($this->listeners('error'))
{
$err = array(
'type' => 'TransportError',
'description' => $desc,
);
$this->emit('error', $err);
}
else
{
echo("ignored transport error $msg $desc\n");
}
}
public function onPacket($packet)
{
$this->emit('packet', $packet);
}
public function onData($data)
{
$this->onPacket(Parser::decodePacket($data));
}
public function onClose()
{
$this->req = $this->res = null;
$this->readyState = 'closed';
$this->emit('close');
$this->removeAllListeners();
}
public function destroy()
{
$this->req = $this->res = null;
$this->readyState = 'closed';
$this->removeAllListeners();
$this->shouldClose = null;
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use PHPSocketIO\Engine\Transport;
use PHPSocketIO\Engine\Parser;
use \PHPSocketIO\Debug;
class Polling extends Transport
{
public $name = 'polling';
public $chunks = '';
public $shouldClose = null;
public $writable = false;
public function onRequest($req)
{
$res = $req->res;
if ('GET' === $req->method)
{
$this->onPollRequest($req, $res);
}
else if('POST' === $req->method)
{
$this->onDataRequest($req, $res);
}
else
{
$res->writeHead(500);
$res->end();
}
}
public function onPollRequest($req, $res)
{
if($this->req)
{
echo ('request overlap');
// assert: this.res, '.req and .res should be (un)set together'
$this->onError('overlap from client');
$res->writeHead(500);
return;
}
$this->req = $req;
$this->res = $res;
$req->onClose = array($this, 'pollRequestOnClose');
$req->cleanup = array($this, 'pollRequestClean');
$this->writable = true;
$this->emit('drain');
// if we're still writable but had a pending close, trigger an empty send
if ($this->writable && $this->shouldClose)
{
echo('triggering empty send to append close packet');
$this->send(array(array('type'=>'noop')));
}
}
public function pollRequestOnClose()
{
$this->onError('poll connection closed prematurely');
$this->pollRequestClean();
}
public function pollRequestClean()
{
if(isset($this->req))
{
$this->req->res = null;
$this->req->onClose = $this->req->cleanup = null;
$this->req = $this->res = null;
}
}
public function onDataRequest($req, $res)
{
if(isset($this->dataReq))
{
// assert: this.dataRes, '.dataReq and .dataRes should be (un)set together'
$this->onError('data request overlap from client');
$res->writeHead(500);
return;
}
$this->dataReq = $req;
$this->dataRes = $res;
$req->onClose = array($this, 'dataRequestOnClose');
$req->onData = array($this, 'dataRequestOnData');
$req->onEnd = array($this, 'dataRequestOnEnd');
}
public function dataRequestCleanup()
{
$this->chunks = '';
$this->dataReq->res = null;
$this->dataReq->onClose = $this->dataReq->onData = $this->dataReq->onEnd = null;
$this->dataReq = $this->dataRes = null;
}
public function dataRequestOnClose()
{
$this->dataRequestCleanup();
$this->onError('data request connection closed prematurely');
}
public function dataRequestOnData($req, $data)
{
$this->chunks .= $data;
// todo maxHttpBufferSize
/*if(strlen($this->chunks) > $this->maxHttpBufferSize)
{
$this->chunks = '';
$req->connection->destroy();
}*/
}
public function dataRequestOnEnd ()
{
$this->onData($this->chunks);
$headers = array(
'Content-Type'=> 'text/html',
'Content-Length'=> 2,
'X-XSS-Protection' => '0',
);
$this->dataRes->writeHead(200, '', $this->headers($this->dataReq, $headers));
$this->dataRes->end('ok');
$this->dataRequestCleanup();
}
public function onData($data)
{
$packets = Parser::decodePayload($data);
if(isset($packets['type']))
{
if('close' === $packets['type'])
{
$this->onClose();
return false;
}
else
{
$packets = array($packets);
}
}
foreach($packets as $packet)
{
$this->onPacket($packet);
}
}
public function onClose()
{
if($this->writable)
{
// close pending poll request
$this->send(array(array('type'=> 'noop')));
}
parent::onClose();
}
public function send($packets)
{
$this->writable = false;
if($this->shouldClose)
{
echo('appending close packet to payload');
$packets[] = array('type'=>'close');
call_user_func($this->shouldClose);
$this->shouldClose = null;
}
$data = Parser::encodePayload($packets, $this->supportsBinary);
$this->write($data);
}
public function write($data)
{
$this->doWrite($data);
if(!empty($this->req->cleanup))
{
call_user_func($this->req->cleanup);
}
}
public function doClose($fn)
{
if(!empty($this->dataReq))
{
//echo('aborting ongoing data request');
$this->dataReq->destroy();
}
if($this->writable)
{
//echo('transport writable - closing right away');
$this->send(array(array('type'=> 'close')));
call_user_func($fn);
}
else
{
//echo("transport not writable - buffering orderly close\n");
$this->shouldClose = $fn;
}
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use \PHPSocketIO\Debug;
class PollingJsonp extends Polling
{
public $head = null;
public $foot = ');';
public function __construct($req)
{
$j = isset($req->_query['j']) ? preg_replace('/[^0-9]/', '', $req->_query['j']) : '';
$this->head = "___eio[ $j ](";
Debug::debug('PollingJsonp __construct');
}
public function __destruct()
{
Debug::debug('PollingJsonp __destruct');
}
public function onData($data)
{
$parsed_data = null;
parse_str($data, $parsed_data);
$data = $parsed_data['d'];
// todo check
//client will send already escaped newlines as \\\\n and newlines as \\n
// \\n must be replaced with \n and \\\\n with \\n
/*data = data.replace(rSlashes, function(match, slashes) {
return slashes ? match : '\n';
});*/
call_user_func(array($this, 'parent::onData'), preg_replace('/\\\\n/', '\\n', $data));
}
public function doWrite($data)
{
$js = json_encode($data);
//$js = preg_replace(array('/\u2028/', '/\u2029/'), array('\\u2028', '\\u2029'), $js);
// prepare response
$data = $this->head . $js . $this->foot;
// explicit UTF-8 is required for pages not served under utf
$headers = array(
'Content-Type'=> 'text/javascript; charset=UTF-8',
'Content-Length'=> strlen($data),
'X-XSS-Protection'=>'0'
);
if(empty($this->res)){echo new \Exception('empty $this->res');return;}
$this->res->writeHead(200, '',$this->headers($this->req, $headers));
$this->res->end($data);
}
public function headers($req, $headers = array())
{
$listeners = $this->listeners('headers');
foreach($listeners as $listener)
{
$listener($headers);
}
return $headers;
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use \PHPSocketIO\Debug;
class PollingXHR extends Polling
{
public function __construct()
{
Debug::debug('PollingXHR __construct');
}
public function __destruct()
{
Debug::debug('PollingXHR __destruct');
}
public function onRequest($req)
{
if('OPTIONS' === $req->method)
{
$res = $req->res;
$headers = $this->headers($req);
$headers['Access-Control-Allow-Headers'] = 'Content-Type';
$res->writeHead(200, '', $headers);
$res->end();
}
else
{
parent::onRequest($req);
}
}
public function doWrite($data)
{
// explicit UTF-8 is required for pages not served under utf todo
//$content_type = $isString
// ? 'text/plain; charset=UTF-8'
// : 'application/octet-stream';
$content_type = preg_match('/^\d+:/', $data) ? 'text/plain; charset=UTF-8' : 'application/octet-stream';
$content_length = strlen($data);
$headers = array(
'Content-Type'=> $content_type,
'Content-Length'=> $content_length,
'X-XSS-Protection' => '0',
);
if(empty($this->res)){echo new \Exception('empty this->res');return;}
$this->res->writeHead(200, '', $this->headers($this->req, $headers));
$this->res->end($data);
}
public function headers($req, $headers = array())
{
if(isset($req->headers['origin']))
{
$headers['Access-Control-Allow-Credentials'] = 'true';
$headers['Access-Control-Allow-Origin'] = $req->headers['origin'];
}
else
{
$headers['Access-Control-Allow-Origin'] = '*';
}
$listeners = $this->listeners('headers');
foreach($listeners as $listener)
{
$listener($headers);
}
return $headers;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use \PHPSocketIO\Engine\Transport;
use \PHPSocketIO\Engine\Parser;
use \PHPSocketIO\Debug;
class WebSocket extends Transport
{
public $writable = true;
public $supportsFraming = true;
public $supportsBinary = true;
public $name = 'websocket';
public function __construct($req)
{
$this->socket = $req->connection;
$this->socket->onMessage = array($this, 'onData2');
$this->socket->onClose = array($this, 'onClose');
$this->socket->onError = array($this, 'onError2');
Debug::debug('WebSocket __construct');
}
public function __destruct()
{
Debug::debug('WebSocket __destruct');
}
public function onData2($connection, $data)
{
call_user_func(array($this, 'parent::onData'), $data);
}
public function onError2($conection, $code, $msg)
{
call_user_func(array($this, 'parent::onClose'), $code, $msg);
}
public function send($packets)
{
foreach($packets as $packet)
{
$data = Parser::encodePacket($packet, $this->supportsBinary);
if ($this->socket) {
$this->socket->send($data);
$this->emit('drain');
}
}
}
public function doClose($fn = null)
{
if($this->socket)
{
$this->socket->close();
$this->socket = null;
if(!empty($fn))
{
call_user_func($fn);
}
}
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace PHPSocketIO\Event;
use PHPSocketIO\Debug;
class Emitter
{
public function __construct()
{
Debug::debug('Emitter __construct');
}
public function __destruct()
{
Debug::debug('Emitter __destruct');
}
/**
* [event=>[[listener1, once?], [listener2,once?], ..], ..]
*/
protected $_eventListenerMap = array();
public function on($event_name, $listener)
{
$this->emit('newListener', $event_name, $listener);
$this->_eventListenerMap[$event_name][] = array($listener, 0);
return $this;
}
public function once($event_name, $listener)
{
$this->_eventListenerMap[$event_name][] = array($listener, 1);
return $this;
}
public function removeListener($event_name, $listener)
{
if(!isset($this->_eventListenerMap[$event_name]))
{
return $this;
}
foreach($this->_eventListenerMap[$event_name] as $key=>$item)
{
if($item[0] === $listener)
{
$this->emit('removeListener', $event_name, $listener);
unset($this->_eventListenerMap[$event_name][$key]);
}
}
if(empty($this->_eventListenerMap[$event_name]))
{
unset($this->_eventListenerMap[$event_name]);
}
return $this;
}
public function removeAllListeners($event_name = null)
{
$this->emit('removeListener', $event_name);
if(null === $event_name)
{
$this->_eventListenerMap = array();
return $this;
}
unset($this->_eventListenerMap[$event_name]);
return $this;
}
public function listeners($event_name)
{
if(empty($this->_eventListenerMap[$event_name]))
{
return array();
}
$listeners = array();
foreach($this->_eventListenerMap[$event_name] as $item)
{
$listeners[] = $item[0];
}
return $listeners;
}
public function emit($event_name = null)
{
if(empty($event_name) || empty($this->_eventListenerMap[$event_name]))
{
return false;
}
foreach($this->_eventListenerMap[$event_name] as $key=>$item)
{
$args = func_get_args();
unset($args[0]);
call_user_func_array($item[0], $args);
// once ?
if($item[1])
{
unset($this->_eventListenerMap[$event_name][$key]);
if(empty($this->_eventListenerMap[$event_name]))
{
unset($this->_eventListenerMap[$event_name]);
}
}
}
return true;
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace PHPSocketIO;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Parser\Parser;
class Nsp extends Emitter
{
public $name = null;
public $server = null;
public $rooms = array();
public $flags = array();
public $sockets = array();
public $connected = array();
public $fns = array();
public $ids = 0;
public $acks = array();
public static $events = array(
'connect' => 'connect', // for symmetry with client
'connection' => 'connection',
'newListener' => 'newListener'
);
//public static $flags = array('json','volatile');
public function __construct($server, $name)
{
$this->name = $name;
$this->server = $server;
$this->initAdapter();
Debug::debug('Nsp __construct');
}
public function __destruct()
{
Debug::debug('Nsp __destruct');
}
public function initAdapter()
{
$adapter_name = $this->server->adapter();
$this->adapter = new $adapter_name($this);
}
public function to($name)
{
if(!isset($this->rooms[$name]))
{
$this->rooms[$name] = $name;
}
return $this;
}
public function in($name)
{
return $this->to($name);
}
public function add($client, $nsp, $fn)
{
$socket_name = $this->server->socket();
$socket = new $socket_name($this, $client);
if('open' === $client->conn->readyState)
{
$this->sockets[$socket->id]=$socket;
$socket->onconnect();
if(!empty($fn)) call_user_func($fn, $socket, $nsp);
$this->emit('connect', $socket);
$this->emit('connection', $socket);
}
else
{
echo('next called after client was closed - ignoring socket');
}
}
/**
* Removes a client. Called by each `Socket`.
*
* @api private
*/
public function remove($socket)
{
// todo $socket->id
unset($this->sockets[$socket->id]);
}
/**
* Emits to all clients.
*
* @return {Namespace} self
* @api public
*/
public function emit($ev = null)
{
$args = func_get_args();
if (isset(self::$events[$ev]))
{
call_user_func_array(array(__CLASS__, 'parent::emit'), $args);
}
else
{
// set up packet object
$parserType = Parser::EVENT; // default
//if (self::hasBin($args)) { $parserType = Parser::BINARY_EVENT; } // binary
$packet = array('type'=> $parserType, 'data'=> $args );
if (is_callable(end($args)))
{
echo('Callbacks are not supported when broadcasting');
return;
}
$this->adapter->broadcast($packet, array(
'rooms'=> $this->rooms,
'flags'=> $this->flags
));
$this->rooms = array();
$this->flags = array();;
}
return $this;
}
public function send()
{
$args = func_get_args();
array_unshift($args, 'message');
$this->emit($args);
return $this;
}
public function write()
{
$args = func_get_args();
return call_user_func_array(array($this, 'send'), $args);
}
public function clients($fn)
{
$this->adapter->clients($this->rooms, $fn);
return $this;
}
/**
* Sets the compress flag.
*
* @param {Boolean} if `true`, compresses the sending data
* @return {Socket} self
* @api public
*/
public function compress($compress)
{
$this->flags['compress'] = $compress;
return $this;
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace PHPSocketIO\Parser;
use \PHPSocketIO\Parser\Parser;
use \PHPSocketIO\Event\Emitter;
use \PHPSocketIO\Debug;
class Decoder extends Emitter
{
public function __construct()
{
Debug::debug('Decoder __construct');
}
public function __destruct()
{
Debug::debug('Decoder __destruct');
}
public function add($obj)
{
if (is_string($obj))
{
$packet = self::decodeString($obj);
if(Parser::BINARY_EVENT == $packet['type'] || Parser::BINARY_ACK == $packet['type'])
{
// binary packet's json todo BinaryReconstructor
$this->reconstructor = new BinaryReconstructor(packet);
// no attachments, labeled binary but no binary data to follow
if ($this->reconstructor->reconPack->attachments === 0)
{
$this->emit('decoded', $packet);
}
} else { // non-binary full packet
$this->emit('decoded', $packet);
}
}
else if (isBuf($obj) || !empty($obj['base64']))
{ // raw binary data
if (!$this->reconstructor)
{
throw new \Exception('got binary data when not reconstructing a packet');
} else {
$packet = $this->reconstructor->takeBinaryData($obj);
if ($packet)
{ // received final buffer
$this->reconstructor = null;
$this->emit('decoded', $packet);
}
}
}
else {
throw new \Exception('Unknown type: ' + obj);
}
}
public function decodeString($str)
{
$p = array();
$i = 0;
// look up type
$p['type'] = $str[0];
if(!isset(Parser::$types[$p['type']])) return self::error();
// look up attachments if type binary
if(Parser::BINARY_EVENT == $p['type'] || Parser::BINARY_ACK == $p['type'])
{
$buf = '';
while ($str[++$i] != '-')
{
$buf .= $str[$i];
if($i == strlen(str)) break;
}
if ($buf != intval($buf) || $str[$i] != '-')
{
throw new \Exception('Illegal attachments');
}
$p['attachments'] = intval($buf);
}
// look up namespace (if any)
if(isset($str[$i + 1]) && '/' === $str[$i + 1])
{
$p['nsp'] = '';
while (++$i)
{
if ($i === strlen($str)) break;
$c = $str[$i];
if (',' === $c) break;
$p['nsp'] .= $c;
}
} else {
$p['nsp'] = '/';
}
// look up id
if(isset($str[$i+1]))
{
$next = $str[$i+1];
if ('' !== $next && strval((int)$next) === strval($next))
{
$p['id'] = '';
while (++$i)
{
$c = $str[$i];
if (null == $c || strval((int)$c) != strval($c))
{
--$i;
break;
}
$p['id'] .= $str[$i];
if($i == strlen($str)) break;
}
$p['id'] = (int)$p['id'];
}
}
// look up json data
if (isset($str[++$i]))
{
// todo try
$p['data'] = json_decode(substr($str, $i), true);
}
return $p;
}
public static function error()
{
return array(
'type'=> Parser::ERROR,
'data'=> 'parser error'
);
}
public function destroy()
{
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace PHPSocketIO\Parser;
use \PHPSocketIO\Parser\Parser;
use \PHPSocketIO\Event\Emitter;
use \PHPSocketIO\Debug;
class Encoder extends Emitter
{
public function __construct()
{
Debug::debug('Encoder __construct');
}
public function __destruct()
{
Debug::debug('Encoder __destruct');
}
public function encode($obj)
{
if(Parser::BINARY_EVENT == $obj['type'] || Parser::BINARY_ACK == $obj['type'])
{
echo new \Exception("not support BINARY_EVENT BINARY_ACK");
return array();
}
else
{
$encoding = self::encodeAsString($obj);
return array($encoding);
}
}
public static function encodeAsString($obj) {
$str = '';
$nsp = false;
// first is type
$str .= $obj['type'];
// attachments if we have them
if (Parser::BINARY_EVENT == $obj['type'] || Parser::BINARY_ACK == $obj['type'])
{
$str .= $obj['attachments'];
$str .= '-';
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (!empty($obj['nsp']) && '/' !== $obj['nsp'])
{
$nsp = true;
$str .= $obj['nsp'];
}
// immediately followed by the id
if (isset($obj['id']))
{
if($nsp)
{
$str .= ',';
$nsp = false;
}
$str .= $obj['id'];
}
// json data
if(isset($obj['data']))
{
if ($nsp) $str .= ',';
$str .= json_encode($obj['data']);
}
return $str;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace PHPSocketIO\Parser;
class Parser
{
/**
* Packet type `connect`.
*
* @api public
*/
const CONNECT = 0;
/**
* Packet type `disconnect`.
*
* @api public
*/
const DISCONNECT = 1;
/**
* Packet type `event`.
*
* @api public
*/
const EVENT = 2;
/**
* Packet type `ack`.
*
* @api public
*/
const ACK = 3;
/**
* Packet type `error`.
*
* @api public
*/
const ERROR = 4;
/**
* Packet type 'binary event'
*
* @api public
*/
const BINARY_EVENT = 5;
/**
* Packet type `binary ack`. For acks with binary arguments.
*
* @api public
*/
const BINARY_ACK = 6;
public static $types = array(
'CONNECT',
'DISCONNECT',
'EVENT',
'BINARY_EVENT',
'ACK',
'BINARY_ACK',
'ERROR'
);
}

View File

@ -0,0 +1,473 @@
<?php
namespace PHPSocketIO;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Parser\Parser;
class Socket extends Emitter
{
public $nsp = null;
public $server = null;
public $adapter = null;
public $id = null;
public $path = '/';
public $request = null;
public $client = null;
public $conn = null;
public $rooms = array();
public $_rooms = array();
public $flags = array();
public $acks = array();
public $connected = true;
public $disconnected = false;
public static $events = array(
'error'=>'error',
'connect' => 'connect',
'disconnect' => 'disconnect',
'newListener' => 'newListener',
'removeListener' => 'removeListener'
);
public static $flagsMap = array(
'json' => 'json',
'volatile' => 'volatile',
'broadcast' => 'broadcast'
);
public function __construct($nsp, $client)
{
$this->nsp = $nsp;
$this->server = $nsp->server;
$this->adapter = $this->nsp->adapter;
$this->id = ($nsp->name !== '/') ? $nsp->name .'#' .$client->id : $client->id;
$this->request = $client->request;
$this->client = $client;
$this->conn = $client->conn;
$this->handshake = $this->buildHandshake();
Debug::debug('IO Socket __construct');
}
public function __destruct()
{
Debug::debug('IO Socket __destruct');
}
public function buildHandshake()
{
//todo check this->request->_query
$info = !empty($this->request->url) ? parse_url($this->request->url) : array();
$query = array();
if(isset($info['query']))
{
parse_str($info['query'], $query);
}
return array(
'headers' => isset($this->request->headers) ? $this->request->headers : array(),
'time'=> date('D M d Y H:i:s') . ' GMT',
'address'=> $this->conn->remoteAddress,
'xdomain'=> isset($this->request->headers['origin']),
'secure' => !empty($this->request->connection->encrypted),
'issued' => time(),
'url' => isset($this->request->url) ? $this->request->url : '',
'query' => $query,
);
}
public function __get($name)
{
if($name === 'broadcast')
{
$this->flags['broadcast'] = true;
return $this;
}
return null;
}
public function emit($ev = null)
{
$args = func_get_args();
if (isset(self::$events[$ev]))
{
call_user_func_array(array(__CLASS__, 'parent::emit'), $args);
}
else
{
$packet = array();
// todo check
//$packet['type'] = hasBin($args) ? Parser::BINARY_EVENT : Parser::EVENT;
$packet['type'] = Parser::EVENT;
$packet['data'] = $args;
$flags = $this->flags;
// access last argument to see if it's an ACK callback
if (is_callable(end($args)))
{
if ($this->_rooms || isset($flags['broadcast']))
{
throw new \Exception('Callbacks are not supported when broadcasting');
}
echo('emitting packet with ack id ' . $this->nsp->ids);
$this->acks[$this->nsp->ids] = array_pop($args);
$packet['id'] = $this->nsp->ids++;
}
if ($this->_rooms || !empty($flags['broadcast']))
{
$this->adapter->broadcast($packet, array(
'except' => array($this->id => $this->id),
'rooms'=> $this->_rooms,
'flags' => $flags
));
}
else
{
// dispatch packet
$this->packet($packet);
}
// reset flags
$this->_rooms = array();
$this->flags = array();
}
return $this;
}
/**
* Targets a room when broadcasting.
*
* @param {String} name
* @return {Socket} self
* @api public
*/
public function to($name)
{
if(!isset($this->_rooms[$name]))
{
$this->_rooms[$name] = $name;
}
return $this;
}
public function in($name)
{
return $this->to($name);
}
/**
* Sends a `message` event.
*
* @return {Socket} self
* @api public
*/
public function send()
{
$args = func_get_args();
array_unshift($args, 'message');
call_user_func_array(array($this, 'emit'), $args);
return $this;
}
public function write()
{
$args = func_get_args();
array_unshift($args, 'message');
call_user_func_array(array($this, 'emit'), $args);
return $this;
}
/**
* Writes a packet.
*
* @param {Object} packet object
* @param {Object} options
* @api private
*/
public function packet($packet, $preEncoded = false)
{
if (!$this->nsp || !$this->client) return;
$packet['nsp'] = $this->nsp->name;
//$volatile = !empty(self::$flagsMap['volatile']);
$volatile = false;
$this->client->packet($packet, $preEncoded, $volatile);
}
/**
* Joins a room.
*
* @param {String} room
* @param {Function} optional, callback
* @return {Socket} self
* @api private
*/
public function join($room)
{
if (!$this->connected) return $this;
if(isset($this->rooms[$room])) return $this;
$this->adapter->add($this->id, $room);
$this->rooms[$room] = $room;
return $this;
}
/**
* Leaves a room.
*
* @param {String} room
* @param {Function} optional, callback
* @return {Socket} self
* @api private
*/
public function leave($room)
{
$this->adapter->del($this->id, $room);
unset($this->rooms[$room]);
return $this;
}
/**
* Leave all rooms.
*
* @api private
*/
public function leaveAll()
{
$this->adapter->delAll($this->id);
$this->rooms = array();
}
/**
* Called by `Namespace` upon succesful
* middleware execution (ie: authorization).
*
* @api private
*/
public function onconnect()
{
$this->nsp->connected[$this->id] = $this;
$this->join($this->id);
$this->packet(array(
'type' => Parser::CONNECT)
);
}
/**
* Called with each packet. Called by `Client`.
*
* @param {Object} packet
* @api private
*/
public function onpacket($packet)
{
switch ($packet['type'])
{
case Parser::EVENT:
$this->onevent($packet);
break;
case Parser::BINARY_EVENT:
$this->onevent($packet);
break;
case Parser::ACK:
$this->onack($packet);
break;
case Parser::BINARY_ACK:
$this->onack($packet);
break;
case Parser::DISCONNECT:
$this->ondisconnect();
break;
case Parser::ERROR:
$this->emit('error', $packet['data']);
}
}
/**
* Called upon event packet.
*
* @param {Object} packet object
* @api private
*/
public function onevent($packet)
{
$args = isset($packet['data']) ? $packet['data'] : array();
if (!empty($packet['id']) || (isset($packet['id']) && $packet['id'] === 0))
{
$args[] = $this->ack($packet['id']);
}
call_user_func_array(array(__CLASS__, 'parent::emit'), $args);
}
/**
* Produces an ack callback to emit with an event.
*
* @param {Number} packet id
* @api private
*/
public function ack($id)
{
$self = $this;
$sent = false;
return function()use(&$sent, $id, $self){
// prevent double callbacks
if ($sent) return;
$args = func_get_args();
$type = $this->hasBin($args) ? Parser::BINARY_ACK : Parser::ACK;
$self->packet(array(
'id' => $id,
'type' => $type,
'data' => $args
));
};
}
/**
* Called upon ack packet.
*
* @api private
*/
public function onack($packet)
{
$ack = $this->acks[$packet['id']];
if (is_callable($ack))
{
call_user_func($ack, $packet['data']);
unset($this->acks[$packet['id']]);
} else {
echo ('bad ack '. packet.id);
}
}
/**
* Called upon client disconnect packet.
*
* @api private
*/
public function ondisconnect()
{
//echo('got disconnect packet');
$this->onclose('client namespace disconnect');
}
/**
* Handles a client error.
*
* @api private
*/
public function onerror($err)
{
if ($this->listeners('error'))
{
$this->emit('error', $err);
}
else
{
//echo('Missing error handler on `socket`.');
}
}
/**
* Called upon closing. Called by `Client`.
*
* @param {String} reason
* @param {Error} optional error object
* @api private
*/
public function onclose($reason)
{
if (!$this->connected) return $this;
$this->emit('disconnect', $reason);
$this->leaveAll();
$this->nsp->remove($this);
$this->client->remove($this);
$this->connected = false;
$this->disconnected = true;
unset($this->nsp->connected[$this->id]);
// ....
$this->nsp = null;
$this->server = null;
$this->adapter = null;
$this->request = null;
$this->client = null;
$this->conn = null;
$this->removeAllListeners();
}
/**
* Produces an `error` packet.
*
* @param {Object} error object
* @api private
*/
public function error($err)
{
$this->packet(array(
'type' => Parser::ERROR, 'data' => $err )
);
}
/**
* Disconnects this client.
*
* @param {Boolean} if `true`, closes the underlying connection
* @return {Socket} self
* @api public
*/
public function disconnect( $close = false )
{
if (!$this->connected) return $this;
if ($close)
{
$this->client->disconnect();
} else {
$this->packet(array(
'type'=> Parser::DISCONNECT
));
$this->onclose('server namespace disconnect');
}
return $this;
}
/**
* Sets the compress flag.
*
* @param {Boolean} if `true`, compresses the sending data
* @return {Socket} self
* @api public
*/
public function compress($compress)
{
$this->flags['compress'] = $compress;
return $this;
}
protected function hasBin($args) {
$hasBin = false;
array_walk_recursive($args, function($item, $key) use ($hasBin) {
if (!ctype_print($item)) {
$hasBin = true;
}
});
return $hasBin;
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace PHPSocketIO;
use Workerman\Worker;
use PHPSocketIO\Engine\Engine;
class SocketIO
{
public $nsps = array();
protected $_nsp = null;
protected $_socket = null;
protected $_adapter = null;
public $eio = null;
public $engine = null;
protected $_origins = '*:*';
protected $_path = null;
public function __construct($port = null, $opts = array())
{
$nsp = isset($opts['nsp']) ? $opts['nsp'] : '\PHPSocketIO\Nsp';
$this->nsp($nsp);
$socket = isset($opts['socket']) ? $opts['socket'] : '\PHPSocketIO\Socket';
$this->socket($socket);
$adapter = isset($opts['adapter']) ? $opts['adapter'] : '\PHPSocketIO\DefaultAdapter';
$this->adapter($adapter);
if(isset($opts['origins']))
{
$this->origins($opts['origins']);
}
unset($opts['nsp'], $opts['socket'], $opts['adapter'], $opts['origins']);
$this->sockets = $this->of('/');
if(!class_exists('Protocols\SocketIO'))
{
class_alias('PHPSocketIO\Engine\Protocols\SocketIO', 'Protocols\SocketIO');
}
if($port)
{
$worker = new Worker('SocketIO://0.0.0.0:'.$port, $opts);
$worker->name = 'PHPSocketIO';
if(isset($opts['ssl'])) {
$worker->transport = 'ssl';
}
$this->attach($worker);
}
}
public function nsp($v = null)
{
if (empty($v)) return $this->_nsp;
$this->_nsp = $v;
return $this;
}
public function socket($v = null)
{
if (empty($v)) return $this->_socket;
$this->_socket = $v;
return $this;
}
public function adapter($v = null)
{
if (empty($v)) return $this->_adapter;
$this->_adapter = $v;
foreach($this->nsps as $nsp)
{
$nsp->initAdapter();
}
return $this;
}
public function origins($v = null)
{
if ($v === null) return $this->_origins;
$this->_origins = $v;
if(isset($this->engine)) {
$this->engine->origins = $this->_origins;
}
return $this;
}
public function attach($srv, $opts = array())
{
$engine = new Engine();
$this->eio = $engine->attach($srv, $opts);
// Export http server
$this->worker = $srv;
// bind to engine events
$this->bind($engine);
return $this;
}
public function bind($engine)
{
$this->engine = $engine;
$this->engine->on('connection', array($this, 'onConnection'));
$this->engine->origins = $this->_origins;
return $this;
}
public function of($name, $fn = null)
{
if($name[0] !== '/')
{
$name = "/$name";
}
if(empty($this->nsps[$name]))
{
$nsp_name = $this->nsp();
$this->nsps[$name] = new $nsp_name($this, $name);
}
if ($fn)
{
$this->nsps[$name]->on('connect', $fn);
}
return $this->nsps[$name];
}
public function onConnection($engine_socket)
{
$client = new Client($this, $engine_socket);
$client->connect('/');
return $this;
}
public function on()
{
$args = array_pad(func_get_args(), 2, null);
if ($args[0] === 'workerStart') {
$this->worker->onWorkerStart = $args[1];
} else if ($args[0] === 'workerStop') {
$this->worker->onWorkerStop = $args[1];
} else if ($args[0] !== null) {
return call_user_func_array(array($this->sockets, 'on'), $args);
}
}
public function in()
{
return call_user_func_array(array($this->sockets, 'in'), func_get_args());
}
public function to()
{
return call_user_func_array(array($this->sockets, 'to'), func_get_args());
}
public function emit()
{
return call_user_func_array(array($this->sockets, 'emit'), func_get_args());
}
public function send()
{
return call_user_func_array(array($this->sockets, 'send'), func_get_args());
}
public function write()
{
return call_user_func_array(array($this->sockets, 'write'), func_get_args());
}
}

View File

@ -0,0 +1,15 @@
<?php
spl_autoload_register(function($name){
$path = str_replace('\\', DIRECTORY_SEPARATOR ,$name);
$path = str_replace('PHPSocketIO', '', $path);
if(is_file($class_file = __DIR__ . "/$path.php"))
{
require_once($class_file);
if(class_exists($name, false))
{
return true;
}
}
return false;
});

View File

@ -0,0 +1,28 @@
<?php
$rootPath = join(DIRECTORY_SEPARATOR, array(__DIR__,".."));
include join(DIRECTORY_SEPARATOR, array($rootPath,"vendor","autoload.php"));
include join(DIRECTORY_SEPARATOR, array($rootPath,"src","Event","Emitter.php"));
ini_set('display_errors', 'on');
$emitter = new PHPSocketIO\Event\Emitter;
$func = function($arg1, $arg2)
{
var_dump($arg1, $arg2);
};
$emitter->on('removeListener', function($event_name, $func){echo $event_name,':',var_export($func, true),"removed\n";});
$emitter->on('newListener', function($event_name, $func){echo $event_name,':',var_export($func, true)," added\n";});
$emitter->on('test', $func);
$emitter->on('test', $func);
$emitter->emit('test', 1 ,2);
echo "----------------------\n";
$emitter->once('test', $func);
$emitter->emit('test', 3 ,4);
echo "----------------------\n";
$emitter->emit('test', 4 ,4);
echo "----------------------\n";
$emitter->removeListener('test', $func)->emit('test', 5 ,6);
echo "----------------------\n";
$emitter->on('test2', function(){echo "test2\n";});
var_dump($emitter->listeners('test2'));

View File

@ -1,3 +1,12 @@
<!--
* @Author: TaoLer <317927823@qq.com>
* @Date: 2021-12-06 16:04:51
* @LastEditTime: 2022-07-30 07:24:25
* @LastEditors: TaoLer
* @Description: 优化版
* @FilePath: \github\TaoLer\view\404.html
* Copyright (c) 2020~2022 https://www.aieok.com All rights reserved.
-->
{extend name="public/base" /}
{block name="title"}404 - {$sysInfo.webname}{/block}
@ -16,22 +25,7 @@
{/block}
{block name="script"}
<script>
layui.cache.user = {
username: '{$user.name??'游客'}'
,uid: {$user.id ? $user.id : -1}
,avatar: '{if condition="$user['user_img'] neq ''"}{$user['user_img']}{else /}/static/res/images/avatar/00.jpg{/if}'
,experience: 83
,sex: '{if condition="$user['sex'] eq 0"}男{else/}女{/if}'
};
layui.config({
version: "3.0.0"
,base: '/static/res/mods/'
}).extend({
fly: 'index'
}).use('fly');
</script>
404
{/block}

View File

@ -10,10 +10,7 @@
<meta property="bytedance:lrDate_time" content="{$lrDate_time|date='c'}" />
<meta property="bytedance:updated_time" content="{$article.update_time|date='c'}" />
{/block}
{block name="link"}<link rel="stylesheet" href="{$Request.domain}/static/res/css/plyr.css" charset="utf-8">
<link rel="stylesheet" type="text/css" id="mce-u0" href="http://www.tp6.com/addons/taonyeditor/tinymce/skins/ui/oxide/content.min.css">
<link rel="stylesheet" type="text/css" id="mce-u1" href="http://www.tp6.com/addons/taonyeditor/tinymce/skins/content/default/content.min.css">
{/block}
{block name="link"}<link rel="stylesheet" href="{$Request.domain}/static/res/css/plyr.css" charset="utf-8">{/block}
{block name="column"}<div class="layui-hide-xs">{include file="/public/column" /}</div>{/block}
{block name="content"}
<div class="layui-container">

View File

@ -1,7 +1,7 @@
<!--
* @Author: TaoLer <alipay_tao@qq.com>
* @Date: 2021-12-06 16:04:51
* @LastEditTime: 2022-07-26 13:43:10
* @LastEditTime: 2022-07-30 07:20:01
* @LastEditors: TaoLer
* @Description: 搜索引擎SEO优化设置
* @FilePath: \github\TaoLer\view\taoler\index\public\base.html
@ -73,7 +73,7 @@
fly: 'index'
}).use('fly');
</script>
{block name="script"}{/block}
{block name="script"} {/block}
</body>
</html>

View File

@ -1,3 +1,12 @@
<!--
* @Author: TaoLer <317927823@qq.com>
* @Date: 2021-12-06 16:04:51
* @LastEditTime: 2022-07-30 08:27:32
* @LastEditors: TaoLer
* @Description: 优化版
* @FilePath: \github\TaoLer\view\taoler\index\public\column.html
* Copyright (c) 2020~2022 https://www.aieok.com All rights reserved.
-->
<div class="fly-panel fly-column layui-hide-xs">
<div class="layui-container">
<ul class="layui-clear">

View File

@ -11,6 +11,7 @@
{//websocket统计脚本}
<div style="text-align:center;color:#999;font-size:14px;padding:0 0 10px;" id="online_count"></div>
</div>
{:hook('showLeftLayer')}
<script>
var $ = layui.jquery;

View File

@ -1,10 +1,10 @@
<!--
* @Author: TaoLer <alipay_tao@qq.com>
* @Date: 2021-12-06 16:04:51
* @LastEditTime: 2022-07-19 11:05:29
* @LastEditTime: 2022-07-30 07:08:19
* @LastEditors: TaoLer
* @Description: 搜索引擎SEO优化设置
* @FilePath: \TaoLer\view\taoler\index\public\user.html
* @FilePath: \github\TaoLer\view\taoler\index\public\user.html
* Copyright (c) 2020~2022 https://www.aieok.com All rights reserved.
-->
<!DOCTYPE html>
@ -34,7 +34,7 @@
<script>
layui.cache.page = 'user';
layui.cache.user = {
username: "{$user.name??'游客'}"
username: "{$user.name ??'游客'}"
,uid: "{$user.id ?? -1}"
,avatar: "{$user['user_img'] ?? '/static/res/images/avatar/00.jpg'}"
,experience: "{$user.point ?? ''}"