背景
工作中难免会遇到单语言无法解决的问题(谨慎点描述就是 单语言实现起来比较麻烦、吃力、复杂), 导致产品需求的实现没这么完美。这时候,我们可以给它添上一双翅膀,大家调侃最多的应该就是Golang赋予PHP能量了吧。 题外话, 其实我认为:
语言之争没有意义,语言只是工具, 它只是为了帮助我们更好地解决问题。当单语言无法满足需求的时候,可以根据业务和成本来决定是否利用其他语言来实现。
多一门技术就多一条门路,只局限于一种语言,您可能就无法前行。
举个例子: 公司3个业务部门都使用PHP语言, 另外一个中台服务部门使用Java, Golang, Python。此时公司想要打通这3个业务部门的用户体系,将各业务部门的用户关联起来形成互通。这个串联工作肯定需要中台服务部门来开发,一切的规范由中台定义。由于跨语言,在中台招聘PHP开发也不太现实,业务部门抽离人员去支持中台也不可能(那要中台干嘛?)。这个时候有两个选择:
HTTP 协议传输
gRPC 远程调用
如果您的业务调用服务频率较高,采用 HTTP 可能不太行,这个时候我们需要 gRPC 通信。
至于什么是gRPC, 可以 点击查阅官方文档
说了这么多, 如果您了解 gRPC, 上面的内容可以忽略, 当然 您可能已经读完了, 嘿嘿。废话说完, 我们开始实现 Golang+PHP 吧, 这里只是实现简单案例: 通过 Go 获取系统Cpu、内存、磁盘信息。
实现
如果您本地没有安装 protoc
, 请先安装。
我使用的是 Mac 环境
brew install protobuf
验证是否安装成功 protoc --version
如果提示没有 protoc
命令, 你可能需要将 protoc 加入到环境变量(查找 protoc 执行文件 find / -name protoc
), 加入环境变量后执行 source ~/.bash_profile
如果您的 PHP 没有安装gRPC
扩展, 请先安装。
pecl install grpc
我们先编写 Golang
程序, 在这里我默认您会安装 Go, 如果不会安装并且您是 Mac 操作系统 可以参考Mac-brew-安装-Golang
新建项目
mkdir go-grpc
确保环境变量已开启 Module, 可项目根目录执行 export GO111MODULE=on
创建 go.mod (包管理,类似 composer)
go mod init go-grpc
安装 grpc 和 protobuf 包
go get google.golang.org/grpc
go get google.golang.org/protobuf/reflect/protoreflect@v1.25.0
创建 proto 文件
创建 system/system.proto
文件, 写入内容
syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本
package system;
// 表示生成的go文件的存放地址,会自动生成目录。
option go_package = "./system";
// 定义服务
service System {
// 获取系统信息接口
rpc GetSystemInfo (GetSystemInfoRequest) returns (GetSystemInfoResponse) {}
}
// 获取系统信息接口请求参数
message GetSystemInfoRequest {}
// 获取系统信息接口返回值
message GetSystemInfoResponse {
double cpuPercent = 1; // CPU使用率
double memPercent = 2; // 内存使用率
double diskPercent = 3; // 磁盘使用率
string cpuGHz = 4; // CPU主频
int32 cpuCounts = 5; // CPU核数
string memTotal = 6; // 总内存
string memUsed = 7; // 剩余内存
string diskTotal = 8; // 磁盘总大小
string diskUsed = 9; // 磁盘剩余大小
}
编写 shell 文件
创建system/system_rpc.sh
文件, 写入内容
#! /bin/sh
# 系统服务 - 该文件目录下执行生成GO RPC文件命令
protoc -I $(pwd)/ $(pwd)/system.proto --go_out=plugins=grpc:./rpc
给文件赋予可执行权限 chmod -R 755 system/system_rpc.sh
生成 GO 代码
进入 cd system
目录
新建 rpc
目录 mkdir rpc
执行 ./system_rpc.sh
此时, 在 system/rpc
目录已生成 system/system.pb.go
文件
安装 gopsutil 包获取系统信息
go get github.com/shirou/gopsutil
go get github.com/tklauser/go-sysconf
编写获取系统信息代码
新建 internal/utils/system.go
文件, 写入内容
package utils
import (
"fmt"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/mem"
"time"
)
// 获取 cpu 使用率
func GetCpuPercent() float64 {
percent, _ := cpu.Percent(time.Second, false)
return percent[0]
}
// 获取 内存 使用率
func GetMemPercent() float64 {
memInfo, _ := mem.VirtualMemory()
return memInfo.UsedPercent
}
// 获取 磁盘 使用率
func GetDiskPercent() float64 {
diskInfo, _ := disk.Usage("/")
return diskInfo.UsedPercent
}
// 字节的单位转换 保留两位小数
func FormatFileSize(fileSize int64) (size string) {
if fileSize < 1024 {
// return strconv.FormatInt(fileSize, 10) + "B"
return fmt.Sprintf("%.2fB", float64(fileSize)/float64(1))
} else if fileSize < (1024 * 1024) {
return fmt.Sprintf("%.2fKB", float64(fileSize)/float64(1024))
} else if fileSize < (1024 * 1024 * 1024) {
return fmt.Sprintf("%.2fMB", float64(fileSize)/float64(1024*1024))
} else if fileSize < (1024 * 1024 * 1024 * 1024) {
return fmt.Sprintf("%.2fGB", float64(fileSize)/float64(1024*1024*1024))
} else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) {
return fmt.Sprintf("%.2fTB", float64(fileSize)/float64(1024*1024*1024*1024))
} else { // if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024)
return fmt.Sprintf("%.2fEB", float64(fileSize)/float64(1024*1024*1024*1024*1024))
}
}
新建 rpc/logic/systeminfo.go
文件, 写入内容
package logic
import (
"context"
"fmt"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/mem"
"go-grpc/system/internal/utils"
"go-grpc/system/rpc/system"
)
type SystemInfoLogic struct {
ctx context.Context
}
func NewSystemInfoLogic(ctx context.Context) *SystemInfoLogic {
return &SystemInfoLogic{
ctx: ctx,
}
}
// 获取系统信息
func (l *SystemInfoLogic) GetSystemInfo(request *system.GetSystemInfoRequest) (*system.GetSystemInfoResponse, error) {
var (
cpuGHz = "0GHz"
)
c, _ := cpu.Info()
for _, v := range c {
if v.Mhz > 0 {
cpuGHz = fmt.Sprintf("%v0GHz", v.Mhz/1000)
}
}
cpuCounts, _ := cpu.Counts(true)
m, _ := mem.VirtualMemory()
w, _ := disk.Usage("/")
return &system.GetSystemInfoResponse{
CpuPercent: utils.GetCpuPercent(),
MemPercent: utils.GetMemPercent(),
DiskPercent: utils.GetDiskPercent(),
CpuGHz: cpuGHz,
CpuCounts: int32(cpuCounts),
MemTotal: utils.FormatFileSize(int64(m.Total)),
MemUsed: utils.FormatFileSize(int64(m.Used)),
DiskTotal: utils.FormatFileSize(int64(w.Total)),
DiskUsed: utils.FormatFileSize(int64(w.Total) - int64(w.Free)),
}, nil
}
新建服务端文件
进入 cd rpc
目录
新建 server
目录 mkdir server
进入 server
目录, 新建 system.go
文件, 写入内容
package server
import (
"context"
"go-grpc/system/rpc/logic"
"go-grpc/system/rpc/system"
)
// 系统信息服务
type System struct {}
// 获取系统信息
func (server *System) GetSystemInfo(ctx context.Context, request *system.GetSystemInfoRequest) (*system.GetSystemInfoResponse, error) {
l := logic.NewSystemInfoLogic(ctx)
return l.GetSystemInfo(request)
}
编写服务启动文件
system
目录下新建 server.go
文件, 写入内容
package main
import (
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"net"
"go-grpc/system/rpc/server"
"go-grpc/system/rpc/system"
)
func main() {
// 监听本地的 10000 端口
lis, err := net.Listen("tcp", ":10000")
if err != nil {
fmt.Printf("failed to listen: %v", err)
return
}
s := grpc.NewServer() // 创建 GRPC 服务器
system.RegisterSystemServer(s, &server.System{}) // 在 GRPC 服务端注册服务
reflection.Register(s) // 在给定的 GRPC 服务器上注册服务器反射服务
// Serve 方法在 lis 上接受传入连接,为每个连接创建一个 ServerTransport 和 server 的 goroutine。
// 该 goroutine 读取 GRPC 请求,然后调用已注册的处理程序来响应它们。
err = s.Serve(lis)
if err != nil {
fmt.Printf("failed to serve: %v", err)
return
}
}
项目目录结构
.
├── go.mod
├── go.sum
└── system
├── internal
│ └── utils
│ └── system.go
├── rpc
│ ├── logic
│ │ └── systeminfo.go
│ ├── server
│ │ └── system.go
│ └── system
│ └── system.pb.go
├── server.go
├── system.proto
└── system_rpc.sh
启动服务
go run server.go
此时, Golang 服务已经编写完成!!!
接下来, 我们编写 PHP 代码, 在这里我默认您用了composer
包管理。
导入 grpc 和 protobuf 包
composer require grpc/grpc
composer require google/protobuf
新建 gRPC 目录
为了方便演示, 我们在项目根目录下新建 mkdir -p grpc/system
文件夹, 然后进入到该目录cd grpc/system
创建 touch system.proto
文件, 写入内容
syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本
package System;
// 系统服务
service System {
// 获取系统信息接口
rpc GetSystemInfo (GetSystemInfoRequest) returns (GetSystemInfoResponse) {}
}
// 获取系统信息接口请求参数
message GetSystemInfoRequest {}
// 获取系统信息接口返回值
message GetSystemInfoResponse {
double cpuPercent = 1; // CPU使用率
double memPercent = 2; // 内存使用率
double diskPercent = 3; // 磁盘使用率
string cpuGHz = 4; // CPU主频
int32 cpuCounts = 5; // CPU核数
string memTotal = 6; // 总内存
string memUsed = 7; // 剩余内存
string diskTotal = 8; // 磁盘总大小
string diskUsed = 9; // 磁盘剩余大小
}
编写 shell 文件
创建 system.sh
文件, 写入内容
#! /bin/sh
# 系统服务 - 该目录文件下执行生成 PHP 文件命令
protoc -I $(pwd)/ $(pwd)/system.proto --php_out=../
给文件赋予可执行权限chmod -R 755 system.sh
添加自动加载命名空间
在 composer.json
文件的 autoload
配置增加如下
"autoload":{
"psr-4":{
"GPBMetadata\\":"grpc/GPBMetadata/",
"System\\":"grpc/system/"
}
},
然后执行 composer dump-autoload
生成 PHP 代码
在 grpc/system
目录下执行 ./system.sh
生成 PHP 代码文件, 此时我们 Tree 看看目录结构
.
├── composer.json
├── composer.lock
├── vendor
└── grpc
├── GPBMetadata
│ └── System.php
├── system
│ ├── GetSystemInfoRequest.php
│ ├── GetSystemInfoRequest.php
│ └── system.proto
│ └── system.sh
编写客户端文件
我们进入到 grpc/system
目录新建客户端文件 SystemClient.php
调用系统服务接口, 写入内容
<?php
namespace System;
class SystemClient extends \Grpc\BaseStub
{
public function __construct($hostname, $opts, $channel = null)
{
parent::__construct($hostname, $opts, $channel);
}
public function getSystemInfo(\System\GetSystemInfoRequest $argument, $metadata = [], $options = [])
{
return $this->_simpleRequest(
'/system.System/GetSystemInfo',
$argument,
[\System\GetSystemInfoResponse::class, 'decode'],
$metadata,
$options
);
}
}
编写调用服务文件
在根目录新建 index.php
文件, 写入内容
<?php
require 'vendor/autoload.php';
// 创建客户端实例
$client = new \System\SystemClient('127.0.0.1:10000', [
'credentials' => \Grpc\ChannelCredentials::createInsecure()
]);
$request = new \System\GetSystemInfoRequest();
$reponse = $client->getSystemInfo($request)->wait();
list($reply, $status) = $reponse;
$data['system'] = [
'cpuPercent' => '0%',
'memPercent' => '0%',
'diskPercent' => '0%',
'cpuGHz' => '0GHz',
'cpuCounts' => 0,
'memTotal' => '0GB',
'memUsed' => '0GB',
'diskTotal' => '0GB',
'diskUsed' => '0GB',
];
if ($status->code === 0) {
$data['system']['cpuPercent'] = floatval(sprintf("%.2f", $reply->getCpuPercent())) . '%';
$data['system']['memPercent'] = floatval(sprintf("%.2f", $reply->getMemPercent())) . '%';
$data['system']['diskPercent'] = floatval(sprintf("%.2f", $reply->getDiskPercent())) . '%';
$data['system']['cpuGHz'] = $reply->getCpuGHz();
$data['system']['cpuCounts'] = $reply->getCpuCounts();
$data['system']['memTotal'] = $reply->getMemTotal();
$data['system']['memUsed'] = $reply->getMemUsed();
$data['system']['diskTotal'] = $reply->getDiskTotal();
$data['system']['diskUsed'] = $reply->getDiskUsed();
}
var_dump($data);
运行调用
根目录执行 php index.php
输出内容如下:
array(1) {
["system"]=>
array(9) {
["cpuPercent"]=>
string(6) "10.97%"
["memPercent"]=>
string(6) "69.17%"
["diskPercent"]=>
string(6) "26.89%"
["cpuGHz"]=>
string(7) "2.80GHz"
["cpuCounts"]=>
int(8)
["memTotal"]=>
string(7) "16.00GB"
["memUsed"]=>
string(7) "11.13GB"
["diskTotal"]=>
string(8) "233.47GB"
["diskUsed"]=>
string(8) "175.94GB"
}
}
到这里整个功能完成, 贴上 PHP 端最终目录结构
.
├── index.php
├── composer.json
├── composer.lock
├── vendor
└── grpc
├── GPBMetadata
│ └── System.php
├── system
│ ├── GetSystemInfoRequest.php
│ ├── GetSystemInfoRequest.php
│ └── system.proto
│ └── system.sh
│ └── SystemClient.php
以上使用的是原生态 PHP, 在此推荐使用 Hyperf
实现 gRPC 客户端
。
评论一下?