PHP转Go系列 | ThinkPHP与Gin框架之打造基于WebSocket技术的消息推送中心

news2025/1/9 20:35:14

大家好,我是码农先森。

在早些年前客户端想要实时获取到最新消息,都是使用定时长轮询的方式,不断的从服务器上获取数据,这种粗暴的骚操作实属不雅。不过现如今我也还见有人还在一些场景下使用,比如在 PC 端扫描二维码,然后使用长轮询的方式从服务端获取最新的扫码信息,来判断用户是否已经扫码完成,诸如这种场景还有不少。其实大家都知道长轮询的方式不好,那为什么还有人使用呢?

我想最直接的原因就是「开发起来简单明了」,人性决定了人类都是趋易避难的高级物种,那个容易上手就用那个。但是我想表达的是除了长轮询的方式外,WebSocket 技术其实也不难,只不过对于从来没有接触过长连接的人来说,刚开始上手时会有一些思维上的障碍。这次我分享的内容是基于 WebSocket 技术的消息推送中心,看起来很高大上,其实也就是通过一些小的例子来演示,从服务端推送数据到客户端的这个过程,接下来的例子简单明了容易上手,我们赶紧开始吧。

话不多说,开整!我们先来看一下整体的项目目录结构,内容主要分为 PHP 和 Go 两部分。

[manongsen@root php_to_go]$ tree -L 2
.
├── go_websocket
│   ├── app
│   │   ├── controller
│   │   |	|── message.go
│   │   │   └── websocket.go
│   │   └── route.go
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── php_websocket
│   ├── app
│   │   ├── controller
│   │   |	|── Push.php
│   │   │   └── Worker.php
│   ├── composer.json
│   ├── composer.lock
│   ├── config
│   │   |── worker_server.php
│   │   └── worker.php
│   ├── route
│   │   └── app.php
│   ├── think
│   ├── vendor
│   └── .env

ThinkPHP

使用 composer 创建基于 ThinkPHP 框架的 php_websocket 项目。

## 当前目录
[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/php_websocket

## 安装 ThinkPHP 框架
[manongsen@root php_websocket]$ composer create-project topthink/think php_websocket
[manongsen@root php_websocket]$ cp .example.env .env

## 安装 Composer 依赖包
[manongsen@root php_websocket]$ composer require topthink/think-worker
[manongsen@root php_websocket]$ composer require predis/predis

使用 php think make:controller Worker 命令创建 Worker.php 控制器。这个控制器中主要实现了 onWorkerStart 这个方法,首先添加了一个 Timer 异步定时器,然后从 Redis 队列中读取消息,最后将消息推送到客户端,这个定时器会每间隔一秒钟调度一次。

// ./php_to_go/php_websocket/app/controller/Worker.php
<?php
declare (strict_types = 1);

namespace app\controller;

use think\Request;
use think\worker\Server;
use Workerman\Lib\Timer;
use think\facade\Cache;
use think\facade\Env;

class Worker extends Server
{
    protected $socket = 'websocket://0.0.0.0:2345';
    protected static $connections = [];

    public function onWorkerStart($worker) {
        // 添加一个异步定时器任务
        Timer::add(1, function () use ($worker) {
            // 从消息中心队列中读取消息
            $redis = Cache::store('redis')->handler();
            $content = $redis->rpop(Env::get("MESSAGE_CENTER_KEY"));

            // 发送消息到客户端
            foreach ($worker->connections as $connection) {
                if (!empty($content)) {
                    $connection->send("PHP语言消息中心: " . $content);
                }
            }
        });
    }

    public function onWorkerReload($worker) {
    }

    public function onConnect($connection) {
    }

	public function onMessage($connection, $data){
	}

    public function onClose($connection) {
    }

    public function onError($connection, $code, $msg) {
    }
}

使用 php think make:controller Push 命令创建 Push.php 控制器。这个控制器的主要作用是接收外部的消息内容,然后推送到 Redis 消息队列中,这里提供的是 API 接口,这个接口可以在外部的后台系统调用。

// ./php_to_go/php_websocket/app/controller/Push.php
<?php

namespace app\controller;

use app\BaseController;
use think\facade\Cache;
use think\facade\Env;

class Push extends BaseController
{
    public function msg()
    {
        // 接收 GET 参数
        $params = $this->request->param();
        if (empty($params["content"])) {
            return json(["code" => -1, "msg" => "内容不能为空"]);
        }
        $content = $params["content"];

        // 推送消息到消息中心队列
        $redis = Cache::store('redis')->handler();
        $redis->lpush(Env::get("MESSAGE_CENTER_KEY"), $content);

        return json(["code" => 0, "msg" => "success"]);
    }
}

先运行 php think worker 启动 HTTP 服务,再运行 php think worker:server 启动 WebSocket 服务,最后来测试一波。

Gin

通过 go mod 初始化 go_websocket 项目。

## 当前目录
[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/go_websocket

## 初始化项目
[manongsen@root go_websocket]$ go mod init go_websocket

## 安装第三方依赖库
[manongsen@root go_websocket]$ go get github.com/gin-gonic/gin
[manongsen@root go_websocket]$ go get github.com/gorilla/websocket

在 go_websocket 项目中创建 websocket 控制器。这个控制器会将客户端连接存储到指定的 Map 数据结构中,其次还提供了 WaitMessage 等待消息的方法,如果从 MsgQueue 通道中读取到了消息,则把消息推送给所有的客户端。

// ./php_to_go/go_websocket/app/controller/websocket.php
package controller

import (
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

// 定义一个消息传输通道
var MsgQueue = make(chan string, 10)

// 定义一个存储客户端连接的 Map
var Clients = make(map[*websocket.Conn]bool)

// 将 HTTP 协议升级至 WebSocket 协议
var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true // 允许所有来源
	},
}

// 将客户端连接存储到 Map
func HandleConnection(c *gin.Context) {
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		fmt.Printf("客户端连接协议升级失败: %v\n", err)
		return
	}
	Clients[conn] = true
}

