本文有相对完整的集成代码和前端测试代码,以及服务器启动注意事项
一、后端(laravel5.5)
1、composer安装Workerman
composer require workerman/workerman
2、生成执行命令,执行后会在App\Console\Command文件夹下生成一个Workerman.php的文件
php artisan make:command Workerman
3、打开Workerman.php的文件,并修改代码,如下:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Tymon\JWTAuth\JWTAuth;
use Workerman\Timer;
use Workerman\Worker;
// 心跳间隔55秒
define('HEARTBEAT_TIME', 3600);
class Workerman extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'Workerman {action} {--daemonize}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* 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] = $arg;
$argv[2] = $this->option('daemonize') ? '-d' : '';//该参数是以daemon(守护进程)方式启动
global $text_worker;
// 创建一个Worker监听9991端口,使用websocket协议通讯
$text_worker = new Worker("websocket://0.0.0.0:9991");
$text_worker->uidConnections = array();//在线用户连接对象
$text_worker->uidInfo = array();//在线用户的用户信息
// 启动4个进程对外提供服务
$text_worker->count = 1;
//当启动workerman的时候 触发此方法
$text_worker->onWorkerStart =function(){
//监听一个内部端口,用来接收服务器的消息,转发给浏览器
$inner_text_worker = new Worker('text://127.0.0.1:5678');
$inner_text_worker->onMessage = function($connection_admin, $data)
{
global $text_worker;
// $data数组格式,里面有uid,表示向那个uid的页面推送数据
$buffer = json_decode($data, true);
//var_dump($buffer);
$to_uid = $buffer['to_uid'];
//var_dump($to_uid);
// 通过workerman,向uid的页面推送数据
if(isset($text_worker->uidConnections[$to_uid])){
$connection = $text_worker->uidConnections[$to_uid];
$ret = $connection->send($data);
} else {
var_dump($to_uid . ': not define');
$ret = false;
}
// 返回推送结果
$connection_admin->send($ret ? 'ok' : 'fail');
};
$inner_text_worker->listen();
// 进程启动后设置一个每10秒运行一次的定时器
Timer::add(10, function(){
global $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 > HEARTBEAT_TIME) {
var_dump("delete:" . $connection->uid);
unset($text_worker->uidConnections["{$connection->uid}"]);
$connection->close();
}
}
});
};
//当浏览器连接的时候触发此函数
$text_worker->onConnect = function($connection) {
};
//向用户发送信息的时候触发
//$connection 当前连接的人的信息 $data 发送的数据
$text_worker->onMessage = function($connection,$data){
// 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间
$connection->lastMessageTime = time();
// 其它业务逻辑...
$data = json_decode($data, true);
if($data['type']=='login') {
$this->create_uid($connection,$data);
}
if($data['type']=='send_message'){
$this->send_message($connection,$data);
}
};
//浏览器断开链接的时候触发
$text_worker->onClose = function($connection){};
Worker::runAll();
}
//创建uid方法
public function create_uid($connection,$data){
global $text_worker;
$connection->uid = $data['uid'];
//保存用户的uid
$text_worker->uidConnections["{$connection->uid}"] = $connection;
//向自己的浏览器返回创建成功的信息
$connection->send(json_encode([
"uid"=>$connection->uid,
"msgType"=>'text',
"content"=>'聊天创建成功,您可以开发发送消息了'
]));
}
public function send_message($connection,$data) {
global $text_worker;
if(isset($data['to_uid'])){
if(isset($text_worker->uidConnections["{$data['to_uid']}"])){
$to_connection=$text_worker->uidConnections["{$data['to_uid']}"];
$to_connection->send(json_encode([
"uid"=>$data['uid'],
"msgType"=>$data['msgType'],
"content"=>$data['message']
]));
}
}
}
}
4、修改App\Console下面的Kernel.php文件,如下:
protected $commands = [
Commands\Workerman::class,
];
5、启动Workerman服务,启动命令如下:
php artisan Workerman start
出现上图所示,则表示启动成功
二、前端如何连接Workerman服务(websocket)服务
1、新建两个html页面用于测试,当然,也可以直接在浏览器打开F12在console里面测试,我这里有写好的页面,直接换个ws地址就能用,支持发送文字,图片
37websoket.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script> <!-- 引入vue.js -->
</head>
<body>
<div id="app">
<div style="padding: 10px;height: 76vh;overflow-y: scroll;" ref="scrollDiv">
<div v-for="(item,index) in content" :key="index">
<div class="left-box" v-if="item.uid == 38">
<div>
<img src="./nv.png" style="width:35px;flex-shrink: 0;">
</div>
<div class="msg-box-l" v-if="item.msgType=='text'">
<div class="msg-text-l">{{item.content}}</div>
</div>
<div class="msg-box-l" v-if="item.msgType=='img'" style="max-width:40%;background: none;box-shadow: none;">
<img :src="item.content" style="width: 100%;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
</div>
</div>
<div class="right-box" v-else>
<div class="msg-box-r" v-if="item.msgType=='text'">
<div class="msg-text-r">{{item.content}}</div>
</div>
<div class="msg-box-r" v-if="item.msgType=='img'" style="max-width:40%;background: none;box-shadow: none;">
<img :src="item.content" style="width: 100%;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
</div>
<div>
<img src="./nan.png" style="width:35px;flex-shrink: 0;">
</div>
</div>
</div>
</div>
<div class="ta">
<textarea type="text" name="" v-model="msg" class="ta1"></textarea>
<div class="r-b">
<input type="file" id="file" @change="sendPic" class="input-file" />
<div @click="send" class="send-btn">发消息</div>
<label for="file" class="send-btn">发图片</label>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
content:[],
msg: "",
path:"ws://127.0.0.1:9991//ws",
socket:""
},
mounted () {
// 初始化
this.init()
},
methods:{
scrollToBottom() {
this.$nextTick(() => {
let scrollElem = this.$refs.scrollDiv;
scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' });
});
},
sendPic(file) {
console.log(file)
const reader = new FileReader();
reader.readAsDataURL(file.target.files[0]);
reader.onload = () => {
// const src = reader.result;
this.content.push({
uid: 37,
msgType: 'img',
content: reader.result
})
this.socket.send(`{"type":"send_message","to_uid":38,"uid":37,"msgType":"img", "message":"${reader.result}"}`)
this.scrollToBottom()
// 这里的reader.result就是文件的base64了。如果是图片的话,就可以直接放到src中展示了
};
},
init: function () {
if(typeof(WebSocket) === "undefined"){
alert("您的浏览器不支持socket")
}else{
// 实例化socket
this.socket = new WebSocket(this.path)
// 监听socket连接
this.socket.onopen = this.open
// 监听socket错误信息
this.socket.onerror = this.error
// 监听socket消息
this.socket.onmessage = this.getMessage
}
},
open: function () {
this.socket.send('{"uid":37,"type":"login"}');
console.log("socket连接成功")
},
error: function () {
console.log("连接错误")
},
getMessage: function (msg) {
this.content.push({
uid: JSON.parse(msg.data).uid,
msgType: JSON.parse(msg.data).msgType,
content: JSON.parse(msg.data).content
})
this.scrollToBottom()
console.log(msg.data)
},
// 发送消息给被连接的服务端
send: function () {
this.content.push({
uid: 37,
msgType: 'text',
content: this.msg
})
this.socket.send(`{"type":"send_message","to_uid":38,"uid":37,"msgType":"text","message":"${this.msg.replace(/\n/g,"\\n").replace(/\r/g,"\\r")}"}`)
this.scrollToBottom()
this.msg = ""
},
close: function () {
console.log("socket已经关闭")
}
},
// destroyed () {
// // 销毁监听
// this.socket.onclose = this.close
// }
});
</script>
</body>
<style type="text/css">
html,body{margin: 0;padding:0}
#app{height: 100vh;background: #F5F5F5}
.left-box {display: flex;flex-shrink: 0;margin-bottom: 10px;margin-top: 10px;}
.right-box {display: flex;justify-content: flex-end;width: 100%;margin-bottom: 10px;margin-top: 10px;}
.msg-box-l{background: #fff;max-width: 80%;padding: 8px;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;margin-left: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
.msg-box-r{background: #95EC69;max-width: 80%;padding: 8px;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;margin-right: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
.msg-text-l{color: #000;word-break: break-all;}
.msg-text-r{color: #000;word-break: break-all;}
.ta{resize: none;width: 100%;height: 100px;position: fixed;bottom: 20px;}
.ta1{resize: none;width: 70%;margin-left:5%;height: 100px;position: fixed;bottom: 20px;}
.r-b{display: flex;align-items: flex-end;flex-direction: column;}
.send-btn{background: #4287FD;color: #fff;padding: 5px 10px;margin: 10px;}
.input-file {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
</style>
</html>
38websoket.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script> <!-- 引入vue.js -->
</head>
<body>
<div id="app">
<div style="padding: 10px;height: 76vh;overflow-y: scroll;" ref="scrollDiv">
<div v-for="(item,index) in content" :key="index">
<div class="left-box" v-if="item.uid == 37">
<div>
<img src="./nv.png" style="width:35px;flex-shrink: 0;">
</div>
<div class="msg-box-l" v-if="item.msgType=='text'">
<div class="msg-text-l">{{item.content}}</div>
</div>
<div class="msg-box-l" v-if="item.msgType=='img'" style="max-width:40%;">
<img :src="item.content" style="width: 100%;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
</div>
</div>
<div class="right-box" v-else>
<div class="msg-box-r" v-if="item.msgType=='text'">
<div class="msg-text-r">{{item.content}}</div>
</div>
<div class="msg-box-r" v-if="item.msgType=='img'" style="max-width:40%;background: none;box-shadow: none;">
<img :src="item.content" style="width: 100%;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;">
</div>
<div>
<img src="./nan.png" style="width:35px;flex-shrink: 0;">
</div>
</div>
</div>
</div>
<div class="ta">
<textarea type="text" name="" v-model="msg" class="ta1"></textarea>
<div class="r-b">
<input type="file" id="file" @change="sendPic" class="input-file" />
<div @click="send" class="send-btn">发消息</div>
<label for="file" class="send-btn">发图片</label>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
content:[],
msg: "",
path:"ws://127.0.0.1:9991//ws",
socket:""
},
mounted () {
// 初始化
this.init()
},
methods:{
scrollToBottom() {
this.$nextTick(() => {
let scrollElem = this.$refs.scrollDiv;
scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' });
});
},
sendPic(file) {
console.log(file)
const reader = new FileReader();
reader.readAsDataURL(file.target.files[0]);
reader.onload = () => {
// const src = reader.result;
this.content.push({
uid: 38,
msgType: 'img',
content: reader.result
})
this.socket.send(`{"type":"send_message","to_uid":37,"uid":38,"msgType":"img", "message":"${reader.result}"}`)
this.scrollToBottom()
// 这里的reader.result就是文件的base64了。如果是图片的话,就可以直接放到src中展示了
};
},
init: function () {
if(typeof(WebSocket) === "undefined"){
alert("您的浏览器不支持socket")
}else{
// 实例化socket
this.socket = new WebSocket(this.path)
// 监听socket连接
this.socket.onopen = this.open
// 监听socket错误信息
this.socket.onerror = this.error
// 监听socket消息
this.socket.onmessage = this.getMessage
}
},
open: function () {
this.socket.send('{"uid":38,"type":"login"}');
console.log("socket连接成功")
},
error: function () {
console.log("连接错误")
},
getMessage: function (msg) {
this.content.push({
uid: JSON.parse(msg.data).uid,
msgType: JSON.parse(msg.data).msgType,
content: JSON.parse(msg.data).content
})
this.scrollToBottom()
console.log(msg.data)
},
// 发送消息给被连接的服务端
send: function () {
this.content.push({
uid: 38,
msgType: 'text',
content: this.msg
})
this.socket.send(`{"type":"send_message","to_uid":37,"uid":38,"msgType":"text","message":"${this.msg.replace(/\n/g,"\\n").replace(/\r/g,"\\r")}"}`)
this.scrollToBottom()
this.msg = ""
},
close: function () {
console.log("socket已经关闭")
}
},
// destroyed () {
// // 销毁监听
// this.socket.onclose = this.close
// }
});
</script>
</body>
<style type="text/css">
html,body{margin: 0;padding:0}
#app{height: 100vh;background: #F5F5F5}
.left-box {display: flex;flex-shrink: 0;margin-bottom: 10px;margin-top: 10px;}
.right-box {display: flex;justify-content: flex-end;width: 100%;margin-bottom: 10px;margin-top: 10px;}
.msg-box-l{background: #fff;max-width: 80%;padding: 8px;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;margin-left: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
.msg-box-r{background: #95EC69;max-width: 80%;padding: 8px;border-top-left-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;margin-right: 10px;margin-top: 10px;box-shadow: 0 2px 11px -7px rgba(2,66,58,.5)}
.msg-text-l{color: #000;word-break: break-all;}
.msg-text-r{color: #000;word-break: break-all;}
.ta{resize: none;width: 100%;height: 100px;position: fixed;bottom: 20px;}
.ta1{resize: none;width: 70%;margin-left:5%;height: 100px;position: fixed;bottom: 20px;}
.r-b{display: flex;align-items: flex-end;flex-direction: column;}
.send-btn{background: #4287FD;color: #fff;padding: 5px 10px;margin: 10px;}
.input-file {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
</style>
</html>
分别保存为两个html文件,两个都在浏览器打开,页面上出现 “聊天创建成功,您可以开发发送消息了”,则说明连接成功,可以正常发消息了
至此调试完成!
三、在服务器运行Workerman服务
我们本地调试完成后,最终需要上传到服务器上去运行,例如:Centos,服务上也需要composer安装Workerman,方式和最开头一样,我直接说说如何启动
在服务器上启动Workerman和在本地有一定的区别,因为通常我们希望在服务器上,Websocket能一直处于启动状态,所以,上述所说的启动命令,要稍微改一下,即在命令后加上:--daemonize(守护进程启动)
php artisan Workerman start --daemonize (关掉终端,服务不会停止)
php artisan Workerman stop --daemonize (服务停止)
php artisan Workerman start (关掉终端,服务会停止)
php artisan Workerman stop (服务停止)
至此,laravel集成Workerman搭建websocket服务已完成