想做聊天这样的app或页面,就会使用到长连接。websocket 是一种方式。服务方可以用 nodejs 这些。对于 php,还是使用 workman 或 swoole 比较方便。这里就记录下 laravel 框架使用 swoole 的过程。
编译安装 swoole 扩展
http://pecl.php.net/package/swoole
当前,最新版本是 4.31。所有一切都是新的,服务器是新的,装的环境也是最新的。所以没有心理压力,都用最新的。
cd /usr/local/src
wget http://pecl.php.net/get/swoole-4.3.1.tgz
tar -xzvf swoole-4.3.1.tgz
cd swoole-4.3.1
phpize
# 对 ssl 的支持。这个还是比较重要的
./configure --enable-openssl
make && make install
make test
vim /alidata/service/php/etc/php.ini
# 尾部追加
extension=swoole.so
# 重启 php
/etc/init.d/php-fpm restart
# 查看扩展
# php -m|grep swoole
上边步骤,仅仅针对环境中只装有一个 php 的。如果多 php, configure 的时候 ,还得带上 phpconfig 参数。
laravel 中的操作
- 创建 swoole.php 配置。
vim config/swoole.php
<?php
return [
'host' => '0.0.0.0',
'port' => 9508,
'config' => [
'worker_num' => 4,
'daemonize' => 1,
'max_request' => 1000,
'dispatch_mode' => 2,
'pid_file' => storage_path('swoole/socket.pid'),
'ssl_cert_file' => storage_path('swoole/ssl/chat.kwva.cn.pem'),
'ssl_key_file' => storage_path('swoole/ssl/chat.kwva.cn.key'),
'log_file' => storage_path('swoole/swoole.log'),
]
];
- 创建 command。
php artisan make:command Swoole
- 再创建一个 handler 。
vim app\Qiubg\Handlers\SwooleWebsocketHandler.php
编写代码
服务端先要 new 一个 swoole_websocket_server 。
new swoole_websocket_server(config('swoole.host'), config('swoole.port'), SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
连接信息存储,可以使用 swoole_table ,也可以使用 redis。具体怎么做,看你哈。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use swoole_websocket_server;
class Swoole extends Command
{
protected $signature = 'swoole:action {action}';
protected $description = 'start or stop swoole';
private $server;
private $pid;
public function __construct()
{
parent::__construct();
$this->pid = storage_path('swoole/socket.pid');
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$arg = $this->argument('action');
switch ($arg) {
case 'start':
$this->start();
break;
case 'stop':
$this->stop();
break;
case 'reload':
$this->reload();
break;
default:
$this->info('no this command');
break;
}
}
private function start()
{
$pid = $this->getPid();
if ($pid && \Swoole\Process::kill($pid,0)) {
$this->info('progress already exist!');
exit;
}
$this->server = new swoole_websocket_server(config('swoole.host'), config('swoole.port'),SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$this->server->set([
'worker_num'=> 4, // 进程数
'daemonize' => 1, // 启用守护进程
'max_request'=> 1000, // work 进程最大任务数
'dispatch_mode'=> 2, // 数据包分发策略,默认 2,固定模式
'log_file' => storage_path('swoole/swoole.log'),
'pid_file' => $this->pid,
'ssl_cert_file' => storage_path('swoole/ssl/chat.kwva.cn.pem'),
'ssl_key_file' => storage_path('swoole/ssl/chat.kwva.cn.key')
]);
$handler = \App::make('Qiubg\Handlers\SwooleWebsocketHandler');
$this->server->on('open', array($handler, 'onOpen'));
$this->server->on('message', array($handler, 'onMessage'));
$this->server->on('close', array($handler, 'onClose'));
$this->info('progress created!');
$this->server->start();
}
private function stop()
{
$pid = $this->getPid();
if (!$pid) {
$this->info('progress not exist!');
exit;
}
$result = \Swoole\Process::kill((int)$pid);
if ($result) {
$this->info('process stop successfull!');
exit;
}
$this->info('process stop failed!');
}
private function reload()
{
$pid = $this->getPid();
if (!$pid) {
$this->info('progress not exist!');
exit;
}
$result = \Swoole\Process::kill((int)$pid);
if ($result) {
$this->info('process stop successfull!');
}
$this->start();
}
private function getPid()
{
return file_exists($this->pid) ? file_get_contents($this->pid) : false;
}
}
handler
<?php
namespace Qiubg\Handlers;
use Log;
use swoole_table;
class SwooleWebsocketHandler
{
private $table;
public function __construct()
{
$this->table = new \swoole_table(1024);
$this->table->column('room_id', swoole_table::TYPE_INT);
$this->table->column('fd', swoole_table::TYPE_INT);
$this->table->column('user_id', swoole_table::TYPE_INT);
$this->table->column('nickname', swoole_table::TYPE_STRING, 64);
$this->table->column('avatar', swoole_table::TYPE_STRING, 255);
$this->table->create();
}
public function onOpen($ws, $request)
{
}
public function allUsers($room_id)
{
$users = [];
foreach($this->table as $item) {
if ($item['room_id'] == $room_id) {
$users[] = $item;
}
}
return $users;
}
public function onMessage($ws, $frame)
{
$data = json_decode($frame->data, true);
switch($data['type']) {
case 'connect':
$user = [
'room_id' => $data['room_id'],
'fd' => $frame->fd,
'user_id' => $data['user_id'],
'nickname' => $data['nickname'],
'avatar' => $data['avatar']
];
$this->table->set($frame->fd, $user);
unset($user['fd']);
$users = $this->allUsers($user['room_id']);
foreach($this->table as $item) {
if ($item['user_id'] == $user['user_id']) {
$out = [
'users' => $users,
'user' => $user,
'type' => 'connect'
];
$ws->push($item['fd'], json_encode($out));
} else {
if ($item['room_id'] == $data['room_id']) {
$out = [
'user' => $user,
'type' => 'connect'
];
$ws->push($item['fd'], json_encode($out));
}
}
}
break;
case 'message':
$message = json_encode([
'type' => 'message',
'from' => [
'user_id' => $data['user_id'],
'nickname' => $data['nickname'],
'avatar' => $data['avatar']
],
'message' => is_string($data['message']) ? nl2br($data['message']) : $data['message']
]);
foreach($this->table as $item) {
if ($item['room_id'] == $data['room_id']) {
$ws->push($item['fd'], $message);
}
}
break;
case 'single':
$data = json_encode([
'type' => 'single',
'from' => [
],
'to' => [
],
'message' => is_string($data['message']) ? nl2br($data['message']) : $data['message']
]);
break;
case 'close':
break;
}
}
public function onClose($ws, $fd)
{
$user = $this->table->get($fd);
if($user) {
$this->table->del($fd);
foreach($this->table as $item) {
if ($item['room_id'] == $user['room_id']) {
$ws->push($item['fd'], json_encode(['type' => 'close', 'user' => $user]));
}
}
}
}
}
这里是服务端的 code 。
前端
ws = new WebSocket("wss://xxx.xxx.com:9508");
//发送房间号相关信息,以识别connect id
ws.onopen = function (evt) {
var data = {
room_id: room_id,
user_id: user_id,
nickname: nickname,
avatar: avatar,
type: 'connect'
};
console.log(data);
ws.send(JSON.stringify(data));
};
// 当有消息时根据消息类型显示不同信息
ws.onmessage = function (evt) {
var data = JSON.parse(evt.data);
console.log(data.type, data, data.message);
//判断是join还是message
if (data.type == 'message') {
}
}
ws.onclose = function () {
};
ws.onerror = function () {
};
启动
php artisan swoole:action start
php artisan swoole:action stop