侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计收到 0 条评论

简单实现Go+PHP gRPC通信

2022-8-11 / 0 评论 / 173 阅读
温馨提示:
本文最后更新于 2022-8-11,已超过半年没有更新,若内容或图片失效,请留言反馈。

背景

工作中难免会遇到单语言无法解决的问题(谨慎点描述就是 单语言实现起来比较麻烦、吃力、复杂), 导致产品需求的实现没这么完美。这时候,我们可以给它添上一双翅膀,大家调侃最多的应该就是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 客户端

评论一下?

OωO
取消