1.安装基础代码
composer create-project laravel/laravel chatdemo composer require wokerman/wokerman
2.
php artisan make:command WokermanServer
app\Console\Commands\WokermanServer.php
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\App; use Workerman\Worker; class WokermanServer extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'wokerman {action} {--daemonize}'; /** * The console command description. * * @var string */ protected $description = 'workerman 启动停止'; /** * Execute the console command. * * @return int */ public function handle() { global $argv;//定义全局变量 $arg = $this->argument('action'); $argv[1] = $arg; $argv[2] = $this->option('daemonize') ? '-d' : '';//该参数是以daemon(守护进程)方式启动 global $text_worker; // 创建一个Worker监听2345端口,使用websocket协议通讯 $text_worker = new Worker("websocket://0.0.0.0:2345"); $text_worker->uidConnections = array();//在线用户连接对象 $text_worker->uidInfo = array();//在线用户的用户信息 // 启动4个进程对外提供服务 $text_worker->count = 4; //引用类文件 $handler = App::make('Handler\WokermanHandler'); $text_worker->onConnect = array($handler,"handle_connection"); $text_worker->onMessage = array($handler,"handle_message"); $text_worker->onClose = array($handler,"handle_close"); $text_worker->onWorkerStart = array($handler,"handle_start"); // 运行worker Worker::runAll(); return 0; } }
在app下新建Handler目录,然后新建 WokermanHandler.php
<?php namespace Handler; use Workerman\Lib\Timer; class WokermanHandler { private $heartbeat_time = 55;//心跳间隔55秒 //当客户端连上来时分配uid,并保存链接,并通知所有客户端 public function handle_connection($connection) { global $text_worker; //判断是否设置了UID if (!isset($connection->uid)) { //给用户分配一个UID $connection->uid = $this->random_string(); //保存用户的uid $text_worker->uidConnections[$connection->uid] = $connection; //向用户返回创建成功的信息 $connection->send("用户:[".$connection->uid."] 创建成功"); } } public function handle_start() { global $text_worker; //每秒都判断客户端是否已下线 Timer::add(1, function () use ($text_worker) { $time_now = time(); foreach ($text_worker->connections as $connection) { // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间 if (empty($connection->lastMessageTime)) { $connection->lastMessageTime = $time_now; continue; } // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接 if ($time_now - $connection->lastMessageTime > $this->heartbeat_time) { $connection->close(); } } }); //每隔30秒就向客户端发送一条心跳验证 Timer::add(50, function () use ($text_worker) { foreach ($text_worker->connections as $conn) { $conn->send('{"type":"ping"}'); } }); } //当客户端发送消息过来时,转发给所有人 public function handle_message($connection, $data) { global $text_worker; //debug //echo "data_info:".$data.PHP_EOL; $connection->lastMessageTime = time(); $data_info=json_decode($data, true); if (!$data_info) { $connection->send("消息格式错误"); return ; } //判断业务类型 switch ($data_info['type']) { case 'login': //判断用户信息是否存在 if (empty($data_info['user_id'])) { $connection->send("{'type':'error','msg':'非法请求'}"); return $connection->close(); } //判断用户是否已经登录了 $user_ids=array_column($text_worker->uidInfo, "user_id"); if (in_array($data_info['user_id'], $user_ids)) { $connection->send("{'type':'error','msg':'你在其它地方已登录'}"); return $connection->close(); } //存储用户信息 $text_worker->uidInfo[$connection->uid]=array( "user_id"=>$data_info['user_id'], "user_name"=>htmlspecialchars($data_info['user_name']), "create_time"=>date("Y-m-d H:i"), ); //返回数据 if (isset($data_info['to_uid']) && $data_info['to_uid'] == "all") { $return_data=array( "type"=>"login", "uid"=>$connection->uid, "user_name"=>htmlspecialchars($data_info['user_name']), "send_time"=>date("Y-m-d H:i", time()), "user_lists"=>$text_worker->uidInfo ); $curral_data=array( "type"=>"login_uid", "uid"=>$connection->uid, ); $connection->send(json_encode($curral_data)); //给所有用户发送一条数据 foreach ($text_worker->connections as $conn) { $conn->send(json_encode($return_data)); } } else { return ; } return; //用户发消息 case 'say': if (!isset($text_worker->uidInfo[$connection->uid]) || empty($text_worker->uidInfo[$connection->uid])) { $connection->send('{"type":"error","msg":"你已经掉线了"}'); } //获取到当前用户的信息 $user_info=$text_worker->uidInfo[$connection->uid]; //判断是私聊还是群聊 if ($data_info['to_uid'] != "all") { //私聊 $return_data=array( "type"=>"say", "from_uid"=>$connection->uid, "from_user_name"=>$user_info['user_name'], "to_uid"=>$data_info['to_uid'], "content"=>nl2br(htmlspecialchars($data_info['content'])), "send_time"=>date("Y-m-d H:i") ); if ($data_info['to_uid'] == $connection->uid) { $connection->send(json_encode($return_data)); return; } //判断用户是否存在,并向对方发送数据 if (isset($text_worker->uidConnections["{$data_info['to_uid']}"])) { $to_connection=$text_worker->uidConnections["{$data_info['to_uid']}"]; $to_connection->send(json_encode($return_data)); } //向你自己发送一条数据 $connection->send(json_encode($return_data)); } else { //群聊 $return_data=array( "type"=>"say", "from_uid"=>$connection->uid, "from_user_name"=>$user_info['user_name'], "to_uid"=>"all", "content"=>nl2br(htmlspecialchars($data_info['content'])), "send_time"=>date("Y-m-d H:i") ); //向所有用户发送数据 foreach ($text_worker->connections as $conn) { $conn->send(json_encode($return_data)); } } return; case "pong": return; } } //当客户端断开时,广播给所有客户端 public function handle_close($connection) { global $text_worker; $user_name=$text_worker->uidInfo[$connection->uid]['user_name'] ?? ""; unset($text_worker->uidConnections[$connection->uid]); unset($text_worker->uidInfo[$connection->uid]); if (!empty($user_name)) { $return_data=array( "type"=>"logout", "uid"=>$connection->uid, "user_name"=>$user_name, "create_time"=>date("Y-m-d H:i:s"), ); foreach ($text_worker->connections as $conn) { $conn->send(json_encode($return_data)); } } } public function random_string() { return substr(uniqid('', true), 15).substr(microtime(), 2, 8); } }
3.修改composer。json 增加handler映射
"autoload": { "psr-4": { "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/", "Handler\\": "app/Handler/" } },
启动websocker服务 ,应该不会报错
composer update php artisan wokerman start
成功的截图
4.写个html调用一下
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>测试websocket</title> </head> <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js" type="text/javascript" charset="utf-8"></script> <body> <div id="chat_list"> <p></p> </div> <input type="text" value="" id="chat_msg"> <button type="button" id="send">发送</button> <script type="text/javascript"> var wsurl = "ws://127.0.0.1:2345"; var ws = new WebSocket(wsurl); ws.onopen = function(){ var params = {} params.type = 'login' params.user_id = Math.ceil(Math.random()*100) params.user_name = '测试用户'+params.user_id params.to_uid = '' ws.send(JSON.stringify(params)) console.log('注册成功',JSON.stringify(params)) } ws.onmessage = function(evt){ console.log('返回的信息data',evt.data) if(msg = parseJSON(evt.data)){ console.log('返回的信息msg',msg) if(msg.type == 'say'){ $("#chat_list").append("<p>"+msg.content+"</p>") } } } ws.onclose = function(){ console.log('关闭了') } $().ready(function(){ $("#send").click(function(){ var m = $("#chat_msg").val(); if(m == ''){ alert('消息不能为空') return false; } params = {} params.type = 'say' params.to_uid = 'all' params.content = m ws.send(JSON.stringify(params)) console.log('发送了消息',JSON.stringify(params)) $("#chat_msg").val('') }) }) function parseJSON(str) { if (typeof str == 'string') { try { return JSON.parse(str); } catch(e) { return false; } } console.log('It is not a string!') } </script> </body> </html>
本文为看恩吧原创文章,转载无需和我联系,但请注明来自knsay.com