使用 PHP WorkerMan 构建 WebSocket 全双工群聊通信(二)

在很早很早以前,WebSocket 协议还没有被发明的时候,人们在 Web 端制作类实时数据动态更新时,一般采用轮询、 长连接 (Long Polling) 来实现。大概就是:

轮询:客户端不停发送 HTTP 请求给服务端,服务端返回最新数据
长连接:客户端发送一条 HTTP 请求给服务端,服务端 HOLD 连接直到有新数据再返回
当时的应用有 WebQQ、FaceBook IM 等

但是这样的实现有一个非常大的缺陷,HTTP 请求是半双工 (Half Duplex) 的,只能由客户端发送请求到服务端返回。大量的请求可能会导致 CPU 资源占用、内存溢出等问题。于是 WebSocket 协议被发明了,与 HTTP 协议类似,地址为:ws:// (HTTP 页面) 或 wss:// (HTTPS 页面)。

WebSocket 是全双工 (Full Duplex) 的,也就是说服务端也可以发送数据到客户端了。那比如在聊天时,就可以省去客户端的请求,对方客户端有数据提交到服务端时直接由服务端发送至当前客户端。

比较知名的 WebSocket 框架有 http://Socket.io (node.js)、Workerman (PHP)、Swoole (PHP) 等 (我只尝试过前两个)

Pokers 的群聊功能就是轮询实现的,但是我的 1H1M1G 的小水管服务器是承受不住持续增长的用户量的,必须尝试用 WebSocket 来实现了