// 等待消息中
func WaitMessage() {
	go func() {
		for {
			select {
			case msg, ok := <-MsgQueue:
				if ok {
					for client := range Clients {
						err := client.WriteMessage(websocket.TextMessage, []byte("Go语言消息中心: "+string(msg)))
						if err != nil {
							fmt.Printf("消息推送失败: %v\n", err)
						}
					}
				}
			default:
				// 避免忙等
				time.Sleep(500 * time.Millisecond)
			}
		}
	}()
}

在 go_websocket 项目中创建 message 控制器。这个控制器的主要作用是接收外部的消息内容,然后推送到 MsgQueue 通道中,这里提供的是 API 接口,这个接口可以在外部的后台系统调用。这里和 PHP 中有一点不同的是,在 Go 中无需引入像 Redis 一样的第三方组件,而是利用自身的 Channel 特性即可实现消息的传递。

// ./php_to_go/go_websocket/app/controller/message.php
package controller

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func PushMsg(c *gin.Context) {
	// 接收 GET 参数
	content := c.Query("content")
	if len(content) == 0 {
		c.JSON(http.StatusOK, gin.H{
			"msg":  "内容不能为空",
			"code": -1,
		})
		return
	}

	// 往通道推送消息
	MsgQueue <- content

	c.JSON(http.StatusOK, gin.H{
		"msg":  "ok",
		"code": 0,
	})
}

运行 go run main.go 启动服务,然后进行消息推送测试。

通过这两个简单的例子,我相信大家已经对 WebSocket 技术已经有所了解吧。从例子中也可以看出来,其实在 PHP 和 Go 中实现上有所区别,PHP 中需要启动两个服务,一个是 HTTP 服务,一个是 WebSocket 服务,而且两者服务直接都是单独的进程,不能相互通信,需要额外借助第三方中间件 Redis 来实现数据的传输。反观 Go 中直接一个服务涵盖了 HTTP 服务和 WebSocket 服务,共享一个进程的数据资源,通过使用 Channel 通道传递消息。

