Ⅰ.序
此篇是接着这篇写滴://www.greatytc.com/p/6c9ad29c552b;
目的是一个搭小聊天室。
Ⅱ.安装swoole扩展
再上一遍的基础上我先安装了sockets calendar exif pcntl shmop sysvmsg sysvsem sysvshm wddx这些扩展,文件
laravel/docker/Dockerfile
,在上一步安装php扩展的下方添加,感觉只有sockets是需要的,谁知道呢...都给整上:
RUN set -xe \
&& docker-php-ext-configure calendar --enable-calendar \
&& docker-php-ext-configure exif --enable-exif \
&& docker-php-ext-configure pcntl --enable-pcntl \
&& docker-php-ext-configure shmop --enable-shmop \
&& docker-php-ext-configure sysvmsg --enable-sysvmsg \
&& docker-php-ext-configure sysvsem --enable-sysvsem \
&& docker-php-ext-configure sysvshm --enable-sysvshm \
&& docker-php-ext-configure wddx --enable-wddx \
&& docker-php-ext-configure sockets --enable-sockets \
&& docker-php-ext-install sockets calendar exif pcntl shmop sysvmsg sysvsem sysvshm wddx
安装swoole扩展
- 文件
laravel/docker/Dockerfile
添加:
RUN set -xe \
&& pecl install -o -f swoole \
&& docker-php-ext-enable swoole
- 报错,大致是这样说的(忘记截图了):错误:加载公共库libstdc++.so.6 文件找不到或者不存在...swoole.so...找不到或者不存在。但其实swoole.so是有的,只是加载出来要libstdc++库。所以:
RUN set -xe \
&& apk add --no-cache --virtual .persistent-deps \
libstdc++
安装LaravelS
composer require hhxsv5/laravel-s
php artisan laravels publish
配置 .env
LARAVELS_LISTEN_IP=workspace
LARAVELS_DAEMONIZE=true
实现 WebSocket 服务器
<?php
namespace App\Services;
use App\Events\Chat;
use App\User;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
class WebSocketService implements WebSocketHandlerInterface
{
const MSG_TYPE_JOIN = 1;
const MSG_TYPE_TEXT = 2;
const MSG_TYPE_LEAVE = 3;
const AGENT_PC_WEB = 1;
const AGENT_PHONE_WEB = 2;
const AGENT_PC_CLIENT = 3;
const AGENT_PHONE_CLIENT = 4;
/**
* @var User
*/
protected $user;
protected $room;
private $agents;
public function __construct()
{
$this->agents = array(
self::AGENT_PC_WEB,
self::AGENT_PHONE_WEB,
self::AGENT_PC_CLIENT,
self::AGENT_PHONE_CLIENT,
);
}
// 连接建立时触发
public function onOpen(Server $server, Request $request)
{
// 在触发 WebSocket 连接建立事件之前,Laravel 应用初始化的生命周期已经结束,你可以在这里获取 Laravel 请求和会话数据
// 调用 push 方法向客户端推送数据,fd 是客户端连接标识字段
Log::info('WebSocket 连接建立');
// $server->push($request->fd, 'Welcome to WebSocket Server built on LaravelS');
}
// 收到消息时触发
public function onMessage(Server $server, Frame $frame)
{
// 调用 push 方法向客户端推送数据
// $server->push($frame->fd, 'This is a message sent from WebSocket Server at ' . date('Y-m-d H:i:s'));
echo "received message: {$frame->data}\n";
$msg = json_decode($frame->data, true);
if ($msg['msg_type'] == self::MSG_TYPE_JOIN) {
// TODO:创建房间。参考:https://github.com/nineyang/chat/blob/master/app/Console/Commands/Swoole.php
Redis::zadd("room:{$msg['room_id']}", intval($msg['msg_from']), $frame->fd);
Redis::hset('room', $frame->fd, $msg['room_id']);
$memberInfo = [
'online' => Redis::zcard("room:{$msg['room_id']}"),
'all' => Redis::zrange("room:{$msg['room_id']}", 0, -1)
];
$message = [
'member_info' => $memberInfo,
'msg' => $msg['msg_body']
];
$this->sendAll($server, $msg['room_id'], $msg['msg_from'], $message, self::MSG_TYPE_JOIN);
} else {
$this->sendAll($server, $msg['room_id'], $msg['msg_from'], $frame->data, self::MSG_TYPE_TEXT);
// event(new Chat($msg['msg_from'], $frame->data));
}
}
// 关闭连接时触发
public function onClose(Server $server, $fd, $reactorId)
{
echo "connection close: {$fd}\n";
$room_id = Redis::hget('room', $fd);
Redis::hdel('room', $fd);
$user_id = intval(Redis::zscore("room:{$room_id}", $fd));
Redis::zrem("room:{$room_id}", $fd);
$balance = Redis::zcard("room:{$room_id}");
if ($balance == 0) {
Redis::del("room:{$room_id}");
return;
}
$memberInfo = [
'online' => $balance,
'all' => Redis::zrange("room:{$room_id}", 0, -1)
];
$message = [
'member_info' => $memberInfo,
'msg' => $user_id . '离开了群聊'
];
$this->sendAll($server, $room_id, $user_id, $message, self::MSG_TYPE_LEAVE);
}
/**
* @param Server $ws
* @param $room_id
* @param null $user_id
* @param null $message
* @param int $type
*/
private function sendAll($ws, $room_id, $user_id = null, $message = null, $type = self::MSG_TYPE_TEXT)
{
// $user = $this->user->find($user_id, ['id', 'name']);
// if (!$user) {
// return;
// }
if ($type != self::MSG_TYPE_TEXT)
$message = json_encode([
'msg_body' => is_string($message) ? nl2br($message) : $message,
'msg_from' => $user_id,
'msg_type' => $type
]);
$members = Redis::zrange("room:{$room_id}", 0, -1);
foreach ($members as $fd) {
$ws->push($fd, $message);
}
}
}
修改配置文件
- 配置文件
config/laravels.php
'websocket' => [
'enable' => true,
'handler' => \App\Services\WebSocketService::class,
],
...
// 可选
'swoole' => [
...
// 每隔 60s 检测一次所有连接,如果某个连接在 600s 内都没有发送任何数据,则关闭该连接
'heartbeat_idle_time' => 600,
'heartbeat_check_interval' => 60,
...
],
- 配置.env
LARAVELS_LISTEN_IP=app // 这里的 IP 需要和 nginx upstream 中配置的监听 IP 保持一致,即php-fpm的服务名
LARAVELS_DAEMONIZE=true
- 配置Nginx配置文件,注意
map
和upstream
配置与server
是平级的
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream laravels{
# Connect IP:Port
# server workspace:5200 weight=5 max_fails=3 fail_timeout=30s;
# keepalive 16;
server app:5200 weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
server {
listen 80;
server_name .purchase.com;
set $root_path '/usr/share/nginx/html';
set $php_path '/var/www/html/public';
client_max_body_size 100m;
charset utf-8;
location ~ (\.html|\.css|\.js|\.jpg|\.jpeg|\.png|\.gif|\.ico|\.ttf|\.woff|\.woff2|\.xls|\.xlsx|\.docx|\.pdf|\.mpga|\.mp3|\.txt) {
root $root_path;
}
location / {
index index.php index.html index.htm;
try_files $uri $uri/ /index.php?$args;
}
# WebSocket 通信
location =/ws {
proxy_connect_timeout 3600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://laravels;
}
location ~ \.php(.*)$ {
root $php_path;
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_buffering off;
fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
include fastcgi_params;
fastcgi_intercept_errors on;
}
location ~ /\.ht {
deny all;
}
}
Websocket的前端实现
直接上代码吧,简陋滴很。(要一个jquery文件)
<!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/css2?family=Nunito:wght@200;600&display=swap" rel="stylesheet">
<script src="{{asset('assets/js/jquery-3.0.0.min.js')}}" type="text/javascript" charset="utf-8"></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 {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.content {
text-align: center;
}
.title {
font-size: 84px;
}
.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: 30px;
}
</style>
</head>
<body style="position: relative">
<div class="flex-center">
<textarea style="height: 100px;width: 500px;border: 1px solid red;" id="msgBody"></textarea>
</div>
<div class="flex-center">
<input type="hidden" name="roomId" id="roomId">
<input type="button" id="btnConnection" value="复制房间连接"/>
<input type="button" id="btnClose" value="关闭"/>
<input type="button" id="btnSend" value="发送"/>
</div>
<textarea id="copy" style="display:none;height: 0;"></textarea>
<div
style="display: none;border: 1px solid red;width:500px;height: 500px; position:absolute;left:50%;top:50%;transform: translate(-50%,-50%);"
id="chatBox">
</div>
</body>
<script>
var uid = Math.ceil(Math.random() * 10) + 1;
var websocket;
var roomId = "{{$roomId}}";
var createWebsocket = function () {
if (typeof (WebSocket) == "undefined") {
alert("您的浏览器不支持WebSocket");
return;
}
var wsUri = (location.protocol == 'https:' ? 'wss://' : 'ws://') + location.host +'/ws';
websocket = new WebSocket(wsUri);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
$("#chatBox").show();
if (websocket.readyState === 1){
websocket.send('{"msg_type":"1","agent_type":"1","room_id":"' + roomId + '","msg_time":"' + Math.floor(((new Date()).valueOf()) / 1000) + '","msg_from":"' + uid + '","msg_body":"' + uid + '加入了群聊"}');
}
};
websocket.onclose = function (evt) {
console.log("Disconnected");
$("#chatBox").css("display", "none");
};
websocket.onmessage = function (evt) {
var data = JSON.parse(evt.data);
var h;
var s = 'left';
if (data.msg_from == uid) {
s = 'right';
}
h = '<div>\n' +
' <div style="text-align: ' + s + ';">' + data.msg_from + ':</div>\n' +
' <div style="text-align: ' + s + ';">' + data.msg_body + '</div>\n' +
' </div>';
if (data.msg_type == 1 || data.msg_type == 3) {
h = '<div style="text-align: center">' + data.msg_body.msg + '</div>';
}
$("#chatBox").append(h);
console.log('Retrieved data from server: ' + evt.data);
};
websocket.onerror = function (evt, e) {
console.log('Error occured: ' + evt.data);
};
};
createWebsocket();
function copyText(str) {
$('#copy').text(str).show();
var ele = document.getElementById("copy");
ele.select();
document.execCommand('copy', false, null);
$('#copy').hide();
alert('复制成功!');
}
$("#btnConnection").click(function () {
copyText(window.location.href);
});
//发送消息
$("#btnSend").click(function () {
websocket.send('{"msg_type":"2","agent_type":"1 ","room_id":"' + roomId + '","msg_time":"' + Math.floor(((new Date()).valueOf()) / 1000) + '","msg_from":"' + uid + '","msg_body":"' + $('#msgBody').val() + '"}');
});
//关闭
$("#btnClose").click(function () {
websocket.close();
});
</script>
</html>
启动
在php源代码容器中,项目根目录下执行:
php bin/laravels start
访问下:http://purchase.com/test/chatRoom/91609929b9699631e84fdef3385f3721(本地的host文件配置好域名)
- 出现问题,这个问题参考的网站下的评论里也出现了,但是没有解决:
php源代码容器中,[ERROR] worker[5] error: exitCode=255, signal=0
解决问题
在nginx容器中查看app:5200是否能连上:
- 安装telnet,参考,下面这样是正常的
/usr/share/nginx/html # apk update
/usr/share/nginx/html # apk add busybox-extras
/usr/share/nginx/html # busybox-extras telnet app:5200
Connected to app:5200
- 如果连不上,可能是5200端口没有给到位,那么,文件
docker/docker-compose.override.yml
,暴露或者映射下端口
app:
build:
context: ../laravel/
dockerfile: docker/Dockerfile
image: "laravel-php:latest"
ports:
- "5200:5200"
environment:
## Application Configuration ##
APP_ENV: "dev"
APP_DEBUG: "true"
volumes:
- ../laravel:/var/www/html
php源代码容器中,[ERROR] worker[5] error: exitCode=255, signal=0
-
参考,composer.json文件加入
"app/Services"
,
"autoload": {
"psr-4": {
"App\": "app/"
},
"classmap": [
"database/seeds",
"app/Services",
"database/factories"
]
},
- 项目根目录下运行命令
composer dump-autoload