2.0.0版本发布

This commit is contained in:
545522390@qq.com 2019-01-17 11:05:47 +08:00
commit f0fee5ab58
459 changed files with 97928 additions and 0 deletions

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
*.js linguist-language=php
*.css linguist-language=php
*.html linguist-language=php

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
!.gitignore
!.gitattributes
*.DS_Store
*.idea
*.svn
*.git
/runtime
/log
/vendor
/static/upload
!composer.json
/composer.lock

8
.htaccess Normal file
View File

@ -0,0 +1,8 @@
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
</IfModule>

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017 Anyon <zoujingli@qq.com>
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.

74
README.md Normal file
View File

@ -0,0 +1,74 @@
# pearProjectApi
基于Vue.js实现的项目管理系统
需要配合[前端项目](https://github.com/a54552239/pearProject)使用链接https://github.com/a54552239/pearProject
有不明白的地方的可以加群275264059或者联系我QQ545522390
### 演示地址
> [https://beta.vilson.xyz](https://beta.vilson.xyz)
### 登陆 ###
账号123456 密码123456
### 界面截图
![1](https://static.vilson.xyz/overview/1.png)
![1](https://static.vilson.xyz/overview/2.png)
![1](https://static.vilson.xyz/overview/3.png)
![1](https://static.vilson.xyz/overview/4.png)
![1](https://static.vilson.xyz/overview/5.png)
![1](https://static.vilson.xyz/overview/6.png)
![1](https://static.vilson.xyz/overview/7.png)
![1](https://static.vilson.xyz/overview/8.png)
![1](https://static.vilson.xyz/overview/9.png)
![1](https://static.vilson.xyz/overview/10.png)
![1](https://static.vilson.xyz/overview/11.png)
![1](https://static.vilson.xyz/overview/12.png)
![1](https://static.vilson.xyz/overview/13.png)
### 安装步骤 ###
```
PHP >= 7.0.0 (推荐PHP7.2版本)
Mysql >= 5.5.0 (需支持innodb引擎)
PDO PHP Extension
Node.js
Composer
```
- 可以直接下载[phpstudy](http://phpstudy.php.cn/phpstudy/PhpStudy20180211.zip)部署环境
1. 下载后端接口文件解压到站点根目录或使用Git: git clone https://github.com/a54552239/pearProjectApi
2. 安装后端依赖
1. 进入接口文件目录
2. 方式一Composer
3. 方式二:下载[vendor.zip](https://static.vilson.xyz/help/pearproject/vendor.zip)直接解压到项目根目录覆盖原有的vender文件夹
3. 下载前端项目
4. 安装node.js
1. 地址http://nodejs.cn/download/ 根据情况选择版本
2. 安装npm淘宝镜像
1. 运行cmd
2. 输入npm install -g cnpm --registry=https://registry.npm.taobao.org
5. 安装前端依赖
1. 进入前端项目目录运行cmd命令行
2. 安装依赖cnpm install
1.如果接口端口不是默认端口,需修改./vue.config.js将DEV_URL的值改为接口的访问地址
3. 启动项目npm run serve
4. 根据提示填写数据库信息进行安装
![1](https://static.vilson.xyz/help/pearproject/3.png)
6. 打包项目(有必要的话)
1. 修改./src/config/config.js修改PRO_URL地址
2. 修改./vue.config.js将publicPath 值改为‘/。如果有CDN的话改为CDN地址
3. 运行cmd输入 npm run build
4. 运行dist目录下的index.html或者将dist目录下的文件部署到服务器上
### 鼓励一下 ###
<img src="https://static.vilson.xyz/pay/wechat.png" alt="Sample" width="150" height="150">
<img src="https://static.vilson.xyz/pay/alipay2.png" alt="Sample" width="150" height="150">

1
application/.htaccess Normal file
View File

@ -0,0 +1 @@
deny from all

271
application/common.php Normal file
View File

@ -0,0 +1,271 @@
<?php
use service\DataService;
use service\NodeService;
use service\RandomService;
use think\Db;
use think\facade\Cache;
use think\facade\Request;
/**
* 获取默认分页信息
* @return int
*/
function defaultRows()
{
$rows = intval(Request::param('rows', cookie('page-rows')));
if (!$rows) {
$rows = 20;
}
cookie('page-rows', $rows);
return $rows;
}
function isDebug()
{
return config('app.app_debug');
}
/**
* 打印输出数据到文件
* @param mixed $data 输出的数据
* @param bool $force 强制替换
* @param string|null $file
*/
function p($data, $force = false, $file = null)
{
is_null($file) && $file = env('runtime_path') . date('Ymd') . '.txt';
$str = (is_string($data) ? $data : (is_array($data) || is_object($data)) ? print_r($data, true) : var_export($data, true)) . PHP_EOL;
$force ? file_put_contents($file, $str) : file_put_contents($file, $str, FILE_APPEND);
}
/**
* RBAC节点权限验证
* @param string $node
* @param string $moduleApp
* @return bool
*/
function auth($node, $moduleApp = 'project')
{
return NodeService::checkAuthNode($node, $moduleApp);
}
/**
* 生产表唯一标记
* @param string $tableName 表名
* @param string $fieldName 字段名
* @param int $len 长度
* @return string
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
function createUniqueCode($tableName, $fieldName = 'code', $len = 24)
{
$code = RandomService::alnumLowercase($len);
$has = Db::name($tableName)->where([$fieldName => $code])->field($fieldName)->find();
if ($has) {
return createUniqueCode($tableName, $fieldName, $len);
}
return $code;
}
/**
* 设备或配置系统参数
* @param string $name 参数名称
* @param bool $value 默认是null为获取值否则为更新
* @return string|bool
* @throws \think\Exception
* @throws \think\exception\PDOException
*/
function sysconf($name, $value = null)
{
static $config = [];
if ($value !== null) {
list($config, $data) = [[], ['name' => $name, 'value' => $value]];
return DataService::save('SystemConfig', $data, 'name');
}
if (empty($config)) {
$config = Db::name('SystemConfig')->column('name,value');
}
return isset($config[$name]) ? $config[$name] : '';
}
/**
* 日期格式标准输出
* @param string $datetime 输入日期
* @param string $format 输出格式
* @return false|string
*/
function format_datetime($datetime, $format = 'Y年m月d日 H:i:s')
{
return date($format, strtotime($datetime));
}
function nowTime()
{
return date('Y-m-d H:i:s', time());
}
// 判断文件或目录是否有写的权限
function is_really_writable($file)
{
if (DIRECTORY_SEPARATOR == '/' AND @ ini_get("safe_mode") == FALSE) {
return is_writable($file);
}
if (!is_file($file) OR ($fp = @fopen($file, "r+")) === FALSE) {
return FALSE;
}
fclose($fp);
return TRUE;
}
/**
* UTF8字符串加密
* @param string $string
* @return string
*/
function encode($string)
{
list($chars, $length) = ['', strlen($string = iconv('utf-8', 'gbk', $string))];
for ($i = 0; $i < $length; $i++) {
$chars .= str_pad(base_convert(ord($string[$i]), 10, 36), 2, 0, 0);
}
return $chars;
}
/**
* UTF8字符串解密
* @param string $string
* @return string
*/
function decode($string)
{
$chars = '';
foreach (str_split($string, 2) as $char) {
$chars .= chr(intval(base_convert($char, 36, 10)));
}
return @iconv('gbk', 'utf-8', $chars);
}
/**
* 获取锁
* @param String $key 锁标识
* @param Int $expire 锁过期时间
* @return Boolean
*/
function lock($key = '', $expire = 5)
{
$is_lock = Cache::store('redis')->get($key);
//不能获取锁
if (!$is_lock) {
Cache::store('redis')->set($key, time() + $expire);
}
return $is_lock ? true : false;
}
/**
* 释放锁
* @param String $key 锁标识
* @return Boolean
*/
function unlock($key = '')
{
return Cache::store('redis')->rm($key);
}
/**
* 下载远程文件到本地
* @param string $url 远程图片地址
* @return string
*/
function local_image($url)
{
return \service\FileService::download($url)['url'];
}
/**
* 提取base64
* @param $base64_url
* @return array
*/
function decodeFile($base64_url)
{
preg_match('/^data:image\/(\w+);base64/', $base64_url, $out);
$type = $out[1];
$type_param = 'data:image/' . $type . ';base64,';
$fileStream = str_replace($type_param, '', $base64_url);
$fileStream = base64_decode($fileStream);
return array(
'type' => $type,
'fileStream' => $fileStream
);
}
//不同环境下获取真实的IP
function get_ip()
{
//判断服务器是否允许$_SERVER
if (isset($_SERVER)) {
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$realip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
$realip = $_SERVER['HTTP_CLIENT_IP'];
} else {
$realip = $_SERVER['REMOTE_ADDR'];
}
} else {
//不允许就使用getenv获取
if (getenv("HTTP_X_FORWARDED_FOR")) {
$realip = getenv("HTTP_X_FORWARDED_FOR");
} elseif (getenv("HTTP_CLIENT_IP")) {
$realip = getenv("HTTP_CLIENT_IP");
} else {
$realip = getenv("REMOTE_ADDR");
}
}
return $realip;
}
/**
* DES 加密
* @param $dat 需要加密的字符串
* @param $key 加密密钥
* @return string
*/
function javaDesEncrypt($dat, $key)
{
/*$block = mcrypt_get_block_size(MCRYPT_DES, MCRYPT_MODE_ECB);
$len = strlen($dat);
$padding = $block - ($len % $block);
$dat .= str_repeat(chr($padding),$padding);
return bin2hex(mcrypt_encrypt(MCRYPT_DES, $key, $dat, MCRYPT_MODE_ECB));*/
return bin2hex(openssl_encrypt($dat, 'des-ecb', $key, OPENSSL_RAW_DATA));
}
/**
* DES 解密
* @param $dat 需要解密的字符串
* @param $key 加密密钥
* @return bool|string
*/
function javaDesDecrypt($dat, $key)
{
/*$str = hex2bin($dat);
$str = mcrypt_decrypt(MCRYPT_DES, $key, $str, MCRYPT_MODE_ECB);
$pad = ord($str[($len = strlen($str)) - 1]);
return substr($str, 0, strlen($str) - $pad);*/
$str = hex2bin($dat);
return openssl_decrypt($str, 'des-ecb', $key, OPENSSL_RAW_DATA);
}

View File

@ -0,0 +1,31 @@
<?php
namespace app\common\Model;
use service\ToolsService;
use think\facade\Cache;
class Areas extends CommonModel
{
protected $append = [];
/**
* 构建AntDesign所需的行政区划数据
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function createJsonForAnt()
{
$list = Cache::store('redis')->get('areadData');
if (!$list) {
$list = self::where('id > 100000')->order('id asc')->select()->toArray();
Cache::store('redis')->set('areadData', $list);
}
if ($list) {
$list = ToolsService::arr2tree($list, 'ID', 'ParentId');
}
return $list;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace app\common\Model;
/**
* 收藏
* Class TaskStar
* @package app\common\Model
*/
class Collection extends CommonModel
{
protected $append = [];
/**
* @param $code
* @param $memberCode
* @param $star
* @return TaskLike|bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function starTask($code, $memberCode, $star)
{
$stared = self::where(['source_code' => $code, 'type' => 'task', 'member_code' => $memberCode])->find();
if ($star && !$stared) {
$data = [
'create_time' => nowTime(),
'code' => createUniqueCode('collection'),
'create_by' => $memberCode,
'source_code' => $code,
'type' => 'task',
'member_code' => $memberCode,
];
return self::create($data);
}
if (!$star) {
return self::where(['source_code' => $code, 'type' => 'task', 'member_code' => $memberCode])->delete();
}
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace app\common\Model;
use app\shop\Model\ShopGoods;
use service\FileService;
use service\ToolsService;
use think\facade\Request;
use think\File;
use think\Model;
class CommonModel extends Model
{
/**
* 返回失败的请求
* @param mixed $msg 消息内容
* @param array $data 返回数据
* @param integer $code 返回代码
*/
protected function error($msg, $data = [], $code = 400)
{
ToolsService::error($msg, $data, $code);
}
/**
* 分页方法
* @param null $where 可以传入查询对象或模型实例
* @param $order
* @param $field
* @param bool $simple 是否简介模式,简介模式不分页
* @param array $config 分页配置page: 当前页rows: 每页数量
* @return array
* @throws \think\exception\DbException
*/
public function _list($where = null, $order = 'id desc', $field = null, $simple = false, $config = [])
{
$rows = intval(Request::param('pageSize', cookie('pageSize')));
if (!$rows) {
$rows = 10;
}
cookie('pageSize', $rows);
$config['query'] = Request::param();
$whereOr = [];
if (isset($where['or']) and $where['or']) {
//todo 怎么or连贯查询
/*
* whereOr查询形式如
$where['or'][]= ['name','like',"xxx"];
$where['or'][] = ['id','=',"xxx"];
*/
$whereOr = $where['or'];
unset($where['or']);
}
$page = $this->where($where)->whereOr($whereOr)->order($order)->field($field)->paginate($rows, $simple, $config);
$list = $page->all();
$result = ['total' => $simple ? count($list) : $page->total(), 'page' => $page->currentPage(), 'list' => $list];
return $result;
}
public function _listWithTrashed($where = null, $order = null, $field = null, $simple = false, $config = [])
{
$rows = intval(Request::param('rows', cookie('pageSize')));
if (!$rows) {
$rows = 10;
}
cookie('pageSize', $rows);
$config['query'] = Request::param();
$whereOr = [];
if (isset($where['or']) and $where['or']) {
//todo 怎么or连贯查询
/*
* whereOr查询形式如
$where['or'][]= ['name','like',"xxx"];
$where['or'][] = ['id','=',"xxx"];
*/
$whereOr = $where['or'];
unset($where['or']);
}
$class = get_class($this);
$count = $class::withTrashed()->where($where)->whereOr($whereOr)->order($order)->field($field)->count();
$page = $config['query']['page'] ? $config['query']['page'] : 1;
$offset = $rows * ($config['query']['page'] - 1);
$list = $class::withTrashed()->where($where)->whereOr($whereOr)->order($order)->field($field)->limit($offset, $rows)->select();
$result = ['total' => $count, 'page' => $page, 'list' => $list];
return $result;
}
public function _edit($data, $where = [])
{
return $this->isUpdate(true)->save($data,$where);
}
public function _add($data)
{
$obj = $this::create($data);
if ($obj->id) {
return $this::get($obj->id);
}
return false;
}
/**
* @param File $file
* @param $path_name
* @return array|bool
* @throws \OSS\Core\OssException
* @throws \think\Exception
* @throws \think\exception\PDOException
* @throws \Exception
*/
public function _uploadImg(File $file, $path_name = '')
{
if (!$path_name) {
$path_name = config('upload.base_path') . config('default');
}
if (!$file->checkExt(strtolower(sysconf('storage_local_exts')))) {
\exception('文件上传类型受限', 1);
}
$path = $path_name;
$info = $file->move($path);
if ($info) {
$filename = str_replace('\\', '/', $path . '/' . $info->getSaveName());
// $image = \think\Image::open($info->getRealPath());
// $image->thumb($image->width() / 2, $image->height() / 2)->save($filename);//压缩
$site_url = FileService::getFileUrl($filename, 'local');
$fileInfo = FileService::save($filename, file_get_contents($site_url));
if ($fileInfo) {
return ['base_url' => $fileInfo['key'], 'url' => $fileInfo['url'], 'filename' => $file->getInfo('name')];
}
}
return false;
}
/*
* 获取当前organization id
* */
public function gecurrentOrganizationCode(){
$currentOrganizationCode = session('currentOrganizationCode');
return $currentOrganizationCode;
}
/*
* 获取当前member session
* */
public function getMemberSession(){
$member_session = session('member');
return $member_session;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace app\common\Model;
use service\ToolsService;
use think\Db;
/**
* 部门
* Class Organization
* @package app\common\Model
*/
class Department extends CommonModel
{
protected $append = [];
/**
* @param $name
* @param string $parentDepartmentCode
* @return Department
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function createDepartment($name, $parentDepartmentCode = '')
{
$path = '';
if ($parentDepartmentCode) {
$parentDepartment = self::where(['code' => $parentDepartmentCode])->field('code,path')->find();
$parentDepartment['path'] && $parentDepartment['path'] = ",{$parentDepartment['path']}";
$path = "{$parentDepartment['code']}{$parentDepartment['path']}";
}
$data = [
'organization_code' => getCurrentOrganizationCode(),
'code' => createUniqueCode('department'),
'name' => $name,
'pcode' => $parentDepartmentCode,
'path' => $path,
'create_time' => nowTime(),
];
return self::create($data);
}
public function deleteDepartment($departmentCode)
{
$department = self::where(['code' => $departmentCode])->find();
if (!$department) {
throw new \Exception('该部门不存在', 1);
}
$prefix = config('database.prefix');
$sql = "select code from {$prefix}department where find_in_set('{$departmentCode}',path)";
$departments = Db::name('department')->query($sql);
$codes = [$departmentCode];
if ($departments) {
foreach ($departments as $department) {
$codes[] = $department['code'];
}
}
$result = self::whereIn('code', $codes)->delete();
DepartmentMember::whereIn('department_code', $codes)->delete();
return $result;
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace app\common\Model;
/**
* 部门成员
* Class ProjectMember
* @package app\common\Model
*/
class DepartmentMember extends CommonModel
{
protected $append = [];
/**
* @param $accountCode
* @param string $departmentCode
* @param int $isOwner
* @param int $isPrincipal
* @return DepartmentMember|MemberAccount
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function inviteMember($accountCode, $departmentCode = '', $isOwner = 0, $isPrincipal = 0)
{
$orgCode = getCurrentOrganizationCode();
if ($departmentCode) {
$department = Department::where(['code' => $departmentCode])->find();
if (!$department) {
throw new \Exception('该部门不存在', 1);
}
$hasJoined = self::where(['account_code' => $accountCode, 'department_code' => $departmentCode])->find();
if ($hasJoined) {
throw new \Exception('已加入该部门', 2);
}
$data = [
'code' => createUniqueCode('departmentMember'),
'account_code' => $accountCode,
'organization_code' => $orgCode,
'department_code' => $departmentCode,
'is_owner' => $isOwner,
'is_principal' => $isPrincipal,
'join_time' => nowTime()
];
$result = self::create($data);
$department_codes = self::where(['account_code' => $accountCode, 'organization_code' => $orgCode])->column('department_code');
if ($department_codes) {
$department_codes = implode(',', $department_codes);
MemberAccount::update(['department_code' => $department_codes], ['code' => $accountCode]);
}
return $result;
} else {
$hasJoined = MemberAccount::where(['member_code' => $accountCode, 'organization_code' => $orgCode])->find();
if ($hasJoined) {
throw new \Exception('已加入该组织', 3);
}
$memberDate = Member::where(['code' => $accountCode])->find();
if (!$memberDate) {
throw new \Exception('该用户不存在', 4);
}
$auth = ProjectAuth::where(['organization_code' => $orgCode, 'is_default' => 1])->field('id')->find();
$authId = '';
if ($auth) {
$authId = $auth['id'];//权限id
}
$data = [
'position' => '资深工程师',
'department' => '某某公司-某某某事业群-某某平台部-某某技术部',
'code' => createUniqueCode('memberAccount'),
'member_code' => $accountCode,
'organization_code' => $orgCode,
'is_owner' => 0,
'authorize' => $authId,
'status' => 1,
'create_time' => nowTime(),
'name' => $memberDate['name'],
'email' => $memberDate['email'],
];
return MemberAccount::create($data);
}
}
/**
* @param $accountCode
* @param $departmentCode
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function removeMember($accountCode, $departmentCode)
{
$orgCode = getCurrentOrganizationCode();
$department = Department::where(['code' => $departmentCode])->find();
if (!$department) {
throw new \Exception('该部门不存在', 1);
}
$hasJoined = self::where(['account_code' => $accountCode, 'department_code' => $departmentCode])->find();
if (!$hasJoined) {
throw new \Exception('尚未加入该部门', 2);
}
$result = $hasJoined->delete();
$department_codes = self::where(['account_code' => $accountCode, 'organization_code' => $orgCode])->column('department_code');
if ($department_codes) {
$department_codes = implode(',', $department_codes);
MemberAccount::update(['department_code' => $department_codes], ['code' => $accountCode]);
}
return $result;
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace app\common\Model;
use function GuzzleHttp\Promise\task;
use think\Db;
/**
* 文件
* Class TaskStar
* @package app\common\Model
*/
class File extends CommonModel
{
protected $append = ['fullName'];
/**
* @param $projectCode
* @param string $taskCode
* @param $data
* @return File
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function createFile($projectCode, $data)
{
$project = Project::where(['code' => $projectCode])->find();
if (!$project) {
throw new \Exception('该项目已失效', 1);
}
$memberCode = getCurrentMember()['code'];
$orgCode = getCurrentOrganizationCode();
$fileData = [
'code' => createUniqueCode('file'),
'create_by' => $memberCode,
'project_code' => $projectCode,
'organization_code' => $orgCode,
'path_name' => isset($data['path_name']) ? $data['path_name'] : '',
'title' => isset($data['title']) ? $data['title'] : '',
'extension' => isset($data['extension']) ? $data['extension'] : '',
'size' => isset($data['size']) ? $data['size'] : '',
'object_type' => isset($data['object_type']) ? $data['object_type'] : '',
'extra' => isset($data['extra']) ? $data['extra'] : '',
'file_url' => isset($data['file_url']) ? $data['file_url'] : '',
'file_type' => isset($data['file_type']) ? $data['file_type'] : '',
'create_time' => nowTime(),
];
$result = self::create($fileData);
return $result;
}
/**
* 放入回收站
* @param $code
* @return File
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recycle($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('文件不存在', 1);
}
if ($info['deleted']) {
throw new \Exception('文件已在回收站', 2);
}
$result = self::update(['deleted' => 1, 'deleted_time' => nowTime()], ['code' => $code]);
return $result;
}
/**
* 恢复文件
* @param $code
* @return File
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recovery($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('文件不存在', 1);
}
if (!$info['deleted']) {
throw new \Exception('文件已恢复', 2);
}
$result = self::update(['deleted' => 0], ['code' => $code]);
return $result;
}
public function deleteFile($code)
{
//todo 权限判断
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('文件不存在', 1);
}
Db::startTrans();
try {
self::where(['code' => $code])->delete();
//todo 删除物理文件
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new \Exception($e->getMessage());
}
return true;
}
public function getFullNameAttr($value, $data)
{
return "{$data['title']}.{$data['extension']}";
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace app\common\Model;
use think\Db;
use think\File;
class Member extends CommonModel
{
protected $append = [];
public function login($account)
{
if ($account == 'admin') {
return [];
}
$where[] = ['account', '=', $account];
return Db::name('member')->where($where)->find();
}
/**
* @param $memberData
* @return Member
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function createMember($memberData)
{
//需要创建的信息。1、用户 2、用户所属组织 3、组织权限 4、所属组织账号
$memberData['create_time'] = nowTime();
$result = self::create($memberData);
Organization::createOrganization($result);
// $organizationData = [
// 'code' => createUniqueCode('organization'),
// 'name' => $memberData['name'] . '的个人项目',
// 'personal' => 1,
// 'create_time' => nowTime(),
// 'owner_code' => $memberData['code'],
// ];
// Organization::create($organizationData);
//
// $defaultAdminAuth = ProjectAuth::get(1)->toArray();
// $defaultMemberAuth = ProjectAuth::get(2)->toArray();
// unset($defaultAdminAuth['id']);
// unset($defaultMemberAuth['id']);
// $defaultAdminAuth['organization_code'] = $defaultMemberAuth['organization_code'] = $organizationData['code'];
// $defaultAdminAuth = ProjectAuth::create($defaultAdminAuth);
// $defaultMemberAuth = ProjectAuth::create($defaultMemberAuth);
// $defaultAdminAuthNode = ProjectAuthNode::where(['auth' => 1])->select()->toArray();
// $defaultMemberAuthNode = ProjectAuthNode::where(['auth' => 2])->select()->toArray();
// foreach ($defaultAdminAuthNode as &$item) {
// unset($item['id']);
// $item['auth'] = $defaultAdminAuth['id'];
// ProjectAuthNode::create($item);
// }
// foreach ($defaultMemberAuthNode as &$item) {
// unset($item['id']);
// $item['auth'] = $defaultMemberAuth['id'];
// ProjectAuthNode::create($item);
// }
//
// $memberAccountData = [
// 'position' => '资深工程师',
// 'department' => '某某公司某某某事业群某某平台部某某技术部BM',
// 'code' => createUniqueCode('organization'),
// 'member_code' => $memberData['code'],
// 'organization_code' => $organizationData['code'],
// 'is_owner' => 1,
// 'status' => 1,
// 'create_time' => nowTime(),
// 'avatar' => $memberData['avatar'],
// 'name' => $memberData['name'],
// 'email' => $memberData['email'],
// ];
// MemberAccount::create($memberAccountData);
return $result;
}
/**
* @param File $file
* @return array|bool
* @throws \think\Exception
* @throws \think\exception\PDOException
* @throws \Exception
*/
public function uploadImg(File $file)
{
return $this->_uploadImg($file, config('upload.base_path') . config('upload.member_avatar'));
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace app\common\Model;
use service\NodeService;
use think\Db;
use think\File;
class MemberAccount extends CommonModel
{
protected $append = ['statusText', 'authorizeArr'];
/**
* 获取当前用户菜单
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function getAuthMenuList()
{
NodeService::applyProjectAuthNode();
$menuModel = new ProjectMenu();
$list = $menuModel->listForUser();
return $list;
}
/**
* @param File $file
* @return array|bool
* @throws \think\Exception
* @throws \think\exception\PDOException
* @throws \Exception
*/
public function uploadImg(File $file)
{
return $this->_uploadImg($file, config('upload.base_path') . config('upload.member_avatar'));
}
public function getAccountByOrganization($account, $organization_code)
{
return $this->where(['account' => $account, 'organization_code' => $organization_code])->find();
}
public function getAuthorizeArrAttr($value, $data)
{
//支持同时设置多个角色,默认关闭
if ($data['authorize']) {
return explode(',', $data['authorize']);
}
return [];
}
public function getStatusTextAttr($value, $data)
{
$status = [0 => '禁用', 1 => '使用中'];
return $status[$data['status']];
}
/**
* @param $accountCode
* @return bool
* @throws \Exception
*/
public function del($accountCode)
{
//todo 权限判断
try {
Db::startTrans();
$memberAccount = self::where(['code' => $accountCode])->find()->toArray();
self::destroy(['code' => $accountCode]);
$projects = Project::where(['organization_code' => $memberAccount['organization_code']])->column('code');
if ($projects) {
ProjectMember::whereIn('project_code', $projects)->where(['member_code' => $memberAccount['member_code']])->delete();
$orgCode = getCurrentOrganizationCode();
DepartmentMember::where(['account_code' => $accountCode, 'organization_code' => $orgCode])->delete();
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new \Exception($e->getMessage(), 201);
}
return true;
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace app\common\Model;
use service\FileService;
use think\Exception;
use think\File;
class Notify extends CommonModel
{
/**
* 按type类型格式化输出列表
* @param $where
* @param bool $size
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function listTypeFormat($where, $size = false)
{
$types = ['notice', 'message', 'task'];
$totalSum = [];
$list = $this->where($where)->order('id desc')->select();
$formatList = [];
$total = $this->where($where)->count('id');
if ($list) {
foreach ($list as &$item) {
foreach ($types as $type) {
!isset($formatList[$type]) and $formatList[$type] = [];
!isset($totalSum[$type]) and $totalSum[$type] = 0;
$sum = $this->where($where)->where(['type'=>$type])->count('id');
$totalSum[$type] = $sum;
if ($size and count($formatList[$type]) >= $size) {
continue;
}
if ($item['type'] == $type) {
$item['from'] = $this->getReceiverByTerminal($item['terminal'], $item['from']);
$item['to'] = $this->getReceiverByTerminal($item['terminal'], $item['to']);
$formatList[$type][] = $item;
// $total++;
}
}
}
}
return ['list' => $formatList, 'total' => $total, 'totalSum' => $totalSum];
}
public function getReceiverByTerminal($terminal, $to)
{
if (!$to) {
return false;
}
switch ($terminal) {
case 'system':
return [];
}
}
public function getFromByType($fromType, $from)
{
if (!$from) {
return false;
}
switch ($fromType) {
case 'admin':
return SystemUser::find($from);
case 'project': //消息
return MemberAccount::find($from);
}
}
public function add($title, $content, $type, $from, $to, $action, $send_data, $terminal,$fromType = 'system')
{
$data = [
'title' => $title,
'content' => $content,
'type' => $type,
'from' => $from,
'to' => $to,
'action' => $action,
'send_data' => $send_data,
'terminal' => $terminal,
'from_type' => $fromType,
'create_time' => nowTime(),
];
return self::create($data);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace app\common\Model;
/**
* 组织
* Class Organization
* @package app\common\Model
*/
class Organization extends CommonModel
{
protected $append = [];
/**
* 创建组织
* @param $memberData
* @param array $data
* @return Organization
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function createOrganization($memberData, $data = [])
{
$defaultAdminAuthId = 3;//默认管理员权限id
$defaultMemberAuthId = 4;//默认成员权限id
if (!isset($data['name'])) {
$data['name'] = $memberData['name'] . '的个人项目';
}
$data['code'] = createUniqueCode('organization');
$data['personal'] = 1;
$data['create_time'] = nowTime();
$data['owner_code'] = $memberData['code'];
$organization = self::create($data);
$defaultAdminAuth = ProjectAuth::get($defaultAdminAuthId)->toArray();
$defaultMemberAuth = ProjectAuth::get($defaultMemberAuthId)->toArray();
unset($defaultAdminAuth['id']);
unset($defaultMemberAuth['id']);
$defaultAdminAuth['organization_code'] = $defaultMemberAuth['organization_code'] = $data['code'];
$defaultAdminAuth = ProjectAuth::create($defaultAdminAuth);
$defaultMemberAuth = ProjectAuth::create($defaultMemberAuth);
$defaultAdminAuthNode = ProjectAuthNode::where(['auth' => $defaultAdminAuthId])->select()->toArray();
$defaultMemberAuthNode = ProjectAuthNode::where(['auth' => $defaultMemberAuthId])->select()->toArray();
foreach ($defaultAdminAuthNode as &$item) {
unset($item['id']);
$item['auth'] = $defaultAdminAuth['id'];
ProjectAuthNode::create($item);
}
foreach ($defaultMemberAuthNode as &$item) {
unset($item['id']);
$item['auth'] = $defaultMemberAuth['id'];
ProjectAuthNode::create($item);
}
$memberAccountData = [
'position' => '资深工程师',
'department' => '某某公司某某某事业群某某平台部某某技术部BM',
'code' => createUniqueCode('organization'),
'member_code' => $memberData['code'],
'organization_code' => $data['code'],
'is_owner' => 1,
'status' => 1,
'create_time' => nowTime(),
'avatar' => $memberData['avatar'],
'name' => $memberData['name'],
'email' => $memberData['email'],
];
MemberAccount::create($memberAccountData);
return $organization;
}
public function edit($code, $data)
{
if (!$code) {
throw new \Exception('请选择组织', 1);
}
$project = self::where(['code' => $code])->field('id', true)->find();
if (!$project) {
throw new \Exception('该组织不存在', 1);
}
$result = self::update($data, ['code' => $code]);
return $result;
}
}

View File

@ -0,0 +1,259 @@
<?php
namespace app\common\Model;
use service\FileService;
use service\RandomService;
use think\Db;
use think\facade\Hook;
use think\File;
/**
* 项目
* Class Organization
* @package app\common\Model
*/
class Project extends CommonModel
{
protected $append = [];
protected $defaultStages = [['name' => '待处理'], ['name' => '进行中'], ['name' => '已完成']];
public static function getEffectInfo($id)
{
return self::where(['id' => $id, 'deleted' => 0, 'archive' => 0])->find();
}
public function getMemberProjects($memberCode = '', $deleted = 0, $page = 1, $pageSize = 10)
{
if (!$memberCode) {
$memberCode = getCurrentMember()['code'];
}
if ($page < 1) {
$page = 1;
}
$offset = ($page - 1) * $page;
$limit = $pageSize;
$prefix = config('database.prefix');
$sql = "select *,p.id as id,p.name as name,p.code as code from {$prefix}project as p join {$prefix}project_member as pm on p.code = pm.project_code where pm.member_code = '{$memberCode}' and p.deleted = {$deleted} order by p.id desc";
$total = Db::query($sql);
$total = count($total);
$sql .= " limit {$offset},{$limit}";
$list = Db::query($sql);
return ['list' => $list, 'total' => $total];
}
/**
* 创建项目
* @param $memberCode
* @param $orgCode
* @param $name
* @param string $description
* @param string $templateCode
* @return Project
* @throws \Exception
*/
public function createProject($memberCode, $orgCode, $name, $description = '', $templateCode = '')
{
//d85f1bvwpml2nhxe94zu7tyi
Db::startTrans();
try {
$project = [
'create_time' => nowTime(),
'code' => createUniqueCode('project'),
'name' => $name,
'description' => $description,
'organization_code' => $orgCode,
'cover' => FileService::getFilePrefix() . 'static/image/default/project-cover.png'
];
$result = self::create($project);
$projectMemberModel = new ProjectMember();
$projectMemberModel->inviteMember($memberCode, $project['code'], 1);
if ($templateCode) {
$stages = TaskStagesTemplate::where(['project_template_code' => $templateCode])->order('sort desc,id asc')->select();
} else {
$stages = $this->defaultStages;
}
if ($stages) {
foreach ($stages as $key => $stage) {
$taskStage = [
'project_code' => $project['code'],
'name' => $stage['name'],
'sort' => $key,
'code' => createUniqueCode('taskStages'),
'create_time' => nowTime(),
];
$stagesResult = TaskStages::create($taskStage);
$taskStage['id'] = $stagesResult['id'];
}
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new \Exception($e->getMessage(), 1);
}
self::projectHook(getCurrentMember()['code'], $project['code'], 'create');
return $result;
}
public function edit($code, $data)
{
if (!$code) {
throw new \Exception('请选择项目', 1);
}
$project = self::where(['code' => $code, 'deleted' => 0])->field('id', true)->find();
if (!$project) {
throw new \Exception('该项目在回收站中无法编辑', 1);
}
$result = self::update($data, ['code' => $code]);
//TODO 项目动态
self::projectHook(getCurrentMember()['code'], $code, 'edit');
return $result;
}
/**
* @param File $file
* @return array|bool
* @throws \think\Exception
* @throws \think\exception\PDOException
* @throws \Exception
*/
public function uploadCover(File $file)
{
return $this->_uploadImg($file);
}
/**
* 放入回收站
* @param $code
* @return Project
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recycle($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('项目不存在', 1);
}
if ($info['deleted']) {
throw new \Exception('项目已在回收站', 2);
}
$result = self::update(['deleted' => 1, 'deleted_time' => nowTime()], ['code' => $code]);
self::projectHook(getCurrentMember()['code'], $code, 'recycle');
return $result;
}
/**
* 恢复项目
* @param $code
* @return Project
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recovery($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('项目不存在', 1);
}
if (!$info['deleted']) {
throw new \Exception('项目已恢复', 2);
}
$result = self::update(['deleted' => 0], ['code' => $code]);
self::projectHook(getCurrentMember()['code'], $code, 'recovery');
return $result;
}
/**
* 项目归档
* @param $code
* @return Project
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function archive($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('项目不存在', 1);
}
if ($info['archive']) {
throw new \Exception('项目已归档', 2);
}
$result = self::update(['archive' => 1, 'archive_time' => nowTime()], ['code' => $code]);
self::projectHook(getCurrentMember()['code'], $code, 'archive');
return $result;
}
/**
* 恢复项目
* @param $code
* @return Project
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recoveryArchive($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('项目不存在', 1);
}
if (!$info['archive']) {
throw new \Exception('项目已恢复', 2);
}
$result = self::update(['archive' => 0], ['code' => $code]);
self::projectHook(getCurrentMember()['code'], $code, 'recoveryArchive');
return $result;
}
/**
* 退出项目
* @param $code
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
* @throws \Exception
*/
public function quit($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('项目不存在', 1);
}
$where = ['project_code' => $code, 'member_code' => getCurrentMember()['code']];
$projectMember = ProjectMember::where($where)->find();
if (!$projectMember) {
throw new \Exception('你不是该项目成员', 2);
}
if ($projectMember['is_owner']) {
throw new \Exception('创建者不能退出项目', 3);
}
$result = ProjectMember::where($where)->delete();
return $result;
}
/** 项目变动钩子
* @param $memberCode
* @param $sourceCode
* @param string $type
* @param string $toMemberCode
* @param int $isComment
* @param string $remark
* @param string $content
* @param string $fileCode
* @param array $data
* @param string $tag
*/
public static function projectHook($memberCode, $sourceCode, $type = 'create', $toMemberCode = '', $isComment = 0, $remark = '', $content = '', $fileCode = '', $data = [], $tag = 'project')
{
$data = ['memberCode' => $memberCode, 'sourceCode' => $sourceCode, 'remark' => $remark, 'type' => $type, 'content' => $content, 'isComment' => $isComment, 'toMemberCode' => $toMemberCode, 'fileCode' => $fileCode, 'data' => $data, 'tag' => $tag];
Hook::listen($tag, $data);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace app\common\Model;
class ProjectAuth extends CommonModel
{
protected $pk = 'id';
protected $append = ['canDelete'];
/**
* @param $id
* @return bool|int
* @throws \Exception
*/
public function del($id)
{
//TODO 删除该权限后,拥有这个权限的账户将被在设置默认权限
if ($this::destroy($id)) {
$where = ['auth' => $id];
$result = ProjectAuthNode::where($where)->delete();
if ($result !== false) {
return true;
}
}
return false;
}
public function getIdAttr($value)
{
return strval($value);
}
public function getStatusTextAttr($value, $data)
{
$status = [0 => '禁用', 1 => '使用中'];
return $status[$data['status']];
}
public function getCanDeleteAttr($value, $data)
{
if ($data['type'] == 'admin' || $data['type'] == 'member') {
return 0;
}
return 1;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace app\common\Model;
class ProjectAuthNode extends CommonModel
{
protected $pk = 'id';
public function getIdAttr($value)
{
return strval($value);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace app\common\Model;
/**
* 项目收藏
* Class ProjectMember
* @package app\common\Model
*/
class ProjectCollection extends CommonModel
{
protected $append = [];
/**
* @param $memberId
* @param $projectId
* @param string $type
* @return ProjectCollection|bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function collect($memberCode, $projectCode, $type = 'collect')
{
$project = Project::where(['code' => $projectCode, 'deleted' => 0])->find();
if (!$project) {
throw new \Exception('该项目已失效', 1);
}
$hasCollected = self::where(['member_code' => $memberCode, 'project_code' => $projectCode])->find();
if ($type == 'collect') {
if ($hasCollected) {
throw new \Exception('该项目已收藏', 1);
}
$data = [
'member_code' => $memberCode,
'project_code' => $projectCode,
'create_time' => nowTime()
];
return self::create($data);
} else {
if (!$hasCollected) {
throw new \Exception('尚未收藏该项目', 1);
}
return self::where(['member_code' => $memberCode, 'project_code' => $projectCode])->delete();
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace app\common\Model;
class ProjectLog extends CommonModel
{
protected $pk = 'id';
}

View File

@ -0,0 +1,61 @@
<?php
namespace app\common\Model;
/**
* 项目成员
* Class ProjectMember
* @package app\common\Model
*/
class ProjectMember extends CommonModel
{
protected $append = [];
/**
* @param $memberCode
* @param $projectCode
* @param int $isOwner
* @return ProjectMember|bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function inviteMember($memberCode, $projectCode, $isOwner = 0)
{
$project = Project::where(['code' => $projectCode, 'deleted' => 0])->find();
if (!$project) {
throw new \Exception('该项目已失效', 1);
}
$hasJoined = self::where(['member_code' => $memberCode, 'project_code' => $projectCode])->find();
if ($hasJoined) {
// throw new \Exception('该成员已加入项目', 1);
return true;
}
$data = [
'member_code' => $memberCode,
'project_code' => $projectCode,
'is_owner' => $isOwner,
'join_time' => nowTime()
];
$result = self::create($data);
Project::projectHook(getCurrentMember()['code'], $projectCode, 'inviteMember', $memberCode);
return $result;
}
public function removeMember($memberCode, $projectCode)
{
$project = Project::where(['code' => $projectCode, 'deleted' => 0])->find();
if (!$project) {
throw new \Exception('该项目已失效', 1);
}
$hasJoined = self::where(['member_code' => $memberCode, 'project_code' => $projectCode])->find();
if (!$hasJoined) {
// throw new \Exception('该成员尚未加入项目', 1);
return true;
}
$result = $hasJoined->delete();
Project::projectHook(getCurrentMember()['code'], $projectCode, 'removeMember', $memberCode);
return $result;
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace app\common\Model;
use service\NodeService;
use service\ToolsService;
class ProjectMenu extends CommonModel
{
protected $pk = 'id';
protected $append = ["statusText", "innerText", "fullUrl"];
public function getIdAttr($value)
{
return strval($value);
}
public function getStatusTextAttr($value, $data)
{
$status = [0 => '禁用', 1 => '使用中'];
return $status[$data['status']];
}
public function getInnerTextAttr($value, $data)
{
$status = [0 => '导航', 1 => '内页'];
return $status[$data['is_inner']];
}
public function getFullUrlAttr($value, $data)
{
if (($data['params'] and $data['values'] != null) or $data['values'] != '') {
$fullUrl = $data['url'] . '/' . $data['values'];
return $fullUrl;
}
return $data['url'];
}
public function childrenMenu()
{
return $this->hasMany('menu', 'pid')->selfRelation();
}
/**
* 获取所有菜单列表
* @return array|string|\think\Collection
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function treeList()
{
$list = $this->order('sort asc,id asc')->select();
$list = $list->toArray();
if ($list) {
foreach ($list as &$item) {
$item['is_inner'] = !!$item['is_inner'];
$item['show_slider'] = !!$item['show_slider'];
unset($item);
}
}
$list = ToolsService::arr2tree($list);
return $list;
}
/**
* 获取用户对应的菜单列表
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function listForUser()
{
NodeService::applyProjectAuthNode();
$list = $this->where(['status' => '1'])->order('sort asc,id asc')->select();
$list = $list->toArray();
if ($list) {
foreach ($list as &$item) {
$item['is_inner'] = !!$item['is_inner'];
unset($item);
}
}
//主账号不做过滤
$menus = session('member.is_owner') ? $list : $this->filterMenu($list, session('member.nodes'));
$new = [];
$this->buildFilterMenuData(ToolsService::arr2tree($menus), $new);
$menus = ToolsService::arr2tree($new);
return $menus;
}
/**
* 过滤没有节点权限的菜单
* @param $menus array 待过滤菜单
* @param $nodes array 拥有的权限节点
* @return array
*/
private function filterMenu($menus, $nodes)
{
$newMenus = [];
foreach ($menus as $key => $menu) {
if ($menu['node'] == '#') {
$newMenus[] = $menu;
} elseif (preg_match('/^https?\:/i', $menu['url'])) {
$newMenus[] = $menu;
continue;
} elseif ($menu['node'] != '#') {
$node = join('/', array_slice(explode('/', preg_replace('/[\W]/', '/', $menu['node'])), 0, 3));
if ($nodes && in_array($node, $nodes)) {
$newMenus[] = $menu;
}
}
}
return $newMenus;
}
/**
* 后台主菜单权限过滤(过滤没有子节点的菜单)
* @param array $menus 当前树形结构的菜单列表
* @param $new array 过滤后的菜单
* @return void
*/
private function buildFilterMenuData($menus, &$new)
{
foreach ($menus as $key => $menu) {
if (($menu['node'] == '#' && isset($menu['children']) && $menu['children']) || ($menu['node'] != '#' && !isset($menu['children'])) || $menu['url'] == 'home') {
$temp = $menu;
unset($temp['children']);
$new[] = $temp;
}
if (isset($menu['children']) && $menu['children']) {
$this->buildFilterMenuData($menu['children'], $new);
}
}
}
/**
* 后台主菜单权限过滤
* @param array $menus 当前菜单列表
* @param array $nodes 系统权限节点数据
* @param bool $isLogin 是否已经登录
* @return array
*/
private function buildMenuData($menus, $nodes, $isLogin)
{
foreach ($menus as $key => &$menu) {
!empty($menu['children']) && $menu['children'] = $this->buildMenuData($menu['children'], $nodes, $isLogin);
if (!empty($menu['children'])) {
$menu['url'] = '#';
} elseif (preg_match('/^https?\:/i', $menu['url'])) {
continue;
} elseif ($menu['node'] != '#') {
$node = join('/', array_slice(explode('/', preg_replace('/[\W]/', '/', $menu['node'])), 0, 3));
if (!in_array($node, $nodes)) {
array_splice($menus, $key, 1);
continue;
}
if (in_array($node, $nodes) && $nodes[$node]['is_login'] && empty($isLogin)) {
array_splice($menus, $key, 1);
} elseif (in_array($node, $nodes) && $nodes[$node]['is_auth'] && $isLogin && !auth($node)) {
array_splice($menus, $key, 1);
}
} else {
array_splice($menus, $key, 1);
}
}
return $menus;
}
public function del($id)
{
$delArr = [$id];
$list = $this::where(['pid' => $id])->select()->toArray();
if ($list) {
foreach ($list as $item) {
$delArr[] = $item['id'];
$list2 = $this::where(['pid' => $item['id']])->select()->toArray();
if ($list2) {
foreach ($list2 as $item2) {
$delArr[] = $item2['id'];
}
}
}
}
return $this::destroy($delArr);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace app\common\Model;
class ProjectNode extends CommonModel
{
}

View File

@ -0,0 +1,87 @@
<?php
namespace app\common\Model;
use service\FileService;
use service\RandomService;
use think\File;
/**
* 项目模板
* Class Organization
* @package app\common\Model
*/
class ProjectTemplate extends CommonModel
{
protected $append = [];
/**
* 创建项目模板
* @param $memberCode
* @param $orgCode
* @param $name
* @param string $description
* @param string $cover
* @return ProjectTemplate
* @throws \think\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
* @throws \think\exception\PDOException
*/
public function createProjectTemplate($memberCode, $orgCode, $name, $description = '', $cover = '')
{
$data = [
'create_time' => nowTime(),
'code' => createUniqueCode('projectTemplate'),
'member_code' => $memberCode,
'name' => $name,
'description' => $description,
'organization_code' => $orgCode,
'cover' => $cover ?? FileService::getFilePrefix() . 'static/image/default/cover.png'
];
$result = self::create($data);
if ($result) {
$taskStagesList = TaskStagesTemplate::$defaultTaskStagesNameList;
if ($taskStagesList) {
foreach ($taskStagesList as $name) {
TaskStagesTemplate::createTaskStagesTemplate($data['code'], $name);
}
}
}
return $result;
}
/**
* 删除模板
* @param $code
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function deleteTemplate($code)
{
$template = self::where(['code' => $code])->field('id')->find();
if (!$template) {
throw new \Exception('该模板不存在', 1);
}
$result = self::destroy(['code' => $code]);
if (!$result) {
throw new \Exception('删除失败', 2);
}
return TaskStagesTemplate::destroy(['project_template_code' => $code]);
}
/**
* @param File $file
* @return array|bool
* @throws \think\Exception
* @throws \think\exception\PDOException
* @throws \Exception
*/
public function uploadCover(File $file)
{
return $this->_uploadImg($file);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace app\common\Model;
/**
* 资源关联
* Class TaskStar
* @package app\common\Model
*/
class SourceLink extends CommonModel
{
protected $append = [];
/**
* @param $sourceType
* @param $sourceCode
* @param $linkType
* @param $linkCode
* @param int $sort
* @return SourceLink
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function createSource($sourceType, $sourceCode, $linkType, $linkCode, $sort = 0)
{
$source = $link = null;
switch ($sourceType) {
case 'file':
$source = File::where(['code' => $sourceCode])->find();
}
if (!$source) {
throw new \Exception('该资源不存在', 1);
}
switch ($linkType) {
case 'task':
$link = Task::where(['code' => $linkCode])->find();
}
if (!$link) {
throw new \Exception('关联主体不存在', 2);
}
$memberCode = getCurrentMember()['code'];
$orgCode = getCurrentOrganizationCode();
$fileData = [
'code' => createUniqueCode('sourceLink'),
'create_by' => $memberCode,
'organization_code' => $orgCode,
'source_type' => $sourceType,
'source_code' => $sourceCode,
'link_type' => $linkType,
'link_code' => $linkCode,
'sort' => $sort,
'create_time' => nowTime(),
];
$result = self::create($fileData);
if ($linkType == 'task') {
Task::taskHook(getCurrentMember()['code'], $linkCode, 'linkFile', '', 0, '', '', '', ['title' => $source['fullName'], 'url' => $source['file_url']]);
}
return $result;
}
public static function getSourceDetail($sourceCode)
{
$source = self::where(['code' => $sourceCode])->find();
$sourceDetail = null;
switch ($source['source_type']) {
case 'file':
$source['title'] = '';
$sourceDetail = File::where(['code' => $source['source_code']])->field('id', true)->find();
if ($sourceDetail) {
$source['title'] = $sourceDetail['title'];
$project = Project::where(['code' => $sourceDetail['project_code']])->field('name')->find();
$sourceDetail['projectName'] = $project['name'];
}
}
$source['sourceDetail'] = $sourceDetail;
return $source;
}
public function deleteSource($code)
{
$source = self::where(['code' => $code])->find();
if (!$source) {
throw new \Exception('该资源不存在', 1);
}
$source = self::getSourceDetail($code);
$result = self::where(['code' => $code])->delete();
if ($source['link_type'] == 'task') {
Task::taskHook(getCurrentMember()['code'], $source['link_code'], 'unlinkFile', '', 0, '', '', '', ['title' => $source['title'], 'url' => $source['sourceDetail']['file_url']]);
}
return $result;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace app\common\Model;
class SystemConfig extends CommonModel
{
protected $pk = 'id';
public function info()
{
$config = $this->select();
$data = [];
foreach ($config as $item) {
$data[$item['name']] = $item['value'];
}
return $data;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace app\common\Model;
class SystemLog extends CommonModel
{
protected $pk = 'id';
public function actionGroup()
{
return $this->group('action')->column('action');
}
}

View File

@ -0,0 +1,584 @@
<?php
namespace app\common\Model;
use function GuzzleHttp\Promise\task;
use service\DateService;
use think\Db;
use think\facade\Hook;
/**
* 任务
* Class Organization
* @package app\common\Model
*/
class Task extends CommonModel
{
protected $append = ['priText', 'liked', 'stared', 'childCount', 'hasComment', 'hasSource'];
public function read($code)
{
if (!$code) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $code])->field('id', true)->find();
if (!$task) {
throw new \Exception('该任务已失效', 404);
}
$project = Project::where(['code' => $task['project_code']])->field('name')->find();
$stage = TaskStages::where(['code' => $task['stage_code']])->field('name')->find();
$task['executor'] = null;
if ($task['assign_to']) {
$task['executor'] = Member::where(['code' => $task['assign_to']])->field('name,code,avatar')->find();
}
if ($task['pcode']) {
$task['parentTask'] = self::where(['code' => $task['pcode']])->field('id', true)->find();
}
$task['projectName'] = $project['name'];
$task['stageName'] = $stage['name'];
//TODO 查看权限
return $task;
}
/**
* @param $projectCode
* @param $deleted
* @throws \think\exception\DbException
*/
public function listForProject($projectCode, $deleted)
{
$this->_list($where);
}
public function dateTotalForProject($projectCode, $beginTime = '', $endTime = '')
{
!$beginTime && $beginTime = date("Y-m-d", strtotime("-20 day"));
!$endTime && $endTime = nowTime();
$dateList = DateService::getDateFromRange($beginTime, $endTime);
$list = [];
if ($dateList) {
foreach ($dateList as $date) {
$currentDate = "{$date} 00:00:00";
$currentDateEnd = "{$date} 23:59:59";
$total = Task::where("project_code = '{$projectCode}' and (create_time between '{$currentDate}' and '{$currentDateEnd}')")->count('id');
$list[] = ['date' => $date, 'total' => $total];
}
}
return $list;
}
public function edit($code, $data)
{
if (!$code) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $code, 'deleted' => 0])->field('id', true)->find();
if (!$task) {
throw new \Exception('该任务在回收站中无法编辑', 1);
}
if (isset($data['description']) && $data['description'] == '<p><br></p>') {
$data['description'] = "";
}
$result = self::update($data, ['code' => $code]);
$member = getCurrentMember();
$type = 'name';
if (isset($data['name'])) {
$type = 'name';
}
if (isset($data['description'])) {
$type = 'content';
if (!$data['description']) {
$type = 'clearContent';
}
}
if (isset($data['pri'])) {
$type = 'pri';
}
if (isset($data['end_time'])) {
$type = 'setEndTime';
if (!$data['end_time']) {
$type = 'clearEndTime';
}
}
self::taskHook($member['code'], $code, $type);
//TODO 任务动态
return $result;
}
public function taskSources($code)
{
if (!$code) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $code])->field('id', true)->find();
if (!$task) {
throw new \Exception('该任务不存在', 2);
}
$sources = SourceLink::where(['link_code' => $code, 'link_type' => 'task'])->field('id', true)->order('id desc')->select()->toArray();
if ($sources) {
foreach ($sources as &$source) {
$source = SourceLink::getSourceDetail($source['code']);
}
}
return $sources;
}
/**
* @param $code
* @param bool $like
* @return bool
* @throws \think\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function like($code, $like = true)
{
if (!$code) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $code, 'deleted' => 0])->field('id', true)->find();
if (!$task) {
throw new \Exception('该任务在回收站中不能点赞', 1);
}
if ($like) {
$result = self::where(['code' => $code])->setInc('like');
} else {
$result = self::where(['code' => $code])->setDec('like');;
}
$member = getCurrentMember();
TaskLike::likeTask($code, $member['code'], $like);
return $result;
}
/**
* @param $code
* @param bool $star
* @return bool
* @throws \think\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function star($code, $star = true)
{
if (!$code) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $code, 'deleted' => 0])->field('id', true)->find();
if (!$task) {
throw new \Exception('该任务在回收站中不能收藏', 1);
}
if ($star) {
$result = self::where(['code' => $code])->setInc('star');
} else {
$result = self::where(['code' => $code])->setDec('star');;
}
$member = getCurrentMember();
Collection::starTask($code, $member['code'], $star);
return $result;
}
/**
* 创建任务
* @param $stageCode
* @param $projectCode
* @param $name
* @param $memberCode
* @param string $assignTo
* @param string $parentCode
* @return Task
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function createTask($stageCode, $projectCode, $name, $memberCode, $assignTo = '', $parentCode = '')
{
if (!$name) {
throw new \Exception('请填写任务标题', 1);
}
$stage = TaskStages::where(['code' => $stageCode])->field('id')->find();
if (!$stage) {
throw new \Exception('该任务列表无效', 2);
}
$project = Project::where(['code' => $projectCode, 'deleted' => 0])->field('id')->find();
if (!$project) {
throw new \Exception('该任务已失效', 3);
}
if ($parentCode) {
$parentTask = self::where(['code' => $parentCode])->find();
if (!$parentTask) {
throw new \Exception('父任务无效', 5);
}
if ($parentTask['deleted']) {
throw new \Exception('父任务在回收站中无法编辑', 6);
}
}
if ($assignTo) {
$assignMember = Member::where(['code' => $assignTo])->field('id')->find();
if (!$assignMember) {
throw new \Exception('任务执行人有误', 4);
}
}
Db::startTrans();
try {
$taskTitles = explode("\n", $name);
foreach ($taskTitles as $taskTitle) {
if (!trim($taskTitle)) {
continue;
}
$maxNum = self::where(['project_code' => $projectCode])->max('id_num');
if (!$maxNum) {
$maxNum = 0;
}
$data = [
'create_time' => nowTime(),
'code' => createUniqueCode('task'),
'create_by' => $memberCode,
'assign_to' => $assignTo,
'id_num' => $maxNum + 1,
'project_code' => $projectCode,
'pcode' => $parentCode,
'stage_code' => $stageCode,
'name' => trim($taskTitle),
];
$result = self::create($data);
// self::update(['sort' => $result['id']], ['id' => $result['id']]);
self::taskHook($memberCode, $data['code'], 'create');
if ($parentCode) {
self::taskHook($memberCode, $parentCode, 'createChild', '', '', 0, '', '', ['taskName' => trim($taskTitle)]);
}
$isExecutor = 0;
$logType = 'inviteMember';
if ($assignTo) {
if ($memberCode == $assignTo) {
$isExecutor = 1;
$logType = 'claim';
}
// Task::taskHook($memberCode, $data['code'], $logType, $assignTo);
TaskMember::inviteMember($assignTo, $data['code'], 1, $isExecutor);
}
if (!$assignTo || !$isExecutor) {
TaskMember::inviteMember($memberCode, $data['code'], 0, 1);
}
}
//todo 添加任务动态
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new \Exception($e->getMessage());
}
return $result;
}
public function taskDone($taskCode, $done)
{
if (!$taskCode) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $taskCode])->find();
if (!$task) {
throw new \Exception('任务已失效', 2);
}
if ($task['deleted']) {
throw new \Exception('任务在回收站中无法进行编辑', 3);
}
Db::startTrans();
try {
$result = self::update(['done' => $done], ['code' => $taskCode]);
//todo 添加任务动态,编辑权限检测
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new \Exception($e->getMessage());
}
$member = getCurrentMember();
$done ? $type = 'done' : $type = 'redo';
self::taskHook($member['code'], $taskCode, $type);
if ($task['pcode']) {
$done ? $type = 'doneChild' : $type = 'redoChild';
self::taskHook($member['code'], $task['pcode'], $type);
}
return $result;
}
/**
* 指派任务
* @param $taskCode
* @param $executorCode
* @return TaskMember|bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function assignTask($taskCode, $executorCode)
{
if (!$taskCode) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $taskCode])->find();
if (!$task) {
throw new \Exception('任务已失效', 2);
}
if ($task['deleted']) {
throw new \Exception('任务在回收站中无法进行指派', 3);
}
Db::startTrans();
try {
$result = TaskMember::inviteMember($executorCode, $taskCode, 1);
//todo 添加任务动态,编辑权限检测
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new \Exception($e->getMessage());
}
return $result;
}
/**
* @param $taskCode
* @param $comment
* @return ProjectLog
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function createComment($taskCode, $comment)
{
if (!$taskCode) {
throw new \Exception('请选择任务', 1);
}
$task = self::where(['code' => $taskCode])->find();
if (!$task) {
throw new \Exception('任务已失效', 2);
}
$data = [
'member_code' => getCurrentMember()['code'],
'task_code' => $taskCode,
'code' => createUniqueCode('taskLog'),
'create_time' => nowTime(),
'is_comment' => 1,
'content' => $comment,
'type' => 'comment'
];
return ProjectLog::create($data);
}
/**
* 任务排序
* @param $stageCode string 移到的任务列表code
* @param $codes array 经过排序的任务code列表
* @return bool
*/
public function sort($stageCode, $codes)
{
if (!$codes) {
return false;
}
if ($codes) {
foreach ($codes as $key => $code) {
self::update(['sort' => $key, 'stage_code' => $stageCode], ['code' => $code]);
}
return true;
}
return false;
}
public function getMemberTasks($memberCode = '', $done = 0, $page = 1, $pageSize = 10)
{
if (!$memberCode) {
$memberCode = getCurrentMember()['code'];
}
if ($page < 1) {
$page = 1;
}
$offset = ($page - 1) * $page;
$limit = $pageSize;
$prefix = config('database.prefix');
$sql = "select *,t.id as id,t.name as name,t.code as code from {$prefix}task as t join {$prefix}project as p on t.project_code = p.code where t.done = {$done} and t.deleted = 0 and t.assign_to = '{$memberCode}' and p.deleted = 0 order by t.id desc";
$total = Db::query($sql);
$total = count($total);
$sql .= " limit {$offset},{$limit}";
$list = Db::query($sql);
return ['list' => $list, 'total' => $total];
}
/**
* 批量放入回收站
* @param $stageCode
* @return Task
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recycleBatch($stageCode)
{
$stage = TaskStages::where(['code' => $stageCode])->find();
if (!$stage) {
throw new \Exception('任务列表不存在', 1);
}
$where = ['stage_code' => $stageCode, 'deleted' => 0];
$taskCodes = self::where($where)->column('code');
$memberCode = getCurrentMember()['code'];
if ($taskCodes) {
foreach ($taskCodes as $taskCode) {
self::taskHook($memberCode, $taskCode, 'recycle');
}
}
$result = self::update(['deleted' => 1, 'deleted_time' => nowTime()], $where);
return $result;
}
/**
* 放入回收站
* @param $code
* @return Project
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recycle($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('任务不存在', 1);
}
if ($info['deleted']) {
throw new \Exception('任务已在回收站', 2);
}
$result = self::update(['deleted' => 1, 'deleted_time' => nowTime()], ['code' => $code]);
self::taskHook(getCurrentMember()['code'], $code, 'recycle');
return $result;
}
/**
* 恢复任务
* @param $code
* @return Project
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function recovery($code)
{
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('任务不存在', 1);
}
if (!$info['deleted']) {
throw new \Exception('任务已恢复', 2);
}
$result = self::update(['deleted' => 0], ['code' => $code]);
self::taskHook(getCurrentMember()['code'], $code, 'recovery');
return $result;
}
public function del($code)
{
//权限判断
$info = self::where(['code' => $code])->find();
if (!$info) {
throw new \Exception('任务不存在', 1);
}
Db::startTrans();
try {
self::where(['code' => $code])->delete();
self::where(['pcode' => $code])->delete();
TaskMember::where(['task_code' => $code])->delete();
TaskLike::where(['task_code' => $code])->delete();
ProjectLog::where(['source_code' => $code, 'action_type' => 'task'])->delete();
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw new \Exception($e->getMessage());
}
return true;
}
public function getPriTextAttr($value, $data)
{
if (!isset($data['pri'])) {
$data['pri'] = 0;
}
$status = [0 => '普通', 1 => '紧急', 2 => '非常紧急'];
return $status[$data['pri']];
}
public function getChildCountAttr($value, $data)
{
$childTasks = [];
if (isset($data['code'])) {
$childTaskCount = self::where(['pcode' => $data['code'], 'deleted' => 0])->count('id');
$childTasks[] = $childTaskCount;
$childTaskCount = self::where(['pcode' => $data['code'], 'deleted' => 0, 'done' => 1])->count('id');
$childTasks[] = $childTaskCount;
}
return $childTasks;
}
public function getHasCommentAttr($value, $data)
{
$comment = 0;
if (isset($data['code'])) {
$comment = ProjectLog::where(['source_code' => $data['code'], 'type' => 'task', 'is_comment' => 1])->count('id');
}
return $comment;
}
public function getHasSourceAttr($value, $data)
{
$sources = 0;
if (isset($data['code'])) {
$sources = SourceLink::where(['link_code' => $data['code'], 'link_type' => 'task'])->count('id');
}
return $sources;
}
public function getLikedAttr($value, $data)
{
$like = 0;
if (isset($data['code'])) {
$member = getCurrentMember();
$taskLike = TaskLike::where(['task_code' => $data['code'], 'member_code' => $member['code']])->find();
if ($taskLike) {
$like = 1;
}
}
return $like;
}
public function getStaredAttr($value, $data)
{
$stared = 0;
if (isset($data['code'])) {
$member = getCurrentMember();
$taskStar = Collection::where(['source_code' => $data['code'], 'type' => 'task', 'member_code' => $member['code']])->find();
if ($taskStar) {
$stared = 1;
}
}
return $stared;
}
/** 任务变动钩子
* @param $memberCode
* @param $taskCode
* @param string $type
* @param string $toMemberCode
* @param int $isComment
* @param string $remark
* @param string $content
* @param string $fileCode
* @param array $data
* @param string $tag
*/
public static function taskHook($memberCode, $taskCode, $type = 'create', $toMemberCode = '', $isComment = 0, $remark = '', $content = '', $fileCode = '', $data = [], $tag = 'task')
{
$data = ['memberCode' => $memberCode, 'taskCode' => $taskCode, 'remark' => $remark, 'type' => $type, 'content' => $content, 'isComment' => $isComment, 'toMemberCode' => $toMemberCode, 'fileCode' => $fileCode, 'data' => $data, 'tag' => $tag];
Hook::listen($tag, $data);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace app\common\Model;
/**
* 任务点赞
* Class TaskLike
* @package app\common\Model
*/
class TaskLike extends CommonModel
{
protected $append = [];
/**
* @param $code
* @param $memberCode
* @param $like
* @return TaskLike|bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function likeTask($code, $memberCode, $like)
{
$liked = self::where(['task_code' => $code, 'member_code' => $memberCode])->find();
if ($like && !$liked) {
$data = [
'create_time' => nowTime(),
'create_by' => $memberCode,
'task_code' => $code,
'member_code' => $memberCode,
];
return self::create($data);
}
if (!$like) {
return self::where(['task_code' => $code, 'member_code' => $memberCode])->delete();
}
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace app\common\Model;
use think\Db;
/**
* 任务成员
* Class ProjectMember
* @package app\common\Model
*/
class TaskMember extends CommonModel
{
protected $append = [];
/**
* @param $memberCode
* @param $taskCode
* @param int $isExecutor
* @param int $isOwner
* @return TaskMember|bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function inviteMember($memberCode, $taskCode, $isExecutor = 0, $isOwner = 0, $fromCreate = false)
{
!$memberCode && $memberCode = '';
$task = Task::where(['code' => $taskCode, 'deleted' => 0])->find();
if (!$task) {
throw new \Exception('该任务已失效', 1);
}
$currentMember = getCurrentMember();
$taskExecutor = self::where(['is_executor' => 1, 'task_code' => $taskCode])->find(); //原执行者
self::update(['is_executor' => 0], ['task_code' => $taskCode]);
if ($memberCode) {
$hasJoined = self::where(['member_code' => $memberCode, 'task_code' => $taskCode])->find();
if ($hasJoined) {
Task::update(['assign_to' => $memberCode], ['code' => $taskCode]);
self::update(['is_executor' => 1], ['task_code' => $taskCode, 'member_code' => $memberCode]);
$logType = 'assign';
if ($memberCode == $currentMember['code']) {
$logType = 'claim';
}
Task::taskHook($currentMember['code'], $taskCode, $logType, $memberCode);
// throw new \Exception('该成员已参与任务', 2);
return true;
}
}
if (!$memberCode) {
//不指派执行人
Task::update(['assign_to' => $memberCode], ['code' => $taskCode]);
if (!$fromCreate) {
if ($taskExecutor) {
Task::taskHook($currentMember['code'], $taskCode, 'removeExecutor', $taskExecutor['member_code']);
}
}
return true;
}
$data = [
'member_code' => $memberCode,
'task_code' => $taskCode,
'is_executor' => $isExecutor,
'is_owner' => $isOwner,
'join_time' => nowTime()
];
//todo 添加任务动态
$result = self::create($data);
if ($isExecutor) {
Task::update(['assign_to' => $memberCode], ['code' => $taskCode]);
if ($memberCode == $currentMember['code']) {
Task::taskHook($currentMember['code'], $taskCode, 'claim');
} else {
Task::taskHook($currentMember['code'], $taskCode, 'assign', $memberCode);
}
}
if ($memberCode) {
$projectModel = new ProjectMember();
$projectModel->inviteMember($memberCode, $task['project_code']);
}
return $result;
}
/**
* 批量邀请成员
* @param $memberCodes
* @param $taskCode
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function inviteMemberBatch($memberCodes, $taskCode)
{
$currentMember = getCurrentMember();
if (!$memberCodes) {
return false;
}
$task = Task::where(['code' => $taskCode, 'deleted' => 0])->find();
if (!$task) {
throw new \Exception('该任务已失效', 1);
}
$isAll = false;
if (in_array('all', $memberCodes)) { //全部项目成员
$memberCodes = ProjectMember::where(['project_code' => $task['project_code']])->column('member_code');
$isAll = true;
}
if ($memberCodes) {
Db::startTrans();
try {
$ownerCode = self::where(['is_owner' => 1, 'task_code' => $taskCode])->column('member_code');
foreach ($memberCodes as $memberCode) {
if ($ownerCode == $memberCode) {
//创建者不能被移除
continue;
}
$hasJoined = self::where(['member_code' => $memberCode, 'task_code' => $taskCode])->find();
if ($hasJoined) {
if (!$isAll) {
if ($hasJoined['is_executor']) {
Task::update(['assign_to' => ''], ['code' => $taskCode]);
Task::taskHook($currentMember['code'], $taskCode, 'removeExecutor', $memberCode);
}
self::where(['task_code' => $taskCode, 'member_code' => $memberCode])->delete();
Task::taskHook($currentMember['code'], $taskCode, 'removeMember', $memberCode);
}
} else {
$data = [
'member_code' => $memberCode,
'task_code' => $taskCode,
'is_executor' => 0,
'join_time' => nowTime()
];
self::create($data);
Task::taskHook($currentMember['code'], $taskCode, 'inviteMember', $memberCode);
}
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
$this->error($e->getMessage(), $e->getCode());;
}
}
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace app\common\Model;
use service\FileService;
use service\RandomService;
use think\File;
/**
* 任务列表
* Class Organization
* @package app\common\Model
*/
class TaskStages extends CommonModel
{
protected $append = [];
/**
* 任务列表下的任务
* @param $stageCode
* @param int $deleted
* @return array|string|\think\Collection
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function tasks($stageCode, $deleted = 0)
{
$list = Task::where(['stage_code' => $stageCode, 'pcode' => '', 'deleted' => $deleted])->order('sort asc,id asc')->field('id', true)->select();
if ($list) {
foreach ($list as &$task) {
$task['executor'] = Member::where(['code' => $task['assign_to']])->field('name,avatar')->find();
}
}
return $list;
}
/**
* @param $name
* @param $projectCode
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function createStage($name, $projectCode)
{
if (!$name) {
throw new \Exception('请填写列表名称', 1);
}
$project = Project::where(['code' => $projectCode, 'deleted' => 0])->field('id')->find();
if (!$project) {
throw new \Exception('该项目已失效', 3);
}
$data = [
'create_time' => nowTime(),
'code' => createUniqueCode('taskStages'),
'project_code' => $projectCode,
'name' => trim($name),
];
$result = self::create($data)->toArray();
self::update(['sort' => $result['id']], ['id' => $result['id']]);
if ($result) {
unset($result['id']);
$result['tasksLoading'] = false; //任务加载状态
$result['fixedCreator'] = false; //添加任务按钮定位
$result['showTaskCard'] = false; //是否显示创建卡片
$result['tasks'] = [];
}
//todo 添加项目动态
return $result;
}
/**
* 列表排序交换两个列表的sort
* @param $preCode string 前一个移动的列表
* @param $nextCode string 后一个移动的列表
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function sort($preCode, $nextCode)
{
$preStage = self::where(['code' => $preCode])->field('sort')->find();
$nextStage = self::where(['code' => $nextCode])->field('sort')->find();
if ($preCode == $nextCode) {
return false;
}
if ($preStage !== false && $preStage !== false) {
self::update(['sort' => $nextStage['sort']], ['code' => $preCode]);
self::update(['sort' => $preStage['sort']], ['code' => $nextCode]);
return true;
}
return false;
}
/**
* 删除列表
* @param $code
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function deleteStage($code)
{
$stage = self::where(['code' => $code])->field('id')->find();
if (!$stage) {
throw new \Exception('该列表不存在', 1);
}
$info = Task::where(['stage_code' => $code, 'deleted' => 0])->find();
if ($info) {
throw new \Exception('请先清空此列表上的任务,然后再删除这个列表', 2);
}
$result = self::destroy(['code' => $code]);
if (!$result) {
throw new \Exception('删除失败', 3);
}
return $result;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace app\common\Model;
use service\FileService;
use service\RandomService;
use think\File;
/**
* 任务列表模板
* Class Organization
* @package app\common\Model
*/
class TaskStagesTemplate extends CommonModel
{
protected $append = [];
public static $defaultTaskStagesNameList = ['待处理', '进行中', '已完成'];
/**
* 创建任务列表模板
* @param $projectTemplateCode
* @param $name
* @return TaskStagesTemplate
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public static function createTaskStagesTemplate($projectTemplateCode, $name)
{
$data = [
'create_time' => nowTime(),
'code' => createUniqueCode('taskStagesTemplate'),
'project_template_code' => $projectTemplateCode,
'name' => $name,
];
$result = self::create($data);
return $result;
}
/**
* 创建任务列表模板
* @param $code
* @param $name
* @param int $sort
* @return TaskStagesTemplate
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function createTemplate($code, $name, $sort = 0)
{
$data = [
'create_time' => nowTime(),
'code' => createUniqueCode('taskStagesTemplate'),
'project_template_code' => $code,
'sort' => $sort,
'name' => $name,
];
$result = self::create($data);
return $result;
}
/**
* 删除模板
* @param $code
* @return bool
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function deleteTemplate($code)
{
$template = self::where(['code' => $code])->field('id')->find();
if (!$template) {
throw new \Exception('该模板不存在', 1);
}
$result = self::destroy(['code' => $code]);
if (!$result) {
throw new \Exception('删除失败', 2);
}
return $result;
}
}

View File

@ -0,0 +1,68 @@
<?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
*/
/**
* 用于检测业务代码死循环或者长时间阻塞等问题
* 如果发现业务卡死可以将下面declare打开去掉//注释并执行php start.php reload
* 然后观察一段时间workerman.log看是否有process_timeout异常
*/
//declare(ticks=1);
use GatewayWorker\Lib\Gateway;
/**
* 主逻辑
* 主要是处理 onConnect onMessage onClose 三个方法
* onConnect onClose 如果不需要可以不用实现并删除
*/
class Events
{
/**
* 当客户端连接时触发
* 如果业务不需此回调可以删除onConnect
* @param int $client_id 连接id
* @throws Exception
*/
public static function onConnect($client_id)
{
// 向当前client_id发送数据
$data = ['action' => 'connect', 'data' => ['client_id' => $client_id]];
Gateway::sendToClient($client_id, json_encode($data));
// 向所有人发送
// Gateway::sendToAll("$client_id login\r\n");
}
/**
* 当客户端发来消息时触发
* @param int $client_id 连接id
* @param mixed $message 具体消息
* @throws Exception
*/
public static function onMessage($client_id, $message)
{
// 向所有人发送
Gateway::sendToAll("$client_id said $message\r\n");
}
/**
* 当用户断开连接时触发
* @param int $client_id 连接id
* @throws Exception
*/
public static function onClose($client_id)
{
// 向所有人发送
GateWay::sendToAll("$client_id logout\r\n");
}
}

View File

@ -0,0 +1,11 @@
{
"name" : "workerman/gateway-worker-for-win-demo",
"keywords": ["distributed","communication"],
"homepage": "http://www.workerman.net",
"license" : "MIT",
"require": {
"workerman/gateway-worker-for-win" : ">=3.0.0",
"workerman/gateway-worker" : ">=3.0.0"
}
}

View File

@ -0,0 +1,175 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "230ece272f676c5e3a976d24a1029942",
"packages": [
{
"name": "workerman/gateway-worker",
"version": "v3.0.12",
"source": {
"type": "git",
"url": "https://github.com/walkor/GatewayWorker.git",
"reference": "c206ec41e21f092055d1ddd3ee296895fc004cb5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/c206ec41e21f092055d1ddd3ee296895fc004cb5",
"reference": "c206ec41e21f092055d1ddd3ee296895fc004cb5",
"shasum": ""
},
"require": {
"workerman/workerman": ">=3.1.8"
},
"type": "library",
"autoload": {
"psr-4": {
"GatewayWorker\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"communication",
"distributed"
],
"time": "2018-08-21T06:17:30+00:00"
},
{
"name": "workerman/gateway-worker-for-win",
"version": "v3.0.7",
"source": {
"type": "git",
"url": "https://github.com/walkor/GatewayWorker-for-win.git",
"reference": "ea75b5d581db1762e9928f5729200a1abcaba496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/GatewayWorker-for-win/zipball/ea75b5d581db1762e9928f5729200a1abcaba496",
"reference": "ea75b5d581db1762e9928f5729200a1abcaba496",
"shasum": ""
},
"require": {
"workerman/workerman-for-win": ">=3.1.8"
},
"type": "library",
"autoload": {
"psr-4": {
"GatewayWorker\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"communication",
"distributed"
],
"time": "2017-06-26T14:51:29+00:00"
},
{
"name": "workerman/workerman",
"version": "v3.5.15",
"source": {
"type": "git",
"url": "https://github.com/walkor/Workerman.git",
"reference": "6df60271e514201a17a96acb8ea16936000444cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/Workerman/zipball/6df60271e514201a17a96acb8ea16936000444cb",
"reference": "6df60271e514201a17a96acb8ea16936000444cb",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"suggest": {
"ext-event": "For better performance. "
},
"type": "library",
"autoload": {
"psr-4": {
"Workerman\\": "./"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "http://www.workerman.net",
"role": "Developer"
}
],
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
"homepage": "http://www.workerman.net",
"keywords": [
"asynchronous",
"event-loop"
],
"time": "2018-09-20T09:11:43+00:00"
},
{
"name": "workerman/workerman-for-win",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/walkor/workerman-for-win.git",
"reference": "cbaae3193e4567fd9cfc8099931c63d9b12174ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/workerman-for-win/zipball/cbaae3193e4567fd9cfc8099931c63d9b12174ee",
"reference": "cbaae3193e4567fd9cfc8099931c63d9b12174ee",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"type": "project",
"autoload": {
"psr-4": {
"Workerman\\": "./"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "http://www.workerman.net",
"role": "Developer"
}
],
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
"homepage": "http://www.workerman.net",
"keywords": [
"asynchronous",
"event-loop"
],
"time": "2017-08-28T10:05:00+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

View File

@ -0,0 +1,37 @@
<?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
*/
use Workerman\Worker;
use Workerman\WebServer;
use GatewayWorker\Gateway;
use GatewayWorker\BusinessWorker;
use Workerman\Autoloader;
require_once __DIR__ . '/vendor/autoload.php';
// 自动加载类
// bussinessWorker 进程
$worker = new BusinessWorker();
// worker名称
$worker->name = 'YourAppBusinessWorker';
// bussinessWorker进程数量
$worker->count = 4;
// 服务注册地址
$worker->registerAddress = '192.168.0.159:2346';
// 如果不是在根目录启动则运行runAll方法
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}

View File

@ -0,0 +1,85 @@
<?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
*/
use Workerman\Worker;
use Workerman\WebServer;
use GatewayWorker\Gateway;
use GatewayWorker\BusinessWorker;
use Workerman\Autoloader;
require_once __DIR__ . '/vendor/autoload.php';
$ssl = false;
$context = array();
if ($ssl) {
// 证书最好是申请的证书
$context = array(
// 更多ssl选项请参考手册 http://php.net/manual/zh/context.ssl.php
'ssl' => array(
// 请使用绝对路径
'local_cert' => '/www/wwwroot/pms/server.pem', // 也可以是crt文件
'local_pk' => '/www/wwwroot/pms/server.key',
'verify_peer' => false,
'allow_self_signed' => true, //如果是自签名证书需要开启此选项
)
);
}
// gateway 进程这里使用Text协议可以用telnet测试
$gateway = new Gateway("websocket://192.168.0.159:2345", $context);
if ($ssl) {
// 开启SSLwebsocket+SSL 即wss
$gateway->transport = 'ssl';
}
// gateway名称status方便查看
$gateway->name = 'YourAppGateway';
// gateway进程数
$gateway->count = 4;
// 本机ip分布式部署时使用内网ip
$gateway->lanIp = '127.0.0.1';
// 内部通讯起始端口,假如$gateway->count=4起始端口为4000
// 则一般会使用4000 4001 4002 4003 4个端口作为内部通讯端口
$gateway->startPort = 2900;
// 服务注册地址
$gateway->registerAddress = '192.168.0.159:2346';
// 心跳间隔
//$gateway->pingInterval = 10;
// 心跳数据
//$gateway->pingData = '{"type":"ping"}';
/*
// 当客户端连接上来时设置连接的onWebSocketConnect即在websocket握手时的回调
$gateway->onConnect = function($connection)
{
$connection->onWebSocketConnect = function($connection , $http_header)
{
// 可以在这里判断连接来源是否合法,不合法就关掉连接
// $_SERVER['HTTP_ORIGIN']标识来自哪个站点的页面发起的websocket链接
if($_SERVER['HTTP_ORIGIN'] != 'http://kedou.workerman.net')
{
$connection->close();
}
// onWebSocketConnect 里面$_GET $_SERVER是可用的
// var_dump($_GET, $_SERVER);
};
};
*/
// 如果不是在根目录启动则运行runAll方法
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}

View File

@ -0,0 +1,29 @@
<?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
*/
use Workerman\Worker;
use GatewayWorker\Register;
require_once __DIR__ . '/vendor/autoload.php';
// 自动加载类
// register 必须是text协议
$register = new Register('http://192.168.0.159:2346');
// 如果不是在根目录启动则运行runAll方法
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}

View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit5472d435d8e6cb539c167a0dead78fd7::getLoader();

View File

@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

View File

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,11 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Workerman\\' => array($vendorDir . '/workerman/workerman', $vendorDir . '/workerman/workerman-for-win'),
'GatewayWorker\\' => array($vendorDir . '/workerman/gateway-worker/src', $vendorDir . '/workerman/gateway-worker-for-win/src'),
);

View File

@ -0,0 +1,52 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit5472d435d8e6cb539c167a0dead78fd7
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit5472d435d8e6cb539c167a0dead78fd7', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit5472d435d8e6cb539c167a0dead78fd7', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit5472d435d8e6cb539c167a0dead78fd7::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
return $loader;
}
}

View File

@ -0,0 +1,41 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit5472d435d8e6cb539c167a0dead78fd7
{
public static $prefixLengthsPsr4 = array (
'W' =>
array (
'Workerman\\' => 10,
),
'G' =>
array (
'GatewayWorker\\' => 14,
),
);
public static $prefixDirsPsr4 = array (
'Workerman\\' =>
array (
0 => __DIR__ . '/..' . '/workerman/workerman',
1 => __DIR__ . '/..' . '/workerman/workerman-for-win',
),
'GatewayWorker\\' =>
array (
0 => __DIR__ . '/..' . '/workerman/gateway-worker/src',
1 => __DIR__ . '/..' . '/workerman/gateway-worker-for-win/src',
),
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit5472d435d8e6cb539c167a0dead78fd7::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit5472d435d8e6cb539c167a0dead78fd7::$prefixDirsPsr4;
}, null, ClassLoader::class);
}
}

View File

@ -0,0 +1,167 @@
[
{
"name": "workerman/gateway-worker",
"version": "v3.0.12",
"version_normalized": "3.0.12.0",
"source": {
"type": "git",
"url": "https://github.com/walkor/GatewayWorker.git",
"reference": "c206ec41e21f092055d1ddd3ee296895fc004cb5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/c206ec41e21f092055d1ddd3ee296895fc004cb5",
"reference": "c206ec41e21f092055d1ddd3ee296895fc004cb5",
"shasum": ""
},
"require": {
"workerman/workerman": ">=3.1.8"
},
"time": "2018-08-21T06:17:30+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"GatewayWorker\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"communication",
"distributed"
]
},
{
"name": "workerman/gateway-worker-for-win",
"version": "v3.0.7",
"version_normalized": "3.0.7.0",
"source": {
"type": "git",
"url": "https://github.com/walkor/GatewayWorker-for-win.git",
"reference": "ea75b5d581db1762e9928f5729200a1abcaba496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/GatewayWorker-for-win/zipball/ea75b5d581db1762e9928f5729200a1abcaba496",
"reference": "ea75b5d581db1762e9928f5729200a1abcaba496",
"shasum": ""
},
"require": {
"workerman/workerman-for-win": ">=3.1.8"
},
"time": "2017-06-26T14:51:29+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"GatewayWorker\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"communication",
"distributed"
]
},
{
"name": "workerman/workerman",
"version": "v3.5.15",
"version_normalized": "3.5.15.0",
"source": {
"type": "git",
"url": "https://github.com/walkor/Workerman.git",
"reference": "6df60271e514201a17a96acb8ea16936000444cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/Workerman/zipball/6df60271e514201a17a96acb8ea16936000444cb",
"reference": "6df60271e514201a17a96acb8ea16936000444cb",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"suggest": {
"ext-event": "For better performance. "
},
"time": "2018-09-20T09:11:43+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Workerman\\": "./"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "http://www.workerman.net",
"role": "Developer"
}
],
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
"homepage": "http://www.workerman.net",
"keywords": [
"asynchronous",
"event-loop"
]
},
{
"name": "workerman/workerman-for-win",
"version": "v3.5.1",
"version_normalized": "3.5.1.0",
"source": {
"type": "git",
"url": "https://github.com/walkor/workerman-for-win.git",
"reference": "cbaae3193e4567fd9cfc8099931c63d9b12174ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/workerman-for-win/zipball/cbaae3193e4567fd9cfc8099931c63d9b12174ee",
"reference": "cbaae3193e4567fd9cfc8099931c63d9b12174ee",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"time": "2017-08-28T10:05:00+00:00",
"type": "project",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Workerman\\": "./"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "http://www.workerman.net",
"role": "Developer"
}
],
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
"homepage": "http://www.workerman.net",
"keywords": [
"asynchronous",
"event-loop"
]
}
]

View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2009-2015 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/workerman/contributors)
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,30 @@
GatewayWorker windows 版本
=================
GatewayWorker基于[Workerman](https://github.com/walkor/Workerman)开发的一个项目框架用于快速开发长连接应用例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等。
GatewayWorker使用经典的Gateway和Worker进程模型。Gateway进程负责维持客户端连接并转发客户端的数据给Worker进程处理Worker进程负责处理实际的业务逻辑并将结果推送给对应的客户端。Gateway服务和Worker服务可以分开部署在不同的服务器上实现分布式集群。
GatewayWorker提供非常方便的API可以全局广播数据、可以向某个群体广播数据、也可以向某个特定客户端推送数据。配合Workerman的定时器也可以定时推送数据。
下载安装
=====
本仓库只是GatewayWorker的核心仓库
完整的版本[点击这里下载](http://www.workerman.net/download/GatewayWorker-for-win.zip)
手册
=======
http://www.workerman.net/gatewaydoc/
使用GatewayWorker-for-win开发的项目
=======
## [tadpole](http://kedou.workerman.net/)
[Live demo](http://kedou.workerman.net/)
[Source code](https://github.com/walkor/workerman)
![workerman-todpole](http://www.workerman.net/img/workerman-todpole.png)
## [chat room](http://chat.workerman.net/)
[Live demo](http://chat.workerman.net/)
[Source code](https://github.com/walkor/workerman-chat)
![workerman-chat](http://www.workerman.net/img/workerman-chat.png)

View File

@ -0,0 +1,12 @@
{
"name" : "workerman/gateway-worker-for-win",
"keywords": ["distributed","communication"],
"homepage": "http://www.workerman.net",
"license" : "MIT",
"require": {
"workerman/workerman-for-win" : ">=3.1.8"
},
"autoload": {
"psr-4": {"GatewayWorker\\": "./src"}
}
}

View File

@ -0,0 +1,546 @@
<?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 GatewayWorker;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\AsyncTcpConnection;
use GatewayWorker\Protocols\GatewayProtocol;
use GatewayWorker\Lib\Context;
/**
*
* BusinessWorker 用于处理Gateway转发来的数据
*
* @author walkor<walkor@workerman.net>
*
*/
class BusinessWorker extends Worker
{
/**
* 保存与 gateway 的连接 connection 对象
*
* @var array
*/
public $gatewayConnections = array();
/**
* 注册中心地址
*
* @var string
*/
public $registerAddress = '127.0.0.1:1236';
/**
* 事件处理类,默认是 Event
*
* @var string
*/
public $eventHandler = 'Events';
/**
* 业务超时时间,可用来定位程序卡在哪里
*
* @var int
*/
public $processTimeout = 30;
/**
* 业务超时时间,可用来定位程序卡在哪里
*
* @var callable
*/
public $processTimeoutHandler = '\\Workerman\\Worker::log';
/**
* 秘钥
*
* @var string
*/
public $secretKey = '';
/**
* businessWorker进程将消息转发给gateway进程的发送缓冲区大小
*
* @var int
*/
public $sendToGatewayBufferSize = 10240000;
/**
* 保存用户设置的 worker 启动回调
*
* @var callback
*/
protected $_onWorkerStart = null;
/**
* 保存用户设置的 workerReload 回调
*
* @var callback
*/
protected $_onWorkerReload = null;
/**
* 保存用户设置的 workerStop 回调
*
* @var callback
*/
protected $_onWorkerStop= null;
/**
* 到注册中心的连接
*
* @var AsyncTcpConnection
*/
protected $_registerConnection = null;
/**
* 处于连接状态的 gateway 通讯地址
*
* @var array
*/
protected $_connectingGatewayAddresses = array();
/**
* 所有 geteway 内部通讯地址
*
* @var array
*/
protected $_gatewayAddresses = array();
/**
* 等待连接个 gateway 地址
*
* @var array
*/
protected $_waitingConnectGatewayAddresses = array();
/**
* Event::onConnect 回调
*
* @var callback
*/
protected $_eventOnConnect = null;
/**
* Event::onMessage 回调
*
* @var callback
*/
protected $_eventOnMessage = null;
/**
* Event::onClose 回调
*
* @var callback
*/
protected $_eventOnClose = null;
/**
* SESSION 版本缓存
*
* @var array
*/
protected $_sessionVersion = array();
/**
* 用于保持长连接的心跳时间间隔
*
* @var int
*/
const PERSISTENCE_CONNECTION_PING_INTERVAL = 25;
/**
* 构造函数
*
* @param string $socket_name
* @param array $context_option
*/
public function __construct($socket_name = '', $context_option = array())
{
parent::__construct($socket_name, $context_option);
$backrace = debug_backtrace();
$this->_autoloadRootPath = dirname($backrace[0]['file']);
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->_onWorkerStart = $this->onWorkerStart;
$this->_onWorkerReload = $this->onWorkerReload;
$this->_onWorkerStop = $this->onWorkerStop;
$this->onWorkerStop = array($this, 'onWorkerStop');
$this->onWorkerStart = array($this, 'onWorkerStart');
$this->onWorkerReload = array($this, 'onWorkerReload');
parent::run();
}
/**
* 当进程启动时一些初始化工作
*
* @return void
*/
protected function onWorkerStart()
{
if (!class_exists('\Protocols\GatewayProtocol')) {
class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol');
}
$this->connectToRegister();
\GatewayWorker\Lib\Gateway::setBusinessWorker($this);
\GatewayWorker\Lib\Gateway::$secretKey = $this->secretKey;
if ($this->_onWorkerStart) {
call_user_func($this->_onWorkerStart, $this);
}
if (is_callable($this->eventHandler . '::onWorkerStart')) {
call_user_func($this->eventHandler . '::onWorkerStart', $this);
}
if (function_exists('pcntl_signal')) {
// 业务超时信号处理
pcntl_signal(SIGALRM, array($this, 'timeoutHandler'), false);
} else {
$this->processTimeout = 0;
}
// 设置回调
if (is_callable($this->eventHandler . '::onConnect')) {
$this->_eventOnConnect = $this->eventHandler . '::onConnect';
}
if (is_callable($this->eventHandler . '::onMessage')) {
$this->_eventOnMessage = $this->eventHandler . '::onMessage';
} else {
echo "Waring: {$this->eventHandler}::onMessage is not callable\n";
}
if (is_callable($this->eventHandler . '::onClose')) {
$this->_eventOnClose = $this->eventHandler . '::onClose';
}
// 如果Register服务器不在本地服务器则需要保持心跳
if (strpos($this->registerAddress, '127.0.0.1') !== 0) {
Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingRegister'));
}
}
/**
* onWorkerReload 回调
*
* @param Worker $worker
*/
protected function onWorkerReload($worker)
{
// 防止进程立刻退出
$worker->reloadable = false;
// 延迟 0.05 秒退出,避免 BusinessWorker 瞬间全部退出导致没有可用的 BusinessWorker 进程
Timer::add(0.05, array('Workerman\Worker', 'stopAll'));
// 执行用户定义的 onWorkerReload 回调
if ($this->_onWorkerReload) {
call_user_func($this->_onWorkerReload, $this);
}
}
/**
* 当进程关闭时一些清理工作
*
* @return void
*/
protected function onWorkerStop()
{
if ($this->_onWorkerStop) {
call_user_func($this->_onWorkerStop, $this);
}
if (is_callable($this->eventHandler . '::onWorkerStop')) {
call_user_func($this->eventHandler . '::onWorkerStop', $this);
}
}
/**
* 连接服务注册中心
*
* @return void
*/
public function connectToRegister()
{
$this->_registerConnection = new AsyncTcpConnection("text://{$this->registerAddress}");
$this->_registerConnection->send('{"event":"worker_connect","secret_key":"' . $this->secretKey . '"}');
$this->_registerConnection->onClose = array($this, 'onRegisterConnectionClose');
$this->_registerConnection->onMessage = array($this, 'onRegisterConnectionMessage');
$this->_registerConnection->connect();
}
/**
* 与注册中心连接关闭时,定时重连
*
* @return void
*/
public function onRegisterConnectionClose()
{
Timer::add(1, array($this, 'connectToRegister'), null, false);
}
/**
* 当注册中心发来消息时
*
* @return void
*/
public function onRegisterConnectionMessage($register_connection, $data)
{
$data = json_decode($data, true);
if (!isset($data['event'])) {
echo "Received bad data from Register\n";
return;
}
$event = $data['event'];
switch ($event) {
case 'broadcast_addresses':
if (!is_array($data['addresses'])) {
echo "Received bad data from Register. Addresses empty\n";
return;
}
$addresses = $data['addresses'];
$this->_gatewayAddresses = array();
foreach ($addresses as $addr) {
$this->_gatewayAddresses[$addr] = $addr;
}
$this->checkGatewayConnections($addresses);
break;
default:
echo "Receive bad event:$event from Register.\n";
}
}
/**
* gateway 转发来数据时
*
* @param TcpConnection $connection
* @param mixed $data
*/
public function onGatewayMessage($connection, $data)
{
$cmd = $data['cmd'];
if ($cmd === GatewayProtocol::CMD_PING) {
return;
}
// 上下文数据
Context::$client_ip = $data['client_ip'];
Context::$client_port = $data['client_port'];
Context::$local_ip = $data['local_ip'];
Context::$local_port = $data['local_port'];
Context::$connection_id = $data['connection_id'];
Context::$client_id = Context::addressToClientId($data['local_ip'], $data['local_port'],
$data['connection_id']);
// $_SERVER 变量
$_SERVER = array(
'REMOTE_ADDR' => long2ip($data['client_ip']),
'REMOTE_PORT' => $data['client_port'],
'GATEWAY_ADDR' => long2ip($data['local_ip']),
'GATEWAY_PORT' => $data['gateway_port'],
'GATEWAY_CLIENT_ID' => Context::$client_id,
);
// 检查session版本如果是过期的session数据则拉取最新的数据
if ($cmd !== GatewayProtocol::CMD_ON_CLOSE && isset($this->_sessionVersion[Context::$client_id]) && $this->_sessionVersion[Context::$client_id] !== crc32($data['ext_data'])) {
$_SESSION = Context::$old_session = \GatewayWorker\Lib\Gateway::getSession(Context::$client_id);
} else {
if (!isset($this->_sessionVersion[Context::$client_id])) {
$this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']);
}
// 尝试解析 session
if ($data['ext_data'] != '') {
Context::$old_session = $_SESSION = Context::sessionDecode($data['ext_data']);
} else {
Context::$old_session = $_SESSION = null;
}
}
if ($this->processTimeout) {
pcntl_alarm($this->processTimeout);
}
// 尝试执行 Event::onConnection、Event::onMessage、Event::onClose
switch ($cmd) {
case GatewayProtocol::CMD_ON_CONNECTION:
if ($this->_eventOnConnect) {
call_user_func($this->_eventOnConnect, Context::$client_id);
}
break;
case GatewayProtocol::CMD_ON_MESSAGE:
if ($this->_eventOnMessage) {
call_user_func($this->_eventOnMessage, Context::$client_id, $data['body']);
}
break;
case GatewayProtocol::CMD_ON_CLOSE:
unset($this->_sessionVersion[Context::$client_id]);
if ($this->_eventOnClose) {
call_user_func($this->_eventOnClose, Context::$client_id);
}
break;
}
if ($this->processTimeout) {
pcntl_alarm(0);
}
// session 必须是数组
if ($_SESSION !== null && !is_array($_SESSION)) {
throw new \Exception('$_SESSION must be an array. But $_SESSION=' . var_export($_SESSION, true) . ' is not array.');
}
// 判断 session 是否被更改
if ($_SESSION !== Context::$old_session && $cmd !== GatewayProtocol::CMD_ON_CLOSE) {
$session_str_now = $_SESSION !== null ? Context::sessionEncode($_SESSION) : '';
\GatewayWorker\Lib\Gateway::setSocketSession(Context::$client_id, $session_str_now);
$this->_sessionVersion[Context::$client_id] = crc32($session_str_now);
}
Context::clear();
}
/**
* 当与 Gateway 的连接断开时触发
*
* @param TcpConnection $connection
* @return void
*/
public function onGatewayClose($connection)
{
$addr = $connection->remoteAddress;
unset($this->gatewayConnections[$addr], $this->_connectingGatewayAddresses[$addr]);
if (isset($this->_gatewayAddresses[$addr]) && !isset($this->_waitingConnectGatewayAddresses[$addr])) {
Timer::add(1, array($this, 'tryToConnectGateway'), array($addr), false);
$this->_waitingConnectGatewayAddresses[$addr] = $addr;
}
}
/**
* 尝试连接 Gateway 内部通讯地址
*
* @param string $addr
*/
public function tryToConnectGateway($addr)
{
if (!isset($this->gatewayConnections[$addr]) && !isset($this->_connectingGatewayAddresses[$addr]) && isset($this->_gatewayAddresses[$addr])) {
$gateway_connection = new AsyncTcpConnection("GatewayProtocol://$addr");
$gateway_connection->remoteAddress = $addr;
$gateway_connection->onConnect = array($this, 'onConnectGateway');
$gateway_connection->onMessage = array($this, 'onGatewayMessage');
$gateway_connection->onClose = array($this, 'onGatewayClose');
$gateway_connection->onError = array($this, 'onGatewayError');
$gateway_connection->maxSendBufferSize = $this->sendToGatewayBufferSize;
if (TcpConnection::$defaultMaxSendBufferSize == $gateway_connection->maxSendBufferSize) {
$gateway_connection->maxSendBufferSize = 50 * 1024 * 1024;
}
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_WORKER_CONNECT;
$gateway_data['body'] = json_encode(array(
'worker_key' =>"{$this->name}:{$this->id}",
'secret_key' => $this->secretKey,
));
$gateway_connection->send($gateway_data);
$gateway_connection->connect();
$this->_connectingGatewayAddresses[$addr] = $addr;
}
unset($this->_waitingConnectGatewayAddresses[$addr]);
}
/**
* 检查 gateway 的通信端口是否都已经连
* 如果有未连接的端口,则尝试连接
*
* @param array $addresses_list
*/
public function checkGatewayConnections($addresses_list)
{
if (empty($addresses_list)) {
return;
}
foreach ($addresses_list as $addr) {
if (!isset($this->_waitingConnectGatewayAddresses[$addr])) {
$this->tryToConnectGateway($addr);
}
}
}
/**
* 当连接上 gateway 的通讯端口时触发
* 将连接 connection 对象保存起来
*
* @param TcpConnection $connection
* @return void
*/
public function onConnectGateway($connection)
{
$this->gatewayConnections[$connection->remoteAddress] = $connection;
unset($this->_connectingGatewayAddresses[$connection->remoteAddress], $this->_waitingConnectGatewayAddresses[$connection->remoteAddress]);
}
/**
* 当与 gateway 的连接出现错误时触发
*
* @param TcpConnection $connection
* @param int $error_no
* @param string $error_msg
*/
public function onGatewayError($connection, $error_no, $error_msg)
{
echo "GatewayConnection Error : $error_no ,$error_msg\n";
}
/**
* 获取所有 Gateway 内部通讯地址
*
* @return array
*/
public function getAllGatewayAddresses()
{
return $this->_gatewayAddresses;
}
/**
* 业务超时回调
*
* @param int $signal
* @throws \Exception
*/
public function timeoutHandler($signal)
{
switch ($signal) {
// 超时时钟
case SIGALRM:
// 超时异常
$e = new \Exception("process_timeout", 506);
$trace_str = $e->getTraceAsString();
// 去掉第一行timeoutHandler的调用栈
$trace_str = $e->getMessage() . ":\n" . substr($trace_str, strpos($trace_str, "\n") + 1) . "\n";
// 开发者没有设置超时处理函数,或者超时处理函数返回空则执行退出
if (!$this->processTimeoutHandler || !call_user_func($this->processTimeoutHandler, $trace_str, $e)) {
Worker::stopAll();
}
break;
}
}
/**
* Register 发送心跳,用来保持长连接
*/
public function pingRegister()
{
if ($this->_registerConnection) {
$this->_registerConnection->send('{"event":"ping"}');
}
}
}

View File

@ -0,0 +1,916 @@
<?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 GatewayWorker;
use GatewayWorker\Lib\Context;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Autoloader;
use Workerman\Connection\AsyncTcpConnection;
use GatewayWorker\Protocols\GatewayProtocol;
/**
*
* Gateway基于Worker 开发
* 用于转发客户端的数据给Worker处理以及转发Worker的数据给客户端
*
* @author walkor<walkor@workerman.net>
*
*/
class Gateway extends Worker
{
/**
* 版本
*
* @var string
*/
const VERSION = '3.0.7';
/**
* 本机 IP
* 单机部署默认 127.0.0.1,如果是分布式部署,需要设置成本机 IP
*
* @var string
*/
public $lanIp = '127.0.0.1';
/**
* 本机端口
*
* @var string
*/
public $lanPort = 0;
/**
* gateway 内部通讯起始端口,每个 gateway 实例应该都不同步长1000
*
* @var int
*/
public $startPort = 2000;
/**
* 注册服务地址,用于注册 Gateway BusinessWorker使之能够通讯
*
* @var string
*/
public $registerAddress = '127.0.0.1:1236';
/**
* 是否可以平滑重启gateway 不能平滑重启,否则会导致连接断开
*
* @var bool
*/
public $reloadable = false;
/**
* 心跳时间间隔
*
* @var int
*/
public $pingInterval = 0;
/**
* $pingNotResponseLimit * $pingInterval 时间内,客户端未发送任何数据,断开客户端连接
*
* @var int
*/
public $pingNotResponseLimit = 0;
/**
* 服务端向客户端发送的心跳数据
*
* @var string
*/
public $pingData = '';
/**
* 秘钥
*
* @var string
*/
public $secretKey = '';
/**
* 路由函数
*
* @var callback
*/
public $router = null;
/**
* gateway进程转发给businessWorker进程的发送缓冲区大小
*
* @var int
*/
public $sendToWorkerBufferSize = 10240000;
/**
* gateway进程将数据发给客户端时每个客户端发送缓冲区大小
*
* @var int
*/
public $sendToClientBufferSize = 1024000;
/**
* 协议加速
*
* @var bool
*/
public $protocolAccelerate = false;
/**
* 保存客户端的所有 connection 对象
*
* @var array
*/
protected $_clientConnections = array();
/**
* uid connection 的映射,一对多关系
*/
protected $_uidConnections = array();
/**
* group connection 的映射,一对多关系
*
* @var array
*/
protected $_groupConnections = array();
/**
* 保存所有 worker 的内部连接的 connection 对象
*
* @var array
*/
protected $_workerConnections = array();
/**
* gateway 内部监听 worker 内部连接的 worker
*
* @var Worker
*/
protected $_innerTcpWorker = null;
/**
* worker 启动时
*
* @var callback
*/
protected $_onWorkerStart = null;
/**
* 当有客户端连接时
*
* @var callback
*/
protected $_onConnect = null;
/**
* 当客户端发来消息时
*
* @var callback
*/
protected $_onMessage = null;
/**
* 当客户端连接关闭时
*
* @var callback
*/
protected $_onClose = null;
/**
* worker 停止时
*
* @var callback
*/
protected $_onWorkerStop = null;
/**
* 进程启动时间
*
* @var int
*/
protected $_startTime = 0;
/**
* gateway 监听的端口
*
* @var int
*/
protected $_gatewayPort = 0;
/**
* 到注册中心的连接
*
* @var AsyncTcpConnection
*/
protected $_registerConnection = null;
/**
* connectionId 记录器
* @var int
*/
protected static $_connectionIdRecorder = 0;
/**
* 用于保持长连接的心跳时间间隔
*
* @var int
*/
const PERSISTENCE_CONNECTION_PING_INTERVAL = 25;
/**
* 构造函数
*
* @param string $socket_name
* @param array $context_option
*/
public function __construct($socket_name, $context_option = array())
{
parent::__construct($socket_name, $context_option);
$this->_gatewayPort = substr(strrchr($socket_name,':'),1);
$this->router = array("\\GatewayWorker\\Gateway", 'routerBind');
$backrace = debug_backtrace();
$this->_autoloadRootPath = dirname($backrace[0]['file']);
}
/**
* {@inheritdoc}
*/
public function run()
{
// 保存用户的回调,当对应的事件发生时触发
$this->_onWorkerStart = $this->onWorkerStart;
$this->onWorkerStart = array($this, 'onWorkerStart');
// 保存用户的回调,当对应的事件发生时触发
$this->_onConnect = $this->onConnect;
$this->onConnect = array($this, 'onClientConnect');
// onMessage禁止用户设置回调
$this->onMessage = array($this, 'onClientMessage');
// 保存用户的回调,当对应的事件发生时触发
$this->_onClose = $this->onClose;
$this->onClose = array($this, 'onClientClose');
// 保存用户的回调,当对应的事件发生时触发
$this->_onWorkerStop = $this->onWorkerStop;
$this->onWorkerStop = array($this, 'onWorkerStop');
// 记录进程启动的时间
$this->_startTime = time();
// 运行父方法
parent::run();
}
/**
* 当客户端发来数据时转发给worker处理
*
* @param TcpConnection $connection
* @param mixed $data
*/
public function onClientMessage($connection, $data)
{
$connection->pingNotResponseCount = -1;
$this->sendToWorker(GatewayProtocol::CMD_ON_MESSAGE, $connection, $data);
}
/**
* 当客户端连接上来时,初始化一些客户端的数据
* 包括全局唯一的client_id、初始化session等
*
* @param TcpConnection $connection
*/
public function onClientConnect($connection)
{
$connection->id = self::generateConnectionId();
// 保存该连接的内部通讯的数据包报头,避免每次重新初始化
$connection->gatewayHeader = array(
'local_ip' => ip2long($this->lanIp),
'local_port' => $this->lanPort,
'client_ip' => ip2long($connection->getRemoteIp()),
'client_port' => $connection->getRemotePort(),
'gateway_port' => $this->_gatewayPort,
'connection_id' => $connection->id,
'flag' => 0,
);
// 连接的 session
$connection->session = '';
// 该连接的心跳参数
$connection->pingNotResponseCount = -1;
// 该链接发送缓冲区大小
$connection->maxSendBufferSize = $this->sendToClientBufferSize;
// 保存客户端连接 connection 对象
$this->_clientConnections[$connection->id] = $connection;
// 如果用户有自定义 onConnect 回调,则执行
if ($this->_onConnect) {
call_user_func($this->_onConnect, $connection);
}
$this->sendToWorker(GatewayProtocol::CMD_ON_CONNECTION, $connection);
}
/**
* 生成connection id
* @return int
*/
protected function generateConnectionId()
{
$max_unsigned_int = 4294967295;
if (self::$_connectionIdRecorder >= $max_unsigned_int) {
self::$_connectionIdRecorder = 0;
}
while(++self::$_connectionIdRecorder <= $max_unsigned_int) {
if(!isset($this->_clientConnections[self::$_connectionIdRecorder])) {
break;
}
}
return self::$_connectionIdRecorder;
}
/**
* 发送数据给 worker 进程
*
* @param int $cmd
* @param TcpConnection $connection
* @param mixed $body
* @return bool
*/
protected function sendToWorker($cmd, $connection, $body = '')
{
$gateway_data = $connection->gatewayHeader;
$gateway_data['cmd'] = $cmd;
$gateway_data['body'] = $body;
$gateway_data['ext_data'] = $connection->session;
if ($this->_workerConnections) {
// 调用路由函数选择一个worker把请求转发给它
/** @var TcpConnection $worker_connection */
$worker_connection = call_user_func($this->router, $this->_workerConnections, $connection, $cmd, $body);
if (false === $worker_connection->send($gateway_data)) {
$msg = "SendBufferToWorker fail. May be the send buffer are overflow. See http://wiki.workerman.net/Error2 for detail";
$this->log($msg);
return false;
}
} // 没有可用的 worker
else {
// gateway 启动后 1-2 秒内 SendBufferToWorker fail 是正常现象,因为与 worker 的连接还没建立起来,
// 所以不记录日志,只是关闭连接
$time_diff = 2;
if (time() - $this->_startTime >= $time_diff) {
$msg = 'SendBufferToWorker fail. The connections between Gateway and BusinessWorker are not ready. See http://wiki.workerman.net/Error3 for detail';
$this->log($msg);
}
$connection->destroy();
return false;
}
return true;
}
/**
* 随机路由,返回 worker connection 对象
*
* @param array $worker_connections
* @param TcpConnection $client_connection
* @param int $cmd
* @param mixed $buffer
* @return TcpConnection
*/
public static function routerRand($worker_connections, $client_connection, $cmd, $buffer)
{
return $worker_connections[array_rand($worker_connections)];
}
/**
* client_id worker 绑定
*
* @param array $worker_connections
* @param TcpConnection $client_connection
* @param int $cmd
* @param mixed $buffer
* @return TcpConnection
*/
public static function routerBind($worker_connections, $client_connection, $cmd, $buffer)
{
if (!isset($client_connection->businessworker_address) || !isset($worker_connections[$client_connection->businessworker_address])) {
$client_connection->businessworker_address = array_rand($worker_connections);
}
return $worker_connections[$client_connection->businessworker_address];
}
/**
* 当客户端关闭时
*
* @param TcpConnection $connection
*/
public function onClientClose($connection)
{
// 尝试通知 worker触发 Event::onClose
$this->sendToWorker(GatewayProtocol::CMD_ON_CLOSE, $connection);
unset($this->_clientConnections[$connection->id]);
// 清理 uid 数据
if (!empty($connection->uid)) {
$uid = $connection->uid;
unset($this->_uidConnections[$uid][$connection->id]);
if (empty($this->_uidConnections[$uid])) {
unset($this->_uidConnections[$uid]);
}
}
// 清理 group 数据
if (!empty($connection->groups)) {
foreach ($connection->groups as $group) {
unset($this->_groupConnections[$group][$connection->id]);
if (empty($this->_groupConnections[$group])) {
unset($this->_groupConnections[$group]);
}
}
}
// 触发 onClose
if ($this->_onClose) {
call_user_func($this->_onClose, $connection);
}
}
/**
* Gateway 启动的时候触发的回调函数
*
* @return void
*/
public function onWorkerStart()
{
// 分配一个内部通讯端口
$this->lanPort = $this->startPort + $this->id;
// 如果有设置心跳,则定时执行
if ($this->pingInterval > 0) {
$timer_interval = $this->pingNotResponseLimit > 0 ? $this->pingInterval / 2 : $this->pingInterval;
Timer::add($timer_interval, array($this, 'ping'));
}
// 如果BusinessWorker ip不是127.0.0.1则需要加gateway到BusinessWorker的心跳
if ($this->lanIp !== '127.0.0.1') {
Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingBusinessWorker'));
}
// 如果 Register 服务器不在本地服务器,则需要保持心跳
if (strpos($this->registerAddress, '127.0.0.1') !== 0) {
Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingRegister'));
}
if (!class_exists('\Protocols\GatewayProtocol')) {
class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol');
}
// 初始化 gateway 内部的监听,用于监听 worker 的连接已经连接上发来的数据
$this->_innerTcpWorker = new Worker("GatewayProtocol://{$this->lanIp}:{$this->lanPort}");
$this->_innerTcpWorker->listen();
// 重新设置自动加载根目录
Autoloader::setRootPath($this->_autoloadRootPath);
// 设置内部监听的相关回调
$this->_innerTcpWorker->onMessage = array($this, 'onWorkerMessage');
$this->_innerTcpWorker->onConnect = array($this, 'onWorkerConnect');
$this->_innerTcpWorker->onClose = array($this, 'onWorkerClose');
// 注册 gateway 的内部通讯地址worker 去连这个地址,以便 gateway 与 worker 之间建立起 TCP 长连接
$this->registerAddress();
if ($this->_onWorkerStart) {
call_user_func($this->_onWorkerStart, $this);
}
}
/**
* worker 通过内部通讯端口连接到 gateway
*
* @param TcpConnection $connection
*/
public function onWorkerConnect($connection)
{
$connection->maxSendBufferSize = $this->sendToWorkerBufferSize;
$connection->authorized = $this->secretKey ? false : true;
}
/**
* worker 发来数据时
*
* @param TcpConnection $connection
* @param mixed $data
* @throws \Exception
*/
public function onWorkerMessage($connection, $data)
{
$cmd = $data['cmd'];
if (empty($connection->authorized) && $cmd !== GatewayProtocol::CMD_WORKER_CONNECT && $cmd !== GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT) {
self::log("Unauthorized request from " . $connection->getRemoteIp() . ":" . $connection->getRemotePort());
return $connection->close();
}
switch ($cmd) {
// BusinessWorker连接Gateway
case GatewayProtocol::CMD_WORKER_CONNECT:
$worker_info = json_decode($data['body'], true);
if ($worker_info['secret_key'] !== $this->secretKey) {
self::log("Gateway: Worker key does not match ".var_export($this->secretKey, true)." !== ". var_export($this->secretKey));
return $connection->close();
}
$key = $connection->getRemoteIp() . ':' . $worker_info['worker_key'];
// 在一台服务器上businessWorker->name不能相同
if (isset($this->_workerConnections[$key])) {
self::log("Gateway: Worker->name conflict. Key:{$key}");
$connection->close();
return;
}
$connection->key = $key;
$this->_workerConnections[$key] = $connection;
$connection->authorized = true;
return;
// GatewayClient连接Gateway
case GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT:
$worker_info = json_decode($data['body'], true);
if ($worker_info['secret_key'] !== $this->secretKey) {
self::log("Gateway: GatewayClient key does not match ".var_export($this->secretKey, true)." !== ".var_export($this->secretKey, true));
return $connection->close();
}
$connection->authorized = true;
return;
// 向某客户端发送数据Gateway::sendToClient($client_id, $message);
case GatewayProtocol::CMD_SEND_TO_ONE:
if (isset($this->_clientConnections[$data['connection_id']])) {
$this->_clientConnections[$data['connection_id']]->send($data['body']);
}
return;
// 踢出用户Gateway::closeClient($client_id, $message);
case GatewayProtocol::CMD_KICK:
if (isset($this->_clientConnections[$data['connection_id']])) {
$this->_clientConnections[$data['connection_id']]->close($data['body']);
}
return;
// 立即销毁用户连接, Gateway::destroyClient($client_id);
case GatewayProtocol::CMD_DESTROY:
if (isset($this->_clientConnections[$data['connection_id']])) {
$this->_clientConnections[$data['connection_id']]->destroy();
}
return;
// 广播, Gateway::sendToAll($message, $client_id_array)
case GatewayProtocol::CMD_SEND_TO_ALL:
$raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE);
$body = $data['body'];
if (!$raw && $this->protocolAccelerate && $this->protocol) {
$body = $this->preEncodeForClient($body);
$raw = true;
}
$ext_data = $data['ext_data'] ? json_decode($data['ext_data'], true) : '';
// $client_id_array 不为空时,只广播给 $client_id_array 指定的客户端
if (isset($ext_data['connections'])) {
foreach ($ext_data['connections'] as $connection_id) {
if (isset($this->_clientConnections[$connection_id])) {
$this->_clientConnections[$connection_id]->send($body, $raw);
}
}
} // $client_id_array 为空时,广播给所有在线客户端
else {
$exclude_connection_id = !empty($ext_data['exclude']) ? $ext_data['exclude'] : null;
foreach ($this->_clientConnections as $client_connection) {
if (!isset($exclude_connection_id[$client_connection->id])) {
$client_connection->send($body, $raw);
}
}
}
return;
// 重新赋值 session
case GatewayProtocol::CMD_SET_SESSION:
if (isset($this->_clientConnections[$data['connection_id']])) {
$this->_clientConnections[$data['connection_id']]->session = $data['ext_data'];
}
return;
// session合并
case GatewayProtocol::CMD_UPDATE_SESSION:
if (!isset($this->_clientConnections[$data['connection_id']])) {
return;
} else {
if (!$this->_clientConnections[$data['connection_id']]->session) {
$this->_clientConnections[$data['connection_id']]->session = $data['ext_data'];
return;
}
$session = Context::sessionDecode($this->_clientConnections[$data['connection_id']]->session);
$session_for_merge = Context::sessionDecode($data['ext_data']);
$session = $session_for_merge + $session;
$this->_clientConnections[$data['connection_id']]->session = Context::sessionEncode($session);
}
return;
case GatewayProtocol::CMD_GET_SESSION_BY_CLIENT_ID:
if (!isset($this->_clientConnections[$data['connection_id']])) {
$session = serialize(null);
} else {
if (!$this->_clientConnections[$data['connection_id']]->session) {
$session = serialize(array());
} else {
$session = $this->_clientConnections[$data['connection_id']]->session;
}
}
$connection->send(pack('N', strlen($session)) . $session, true);
return;
// 获得客户端在线状态 Gateway::getALLClientInfo()
case GatewayProtocol::CMD_GET_ALL_CLIENT_INFO:
$client_info_array = array();
foreach ($this->_clientConnections as $connection_id => $client_connection) {
$client_info_array[$connection_id] = $client_connection->session;
}
$buffer = serialize($client_info_array);
$connection->send(pack('N', strlen($buffer)) . $buffer, true);
return;
// 判断某个 client_id 是否在线 Gateway::isOnline($client_id)
case GatewayProtocol::CMD_IS_ONLINE:
$buffer = serialize((int)isset($this->_clientConnections[$data['connection_id']]));
$connection->send(pack('N', strlen($buffer)) . $buffer, true);
return;
// 将 client_id 与 uid 绑定
case GatewayProtocol::CMD_BIND_UID:
$uid = $data['ext_data'];
if (empty($uid)) {
echo "bindUid(client_id, uid) uid empty, uid=" . var_export($uid, true);
return;
}
$connection_id = $data['connection_id'];
if (!isset($this->_clientConnections[$connection_id])) {
return;
}
$client_connection = $this->_clientConnections[$connection_id];
if (isset($client_connection->uid)) {
$current_uid = $client_connection->uid;
unset($this->_uidConnections[$current_uid][$connection_id]);
if (empty($this->_uidConnections[$current_uid])) {
unset($this->_uidConnections[$current_uid]);
}
}
$client_connection->uid = $uid;
$this->_uidConnections[$uid][$connection_id] = $client_connection;
return;
// client_id 与 uid 解绑 Gateway::unbindUid($client_id, $uid);
case GatewayProtocol::CMD_UNBIND_UID:
$connection_id = $data['connection_id'];
if (!isset($this->_clientConnections[$connection_id])) {
return;
}
$client_connection = $this->_clientConnections[$connection_id];
if (isset($client_connection->uid)) {
$current_uid = $client_connection->uid;
unset($this->_uidConnections[$current_uid][$connection_id]);
if (empty($this->_uidConnections[$current_uid])) {
unset($this->_uidConnections[$current_uid]);
}
$client_connection->uid_info = '';
$client_connection->uid = null;
}
return;
// 发送数据给 uid Gateway::sendToUid($uid, $msg);
case GatewayProtocol::CMD_SEND_TO_UID:
$uid_array = json_decode($data['ext_data'], true);
foreach ($uid_array as $uid) {
if (!empty($this->_uidConnections[$uid])) {
foreach ($this->_uidConnections[$uid] as $connection) {
/** @var TcpConnection $connection */
$connection->send($data['body']);
}
}
}
return;
// 将 $client_id 加入用户组 Gateway::joinGroup($client_id, $group);
case GatewayProtocol::CMD_JOIN_GROUP:
$group = $data['ext_data'];
if (empty($group)) {
echo "join(group) group empty, group=" . var_export($group, true);
return;
}
$connection_id = $data['connection_id'];
if (!isset($this->_clientConnections[$connection_id])) {
return;
}
$client_connection = $this->_clientConnections[$connection_id];
if (!isset($client_connection->groups)) {
$client_connection->groups = array();
}
$client_connection->groups[$group] = $group;
$this->_groupConnections[$group][$connection_id] = $client_connection;
return;
// 将 $client_id 从某个用户组中移除 Gateway::leaveGroup($client_id, $group);
case GatewayProtocol::CMD_LEAVE_GROUP:
$group = $data['ext_data'];
if (empty($group)) {
echo "leave(group) group empty, group=" . var_export($group, true);
return;
}
$connection_id = $data['connection_id'];
if (!isset($this->_clientConnections[$connection_id])) {
return;
}
$client_connection = $this->_clientConnections[$connection_id];
if (!isset($client_connection->groups[$group])) {
return;
}
unset($client_connection->groups[$group], $this->_groupConnections[$group][$connection_id]);
return;
// 向某个用户组发送消息 Gateway::sendToGroup($group, $msg);
case GatewayProtocol::CMD_SEND_TO_GROUP:
$raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE);
$body = $data['body'];
if (!$raw && $this->protocolAccelerate && $this->protocol) {
$body = $this->preEncodeForClient($body);
$raw = true;
}
$ext_data = json_decode($data['ext_data'], true);
$group_array = $ext_data['group'];
$exclude_connection_id = $ext_data['exclude'];
foreach ($group_array as $group) {
if (!empty($this->_groupConnections[$group])) {
foreach ($this->_groupConnections[$group] as $connection) {
if(!isset($exclude_connection_id[$connection->id]))
{
/** @var TcpConnection $connection */
$connection->send($body, $raw);
}
}
}
}
return;
// 获取某用户组成员信息 Gateway::getClientInfoByGroup($group);
case GatewayProtocol::CMD_GET_CLINET_INFO_BY_GROUP:
$group = $data['ext_data'];
if (!isset($this->_groupConnections[$group])) {
$buffer = serialize(array());
$connection->send(pack('N', strlen($buffer)) . $buffer, true);
return;
}
$client_info_array = array();
foreach ($this->_groupConnections[$group] as $connection_id => $client_connection) {
$client_info_array[$connection_id] = $client_connection->session;
}
$buffer = serialize($client_info_array);
$connection->send(pack('N', strlen($buffer)) . $buffer, true);
return;
// 获取用户组成员数 Gateway::getClientCountByGroup($group);
case GatewayProtocol::CMD_GET_CLIENT_COUNT_BY_GROUP:
$group = $data['ext_data'];
$count = 0;
if ($group !== '') {
if (isset($this->_groupConnections[$group])) {
$count = count($this->_groupConnections[$group]);
}
} else {
$count = count($this->_clientConnections);
}
$buffer = serialize($count);
$connection->send(pack('N', strlen($buffer)) . $buffer, true);
return;
// 获取与某个 uid 绑定的所有 client_id Gateway::getClientIdByUid($uid);
case GatewayProtocol::CMD_GET_CLIENT_ID_BY_UID:
$uid = $data['ext_data'];
if (empty($this->_uidConnections[$uid])) {
$buffer = serialize(array());
} else {
$buffer = serialize(array_keys($this->_uidConnections[$uid]));
}
$connection->send(pack('N', strlen($buffer)) . $buffer, true);
return;
default :
$err_msg = "gateway inner pack err cmd=$cmd";
throw new \Exception($err_msg);
}
}
/**
* 当worker连接关闭时
*
* @param TcpConnection $connection
*/
public function onWorkerClose($connection)
{
// $this->log("{$connection->key} CLOSE INNER_CONNECTION\n");
if (isset($connection->key)) {
unset($this->_workerConnections[$connection->key]);
}
}
/**
* 存储当前 Gateway 的内部通信地址
*
* @return bool
*/
public function registerAddress()
{
$address = $this->lanIp . ':' . $this->lanPort;
$this->_registerConnection = new AsyncTcpConnection("text://{$this->registerAddress}");
$this->_registerConnection->send('{"event":"gateway_connect", "address":"' . $address . '", "secret_key":"' . $this->secretKey . '"}');
$this->_registerConnection->onClose = array($this, 'onRegisterConnectionClose');
$this->_registerConnection->connect();
}
public function onRegisterConnectionClose()
{
Timer::add(1, array($this, 'registerAddress'), null, false);
}
/**
* 心跳逻辑
*
* @return void
*/
public function ping()
{
$ping_data = $this->pingData ? (string)$this->pingData : null;
$raw = false;
if ($this->protocolAccelerate && $ping_data && $this->protocol) {
$ping_data = $this->preEncodeForClient($ping_data);
$raw = true;
}
// 遍历所有客户端连接
foreach ($this->_clientConnections as $connection) {
// 上次发送的心跳还没有回复次数大于限定值就断开
if ($this->pingNotResponseLimit > 0 &&
$connection->pingNotResponseCount >= $this->pingNotResponseLimit * 2
) {
$connection->destroy();
continue;
}
// $connection->pingNotResponseCount 为 -1 说明最近客户端有发来消息,则不给客户端发送心跳
$connection->pingNotResponseCount++;
if ($ping_data) {
if ($connection->pingNotResponseCount === 0 ||
($this->pingNotResponseLimit > 0 && $connection->pingNotResponseCount % 2 === 1)
) {
continue;
}
$connection->send($ping_data, $raw);
}
}
}
/**
* BusinessWorker 发送心跳数据,用于保持长连接
*
* @return void
*/
public function pingBusinessWorker()
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_PING;
foreach ($this->_workerConnections as $connection) {
$connection->send($gateway_data);
}
}
/**
* Register 发送心跳,用来保持长连接
*/
public function pingRegister()
{
if ($this->_registerConnection) {
$this->_registerConnection->send('{"event":"ping"}');
}
}
/**
* @param mixed $data
*
* @return string
*/
protected function preEncodeForClient($data)
{
foreach ($this->_clientConnections as $client_connection) {
return call_user_func(array($client_connection->protocol, 'encode'), $data, $client_connection);
}
}
/**
* gateway 关闭时触发,清理数据
*
* @return void
*/
public function onWorkerStop()
{
// 尝试触发用户设置的回调
if ($this->_onWorkerStop) {
call_user_func($this->_onWorkerStop, $this);
}
}
}

View File

@ -0,0 +1,136 @@
<?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 GatewayWorker\Lib;
use Exception;
/**
* 上下文 包含当前用户 uid 内部通信 local_ip local_port socket_id以及客户端 client_ip client_port
*/
class Context
{
/**
* 内部通讯 id
*
* @var string
*/
public static $local_ip;
/**
* 内部通讯端口
*
* @var int
*/
public static $local_port;
/**
* 客户端 ip
*
* @var string
*/
public static $client_ip;
/**
* 客户端端口
*
* @var int
*/
public static $client_port;
/**
* client_id
*
* @var string
*/
public static $client_id;
/**
* 连接 connection->id
*
* @var int
*/
public static $connection_id;
/**
* 旧的session
*
* @var string
*/
public static $old_session;
/**
* 编码 session
*
* @param mixed $session_data
* @return string
*/
public static function sessionEncode($session_data = '')
{
if ($session_data !== '') {
return serialize($session_data);
}
return '';
}
/**
* 解码 session
*
* @param string $session_buffer
* @return mixed
*/
public static function sessionDecode($session_buffer)
{
return unserialize($session_buffer);
}
/**
* 清除上下文
*
* @return void
*/
public static function clear()
{
self::$local_ip = self::$local_port = self::$client_ip = self::$client_port =
self::$client_id = self::$connection_id = self::$old_session = null;
}
/**
* 通讯地址到 client_id 的转换
*
* @param int $local_ip
* @param int $local_port
* @param int $connection_id
* @return string
*/
public static function addressToClientId($local_ip, $local_port, $connection_id)
{
return bin2hex(pack('NnN', $local_ip, $local_port, $connection_id));
}
/**
* client_id 到通讯地址的转换
*
* @param string $client_id
* @return array
* @throws Exception
*/
public static function clientIdToAddress($client_id)
{
if (strlen($client_id) !== 20) {
echo new Exception("client_id $client_id is invalid");
return false;
}
return unpack('Nlocal_ip/nlocal_port/Nconnection_id', pack('H*', $client_id));
}
}

View File

@ -0,0 +1,76 @@
<?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 GatewayWorker\Lib;
use Config\Db as DbConfig;
use Exception;
/**
* 数据库类
*/
class Db
{
/**
* 实例数组
*
* @var array
*/
protected static $instance = array();
/**
* 获取实例
*
* @param string $config_name
* @return DbConnection
* @throws Exception
*/
public static function instance($config_name)
{
if (!isset(DbConfig::$$config_name)) {
echo "\\Config\\Db::$config_name not set\n";
throw new Exception("\\Config\\Db::$config_name not set\n");
}
if (empty(self::$instance[$config_name])) {
$config = DbConfig::$$config_name;
self::$instance[$config_name] = new DbConnection($config['host'], $config['port'],
$config['user'], $config['password'], $config['dbname']);
}
return self::$instance[$config_name];
}
/**
* 关闭数据库实例
*
* @param string $config_name
*/
public static function close($config_name)
{
if (isset(self::$instance[$config_name])) {
self::$instance[$config_name]->closeConnection();
self::$instance[$config_name] = null;
}
}
/**
* 关闭所有数据库实例
*/
public static function closeAll()
{
foreach (self::$instance as $connection) {
$connection->closeConnection();
}
self::$instance = array();
}
}

View File

@ -0,0 +1,919 @@
<?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 GatewayWorker\Lib;
use Exception;
use GatewayWorker\Protocols\GatewayProtocol;
use Workerman\Connection\TcpConnection;
/**
* 数据发送相关
*/
class Gateway
{
/**
* gateway 实例
*
* @var object
*/
protected static $businessWorker = null;
/**
* 注册中心地址
*
* @var string
*/
public static $registerAddress = '127.0.0.1:1236';
/**
* 秘钥
* @var string
*/
public static $secretKey = '';
/**
* 链接超时时间
* @var int
*/
public static $connectTimeout = 3;
/**
* 与Gateway是否是长链接
* @var bool
*/
public static $persistentConnection = true;
/**
* 向所有客户端连接(或者 client_id_array 指定的客户端连接)广播消息
*
* @param string $message 向客户端发送的消息
* @param array $client_id_array 客户端 id 数组
* @param array $exclude_client_id 不给这些client_id发
* @param bool $raw 是否发送原始数据即不调用gateway的协议的encode方法
* @return void
*/
public static function sendToAll($message, $client_id_array = null, $exclude_client_id = null, $raw = false)
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_ALL;
$gateway_data['body'] = $message;
if ($raw) {
$gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE;
}
if ($exclude_client_id) {
if (!is_array($exclude_client_id)) {
$exclude_client_id = array($exclude_client_id);
}
if ($client_id_array) {
$exclude_client_id = array_flip($exclude_client_id);
}
}
if ($client_id_array) {
if (!is_array($client_id_array)) {
echo new \Exception('bad $client_id_array:'.var_export($client_id_array, true));
return;
}
$data_array = array();
foreach ($client_id_array as $client_id) {
if (isset($exclude_client_id[$client_id])) {
continue;
}
$address = Context::clientIdToAddress($client_id);
if ($address) {
$key = long2ip($address['local_ip']) . ":{$address['local_port']}";
$data_array[$key][$address['connection_id']] = $address['connection_id'];
}
}
foreach ($data_array as $addr => $connection_id_list) {
$the_gateway_data = $gateway_data;
$the_gateway_data['ext_data'] = json_encode(array('connections' => $connection_id_list));
self::sendToGateway($addr, $the_gateway_data);
}
return;
} elseif (empty($client_id_array) && is_array($client_id_array)) {
return;
}
if (!$exclude_client_id) {
return self::sendToAllGateway($gateway_data);
}
$address_connection_array = self::clientIdArrayToAddressArray($exclude_client_id);
// 如果有businessWorker实例说明运行在workerman环境中通过businessWorker中的长连接发送数据
if (self::$businessWorker) {
foreach (self::$businessWorker->gatewayConnections as $address => $gateway_connection) {
$gateway_data['ext_data'] = isset($address_connection_array[$address]) ?
json_encode(array('exclude'=> $address_connection_array[$address])) : '';
/** @var TcpConnection $gateway_connection */
$gateway_connection->send($gateway_data);
}
} // 运行在其它环境中通过注册中心得到gateway地址
else {
$all_addresses = self::getAllGatewayAddressesFromRegister();
if (!$all_addresses) {
throw new Exception('Gateway::getAllGatewayAddressesFromRegister() with registerAddress:' .
self::$registerAddress . ' return ' . var_export($all_addresses, true));
}
foreach ($all_addresses as $address) {
$gateway_data['ext_data'] = isset($address_connection_array[$address]) ?
json_encode(array('exclude'=> $address_connection_array[$address])) : '';
self::sendToGateway($address, $gateway_data);
}
}
}
/**
* 向某个客户端连接发消息
*
* @param int $client_id
* @param string $message
* @return bool
*/
public static function sendToClient($client_id, $message)
{
return self::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SEND_TO_ONE, $message);
}
/**
* 向当前客户端连接发送消息
*
* @param string $message
* @return bool
*/
public static function sendToCurrentClient($message)
{
return self::sendCmdAndMessageToClient(null, GatewayProtocol::CMD_SEND_TO_ONE, $message);
}
/**
* 判断某个uid是否在线
*
* @param string $uid
* @return int 0|1
*/
public static function isUidOnline($uid)
{
return (int)self::getClientIdByUid($uid);
}
/**
* 判断某个客户端连接是否在线
*
* @param int $client_id
* @return int 0|1
*/
public static function isOnline($client_id)
{
$address_data = Context::clientIdToAddress($client_id);
if (!$address_data) {
return 0;
}
$address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}";
if (isset(self::$businessWorker)) {
if (!isset(self::$businessWorker->gatewayConnections[$address])) {
return 0;
}
}
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_IS_ONLINE;
$gateway_data['connection_id'] = $address_data['connection_id'];
return (int)self::sendAndRecv($address, $gateway_data);
}
/**
* 获取所有在线用户的sessionclient_id为 key
*
* @param string $group
* @return array
*/
public static function getAllClientInfo($group = null)
{
return self::getAllClientSessions($group);
}
/**
* 获取所有在线用户的sessionclient_id为 key
*
* @param string $group
* @return array
*/
public static function getAllClientSessions($group = null)
{
$gateway_data = GatewayProtocol::$empty;
if (!$group) {
$gateway_data['cmd'] = GatewayProtocol::CMD_GET_ALL_CLIENT_INFO;
} else {
$gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLINET_INFO_BY_GROUP;
$gateway_data['ext_data'] = $group;
}
$status_data = array();
$all_buffer_array = self::getBufferFromAllGateway($gateway_data);
foreach ($all_buffer_array as $local_ip => $buffer_array) {
foreach ($buffer_array as $local_port => $data) {
if ($data) {
foreach ($data as $connection_id => $session_buffer) {
$client_id = Context::addressToClientId($local_ip, $local_port, $connection_id);
if ($client_id === Context::$client_id) {
$status_data[$client_id] = (array)$_SESSION;
} else {
$status_data[$client_id] = $session_buffer ? Context::sessionDecode($session_buffer) : array();
}
}
}
}
}
return $status_data;
}
/**
* 获取某个组的连接信息
*
* @param string $group
* @return array
*/
public static function getClientInfoByGroup($group)
{
return self::getAllClientSessions($group);
}
/**
* 获取某个组的连接信息
*
* @param string $group
* @return array
*/
public static function getClientSessionsByGroup($group)
{
return self::getAllClientSessions($group);
}
/**
* 获取所有连接数
*
* @return int
*/
public static function getAllClientCount()
{
return self::getClientCountByGroup();
}
/**
* 获取某个组的在线连接数
*
* @param string $group
* @return int
*/
public static function getClientCountByGroup($group = '')
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_COUNT_BY_GROUP;
$gateway_data['ext_data'] = $group;
$total_count = 0;
$all_buffer_array = self::getBufferFromAllGateway($gateway_data);
foreach ($all_buffer_array as $local_ip => $buffer_array) {
foreach ($buffer_array as $local_port => $count) {
if ($count) {
$total_count += $count;
}
}
}
return $total_count;
}
/**
* 获取与 uid 绑定的 client_id 列表
*
* @param string $uid
* @return array
*/
public static function getClientIdByUid($uid)
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_ID_BY_UID;
$gateway_data['ext_data'] = $uid;
$client_list = array();
$all_buffer_array = self::getBufferFromAllGateway($gateway_data);
foreach ($all_buffer_array as $local_ip => $buffer_array) {
foreach ($buffer_array as $local_port => $connection_id_array) {
if ($connection_id_array) {
foreach ($connection_id_array as $connection_id) {
$client_list[] = Context::addressToClientId($local_ip, $local_port, $connection_id);
}
}
}
}
return $client_list;
}
/**
* 生成验证包,用于验证此客户端的合法性
*
* @return string
*/
protected static function generateAuthBuffer()
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT;
$gateway_data['body'] = json_encode(array(
'secret_key' => self::$secretKey,
));
return GatewayProtocol::encode($gateway_data);
}
/**
* 批量向所有 gateway 发包,并得到返回数组
*
* @param string $gateway_data
* @return array
* @throws Exception
*/
protected static function getBufferFromAllGateway($gateway_data)
{
$gateway_buffer = GatewayProtocol::encode($gateway_data);
$gateway_buffer = self::$secretKey ? self::generateAuthBuffer() . $gateway_buffer : $gateway_buffer;
if (isset(self::$businessWorker)) {
$all_addresses = self::$businessWorker->getAllGatewayAddresses();
if (empty($all_addresses)) {
throw new Exception('businessWorker::getAllGatewayAddresses return empty');
}
} else {
$all_addresses = self::getAllGatewayAddressesFromRegister();
if (empty($all_addresses)) {
return array();
}
}
$client_array = $status_data = $client_address_map = $receive_buffer_array = $recv_length_array = array();
// 批量向所有gateway进程发送请求数据
foreach ($all_addresses as $address) {
$client = stream_socket_client("tcp://$address", $errno, $errmsg, self::$connectTimeout);
if ($client && strlen($gateway_buffer) === stream_socket_sendto($client, $gateway_buffer)) {
$socket_id = (int)$client;
$client_array[$socket_id] = $client;
$client_address_map[$socket_id] = explode(':', $address);
$receive_buffer_array[$socket_id] = '';
}
}
// 超时5秒
$timeout = 5;
$time_start = microtime(true);
// 批量接收请求
while (count($client_array) > 0) {
$write = $except = array();
$read = $client_array;
if (@stream_select($read, $write, $except, $timeout)) {
foreach ($read as $client) {
$socket_id = (int)$client;
$buffer = stream_socket_recvfrom($client, 65535);
if ($buffer !== '' && $buffer !== false) {
$receive_buffer_array[$socket_id] .= $buffer;
$receive_length = strlen($receive_buffer_array[$socket_id]);
if (empty($recv_length_array[$socket_id]) && $receive_length >= 4) {
$recv_length_array[$socket_id] = current(unpack('N', $receive_buffer_array[$socket_id]));
}
if (!empty($recv_length_array[$socket_id]) && $receive_length >= $recv_length_array[$socket_id] + 4) {
unset($client_array[$socket_id]);
}
} elseif (feof($client)) {
unset($client_array[$socket_id]);
}
}
}
if (microtime(true) - $time_start > $timeout) {
break;
}
}
$format_buffer_array = array();
foreach ($receive_buffer_array as $socket_id => $buffer) {
$local_ip = ip2long($client_address_map[$socket_id][0]);
$local_port = $client_address_map[$socket_id][1];
$format_buffer_array[$local_ip][$local_port] = unserialize(substr($buffer, 4));
}
return $format_buffer_array;
}
/**
* 踢掉某个客户端,并以$message通知被踢掉客户端
*
* @param int $client_id
* @param string $message
* @return bool
*/
public static function closeClient($client_id, $message = null)
{
if ($client_id === Context::$client_id) {
return self::closeCurrentClient($message);
} // 不是发给当前用户则使用存储中的地址
else {
$address_data = Context::clientIdToAddress($client_id);
if (!$address_data) {
return false;
}
$address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}";
return self::kickAddress($address, $address_data['connection_id'], $message);
}
}
/**
* 踢掉当前客户端,并以$message通知被踢掉客户端
*
* @param string $message
* @return bool
* @throws Exception
*/
public static function closeCurrentClient($message = null)
{
if (!Context::$connection_id) {
throw new Exception('closeCurrentClient can not be called in async context');
}
$address = long2ip(Context::$local_ip) . ':' . Context::$local_port;
return self::kickAddress($address, Context::$connection_id, $message);
}
/**
* 踢掉某个客户端并直接立即销毁相关连接
*
* @param int $client_id
* @return bool
*/
public static function destoryClient($client_id)
{
if ($client_id === Context::$client_id) {
return self::destoryCurrentClient();
} // 不是发给当前用户则使用存储中的地址
else {
$address_data = Context::clientIdToAddress($client_id);
if (!$address_data) {
return false;
}
$address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}";
return self::destroyAddress($address, $address_data['connection_id']);
}
}
/**
* 踢掉当前客户端并直接立即销毁相关连接
*
* @return bool
* @throws Exception
*/
public static function destoryCurrentClient()
{
if (!Context::$connection_id) {
throw new Exception('destoryCurrentClient can not be called in async context');
}
$address = long2ip(Context::$local_ip) . ':' . Context::$local_port;
return self::destroyAddress($address, Context::$connection_id);
}
/**
* client_id uid 绑定
*
* @param int $client_id
* @param int|string $uid
* @return bool
*/
public static function bindUid($client_id, $uid)
{
return self::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_BIND_UID, '', $uid);
}
/**
* client_id uid 解除绑定
*
* @param int $client_id
* @param int|string $uid
* @return bool
*/
public static function unbindUid($client_id, $uid)
{
return self::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UNBIND_UID, '', $uid);
}
/**
* client_id 加入组
*
* @param int $client_id
* @param int|string $group
* @return bool
*/
public static function joinGroup($client_id, $group)
{
return self::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_JOIN_GROUP, '', $group);
}
/**
* client_id 离开组
*
* @param int $client_id
* @param int|string $group
* @return bool
*/
public static function leaveGroup($client_id, $group)
{
return self::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_LEAVE_GROUP, '', $group);
}
/**
* 向所有 uid 发送
*
* @param int|string|array $uid
* @param string $message
*/
public static function sendToUid($uid, $message)
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_UID;
$gateway_data['body'] = $message;
if (!is_array($uid)) {
$uid = array($uid);
}
$gateway_data['ext_data'] = json_encode($uid);
self::sendToAllGateway($gateway_data);
}
/**
* group 发送
*
* @param int|string|array $group 组(不允许是 0 '0' false null array()等为空的值)
* @param string $message 消息
* @param array $exclude_client_id 不给这些client_id发
* @param bool $raw 发送原始数据即不调用gateway的协议的encode方法
*/
public static function sendToGroup($group, $message, $exclude_client_id = null, $raw = false)
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_GROUP;
$gateway_data['body'] = $message;
if ($raw) {
$gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE;
}
if (!is_array($group)) {
$group = array($group);
}
// 分组发送没有排除的client_id直接发送
$default_ext_data_buffer = json_encode(array('group'=> $group, 'exclude'=> null));
if (empty($exclude_client_id)) {
$gateway_data['ext_data'] = $default_ext_data_buffer;
return self::sendToAllGateway($gateway_data);
}
// 分组发送有排除的client_id需要将client_id转换成对应gateway进程内的connectionId
if (!is_array($exclude_client_id)) {
$exclude_client_id = array($exclude_client_id);
}
$address_connection_array = self::clientIdArrayToAddressArray($exclude_client_id);
// 如果有businessWorker实例说明运行在workerman环境中通过businessWorker中的长连接发送数据
if (self::$businessWorker) {
foreach (self::$businessWorker->gatewayConnections as $address => $gateway_connection) {
$gateway_data['ext_data'] = isset($address_connection_array[$address]) ?
json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) :
$default_ext_data_buffer;
/** @var TcpConnection $gateway_connection */
$gateway_connection->send($gateway_data);
}
} // 运行在其它环境中通过注册中心得到gateway地址
else {
$all_addresses = self::getAllGatewayAddressesFromRegister();
if (!$all_addresses) {
throw new Exception('Gateway::getAllGatewayAddressesFromRegister() with registerAddress:' .
self::$registerAddress . ' return ' . var_export($all_addresses, true));
}
foreach ($all_addresses as $address) {
$gateway_data['ext_data'] = isset($address_connection_array[$address]) ?
json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) :
$default_ext_data_buffer;
self::sendToGateway($address, $gateway_data);
}
}
}
/**
* 更新 session框架自动调用开发者不要调用
*
* @param int $client_id
* @param string $session_str
* @return bool
*/
public static function setSocketSession($client_id, $session_str)
{
return self::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SET_SESSION, '', $session_str);
}
/**
* 设置 session原session值会被覆盖
*
* @param int $client_id
* @param array $session
*/
public static function setSession($client_id, array $session)
{
if (Context::$client_id === $client_id) {
$_SESSION = $session;
Context::$old_session = $_SESSION;
}
return self::setSocketSession($client_id, Context::sessionEncode($session));
}
/**
* 更新 session实际上是与老的session合并
*
* @param int $client_id
* @param array $session
*/
public static function updateSession($client_id, array $session)
{
if (Context::$client_id === $client_id) {
$_SESSION = $session + (array)$_SESSION;
Context::$old_session = $_SESSION;
}
return self::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UPDATE_SESSION, '', Context::sessionEncode($session));
}
/**
* 获取某个client_id的session
*
* @param int $client_id
* @return mixed false表示出错、null表示用户不存在、array表示具体的session信息
*/
public static function getSession($client_id)
{
$address_data = Context::clientIdToAddress($client_id);
if (!$address_data) {
return false;
}
$address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}";
if (isset(self::$businessWorker)) {
if (!isset(self::$businessWorker->gatewayConnections[$address])) {
return null;
}
}
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_GET_SESSION_BY_CLIENT_ID;
$gateway_data['connection_id'] = $address_data['connection_id'];
return self::sendAndRecv($address, $gateway_data);
}
/**
* 向某个用户网关发送命令和消息
*
* @param int $client_id
* @param int $cmd
* @param string $message
* @param string $ext_data
* @return boolean
*/
protected static function sendCmdAndMessageToClient($client_id, $cmd, $message, $ext_data = '')
{
// 如果是发给当前用户则直接获取上下文中的地址
if ($client_id === Context::$client_id || $client_id === null) {
$address = long2ip(Context::$local_ip) . ':' . Context::$local_port;
$connection_id = Context::$connection_id;
} else {
$address_data = Context::clientIdToAddress($client_id);
if (!$address_data) {
return false;
}
$address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}";
$connection_id = $address_data['connection_id'];
}
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = $cmd;
$gateway_data['connection_id'] = $connection_id;
$gateway_data['body'] = $message;
if (!empty($ext_data)) {
$gateway_data['ext_data'] = $ext_data;
}
return self::sendToGateway($address, $gateway_data);
}
/**
* 发送数据并返回
*
* @param int $address
* @param mixed $data
* @return bool
* @throws Exception
*/
protected static function sendAndRecv($address, $data)
{
$buffer = GatewayProtocol::encode($data);
$buffer = self::$secretKey ? self::generateAuthBuffer() . $buffer : $buffer;
$client = stream_socket_client("tcp://$address", $errno, $errmsg, self::$connectTimeout);
if (!$client) {
throw new Exception("can not connect to tcp://$address $errmsg");
}
if (strlen($buffer) === stream_socket_sendto($client, $buffer)) {
$timeout = 5;
// 阻塞读
stream_set_blocking($client, 1);
// 1秒超时
stream_set_timeout($client, 1);
$all_buffer = '';
$time_start = microtime(true);
$pack_len = 0;
while (1) {
$buf = stream_socket_recvfrom($client, 655350);
if ($buf !== '' && $buf !== false) {
$all_buffer .= $buf;
} else {
if (feof($client)) {
throw new Exception("connection close tcp://$address");
} elseif (microtime(true) - $time_start > $timeout) {
break;
}
continue;
}
$recv_len = strlen($all_buffer);
if (!$pack_len && $recv_len >= 4) {
$pack_len= current(unpack('N', $all_buffer));
}
// 回复的数据都是以\n结尾
if (($pack_len && $recv_len >= $pack_len + 4) || microtime(true) - $time_start > $timeout) {
break;
}
}
// 返回结果
return unserialize(substr($all_buffer, 4));
} else {
throw new Exception("sendAndRecv($address, \$bufer) fail ! Can not send data!", 502);
}
}
/**
* 发送数据到网关
*
* @param string $address
* @param array $gateway_data
* @return bool
*/
protected static function sendToGateway($address, $gateway_data)
{
return self::sendBufferToGateway($address, GatewayProtocol::encode($gateway_data));
}
/**
* 发送buffer数据到网关
* @param string $address
* @param string $gateway_buffer
* @return bool
*/
protected static function sendBufferToGateway($address, $gateway_buffer)
{
// 有$businessWorker说明是workerman环境使用$businessWorker发送数据
if (self::$businessWorker) {
if (!isset(self::$businessWorker->gatewayConnections[$address])) {
return false;
}
return self::$businessWorker->gatewayConnections[$address]->send($gateway_buffer, true);
}
// 非workerman环境
$gateway_buffer = self::$secretKey ? self::generateAuthBuffer() . $gateway_buffer : $gateway_buffer;
$flag = self::$persistentConnection ? STREAM_CLIENT_PERSISTENT | STREAM_CLIENT_CONNECT : STREAM_CLIENT_CONNECT;
$client = stream_socket_client("tcp://$address", $errno, $errmsg, self::$connectTimeout, $flag);
return strlen($gateway_buffer) == stream_socket_sendto($client, $gateway_buffer);
}
/**
* 向所有 gateway 发送数据
*
* @param string $gateway_data
* @throws Exception
*/
protected static function sendToAllGateway($gateway_data)
{
$buffer = GatewayProtocol::encode($gateway_data);
// 如果有businessWorker实例说明运行在workerman环境中通过businessWorker中的长连接发送数据
if (self::$businessWorker) {
foreach (self::$businessWorker->gatewayConnections as $gateway_connection) {
/** @var TcpConnection $gateway_connection */
$gateway_connection->send($buffer, true);
}
} // 运行在其它环境中通过注册中心得到gateway地址
else {
$all_addresses = self::getAllGatewayAddressesFromRegister();
if (!$all_addresses) {
throw new Exception('Gateway::getAllGatewayAddressesFromRegister() with registerAddress:' .
self::$registerAddress . ' return ' . var_export($all_addresses, true));
}
foreach ($all_addresses as $address) {
self::sendBufferToGateway($address, $buffer);
}
}
}
/**
* 踢掉某个网关的 socket
*
* @param string $address
* @param int $connection_id
* @return bool
*/
protected static function kickAddress($address, $connection_id, $message)
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_KICK;
$gateway_data['connection_id'] = $connection_id;
$gateway_data['body'] = $message;
return self::sendToGateway($address, $gateway_data);
}
/**
* 销毁某个网关的 socket
*
* @param string $address
* @param int $connection_id
* @return bool
*/
protected static function destroyAddress($address, $connection_id)
{
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_DESTROY;
$gateway_data['connection_id'] = $connection_id;
return self::sendToGateway($address, $gateway_data);
}
/**
* 将clientid数组转换成address数组
*
* @param array $client_id_array
* @return array
*/
protected static function clientIdArrayToAddressArray(array $client_id_array)
{
$address_connection_array = array();
foreach ($client_id_array as $client_id) {
$address_data = Context::clientIdToAddress($client_id);
if ($address_data) {
$address = long2ip($address_data['local_ip']) .
":{$address_data['local_port']}";
$address_connection_array[$address][$address_data['connection_id']] = $address_data['connection_id'];
}
}
return $address_connection_array;
}
/**
* 设置 gateway 实例
*
* @param \GatewayWorker\BusinessWorker $business_worker_instance
*/
public static function setBusinessWorker($business_worker_instance)
{
self::$businessWorker = $business_worker_instance;
}
/**
* 获取通过注册中心获取所有 gateway 通讯地址
*
* @return array
* @throws Exception
*/
protected static function getAllGatewayAddressesFromRegister()
{
static $addresses_cache, $last_update;
$time_now = time();
$expiration_time = 1;
if(empty($addresses_cache) || $time_now - $last_update > $expiration_time) {
$client = stream_socket_client('tcp://' . self::$registerAddress, $errno, $errmsg, self::$connectTimeout);
if (!$client) {
throw new Exception('Can not connect to tcp://' . self::$registerAddress . ' ' . $errmsg);
}
fwrite($client, '{"event":"worker_connect","secret_key":"' . self::$secretKey . '"}' . "\n");
stream_set_timeout($client, 5);
$ret = fgets($client, 655350);
if (!$ret || !$data = json_decode(trim($ret), true)) {
throw new Exception('getAllGatewayAddressesFromRegister fail. tcp://' .
self::$registerAddress . ' return ' . var_export($ret, true));
}
$last_update = $time_now;
$addresses_cache = $data['addresses'];
}
return $addresses_cache;
}
}
if (!class_exists('\Protocols\GatewayProtocol')) {
class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol');
}

View File

@ -0,0 +1,204 @@
<?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 GatewayWorker\Protocols;
/**
* Gateway Worker 间通讯的二进制协议
*
* struct GatewayProtocol
* {
* unsigned int pack_len,
* unsigned char cmd,//命令字
* unsigned int local_ip,
* unsigned short local_port,
* unsigned int client_ip,
* unsigned short client_port,
* unsigned int connection_id,
* unsigned char flag,
* unsigned short gateway_port,
* unsigned int ext_len,
* char[ext_len] ext_data,
* char[pack_length-HEAD_LEN] body//包体
* }
* NCNnNnNCnN
*/
class GatewayProtocol
{
// 发给workergateway有一个新的连接
const CMD_ON_CONNECTION = 1;
// 发给worker的客户端有消息
const CMD_ON_MESSAGE = 3;
// 发给worker上的关闭链接事件
const CMD_ON_CLOSE = 4;
// 发给gateway的向单个用户发送数据
const CMD_SEND_TO_ONE = 5;
// 发给gateway的向所有用户发送数据
const CMD_SEND_TO_ALL = 6;
// 发给gateway的踢出用户
// 1、如果有待发消息将在发送完后立即销毁用户连接
// 2、如果无待发消息将立即销毁用户连接
const CMD_KICK = 7;
// 发给gateway的立即销毁用户连接
const CMD_DESTROY = 8;
// 发给gateway通知用户session更新
const CMD_UPDATE_SESSION = 9;
// 获取在线状态
const CMD_GET_ALL_CLIENT_INFO = 10;
// 判断是否在线
const CMD_IS_ONLINE = 11;
// client_id绑定到uid
const CMD_BIND_UID = 12;
// 解绑
const CMD_UNBIND_UID = 13;
// 向uid发送数据
const CMD_SEND_TO_UID = 14;
// 根据uid获取绑定的clientid
const CMD_GET_CLIENT_ID_BY_UID = 15;
// 加入组
const CMD_JOIN_GROUP = 20;
// 离开组
const CMD_LEAVE_GROUP = 21;
// 向组成员发消息
const CMD_SEND_TO_GROUP = 22;
// 获取组成员
const CMD_GET_CLINET_INFO_BY_GROUP = 23;
// 获取组成员数
const CMD_GET_CLIENT_COUNT_BY_GROUP = 24;
// worker连接gateway事件
const CMD_WORKER_CONNECT = 200;
// 心跳
const CMD_PING = 201;
// GatewayClient连接gateway事件
const CMD_GATEWAY_CLIENT_CONNECT = 202;
// 根据client_id获取session
const CMD_GET_SESSION_BY_CLIENT_ID = 203;
// 发给gateway覆盖session
const CMD_SET_SESSION = 204;
// 包体是标量
const FLAG_BODY_IS_SCALAR = 0x01;
// 通知gateway在send时不调用协议encode方法在广播组播时提升性能
const FLAG_NOT_CALL_ENCODE = 0x02;
/**
* 包头长度
*
* @var int
*/
const HEAD_LEN = 28;
public static $empty = array(
'cmd' => 0,
'local_ip' => 0,
'local_port' => 0,
'client_ip' => 0,
'client_port' => 0,
'connection_id' => 0,
'flag' => 0,
'gateway_port' => 0,
'ext_data' => '',
'body' => '',
);
/**
* 返回包长度
*
* @param string $buffer
* @return int return current package length
*/
public static function input($buffer)
{
if (strlen($buffer) < self::HEAD_LEN) {
return 0;
}
$data = unpack("Npack_len", $buffer);
return $data['pack_len'];
}
/**
* 获取整个包的 buffer
*
* @param mixed $data
* @return string
*/
public static function encode($data)
{
$flag = (int)is_scalar($data['body']);
if (!$flag) {
$data['body'] = serialize($data['body']);
}
$data['flag'] |= $flag;
$ext_len = strlen($data['ext_data']);
$package_len = self::HEAD_LEN + $ext_len + strlen($data['body']);
return pack("NCNnNnNCnN", $package_len,
$data['cmd'], $data['local_ip'],
$data['local_port'], $data['client_ip'],
$data['client_port'], $data['connection_id'],
$data['flag'], $data['gateway_port'],
$ext_len) . $data['ext_data'] . $data['body'];
}
/**
* 从二进制数据转换为数组
*
* @param string $buffer
* @return array
*/
public static function decode($buffer)
{
$data = unpack("Npack_len/Ccmd/Nlocal_ip/nlocal_port/Nclient_ip/nclient_port/Nconnection_id/Cflag/ngateway_port/Next_len",
$buffer);
if ($data['ext_len'] > 0) {
$data['ext_data'] = substr($buffer, self::HEAD_LEN, $data['ext_len']);
if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) {
$data['body'] = substr($buffer, self::HEAD_LEN + $data['ext_len']);
} else {
$data['body'] = unserialize(substr($buffer, self::HEAD_LEN + $data['ext_len']));
}
} else {
$data['ext_data'] = '';
if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) {
$data['body'] = substr($buffer, self::HEAD_LEN);
} else {
$data['body'] = unserialize(substr($buffer, self::HEAD_LEN));
}
}
return $data;
}
}

View File

@ -0,0 +1,190 @@
<?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 GatewayWorker;
use Workerman\Worker;
use Workerman\Lib\Timer;
/**
*
* 注册中心,用于注册 Gateway BusinessWorker
*
* @author walkor<walkor@workerman.net>
*
*/
class Register extends Worker
{
/**
* {@inheritdoc}
*/
public $name = 'Register';
/**
* {@inheritdoc}
*/
public $reloadable = false;
/**
* 秘钥
* @var string
*/
public $secretKey = '';
/**
* 所有 gateway 的连接
*
* @var array
*/
protected $_gatewayConnections = array();
/**
* 所有 worker 的连接
*
* @var array
*/
protected $_workerConnections = array();
/**
* 进程启动时间
*
* @var int
*/
protected $_startTime = 0;
/**
* {@inheritdoc}
*/
public function run()
{
// 设置 onMessage 连接回调
$this->onConnect = array($this, 'onConnect');
// 设置 onMessage 回调
$this->onMessage = array($this, 'onMessage');
// 设置 onClose 回调
$this->onClose = array($this, 'onClose');
// 记录进程启动的时间
$this->_startTime = time();
// 强制使用text协议
$this->protocol = '\Workerman\Protocols\Text';
// 运行父方法
parent::run();
}
/**
* 设置个定时器,将未及时发送验证的连接关闭
*
* @param \Workerman\Connection\ConnectionInterface $connection
* @return void
*/
public function onConnect($connection)
{
$connection->timeout_timerid = Timer::add(10, function () use ($connection) {
Worker::log("Register auth timeout (".$connection->getRemoteIp()."). See http://wiki.workerman.net/Error4 for detail");
$connection->close();
}, null, false);
}
/**
* 设置消息回调
*
* @param \Workerman\Connection\ConnectionInterface $connection
* @param string $buffer
* @return void
*/
public function onMessage($connection, $buffer)
{
// 删除定时器
Timer::del($connection->timeout_timerid);
$data = @json_decode($buffer, true);
if (empty($data['event'])) {
$error = "Bad request for Register service. Request info(IP:".$connection->getRemoteIp().", Request Buffer:$buffer). See http://wiki.workerman.net/Error4 for detail";
Worker::log($error);
return $connection->close($error);
}
$event = $data['event'];
$secret_key = isset($data['secret_key']) ? $data['secret_key'] : '';
// 开始验证
switch ($event) {
// 是 gateway 连接
case 'gateway_connect':
if (empty($data['address'])) {
echo "address not found\n";
return $connection->close();
}
if ($secret_key !== $this->secretKey) {
Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
return $connection->close();
}
$this->_gatewayConnections[$connection->id] = $data['address'];
$this->broadcastAddresses();
break;
// 是 worker 连接
case 'worker_connect':
if ($secret_key !== $this->secretKey) {
Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
return $connection->close();
}
$this->_workerConnections[$connection->id] = $connection;
$this->broadcastAddresses($connection);
break;
case 'ping':
break;
default:
Worker::log("Register unknown event:$event IP: ".$connection->getRemoteIp()." Buffer:$buffer. See http://wiki.workerman.net/Error4 for detail");
$connection->close();
}
}
/**
* 连接关闭时
*
* @param \Workerman\Connection\ConnectionInterface $connection
*/
public function onClose($connection)
{
if (isset($this->_gatewayConnections[$connection->id])) {
unset($this->_gatewayConnections[$connection->id]);
$this->broadcastAddresses();
}
if (isset($this->_workerConnections[$connection->id])) {
unset($this->_workerConnections[$connection->id]);
}
}
/**
* BusinessWorker 广播 gateway 内部通讯地址
*
* @param \Workerman\Connection\ConnectionInterface $connection
*/
public function broadcastAddresses($connection = null)
{
$data = array(
'event' => 'broadcast_addresses',
'addresses' => array_unique(array_values($this->_gatewayConnections)),
);
$buffer = json_encode($data);
if ($connection) {
$connection->send($buffer);
return;
}
foreach ($this->_workerConnections as $con) {
$con->send($buffer);
}
}
}

View File

@ -0,0 +1,4 @@
.buildpath
.project
.settings
.idea

View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2009-2015 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/workerman/contributors)
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,38 @@
GatewayWorker
=================
GatewayWorker基于[Workerman](https://github.com/walkor/Workerman)开发的一个项目框架用于快速开发长连接应用例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等。
GatewayWorker使用经典的Gateway和Worker进程模型。Gateway进程负责维持客户端连接并转发客户端的数据给Worker进程处理Worker进程负责处理实际的业务逻辑并将结果推送给对应的客户端。Gateway服务和Worker服务可以分开部署在不同的服务器上实现分布式集群。
GatewayWorker提供非常方便的API可以全局广播数据、可以向某个群体广播数据、也可以向某个特定客户端推送数据。配合Workerman的定时器也可以定时推送数据。
快速开始
======
开发者可以从一个简单的demo开始(demo中包含了GatewayWorker内核以及start_gateway.php start_business.php等启动入口文件)<br>
[点击这里下载demo](http://www.workerman.net/download/GatewayWorker.zip)。<br>
demo说明见源码readme。
手册
=======
http://www.workerman.net/gatewaydoc/
安装内核
=======
只安装GatewayWorker内核文件不包含start_gateway.php start_businessworker.php等启动入口文件
```
composer require workerman/gateway-worker
```
使用GatewayWorker开发的项目
=======
## [tadpole](http://kedou.workerman.net/)
[Live demo](http://kedou.workerman.net/)
[Source code](https://github.com/walkor/workerman)
![workerman todpole](http://www.workerman.net/img/workerman-todpole.png)
## [chat room](http://chat.workerman.net/)
[Live demo](http://chat.workerman.net/)
[Source code](https://github.com/walkor/workerman-chat)
![workerman-chat](http://www.workerman.net/img/workerman-chat.png)

View File

@ -0,0 +1,12 @@
{
"name" : "workerman/gateway-worker",
"keywords": ["distributed","communication"],
"homepage": "http://www.workerman.net",
"license" : "MIT",
"require": {
"workerman/workerman" : ">=3.1.8"
},
"autoload": {
"psr-4": {"GatewayWorker\\": "./src"}
}
}

View File

@ -0,0 +1,560 @@
<?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 GatewayWorker;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\AsyncTcpConnection;
use GatewayWorker\Protocols\GatewayProtocol;
use GatewayWorker\Lib\Context;
/**
*
* BusinessWorker 用于处理Gateway转发来的数据
*
* @author walkor<walkor@workerman.net>
*
*/
class BusinessWorker extends Worker
{
/**
* 保存与 gateway 的连接 connection 对象
*
* @var array
*/
public $gatewayConnections = array();
/**
* 注册中心地址
*
* @var string|array
*/
public $registerAddress = '127.0.0.1:1236';
/**
* 事件处理类,默认是 Event
*
* @var string
*/
public $eventHandler = 'Events';
/**
* 业务超时时间,可用来定位程序卡在哪里
*
* @var int
*/
public $processTimeout = 30;
/**
* 业务超时时间,可用来定位程序卡在哪里
*
* @var callable
*/
public $processTimeoutHandler = '\\Workerman\\Worker::log';
/**
* 秘钥
*
* @var string
*/
public $secretKey = '';
/**
* businessWorker进程将消息转发给gateway进程的发送缓冲区大小
*
* @var int
*/
public $sendToGatewayBufferSize = 10240000;
/**
* 保存用户设置的 worker 启动回调
*
* @var callback
*/
protected $_onWorkerStart = null;
/**
* 保存用户设置的 workerReload 回调
*
* @var callback
*/
protected $_onWorkerReload = null;
/**
* 保存用户设置的 workerStop 回调
*
* @var callback
*/
protected $_onWorkerStop= null;
/**
* 到注册中心的连接
*
* @var AsyncTcpConnection
*/
protected $_registerConnection = null;
/**
* 处于连接状态的 gateway 通讯地址
*
* @var array
*/
protected $_connectingGatewayAddresses = array();
/**
* 所有 geteway 内部通讯地址
*
* @var array
*/
protected $_gatewayAddresses = array();
/**
* 等待连接个 gateway 地址
*
* @var array
*/
protected $_waitingConnectGatewayAddresses = array();
/**
* Event::onConnect 回调
*
* @var callback
*/
protected $_eventOnConnect = null;
/**
* Event::onMessage 回调
*
* @var callback
*/
protected $_eventOnMessage = null;
/**
* Event::onClose 回调
*
* @var callback
*/
protected $_eventOnClose = null;
/**
* websocket回调
*
* @var null
*/
protected $_eventOnWebSocketConnect = null;
/**
* SESSION 版本缓存
*
* @var array
*/
protected $_sessionVersion = array();
/**
* 用于保持长连接的心跳时间间隔
*
* @var int
*/
const PERSISTENCE_CONNECTION_PING_INTERVAL = 25;
/**
* 构造函数
*
* @param string $socket_name
* @param array $context_option
*/
public function __construct($socket_name = '', $context_option = array())
{
parent::__construct($socket_name, $context_option);
$backrace = debug_backtrace();
$this->_autoloadRootPath = dirname($backrace[0]['file']);
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->_onWorkerStart = $this->onWorkerStart;
$this->_onWorkerReload = $this->onWorkerReload;
$this->_onWorkerStop = $this->onWorkerStop;
$this->onWorkerStop = array($this, 'onWorkerStop');
$this->onWorkerStart = array($this, 'onWorkerStart');
$this->onWorkerReload = array($this, 'onWorkerReload');
parent::run();
}
/**
* 当进程启动时一些初始化工作
*
* @return void
*/
protected function onWorkerStart()
{
if (!class_exists('\Protocols\GatewayProtocol')) {
class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol');
}
if (!is_array($this->registerAddress)) {
$this->registerAddress = array($this->registerAddress);
}
$this->connectToRegister();
\GatewayWorker\Lib\Gateway::setBusinessWorker($this);
\GatewayWorker\Lib\Gateway::$secretKey = $this->secretKey;
if ($this->_onWorkerStart) {
call_user_func($this->_onWorkerStart, $this);
}
if (is_callable($this->eventHandler . '::onWorkerStart')) {
call_user_func($this->eventHandler . '::onWorkerStart', $this);
}
if (function_exists('pcntl_signal')) {
// 业务超时信号处理
pcntl_signal(SIGALRM, array($this, 'timeoutHandler'), false);
} else {
$this->processTimeout = 0;
}
// 设置回调
if (is_callable($this->eventHandler . '::onConnect')) {
$this->_eventOnConnect = $this->eventHandler . '::onConnect';
}
if (is_callable($this->eventHandler . '::onMessage')) {
$this->_eventOnMessage = $this->eventHandler . '::onMessage';
} else {
echo "Waring: {$this->eventHandler}::onMessage is not callable\n";
}
if (is_callable($this->eventHandler . '::onClose')) {
$this->_eventOnClose = $this->eventHandler . '::onClose';
}
if (is_callable($this->eventHandler . '::onWebSocketConnect')) {
$this->_eventOnWebSocketConnect = $this->eventHandler . '::onWebSocketConnect';
}
}
/**
* onWorkerReload 回调
*
* @param Worker $worker
*/
protected function onWorkerReload($worker)
{
// 防止进程立刻退出
$worker->reloadable = false;
// 延迟 0.05 秒退出,避免 BusinessWorker 瞬间全部退出导致没有可用的 BusinessWorker 进程
Timer::add(0.05, array('Workerman\Worker', 'stopAll'));
// 执行用户定义的 onWorkerReload 回调
if ($this->_onWorkerReload) {
call_user_func($this->_onWorkerReload, $this);
}
}
/**
* 当进程关闭时一些清理工作
*
* @return void
*/
protected function onWorkerStop()
{
if ($this->_onWorkerStop) {
call_user_func($this->_onWorkerStop, $this);
}
if (is_callable($this->eventHandler . '::onWorkerStop')) {
call_user_func($this->eventHandler . '::onWorkerStop', $this);
}
}
/**
* 连接服务注册中心
*
* @return void
*/
public function connectToRegister()
{
foreach ($this->registerAddress as $register_address) {
$register_connection = new AsyncTcpConnection("text://{$register_address}");
$secret_key = $this->secretKey;
$register_connection->onConnect = function () use ($register_connection, $secret_key, $register_address) {
$register_connection->send('{"event":"worker_connect","secret_key":"' . $secret_key . '"}');
// 如果Register服务器不在本地服务器则需要保持心跳
if (strpos($register_address, '127.0.0.1') !== 0) {
$register_connection->ping_timer = Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, function () use ($register_connection) {
$register_connection->send('{"event":"ping"}');
});
}
};
$register_connection->onClose = function ($register_connection) {
if(!empty($register_connection->ping_timer)) {
Timer::del($register_connection->ping_timer);
}
$register_connection->reconnect(1);
};
$register_connection->onMessage = array($this, 'onRegisterConnectionMessage');
$register_connection->connect();
}
}
/**
* 当注册中心发来消息时
*
* @return void
*/
public function onRegisterConnectionMessage($register_connection, $data)
{
$data = json_decode($data, true);
if (!isset($data['event'])) {
echo "Received bad data from Register\n";
return;
}
$event = $data['event'];
switch ($event) {
case 'broadcast_addresses':
if (!is_array($data['addresses'])) {
echo "Received bad data from Register. Addresses empty\n";
return;
}
$addresses = $data['addresses'];
$this->_gatewayAddresses = array();
foreach ($addresses as $addr) {
$this->_gatewayAddresses[$addr] = $addr;
}
$this->checkGatewayConnections($addresses);
break;
default:
echo "Receive bad event:$event from Register.\n";
}
}
/**
* gateway 转发来数据时
*
* @param TcpConnection $connection
* @param mixed $data
*/
public function onGatewayMessage($connection, $data)
{
$cmd = $data['cmd'];
if ($cmd === GatewayProtocol::CMD_PING) {
return;
}
// 上下文数据
Context::$client_ip = $data['client_ip'];
Context::$client_port = $data['client_port'];
Context::$local_ip = $data['local_ip'];
Context::$local_port = $data['local_port'];
Context::$connection_id = $data['connection_id'];
Context::$client_id = Context::addressToClientId($data['local_ip'], $data['local_port'],
$data['connection_id']);
// $_SERVER 变量
$_SERVER = array(
'REMOTE_ADDR' => long2ip($data['client_ip']),
'REMOTE_PORT' => $data['client_port'],
'GATEWAY_ADDR' => long2ip($data['local_ip']),
'GATEWAY_PORT' => $data['gateway_port'],
'GATEWAY_CLIENT_ID' => Context::$client_id,
);
// 检查session版本如果是过期的session数据则拉取最新的数据
if ($cmd !== GatewayProtocol::CMD_ON_CLOSE && isset($this->_sessionVersion[Context::$client_id]) && $this->_sessionVersion[Context::$client_id] !== crc32($data['ext_data'])) {
$_SESSION = Context::$old_session = \GatewayWorker\Lib\Gateway::getSession(Context::$client_id);
} else {
if (!isset($this->_sessionVersion[Context::$client_id])) {
$this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']);
}
// 尝试解析 session
if ($data['ext_data'] != '') {
Context::$old_session = $_SESSION = Context::sessionDecode($data['ext_data']);
} else {
Context::$old_session = $_SESSION = null;
}
}
if ($this->processTimeout) {
pcntl_alarm($this->processTimeout);
}
// 尝试执行 Event::onConnection、Event::onMessage、Event::onClose
switch ($cmd) {
case GatewayProtocol::CMD_ON_CONNECT:
if ($this->_eventOnConnect) {
call_user_func($this->_eventOnConnect, Context::$client_id);
}
break;
case GatewayProtocol::CMD_ON_MESSAGE:
if ($this->_eventOnMessage) {
call_user_func($this->_eventOnMessage, Context::$client_id, $data['body']);
}
break;
case GatewayProtocol::CMD_ON_CLOSE:
unset($this->_sessionVersion[Context::$client_id]);
if ($this->_eventOnClose) {
call_user_func($this->_eventOnClose, Context::$client_id);
}
break;
case GatewayProtocol::CMD_ON_WEBSOCKET_CONNECT:
if ($this->_eventOnWebSocketConnect) {
call_user_func($this->_eventOnWebSocketConnect, Context::$client_id, $data['body']);
}
break;
}
if ($this->processTimeout) {
pcntl_alarm(0);
}
// session 必须是数组
if ($_SESSION !== null && !is_array($_SESSION)) {
throw new \Exception('$_SESSION must be an array. But $_SESSION=' . var_export($_SESSION, true) . ' is not array.');
}
// 判断 session 是否被更改
if ($_SESSION !== Context::$old_session && $cmd !== GatewayProtocol::CMD_ON_CLOSE) {
$session_str_now = $_SESSION !== null ? Context::sessionEncode($_SESSION) : '';
\GatewayWorker\Lib\Gateway::setSocketSession(Context::$client_id, $session_str_now);
$this->_sessionVersion[Context::$client_id] = crc32($session_str_now);
}
Context::clear();
}
/**
* 当与 Gateway 的连接断开时触发
*
* @param TcpConnection $connection
* @return void
*/
public function onGatewayClose($connection)
{
$addr = $connection->remoteAddress;
unset($this->gatewayConnections[$addr], $this->_connectingGatewayAddresses[$addr]);
if (isset($this->_gatewayAddresses[$addr]) && !isset($this->_waitingConnectGatewayAddresses[$addr])) {
Timer::add(1, array($this, 'tryToConnectGateway'), array($addr), false);
$this->_waitingConnectGatewayAddresses[$addr] = $addr;
}
}
/**
* 尝试连接 Gateway 内部通讯地址
*
* @param string $addr
*/
public function tryToConnectGateway($addr)
{
if (!isset($this->gatewayConnections[$addr]) && !isset($this->_connectingGatewayAddresses[$addr]) && isset($this->_gatewayAddresses[$addr])) {
$gateway_connection = new AsyncTcpConnection("GatewayProtocol://$addr");
$gateway_connection->remoteAddress = $addr;
$gateway_connection->onConnect = array($this, 'onConnectGateway');
$gateway_connection->onMessage = array($this, 'onGatewayMessage');
$gateway_connection->onClose = array($this, 'onGatewayClose');
$gateway_connection->onError = array($this, 'onGatewayError');
$gateway_connection->maxSendBufferSize = $this->sendToGatewayBufferSize;
if (TcpConnection::$defaultMaxSendBufferSize == $gateway_connection->maxSendBufferSize) {
$gateway_connection->maxSendBufferSize = 50 * 1024 * 1024;
}
$gateway_data = GatewayProtocol::$empty;
$gateway_data['cmd'] = GatewayProtocol::CMD_WORKER_CONNECT;
$gateway_data['body'] = json_encode(array(
'worker_key' =>"{$this->name}:{$this->id}",
'secret_key' => $this->secretKey,
));
$gateway_connection->send($gateway_data);
$gateway_connection->connect();
$this->_connectingGatewayAddresses[$addr] = $addr;
}
unset($this->_waitingConnectGatewayAddresses[$addr]);
}
/**
* 检查 gateway 的通信端口是否都已经连
* 如果有未连接的端口,则尝试连接
*
* @param array $addresses_list
*/
public function checkGatewayConnections($addresses_list)
{
if (empty($addresses_list)) {
return;
}
foreach ($addresses_list as $addr) {
if (!isset($this->_waitingConnectGatewayAddresses[$addr])) {
$this->tryToConnectGateway($addr);
}
}
}
/**
* 当连接上 gateway 的通讯端口时触发
* 将连接 connection 对象保存起来
*
* @param TcpConnection $connection
* @return void
*/
public function onConnectGateway($connection)
{
$this->gatewayConnections[$connection->remoteAddress] = $connection;
unset($this->_connectingGatewayAddresses[$connection->remoteAddress], $this->_waitingConnectGatewayAddresses[$connection->remoteAddress]);
}
/**
* 当与 gateway 的连接出现错误时触发
*
* @param TcpConnection $connection
* @param int $error_no
* @param string $error_msg
*/
public function onGatewayError($connection, $error_no, $error_msg)
{
echo "GatewayConnection Error : $error_no ,$error_msg\n";
}
/**
* 获取所有 Gateway 内部通讯地址
*
* @return array
*/
public function getAllGatewayAddresses()
{
return $this->_gatewayAddresses;
}
/**
* 业务超时回调
*
* @param int $signal
* @throws \Exception
*/
public function timeoutHandler($signal)
{
switch ($signal) {
// 超时时钟
case SIGALRM:
// 超时异常
$e = new \Exception("process_timeout", 506);
$trace_str = $e->getTraceAsString();
// 去掉第一行timeoutHandler的调用栈
$trace_str = $e->getMessage() . ":\n" . substr($trace_str, strpos($trace_str, "\n") + 1) . "\n";
// 开发者没有设置超时处理函数,或者超时处理函数返回空则执行退出
if (!$this->processTimeoutHandler || !call_user_func($this->processTimeoutHandler, $trace_str, $e)) {
Worker::stopAll();
}
break;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,136 @@
<?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 GatewayWorker\Lib;
use Exception;
/**
* 上下文 包含当前用户 uid 内部通信 local_ip local_port socket_id以及客户端 client_ip client_port
*/
class Context
{
/**
* 内部通讯 id
*
* @var string
*/
public static $local_ip;
/**
* 内部通讯端口
*
* @var int
*/
public static $local_port;
/**
* 客户端 ip
*
* @var string
*/
public static $client_ip;
/**
* 客户端端口
*
* @var int
*/
public static $client_port;
/**
* client_id
*
* @var string
*/
public static $client_id;
/**
* 连接 connection->id
*
* @var int
*/
public static $connection_id;
/**
* 旧的session
*
* @var string
*/
public static $old_session;
/**
* 编码 session
*
* @param mixed $session_data
* @return string
*/
public static function sessionEncode($session_data = '')
{
if ($session_data !== '') {
return serialize($session_data);
}
return '';
}
/**
* 解码 session
*
* @param string $session_buffer
* @return mixed
*/
public static function sessionDecode($session_buffer)
{
return unserialize($session_buffer);
}
/**
* 清除上下文
*
* @return void
*/
public static function clear()
{
self::$local_ip = self::$local_port = self::$client_ip = self::$client_port =
self::$client_id = self::$connection_id = self::$old_session = null;
}
/**
* 通讯地址到 client_id 的转换
*
* @param int $local_ip
* @param int $local_port
* @param int $connection_id
* @return string
*/
public static function addressToClientId($local_ip, $local_port, $connection_id)
{
return bin2hex(pack('NnN', $local_ip, $local_port, $connection_id));
}
/**
* client_id 到通讯地址的转换
*
* @param string $client_id
* @return array
* @throws Exception
*/
public static function clientIdToAddress($client_id)
{
if (strlen($client_id) !== 20) {
echo new Exception("client_id $client_id is invalid");
return false;
}
return unpack('Nlocal_ip/nlocal_port/Nconnection_id', pack('H*', $client_id));
}
}

View File

@ -0,0 +1,76 @@
<?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 GatewayWorker\Lib;
use Config\Db as DbConfig;
use Exception;
/**
* 数据库类
*/
class Db
{
/**
* 实例数组
*
* @var array
*/
protected static $instance = array();
/**
* 获取实例
*
* @param string $config_name
* @return DbConnection
* @throws Exception
*/
public static function instance($config_name)
{
if (!isset(DbConfig::$$config_name)) {
echo "\\Config\\Db::$config_name not set\n";
throw new Exception("\\Config\\Db::$config_name not set\n");
}
if (empty(self::$instance[$config_name])) {
$config = DbConfig::$$config_name;
self::$instance[$config_name] = new DbConnection($config['host'], $config['port'],
$config['user'], $config['password'], $config['dbname'],$config['charset']);
}
return self::$instance[$config_name];
}
/**
* 关闭数据库实例
*
* @param string $config_name
*/
public static function close($config_name)
{
if (isset(self::$instance[$config_name])) {
self::$instance[$config_name]->closeConnection();
self::$instance[$config_name] = null;
}
}
/**
* 关闭所有数据库实例
*/
public static function closeAll()
{
foreach (self::$instance as $connection) {
$connection->closeConnection();
}
self::$instance = array();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,216 @@
<?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 GatewayWorker\Protocols;
/**
* Gateway Worker 间通讯的二进制协议
*
* struct GatewayProtocol
* {
* unsigned int pack_len,
* unsigned char cmd,//命令字
* unsigned int local_ip,
* unsigned short local_port,
* unsigned int client_ip,
* unsigned short client_port,
* unsigned int connection_id,
* unsigned char flag,
* unsigned short gateway_port,
* unsigned int ext_len,
* char[ext_len] ext_data,
* char[pack_length-HEAD_LEN] body//包体
* }
* NCNnNnNCnN
*/
class GatewayProtocol
{
// 发给workergateway有一个新的连接
const CMD_ON_CONNECT = 1;
// 发给worker的客户端有消息
const CMD_ON_MESSAGE = 3;
// 发给worker上的关闭链接事件
const CMD_ON_CLOSE = 4;
// 发给gateway的向单个用户发送数据
const CMD_SEND_TO_ONE = 5;
// 发给gateway的向所有用户发送数据
const CMD_SEND_TO_ALL = 6;
// 发给gateway的踢出用户
// 1、如果有待发消息将在发送完后立即销毁用户连接
// 2、如果无待发消息将立即销毁用户连接
const CMD_KICK = 7;
// 发给gateway的立即销毁用户连接
const CMD_DESTROY = 8;
// 发给gateway通知用户session更新
const CMD_UPDATE_SESSION = 9;
// 获取在线状态
const CMD_GET_ALL_CLIENT_SESSIONS = 10;
// 判断是否在线
const CMD_IS_ONLINE = 11;
// client_id绑定到uid
const CMD_BIND_UID = 12;
// 解绑
const CMD_UNBIND_UID = 13;
// 向uid发送数据
const CMD_SEND_TO_UID = 14;
// 根据uid获取绑定的clientid
const CMD_GET_CLIENT_ID_BY_UID = 15;
// 加入组
const CMD_JOIN_GROUP = 20;
// 离开组
const CMD_LEAVE_GROUP = 21;
// 向组成员发消息
const CMD_SEND_TO_GROUP = 22;
// 获取组成员
const CMD_GET_CLIENT_SESSIONS_BY_GROUP = 23;
// 获取组在线连接数
const CMD_GET_CLIENT_COUNT_BY_GROUP = 24;
// 按照条件查找
const CMD_SELECT = 25;
// 获取在线的群组ID
const CMD_GET_GROUP_ID_LIST = 26;
// 取消分组
const CMD_UNGROUP = 27;
// worker连接gateway事件
const CMD_WORKER_CONNECT = 200;
// 心跳
const CMD_PING = 201;
// GatewayClient连接gateway事件
const CMD_GATEWAY_CLIENT_CONNECT = 202;
// 根据client_id获取session
const CMD_GET_SESSION_BY_CLIENT_ID = 203;
// 发给gateway覆盖session
const CMD_SET_SESSION = 204;
// 当websocket握手时触发只有websocket协议支持此命令字
const CMD_ON_WEBSOCKET_CONNECT = 205;
// 包体是标量
const FLAG_BODY_IS_SCALAR = 0x01;
// 通知gateway在send时不调用协议encode方法在广播组播时提升性能
const FLAG_NOT_CALL_ENCODE = 0x02;
/**
* 包头长度
*
* @var int
*/
const HEAD_LEN = 28;
public static $empty = array(
'cmd' => 0,
'local_ip' => 0,
'local_port' => 0,
'client_ip' => 0,
'client_port' => 0,
'connection_id' => 0,
'flag' => 0,
'gateway_port' => 0,
'ext_data' => '',
'body' => '',
);
/**
* 返回包长度
*
* @param string $buffer
* @return int return current package length
*/
public static function input($buffer)
{
if (strlen($buffer) < self::HEAD_LEN) {
return 0;
}
$data = unpack("Npack_len", $buffer);
return $data['pack_len'];
}
/**
* 获取整个包的 buffer
*
* @param mixed $data
* @return string
*/
public static function encode($data)
{
$flag = (int)is_scalar($data['body']);
if (!$flag) {
$data['body'] = serialize($data['body']);
}
$data['flag'] |= $flag;
$ext_len = strlen($data['ext_data']);
$package_len = self::HEAD_LEN + $ext_len + strlen($data['body']);
return pack("NCNnNnNCnN", $package_len,
$data['cmd'], $data['local_ip'],
$data['local_port'], $data['client_ip'],
$data['client_port'], $data['connection_id'],
$data['flag'], $data['gateway_port'],
$ext_len) . $data['ext_data'] . $data['body'];
}
/**
* 从二进制数据转换为数组
*
* @param string $buffer
* @return array
*/
public static function decode($buffer)
{
$data = unpack("Npack_len/Ccmd/Nlocal_ip/nlocal_port/Nclient_ip/nclient_port/Nconnection_id/Cflag/ngateway_port/Next_len",
$buffer);
if ($data['ext_len'] > 0) {
$data['ext_data'] = substr($buffer, self::HEAD_LEN, $data['ext_len']);
if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) {
$data['body'] = substr($buffer, self::HEAD_LEN + $data['ext_len']);
} else {
$data['body'] = unserialize(substr($buffer, self::HEAD_LEN + $data['ext_len']));
}
} else {
$data['ext_data'] = '';
if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) {
$data['body'] = substr($buffer, self::HEAD_LEN);
} else {
$data['body'] = unserialize(substr($buffer, self::HEAD_LEN));
}
}
return $data;
}
}

View File

@ -0,0 +1,190 @@
<?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 GatewayWorker;
use Workerman\Worker;
use Workerman\Lib\Timer;
/**
*
* 注册中心,用于注册 Gateway BusinessWorker
*
* @author walkor<walkor@workerman.net>
*
*/
class Register extends Worker
{
/**
* {@inheritdoc}
*/
public $name = 'Register';
/**
* {@inheritdoc}
*/
public $reloadable = false;
/**
* 秘钥
* @var string
*/
public $secretKey = '';
/**
* 所有 gateway 的连接
*
* @var array
*/
protected $_gatewayConnections = array();
/**
* 所有 worker 的连接
*
* @var array
*/
protected $_workerConnections = array();
/**
* 进程启动时间
*
* @var int
*/
protected $_startTime = 0;
/**
* {@inheritdoc}
*/
public function run()
{
// 设置 onMessage 连接回调
$this->onConnect = array($this, 'onConnect');
// 设置 onMessage 回调
$this->onMessage = array($this, 'onMessage');
// 设置 onClose 回调
$this->onClose = array($this, 'onClose');
// 记录进程启动的时间
$this->_startTime = time();
// 强制使用text协议
$this->protocol = '\Workerman\Protocols\Text';
// 运行父方法
parent::run();
}
/**
* 设置个定时器,将未及时发送验证的连接关闭
*
* @param \Workerman\Connection\ConnectionInterface $connection
* @return void
*/
public function onConnect($connection)
{
$connection->timeout_timerid = Timer::add(10, function () use ($connection) {
Worker::log("Register auth timeout (".$connection->getRemoteIp()."). See http://wiki.workerman.net/Error4 for detail");
$connection->close();
}, null, false);
}
/**
* 设置消息回调
*
* @param \Workerman\Connection\ConnectionInterface $connection
* @param string $buffer
* @return void
*/
public function onMessage($connection, $buffer)
{
// 删除定时器
Timer::del($connection->timeout_timerid);
$data = @json_decode($buffer, true);
if (empty($data['event'])) {
$error = "Bad request for Register service. Request info(IP:".$connection->getRemoteIp().", Request Buffer:$buffer). See http://wiki.workerman.net/Error4 for detail";
Worker::log($error);
return $connection->close($error);
}
$event = $data['event'];
$secret_key = isset($data['secret_key']) ? $data['secret_key'] : '';
// 开始验证
switch ($event) {
// 是 gateway 连接
case 'gateway_connect':
if (empty($data['address'])) {
echo "address not found\n";
return $connection->close();
}
if ($secret_key !== $this->secretKey) {
Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
return $connection->close();
}
$this->_gatewayConnections[$connection->id] = $data['address'];
$this->broadcastAddresses();
break;
// 是 worker 连接
case 'worker_connect':
if ($secret_key !== $this->secretKey) {
Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
return $connection->close();
}
$this->_workerConnections[$connection->id] = $connection;
$this->broadcastAddresses($connection);
break;
case 'ping':
break;
default:
Worker::log("Register unknown event:$event IP: ".$connection->getRemoteIp()." Buffer:$buffer. See http://wiki.workerman.net/Error4 for detail");
$connection->close();
}
}
/**
* 连接关闭时
*
* @param \Workerman\Connection\ConnectionInterface $connection
*/
public function onClose($connection)
{
if (isset($this->_gatewayConnections[$connection->id])) {
unset($this->_gatewayConnections[$connection->id]);
$this->broadcastAddresses();
}
if (isset($this->_workerConnections[$connection->id])) {
unset($this->_workerConnections[$connection->id]);
}
}
/**
* BusinessWorker 广播 gateway 内部通讯地址
*
* @param \Workerman\Connection\ConnectionInterface $connection
*/
public function broadcastAddresses($connection = null)
{
$data = array(
'event' => 'broadcast_addresses',
'addresses' => array_unique(array_values($this->_gatewayConnections)),
);
$buffer = json_encode($data);
if ($connection) {
$connection->send($buffer);
return;
}
foreach ($this->_workerConnections as $con) {
$con->send($buffer);
}
}
}

View File

@ -0,0 +1,3 @@
.project
.buildpath
.settings/org.eclipse.php.core.prefs

View File

@ -0,0 +1,80 @@
<?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 Workerman;
// 包含常量定义文件
require_once __DIR__.'/Lib/Constants.php';
/**
* 自动加载类
* @author walkor<walkor@workerman.net>
*/
class Autoloader
{
// 应用的初始化目录,作为加载类文件的参考目录
protected static $_appInitPath = '';
/**
* 设置应用初始化目录
* @param string $root_path
* @return void
*/
public static function setRootPath($root_path)
{
self::$_appInitPath = $root_path;
}
/**
* 根据命名空间加载文件
* @param string $name
* @return boolean
*/
public static function loadByNamespace($name)
{
// 相对路径
$class_path = str_replace('\\', DIRECTORY_SEPARATOR ,$name);
// 如果是Workerman命名空间则在当前目录寻找类文件
if(strpos($name, 'Workerman\\') === 0)
{
$class_file = __DIR__.substr($class_path, strlen('Workerman')).'.php';
}
else
{
// 先尝试在应用目录寻找文件
if(self::$_appInitPath)
{
$class_file = self::$_appInitPath . DIRECTORY_SEPARATOR . $class_path.'.php';
}
// 文件不存在,则在上一层目录寻找
if(empty($class_file) || !is_file($class_file))
{
$class_file = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR . "$class_path.php";
}
}
// 找到文件
if(is_file($class_file))
{
// 加载
require_once($class_file);
if(class_exists($name, false))
{
return true;
}
}
return false;
}
}
// 设置类自动加载回调函数
spl_autoload_register('\Workerman\Autoloader::loadByNamespace');

View File

@ -0,0 +1,328 @@
<?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 Workerman\Connection;
use Workerman\Events\EventInterface;
use Workerman\Lib\Timer;
use Workerman\Worker;
use Exception;
/**
* AsyncTcpConnection.
*/
class AsyncTcpConnection extends TcpConnection
{
/**
* Emitted when socket connection is successfully established.
*
* @var callback
*/
public $onConnect = null;
/**
* Transport layer protocol.
*
* @var string
*/
public $transport = 'tcp';
/**
* Status.
*
* @var int
*/
protected $_status = self::STATUS_INITIAL;
/**
* Remote host.
*
* @var string
*/
protected $_remoteHost = '';
/**
* Connect start time.
*
* @var string
*/
protected $_connectStartTime = 0;
/**
* Remote URI.
*
* @var string
*/
protected $_remoteURI = '';
/**
* Context option.
*
* @var resource
*/
protected $_contextOption = null;
/**
* Reconnect timer.
*
* @var int
*/
protected $_reconnectTimer = null;
/**
* PHP built-in protocols.
*
* @var array
*/
protected static $_builtinTransports = array(
'tcp' => 'tcp',
'udp' => 'udp',
'unix' => 'unix',
'ssl' => 'ssl',
'sslv2' => 'sslv2',
'sslv3' => 'sslv3',
'tls' => 'tls'
);
/**
* Construct.
*
* @param string $remote_address
* @param array $context_option
* @throws Exception
*/
public function __construct($remote_address, $context_option = null)
{
$address_info = parse_url($remote_address);
if (!$address_info) {
list($scheme, $this->_remoteAddress) = explode(':', $remote_address, 2);
if (!$this->_remoteAddress) {
echo new \Exception('bad remote_address');
}
} else {
if (!isset($address_info['port'])) {
$address_info['port'] = 80;
}
if (!isset($address_info['path'])) {
$address_info['path'] = '/';
}
if (!isset($address_info['query'])) {
$address_info['query'] = '';
} else {
$address_info['query'] = '?' . $address_info['query'];
}
$this->_remoteAddress = "{$address_info['host']}:{$address_info['port']}";
$this->_remoteHost = $address_info['host'];
$this->_remoteURI = "{$address_info['path']}{$address_info['query']}";
$scheme = isset($address_info['scheme']) ? $address_info['scheme'] : 'tcp';
}
$this->id = $this->_id = self::$_idRecorder++;
// Check application layer protocol class.
if (!isset(self::$_builtinTransports[$scheme])) {
$scheme = ucfirst($scheme);
$this->protocol = '\\Protocols\\' . $scheme;
if (!class_exists($this->protocol)) {
$this->protocol = "\\Workerman\\Protocols\\$scheme";
if (!class_exists($this->protocol)) {
throw new Exception("class \\Protocols\\$scheme not exist");
}
}
} else {
$this->transport = self::$_builtinTransports[$scheme];
}
// For statistics.
self::$statistics['connection_count']++;
$this->maxSendBufferSize = self::$defaultMaxSendBufferSize;
$this->_contextOption = $context_option;
static::$connections[$this->id] = $this;
}
/**
* Do connect.
*
* @return void
*/
public function connect()
{
if ($this->_status !== self::STATUS_INITIAL && $this->_status !== self::STATUS_CLOSING &&
$this->_status !== self::STATUS_CLOSED) {
return;
}
$this->_status = self::STATUS_CONNECTING;
$this->_connectStartTime = microtime(true);
// Open socket connection asynchronously.
if ($this->_contextOption) {
$context = stream_context_create($this->_contextOption);
$this->_socket = stream_socket_client("{$this->transport}://{$this->_remoteAddress}", $errno, $errstr, 0,
STREAM_CLIENT_ASYNC_CONNECT, $context);
} else {
$this->_socket = stream_socket_client("{$this->transport}://{$this->_remoteAddress}", $errno, $errstr, 0,
STREAM_CLIENT_ASYNC_CONNECT);
}
// If failed attempt to emit onError callback.
if (!$this->_socket) {
$this->emitError(WORKERMAN_CONNECT_FAIL, $errstr);
if ($this->_status === self::STATUS_CLOSING) {
$this->destroy();
}
if ($this->_status === self::STATUS_CLOSED) {
$this->onConnect = null;
}
return;
}
// Add socket to global event loop waiting connection is successfully established or faild.
Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'checkConnection'));
// For windows.
if(DIRECTORY_SEPARATOR === '\\') {
Worker::$globalEvent->add($this->_socket, EventInterface::EV_EXCEPT, array($this, 'checkConnection'));
}
}
/**
* Reconnect.
*
* @param int $after
* @return void
*/
public function reConnect($after = 0) {
$this->_status = self::STATUS_INITIAL;
if ($this->_reconnectTimer) {
Timer::del($this->_reconnectTimer);
}
if ($after > 0) {
$this->_reconnectTimer = Timer::add($after, array($this, 'connect'), null, false);
return;
}
$this->connect();
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteHost()
{
return $this->_remoteHost;
}
/**
* Get remote URI.
*
* @return string
*/
public function getRemoteURI()
{
return $this->_remoteURI;
}
/**
* Try to emit onError callback.
*
* @param int $code
* @param string $msg
* @return void
*/
protected function emitError($code, $msg)
{
$this->_status = self::STATUS_CLOSING;
if ($this->onError) {
try {
call_user_func($this->onError, $this, $code, $msg);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
}
/**
* Check connection is successfully established or faild.
*
* @param resource $socket
* @return void
*/
public function checkConnection($socket)
{
// Remove EV_EXPECT for windows.
if(DIRECTORY_SEPARATOR === '\\') {
Worker::$globalEvent->del($socket, EventInterface::EV_EXCEPT);
}
// Check socket state.
if ($address = stream_socket_get_name($socket, true)) {
// Remove write listener.
Worker::$globalEvent->del($socket, EventInterface::EV_WRITE);
// Nonblocking.
stream_set_blocking($socket, 0);
// Compatible with hhvm
if (function_exists('stream_set_read_buffer')) {
stream_set_read_buffer($socket, 0);
}
// Try to open keepalive for tcp and disable Nagle algorithm.
if (function_exists('socket_import_stream') && $this->transport === 'tcp') {
$raw_socket = socket_import_stream($socket);
socket_set_option($raw_socket, SOL_SOCKET, SO_KEEPALIVE, 1);
socket_set_option($raw_socket, SOL_TCP, TCP_NODELAY, 1);
}
// Register a listener waiting read event.
Worker::$globalEvent->add($socket, EventInterface::EV_READ, array($this, 'baseRead'));
// There are some data waiting to send.
if ($this->_sendBuffer) {
Worker::$globalEvent->add($socket, EventInterface::EV_WRITE, array($this, 'baseWrite'));
}
$this->_status = self::STATUS_ESTABLISHED;
$this->_remoteAddress = $address;
$this->_sslHandshakeCompleted = true;
// Try to emit onConnect callback.
if ($this->onConnect) {
try {
call_user_func($this->onConnect, $this);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
// Try to emit protocol::onConnect
if (method_exists($this->protocol, 'onConnect')) {
try {
call_user_func(array($this->protocol, 'onConnect'), $this);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
} else {
// Connection failed.
$this->emitError(WORKERMAN_CONNECT_FAIL, 'connect ' . $this->_remoteAddress . ' fail after ' . round(microtime(true) - $this->_connectStartTime, 4) . ' seconds');
if ($this->_status === self::STATUS_CLOSING) {
$this->destroy();
}
if ($this->_status === self::STATUS_CLOSED) {
$this->onConnect = null;
}
}
}
}

View File

@ -0,0 +1,101 @@
<?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 Workerman\Connection;
use Workerman\Events\EventInterface;
use Workerman\Worker;
use Exception;
/**
* AsyncTcpConnection.
*/
class AsyncUdpConnection extends UdpConnection
{
/**
* Construct.
*
* @param string $remote_address
* @throws Exception
*/
public function __construct($remote_address)
{
// Get the application layer communication protocol and listening address.
list($scheme, $address) = explode(':', $remote_address, 2);
// Check application layer protocol class.
if ($scheme !== 'udp') {
$scheme = ucfirst($scheme);
$this->protocol = '\\Protocols\\' . $scheme;
if (!class_exists($this->protocol)) {
$this->protocol = "\\Workerman\\Protocols\\$scheme";
if (!class_exists($this->protocol)) {
throw new Exception("class \\Protocols\\$scheme not exist");
}
}
}
$this->_remoteAddress = substr($address, 2);
$this->_socket = stream_socket_client("udp://{$this->_remoteAddress}");
Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead'));
}
/**
* For udp package.
*
* @param resource $socket
* @return bool
*/
public function baseRead($socket)
{
$recv_buffer = stream_socket_recvfrom($socket, Worker::MAX_UDP_PACKAGE_SIZE, 0, $remote_address);
if (false === $recv_buffer || empty($remote_address)) {
return false;
}
if ($this->onMessage) {
if ($this->protocol) {
$parser = $this->protocol;
$recv_buffer = $parser::decode($recv_buffer, $this);
}
ConnectionInterface::$statistics['total_request']++;
try {
call_user_func($this->onMessage, $this, $recv_buffer);
} catch (\Exception $e) {
self::log($e);
exit(250);
} catch (\Error $e) {
self::log($e);
exit(250);
}
}
return true;
}
/**
* Close connection.
*
* @param mixed $data
* @return bool
*/
public function close($data = null, $raw = false)
{
if ($data !== null) {
$this->send($data, $raw);
}
Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ);
fclose($this->_socket);
return true;
}
}

View File

@ -0,0 +1,125 @@
<?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 Workerman\Connection;
/**
* ConnectionInterface.
*/
abstract class ConnectionInterface
{
/**
* Statistics for status command.
*
* @var array
*/
public static $statistics = array(
'connection_count' => 0,
'total_request' => 0,
'throw_exception' => 0,
'send_fail' => 0,
);
/**
* Emitted when data is received.
*
* @var callback
*/
public $onMessage = null;
/**
* Emitted when the other end of the socket sends a FIN packet.
*
* @var callback
*/
public $onClose = null;
/**
* Emitted when an error occurs with connection.
*
* @var callback
*/
public $onError = null;
/**
* Sends data on the connection.
*
* @param string $send_buffer
* @return void|boolean
*/
abstract public function send($send_buffer);
/**
* Get remote IP.
*
* @return string
*/
abstract public function getRemoteIp();
/**
* Get remote port.
*
* @return int
*/
abstract public function getRemotePort();
/**
* Get remote address.
*
* @return string
*/
abstract public function getRemoteAddress();
/**
* Get remote IP.
*
* @return string
*/
abstract public function getLocalIp();
/**
* Get remote port.
*
* @return int
*/
abstract public function getLocalPort();
/**
* Get remote address.
*
* @return string
*/
abstract public function getLocalAddress();
/**
* Is ipv4.
*
* @return bool
*/
abstract public function isIPv4();
/**
* Is ipv6.
*
* @return bool
*/
abstract public function isIPv6();
/**
* Close connection.
*
* @param $data
* @return void
*/
abstract public function close($data = null);
}

View File

@ -0,0 +1,876 @@
<?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 Workerman\Connection;
use Workerman\Events\EventInterface;
use Workerman\Worker;
use Exception;
/**
* TcpConnection.
*/
class TcpConnection extends ConnectionInterface
{
/**
* Read buffer size.
*
* @var int
*/
const READ_BUFFER_SIZE = 65535;
/**
* Status initial.
*
* @var int
*/
const STATUS_INITIAL = 0;
/**
* Status connecting.
*
* @var int
*/
const STATUS_CONNECTING = 1;
/**
* Status connection established.
*
* @var int
*/
const STATUS_ESTABLISHED = 2;
/**
* Status closing.
*
* @var int
*/
const STATUS_CLOSING = 4;
/**
* Status closed.
*
* @var int
*/
const STATUS_CLOSED = 8;
/**
* Emitted when data is received.
*
* @var callback
*/
public $onMessage = null;
/**
* Emitted when the other end of the socket sends a FIN packet.
*
* @var callback
*/
public $onClose = null;
/**
* Emitted when an error occurs with connection.
*
* @var callback
*/
public $onError = null;
/**
* Emitted when the send buffer becomes full.
*
* @var callback
*/
public $onBufferFull = null;
/**
* Emitted when the send buffer becomes empty.
*
* @var callback
*/
public $onBufferDrain = null;
/**
* Application layer protocol.
* The format is like this Workerman\\Protocols\\Http.
*
* @var \Workerman\Protocols\ProtocolInterface
*/
public $protocol = null;
/**
* Transport (tcp/udp/unix/ssl).
*
* @var string
*/
public $transport = 'tcp';
/**
* Which worker belong to.
*
* @var Worker
*/
public $worker = null;
/**
* Bytes read.
*
* @var int
*/
public $bytesRead = 0;
/**
* Bytes written.
*
* @var int
*/
public $bytesWritten = 0;
/**
* Connection->id.
*
* @var int
*/
public $id = 0;
/**
* A copy of $worker->id which used to clean up the connection in worker->connections
*
* @var int
*/
protected $_id = 0;
/**
* Sets the maximum send buffer size for the current connection.
* OnBufferFull callback will be emited When the send buffer is full.
*
* @var int
*/
public $maxSendBufferSize = 1048576;
/**
* Default send buffer size.
*
* @var int
*/
public static $defaultMaxSendBufferSize = 1048576;
/**
* Maximum acceptable packet size.
*
* @var int
*/
public static $maxPackageSize = 10485760;
/**
* Id recorder.
*
* @var int
*/
protected static $_idRecorder = 1;
/**
* Socket
*
* @var resource
*/
protected $_socket = null;
/**
* Send buffer.
*
* @var string
*/
protected $_sendBuffer = '';
/**
* Receive buffer.
*
* @var string
*/
protected $_recvBuffer = '';
/**
* Current package length.
*
* @var int
*/
protected $_currentPackageLength = 0;
/**
* Connection status.
*
* @var int
*/
protected $_status = self::STATUS_ESTABLISHED;
/**
* Remote address.
*
* @var string
*/
protected $_remoteAddress = '';
/**
* Is paused.
*
* @var bool
*/
protected $_isPaused = false;
/**
* SSL handshake completed or not.
*
* @var bool
*/
protected $_sslHandshakeCompleted = false;
/**
* All connection instances.
*
* @var array
*/
public static $connections = array();
/**
* Status to string.
*
* @var array
*/
public static $_statusToString = array(
self::STATUS_INITIAL => 'INITIAL',
self::STATUS_CONNECTING => 'CONNECTING',
self::STATUS_ESTABLISHED => 'ESTABLISHED',
self::STATUS_CLOSING => 'CLOSING',
self::STATUS_CLOSED => 'CLOSED',
);
/**
* Construct.
*
* @param resource $socket
* @param string $remote_address
*/
public function __construct($socket, $remote_address = '')
{
self::$statistics['connection_count']++;
$this->id = $this->_id = self::$_idRecorder++;
$this->_socket = $socket;
stream_set_blocking($this->_socket, 0);
// Compatible with hhvm
if (function_exists('stream_set_read_buffer')) {
stream_set_read_buffer($this->_socket, 0);
}
Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead'));
$this->maxSendBufferSize = self::$defaultMaxSendBufferSize;
$this->_remoteAddress = $remote_address;
static::$connections[$this->id] = $this;
}
/**
* Get status.
*
* @param bool $raw_output
*
* @return int
*/
public function getStatus($raw_output = true)
{
if ($raw_output) {
return $this->_status;
}
return self::$_statusToString[$this->_status];
}
/**
* Sends data on the connection.
*
* @param string $send_buffer
* @param bool $raw
* @return void|bool|null
*/
public function send($send_buffer, $raw = false)
{
if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) {
return false;
}
// Try to call protocol::encode($send_buffer) before sending.
if (false === $raw && $this->protocol !== null) {
$parser = $this->protocol;
$send_buffer = $parser::encode($send_buffer, $this);
if ($send_buffer === '') {
return null;
}
}
if ($this->_status !== self::STATUS_ESTABLISHED ||
($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true)
) {
if ($this->_sendBuffer) {
if ($this->bufferIsFull()) {
self::$statistics['send_fail']++;
return false;
}
}
$this->_sendBuffer .= $send_buffer;
$this->checkBufferWillFull();
return null;
}
// Attempt to send data directly.
if ($this->_sendBuffer === '') {
$len = @fwrite($this->_socket, $send_buffer, 8192);
// send successful.
if ($len === strlen($send_buffer)) {
$this->bytesWritten += $len;
return true;
}
// Send only part of the data.
if ($len > 0) {
$this->_sendBuffer = substr($send_buffer, $len);
$this->bytesWritten += $len;
} else {
// Connection closed?
if (!is_resource($this->_socket) || feof($this->_socket)) {
self::$statistics['send_fail']++;
if ($this->onError) {
try {
call_user_func($this->onError, $this, WORKERMAN_SEND_FAIL, 'client closed');
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
$this->destroy();
return false;
}
$this->_sendBuffer = $send_buffer;
}
Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite'));
// Check if the send buffer will be full.
$this->checkBufferWillFull();
return null;
} else {
if ($this->bufferIsFull()) {
self::$statistics['send_fail']++;
return false;
}
$this->_sendBuffer .= $send_buffer;
// Check if the send buffer is full.
$this->checkBufferWillFull();
}
}
/**
* Get remote IP.
*
* @return string
*/
public function getRemoteIp()
{
$pos = strrpos($this->_remoteAddress, ':');
if ($pos) {
return substr($this->_remoteAddress, 0, $pos);
}
return '';
}
/**
* Get remote port.
*
* @return int
*/
public function getRemotePort()
{
if ($this->_remoteAddress) {
return (int)substr(strrchr($this->_remoteAddress, ':'), 1);
}
return 0;
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteAddress()
{
return $this->_remoteAddress;
}
/**
* Get local IP.
*
* @return string
*/
public function getLocalIp()
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return '';
}
return substr($address, 0, $pos);
}
/**
* Get local port.
*
* @return int
*/
public function getLocalPort()
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return 0;
}
return (int)substr(strrchr($address, ':'), 1);
}
/**
* Get local address.
*
* @return string
*/
public function getLocalAddress()
{
return (string)@stream_socket_get_name($this->_socket, false);
}
/**
* Get send buffer queue size.
*
* @return integer
*/
public function getSendBufferQueueSize()
{
return strlen($this->_sendBuffer);
}
/**
* Get recv buffer queue size.
*
* @return integer
*/
public function getRecvBufferQueueSize()
{
return strlen($this->_recvBuffer);
}
/**
* Is ipv4.
*
* return bool.
*/
public function isIpV4()
{
if ($this->transport === 'unix') {
return false;
}
return strpos($this->getRemoteIp(), ':') === false;
}
/**
* Is ipv6.
*
* return bool.
*/
public function isIpV6()
{
if ($this->transport === 'unix') {
return false;
}
return strpos($this->getRemoteIp(), ':') !== false;
}
/**
* Pauses the reading of data. That is onMessage will not be emitted. Useful to throttle back an upload.
*
* @return void
*/
public function pauseRecv()
{
Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ);
$this->_isPaused = true;
}
/**
* Resumes reading after a call to pauseRecv.
*
* @return void
*/
public function resumeRecv()
{
if ($this->_isPaused === true) {
Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead'));
$this->_isPaused = false;
$this->baseRead($this->_socket, false);
}
}
/**
* Base read handler.
*
* @param resource $socket
* @param bool $check_eof
* @return void
*/
public function baseRead($socket, $check_eof = true)
{
// SSL handshake.
if ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) {
$ret = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_SSLv2_SERVER |
STREAM_CRYPTO_METHOD_SSLv3_SERVER | STREAM_CRYPTO_METHOD_SSLv23_SERVER);
// Negotiation has failed.
if(false === $ret) {
if (!feof($socket)) {
echo "\nSSL Handshake fail. \nBuffer:".bin2hex(fread($socket, 8182))."\n";
}
return $this->destroy();
} elseif(0 === $ret) {
// There isn't enough data and should try again.
return;
}
if (isset($this->onSslHandshake)) {
try {
call_user_func($this->onSslHandshake, $this);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
$this->_sslHandshakeCompleted = true;
if ($this->_sendBuffer) {
Worker::$globalEvent->add($socket, EventInterface::EV_WRITE, array($this, 'baseWrite'));
}
return;
}
$buffer = @fread($socket, self::READ_BUFFER_SIZE);
// Check connection closed.
if ($buffer === '' || $buffer === false) {
if ($check_eof && (feof($socket) || !is_resource($socket) || $buffer === false)) {
$this->destroy();
return;
}
} else {
$this->bytesRead += strlen($buffer);
$this->_recvBuffer .= $buffer;
}
// If the application layer protocol has been set up.
if ($this->protocol !== null) {
$parser = $this->protocol;
while ($this->_recvBuffer !== '' && !$this->_isPaused) {
// The current packet length is known.
if ($this->_currentPackageLength) {
// Data is not enough for a package.
if ($this->_currentPackageLength > strlen($this->_recvBuffer)) {
break;
}
} else {
// Get current package length.
$this->_currentPackageLength = $parser::input($this->_recvBuffer, $this);
// The packet length is unknown.
if ($this->_currentPackageLength === 0) {
break;
} elseif ($this->_currentPackageLength > 0 && $this->_currentPackageLength <= self::$maxPackageSize) {
// Data is not enough for a package.
if ($this->_currentPackageLength > strlen($this->_recvBuffer)) {
break;
}
} // Wrong package.
else {
echo 'error package. package_length=' . var_export($this->_currentPackageLength, true);
$this->destroy();
return;
}
}
// The data is enough for a packet.
self::$statistics['total_request']++;
// The current packet length is equal to the length of the buffer.
if (strlen($this->_recvBuffer) === $this->_currentPackageLength) {
$one_request_buffer = $this->_recvBuffer;
$this->_recvBuffer = '';
} else {
// Get a full package from the buffer.
$one_request_buffer = substr($this->_recvBuffer, 0, $this->_currentPackageLength);
// Remove the current package from the receive buffer.
$this->_recvBuffer = substr($this->_recvBuffer, $this->_currentPackageLength);
}
// Reset the current packet length to 0.
$this->_currentPackageLength = 0;
if (!$this->onMessage) {
continue;
}
try {
// Decode request buffer before Emitting onMessage callback.
call_user_func($this->onMessage, $this, $parser::decode($one_request_buffer, $this));
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
return;
}
if ($this->_recvBuffer === '' || $this->_isPaused) {
return;
}
// Applications protocol is not set.
self::$statistics['total_request']++;
if (!$this->onMessage) {
$this->_recvBuffer = '';
return;
}
try {
call_user_func($this->onMessage, $this, $this->_recvBuffer);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
// Clean receive buffer.
$this->_recvBuffer = '';
}
/**
* Base write handler.
*
* @return void|bool
*/
public function baseWrite()
{
$len = @fwrite($this->_socket, $this->_sendBuffer, 8192);
if ($len === strlen($this->_sendBuffer)) {
$this->bytesWritten += $len;
Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE);
$this->_sendBuffer = '';
// Try to emit onBufferDrain callback when the send buffer becomes empty.
if ($this->onBufferDrain) {
try {
call_user_func($this->onBufferDrain, $this);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
if ($this->_status === self::STATUS_CLOSING) {
$this->destroy();
}
return true;
}
if ($len > 0) {
$this->bytesWritten += $len;
$this->_sendBuffer = substr($this->_sendBuffer, $len);
} else {
self::$statistics['send_fail']++;
$this->destroy();
}
}
/**
* This method pulls all the data out of a readable stream, and writes it to the supplied destination.
*
* @param TcpConnection $dest
* @return void
*/
public function pipe($dest)
{
$source = $this;
$this->onMessage = function ($source, $data) use ($dest) {
$dest->send($data);
};
$this->onClose = function ($source) use ($dest) {
$dest->destroy();
};
$dest->onBufferFull = function ($dest) use ($source) {
$source->pauseRecv();
};
$dest->onBufferDrain = function ($dest) use ($source) {
$source->resumeRecv();
};
}
/**
* Remove $length of data from receive buffer.
*
* @param int $length
* @return void
*/
public function consumeRecvBuffer($length)
{
$this->_recvBuffer = substr($this->_recvBuffer, $length);
}
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
public function close($data = null, $raw = false)
{
if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) {
return;
} else {
if ($data !== null) {
$this->send($data, $raw);
}
$this->_status = self::STATUS_CLOSING;
}
if ($this->_sendBuffer === '') {
$this->destroy();
}
}
/**
* Get the real socket.
*
* @return resource
*/
public function getSocket()
{
return $this->_socket;
}
/**
* Check whether the send buffer will be full.
*
* @return void
*/
protected function checkBufferWillFull()
{
if ($this->maxSendBufferSize <= strlen($this->_sendBuffer)) {
if ($this->onBufferFull) {
try {
call_user_func($this->onBufferFull, $this);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
}
}
/**
* Whether send buffer is full.
*
* @return bool
*/
protected function bufferIsFull()
{
// Buffer has been marked as full but still has data to send then the packet is discarded.
if ($this->maxSendBufferSize <= strlen($this->_sendBuffer)) {
if ($this->onError) {
try {
call_user_func($this->onError, $this, WORKERMAN_SEND_FAIL, 'send buffer full and drop package');
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
return true;
}
return false;
}
/**
* Destroy connection.
*
* @return void
*/
public function destroy()
{
// Avoid repeated calls.
if ($this->_status === self::STATUS_CLOSED) {
return;
}
// Remove event listener.
Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ);
Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE);
// Close socket.
@fclose($this->_socket);
// Remove from worker->connections.
if ($this->worker) {
unset($this->worker->connections[$this->_id]);
}
unset(static::$connections[$this->_id]);
$this->_status = self::STATUS_CLOSED;
// Try to emit onClose callback.
if ($this->onClose) {
try {
call_user_func($this->onClose, $this);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
// Try to emit protocol::onClose
if (method_exists($this->protocol, 'onClose')) {
try {
call_user_func(array($this->protocol, 'onClose'), $this);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
if ($this->_status === self::STATUS_CLOSED) {
// Cleaning up the callback to avoid memory leaks.
$this->onMessage = $this->onClose = $this->onError = $this->onBufferFull = $this->onBufferDrain = null;
}
}
/**
* Destruct.
*
* @return void
*/
public function __destruct()
{
self::$statistics['connection_count']--;
}
}

View File

@ -0,0 +1,191 @@
<?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 Workerman\Connection;
/**
* UdpConnection.
*/
class UdpConnection extends ConnectionInterface
{
/**
* Application layer protocol.
* The format is like this Workerman\\Protocols\\Http.
*
* @var \Workerman\Protocols\ProtocolInterface
*/
public $protocol = null;
/**
* Udp socket.
*
* @var resource
*/
protected $_socket = null;
/**
* Remote address.
*
* @var string
*/
protected $_remoteAddress = '';
/**
* Construct.
*
* @param resource $socket
* @param string $remote_address
*/
public function __construct($socket, $remote_address)
{
$this->_socket = $socket;
$this->_remoteAddress = $remote_address;
}
/**
* Sends data on the connection.
*
* @param string $send_buffer
* @param bool $raw
* @return void|boolean
*/
public function send($send_buffer, $raw = false)
{
if (false === $raw && $this->protocol) {
$parser = $this->protocol;
$send_buffer = $parser::encode($send_buffer, $this);
if ($send_buffer === '') {
return null;
}
}
return strlen($send_buffer) === stream_socket_sendto($this->_socket, $send_buffer, 0, $this->_remoteAddress);
}
/**
* Get remote IP.
*
* @return string
*/
public function getRemoteIp()
{
$pos = strrpos($this->_remoteAddress, ':');
if ($pos) {
return trim(substr($this->_remoteAddress, 0, $pos), '[]');
}
return '';
}
/**
* Get remote port.
*
* @return int
*/
public function getRemotePort()
{
if ($this->_remoteAddress) {
return (int)substr(strrchr($this->_remoteAddress, ':'), 1);
}
return 0;
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteAddress()
{
return $this->_remoteAddress;
}
/**
* Get local IP.
*
* @return string
*/
public function getLocalIp()
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return '';
}
return substr($address, 0, $pos);
}
/**
* Get local port.
*
* @return int
*/
public function getLocalPort()
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return 0;
}
return (int)substr(strrchr($address, ':'), 1);
}
/**
* Get local address.
*
* @return string
*/
public function getLocalAddress()
{
return (string)@stream_socket_get_name($this->_socket, false);
}
/**
* Is ipv4.
*
* return bool.
*/
public function isIpV4()
{
if ($this->transport === 'unix') {
return false;
}
return strpos($this->getRemoteIp(), ':') === false;
}
/**
* Is ipv6.
*
* return bool.
*/
public function isIpV6()
{
if ($this->transport === 'unix') {
return false;
}
return strpos($this->getRemoteIp(), ':') !== false;
}
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return bool
*/
public function close($data = null, $raw = false)
{
if ($data !== null) {
$this->send($data, $raw);
}
return true;
}
}

View File

@ -0,0 +1,184 @@
<?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 有个鬼<42765633@qq.com>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Workerman\Events;
use Workerman\Worker;
/**
* ev eventloop
*/
class Ev implements EventInterface
{
/**
* All listeners for read/write event.
*
* @var array
*/
protected $_allEvents = array();
/**
* Event listeners of signal.
*
* @var array
*/
protected $_eventSignal = array();
/**
* All timer event listeners.
* [func, args, event, flag, time_interval]
*
* @var array
*/
protected $_eventTimer = array();
/**
* Timer id.
*
* @var int
*/
protected static $_timerId = 1;
/**
* Add a timer.
* {@inheritdoc}
*/
public function add($fd, $flag, $func, $args = null)
{
$callback = function ($event, $socket) use ($fd, $func) {
try {
call_user_func($func, $fd);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
};
switch ($flag) {
case self::EV_SIGNAL:
$event = new \EvSignal($fd, $callback);
$this->_eventSignal[$fd] = $event;
return true;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
$repeat = $flag == self::EV_TIMER_ONCE ? 0 : $fd;
$param = array($func, (array)$args, $flag, $fd, self::$_timerId);
$event = new \EvTimer($fd, $repeat, array($this, 'timerCallback'), $param);
$this->_eventTimer[self::$_timerId] = $event;
return self::$_timerId++;
default :
$fd_key = (int)$fd;
$real_flag = $flag === self::EV_READ ? \Ev::READ : \Ev::WRITE;
$event = new \EvIo($fd, $real_flag, $callback);
$this->_allEvents[$fd_key][$flag] = $event;
return true;
}
}
/**
* Remove a timer.
* {@inheritdoc}
*/
public function del($fd, $flag)
{
switch ($flag) {
case self::EV_READ:
case self::EV_WRITE:
$fd_key = (int)$fd;
if (isset($this->_allEvents[$fd_key][$flag])) {
$this->_allEvents[$fd_key][$flag]->stop();
unset($this->_allEvents[$fd_key][$flag]);
}
if (empty($this->_allEvents[$fd_key])) {
unset($this->_allEvents[$fd_key]);
}
break;
case self::EV_SIGNAL:
$fd_key = (int)$fd;
if (isset($this->_eventSignal[$fd_key])) {
$this->_eventSignal[$fd_key]->stop();
unset($this->_eventSignal[$fd_key]);
}
break;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
if (isset($this->_eventTimer[$fd])) {
$this->_eventTimer[$fd]->stop();
unset($this->_eventTimer[$fd]);
}
break;
}
return true;
}
/**
* Timer callback.
*
* @param \EvWatcher $event
*/
public function timerCallback($event)
{
$param = $event->data;
$timer_id = $param[4];
if ($param[2] === self::EV_TIMER_ONCE) {
$this->_eventTimer[$timer_id]->stop();
unset($this->_eventTimer[$timer_id]);
}
try {
call_user_func_array($param[0], $param[1]);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
/**
* Remove all timers.
*
* @return void
*/
public function clearAllTimer()
{
foreach ($this->_eventTimer as $event) {
$event->stop();
}
$this->_eventTimer = array();
}
/**
* Main loop.
*
* @see EventInterface::loop()
*/
public function loop()
{
\Ev::run();
}
/**
* Destroy loop.
*
* @return void
*/
public function destroy()
{
foreach ($this->_allEvents as $event) {
$event->stop();
}
}
}

View File

@ -0,0 +1,199 @@
<?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 有个鬼<42765633@qq.com>
* @copyright 有个鬼<42765633@qq.com>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Workerman\Events;
use Workerman\Worker;
/**
* libevent eventloop
*/
class Event implements EventInterface
{
/**
* Event base.
* @var object
*/
protected $_eventBase = null;
/**
* All listeners for read/write event.
* @var array
*/
protected $_allEvents = array();
/**
* Event listeners of signal.
* @var array
*/
protected $_eventSignal = array();
/**
* All timer event listeners.
* [func, args, event, flag, time_interval]
* @var array
*/
protected $_eventTimer = array();
/**
* Timer id.
* @var int
*/
protected static $_timerId = 1;
/**
* construct
* @return void
*/
public function __construct()
{
$this->_eventBase = new \EventBase();
}
/**
* @see EventInterface::add()
*/
public function add($fd, $flag, $func, $args=array())
{
switch ($flag) {
case self::EV_SIGNAL:
$fd_key = (int)$fd;
$event = \Event::signal($this->_eventBase, $fd, $func);
if (!$event||!$event->add()) {
return false;
}
$this->_eventSignal[$fd_key] = $event;
return true;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
$param = array($func, (array)$args, $flag, $fd, self::$_timerId);
$event = new \Event($this->_eventBase, -1, \Event::TIMEOUT|\Event::PERSIST, array($this, "timerCallback"), $param);
if (!$event||!$event->addTimer($fd)) {
return false;
}
$this->_eventTimer[self::$_timerId] = $event;
return self::$_timerId++;
default :
$fd_key = (int)$fd;
$real_flag = $flag === self::EV_READ ? \Event::READ | \Event::PERSIST : \Event::WRITE | \Event::PERSIST;
$event = new \Event($this->_eventBase, $fd, $real_flag, $func, $fd);
if (!$event||!$event->add()) {
return false;
}
$this->_allEvents[$fd_key][$flag] = $event;
return true;
}
}
/**
* @see Events\EventInterface::del()
*/
public function del($fd, $flag)
{
switch ($flag) {
case self::EV_READ:
case self::EV_WRITE:
$fd_key = (int)$fd;
if (isset($this->_allEvents[$fd_key][$flag])) {
$this->_allEvents[$fd_key][$flag]->del();
unset($this->_allEvents[$fd_key][$flag]);
}
if (empty($this->_allEvents[$fd_key])) {
unset($this->_allEvents[$fd_key]);
}
break;
case self::EV_SIGNAL:
$fd_key = (int)$fd;
if (isset($this->_eventSignal[$fd_key])) {
$this->_eventSignal[$fd_key]->del();
unset($this->_eventSignal[$fd_key]);
}
break;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
if (isset($this->_eventTimer[$fd])) {
$this->_eventTimer[$fd]->del();
unset($this->_eventTimer[$fd]);
}
break;
}
return true;
}
/**
* Timer callback.
* @param null $fd
* @param int $what
* @param int $timer_id
*/
public function timerCallback($fd, $what, $param)
{
$timer_id = $param[4];
if ($param[2] === self::EV_TIMER_ONCE) {
$this->_eventTimer[$timer_id]->del();
unset($this->_eventTimer[$timer_id]);
}
try {
call_user_func_array($param[0], $param[1]);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
/**
* @see Events\EventInterface::clearAllTimer()
* @return void
*/
public function clearAllTimer()
{
foreach ($this->_eventTimer as $event) {
$event->del();
}
$this->_eventTimer = array();
}
/**
* @see EventInterface::loop()
*/
public function loop()
{
$this->_eventBase->loop();
}
/**
* Destroy loop.
*
* @return void
*/
public function destroy()
{
foreach ($this->_eventSignal as $event) {
$event->del();
}
}
}

View File

@ -0,0 +1,100 @@
<?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 Workerman\Events;
interface EventInterface
{
/**
* Read event.
*
* @var int
*/
const EV_READ = 1;
/**
* Write event.
*
* @var int
*/
const EV_WRITE = 2;
/**
* Except event
*
* @var int
*/
const EV_EXCEPT = 3;
/**
* Signal event.
*
* @var int
*/
const EV_SIGNAL = 4;
/**
* Timer event.
*
* @var int
*/
const EV_TIMER = 8;
/**
* Timer once event.
*
* @var int
*/
const EV_TIMER_ONCE = 16;
/**
* Add event listener to event loop.
*
* @param mixed $fd
* @param int $flag
* @param callable $func
* @param mixed $args
* @return bool
*/
public function add($fd, $flag, $func, $args = null);
/**
* Remove event listener from event loop.
*
* @param mixed $fd
* @param int $flag
* @return bool
*/
public function del($fd, $flag);
/**
* Remove all timers.
*
* @return void
*/
public function clearAllTimer();
/**
* Main loop.
*
* @return void
*/
public function loop();
/**
* Destroy loop.
*
* @return mixed
*/
public function destroy();
}

View File

@ -0,0 +1,217 @@
<?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 Workerman\Events;
use Workerman\Worker;
/**
* libevent eventloop
*/
class Libevent implements EventInterface
{
/**
* Event base.
*
* @var resource
*/
protected $_eventBase = null;
/**
* All listeners for read/write event.
*
* @var array
*/
protected $_allEvents = array();
/**
* Event listeners of signal.
*
* @var array
*/
protected $_eventSignal = array();
/**
* All timer event listeners.
* [func, args, event, flag, time_interval]
*
* @var array
*/
protected $_eventTimer = array();
/**
* construct
*/
public function __construct()
{
$this->_eventBase = event_base_new();
}
/**
* {@inheritdoc}
*/
public function add($fd, $flag, $func, $args = array())
{
switch ($flag) {
case self::EV_SIGNAL:
$fd_key = (int)$fd;
$real_flag = EV_SIGNAL | EV_PERSIST;
$this->_eventSignal[$fd_key] = event_new();
if (!event_set($this->_eventSignal[$fd_key], $fd, $real_flag, $func, null)) {
return false;
}
if (!event_base_set($this->_eventSignal[$fd_key], $this->_eventBase)) {
return false;
}
if (!event_add($this->_eventSignal[$fd_key])) {
return false;
}
return true;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
$event = event_new();
$timer_id = (int)$event;
if (!event_set($event, 0, EV_TIMEOUT, array($this, 'timerCallback'), $timer_id)) {
return false;
}
if (!event_base_set($event, $this->_eventBase)) {
return false;
}
$time_interval = $fd * 1000000;
if (!event_add($event, $time_interval)) {
return false;
}
$this->_eventTimer[$timer_id] = array($func, (array)$args, $event, $flag, $time_interval);
return $timer_id;
default :
$fd_key = (int)$fd;
$real_flag = $flag === self::EV_READ ? EV_READ | EV_PERSIST : EV_WRITE | EV_PERSIST;
$event = event_new();
if (!event_set($event, $fd, $real_flag, $func, null)) {
return false;
}
if (!event_base_set($event, $this->_eventBase)) {
return false;
}
if (!event_add($event)) {
return false;
}
$this->_allEvents[$fd_key][$flag] = $event;
return true;
}
}
/**
* {@inheritdoc}
*/
public function del($fd, $flag)
{
switch ($flag) {
case self::EV_READ:
case self::EV_WRITE:
$fd_key = (int)$fd;
if (isset($this->_allEvents[$fd_key][$flag])) {
event_del($this->_allEvents[$fd_key][$flag]);
unset($this->_allEvents[$fd_key][$flag]);
}
if (empty($this->_allEvents[$fd_key])) {
unset($this->_allEvents[$fd_key]);
}
break;
case self::EV_SIGNAL:
$fd_key = (int)$fd;
if (isset($this->_eventSignal[$fd_key])) {
event_del($this->_eventSignal[$fd_key]);
unset($this->_eventSignal[$fd_key]);
}
break;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
// 这里 fd 为timerid
if (isset($this->_eventTimer[$fd])) {
event_del($this->_eventTimer[$fd][2]);
unset($this->_eventTimer[$fd]);
}
break;
}
return true;
}
/**
* Timer callback.
*
* @param mixed $_null1
* @param int $_null2
* @param mixed $timer_id
*/
protected function timerCallback($_null1, $_null2, $timer_id)
{
if ($this->_eventTimer[$timer_id][3] === self::EV_TIMER) {
event_add($this->_eventTimer[$timer_id][2], $this->_eventTimer[$timer_id][4]);
}
try {
call_user_func_array($this->_eventTimer[$timer_id][0], $this->_eventTimer[$timer_id][1]);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
if (isset($this->_eventTimer[$timer_id]) && $this->_eventTimer[$timer_id][3] === self::EV_TIMER_ONCE) {
$this->del($timer_id, self::EV_TIMER_ONCE);
}
}
/**
* {@inheritdoc}
*/
public function clearAllTimer()
{
foreach ($this->_eventTimer as $task_data) {
event_del($task_data[2]);
}
$this->_eventTimer = array();
}
/**
* {@inheritdoc}
*/
public function loop()
{
event_base_loop($this->_eventBase);
}
/**
* Destroy loop.
*
* @return void
*/
public function destroy()
{
foreach ($this->_eventSignal as $event) {
event_del($event);
}
}
}

View File

@ -0,0 +1,173 @@
<?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 Workerman\Events\React;
use Workerman\Events\EventInterface;
/**
* Class ExtEventLoop
* @package Workerman\Events\React
*/
class ExtEventLoop extends \React\EventLoop\ExtEventLoop
{
/**
* Event base.
*
* @var EventBase
*/
protected $_eventBase = null;
/**
* All signal Event instances.
*
* @var array
*/
protected $_signalEvents = array();
/**
* @var array
*/
protected $_timerIdMap = array();
/**
* @var int
*/
protected $_timerIdIndex = 0;
/**
* Add event listener to event loop.
*
* @param $fd
* @param $flag
* @param $func
* @param array $args
* @return bool
*/
public function add($fd, $flag, $func, $args = array())
{
$args = (array)$args;
switch ($flag) {
case EventInterface::EV_READ:
return $this->addReadStream($fd, $func);
case EventInterface::EV_WRITE:
return $this->addWriteStream($fd, $func);
case EventInterface::EV_SIGNAL:
return $this->addSignal($fd, $func);
case EventInterface::EV_TIMER:
$timer_obj = $this->addPeriodicTimer($fd, function() use ($func, $args) {
call_user_func_array($func, $args);
});
$this->_timerIdMap[++$this->_timerIdIndex] = $timer_obj;
return $this->_timerIdIndex;
case EventInterface::EV_TIMER_ONCE:
$timer_obj = $this->addTimer($fd, function() use ($func, $args) {
call_user_func_array($func, $args);
});
$this->_timerIdMap[++$this->_timerIdIndex] = $timer_obj;
return $this->_timerIdIndex;
}
return false;
}
/**
* Remove event listener from event loop.
*
* @param mixed $fd
* @param int $flag
* @return bool
*/
public function del($fd, $flag)
{
switch ($flag) {
case EventInterface::EV_READ:
return $this->removeReadStream($fd);
case EventInterface::EV_WRITE:
return $this->removeWriteStream($fd);
case EventInterface::EV_SIGNAL:
return $this->removeSignal($fd);
case EventInterface::EV_TIMER:
case EventInterface::EV_TIMER_ONCE;
if (isset($this->_timerIdMap[$fd])){
$timer_obj = $this->_timerIdMap[$fd];
unset($this->_timerIdMap[$fd]);
$this->cancelTimer($timer_obj);
return true;
}
}
return false;
}
/**
* Main loop.
*
* @return void
*/
public function loop()
{
$this->run();
}
/**
* Construct
*/
public function __construct()
{
parent::__construct();
$class = new \ReflectionClass('\React\EventLoop\ExtEventLoop');
$property = $class->getProperty('eventBase');
$property->setAccessible(true);
$this->_eventBase = $property->getValue($this);
}
/**
* Add signal handler.
*
* @param $signal
* @param $callback
* @return bool
*/
public function addSignal($signal, $callback)
{
$event = \Event::signal($this->_eventBase, $signal, $callback);
if (!$event||!$event->add()) {
return false;
}
$this->_signalEvents[$signal] = $event;
}
/**
* Remove signal handler.
*
* @param $signal
*/
public function removeSignal($signal)
{
if (isset($this->_signalEvents[$signal])) {
$this->_signalEvents[$signal]->del();
unset($this->_signalEvents[$signal]);
}
}
/**
* Destroy loop.
*
* @return void
*/
public function destroy()
{
foreach ($this->_signalEvents as $event) {
$event->del();
}
}
}

View File

@ -0,0 +1,174 @@
<?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 Workerman\Events\React;
use Workerman\Events\EventInterface;
/**
* Class LibEventLoop
* @package Workerman\Events\React
*/
class LibEventLoop extends \React\EventLoop\LibEventLoop
{
/**
* Event base.
*
* @var event_base resource
*/
protected $_eventBase = null;
/**
* All signal Event instances.
*
* @var array
*/
protected $_signalEvents = array();
/**
* @var array
*/
protected $_timerIdMap = array();
/**
* @var int
*/
protected $_timerIdIndex = 0;
/**
* Add event listener to event loop.
*
* @param $fd
* @param $flag
* @param $func
* @param array $args
* @return bool
*/
public function add($fd, $flag, $func, $args = array())
{
$args = (array)$args;
switch ($flag) {
case EventInterface::EV_READ:
return $this->addReadStream($fd, $func);
case EventInterface::EV_WRITE:
return $this->addWriteStream($fd, $func);
case EventInterface::EV_SIGNAL:
return $this->addSignal($fd, $func);
case EventInterface::EV_TIMER:
$timer_obj = $this->addPeriodicTimer($fd, function() use ($func, $args) {
call_user_func_array($func, $args);
});
$this->_timerIdMap[++$this->_timerIdIndex] = $timer_obj;
return $this->_timerIdIndex;
case EventInterface::EV_TIMER_ONCE:
$timer_obj = $this->addTimer($fd, function() use ($func, $args) {
call_user_func_array($func, $args);
});
$this->_timerIdMap[++$this->_timerIdIndex] = $timer_obj;
return $this->_timerIdIndex;
}
return false;
}
/**
* Remove event listener from event loop.
*
* @param mixed $fd
* @param int $flag
* @return bool
*/
public function del($fd, $flag)
{
switch ($flag) {
case EventInterface::EV_READ:
return $this->removeReadStream($fd);
case EventInterface::EV_WRITE:
return $this->removeWriteStream($fd);
case EventInterface::EV_SIGNAL:
return $this->removeSignal($fd);
case EventInterface::EV_TIMER:
case EventInterface::EV_TIMER_ONCE;
if (isset($this->_timerIdMap[$fd])){
$timer_obj = $this->_timerIdMap[$fd];
unset($this->_timerIdMap[$fd]);
$this->cancelTimer($timer_obj);
return true;
}
}
return false;
}
/**
* Main loop.
*
* @return void
*/
public function loop()
{
$this->run();
}
/**
* Construct.
*/
public function __construct()
{
parent::__construct();
$class = new \ReflectionClass('\React\EventLoop\LibEventLoop');
$property = $class->getProperty('eventBase');
$property->setAccessible(true);
$this->_eventBase = $property->getValue($this);
}
/**
* Add signal handler.
*
* @param $signal
* @param $callback
* @return bool
*/
public function addSignal($signal, $callback)
{
$event = event_new();
$this->_signalEvents[$signal] = $event;
event_set($event, $signal, EV_SIGNAL | EV_PERSIST, $callback);
event_base_set($event, $this->_eventBase);
event_add($event);
}
/**
* Remove signal handler.
*
* @param $signal
*/
public function removeSignal($signal)
{
if (isset($this->_signalEvents[$signal])) {
$event = $this->_signalEvents[$signal];
event_del($event);
unset($this->_signalEvents[$signal]);
}
}
/**
* Destroy loop.
*
* @return void
*/
public function destroy()
{
foreach ($this->_signalEvents as $event) {
event_del($event);
}
}
}

View File

@ -0,0 +1,176 @@
<?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 Workerman\Events\React;
use Workerman\Events\EventInterface;
/**
* Class StreamSelectLoop
* @package Workerman\Events\React
*/
class StreamSelectLoop extends \React\EventLoop\StreamSelectLoop
{
/**
* @var array
*/
protected $_timerIdMap = array();
/**
* @var int
*/
protected $_timerIdIndex = 0;
/**
* Add event listener to event loop.
*
* @param $fd
* @param $flag
* @param $func
* @param array $args
* @return bool
*/
public function add($fd, $flag, $func, $args = array())
{
$args = (array)$args;
switch ($flag) {
case EventInterface::EV_READ:
return $this->addReadStream($fd, $func);
case EventInterface::EV_WRITE:
return $this->addWriteStream($fd, $func);
case EventInterface::EV_SIGNAL:
return $this->addSignal($fd, $func);
case EventInterface::EV_TIMER:
$timer_obj = $this->addPeriodicTimer($fd, function() use ($func, $args) {
call_user_func_array($func, $args);
});
$this->_timerIdMap[++$this->_timerIdIndex] = $timer_obj;
return $this->_timerIdIndex;
case EventInterface::EV_TIMER_ONCE:
$index = ++$this->_timerIdIndex;
$timer_obj = $this->addTimer($fd, function() use ($func, $args, $index) {
$this->del($index,EventInterface::EV_TIMER_ONCE);
call_user_func_array($func, $args);
});
$this->_timerIdMap[$index] = $timer_obj;
return $this->_timerIdIndex;
}
return false;
}
/**
* Remove event listener from event loop.
*
* @param mixed $fd
* @param int $flag
* @return bool
*/
public function del($fd, $flag)
{
switch ($flag) {
case EventInterface::EV_READ:
return $this->removeReadStream($fd);
case EventInterface::EV_WRITE:
return $this->removeWriteStream($fd);
case EventInterface::EV_SIGNAL:
return $this->removeSignal($fd);
case EventInterface::EV_TIMER:
case EventInterface::EV_TIMER_ONCE;
if (isset($this->_timerIdMap[$fd])){
$timer_obj = $this->_timerIdMap[$fd];
unset($this->_timerIdMap[$fd]);
$this->cancelTimer($timer_obj);
return true;
}
}
return false;
}
/**
* Main loop.
*
* @return void
*/
public function loop()
{
$this->run();
}
/**
* Add signal handler.
*
* @param $signal
* @param $callback
* @return bool
*/
public function addSignal($signal, $callback)
{
if(DIRECTORY_SEPARATOR === '/') {
pcntl_signal($signal, $callback);
}
}
/**
* Remove signal handler.
*
* @param $signal
*/
public function removeSignal($signal)
{
if(DIRECTORY_SEPARATOR === '/') {
pcntl_signal($signal, SIG_IGN);
}
}
/**
* Emulate a stream_select() implementation that does not break when passed
* empty stream arrays.
*
* @param array &$read An array of read streams to select upon.
* @param array &$write An array of write streams to select upon.
* @param integer|null $timeout Activity timeout in microseconds, or null to wait forever.
*
* @return integer|false The total number of streams that are ready for read/write.
* Can return false if stream_select() is interrupted by a signal.
*/
protected function streamSelect(array &$read, array &$write, $timeout)
{
if ($read || $write) {
$except = null;
// Calls signal handlers for pending signals
if(DIRECTORY_SEPARATOR === '/') {
pcntl_signal_dispatch();
}
// suppress warnings that occur, when stream_select is interrupted by a signal
return @stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout);
}
// Calls signal handlers for pending signals
if(DIRECTORY_SEPARATOR === '/') {
pcntl_signal_dispatch();
}
$timeout && usleep($timeout);
return 0;
}
/**
* Destroy loop.
*
* @return void
*/
public function destroy()
{
}
}

View File

@ -0,0 +1,322 @@
<?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 Workerman\Events;
/**
* select eventloop
*/
class Select implements EventInterface
{
/**
* All listeners for read/write event.
*
* @var array
*/
public $_allEvents = array();
/**
* Event listeners of signal.
*
* @var array
*/
public $_signalEvents = array();
/**
* Fds waiting for read event.
*
* @var array
*/
protected $_readFds = array();
/**
* Fds waiting for write event.
*
* @var array
*/
protected $_writeFds = array();
/**
* Fds waiting for except event.
*
* @var array
*/
protected $_exceptFds = array();
/**
* Timer scheduler.
* {['data':timer_id, 'priority':run_timestamp], ..}
*
* @var \SplPriorityQueue
*/
protected $_scheduler = null;
/**
* All timer event listeners.
* [[func, args, flag, timer_interval], ..]
*
* @var array
*/
protected $_eventTimer = array();
/**
* Timer id.
*
* @var int
*/
protected $_timerId = 1;
/**
* Select timeout.
*
* @var int
*/
protected $_selectTimeout = 100000000;
/**
* Paired socket channels
*
* @var array
*/
protected $channel = array();
/**
* Construct.
*/
public function __construct()
{
// Create a pipeline and put into the collection of the read to read the descriptor to avoid empty polling.
$this->channel = stream_socket_pair(DIRECTORY_SEPARATOR === '/' ? STREAM_PF_UNIX : STREAM_PF_INET,
STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
if($this->channel) {
stream_set_blocking($this->channel[0], 0);
$this->_readFds[0] = $this->channel[0];
}
// Init SplPriorityQueue.
$this->_scheduler = new \SplPriorityQueue();
$this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
}
/**
* {@inheritdoc}
*/
public function add($fd, $flag, $func, $args = array())
{
switch ($flag) {
case self::EV_READ:
$fd_key = (int)$fd;
$this->_allEvents[$fd_key][$flag] = array($func, $fd);
$this->_readFds[$fd_key] = $fd;
break;
case self::EV_WRITE:
$fd_key = (int)$fd;
$this->_allEvents[$fd_key][$flag] = array($func, $fd);
$this->_writeFds[$fd_key] = $fd;
break;
case self::EV_EXCEPT:
$fd_key = (int)$fd;
$this->_allEvents[$fd_key][$flag] = array($func, $fd);
$this->_exceptFds[$fd_key] = $fd;
break;
case self::EV_SIGNAL:
// Windows not support signal.
if(DIRECTORY_SEPARATOR !== '/') {
return false;
}
$fd_key = (int)$fd;
$this->_signalEvents[$fd_key][$flag] = array($func, $fd);
pcntl_signal($fd, array($this, 'signalHandler'));
break;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
$timer_id = $this->_timerId++;
$run_time = microtime(true) + $fd;
$this->_scheduler->insert($timer_id, -$run_time);
$this->_eventTimer[$timer_id] = array($func, (array)$args, $flag, $fd);
$select_timeout = ($run_time - microtime(true)) * 1000000;
if( $this->_selectTimeout > $select_timeout ){
$this->_selectTimeout = $select_timeout;
}
return $timer_id;
}
return true;
}
/**
* Signal handler.
*
* @param int $signal
*/
public function signalHandler($signal)
{
call_user_func_array($this->_signalEvents[$signal][self::EV_SIGNAL][0], array($signal));
}
/**
* {@inheritdoc}
*/
public function del($fd, $flag)
{
$fd_key = (int)$fd;
switch ($flag) {
case self::EV_READ:
unset($this->_allEvents[$fd_key][$flag], $this->_readFds[$fd_key]);
if (empty($this->_allEvents[$fd_key])) {
unset($this->_allEvents[$fd_key]);
}
return true;
case self::EV_WRITE:
unset($this->_allEvents[$fd_key][$flag], $this->_writeFds[$fd_key]);
if (empty($this->_allEvents[$fd_key])) {
unset($this->_allEvents[$fd_key]);
}
return true;
case self::EV_EXCEPT:
unset($this->_allEvents[$fd_key][$flag], $this->_exceptFds[$fd_key]);
if(empty($this->_allEvents[$fd_key]))
{
unset($this->_allEvents[$fd_key]);
}
return true;
case self::EV_SIGNAL:
if(DIRECTORY_SEPARATOR !== '/') {
return false;
}
unset($this->_signalEvents[$fd_key]);
pcntl_signal($fd, SIG_IGN);
break;
case self::EV_TIMER:
case self::EV_TIMER_ONCE;
unset($this->_eventTimer[$fd_key]);
return true;
}
return false;
}
/**
* Tick for timer.
*
* @return void
*/
protected function tick()
{
while (!$this->_scheduler->isEmpty()) {
$scheduler_data = $this->_scheduler->top();
$timer_id = $scheduler_data['data'];
$next_run_time = -$scheduler_data['priority'];
$time_now = microtime(true);
$this->_selectTimeout = ($next_run_time - $time_now) * 1000000;
if ($this->_selectTimeout <= 0) {
$this->_scheduler->extract();
if (!isset($this->_eventTimer[$timer_id])) {
continue;
}
// [func, args, flag, timer_interval]
$task_data = $this->_eventTimer[$timer_id];
if ($task_data[2] === self::EV_TIMER) {
$next_run_time = $time_now + $task_data[3];
$this->_scheduler->insert($timer_id, -$next_run_time);
}
call_user_func_array($task_data[0], $task_data[1]);
if (isset($this->_eventTimer[$timer_id]) && $task_data[2] === self::EV_TIMER_ONCE) {
$this->del($timer_id, self::EV_TIMER_ONCE);
}
continue;
}
return;
}
$this->_selectTimeout = 100000000;
}
/**
* {@inheritdoc}
*/
public function clearAllTimer()
{
$this->_scheduler = new \SplPriorityQueue();
$this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
$this->_eventTimer = array();
}
/**
* {@inheritdoc}
*/
public function loop()
{
$e = null;
while (1) {
if(DIRECTORY_SEPARATOR === '/') {
// Calls signal handlers for pending signals
pcntl_signal_dispatch();
}
$read = $this->_readFds;
$write = $this->_writeFds;
$except = $this->_writeFds;
// Waiting read/write/signal/timeout events.
$ret = @stream_select($read, $write, $except, 0, $this->_selectTimeout);
if (!$this->_scheduler->isEmpty()) {
$this->tick();
}
if (!$ret) {
continue;
}
if ($read) {
foreach ($read as $fd) {
$fd_key = (int)$fd;
if (isset($this->_allEvents[$fd_key][self::EV_READ])) {
call_user_func_array($this->_allEvents[$fd_key][self::EV_READ][0],
array($this->_allEvents[$fd_key][self::EV_READ][1]));
}
}
}
if ($write) {
foreach ($write as $fd) {
$fd_key = (int)$fd;
if (isset($this->_allEvents[$fd_key][self::EV_WRITE])) {
call_user_func_array($this->_allEvents[$fd_key][self::EV_WRITE][0],
array($this->_allEvents[$fd_key][self::EV_WRITE][1]));
}
}
}
if($except) {
foreach($except as $fd) {
$fd_key = (int) $fd;
if(isset($this->_allEvents[$fd_key][self::EV_EXCEPT])) {
call_user_func_array($this->_allEvents[$fd_key][self::EV_EXCEPT][0],
array($this->_allEvents[$fd_key][self::EV_EXCEPT][1]));
}
}
}
}
}
/**
* Destroy loop.
*
* @return void
*/
public function destroy()
{
}
}

View File

@ -0,0 +1,40 @@
<?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
*/
// Date.timezone
if (!ini_get('date.timezone')) {
date_default_timezone_set('Asia/Shanghai');
}
// Display errors.
ini_set('display_errors', 'on');
// Reporting all.
error_reporting(E_ALL);
// Reset opcache.
if (function_exists('opcache_reset')) {
opcache_reset();
}
// For onError callback.
define('WORKERMAN_CONNECT_FAIL', 1);
// For onError callback.
define('WORKERMAN_SEND_FAIL', 2);
// Compatible with php7
if(!class_exists('Error'))
{
class Error extends Exception
{
}
}

View File

@ -0,0 +1,176 @@
<?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 Workerman\Lib;
use Workerman\Events\EventInterface;
use Exception;
/**
* Timer.
*
* example:
* Workerman\Lib\Timer::add($time_interval, callback, array($arg1, $arg2..));
*/
class Timer
{
/**
* Tasks that based on ALARM signal.
* [
* run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
* run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
* ..
* ]
*
* @var array
*/
protected static $_tasks = array();
/**
* event
*
* @var \Workerman\Events\EventInterface
*/
protected static $_event = null;
/**
* Init.
*
* @param \Workerman\Events\EventInterface $event
* @return void
*/
public static function init($event = null)
{
if ($event) {
self::$_event = $event;
} else {
pcntl_signal(SIGALRM, array('\Workerman\Lib\Timer', 'signalHandle'), false);
}
}
/**
* ALARM signal handler.
*
* @return void
*/
public static function signalHandle()
{
if (!self::$_event) {
pcntl_alarm(1);
self::tick();
}
}
/**
* Add a timer.
*
* @param int $time_interval
* @param callback $func
* @param mixed $args
* @param bool $persistent
* @return int/false
*/
public static function add($time_interval, $func, $args = array(), $persistent = true)
{
if ($time_interval <= 0) {
echo new Exception("bad time_interval");
return false;
}
if (self::$_event) {
return self::$_event->add($time_interval,
$persistent ? EventInterface::EV_TIMER : EventInterface::EV_TIMER_ONCE, $func, $args);
}
if (!is_callable($func)) {
echo new Exception("not callable");
return false;
}
if (empty(self::$_tasks)) {
pcntl_alarm(1);
}
$time_now = time();
$run_time = $time_now + $time_interval;
if (!isset(self::$_tasks[$run_time])) {
self::$_tasks[$run_time] = array();
}
self::$_tasks[$run_time][] = array($func, (array)$args, $persistent, $time_interval);
return 1;
}
/**
* Tick.
*
* @return void
*/
public static function tick()
{
if (empty(self::$_tasks)) {
pcntl_alarm(0);
return;
}
$time_now = time();
foreach (self::$_tasks as $run_time => $task_data) {
if ($time_now >= $run_time) {
foreach ($task_data as $index => $one_task) {
$task_func = $one_task[0];
$task_args = $one_task[1];
$persistent = $one_task[2];
$time_interval = $one_task[3];
try {
call_user_func_array($task_func, $task_args);
} catch (\Exception $e) {
echo $e;
}
if ($persistent) {
self::add($time_interval, $task_func, $task_args);
}
}
unset(self::$_tasks[$run_time]);
}
}
}
/**
* Remove a timer.
*
* @param mixed $timer_id
* @return bool
*/
public static function del($timer_id)
{
if (self::$_event) {
return self::$_event->del($timer_id, EventInterface::EV_TIMER);
}
return false;
}
/**
* Remove all timers.
*
* @return void
*/
public static function delAll()
{
self::$_tasks = array();
pcntl_alarm(0);
if (self::$_event) {
self::$_event->clearAllTimer();
}
}
}

View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2009-2015 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/workerman/contributors)
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,61 @@
<?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 Workerman\Protocols;
use Workerman\Connection\TcpConnection;
/**
* Frame Protocol.
*/
class Frame
{
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param TcpConnection $connection
* @return int
*/
public static function input($buffer, TcpConnection $connection)
{
if (strlen($buffer) < 4) {
return 0;
}
$unpack_data = unpack('Ntotal_length', $buffer);
return $unpack_data['total_length'];
}
/**
* Decode.
*
* @param string $buffer
* @return string
*/
public static function decode($buffer)
{
return substr($buffer, 4);
}
/**
* Encode.
*
* @param string $buffer
* @return string
*/
public static function encode($buffer)
{
$total_length = 4 + strlen($buffer);
return pack('N', $total_length) . $buffer;
}
}

View File

@ -0,0 +1,585 @@
<?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 Workerman\Protocols;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
/**
* http protocol
*/
class Http
{
/**
* The supported HTTP methods
* @var array
*/
public static $methods = array('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS');
/**
* Check the integrity of the package.
*
* @param string $recv_buffer
* @param TcpConnection $connection
* @return int
*/
public static function input($recv_buffer, TcpConnection $connection)
{
if (!strpos($recv_buffer, "\r\n\r\n")) {
// Judge whether the package length exceeds the limit.
if (strlen($recv_buffer) >= TcpConnection::$maxPackageSize) {
$connection->close();
return 0;
}
return 0;
}
list($header,) = explode("\r\n\r\n", $recv_buffer, 2);
$method = substr($header, 0, strpos($header, ' '));
if(in_array($method, static::$methods)) {
return static::getRequestSize($header, $method);
}else{
$connection->send("HTTP/1.1 400 Bad Request\r\n\r\n", true);
return 0;
}
}
/**
* Get whole size of the request
* includes the request headers and request body.
* @param string $header The request headers
* @param string $method The request method
* @return integer
*/
protected static function getRequestSize($header, $method)
{
if($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD') {
return strlen($header) + 4;
}
$match = array();
if (preg_match("/\r\nContent-Length: ?(\d+)/i", $header, $match)) {
$content_length = isset($match[1]) ? $match[1] : 0;
return $content_length + strlen($header) + 4;
}
return 0;
}
/**
* Parse $_POST、$_GET、$_COOKIE.
*
* @param string $recv_buffer
* @param TcpConnection $connection
* @return array
*/
public static function decode($recv_buffer, TcpConnection $connection)
{
// Init.
$_POST = $_GET = $_COOKIE = $_REQUEST = $_SESSION = $_FILES = array();
$GLOBALS['HTTP_RAW_POST_DATA'] = '';
// Clear cache.
HttpCache::$header = array('Connection' => 'Connection: keep-alive');
HttpCache::$instance = new HttpCache();
// $_SERVER
$_SERVER = array(
'QUERY_STRING' => '',
'REQUEST_METHOD' => '',
'REQUEST_URI' => '',
'SERVER_PROTOCOL' => '',
'SERVER_SOFTWARE' => 'workerman/'.Worker::VERSION,
'SERVER_NAME' => '',
'HTTP_HOST' => '',
'HTTP_USER_AGENT' => '',
'HTTP_ACCEPT' => '',
'HTTP_ACCEPT_LANGUAGE' => '',
'HTTP_ACCEPT_ENCODING' => '',
'HTTP_COOKIE' => '',
'HTTP_CONNECTION' => '',
'REMOTE_ADDR' => '',
'REMOTE_PORT' => '0',
'REQUEST_TIME' => time()
);
// Parse headers.
list($http_header, $http_body) = explode("\r\n\r\n", $recv_buffer, 2);
$header_data = explode("\r\n", $http_header);
list($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']) = explode(' ',
$header_data[0]);
$http_post_boundary = '';
unset($header_data[0]);
foreach ($header_data as $content) {
// \r\n\r\n
if (empty($content)) {
continue;
}
list($key, $value) = explode(':', $content, 2);
$key = str_replace('-', '_', strtoupper($key));
$value = trim($value);
$_SERVER['HTTP_' . $key] = $value;
switch ($key) {
// HTTP_HOST
case 'HOST':
$tmp = explode(':', $value);
$_SERVER['SERVER_NAME'] = $tmp[0];
if (isset($tmp[1])) {
$_SERVER['SERVER_PORT'] = $tmp[1];
}
break;
// cookie
case 'COOKIE':
parse_str(str_replace('; ', '&', $_SERVER['HTTP_COOKIE']), $_COOKIE);
break;
// content-type
case 'CONTENT_TYPE':
if (!preg_match('/boundary="?(\S+)"?/', $value, $match)) {
if ($pos = strpos($value, ';')) {
$_SERVER['CONTENT_TYPE'] = substr($value, 0, $pos);
} else {
$_SERVER['CONTENT_TYPE'] = $value;
}
} else {
$_SERVER['CONTENT_TYPE'] = 'multipart/form-data';
$http_post_boundary = '--' . $match[1];
}
break;
case 'CONTENT_LENGTH':
$_SERVER['CONTENT_LENGTH'] = $value;
break;
}
}
// Parse $_POST.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_SERVER['CONTENT_TYPE'])) {
switch ($_SERVER['CONTENT_TYPE']) {
case 'multipart/form-data':
self::parseUploadFiles($http_body, $http_post_boundary);
break;
case 'application/x-www-form-urlencoded':
parse_str($http_body, $_POST);
break;
}
}
}
// HTTP_RAW_REQUEST_DATA HTTP_RAW_POST_DATA
$GLOBALS['HTTP_RAW_REQUEST_DATA'] = $GLOBALS['HTTP_RAW_POST_DATA'] = $http_body;
// QUERY_STRING
$_SERVER['QUERY_STRING'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
if ($_SERVER['QUERY_STRING']) {
// $GET
parse_str($_SERVER['QUERY_STRING'], $_GET);
} else {
$_SERVER['QUERY_STRING'] = '';
}
// REQUEST
$_REQUEST = array_merge($_GET, $_POST);
// REMOTE_ADDR REMOTE_PORT
$_SERVER['REMOTE_ADDR'] = $connection->getRemoteIp();
$_SERVER['REMOTE_PORT'] = $connection->getRemotePort();
return array('get' => $_GET, 'post' => $_POST, 'cookie' => $_COOKIE, 'server' => $_SERVER, 'files' => $_FILES);
}
/**
* Http encode.
*
* @param string $content
* @param TcpConnection $connection
* @return string
*/
public static function encode($content, TcpConnection $connection)
{
// Default http-code.
if (!isset(HttpCache::$header['Http-Code'])) {
$header = "HTTP/1.1 200 OK\r\n";
} else {
$header = HttpCache::$header['Http-Code'] . "\r\n";
unset(HttpCache::$header['Http-Code']);
}
// Content-Type
if (!isset(HttpCache::$header['Content-Type'])) {
$header .= "Content-Type: text/html;charset=utf-8\r\n";
}
// other headers
foreach (HttpCache::$header as $key => $item) {
if ('Set-Cookie' === $key && is_array($item)) {
foreach ($item as $it) {
$header .= $it . "\r\n";
}
} else {
$header .= $item . "\r\n";
}
}
// header
$header .= "Server: workerman/" . Worker::VERSION . "\r\nContent-Length: " . strlen($content) . "\r\n\r\n";
// save session
self::sessionWriteClose();
// the whole http package
return $header . $content;
}
/**
* 设置http头
*
* @return bool|void
*/
public static function header($content, $replace = true, $http_response_code = 0)
{
if (PHP_SAPI != 'cli') {
return $http_response_code ? header($content, $replace, $http_response_code) : header($content, $replace);
}
if (strpos($content, 'HTTP') === 0) {
$key = 'Http-Code';
} else {
$key = strstr($content, ":", true);
if (empty($key)) {
return false;
}
}
if ('location' === strtolower($key) && !$http_response_code) {
return self::header($content, true, 302);
}
if (isset(HttpCache::$codes[$http_response_code])) {
HttpCache::$header['Http-Code'] = "HTTP/1.1 $http_response_code " . HttpCache::$codes[$http_response_code];
if ($key === 'Http-Code') {
return true;
}
}
if ($key === 'Set-Cookie') {
HttpCache::$header[$key][] = $content;
} else {
HttpCache::$header[$key] = $content;
}
return true;
}
/**
* Remove header.
*
* @param string $name
* @return void
*/
public static function headerRemove($name)
{
if (PHP_SAPI != 'cli') {
header_remove($name);
return;
}
unset(HttpCache::$header[$name]);
}
/**
* Set cookie.
*
* @param string $name
* @param string $value
* @param integer $maxage
* @param string $path
* @param string $domain
* @param bool $secure
* @param bool $HTTPOnly
* @return bool|void
*/
public static function setcookie(
$name,
$value = '',
$maxage = 0,
$path = '',
$domain = '',
$secure = false,
$HTTPOnly = false
) {
if (PHP_SAPI != 'cli') {
return setcookie($name, $value, $maxage, $path, $domain, $secure, $HTTPOnly);
}
return self::header(
'Set-Cookie: ' . $name . '=' . rawurlencode($value)
. (empty($domain) ? '' : '; Domain=' . $domain)
. (empty($maxage) ? '' : '; Max-Age=' . $maxage)
. (empty($path) ? '' : '; Path=' . $path)
. (!$secure ? '' : '; Secure')
. (!$HTTPOnly ? '' : '; HttpOnly'), false);
}
/**
* sessionStart
*
* @return bool
*/
public static function sessionStart()
{
if (PHP_SAPI != 'cli') {
return session_start();
}
self::tryGcSessions();
if (HttpCache::$instance->sessionStarted) {
echo "already sessionStarted\n";
return true;
}
HttpCache::$instance->sessionStarted = true;
// Generate a SID.
if (!isset($_COOKIE[HttpCache::$sessionName]) || !is_file(HttpCache::$sessionPath . '/ses' . $_COOKIE[HttpCache::$sessionName])) {
$file_name = tempnam(HttpCache::$sessionPath, 'ses');
if (!$file_name) {
return false;
}
HttpCache::$instance->sessionFile = $file_name;
$session_id = substr(basename($file_name), strlen('ses'));
return self::setcookie(
HttpCache::$sessionName
, $session_id
, ini_get('session.cookie_lifetime')
, ini_get('session.cookie_path')
, ini_get('session.cookie_domain')
, ini_get('session.cookie_secure')
, ini_get('session.cookie_httponly')
);
}
if (!HttpCache::$instance->sessionFile) {
HttpCache::$instance->sessionFile = HttpCache::$sessionPath . '/ses' . $_COOKIE[HttpCache::$sessionName];
}
// Read session from session file.
if (HttpCache::$instance->sessionFile) {
$raw = file_get_contents(HttpCache::$instance->sessionFile);
if ($raw) {
$_SESSION = unserialize($raw);
}
}
return true;
}
/**
* Save session.
*
* @return bool
*/
public static function sessionWriteClose()
{
if (PHP_SAPI != 'cli') {
return session_write_close();
}
if (!empty(HttpCache::$instance->sessionStarted) && !empty($_SESSION)) {
$session_str = serialize($_SESSION);
if ($session_str && HttpCache::$instance->sessionFile) {
return file_put_contents(HttpCache::$instance->sessionFile, $session_str);
}
}
return empty($_SESSION);
}
/**
* End, like call exit in php-fpm.
*
* @param string $msg
* @throws \Exception
*/
public static function end($msg = '')
{
if (PHP_SAPI != 'cli') {
exit($msg);
}
if ($msg) {
echo $msg;
}
throw new \Exception('jump_exit');
}
/**
* Get mime types.
*
* @return string
*/
public static function getMimeTypesFile()
{
return __DIR__ . '/Http/mime.types';
}
/**
* Parse $_FILES.
*
* @param string $http_body
* @param string $http_post_boundary
* @return void
*/
protected static function parseUploadFiles($http_body, $http_post_boundary)
{
$http_body = substr($http_body, 0, strlen($http_body) - (strlen($http_post_boundary) + 4));
$boundary_data_array = explode($http_post_boundary . "\r\n", $http_body);
if ($boundary_data_array[0] === '') {
unset($boundary_data_array[0]);
}
$key = -1;
foreach ($boundary_data_array as $boundary_data_buffer) {
list($boundary_header_buffer, $boundary_value) = explode("\r\n\r\n", $boundary_data_buffer, 2);
// Remove \r\n from the end of buffer.
$boundary_value = substr($boundary_value, 0, -2);
$key ++;
foreach (explode("\r\n", $boundary_header_buffer) as $item) {
list($header_key, $header_value) = explode(": ", $item);
$header_key = strtolower($header_key);
switch ($header_key) {
case "content-disposition":
// Is file data.
if (preg_match('/name="(.*?)"; filename="(.*?)"$/', $header_value, $match)) {
// Parse $_FILES.
$_FILES[$key] = array(
'name' => $match[1],
'file_name' => $match[2],
'file_data' => $boundary_value,
'file_size' => strlen($boundary_value),
);
continue;
} // Is post field.
else {
// Parse $_POST.
if (preg_match('/name="(.*?)"$/', $header_value, $match)) {
$_POST[$match[1]] = $boundary_value;
}
}
break;
case "content-type":
// add file_type
$_FILES[$key]['file_type'] = trim($header_value);
break;
}
}
}
}
/**
* Try GC sessions.
*
* @return void
*/
public static function tryGcSessions()
{
if (HttpCache::$sessionGcProbability <= 0 ||
HttpCache::$sessionGcDivisor <= 0 ||
rand(1, HttpCache::$sessionGcDivisor) > HttpCache::$sessionGcProbability) {
return;
}
$time_now = time();
foreach(glob(HttpCache::$sessionPath.'/ses*') as $file) {
if(is_file($file) && $time_now - filemtime($file) > HttpCache::$sessionGcMaxLifeTime) {
unlink($file);
}
}
}
}
/**
* Http cache for the current http response.
*/
class HttpCache
{
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',
);
/**
* @var HttpCache
*/
public static $instance = null;
public static $header = array();
public static $sessionPath = '';
public static $sessionName = '';
public static $sessionGcProbability = 1;
public static $sessionGcDivisor = 1000;
public static $sessionGcMaxLifeTime = 1440;
public $sessionStarted = false;
public $sessionFile = '';
public static function init()
{
self::$sessionName = ini_get('session.name');
self::$sessionPath = @session_save_path();
if (!self::$sessionPath || strpos(self::$sessionPath, 'tcp://') === 0) {
self::$sessionPath = sys_get_temp_dir();
}
if ($gc_probability = ini_get('session.gc_probability')) {
self::$sessionGcProbability = $gc_probability;
}
if ($gc_divisor = ini_get('session.gc_divisor')) {
self::$sessionGcDivisor = $gc_divisor;
}
if ($gc_max_life_time = ini_get('session.gc_maxlifetime')) {
self::$sessionGcMaxLifeTime = $gc_max_life_time;
}
}
}
HttpCache::init();

View File

@ -0,0 +1,80 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/x-javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/java-archive jar war ear;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.ms-excel xls;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream eot;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@ -0,0 +1,52 @@
<?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 Workerman\Protocols;
use Workerman\Connection\ConnectionInterface;
/**
* Protocol interface
*/
interface ProtocolInterface
{
/**
* Check the integrity of the package.
* Please return the length of package.
* If length is unknow please return 0 that mean wating more data.
* If the package has something wrong please return false the connection will be closed.
*
* @param ConnectionInterface $connection
* @param string $recv_buffer
* @return int|false
*/
public static function input($recv_buffer, ConnectionInterface $connection);
/**
* Decode package and emit onMessage($message) callback, $message is the result that decode returned.
*
* @param ConnectionInterface $connection
* @param string $recv_buffer
* @return mixed
*/
public static function decode($recv_buffer, ConnectionInterface $connection);
/**
* Encode package brefore sending to client.
*
* @param ConnectionInterface $connection
* @param mixed $data
* @return string
*/
public static function encode($data, ConnectionInterface $connection);
}

View File

@ -0,0 +1,70 @@
<?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 Workerman\Protocols;
use Workerman\Connection\TcpConnection;
/**
* Text Protocol.
*/
class Text
{
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param TcpConnection $connection
* @return int
*/
public static function input($buffer, TcpConnection $connection)
{
// Judge whether the package length exceeds the limit.
if (strlen($buffer) >= TcpConnection::$maxPackageSize) {
$connection->close();
return 0;
}
// Find the position of "\n".
$pos = strpos($buffer, "\n");
// No "\n", packet length is unknown, continue to wait for the data so return 0.
if ($pos === false) {
return 0;
}
// Return the current package length.
return $pos + 1;
}
/**
* Encode.
*
* @param string $buffer
* @return string
*/
public static function encode($buffer)
{
// Add "\n"
return $buffer . "\n";
}
/**
* Decode.
*
* @param string $buffer
* @return string
*/
public static function decode($buffer)
{
// Remove "\n"
return trim($buffer);
}
}

View File

@ -0,0 +1,473 @@
<?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 Workerman\Protocols;
use Workerman\Connection\ConnectionInterface;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
/**
* WebSocket protocol.
*/
class Websocket implements \Workerman\Protocols\ProtocolInterface
{
/**
* Websocket blob type.
*
* @var string
*/
const BINARY_TYPE_BLOB = "\x81";
/**
* Websocket arraybuffer type.
*
* @var string
*/
const BINARY_TYPE_ARRAYBUFFER = "\x82";
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return int
*/
public static function input($buffer, ConnectionInterface $connection)
{
// Receive length.
$recv_len = strlen($buffer);
// We need more data.
if ($recv_len < 2) {
return 0;
}
// Has not yet completed the handshake.
if (empty($connection->websocketHandshake)) {
return static::dealHandshake($buffer, $connection);
}
// Buffer websocket frame data.
if ($connection->websocketCurrentFrameLength) {
// We need more frame data.
if ($connection->websocketCurrentFrameLength > $recv_len) {
// Return 0, because it is not clear the full packet length, waiting for the frame of fin=1.
return 0;
}
} else {
$firstbyte = ord($buffer[0]);
$secondbyte = ord($buffer[1]);
$data_len = $secondbyte & 127;
$is_fin_frame = $firstbyte >> 7;
$masked = $secondbyte >> 7;
$opcode = $firstbyte & 0xf;
switch ($opcode) {
case 0x0:
break;
// Blob type.
case 0x1:
break;
// Arraybuffer type.
case 0x2:
break;
// Close package.
case 0x8:
// Try to emit onWebSocketClose callback.
if (isset($connection->onWebSocketClose)) {
try {
call_user_func($connection->onWebSocketClose, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
} // Close connection.
else {
$connection->close();
}
return 0;
// Ping package.
case 0x9:
// Try to emit onWebSocketPing callback.
if (isset($connection->onWebSocketPing)) {
try {
call_user_func($connection->onWebSocketPing, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
} // Send pong package to client.
else {
$connection->send(pack('H*', '8a00'), true);
}
// Consume data from receive buffer.
if (!$data_len) {
$head_len = $masked ? 6 : 2;
$connection->consumeRecvBuffer($head_len);
if ($recv_len > $head_len) {
return static::input(substr($buffer, $head_len), $connection);
}
return 0;
}
break;
// Pong package.
case 0xa:
// Try to emit onWebSocketPong callback.
if (isset($connection->onWebSocketPong)) {
try {
call_user_func($connection->onWebSocketPong, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
// Consume data from receive buffer.
if (!$data_len) {
$head_len = $masked ? 6 : 2;
$connection->consumeRecvBuffer($head_len);
if ($recv_len > $head_len) {
return static::input(substr($buffer, $head_len), $connection);
}
return 0;
}
break;
// Wrong opcode.
default :
echo "error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n";
$connection->close();
return 0;
}
// Calculate packet length.
$head_len = 6;
if ($data_len === 126) {
$head_len = 8;
if ($head_len > $recv_len) {
return 0;
}
$pack = unpack('nn/ntotal_len', $buffer);
$data_len = $pack['total_len'];
} else {
if ($data_len === 127) {
$head_len = 14;
if ($head_len > $recv_len) {
return 0;
}
$arr = unpack('n/N2c', $buffer);
$data_len = $arr['c1']*4294967296 + $arr['c2'];
}
}
$current_frame_length = $head_len + $data_len;
$total_package_size = strlen($connection->websocketDataBuffer) + $current_frame_length;
if ($total_package_size > TcpConnection::$maxPackageSize) {
echo "error package. package_length=$total_package_size\n";
$connection->close();
return 0;
}
if ($is_fin_frame) {
return $current_frame_length;
} else {
$connection->websocketCurrentFrameLength = $current_frame_length;
}
}
// Received just a frame length data.
if ($connection->websocketCurrentFrameLength === $recv_len) {
static::decode($buffer, $connection);
$connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
$connection->websocketCurrentFrameLength = 0;
return 0;
} // The length of the received data is greater than the length of a frame.
elseif ($connection->websocketCurrentFrameLength < $recv_len) {
static::decode(substr($buffer, 0, $connection->websocketCurrentFrameLength), $connection);
$connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
$current_frame_length = $connection->websocketCurrentFrameLength;
$connection->websocketCurrentFrameLength = 0;
// Continue to read next frame.
return static::input(substr($buffer, $current_frame_length), $connection);
} // The length of the received data is less than the length of a frame.
else {
return 0;
}
}
/**
* Websocket encode.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return string
*/
public static function encode($buffer, ConnectionInterface $connection)
{
if (!is_scalar($buffer)) {
throw new \Exception("You can't send(" . gettype($buffer) . ") to client, you need to convert it to a string. ");
}
$len = strlen($buffer);
if (empty($connection->websocketType)) {
$connection->websocketType = static::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;
}
}
// Handshake not completed so temporary buffer websocket data waiting for send.
if (empty($connection->websocketHandshake)) {
if (empty($connection->tmpWebsocketData)) {
$connection->tmpWebsocketData = '';
}
// If buffer has already full then discard the current package.
if (strlen($connection->tmpWebsocketData) > $connection->maxSendBufferSize) {
if ($connection->onError) {
try {
call_user_func($connection->onError, $connection, WORKERMAN_SEND_FAIL, 'send buffer full and drop package');
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
return '';
}
$connection->tmpWebsocketData .= $encode_buffer;
// Check buffer is full.
if ($connection->maxSendBufferSize <= strlen($connection->tmpWebsocketData)) {
if ($connection->onBufferFull) {
try {
call_user_func($connection->onBufferFull, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
}
// Return empty string.
return '';
}
return $encode_buffer;
}
/**
* Websocket decode.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return string
*/
public static function decode($buffer, ConnectionInterface $connection)
{
$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 {
if ($connection->websocketDataBuffer !== '') {
$decoded = $connection->websocketDataBuffer . $decoded;
$connection->websocketDataBuffer = '';
}
return $decoded;
}
}
/**
* Websocket handshake.
*
* @param string $buffer
* @param \Workerman\Connection\TcpConnection $connection
* @return int
*/
protected static function dealHandshake($buffer, $connection)
{
// HTTP protocol.
if (0 === strpos($buffer, 'GET')) {
// Find \r\n\r\n.
$heder_end_pos = strpos($buffer, "\r\n\r\n");
if (!$heder_end_pos) {
return 0;
}
$header_length = $heder_end_pos + 4;
// Get Sec-WebSocket-Key.
$Sec_WebSocket_Key = '';
if (preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) {
$Sec_WebSocket_Key = $match[1];
} else {
$connection->send("HTTP/1.1 400 Bad Request\r\n\r\n<b>400 Bad Request</b><br>Sec-WebSocket-Key not found.<br>This is a WebSocket service and can not be accessed via HTTP.<br>See <a href=\"http://wiki.workerman.net/Error1\">http://wiki.workerman.net/Error1</a> for detail.",
true);
$connection->close();
return 0;
}
// Calculation websocket key.
$new_key = base64_encode(sha1($Sec_WebSocket_Key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
// Handshake response data.
$handshake_message = "HTTP/1.1 101 Switching Protocols\r\n";
$handshake_message .= "Upgrade: websocket\r\n";
$handshake_message .= "Sec-WebSocket-Version: 13\r\n";
$handshake_message .= "Connection: Upgrade\r\n";
$handshake_message .= "Server: workerman/".Worker::VERSION."\r\n";
$handshake_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
// Mark handshake complete..
$connection->websocketHandshake = true;
// Websocket data buffer.
$connection->websocketDataBuffer = '';
// Current websocket frame length.
$connection->websocketCurrentFrameLength = 0;
// Current websocket frame data.
$connection->websocketCurrentFrameBuffer = '';
// Consume handshake data.
$connection->consumeRecvBuffer($header_length);
// Send handshake response.
$connection->send($handshake_message, true);
// There are data waiting to be sent.
if (!empty($connection->tmpWebsocketData)) {
$connection->send($connection->tmpWebsocketData, true);
$connection->tmpWebsocketData = '';
}
// blob or arraybuffer
if (empty($connection->websocketType)) {
$connection->websocketType = static::BINARY_TYPE_BLOB;
}
// Try to emit onWebSocketConnect callback.
if (isset($connection->onWebSocketConnect)) {
static::parseHttpHeader($buffer);
try {
call_user_func($connection->onWebSocketConnect, $connection, $buffer);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
if (!empty($_SESSION) && class_exists('\GatewayWorker\Lib\Context')) {
$connection->session = \GatewayWorker\Lib\Context::sessionEncode($_SESSION);
}
$_GET = $_SERVER = $_SESSION = $_COOKIE = array();
}
if (strlen($buffer) > $header_length) {
return static::input(substr($buffer, $header_length), $connection);
}
return 0;
} // Is flash policy-file-request.
elseif (0 === strpos($buffer, '<polic')) {
$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;
}
// Bad websocket handshake request.
$connection->send("HTTP/1.1 400 Bad Request\r\n\r\n<b>400 Bad Request</b><br>Invalid handshake data for websocket. <br> See <a href=\"http://wiki.workerman.net/Error1\">http://wiki.workerman.net/Error1</a> for detail.",
true);
$connection->close();
return 0;
}
/**
* Parse http header.
*
* @param string $buffer
* @return void
*/
protected static function parseHttpHeader($buffer)
{
// Parse headers.
list($http_header, ) = explode("\r\n\r\n", $buffer, 2);
$header_data = explode("\r\n", $http_header);
if ($_SERVER) {
$_SERVER = array();
}
list($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']) = explode(' ',
$header_data[0]);
unset($header_data[0]);
foreach ($header_data as $content) {
// \r\n\r\n
if (empty($content)) {
continue;
}
list($key, $value) = explode(':', $content, 2);
$key = str_replace('-', '_', strtoupper($key));
$value = trim($value);
$_SERVER['HTTP_' . $key] = $value;
switch ($key) {
// HTTP_HOST
case 'HOST':
$tmp = explode(':', $value);
$_SERVER['SERVER_NAME'] = $tmp[0];
if (isset($tmp[1])) {
$_SERVER['SERVER_PORT'] = $tmp[1];
}
break;
// cookie
case 'COOKIE':
parse_str(str_replace('; ', '&', $_SERVER['HTTP_COOKIE']), $_COOKIE);
break;
}
}
// QUERY_STRING
$_SERVER['QUERY_STRING'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY);
if ($_SERVER['QUERY_STRING']) {
// $GET
parse_str($_SERVER['QUERY_STRING'], $_GET);
} else {
$_SERVER['QUERY_STRING'] = '';
}
}
}

View File

@ -0,0 +1,433 @@
<?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 Workerman\Protocols;
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\TcpConnection;
/**
* Websocket protocol for client.
*/
class Ws
{
/**
* Websocket blob type.
*
* @var string
*/
const BINARY_TYPE_BLOB = "\x81";
/**
* Websocket arraybuffer type.
*
* @var string
*/
const BINARY_TYPE_ARRAYBUFFER = "\x82";
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return int
*/
public static function input($buffer, $connection)
{
if (empty($connection->handshakeStep)) {
echo "recv data before handshake. Buffer:" . bin2hex($buffer) . "\n";
return false;
}
// Recv handshake response
if ($connection->handshakeStep === 1) {
return self::dealHandshake($buffer, $connection);
}
$recv_len = strlen($buffer);
if ($recv_len < 2) {
return 0;
}
// Buffer websocket frame data.
if ($connection->websocketCurrentFrameLength) {
// We need more frame data.
if ($connection->websocketCurrentFrameLength > $recv_len) {
// Return 0, because it is not clear the full packet length, waiting for the frame of fin=1.
return 0;
}
} else {
$firstbyte = ord($buffer[0]);
$secondbyte = ord($buffer[1]);
$data_len = $secondbyte & 127;
$is_fin_frame = $firstbyte >> 7;
$masked = $secondbyte >> 7;
$opcode = $firstbyte & 0xf;
switch ($opcode) {
case 0x0:
break;
// Blob type.
case 0x1:
break;
// Arraybuffer type.
case 0x2:
break;
// Close package.
case 0x8:
// Try to emit onWebSocketClose callback.
if (isset($connection->onWebSocketClose)) {
try {
call_user_func($connection->onWebSocketClose, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
} // Close connection.
else {
$connection->close();
}
return 0;
// Ping package.
case 0x9:
// Try to emit onWebSocketPing callback.
if (isset($connection->onWebSocketPing)) {
try {
call_user_func($connection->onWebSocketPing, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
} // Send pong package to client.
else {
$connection->send(pack('H*', '8a00'), true);
}
// Consume data from receive buffer.
if (!$data_len) {
$head_len = $masked ? 6 : 2;
$connection->consumeRecvBuffer($head_len);
if ($recv_len > $head_len) {
return self::input(substr($buffer, $head_len), $connection);
}
return 0;
}
break;
// Pong package.
case 0xa:
// Try to emit onWebSocketPong callback.
if (isset($connection->onWebSocketPong)) {
try {
call_user_func($connection->onWebSocketPong, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
// Consume data from receive buffer.
if (!$data_len) {
$head_len = $masked ? 6 : 2;
$connection->consumeRecvBuffer($head_len);
if ($recv_len > $head_len) {
return self::input(substr($buffer, $head_len), $connection);
}
return 0;
}
break;
// Wrong opcode.
default :
echo "error opcode $opcode and close websocket connection. Buffer:" . $buffer . "\n";
$connection->close();
return 0;
}
// Calculate packet length.
if ($data_len === 126) {
if (strlen($buffer) < 6) {
return 0;
}
$pack = unpack('nn/ntotal_len', $buffer);
$current_frame_length = $pack['total_len'] + 4;
} else if ($data_len === 127) {
if (strlen($buffer) < 10) {
return 0;
}
$arr = unpack('n/N2c', $buffer);
$current_frame_length = $arr['c1']*4294967296 + $arr['c2'] + 10;
} else {
$current_frame_length = $data_len + 2;
}
$total_package_size = strlen($connection->websocketDataBuffer) + $current_frame_length;
if ($total_package_size > TcpConnection::$maxPackageSize) {
echo "error package. package_length=$total_package_size\n";
$connection->close();
return 0;
}
if ($is_fin_frame) {
return $current_frame_length;
} else {
$connection->websocketCurrentFrameLength = $current_frame_length;
}
}
// Received just a frame length data.
if ($connection->websocketCurrentFrameLength === $recv_len) {
self::decode($buffer, $connection);
$connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
$connection->websocketCurrentFrameLength = 0;
return 0;
} // The length of the received data is greater than the length of a 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;
// Continue to read next frame.
return self::input(substr($buffer, $current_frame_length), $connection);
} // The length of the received data is less than the length of a frame.
else {
return 0;
}
}
/**
* Websocket encode.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return string
*/
public static function encode($payload, $connection)
{
if (empty($connection->websocketType)) {
$connection->websocketType = self::BINARY_TYPE_BLOB;
}
$payload = (string)$payload;
if (empty($connection->handshakeStep)) {
self::sendHandshake($connection);
}
$mask = 1;
$mask_key = "\x00\x00\x00\x00";
$pack = '';
$length = $length_flag = strlen($payload);
if (65535 < $length) {
$pack = pack('NN', ($length & 0xFFFFFFFF00000000) >> 32, $length & 0x00000000FFFFFFFF);
$length_flag = 127;
} else if (125 < $length) {
$pack = pack('n*', $length);
$length_flag = 126;
}
$head = ($mask << 7) | $length_flag;
$head = $connection->websocketType . chr($head) . $pack;
$frame = $head . $mask_key;
// append payload to frame:
for ($i = 0; $i < $length; $i++) {
$frame .= $payload[$i] ^ $mask_key[$i % 4];
}
if ($connection->handshakeStep === 1) {
// If buffer has already full then discard the current package.
if (strlen($connection->tmpWebsocketData) > $connection->maxSendBufferSize) {
if ($connection->onError) {
try {
call_user_func($connection->onError, $connection, WORKERMAN_SEND_FAIL, 'send buffer full and drop package');
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
return '';
}
$connection->tmpWebsocketData = $connection->tmpWebsocketData . $frame;
// Check buffer is full.
if ($connection->maxSendBufferSize <= strlen($connection->tmpWebsocketData)) {
if ($connection->onBufferFull) {
try {
call_user_func($connection->onBufferFull, $connection);
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
}
return '';
}
return $frame;
}
/**
* Websocket decode.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return string
*/
public static function decode($bytes, $connection)
{
$masked = ord($bytes[1]) >> 7;
$data_length = $masked ? ord($bytes[1]) & 127 : ord($bytes[1]);
$decoded_data = '';
if ($masked === true) {
if ($data_length === 126) {
$mask = substr($bytes, 4, 4);
$coded_data = substr($bytes, 8);
} else if ($data_length === 127) {
$mask = substr($bytes, 10, 4);
$coded_data = substr($bytes, 14);
} else {
$mask = substr($bytes, 2, 4);
$coded_data = substr($bytes, 6);
}
for ($i = 0; $i < strlen($coded_data); $i++) {
$decoded_data .= $coded_data[$i] ^ $mask[$i % 4];
}
} else {
if ($data_length === 126) {
$decoded_data = substr($bytes, 4);
} else if ($data_length === 127) {
$decoded_data = substr($bytes, 10);
} else {
$decoded_data = substr($bytes, 2);
}
}
if ($connection->websocketCurrentFrameLength) {
$connection->websocketDataBuffer .= $decoded_data;
return $connection->websocketDataBuffer;
} else {
if ($connection->websocketDataBuffer !== '') {
$decoded_data = $connection->websocketDataBuffer . $decoded_data;
$connection->websocketDataBuffer = '';
}
return $decoded_data;
}
}
/**
* Send websocket handshake data.
*
* @return void
*/
public static function onConnect($connection)
{
self::sendHandshake($connection);
}
/**
* Clean
*
* @param $connection
*/
public static function onClose($connection)
{
$connection->handshakeStep = null;
$connection->websocketCurrentFrameLength = 0;
$connection->tmpWebsocketData = '';
$connection->websocketDataBuffer = '';
if (!empty($connection->websocketPingTimer)) {
Timer::del($connection->websocketPingTimer);
$connection->websocketPingTimer = null;
}
}
/**
* Send websocket handshake.
*
* @param \Workerman\Connection\TcpConnection $connection
* @return void
*/
public static function sendHandshake($connection)
{
if (!empty($connection->handshakeStep)) {
return;
}
// Get Host.
$port = $connection->getRemotePort();
$host = $port === 80 ? $connection->getRemoteHost() : $connection->getRemoteHost() . ':' . $port;
// Handshake header.
$header = 'GET ' . $connection->getRemoteURI() . " HTTP/1.1\r\n".
"Host: $host\r\n".
"Connection: Upgrade\r\n".
"Upgrade: websocket\r\n".
"Origin: ". (isset($connection->websocketOrigin) ? $connection->websocketOrigin : '*') ."\r\n".
"Sec-WebSocket-Version: 13\r\n".
"Sec-WebSocket-Key: " . base64_encode(md5(mt_rand(), true)) . "\r\n\r\n";
$connection->send($header, true);
$connection->handshakeStep = 1;
$connection->websocketCurrentFrameLength = 0;
$connection->websocketDataBuffer = '';
$connection->tmpWebsocketData = '';
}
/**
* Websocket handshake.
*
* @param string $buffer
* @param \Workerman\Connection\TcpConnection $connection
* @return int
*/
public static function dealHandshake($buffer, $connection)
{
$pos = strpos($buffer, "\r\n\r\n");
if ($pos) {
// handshake complete
$connection->handshakeStep = 2;
$handshake_response_length = $pos + 4;
// Try to emit onWebSocketConnect callback.
if (isset($connection->onWebSocketConnect)) {
try {
call_user_func($connection->onWebSocketConnect, $connection, substr($buffer, 0, $handshake_response_length));
} catch (\Exception $e) {
Worker::log($e);
exit(250);
} catch (\Error $e) {
Worker::log($e);
exit(250);
}
}
// Headbeat.
if (!empty($connection->websocketPingInterval)) {
$connection->websocketPingTimer = Timer::add($connection->websocketPingInterval, function() use ($connection){
if (false === $connection->send(pack('H*', '8900'), true)) {
Timer::del($connection->websocketPingTimer);
$connection->websocketPingTimer = null;
}
});
}
$connection->consumeRecvBuffer($handshake_response_length);
if (!empty($connection->tmpWebsocketData)) {
$connection->send($connection->tmpWebsocketData, true);
$connection->tmpWebsocketData = '';
}
if (strlen($buffer) > $handshake_response_length) {
return self::input(substr($buffer, $handshake_response_length), $connection);
}
}
return 0;
}
}

View File

@ -0,0 +1,32 @@
# workerman-for-win
workerman-for-win
## 环境要求
(php>=5.3.3)
## 运行
运行一个文件
php your_file.php
同时运行多个文件
php your_file.php your_file2.php ...
## 与Linux多进程版本的区别
1、单进程也就是说count属性无效
2、由于php在win下无法fork进程Applications/YourApp/start.php被拆成多个子启动项如start_web.php start_gateway.php等每个文件自动启动一个进程运行
3、由于php在win下不支持信号所以无法使用reload、status、restart、stop命令也没有start命令
## 手册
开发与Linux版本基本无差别可以直接参考Linux版本手册
http://doc3.workerman.net/
## 说明
此版本可用于windows下开发使用不建议用在生产环境
## 移植
### windows到Linux需要Linux的Workerman版本3.1.0及以上)
可以直接将Applications下的应用目录拷贝到Linux版本的Applications下直接运行
### Linux到windows
Linux下的应用需要将Applications/YourApp/start.php拆成多个启动项

View File

@ -0,0 +1,301 @@
<?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 Workerman;
use Workerman\Protocols\Http;
use Workerman\Protocols\HttpCache;
/**
* WebServer.
*/
class WebServer extends Worker
{
/**
* Virtual host to path mapping.
*
* @var array ['workerman.net'=>'/home', 'www.workerman.net'=>'home/www']
*/
protected $serverRoot = array();
/**
* Mime mapping.
*
* @var array
*/
protected static $mimeTypeMap = array();
/**
* Used to save user OnWorkerStart callback settings.
*
* @var callback
*/
protected $_onWorkerStart = null;
/**
* Add virtual host.
*
* @param string $domain
* @param string $root_path
* @return void
*/
public function addRoot($domain, $root_path)
{
$this->serverRoot[$domain] = $root_path;
}
/**
* Construct.
*
* @param string $socket_name
* @param array $context_option
*/
public function __construct($socket_name, $context_option = array())
{
list(, $address) = explode(':', $socket_name, 2);
parent::__construct('http:' . $address, $context_option);
$this->name = 'WebServer';
}
/**
* Run webserver instance.
*
* @see Workerman.Worker::run()
*/
public function run()
{
$this->_onWorkerStart = $this->onWorkerStart;
$this->onWorkerStart = array($this, 'onWorkerStart');
$this->onMessage = array($this, 'onMessage');
parent::run();
}
/**
* Emit when process start.
*
* @throws \Exception
*/
public function onWorkerStart()
{
if (empty($this->serverRoot)) {
echo new \Exception('server root not set, please use WebServer::addRoot($domain, $root_path) to set server root path');
exit(250);
}
// Init mimeMap.
$this->initMimeTypeMap();
// Try to emit onWorkerStart callback.
if ($this->_onWorkerStart) {
try {
call_user_func($this->_onWorkerStart, $this);
} catch (\Exception $e) {
self::log($e);
exit(250);
} catch (\Error $e) {
self::log($e);
exit(250);
}
}
}
/**
* Init mime map.
*
* @return void
*/
public function initMimeTypeMap()
{
$mime_file = Http::getMimeTypesFile();
if (!is_file($mime_file)) {
$this->log("$mime_file mime.type file not fond");
return;
}
$items = file($mime_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!is_array($items)) {
$this->log("get $mime_file mime.type content fail");
return;
}
foreach ($items as $content) {
if (preg_match("/\s*(\S+)\s+(\S.+)/", $content, $match)) {
$mime_type = $match[1];
$workerman_file_extension_var = $match[2];
$workerman_file_extension_array = explode(' ', substr($workerman_file_extension_var, 0, -1));
foreach ($workerman_file_extension_array as $workerman_file_extension) {
self::$mimeTypeMap[$workerman_file_extension] = $mime_type;
}
}
}
}
/**
* Emit when http message coming.
*
* @param Connection\TcpConnection $connection
* @return void
*/
public function onMessage($connection)
{
// REQUEST_URI.
$workerman_url_info = parse_url($_SERVER['REQUEST_URI']);
if (!$workerman_url_info) {
Http::header('HTTP/1.1 400 Bad Request');
$connection->close('<h1>400 Bad Request</h1>');
return;
}
$workerman_path = isset($workerman_url_info['path']) ? $workerman_url_info['path'] : '/';
$workerman_path_info = pathinfo($workerman_path);
$workerman_file_extension = isset($workerman_path_info['extension']) ? $workerman_path_info['extension'] : '';
if ($workerman_file_extension === '') {
$workerman_path = ($len = strlen($workerman_path)) && $workerman_path[$len - 1] === '/' ? $workerman_path . 'index.php' : $workerman_path . '/index.php';
$workerman_file_extension = 'php';
}
$workerman_root_dir = isset($this->serverRoot[$_SERVER['SERVER_NAME']]) ? $this->serverRoot[$_SERVER['SERVER_NAME']] : current($this->serverRoot);
$workerman_file = "$workerman_root_dir/$workerman_path";
if ($workerman_file_extension === 'php' && !is_file($workerman_file)) {
$workerman_file = "$workerman_root_dir/index.php";
if (!is_file($workerman_file)) {
$workerman_file = "$workerman_root_dir/index.html";
$workerman_file_extension = 'html';
}
}
// File exsits.
if (is_file($workerman_file)) {
// Security check.
if ((!($workerman_request_realpath = realpath($workerman_file)) || !($workerman_root_dir_realpath = realpath($workerman_root_dir))) || 0 !== strpos($workerman_request_realpath,
$workerman_root_dir_realpath)
) {
Http::header('HTTP/1.1 400 Bad Request');
$connection->close('<h1>400 Bad Request</h1>');
return;
}
$workerman_file = realpath($workerman_file);
// Request php file.
if ($workerman_file_extension === 'php') {
$workerman_cwd = getcwd();
chdir($workerman_root_dir);
ini_set('display_errors', 'off');
ob_start();
// Try to include php file.
try {
// $_SERVER.
$_SERVER['REMOTE_ADDR'] = $connection->getRemoteIp();
$_SERVER['REMOTE_PORT'] = $connection->getRemotePort();
include $workerman_file;
} catch (\Exception $e) {
// Jump_exit?
if ($e->getMessage() != 'jump_exit') {
echo $e;
}
}
$content = ob_get_clean();
ini_set('display_errors', 'on');
if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") {
$connection->send($content);
} else {
$connection->close($content);
}
chdir($workerman_cwd);
return;
}
// Send file to client.
return self::sendFile($connection, $workerman_file);
} else {
// 404
Http::header("HTTP/1.1 404 Not Found");
$connection->close('<html><head><title>404 File not found</title></head><body><center><h3>404 Not Found</h3></center></body></html>');
return;
}
}
public static function sendFile($connection, $file_path)
{
// Check 304.
$info = stat($file_path);
$modified_time = $info ? date('D, d M Y H:i:s', $info['mtime']) . ' ' . date_default_timezone_get() : '';
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $info) {
// Http 304.
if ($modified_time === $_SERVER['HTTP_IF_MODIFIED_SINCE']) {
// 304
Http::header('HTTP/1.1 304 Not Modified');
// Send nothing but http headers..
$connection->close('');
return;
}
}
// Http header.
if ($modified_time) {
$modified_time = "Last-Modified: $modified_time\r\n";
}
$file_size = filesize($file_path);
$file_info = pathinfo($file_path);
$extension = isset($file_info['extension']) ? $file_info['extension'] : '';
$file_name = isset($file_info['filename']) ? $file_info['filename'] : '';
$header = "HTTP/1.1 200 OK\r\n";
if (isset(self::$mimeTypeMap[$extension])) {
$header .= "Content-Type: " . self::$mimeTypeMap[$extension] . "\r\n";
} else {
$header .= "Content-Type: application/octet-stream\r\n";
$header .= "Content-Disposition: attachment; filename=\"$file_name\"\r\n";
}
$header .= "Connection: keep-alive\r\n";
$header .= $modified_time;
$header .= "Content-Length: $file_size\r\n\r\n";
$trunk_limit_size = 1024*1024;
if ($file_size < $trunk_limit_size) {
return $connection->send($header.file_get_contents($file_path), true);
}
$connection->send($header, true);
// Read file content from disk piece by piece and send to client.
$connection->fileHandler = fopen($file_path, 'r');
$do_write = function()use($connection)
{
// Send buffer not full.
while(empty($connection->bufferFull))
{
// Read from disk.
$buffer = fread($connection->fileHandler, 8192);
// Read eof.
if($buffer === '' || $buffer === false)
{
return;
}
$connection->send($buffer, true);
}
};
// Send buffer full.
$connection->onBufferFull = function($connection)
{
$connection->bufferFull = true;
};
// Send buffer drain.
$connection->onBufferDrain = function($connection)use($do_write)
{
$connection->bufferFull = false;
$do_write();
};
$do_write();
}
}

View File

@ -0,0 +1,946 @@
<?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 Workerman;
require_once __DIR__ . '/Lib/Constants.php';
use \Workerman\Events\Libevent;
use \Workerman\Events\Event;
use \Workerman\Events\React;
use \Workerman\Events\Select;
use \Workerman\Events\EventInterface;
use \Workerman\Connection\ConnectionInterface;
use \Workerman\Connection\TcpConnection;
use \Workerman\Connection\UdpConnection;
use \Workerman\Lib\Timer;
use \Workerman\Autoloader;
use \Exception;
/**
*
* @author walkor<walkor@workerman.net>
*/
class Worker
{
/**
* 版本号
* @var string
*/
const VERSION = '3.5.1';
/**
* 状态 启动中
* @var int
*/
const STATUS_STARTING = 1;
/**
* 状态 运行中
* @var int
*/
const STATUS_RUNNING = 2;
/**
* 状态 停止
* @var int
*/
const STATUS_SHUTDOWN = 4;
/**
* 状态 平滑重启中
* @var int
*/
const STATUS_RELOADING = 8;
/**
* 给子进程发送重启命令 KILL_WORKER_TIMER_TIME 秒后
* 如果对应进程仍然未重启则强行杀死
* @var int
*/
const KILL_WORKER_TIMER_TIME = 1;
/**
* 默认的backlog即内核中用于存放未被进程认领accept的连接队列长度
* @var int
*/
const DEFAUL_BACKLOG = 1024;
/**
* udp最大包长
* @var int
*/
const MAX_UDP_PACKAGE_SIZE = 65535;
/**
* worker id
* @var int
*/
public $id = 0;
/**
* worker的名称用于在运行status命令时标记进程
* @var string
*/
public $name = 'none';
/**
* 设置当前worker实例的进程数
* @var int
*/
public $count = 1;
/**
* 设置当前worker进程的运行用户启动时需要root超级权限
* @var string
*/
public $user = '';
/**
* 当前worker进程是否可以平滑重启
* @var bool
*/
public $reloadable = true;
/**
* reuse port
* @var bool
*/
public $reusePort = false;
/**
* 当worker进程启动时如果设置了$onWorkerStart回调函数则运行
* 此钩子函数一般用于进程启动后初始化工作
* @var callback
*/
public $onWorkerStart = null;
/**
* 当有客户端连接时,如果设置了$onConnect回调函数则运行
* @var callback
*/
public $onConnect = null;
/**
* 当客户端连接上发来数据时,如果设置了$onMessage回调则运行
* @var callback
*/
public $onMessage = null;
/**
* 当客户端的连接关闭时,如果设置了$onClose回调则运行
* @var callback
*/
public $onClose = null;
/**
* 当客户端的连接发生错误时,如果设置了$onError回调则运行
* 错误一般为客户端断开连接导致数据发送失败、服务端的发送缓冲区满导致发送失败等
* 具体错误码及错误详情会以参数的形式传递给回调,参见手册
* @var callback
*/
public $onError = null;
/**
* 当连接的发送缓冲区满时,如果设置了$onBufferFull回调则执行
* @var callback
*/
public $onBufferFull = null;
/**
* 当链接的发送缓冲区被清空时,如果设置了$onBufferDrain回调则执行
* @var callback
*/
public $onBufferDrain = null;
/**
* 当前进程退出时(由于平滑重启或者服务停止导致),如果设置了此回调,则运行
* @var callback
*/
public $onWorkerStop = null;
/**
* 当收到reload命令时的回调函数
* @var callback
*/
public $onWorkerReload = null;
/**
* 传输层协议
* @var string
*/
public $transport = 'tcp';
/**
* 所有的客户端连接
* @var array
*/
public $connections = array();
/**
* 应用层协议由初始化worker时指定
* 例如 new worker('http://0.0.0.0:8080');指定使用http协议
* @var string
*/
protected $protocol = null;
/**
* 当前worker实例初始化目录位置用于设置应用自动加载的根目录
* @var string
*/
protected $_autoloadRootPath = '';
/**
* 是否以守护进程的方式运行。运行start时加上-d参数会自动以守护进程方式运行
* 例如 php start.php start -d
* @var bool
*/
public static $daemonize = false;
/**
* 重定向标准输出即将所有echo、var_dump等终端输出写到对应文件中
* 注意 此参数只有在以守护进程方式运行时有效
* @var string
*/
public static $stdoutFile = '/dev/null';
/**
* pid文件的路径及名称
* 例如 Worker::$pidFile = '/tmp/workerman.pid';
* 注意 此属性一般不必手动设置默认会放到php临时目录中
* @var string
*/
public static $pidFile = '';
/**
* 日志目录默认在workerman根目录下与Applications同级
* 可以手动设置
* 例如 Worker::$logFile = '/tmp/workerman.log';
* @var unknown_type
*/
public static $logFile = '';
/**
* 全局事件轮询库,用于监听所有资源的可读可写事件
* @var Select/Libevent
*/
public static $globalEvent = null;
/**
* 主进程停止时触发的回调Win系统下不起作用
* @var unknown_type
*/
public static $onMasterStop = null;
/**
* 事件轮询库类名
* @var string
*/
public static $eventLoopClass = '';
/**
* 主进程pid
* @var int
*/
protected static $_masterPid = 0;
/**
* 监听的socket
* @var stream
*/
protected $_mainSocket = null;
/**
* socket名称包括应用层协议+ip+端口号在初始化worker时设置
* 值类似 http://0.0.0.0:80
* @var string
*/
protected $_socketName = '';
/**
* socket的上下文具体选项设置可以在初始化worker时传递
* @var context
*/
protected $_context = null;
/**
* 所有的worker实例
* @var array
*/
protected static $_workers = array();
/**
* 所有worker进程的pid
* 格式为 [worker_id=>[pid=>pid, pid=>pid, ..], ..]
* @var array
*/
protected static $_pidMap = array();
/**
* 所有需要重启的进程pid
* 格式为 [pid=>pid, pid=>pid]
* @var array
*/
protected static $_pidsToRestart = array();
/**
* 当前worker状态
* @var int
*/
protected static $_status = self::STATUS_STARTING;
/**
* 所有worke名称(name属性)中的最大长度,用于在运行 status 命令时格式化输出
* @var int
*/
protected static $_maxWorkerNameLength = 12;
/**
* 所有socket名称(_socketName属性)中的最大长度,用于在运行 status 命令时格式化输出
* @var int
*/
protected static $_maxSocketNameLength = 12;
/**
* 所有user名称(user属性)中的最大长度,用于在运行 status 命令时格式化输出
* @var int
*/
protected static $_maxUserNameLength = 12;
/**
* 运行 status 命令时用于保存结果的文件名
* @var string
*/
protected static $_statisticsFile = '';
/**
* 启动的全局入口文件
* 例如 php start.php start 则入口文件为start.php
* @var string
*/
protected static $_startFile = '';
/**
* 用来保存子进程句柄windows
* @var array
*/
protected static $_process = array();
/**
* 要执行的文件
* @var array
*/
protected static $_startFiles = array();
/**
* Available event loops.
*
* @var array
*/
protected static $_availableEventLoops = array(
'libevent' => '\Workerman\Events\Libevent',
'event' => '\Workerman\Events\Event'
);
/**
* PHP built-in protocols.
*
* @var array
*/
protected static $_builtinTransports = array(
'tcp' => 'tcp',
'udp' => 'udp',
'unix' => 'unix',
'ssl' => 'tcp'
);
/**
* 运行所有worker实例
* @return void
*/
public static function runAll()
{
// 初始化环境变量
self::init();
// 解析命令
self::parseCommand();
// 初始化所有worker实例主要是监听端口
self::initWorkers();
// 展示启动界面
self::displayUI();
// 运行所有的worker
self::runAllWorkers();
// 监控worker
self::monitorWorkers();
}
/**
* 初始化一些环境变量
* @return void
*/
public static function init()
{
if(strpos(strtolower(PHP_OS), 'win') !== 0)
{
exit("workerman-for-win can not run in linux\n");
}
if (false !== strpos(ini_get('disable_functions'), 'proc_open'))
{
exit("\r\nWarning: proc_open() has been disabled for security reasons. \r\n\r\nSee http://wiki.workerman.net/Error5\r\n");
}
$backtrace = debug_backtrace();
self::$_startFile = $backtrace[count($backtrace)-1]['file'];
// 没有设置日志文件,则生成一个默认值
if(empty(self::$logFile))
{
self::$logFile = __DIR__ . '/../workerman.log';
}
// 标记状态为启动中
self::$_status = self::STATUS_STARTING;
$event_loop_class = self::getEventLoopName();
self::$globalEvent = new $event_loop_class;
Timer::init(self::$globalEvent);
}
/**
* 初始化所有的worker实例主要工作为获得格式化所需数据及监听端口
* @return void
*/
protected static function initWorkers()
{
foreach(self::$_workers as $worker)
{
// 没有设置worker名称则使用none代替
if(empty($worker->name))
{
$worker->name = 'none';
}
// 获得所有worker名称中最大长度
$worker_name_length = strlen($worker->name);
if(self::$_maxWorkerNameLength < $worker_name_length)
{
self::$_maxWorkerNameLength = $worker_name_length;
}
// 获得所有_socketName中最大长度
$socket_name_length = strlen($worker->getSocketName());
if(self::$_maxSocketNameLength < $socket_name_length)
{
self::$_maxSocketNameLength = $socket_name_length;
}
$user_name_length = strlen($worker->user);
if(self::$_maxUserNameLength < $user_name_length)
{
self::$_maxUserNameLength = $user_name_length;
}
}
}
/**
* 运行所有的worker
*/
public static function runAllWorkers()
{
// 只有一个start文件时执行run
if(count(self::$_startFiles) === 1)
{
// win不支持同一个页面执初始化多个worker
if(count(self::$_workers) > 1)
{
echo "@@@ Error: multi workers init in one php file are not support @@@\r\n";
echo "@@@ Please visit http://wiki.workerman.net/Multi_woker_for_win @@@\r\n";
}
elseif(count(self::$_workers) <= 0)
{
exit("@@@no worker inited@@@\r\n\r\n");
}
// 执行worker的run方法
reset(self::$_workers);
$worker = current(self::$_workers);
$worker->listen();
// 子进程阻塞在这里
$worker->run();
exit("@@@child exit@@@\r\n");
}
// 多个start文件则多进程打开
elseif(count(self::$_startFiles) > 1)
{
self::$globalEvent = new Select();
Timer::init(self::$globalEvent);
foreach(self::$_startFiles as $start_file)
{
self::openProcess($start_file);
}
}
// 没有start文件提示错误
else
{
//exit("@@@no worker inited@@@\r\n");
}
}
/**
* 打开一个子进程
* @param string $start_file
*/
public static function openProcess($start_file)
{
// 保存子进程的输出
$start_file = realpath($start_file);
$std_file = sys_get_temp_dir() . '/'.str_replace(array('/', "\\", ':'), '_', $start_file).'.out.txt';
// 将stdou stderr 重定向到文件
$descriptorspec = array(
0 => array('pipe', 'a'), // stdin
1 => array('file', $std_file, 'w'), // stdout
2 => array('file', $std_file, 'w') // stderr
);
// 保存stdin句柄用来探测子进程是否关闭
$pipes = array();
// 打开子进程
$process= proc_open("php \"$start_file\" -q", $descriptorspec, $pipes);
// 打开stdout stderr 文件句柄
$std_handler = fopen($std_file, 'a+');
// 非阻塞
stream_set_blocking($std_handler, 0);
// 定时读取子进程的stdout stderr
$timer_id = Timer::add(0.1, function()use($std_handler)
{
echo fread($std_handler, 65535);
});
// 保存子进程句柄
self::$_process[$start_file] = array($process, $start_file, $timer_id);
}
/**
* 定时检查子进程是否退出了
*/
protected static function monitorWorkers()
{
// 定时检查子进程是否退出了
Timer::add(0.5, "\\Workerman\\Worker::checkWorkerStatus");
// 主进程loop
self::$globalEvent->loop();
}
public static function checkWorkerStatus()
{
foreach(self::$_process as $process_data)
{
$process = $process_data[0];
$start_file = $process_data[1];
$timer_id = $process_data[2];
$status = proc_get_status($process);
if(isset($status['running']))
{
// 子进程退出了,重启一个子进程
if(!$status['running'])
{
echo "process $start_file terminated and try to restart\n";
Timer::del($timer_id);
@proc_close($process);
// 重新打开一个子进程
self::openProcess($start_file);
}
}
else
{
echo "proc_get_status fail\n";
}
}
}
/**
* Get all worker instances.
*
* @return array
*/
public static function getAllWorkers()
{
return self::$_workers;
}
/**
* Get global event-loop instance.
*
* @return EventInterface
*/
public static function getEventLoop()
{
return self::$globalEvent;
}
/**
* 展示启动界面
* @return void
*/
protected static function displayUI()
{
global $argv;
// -q不打印
if(in_array('-q', $argv))
{
return;
}
echo "----------------------- WORKERMAN -----------------------------\n";
echo 'Workerman version:' . Worker::VERSION . " PHP version:".PHP_VERSION."\n";
echo "------------------------ WORKERS -------------------------------\n";
echo "worker",str_pad('', self::$_maxWorkerNameLength+2-strlen('worker')), "listen",str_pad('', self::$_maxSocketNameLength+2-strlen('listen')), "processes ","status\n";
foreach(self::$_workers as $worker)
{
echo str_pad($worker->name, self::$_maxWorkerNameLength+2),str_pad($worker->getSocketName(), self::$_maxSocketNameLength+2), str_pad(' '.$worker->count, 9), " [OK] \n";;
}
echo "----------------------------------------------------------------\n";
echo "Press Ctrl-C to quit. Start success.\n";
}
/**
* 解析运行命令
* php yourfile.php start | stop | restart | reload | status
* @return void
*/
public static function parseCommand()
{
global $argv;
foreach($argv as $file)
{
$ext = pathinfo($file, PATHINFO_EXTENSION );
if($ext !== 'php')
{
continue;
}
if(is_file($file))
{
self::$_startFiles[$file] = $file;
include_once $file;
}
}
}
/**
* 执行关闭流程
* @return void
*/
public static function stopAll()
{
self::$_status = self::STATUS_SHUTDOWN;
exit(0);
}
/**
* 记录日志
* @param string $msg
* @return void
*/
public static function log($msg)
{
$msg = $msg."\n";
if(self::$_status === self::STATUS_STARTING || !self::$daemonize)
{
echo $msg;
}
file_put_contents(self::$logFile, date('Y-m-d H:i:s') . " " . $msg, FILE_APPEND | LOCK_EX);
}
/**
* worker构造函数
* @param string $socket_name
* @return void
*/
public function __construct($socket_name = '', $context_option = array())
{
// 保存worker实例
$this->workerId = spl_object_hash($this);
self::$_workers[$this->workerId] = $this;
self::$_pidMap[$this->workerId] = array();
// 获得实例化文件路径,用于自动加载设置根目录
$backrace = debug_backtrace();
$this->_autoloadRootPath = dirname($backrace[0]['file']);
// 设置socket上下文
if($socket_name)
{
$this->_socketName = $socket_name;
if(!isset($context_option['socket']['backlog']))
{
$context_option['socket']['backlog'] = self::DEFAUL_BACKLOG;
}
$this->_context = stream_context_create($context_option);
}
// 设置一个空的onMessage当onMessage未设置时用来消费socket数据
$this->onMessage = function(){};
}
/**
* 监听端口
* @throws Exception
*/
public function listen()
{
if (!$this->_socketName || $this->_mainSocket) {
return;
}
// Autoload.
Autoloader::setRootPath($this->_autoloadRootPath);
// Get the application layer communication protocol and listening address.
list($scheme, $address) = explode(':', $this->_socketName, 2);
// Check application layer protocol class.
if (!isset(self::$_builtinTransports[$scheme])) {
if(class_exists($scheme)){
$this->protocol = $scheme;
} else {
$scheme = ucfirst($scheme);
$this->protocol = '\\Protocols\\' . $scheme;
if (!class_exists($this->protocol)) {
$this->protocol = "\\Workerman\\Protocols\\$scheme";
if (!class_exists($this->protocol)) {
throw new Exception("class \\Protocols\\$scheme not exist");
}
}
}
if (!isset(self::$_builtinTransports[$this->transport])) {
throw new \Exception('Bad worker->transport ' . var_export($this->transport, true));
}
} else {
$this->transport = $scheme;
}
$local_socket = self::$_builtinTransports[$this->transport] . ":" . $address;
// Flag.
$flags = $this->transport === 'udp' ? STREAM_SERVER_BIND : STREAM_SERVER_BIND | STREAM_SERVER_LISTEN;
$errno = 0;
$errmsg = '';
// SO_REUSEPORT.
if ($this->reusePort) {
stream_context_set_option($this->_context, 'socket', 'so_reuseport', 1);
}
// Create an Internet or Unix domain server socket.
$this->_mainSocket = stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context);
if (!$this->_mainSocket) {
throw new Exception($errmsg);
}
if ($this->transport === 'ssl') {
stream_socket_enable_crypto($this->_mainSocket, false);
}
// Try to open keepalive for tcp and disable Nagle algorithm.
if (function_exists('socket_import_stream') && self::$_builtinTransports[$this->transport] === 'tcp') {
$socket = socket_import_stream($this->_mainSocket);
@socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
@socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);
}
// Non blocking.
stream_set_blocking($this->_mainSocket, 0);
// Register a listener to be notified when server socket is ready to read.
if (self::$globalEvent) {
if ($this->transport !== 'udp') {
self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection'));
} else {
self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ,
array($this, 'acceptUdpConnection'));
}
}
}
/**
* Get event loop name.
*
* @return string
*/
protected static function getEventLoopName()
{
if (self::$eventLoopClass) {
return self::$eventLoopClass;
}
$loop_name = '';
foreach (self::$_availableEventLoops as $name=>$class) {
if (extension_loaded($name)) {
$loop_name = $name;
break;
}
}
if ($loop_name) {
if (interface_exists('\React\EventLoop\LoopInterface')) {
switch ($loop_name) {
case 'libevent':
self::$eventLoopClass = '\Workerman\Events\React\LibEventLoop';
break;
case 'event':
self::$eventLoopClass = '\Workerman\Events\React\ExtEventLoop';
break;
default :
self::$eventLoopClass = '\Workerman\Events\React\StreamSelectLoop';
break;
}
} else {
self::$eventLoopClass = self::$_availableEventLoops[$loop_name];
}
} else {
self::$eventLoopClass = interface_exists('\React\EventLoop\LoopInterface')? '\Workerman\Events\React\StreamSelectLoop':'\Workerman\Events\Select';
}
return self::$eventLoopClass;
}
/**
* 获得 socket name
* @return string
*/
public function getSocketName()
{
return $this->_socketName ? $this->_socketName : 'none';
}
/**
* 运行worker实例
*/
public function run()
{
// 设置自动加载根目录
Autoloader::setRootPath($this->_autoloadRootPath);
// Create a global event loop.
if (!self::$globalEvent) {
$event_loop_class = self::getEventLoopName();
self::$globalEvent = new $event_loop_class;
}
// 监听_mainSocket上的可读事件客户端连接事件
if($this->_socketName)
{
if($this->transport !== 'udp')
{
self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection'));
}
else
{
self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptUdpConnection'));
}
}
// 用全局事件轮询初始化定时器
Timer::init(self::$globalEvent);
// 如果有设置进程启动回调,则执行
if($this->onWorkerStart)
{
call_user_func($this->onWorkerStart, $this);
}
// 子进程主循环
self::$globalEvent->loop();
}
/**
* 停止当前worker实例
* @return void
*/
public function stop()
{
// 如果有设置进程终止回调,则执行
if($this->onWorkerStop)
{
call_user_func($this->onWorkerStop, $this);
}
// 删除相关监听事件关闭_mainSocket
self::$globalEvent->del($this->_mainSocket, EventInterface::EV_READ);
@fclose($this->_mainSocket);
}
/**
* 接收一个客户端连接
* @param resources $socket
* @return void
*/
public function acceptConnection($socket)
{
// Accept a connection on server socket.
$new_socket = @stream_socket_accept($socket, 0, $remote_address);
// Thundering herd.
if (!$new_socket) {
return;
}
// TcpConnection.
$connection = new TcpConnection($new_socket, $remote_address);
$this->connections[$connection->id] = $connection;
$connection->worker = $this;
$connection->protocol = $this->protocol;
$connection->transport = $this->transport;
$connection->onMessage = $this->onMessage;
$connection->onClose = $this->onClose;
$connection->onError = $this->onError;
$connection->onBufferDrain = $this->onBufferDrain;
$connection->onBufferFull = $this->onBufferFull;
// Try to emit onConnect callback.
if ($this->onConnect) {
try {
call_user_func($this->onConnect, $connection);
} catch (\Exception $e) {
self::log($e);
exit(250);
} catch (\Error $e) {
self::log($e);
exit(250);
}
}
}
/**
* 处理udp连接udp其实是无连接的这里为保证和tcp连接接口一致
* @param resource $socket
*/
public function acceptUdpConnection($socket)
{
$recv_buffer = stream_socket_recvfrom($socket, self::MAX_UDP_PACKAGE_SIZE, 0, $remote_address);
if (false === $recv_buffer || empty($remote_address)) {
return false;
}
// UdpConnection.
$connection = new UdpConnection($socket, $remote_address);
$connection->protocol = $this->protocol;
if ($this->onMessage) {
if ($this->protocol) {
$parser = $this->protocol;
$recv_buffer = $parser::decode($recv_buffer, $connection);
}
ConnectionInterface::$statistics['total_request']++;
try {
call_user_func($this->onMessage, $connection, $recv_buffer);
} catch (\Exception $e) {
self::log($e);
exit(250);
} catch (\Error $e) {
self::log($e);
exit(250);
}
}
return true;
}
}

Some files were not shown because too many files have changed in this diff Show More