此外,在 PHP 中需要使用 Timer 异步定时器来读取 Redis 消息队列中的数据,不能用 for 循环或者 Redis 的阻塞队列,因为它会阻塞整个进程的执行。而在 Go 中直接开启一个协程,在协程中等待通道中的消息即可,会一直阻塞到消息的到来,而且它不会阻塞整个进程的执行,由此可见在这个例子中 Go 相较于 PHP 的优势显著。最后可能有些从来没有使用过 WebSocket 技术的朋友,可能看完这篇文章之后也依然会云里雾里,所以建议这些朋友可以自己亲自实践一下文中的案例,实践过后我相信你会别有一番技术体验。如果有想要获取完整案例代码的朋友,可以在公众号内回复「2463」即可,希望对大家能有所帮助。

感谢大家阅读,个人观点仅供参考,欢迎在评论区发表不同观点。

欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

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

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

相关文章

浅谈JDK

JDK(Java Development Kit) JDK是Java开发工具包&#xff0c;是Java编程语言的核心软件开发工具。 JDK包含了一系列用于开发、编译和运行Java应用程序的工具和资源。其中包括&#xff1a; 1.Java编译器&#xff08;javac&#xff09;&#xff1a;用于将Java源代码编译成字节…

MS8923/8923S低压、高精度、推挽输出比较器

MS8923/8923S 是一款差分输入、高速、低功耗比较器&#xff0c;具 有互补 TTL 输出。其传输延时在 10ns 左右&#xff0c;输入共模范围可以 到负轨。 MS8923/8923S 可以在线性区保持输出稳定特性&#xff0c;单电 源供电是 5.0V &#xff0c;双电源供电是 5V 。 MS89…

【算法/学习】:记忆化搜索

✨ 落魄谷中寒风吹&#xff0c;春秋蝉鸣少年归 &#x1f30f; &#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;算法学习 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f44…

数据结构——队列的讲解(超详细)

前言&#xff1a; 我们在之前刚讲述完对于栈的讲解&#xff0c;下面我们在讲另一个类似栈的数据结构——队列&#xff0c;它们都是线性表&#xff0c;但结构是大有不同&#xff0c;下面我们直接进入讲解&#xff01; 目录 1.队列的概念和结构 1.1.队列的概念 1.2.队列的结构 2.…

基于Python的去哪儿网数据采集与分析可视化大屏设计与实现

摘要 本文旨在介绍如何利用Python进行去哪儿网景点数据的采集与分析。通过采集去哪儿网上的景点数据&#xff0c;我们可以获取大量的旅游相关信息&#xff0c;并基于这些数据进行深入分析和洞察&#xff0c;为旅游行业、市场营销策略以及用户个性化推荐等提供支持。 本文将使用…

MySQL(DQL)

一&#xff0c;SQL语言分类 &#xff08;1&#xff09;数据查询语言&#xff08;DQL&#xff1a;Data Query Language&#xff09;其语句&#xff0c;也称为 “数据检索语句”&#xff0c;用以从表中获得数据&#xff0c;确定数据怎样在应用程 序给出。关键字 SELECT 是 DQL&a…

Python打包命令汇总

1、pyinstaller打包 环境安装&#xff1a;pip install pyinstaller 网络不好可以通过 -i 指定安装源&#xff1a;pip install pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple/安装完成后通过&#xff1a;pyinstaller --version 查看是否安装成功 打包单个脚本&…

操作系统笔记二

虚拟内存 把不常用的数据放到硬盘上去&#xff0c;常用的代码或者数据才加载到内存&#xff0c;来实现虚拟的大内存的感觉 覆盖技术 目标&#xff1a;在较小内存运行较大程序。 原理&#xff1a;把程序按自身逻辑结构划分若干功能上相对独立的程序模块。不回同时执行的模块共…

FreeRTOS学习笔记(一)—— 裸机和RTOS,Freertos移植(MDK),stm32cubeIDE使用Freertos

FreeRTOS学习笔记&#xff08;一&#xff09;—— 裸机和RTOS&#xff0c;Freertos移植&#xff08;MDK&#xff09;&#xff0c;stm32cubeIDE使用Freertos 文章目录 FreeRTOS学习笔记&#xff08;一&#xff09;—— 裸机和RTOS&#xff0c;Freertos移植&#xff08;MDK&#…

