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

大家好,我是码农先森。

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

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

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

  
  
Copy
[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 项目。

  
  
Copy
## 当前目录 [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 队列中读取消息,最后将消息推送到客户端,这个定时器会每间隔一秒钟调度一次。

  
  
Copy
// ./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 接口,这个接口可以在外部的后台系统调用。

  
  
Copy
// ./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 项目。

  
  
Copy
## 当前目录 [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 通道中读取到了消息,则把消息推送给所有的客户端。

  
  
Copy
// ./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 特性即可实现消息的传递。

  
  
Copy
// ./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」即可,希望对大家能有所帮助。

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

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

原创作者: yxhblogs 转载于: https://www.cnblogs.com/yxhblogs/p/18348070
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值