闲暇之余大概的看了一下 socketio.js 发现这个比传统的websocket更加方便、更加稳定;
但是socketio.js的官方服务端是node,当然文档中提供的有各种语言的服务端代码,但独独少了PHP,因为就PHP本身来说对socket的支持不算友好,但是现在有了swoole的加持,我们也可以提供很好的服务;
如果需要对socketio.js有更多的了解 可以查看下面链接:
https://socket.io/docs/v4/
对比js 中的 websocket 与 socketio.js
- 传统的websocket 需要自己写心跳检测机制、而socketio有自带的心跳检测,只需要简单的配置就可以实现心跳机制;
- socketio 有自带的断线重连机制;
- socketio 可以自定义事件,只要服务端支持即可,所有的代码不用放在message处理了,使代码更好管理、更有层次;
- 对平台兼容良好
总之 socketio 用起来确实比js 的websocket 更加方便、更加稳定;
这里简单说一嘴 其实thinkphp-swoole 扩展也对socketio提供了支持,我没有具体的看过,简单来说我觉得太过于复杂了,
人们对于未知的东西是不可控的,所以我决定自己写一个,以常人最简单的思维去实现,大家都能看得懂,方便后期维护和修改以及扩展,总之以最朴素的代码,最简洁的逻辑和方式来实现,不宜过度的封装,不宜使用什么trigger,event之类高大上的东西,以普通程序猿的常用思维来干一套!
本文以thinkphp6 + swoole来实现对socketio.js 的支持;下面我们来看一下效果
实现的场景是一对多私聊,对于公聊就更加简单了,不用维护uid与fd关系,这里就不写了,还有一个是群聊(房间),基础实现了实现房间也很简单;
- 首先在appconfig 里面 增加一个swoole.php 配置,主要对端口以及swoole的一些基础设置,增加一个redis来绑定uid与fd的关系;
<?php
return [
'websocket' => [
'chat' => [
'host' => '0.0.0.0',
'port' => '9509',
'process_name' => 'chart',
'socket_config' => [
'open_websocket_pong_frame' => true,
'heartbeat_idle_time' => 60,
'heartbeat_check_interval' => 30,
'open_websocket_ping_frame' => true,
'reactor_num' => 2,
'worker_num' => 2,
'task_worker_num' => 1,
'daemonize' => false,
//'log_file' => '/www/wwwlogs/run.log',
//'pid_file' => '/www/wwwlogs/server.pid',
'reload_async' => false,
'dispatch_mode' => 5
],
'ping_interval' => 5000,//实现对socket.io 心跳配置
'ping_timeout' => 6000,//实现对socket.io 心跳超时配置
//这里实现对socket.io事件回调类配置,需要自定义
'event' => [
'PrivateChat' => \server\swoole\chat\PrivateChat::class, //私聊
'room' => \server\swoole\chat\Room::class //群聊
],
'redis' => [
'host' => '127.0.0.1',
'port' => '6379',
'timeout' => 5,
'pass' => ''
],
'room' => [
'prefix_room_id' => 'chat_'
]
]
]
];
- 实现swoole websocket 事件回调,对于不同websocket服务都可以调用它,只需要通过不同的name进行区分即可,使用call_user_func_array函数,对不同websocket服务进行事件自定义;
<?php
namespace server\swoole;
use server\Single;
use think\facade\Config;
class SwooleWebsocketServer
{
use Single;
public $ws = null;
public $name;
public $config = [];
public function __construct($name = 'default')
{
$this->name = $name;
$this->config = Config::load('swoole', 'config')['websocket'][$name];
$this->ws = new \Swoole\WebSocket\Server($this->config['host'], $this->config['port']);
$this->ws->set($this->config['socket_config']);
if (isset($this->config['process_name']) && !empty($this->config['process_name'])) {
swoole_set_process_name($this->config['process_name']);
}
}
public function onConnect()
{
$this->ws->on('connect', call_user_func_array(["server\\swoole\\{$this->name}\\Event", "connect"], [$this->config]));
}
public function onOpen()
{
$this->ws->on('open', call_user_func_array(["server\\swoole\\{$this->name}\\Event", "open"], [$this->config]));
}
public function onMessage()
{
$this->ws->on('message', call_user_func_array(["server\\swoole\\{$this->name}\\Event", 'message'],[$this->config]));
}
public function onClose()
{
$this->ws->on('close', call_user_func_array(["server\\swoole\\{$this->name}\\Event", 'close'],[$this->config]));
}
public function onTask()
{
$this->ws->on('task', call_user_func_array(["server\\swoole\\{$this->name}\\Event", 'task'],[]));
}
public function onStart()
{
$this->ws->start();
}
public function start()
{
$this->onOpen();
$this->onConnect();
$this->onMessage();
$this->onClose();
$this->onTask();
$this->onStart();
}
}
- 对事件的实现的trait类,不同websocket服务都可以引用该类或者重写该类
<?php
namespace server\swoole;
use server\swoole\socketIo\Packet;
use server\swoole\socketIo\Parser;
trait Handle
{
/**
* @param array $config
* @return \Closure
*
*/
public static function connect(array $config) : \Closure
{
return function ($server,$fd) use ($config)
{
};
}
/**
* @param array $config
* @return \Closure
*
*/
public static function open(array $config = []): \Closure
{
return function ($server, $request) use ($config) {
$payload = json_encode([
'sid' => base64_encode(uniqid()),
'upgrades' => [],
'pingInterval' => $config['ping_interval'],
'pingTimeout' => $config['ping_timeout'],
]);
$initPayload = Packet::OPEN . $payload;
$connectPayload = Packet::MESSAGE . Packet::CONNECT;
$server->push($request->fd, $initPayload);
$server->push($request->fd, $connectPayload);
};
}
/**
* @param array $config
* @return \Closure
*
*/
public static function message(array $config = []): \Closure
{
return function ($server, $frame) use ($config) {
$payload = Parser::decode($frame);
if (Parser::execute($server, $frame)) {
return;
}
['event' => $event] = $payload;
return call_user_func_array([$config['event'][$event], 'send'], [$server, $frame,$config]);
};
}
/**
* @param array $config
* @return \Closure
*/
public static function close($config = []) : \Closure
{
return function ($ws, $fd) use ($config) {
echo "client-{$fd} is closed\n";
};
}
/**
* @return \Closure
*/
public static function task() : \Closure
{
return function ($ws, $task_id, $reactor_id, $data) {
};
}
}
这里说一嘴,open 方法非常重要,主要就是实现 upgrade#I/websocket.发送noop消息,之前hana住的轮询请求返回,如果忽略会导致链接不成功
message 方法 call_user_func_array
函数主要实现对socket.io的事件回调处理,也就是我们config配置里面,根据事件名称调用不同的处理类
其他类都是针对于 socket.io
的收发消息进行转码和编码,由于文字有限,这里就不一一列出来了。放在源代码里面了,感兴趣的朋友可以自己去看下;
- 利用`thinkphp启动服务,appcommand下建立一个 Chat.php 文件
<?php
namespace app\command;
use server\swoole\SwooleWebsocketServer;
use think\console\Command;
use think\console\Input;
use think\console\Output;
class Chat extends Command
{
protected function configure()
{
$this->setName('chat')->setDescription('The chart');
}
protected function execute(Input $input, Output $output)
{
SwooleWebsocketServer::getInstance('chat')->start();
}
}
- 执行命令
php think chat
- h5页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/element/plus/css/index.css">
</head>
<body>
<div >
<div style="width: 100%; height: 200px; border: 1px solid #cccc77; border-radius: 10px; padding: 10px; margin: 10px">
</div>
<div style="margin-top: 10px;">
<el-input type="textarea" v-model="message"></el-input>
</div>
<div>
<el-button type="danger" @click="handleSend">发送消息</el-button>
</div>
</div>
</body>
</html>
<script src="/static/js/socket.io-2.0.0.js"></script>
<script src="/static/vue/3.0.11/vue.global.js"></script>
<script src="/static/element/plus/js/index.full.js"></script>
<script>
const App = {
data:function () {
return {
socket:{},
message:""
};
},
methods:{
initSocket(){
this.socket = io('127.0.0.1:9509?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjgsImFjY291bnQiOiJ0YW5ndGFuZyIsImludml0ZV9jb2RlIjoiemJxMmRwIiwiZXhwIjoxNjU2MTM5MDgzfQ.-gJo4Q1mPdZAMSFPf25vUNqbEYM8eqfRlE36ZSNYbSk', {
path: '/ws',
transports: ['websocket']
});
this.socket.on("error", (error) => {
console.log(error)
});
this.socket.on('PrivateChat',async (aa) => {
console.log(aa)
})
},
handleSend(){
this.socket.emit("PrivateChat", {sender:8,to:-1,text:this.message}, (response) => {
console.log(response); // "got it"
});
},
},
mounted:function () {
this.initSocket()
}
};
const app = Vue.createApp(App);
app.use(ElementPlus, { size: 'small', zIndex: 3000 });
app.mount("#app");
</script>
总结一下:总之这里就做一个简单的介绍,源代码请看这里:
https://gitee.com/phpbloger/think-element/tree/master/extend/server/swoole
由于我demo中写了用户鉴权之类的东西,如果你们需要也可以自己定义实现一下,对于后台展示的聊天界面,由于只写了一个简单的demo,也没上传了,如果自己需要可以下方留言;
还没有内容