<?php//引入 composer
require '../vendor/autoload.php';
require_once '../vendor/workerman/workerman/Autoloader.php';
require_once '../vendor/workerman/channel/src/Server.php'; //Workerman 分组发送
require_once '../vendor/workerman/channel/src/Client.php'; //Workerman 分组发送
define('LAZER_DATA_PATH', dirname(dirname(__FILE__)) . '/data/'); //Pokers 使用的 json 数据库use Lazer\Classes\Database as Lazer;
use Workerman\Worker;
use Workerman\Lib\Timer;$channel_server = new Channel\Server('0.0.0.0', 2206); //分组服务器地址
$worker = new Worker('websocket://0.0.0.0:2000'); //WebSocket 地址
$worker->count = 2; //Workerman 进程数
// 全局群组到连接的映射数组
$group_con_map = array();$worker->onWorkerStart = function ($worker) {// Channel客户端连接到Channel服务端Channel\Client::connect('0.0.0.0', 2206);// 监听全局分组发送消息事件Channel\Client::on('send', function ($event_data) {$thread = $event_data['thread_id'];$con_id = $event_data['con_id'];$mes_id = $event_data['mes_id'];$speaker = $event_data['speaker'];$class = $event_data['class_id'];$array = Lazer::table('messages')->limit(1)->where('id', '=', (int) $mes_id)->andWhere('speaker', '=', (int) $speaker)->andWhere('belong_class', '=', (int) $class)->find()->asArray();if (!!$array[0]['speaker']) {global $group_con_map;if (isset($group_con_map[$thread])) {foreach ($group_con_map[$thread] as $con) {$con->send(json_encode($array[0])); //发送数据到群组每位成员}}} else {$array = ['op' => 'sent','status' => false,'code' => 108,'msg' => 'Illegal Request'];global $group_con_map;$group_con_map[$thread][$con_id]->send(json_encode($array));}});//心跳计时Timer::add(55, function () use ($worker) {foreach ($worker->connections as $connection) {$array = ['op' => 'keep'];$connection->send(json_encode($array));}});
};//发送消息
$worker->onMessage = function ($con, $data) {$data = json_decode($data, true);$cmd = $data['action'];$thread = $data['thread_id'];$class = $data['class_id'];$user = $data['speaker'];$user_name = $data['speaker_name'];@$mes_id = $data['mes_id'];if (!empty($user_name) && !empty($thread) && !empty($class) && !empty($user)) {$array = Lazer::table('classes')->limit(1)->where('id', '=', (int) $class)->find()->asArray();if (!!$array) {$array = Lazer::table('threads')->limit(1)->where('id', '=', (int) $thread)->andWhere('belong_class', '=', (int) $class)->find()->asArray();if (!!$array) {$array = Lazer::table('users')->limit(1)->where('id', '=', (int) $user)->andWhere('name', '=', (string) $user_name)->find()->asArray();if (!!$array && in_array((string) $class, explode(',', $array[0]['class']))) { //判断用户存在switch ($cmd) {case "join": //客户端加入群组global $group_con_map;// 将连接加入到对应的群组数组里$group_con_map[$thread][$con->id] = $con;$array = ['op' => 'join','thread' => $thread,'status' => true,'code' => 100];break;case "send": //客户端发送内容Channel\Client::publish('send', array('thread_id' => $thread,'class_id' => $class,'speaker' => $user,'speaker_name' => $user_name,'con_id' => $con->id,'mes_id' => $mes_id));$array = ['op' => 'send','status' => true,'code' => 105];break;default:$array = ['op' => 'send','status' => false,'code' => 101,'msg' => 'Illegal request'];break;}} else {$array = ['op' => 'send','status' => false,'code' => 107,'msg' => 'User does not exist or not in the class'];}} else {$array = ['op' => 'send','status' => false,'code' => 102,'msg' => 'Thread does not exist'];}} else {$array = ['op' => 'send','status' => false,'code' => 103,'msg' => 'Class does not exist'];}} else {$array = ['op' => 'send','status' => false,'code' => 104,'msg' => 'Illegal request'];}$con->send(json_encode($array));
};// 这里很重要,连接关闭时把连接从全局群组数据中删除,避免内存泄漏
$worker->onClose = function ($con) {global $group_con_map;if (isset($con->group_id)) {unset($group_con_map[$con->group_id][$con->id]);if (empty($group_con_map[$con->group_id])) {unset($group_con_map[$con->group_id]);}}
};$worker->onConnect = function ($con) {$array = ['op' => 'connect','status' => true];$con->send(json_encode($array));
};Worker::runAll();

前端js

//websocket 连接this.ws = new WebSocket('wss://pokers.zeo.im/wss');this.ws.onmessage = function (data) {var re = eval('(' + data.data + ')');switch (re.op) {case 'send':if (!re.status) {antd.$message.error('Service Unavailable');}break;case 'connect':console.log('Connected to Pokers Server');break;case 'join':if (!re.status) {antd.$message.error('Service Unavailable');}break;case 'keep':break;default://在内容段后添加一段antd.opened_mes_info.meses.push(re);if (parseInt(re.speaker) !== parseInt(antd.user.id)) {if ($(window).height() + $('#mes-container').scrollTop() >= $('#mes-inner').height()) {//当前窗口可视区域+滑动距离大于总可滑动高度,有更新直接到底部antd.bottom_mes();} else {antd.unread.visible = true;setTimeout(function () {antd.unread.visible = false;}, 1000);}}antd.update_mes();break;}};

JavaScript 连接代码

//广播全 thread 在线用户
this.ws.send('{"action":"send", "thread_id":' + antd.opened_mes_info.thread_id + ', "class_id":' + antd.opened_mes_info.class_id + ', "speaker":' + antd.user.id + ',"speaker_name":"' + antd.user.info.name + '","mes_id":' + res.data.code + '}');

JavaScript 发送代码

//加入当前 Thread
this.ws.send('{"action":"join", "thread_id":' + id + ', "class_id":' + belong_class + ', "speaker":' + antd.user.id + ',"speaker_name":"' + antd.user.info.name + '"}');

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/76324.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在阿里云 linux 服务器上查看当前服务器的Nginx配置信息

我们可以通过命令 sudo nginx -t查看到nginx.conf的路径 可以通过 sudo nginx -T查看 nginx 详细配置信息&#xff0c;包括加载的配置文件和配置块的内容 其中也会包括配置文件的内容

环境扫描/透射电子显微镜气体样品架的真空压力和微小流量控制解决方案

摘要&#xff1a;针对环境扫描/透射电子显微镜对样品杆中的真空压力气氛环境和流体流量精密控制控制要求&#xff0c;本文提出了更简单高效和准确的国产化解决方案。解决方案的关键是采用动态平衡法控制真空压力&#xff0c;真空压力控制范围为1E-03Pa~0.7MPa&#xff1b;采用压…

git 合并分支某次(commit)提交

需求&#xff1a;将develop分支某次提交合并到master上面&#xff0c;其他修改不同步&#xff1b; //切换到master分支 git checkout master //查看develop分支提交记录&#xff0c;获取对应记录哈希值&#xff1b; git log develop // 按上下按钮可以上下查询对应记录&#xf…

typeScript--[接口interface的继承]

和类一样&#xff0c;接口也可以通过关键字 extents 相互继承。接口继承&#xff0c;分为&#xff1a;单继承和多继承&#xff0c;即继承多个接口。另外&#xff0c;接口也可以继承类&#xff0c;它会继承类的成员&#xff0c;但不包括具体的实现&#xff0c;只会把类的成员作为…

DevOps到底是什么意思?

前言: 当我们谈到 DevOps 时,可能讨论的是:流程和管理,运维和自动化,架构和服务,以及文化和组织等等概念。那么,到底什么是"DevOps"呢? 那么,DevOps是什么呢? 有人说它是一种方法,也有人说它是一种工具,还有人说它是一种思想。更有甚者,说它是一种哲学…

读高性能MySQL(第4版)笔记06_优化数据类型(上)

1. 良好的逻辑设计和物理设计是高性能的基石 1.1. 反范式的schema可以加速某些类型的查询&#xff0c;但同时可能减慢其他类型的查询 1.2. 添加计数器和汇总表是一个优化查询的好方法&#xff0c;但它们的维护成本可能很 1.3. 将修改schema作为一个常见事件来规划 2. 让事情…

仅做笔记用:Stable Diffusion 通过 ControlNet 扩展图片 / 扩图

发觉之前的 Outpainting 脚本效果仍旧不是很理想。这里又找了一下有没有效果更好的途径来扩图。于是就找到了通过 ControlNet 的方式来实现效果更好的扩图。这里临时记录一下在 Stable Diffusion 怎么使用 ControlNet 来扩展图片。 下载 control_v11p_sd15_inpaint_fp16.safet…

【源码】JavaWeb+Mysql招聘管理系统 课设

简介 用idea和eclipse都可以&#xff0c;数据库是mysql&#xff0c;这是一个Java和mysql做的web系统&#xff0c;用于期末课设作业 cout<<"如果需要的小伙伴可以http://www.codeying.top";可定做课设 线上招聘平台整合了各种就业指导资源&#xff0c;通过了…

Android获取系统读取权限

在Androidifest.xml文件中加上授权语句 <uses-permission android:name"android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE"/>

Git 概述命令、idea中的使用

目录 Git概述 Git代码托管服务 Git常用命令 Git 全局设置 获取 Git 仓库 ​编辑Git 工作区中文件的状态 本地仓库操作 远程仓库操作 ​编辑分支操作 标签操作 在IDEA中使用Git 1.获取Git仓库 .gitignore 表示忽略 2.本地仓库操作 3.远程仓库操作 4.分支操作 Git是…

C++设计模式-更新中

单例模式 这个类实现了单例模式。单例模式是一种设计模式&#xff0c;旨在确保一个类只有一个实例&#xff0c;并提供一个全局访问点来获取该实例。 在 ConnectionManager 类中&#xff0c;它通过以下方式实现了单例模式&#xff1a; 构造函数 ConnectionManager() 被声明为…

c++qt day2

封装一个结构体&#xff0c;结构体中包含一个私有数组&#xff0c;用来存放学生的成绩&#xff0c;包含一个私有变量&#xff0c;用来记录学生个数&#xff0c; 提供一个公有成员函数&#xff0c;void setNum(int num)用于设置学生个数 提供一个公有成员函数&#xff1a;void…

Spring Boot跨域问题简介

什么是跨域问题&#xff1f; 在Web开发中&#xff0c;跨域指的是在浏览器中访问一个不同于当前域名的资源。浏览器出于安全考虑&#xff0c;限制了这种跨域资源的访问。具体来说&#xff0c;当浏览器使用XMLHttpRequest或Fetch API发送跨域请求时&#xff0c;目标服务器必须在…

虚拟机Ubuntu操作系统最基本终端命令(安装包+详细解释+详细演示)

虚拟机及乌班图&#xff08;Ubuntu操作系统&#xff09; 提示&#xff1a;大家需要软件的可以直接在此链接中提取 链接&#xff1a;https://pan.baidu.com/s/1_4VHGTlXjIuVhBINeOuBCA 提取码&#xff1a;nd0c 文章目录 虚拟机及乌班图&#xff08;Ubuntu操作系统&#xff09;终…

PHP is_array()函数详解,PHP判断是否为数组

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 is_array 一、基本使用二、空数组三、同时判断多个…

多线程之基础篇(一)

一、Thread类 1、线程的创建 大家都熟知创建单个线程的三种方式&#xff0c;通过继承Thread类创建线程并重写该类的run()方法&#xff1b;通过实现Runnable接口创建线程一样要重写run()方法&#xff1b;以上的两个run()方法都是线程的执行体&#xff1b;第三&#xff0c;使用…

组件安全以及漏洞复现

组件安全 1. 概述 A9:2017-使⽤含有已知漏洞的组件 A06:2021-Vulnerable and Outdated Components ​ 组件&#xff08;例如&#xff1a;库、框架和其他软件模块&#xff09;拥有和应用程序相同的权限。如果应用程序中含有已知漏洞的组件被攻击者利用&#xff0c;可能会造成…

目标检测入门

一、目标检测任务对比 二、目标检测发展路线 基于深度学习的目标检测大致可以分为一阶段(One Stage)模型和二阶段(Two Stage)模型。目标检测的一阶段模型是指没有独立地提取候选区域(Region Proposal)&#xff0c;直接输入图像得到图中存在的物体类别和相应的位置信息。典型的一…

进程与线程的关系,进程调度的基本过程

目标&#xff1a; 1. 了解进程与线程的关系 2. 进程调度的基本过程 进程与线程的关系 在我们学习进程调度前&#xff0c;我们先了解一下进程与线程&#xff1a; 1.进程是线程的容器 进程包含线程&#xff0c;一个进程里可以有一个线程&#xff0c;也可以有多个线程。 多个线程…

分类模型训练pil、torchvision.transforms和opencv的resize

参考&#xff1a;https://blog.csdn.net/weixin_41012399/article/details/126049885 https://www.cnpython.com/qa/1291644 https://blog.csdn.net/weixin_44966641/article/details/125084573 https://blog.csdn.net/IEEE_FELLOW/article/details/115536987 训练时用pil读取图…