uniapp/vue个性化单选、复选组件

个性化单选和复选组件在网页设计中非常常见&#xff0c;它们不仅能够提升用户界面的美观度&#xff0c;还能改善用户体验。此组件是使用vue uniapp实现的个性化单选复选组件。设计完成后&#xff0c;点击生成源码即可。 拖动组件过设计区 每行显示数量 默认支持每行三个&#…

Maven-学习首篇

目录 Maven简介基本概念&特点Maven的安装与配置Maven基础概念及使用方法Maven的项目结构Maven的使用Maven的依赖管理Maven的生命周期和插件常见疑问Maven的插件机制是如何工作的&#xff1f;Maven的POM文件主要包含哪些内容&#xff1f;Maven的生命周期包括哪些阶段&#x…

【C++语言】list的构造函数与迭代器

1. list的介绍及使用 1.1 list的介绍 list的文档介绍 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点…

C++中的IO流

目录 1.C语言的输入与输出 2.流是什么 3.CIO流 标准IO流 IO流的四个标志 C文件IO流 4.stringstream的简单介绍 1.C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键 盘)读取数据&#xff0c;并将值存放在变…

钢铁百科:A572Gr60和SA572Gr60材质分析、A572Gr60和SA572Gr60简介

A572Gr60和SA572Gr60是两种常用的结构钢板&#xff0c;它们在材质、执行标准、化学成分、力学性能、交货状态、应用范围和常用规格方面有所不同。 材质&#xff1a; A572Gr60&#xff1a;属于美国材料与试验协会&#xff08;ASTM&#xff09;标准下的A572系列高性能结构钢&…

UIAbility组件基础(一)

一、概述 UIAbility组件是一种包含UI的应用组件&#xff0c;主要用于和用户交互。UIAbility组件是系统调度的基本单元&#xff0c;为应用提供绘制界面的窗口。一个应用可以包含一个或多个UIAbility组件。每一个UIAbility组件实例都会在最近任务列表中显示一个对应的任务。 U…

自研低代码海报制作平台学习分享计划

vue3组件库开发前面咱卷完了JuanTree组件&#xff0c;接下来一起来卷vue3低代码海报制作平台的基础组件实现。首先是拖拽基础组件的开发&#xff0c;整好把前面学习的知识点再运用进来。 文章目录 效果演示基本拖拽区域拖拽旋转其他效果待实现 录屏说明 看一步步实现的效果&…

C++--类和对象(二)

类和对象的基础定义可参看&#xff1a;C--类和对象&#xff08;一&#xff09;-CSDN博客 本篇讲述类和对象里相当重要的几个成员函数 目录 类的默认成员函数&#xff1a; 1.构造函数 2.析构函数 3.拷贝构造函数 &#xff08;1&#xff09;无限递归调用拷贝构造 &#xff…

在Ubuntu中重装Vscode(没有Edit Configurations(JSON)以及有错误但不标红波浪线怎么办?)

在学习时需要将vscode删除重装&#xff0c;市面上很多方法都不能删干净&#xff0c;删除之后拓展都还在。因此下面的方法可以彻底删除。注意&#xff0c;我安装时使用的是snap方法。 如果你的VScode没有Edit Configurations(JSON)&#xff0c;以及有错误但不标红波浪线的话&…

基于QT实现的TCP连接的网络通信(客户端)

上篇介绍了QT实现网络通信的服务器端&#xff0c;还没看服务器的朋友们先去上篇了解&#xff0c;这篇我来实现一下客户端的实现。 首先还是新建一个项目 选择mainwindow类 在通信前将.pro文件的第一行代码中追加network 窗口搭建 在mainwindow.ui中完成一下窗口的搭建 首先在…

序列建模之循环和递归网络 - 双向RNN篇

序言 在序列建模的广阔领域中&#xff0c;循环神经网络&#xff08; RNN \text{RNN} RNN&#xff09;以其独特的循环结构&#xff0c;在处理序列数据方面展现出了强大的能力。然而&#xff0c;传统的单向 RNN \text{RNN} RNN在处理某些复杂任务时&#xff0c;如自然语言处理中…