1、拉取laravel项目
composer create-project laravel/laravel
2、安装predis和guzzlehttp
composer require guzzlehttp/guzzle
composer require predis/predis
3 、创建WebSocketServerCommand
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use swoole_websocket_server;
class WebSocketServerCommand extends Command
{
public $ws;
protected $userListCacheKey = 'socket_user_list';
protected $signature = 'swoole {action}';
protected $description = 'websocket 服务';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$arg = $this->argument('action');
switch ($arg) {
case 'start':
$this->info('swoole server started');
$this->start();
break;
case 'stop':
$this->info('swoole server stoped');
$this->stop();
break;
case 'restart':
$this->info('swoole server restarted');
$this->restart();
break;
}
}
private function start()
{
Redis::del($this->userListCacheKey);
$this->ws = new swoole_websocket_server('0.0.0.0', config('websocket.port'));
$this->ws->on('open', function ($ws, $request) {
$userId = $request->fd;
Redis::hset($this->userListCacheKey, $request->fd, $userId);
$this->info("client $request->fd is connected\n");
$this->pushMessage($request->fd, "welcome!client $request->fd connected!\n");
});
$this->ws->on('message', function ($ws, $frame) {
$this->receiveMessageHandler($frame->fd, $frame->data);
});
$this->ws->on('request', function ($request, $response) {
$this->messageHandler($request->post);
Log::info(\GuzzleHttp\json_encode($request->post));
$response->end('ok');
});
$this->ws->on('close', function ($ws, $fd) {
if (Redis::hexists($this->userListCacheKey, strval($fd))) {
Redis::hdel($this->userListCacheKey, [strval($fd)]);
}
$this->info("client $fd is close\n");
});
$this->ws->start();
}
private function stop()
{
exec('ps -ef | grep \'swoole start\'', $result);
if ($result) {
foreach ($result as $item) {
$process = explode(' ', preg_replace('#\s{2,}#', ' ', $item));
if (isset($process[1])) {
$command = 'exec kill ' . $process[1];
$this->info($command);
exec($command);
}
}
}
$command = 'rm -rf ' . base_path('storage/framework/cache/*');
$this->info($command);
exec($command);
}
private function restart()
{
$this->stop();
$this->start();
}
private function receiveMessageHandler($fd, $message)
{
if ($message === 'ping') {
@$this->ws->push($fd, 'ping');
} else {
if (json_decode($message)) {
$message = json_decode($message);
if ($message->to > 0) {
$this->pushMessage($message->to, $message->msg, $fd);
} else {
$this->info("receive client $fd message:" . $message->to);
$this->pushMessageToAll($message->msg);
}
} else {
$this->pushMessageToAll($message);
}
}
}
private function messageHandler($data)
{
if (isset($data['type']) && in_array($data['type'], ['single', 'broadcast', 'group']) && isset($data['msg']) && $data['msg']) {
if ('single' == $data['type'] && isset($data['fd']) && is_int((int)$data['fd']) && $data['fd'] > 0) {
$this->pushMessage((int)$data['fd'], $data['msg']);
}
if ('broadcast' == $data['type']) {
$this->pushMessageToAll($data['msg']);
}
if ('group' == $data['type'] && isset($data['groupId']) && $data['groupId']) {
$fdList = Redis::HGETALL($this->userListCacheKey . "_" . $data['groupId']);
if ($fdList) {
foreach ($fdList as $fd) {
$this->pushMessage($fd, $data['msg']);
}
}
}
}
$this->info("send to " . $data['type'] . " messages: " . $data['msg']);
}
private function pushMessageToAll($message)
{
$fdList = Redis::HGETALL($this->userListCacheKey);
if ($fdList) {
foreach ($fdList as $fd) {
$this->info("send to " . $fd . " messages: " . $message);
$this->pushMessage($fd, $message);
}
}
}
private function pushMessage($fd, $message, $from = 'sys', $type = 'broadcast')
{
$fd = (int)$fd;
if ($this->ws->isEstablished($fd)) {
@$this->ws->push($fd, json_encode([
'type' => $type,
'from' => $from,
'msg' => $message,
]));
}
}
}
4、设置推送接口路由和方法
Route::post('/message', 'MessagePushController@message');
5、手动推送代码
public function message(Request $request)
{
$this->validate($request, [
'msg' => 'string|required',
'type' => 'string|required|in:single,broadcast,group',
'fd' => 'nullable|required_if:type,single|integer',
'groupId' => 'nullable|required_if:type,group|integer',
]);
$client = new Client(['base_uri' => 'http://127.0.0.1:'.config('websocket.port')]);
$response = $client->request('POST', '/post', [
'form_params' => $request->all()
]);
return new JsonResponse(['status' => $response->getStatusCode() == 200 ? 'success' : 'fail']);
}
6、创建聊天页面的路由
Route::get('/chat', function () {
return view('chatRoom');
});
7、创建聊天页面
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
<script type="text/javascript" src="js/app.js"></script>
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Nunito', sans-serif;
font-weight: 200;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.room {
width: 420px;
height: 600px;
border: #4e555b 1px solid;
overflow-y: scroll;
}
.content {
text-align: center;
}
.title {
font-size: 42px;
}
.links > a {
color: #636b6f;
padding: 0 25px;
font-size: 13px;
font-weight: 600;
letter-spacing: .1rem;
text-decoration: none;
text-transform: uppercase;
}
.m-b-md {
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title m-b-md">
websocket chat
</div>
<div class="room">
</div>
<div class="input-group" style="margin-top:10px;">
<input type="text" height="160px" id="input-message" placeholder="your message"
style="float: left;width: 60%;line-height: 30px">
<input type="number" height="160px" id="to" placeholder="to"
style="float: left;width: 10%;line-height: 30px">
<button type="submit" id="btn" style="float: right;width: 50px;line-height: 30px">发送</button>
</div>
</div>
</div>
</body>
<script>
$("#btn").click(function (event) {
var message = $('#input-message').val();
var to = $('#to').val();
console.log('to:' + to);
console.log("send message :" + message);
SendData(message, to);
});
var url = 'ws://127.0.0.1:9502';
var ws;
var lockReconnect = false;
createWebSocket(url);
function createWebSocket(url) {
try {
if ('WebSocket' in window) {
console.log("连接 WebSocket");
ws = new WebSocket(url);
} else if ('MozWebSocket' in window) {
console.log("尝试重新连接 MozWebSocket");
ws = new MozWebSocket(url);
} else {
alert("您的浏览器不支持websocket")
}
ws.onopen = function (event) {
heartCheck.start();
console.log("已经与服务器建立了连接\r\n当前连接状态:" + this.readyState);
};
ws.onmessage = function (event) {
heartCheck.reset().start();
if (event.data !== 'ping') {
buildReceiveMessage(event.data);
console.log("接收到服务器发送的数据:\r\n" + event.data);
}
};
ws.onclose = function (event) {
console.log("已经与服务器断开连接\r\n当前连接状态:" + this.readyState);
reconnect(url);
};
ws.onerror = function (event) {
console.log("WebSocket异常!");
reconnect(url);
};
} catch (e) {
reconnect(url);
console.log(e);
}
}
function reconnect(url) {
if (lockReconnect) return;
lockReconnect = true;
setTimeout(function () {
console.log("尝试重新连接");
createWebSocket(url);
lockReconnect = false;
}, 2000);
}
function isJSON(str) {
if (typeof str == 'string') {
try {
let obj = JSON.parse(str);
if (typeof obj == 'object') {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
}
function SendData(message, to) {
try {
if (message) {
console.log("发送数据:" + message);
var param = {};
param.msg = message;
param.to = to;
ws.send(JSON.stringify(param));
buildSendMessage(message);
}
} catch (ex) {
alert(ex.message);
}
}
function buildReceiveMessage(message) {
message = JSON.parse(message);
$('.room').append("<div style='width: 100%;text-align: left;'><div style='font-size: xx-small;margin-right: 5px;padding-left: 5px'>" + message.from + "</div><div style='display:inline-block ;border: #4e555b 1px solid;background-color: #d6e9f8;text-align: left;color: #1d2124;font-weight: bold;margin: 0 5px 10px 5px;border-radius: 8px;padding: 2px 5px'>" + message.msg + "</div></div>");
}
function buildSendMessage(message) {
$('.room').append("<div style='width: 100%;text-align: right;'><div style='font-size: xx-small;margin-right: 5px;padding-right: 5px'>我</div><div style='display:inline-block ;border: #4e555b 1px solid;background-color: #2a9055;text-align: right;color: #1d2124;font-weight: bold;margin: 0 5px 10px 5px;border-radius: 8px;padding: 2px 5px'>" + message + "</div></div>");
}
var heartCheck = {
timeout: 15000,
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
var self = this;
this.timeoutObj = setTimeout(function () {
ws.send("ping");
console.log("ping!");
self.serverTimeoutObj = setTimeout(function () {
console.log("try=close");
ws.close();
}, self.timeout)
}, this.timeout)
}
};
</script>
</html>
websocket操作命令
php artisan swoole start
php artisan swoole stop
php artisan swoole restart