基于thinkphp5.1进行整合swoole。此篇文章涉及的知识点有点多。值得注意的是:这篇文章不是单单整合swoole服务端,还包括模型整合,redis协程开发等。
基于thinkphp5.1开启socket服务,我这里使用了自定义服务类
在config文件夹配置里面加入PHP配置代码
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
use think\facade\Env;
// +----------------------------------------------------------------------
// | Swoole设置 php think swoole:server 命令行下有效
// +----------------------------------------------------------------------
return [
'swoole_class' => 'app\server\Swoole', // 自定义服务类名称
];
Swoole的自定义类
<?php
namespace app\server;
use think\swoole\Server;
use \app\dati\logic\Player;
use think\facade\Cache;
class Swoole extends Server
{
use \app\dati\logic\Common;
protected $host = '0.0.0.0';
protected $port = 9504;
protected $serverType = 'socket'; //服务类型
protected $option = [
'worker_num' => 1, //处理进程数
'backlog' => 128,
'task_worker_num' =>1, //开启任务数量
'mode' => SWOOLE_PROCESS,
'socket_type' => SWOOLE_SOCK_TCP,
'daemonize' => false,
//可以设置 dispatch_mode=5 设置以此值进行 hash 固定分配。可以保证某一个 UID 的连接全部会分配到同一个 Worker 进程
'dispatch_mode' =>5,
'hook_flags' => SWOOLE_HOOK_ALL, //swoole一键协程
'task_enable_coroutine'=>true, //任务协程
];
/**
* $frame->fd 客户端的 socket id,使用 $server->push 推送数据时需要用到
* $frame->data 数据内容,可以是文本内容也可以是二进制数据,可以通过 opcode 的值来判断
* $frame->opcode WebSocket 的 OpCode 类型,可以参考 WebSocket 协议标准文档
* $frame->finish 表示数据帧是否完整,一个 WebSocket 请求可能会分成多个数据帧进行发送(底层已经实现了自动合并数据帧,现在不用担心接收到的数据帧不完整)
* 参考文档
* https://wiki.swoole.com/#/websocket_server?id=onmessage
* */
public function onMessage($ws, $frame) {
}
public function onClose($server,$fd){
}
public function onWorkerStart(){
$_SERVER['redispool'] = new \Swoole\Database\RedisPool((new \Swoole\Database\RedisConfig)
->withHost('127.0.0.1')
->withPort(6379)
->withAuth('')
->withDbIndex(0)
->withTimeout(1)
,100
);
}
public function onWorkerStop($server,$workerId){
}
/**
* https://wiki.swoole.com/#/server/events?id=ontask
* 异步风格写法
* */
/*public function onTask($server, $task_id, $from_id, $data){
go(function() use ($data){
//$s=rand(1,10);
sleep(5);
//每个任务
//$this->swoole->clearTimer($data);
//$server->clearTimer($data);
echo '任务执行ID:'.$data . '\n';
//co::sleep(0.2); //协程 API
});
}*/
//V4.2.12 起如果开启了 task_enable_coroutine
public function onTask($server, $task){
echo "#\ttask_id:{$task->id}\tonTask: worker_id={$server->worker_id}\n";
}
}
在使用swoole的协程的时候,一不小心就会出现公用MYSQL连接的问题,解决这个问题,我们就结合使用上下文类进行处理,其实上下文主要解决的问题就是:在同一个协程内公用一个资源,与其他协程独立,互不影响,也互不干扰。不明白的可以看下这里的第九点
THINKPHP运行的方式是单例执行框架代码的,当我们使用协程去处理的话,需要考虑同一条连接mysql是可能产生并发的,他会提示一个有另外一个协程正在处理,不可同时使用,需要解决当前问题,只需要兼容一下数据库模型即可。
原理:重载连接数据库操作并放到上下文资源操作类即可。
我们先建立一个model的基类,做重载连接数据库操作即可
<?php
/**
* BaseModel.php
* 兼容协程重载基类
* @author : iqgmy
* @date : 2017.5.28
* @version : v1.0.0.0
*/
namespace app;
use think\Model;
use think\Db;
use \app\server\Context;
class BaseModel extends Model
{
public $coroute = false;
/*------------------------------------------------------ */
//-- 优先自动执行
/*------------------------------------------------------ */
public function initialize(){
parent::initialize();
}
/**
* 创建模型的查询对象
* @access protected
* @return Query
*/
protected function buildQuery()
{
//兼容协程
if(class_exists('Co')&& \Co::getcid() > 0){
$key = get_class();
$query = Context::get($key);
if(is_null($query)){
//重置并且需要重连数据库
$query = Db::connect($this->connection, true, $this->query);
Context::put($key,$query);
}
}else{
// 设置当前模型 确保查询返回模型对象
$query = Db::connect($this->connection, false, $this->query);
}
$query->model($this)
->name($this->name)
->json($this->json, $this->jsonAssoc)
->setJsonFieldType($this->jsonType);
if (isset(static::$readMaster['*']) || isset(static::$readMaster[static::class])) {
$query->master(true);
}
// 设置当前数据表和模型名
if (!empty($this->table)) {
$query->table($this->table);
}
if (!empty($this->pk)) {
$query->pk($this->pk);
}
return $query;
}
}
在使用协程开发的时候,我们要时时刻刻保持警惕,我是否有共用一个连接一个数据操作,否则很容易出现has already been bound to another coroutine
注意:上面兼容model是有问题的,可以修改tp内核的model文件使其兼容起来
在整合swoole自定义的时候,开启worker就已经开启了一个全局的redis连接池,当然我也对其做了一个封装。原理和mysql上下文一起使用的
<?php
namespace app\server;
use Swoole\Coroutine;
class CorRedis{
public $connect ;
protected $debug = false; //调试模式
//实例对象
public static function instance(){
if($this->debug){
$redis = new \Redis(); //可以实现这个。这个暂时没有做调试
}else{
$redis = Context::get('corredis');
if(is_null($redis)){
$redis = $_SERVER['redispool']->get();
if(is_null($redis)){
throw new Exception('redis连接池为空',4000);
}
Context::put('corredis',$redis);
}
}
return $redis;
}
public static function close(){
//$redis = Context::get('corredis');
Context::delete('corredis');
}
//最终返回一个对象
public function __call($method, $args){
// return call_user_func_array([$this->instance(),$method],$args);
}
//最终返回一个对象
public static function __callStatic($method, $args){
return call_user_func_array([self::instance(),$method],$args);
}
}
在使用上下文连接的时候,使用完之后必须要销毁掉,不然很容易造成资源浪费。
在学习的过程中,只有自己知道了原理,然后慢慢的摸索,我们就会懂得很多。