Laravel 5.4 结合 Workerman 实现 TCP 长连接
发布时间:2019-01-02, 17:49:03 分类:PHP | 编辑 off 网址 | 辅助
正文 5348字数 2,430,256阅读
一、安装 workerman
在项目根目录执行
在项目根目录执行
composer require workerman/workerman
Run code
Cut to clipboard
二、创建自定义 artisan 命令来启动 workerman 服务
由于 laravel 不能直接在根目录下执行 php 命令,所以需要创建 artisan 命令用于后面 workerman 服务的开启。
1,生成 WorkermanCommand 文件
执行以上命令行会在 app/Console/Commands/ 目录下生成 WorkermanCommand.php 文件。
我只实现了 start 命令,其他命令童鞋们自行实现吧。
这里使用了 PHP 类方法的回调。(PHP几种回调写法)
这里我们创建了一个自定义命令 wk [action] ,通过此命令即可开启 workerman 服务。
由于 laravel 不能直接在根目录下执行 php 命令,所以需要创建 artisan 命令用于后面 workerman 服务的开启。
1,生成 WorkermanCommand 文件
php artisan make:command WorkermanCommand
Run code
Cut to clipboard
执行以上命令行会在 app/Console/Commands/ 目录下生成 WorkermanCommand.php 文件。
<?php
namespace App\Console\Commands;
use Workerman\Worker;
use Illuminate\Console\Command;
class WorkermanCommand extends Command
{
private $server;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'wk {action}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start a Workerman server.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
global $argv;
$arg = $this->argument('action');
$argv[1] = $argv[2];
$argv[2] = isset($argv[3]) ? "-{$argv[3]}" : '';
switch ($arg) {
case 'start':
$this->start();
break;
case 'stop':
break;
case 'restart':
break;
case 'reload':
break;
case 'status':
break;
case 'connections':
break;
}
}
private function start()
{
// 创建一个Worker监听20002端口,不使用任何应用层协议
$this->server = new Worker("tcp://0.0.0.0:20002");
// 启动4个进程对外提供服务
$this->server->count = 4;
$handler = \App::make('handlers\WorkermanHandler');
// 连接时回调
$this->server->onConnect = [$handler, 'onConnect'];
// 收到客户端信息时回调
$this->server->onMessage = [$handler, 'onMessage'];
// 进程启动后的回调
$this->server->onWorkerStart = [$handler, 'onWorkerStart'];
// 断开时触发的回调
$this->server->onClose = [$handler, 'onClose'];
// 运行worker
Worker::runAll();
}
}
Run code
Cut to clipboard
我只实现了 start 命令,其他命令童鞋们自行实现吧。
这里使用了 PHP 类方法的回调。(PHP几种回调写法)
这里我们创建了一个自定义命令 wk [action] ,通过此命令即可开启 workerman 服务。
注册命令
https://laravel-china.org/docs/laravel/5.4/artisan/1245#7ed55e
命令编写完成后,需要注册 Artisan 后才能使用。注册文件为 app/Console/Kernel.php 。在这个文件中, 你会在 commands 属性中看到一个命令列表。要注册你的命令,只需将其加到该列表中即可。当 Artisan 启动时,所有罗列在这个 属性的命令,都会被 服务容器 解析并向 Artisan 注册:
在这个自定义命令还引用了其他的类文件,如:
所以,需要创建一个 WorkermanHandler.php 的文件来处理对应的操作。
2,创建 WorkermanHandler.php
创建文件 app/handlers/WorkermanHandler.php
3,修改 composer.json 文件,让 app/handlers 文件夹下的类文件自动加载。
运行命令类文件自动加载:
至此。workman的命令定义已经完成。
使用:
如果看到以下内容,说明 workerman 服务启动正常:
https://www.jianshu.com/p/00623acb3dad
https://laravel-china.org/docs/laravel/5.4/artisan/1245#7ed55e
命令编写完成后,需要注册 Artisan 后才能使用。注册文件为 app/Console/Kernel.php 。在这个文件中, 你会在 commands 属性中看到一个命令列表。要注册你的命令,只需将其加到该列表中即可。当 Artisan 启动时,所有罗列在这个 属性的命令,都会被 服务容器 解析并向 Artisan 注册:
protected $commands = [
//
Commands\WorkermanCommand::class
];
Run code
Cut to clipboard
在这个自定义命令还引用了其他的类文件,如:
$handler = \App::make('handlers\WorkermanHandler');
Run code
Cut to clipboard
所以,需要创建一个 WorkermanHandler.php 的文件来处理对应的操作。
2,创建 WorkermanHandler.php
创建文件 app/handlers/WorkermanHandler.php
<?php
namespace handlers;
use Workerman\Lib\Timer;
// 心跳间隔10秒
define('HEARTBEAT_TIME', 10);
class WorkermanHandler
{
// 处理客户端连接
public function onConnect($connection)
{
echo "new connection from ip " . $connection->getRemoteIp() . "\n";
}
// 处理客户端消息
public function onMessage($connection, $data)
{
// 向客户端发送hello $data
$connection->send('Hello, your send message is: ' . $data);
}
// 处理客户端断开
public function onClose($connection)
{
echo "connection closed from ip {$connection->getRemoteIp()}\n";
}
public function onWorkerStart($worker)
{
Timer::add(1, function () use ($worker) {
$time_now = time();
foreach ($worker->connections as $connection) {
// 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $time_now;
continue;
}
// 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
if ($time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
echo "Client ip {$connection->getRemoteIp()} timeout!!!\n";
$connection->close();
}
}
});
}
}
Run code
Cut to clipboard
3,修改 composer.json 文件,让 app/handlers 文件夹下的类文件自动加载。
"autoload": {
"classmap": [
...
"app/handlers"
],
...
},
Run code
Cut to clipboard
运行命令类文件自动加载:
composer dump-autoload
Run code
Cut to clipboard
至此。workman的命令定义已经完成。
使用:
php artisan wk start
Run code
Cut to clipboard
如果看到以下内容,说明 workerman 服务启动正常:
Workerman[artisan] start in DEBUG mode
----------------------- WORKERMAN -----------------------------
Workerman version:3.5.4 PHP version:7.1.4
------------------------ WORKERS -------------------------------
user worker listen processes status
root none tcp://0.0.0.0:20002 1 [OK]
----------------------------------------------------------------
Press Ctrl+C to quit. Start success.
Run code
Cut to clipboard
https://www.jianshu.com/p/00623acb3dad
(支付宝)给作者钱财以资鼓励 (微信)→
有过 14 条评论 »
实例一、使用HTTP协议对外提供Web服务
创建http_test.php文件(位置任意,能引用到Workerman/Autoloader.php即可,下同)
<?php use Workerman\Worker; require_once __DIR__ . '/Workerman/Autoloader.php'; // 创建一个Worker监听2345端口,使用http协议通讯 $http_worker = new Worker("http://0.0.0.0:2345"); // 启动4个进程对外提供服务 $http_worker->count = 4; // 接收到浏览器发送的数据时回复hello world给浏览器 $http_worker->onMessage = function($connection, $data) { // 向浏览器发送hello world $connection->send('hello world'); }; // 运行worker Worker::runAll();
命令行运行(windows用户用 cmd命令行,下同)
php http_test.php
测试
假设服务端ip为127.0.0.1
在浏览器中访问url http://127.0.0.1:2345
实例二、使用WebSocket协议对外提供服务
创建ws_test.php文件
<?php use Workerman\Worker; require_once __DIR__ . '/Workerman/Autoloader.php'; // 注意:这里与上个例子不同,使用的是websocket协议 $ws_worker = new Worker("websocket://0.0.0.0:2000"); // 启动4个进程对外提供服务 $ws_worker->count = 4; // 当收到客户端发来的数据后返回hello $data给客户端 $ws_worker->onMessage = function($connection, $data) { // 向客户端发送hello $data $connection->send('hello ' . $data); }; // 运行worker Worker::runAll();
命令行运行
php ws_test.php start
测试
打开chrome浏览器,按F12打开调试控制台,在Console一栏输入(或者把下面代码放入到html页面用js运行)
// 假设服务端ip为127.0.0.1 ws = new WebSocket("ws://127.0.0.1:2000"); ws.onopen = function() { alert("连接成功"); ws.send('tom'); alert("给服务端发送一个字符串:tom"); }; ws.onmessage = function(e) { alert("收到服务端的消息:" + e.data); };
实例三、直接使用TCP传输数据
创建tcp_test.php
<?php use Workerman\Worker; require_once __DIR__ . '/Workerman/Autoloader.php'; // 创建一个Worker监听2347端口,不使用任何应用层协议 $tcp_worker = new Worker("tcp://0.0.0.0:2347"); // 启动4个进程对外提供服务 $tcp_worker->count = 4; // 当客户端发来数据时 $tcp_worker->onMessage = function($connection, $data) { // 向客户端发送hello $data $connection->send('hello ' . $data); }; // 运行worker Worker::runAll();
命令行运行
php tcp_test.php start
测试:命令行运行 (以下是linux命令行效果,与windows下效果有所不同)
telnet 127.0.0.1 2347 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. tom hello tom
composer update 这个命令在我们现在的逻辑中,可能会对项目造成巨大伤害。 因为 composer update 的逻辑是按照 composer.json 指定的扩展包版本规则,把所有扩展包更新到最新版本,注意,是 所有扩展包,
<?php namespace App\Console\Commands; use Workerman\Worker; use Illuminate\Console\Command; class WorkermanCommand extends Command { private $server; /** * The name and signature of the console command. * * @var string */ protected $signature = 'wk {action}'; /** * The console command description. * * @var string */ protected $description = 'Start a Workerman server.'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { global $argv; $arg = $this->argument('action'); $argv[1] = $argv[2]; $argv[2] = isset($argv[3]) ? "-{$argv[3]}" : ''; switch ($arg) { case 'start': $this->start(); break; case 'stop': break; case 'restart': break; case 'reload': break; case 'status': break; case 'connections': break; } } private function start() { // 证书最好是申请的证书 $context = array( // 更多ssl选项请参考手册 http://php.net/manual/zh/context.ssl.php 'ssl' => array( // 请使用绝对路径 'local_cert' => '/www/wdlinux/nginx/conf/cert/a.gengdian.net-ca-bundle.crt', // 也可以是crt文件 'local_pk' => '/www/wdlinux/nginx/conf/cert/a.gengdian.net.key', 'verify_peer' => false, //'verify_peer_name' => false, //'allow_self_signed' => true, //如果是自签名证书需要开启此选项 ) ); // 这里设置的是websocket协议(端口任意,但是需要保证没被其它程序占用) $worker = new Worker("websocket://0.0.0.0:20002",$context); // 设置transport开启ssl,websocket+ssl即wss $this->server->transport = 'ssl'; // 启动4个进程对外提供服务 $worker->count = 1; $worker->uidConnections = array(); $handler = \App::make('App\Handlers\WorkermanHandler'); // 连接时回调 $worker->onConnect = [$handler, 'onConnect']; // 收到客户端信息时回调 $worker->onMessage = [$handler, 'onMessage']; // 进程启动后的回调 $worker->onWorkerStart = [$handler, 'onWorkerStart']; // 断开时触发的回调 $worker->onClose = [$handler, 'onClose']; // 运行worker Worker::runAll(); } }
<template> <div class="test"> </div> </template> <script> export default { name : 'test', data() { return { websock: null, } }, created() { this.initWebSocket(); }, destroyed() { this.websock.close() //离开路由之后断开websocket连接 }, methods: { initWebSocket(){ //初始化weosocket const wsuri = "ws://127.0.0.1:8080"; this.websock = new WebSocket(wsuri); this.websock.onmessage = this.websocketonmessage; this.websock.onopen = this.websocketonopen; this.websock.onerror = this.websocketonerror; this.websock.onclose = this.websocketclose; }, websocketonopen(){ //连接建立之后执行send方法发送数据 let actions = {"test":"12345"}; this.websocketsend(JSON.stringify(actions)); }, websocketonerror(){//连接建立失败重连 this.initWebSocket(); }, websocketonmessage(e){ //数据接收 const redata = JSON.parse(e.data); }, websocketsend(Data){//数据发送 this.websock.send(Data); }, websocketclose(e){ //关闭 console.log('断开连接',e); }, }, } </script> <style lang='less'> </style>
nohup php a.php & 这样即使退出了终端,a.php依然在后台运行。
<?php namespace App\Console\Commands; use Workerman\Worker; use Illuminate\Console\Command; class WorkermanCommand extends Command { private $server; /** * The name and signature of the console command. * * @var string */ //protected $signature = 'wk {action}'; protected $signature = 'workman {action} {--d}'; /** * The console command description. * * @var string */ protected $description = 'Start a Workerman server.'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { global $argv; $action = $this->argument('action'); $argv[0] = 'wk'; $argv[1] = $action; $argv[2] = $this->option('d') ? '-d' : ''; // php artisan workman start --d switch ($action) { case 'start': $this->start(); break; case 'stop': break; case 'restart': break; case 'reload': break; case 'status': break; case 'connections': break; } } private function start() { // 证书最好是申请的证书 $context = array( // 更多ssl选项请参考手册 http://php.net/manual/zh/context.ssl.php 'ssl' => array( // 请使用绝对路径 'local_cert' => '/www/wdlinux/nginx/conf/cert/a.******.net-ca-bundle.crt', // 也可以是crt文件 'local_pk' => '/www/wdlinux/nginx/conf/cert/a.******.net.key', 'verify_peer' => false, //'verify_peer_name' => false, //'allow_self_signed' => true, //如果是自签名证书需要开启此选项 ) ); // 这里设置的是websocket协议(端口任意,但是需要保证没被其它程序占用) $worker = new Worker("websocket://0.0.0.0:20002",$context); // 设置transport开启ssl,websocket+ssl即wss //$this->server->transport = 'ssl'; // 启动4个进程对外提供服务 $worker->count = 1; $worker->uidConnections = array(); $handler = \App::make('App\Handlers\WorkermanHandler'); // 连接时回调 /*$worker->onWorkerStart = function($worker){ global $handler; // 开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符 $inner_text_worker = new Worker('Text://0.0.0.0:5678'); $inner_text_worker->onMessage = [$handler, 'onMessage']; $inner_text_worker->listen(); };*/ $worker->onConnect = [$handler, 'onConnect']; // 收到客户端信息时回调 $worker->onMessage = [$handler, 'onMessage']; // 进程启动后的回调 $worker->onWorkerStart = [$handler, 'onWorkerStart']; // 断开时触发的回调 $worker->onClose = [$handler, 'onClose']; // 运行worker Worker::runAll(); } }
Input "php wk stop" to stop. Start success.
<?php namespace App\Console\Commands; use Workerman\Worker; use Illuminate\Console\Command; class WorkermanCommand extends Command { private $server; /** * The name and signature of the console command. * * @var string */ //protected $signature = 'wk {action}'; protected $signature = 'workman {action} {--d}'; /** * The console command description. * * @var string */ protected $description = 'Start a Workerman server.'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { global $argv; $action = $this->argument('action'); $argv[0] = 'wk'; $argv[1] = $action; $argv[2] = $this->option('d') ? '-d' : ''; // php artisan workman start --d switch ($action) { case 'start': $this->start(); break; case 'stop': break; case 'restart': break; case 'reload': break; case 'status': break; case 'connections': break; } } private function start() { // 证书最好是申请的证书 $context = array( // 更多ssl选项请参考手册 http://php.net/manual/zh/context.ssl.php 'ssl' => array( // 请使用绝对路径 'local_cert' => '/www/wdlinux/nginx/conf/cert/a.***.net-ca-bundle.crt', // 也可以是crt文件 'local_pk' => '/www/wdlinux/nginx/conf/cert/a.***.net.key', 'verify_peer' => false, //'verify_peer_name' => false, //'allow_self_signed' => true, //如果是自签名证书需要开启此选项 ) ); // 这里设置的是websocket协议(端口任意,但是需要保证没被其它程序占用) $worker = new Worker("websocket://0.0.0.0:20002",$context); // 设置transport开启ssl,websocket+ssl即wss //$this->server->transport = 'ssl'; // 设置transport开启ssl $worker->transport = 'ssl'; // 启动4个进程对外提供服务 $worker->count = 1; $worker->uidConnections = array(); $handler = \App::make('App\Handlers\WorkermanHandler'); // 连接时回调 /*$worker->onWorkerStart = function($worker){ global $handler; // 开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符 $inner_text_worker = new Worker('Text://0.0.0.0:5678'); $inner_text_worker->onMessage = [$handler, 'onMessage']; $inner_text_worker->listen(); };*/ $worker->onConnect = [$handler, 'onConnect']; // 收到客户端信息时回调 $worker->onMessage = [$handler, 'onMessage']; // 进程启动后的回调 $worker->onWorkerStart = [$handler, 'onWorkerStart']; // 断开时触发的回调 $worker->onClose = [$handler, 'onClose']; // 运行worker Worker::runAll(); } }
小程序使用wss
微信小程序wss地址不允许使用端口,于是就利用nginx转发。
1、nginx版本是需要大于1.3
2、nginx配置文件修改
upstream websocket { server ip:端口; //转发 } server { listen 443; server_name 域名; ssl on; ssl_certificate 证书; ssl_certificate_key 证书; ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location /wss { access_log /usr/share/nginx/logs/https-websocket.log; proxy_pass http://websocket/; # 代理到上面 proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; rewrite /wss/(.*) /$1 break; proxy_redirect off; } }
$gateway = new Gateway("websocket://0.0.0.0:9993");
4、小程序那直接使用这个url : wss://域名/wss
水水水水
function getClientIP() { global $ip; if (getenv("HTTP_CLIENT_IP")) $ip = getenv("HTTP_CLIENT_IP"); else if(getenv("HTTP_X_FORWARDED_FOR")) $ip = getenv("HTTP_X_FORWARDED_FOR"); else if(getenv("REMOTE_ADDR")) $ip = getenv("REMOTE_ADDR"); else $ip = "Unknow"; return $ip; }
$roomUuid = 1; $chatInfo = DB::table('chat_info') ->where('chat_info.room_uuid', $roomUuid) ->leftJoin('user_rooms', function ($join) { $join->on('user_rooms.user_uuid', '=', 'chat_info.user_uuid') ->on('user_rooms.room_uuid', '=', 'chat_info.room_uuid'); })