基於hprose-golang創建RPC微服務

  • 2019 年 10 月 3 日
  • 筆記

Hprose(High Performance Remote Object Service Engine)
是一款先進的輕量級、跨語言、跨平台、無侵入式、高性能動態遠程對象調用引擎庫。它不僅簡單易用,而且功能強大。

官網:https://hprose.com/

本文將講解如何使用Hprose go 服務端編寫一個微服務,並實現客戶端調用。

本文的涉及的項目程式碼託管在github:https://github.com/52fhy/hprose-sample 。

使用Go實現服務端

初始化

git初始化:

git init  echo "main" >> .gitignore  echo "# hprose-sample" >> README.md

項目使用go mod管理依賴,請確保安裝的Go版本支援該命令。先初始化go.mod文件:

go mod init sample

最終項目目錄結構一覽:

├── config  │   └── rd.ini  ├── dao  ├── main.go  ├── model  └── util      ├── config.go      └── state.go  ├── service  │   └── sample.go  ├── go.mod  ├── go.sum  ├── client_test.go  ├── README.md  ├── php  ├── logs

golang寫微服務的好處就是我們可以按照自己理想的目錄結構寫程式碼,而無需關注程式碼 autoload 問題。

配置項

我們使用go-ini/ini來管理配置文件。

項目地址:https://github.com/go-ini/ini
文檔地址:https://ini.unknwon.io/

這個庫使用起來很簡單,文檔完善。有2種用法,一種是直接載入配置文件,一種是將配置映射到結構體,使用面向對象的方法獲取配置。這裡我們採用第二種方案。

首先在conf/里建個配置文件rd.ini:

ListenAddr = 0.0.0.0:8080    [Mysql]  Host = localhost  Port = 3306  User = root  Password =  Database = sample    [Redis]  Host = localhost  Port = 6379  Auth =

編寫util/config.go載入配置:

package util    import "github.com/go-ini/ini"    type MysqlCfg struct{      Host string      Port int32      User string      Password string      Database string  }    type RedisCfg struct{      Host string      Port int32      Auth string  }    type Config struct {      ListenAddr string      Mysql MysqlCfg      Redis RedisCfg  }    //全局變數  var Cfg Config    //載入配置  func InitConfig(ConfigFile string) error {      return ini.MapTo(Cfg, ConfigFile)  }

main.go

這裡我們需要實現項目初始化、服務註冊到RPC並啟動一個TCP server。

package main    import (      "flag"      "fmt"      "github.com/hprose/hprose-golang/rpc"      "sample/service"      "sample/util"  )    func hello(name string) string {      return "Hello " + name + "!"  }    func main() {      //解析命令行參數      configFile := flag.String("c", "config/rd.ini", "config file")      flag.Parse()        err := util.InitConfig(*configFile)      if err != nil {          fmt.Printf("load config file fail, err:%vn", err)          return      }        fmt.Printf("server is running at %sn", util.Cfg.ListenAddr)        //tcp,推薦      server := rpc.NewTCPServer("tcp4://" + util.Cfg.ListenAddr + "/")        //註冊func      server.AddFunction("hello", hello)        //註冊struct,命名空間是Sample      server.AddInstanceMethods(&service.SampleService{}, rpc.Options{NameSpace: "Sample"})      err = server.Start()      if err != nil {          fmt.Printf("start server fail, err:%vn", err)          return      }  }

我們看到,RPC里註冊了一個函數hello,還註冊了service.SampleService里的所有方法。

註:這裡註冊服務的時候使用了NameSpace選項從而支援命名空間,這個在官方的WIKI里沒有示例說明,很容易忽略。

其中SampleService是一個結構體,定義在service/sample.go文件里:

sample.go

package service    import (      "sample/model"      "sample/util"  )    //定義服務  type SampleService struct {  }    //服務里的方法  func (this *SampleService) GetUserInfo(uid int64) util.State {      var state util.State        if uid <= 0 {          return state.SetErrCode(1001).SetErrMsg("uid不正確").End()      }        var user model.User      user.Id = uid      user.Name = "test"      return state.SetData(user).End()  }  

日誌

作為一個線上項目,我們需要在業務程式碼里列印一些日誌輔助我們排查問題。日誌這裡直接使用 beego的日誌庫logs

package util    import (      "errors"      "fmt"      "github.com/astaxie/beego/logs"  )    var Logger *logs.BeeLogger    func InitLog() error {      Logger = logs.NewLogger(10)        err := Logger.SetLogger(logs.AdapterMultiFile, fmt.Sprintf(`{"filename":"/work/git/hprose-sample/logs/main.log", "daily":true,"maxdays":7,"rotate":true}`))      if err != nil {          return errors.New("init beego log error:" + err.Error())      }      Logger.Async(1000)      return nil  }  

這裡定義里全局變數Logger,之後可以在項目任意地方使用。

日誌選項里filename最好是動態配置,這裡為了演示,直接寫的固定值。

使用示例:

if uid <= 0 {      util.Logger.Debug("uid error. uid:%d", uid)  }

Go測試用例

每個項目都應該寫測試用例。下面的用例里,我們將測試上面註冊的服務是否正常。

package main    import (      "github.com/hprose/hprose-golang/rpc"      "sample/util"      "testing"  )    //stub:申明服務里擁有的方法  type clientStub struct {      Hello       func(string) string      GetUserInfo func(uid int64) util.State  }    //獲取一個客戶端  func GetClient() *rpc.TCPClient {      return rpc.NewTCPClient("tcp4://127.0.0.1:8050")  }    //測試服務里的方法  func TestSampleService_GetUserInfo(t *testing.T) {      client := GetClient()        defer client.Close()      var stub clientStub      client.UseService(&stub, "Sample") //使用命名空間        rep := stub.GetUserInfo(10001)      if rep.ErrCode > 0 {          t.Error(rep.ErrMsg)      } else {          t.Log(rep.Data)      }  }    //測試普通方法  func TestHello(t *testing.T) {      client := GetClient()        defer client.Close()      var stub clientStub      client.UseService(&stub)        rep := stub.Hello("func")      if rep == "" {          t.Error(rep)      } else {          t.Log(rep)      }  }

運行:

$ go test -v    === RUN   TestSampleService_GetUserInfo  --- PASS: TestSampleService_GetUserInfo (0.00s)      client_test.go:31: map[name:test id:10001]  === RUN   TestHello  --- PASS: TestHello (0.00s)      client_test.go:47: Hello func!  PASS  ok      sample  0.016s  

PHP調用

php-client

需要先下載hprose/hprose

composer config repo.packagist composer https://packagist.phpcomposer.com  composer require "hprose/hprose:^2.0"

client.php

<?php    include "vendor/autoload.php";    try{      $TcpServerAddr = "tcp://127.0.0.1:8050";      $client = HproseSocketClient::create($TcpServerAddr, false);      $service = $client->useService('', 'Sample');      $rep = $service->GetUserInfo(10);      print_r($rep);  } catch (Exception $e){      echo $e->getMessage();  }  

運行:

$ php php/client.php    stdClass Object  (      [errCode] => 0      [errMsg] =>      [data] => stdClass Object          (              [id] => 10              [name] => test          )    )  

實際使用時最好對該處調用的程式碼做進一步的封裝,例如實現異常捕獲、返回碼轉換、日誌列印等等。

編寫codetips

本節不是必須的,但是在多人合作的項目上,可以提高溝通效率。

hprose 不支援一鍵生成各語言的客戶端程式碼(沒有IDL支援),在寫程式碼的時候PHP編譯器沒法提示。我們可以寫一個類或者多個類,主要是Model類和Service類:

  • Model類定義欄位屬性,當傳參或者讀取返回對象里內容的是,可以使用Get/Set方法;
  • Service類類似於抽象類,僅僅是把go服務端里的方法用PHP定義一個空方法,包括參數類型、返回值類型,這個類並不會真正引入,只是給IDE作為程式碼提示用的。

示例:

class SampleService  {      /**       * 獲取用戶資訊       * @param int $uid       * @return State       */      public function GetUserInfo(int $uid): State      {      }    }

調用的地方(請使用phpStorm查看提示效果):

/**   * @return SampleService   * @throws Exception   */  function getClient()  {      $TcpServerAddr = "tcp://127.0.0.1:8050";      $client = HproseSocketClient::create($TcpServerAddr, false);      $service = $client->useService('', 'Sample');      return $service;  }    try {      $client = getClient();      $rep = $client->GetUserInfo(10);      echo $rep->errCode . PHP_EOL;      print_r($rep);  } catch (Exception $e) {      echo $e->getMessage();  }

方法getClient返回的注釋里加了@return SampleService,下面調用的$rep->errCode就會有程式碼提示了。詳見:https://github.com/52fhy/hprose-sample/tree/master/php 。

部署

線上微服務需要後台長期穩定運行,可以使用supervisord工具。

如果還沒有安裝,請餐參考:Supervisor使用教程

新增一個常駐任務,需要新建配置。

以上述sample為例,新建配置:go_hprose_sample.ini:

[program:go_hprose_sample]  command=/usr/local/bin/go  /work/git/hprose-sample/main  priority=999                ; the relative start priority (default 999)  autostart=true              ; start at supervisord start (default: true)  autorestart=true            ; retstart at unexpected quit (default: true)  startsecs=10                ; number of secs prog must stay running (def. 10)  startretries=3              ; max # of serial start failures (default 3)  exitcodes=0,2               ; 'expected' exit codes for process (default 0,2)  stopsignal=QUIT             ; signal used to kill process (default TERM)  stopwaitsecs=10             ; max num secs to wait before SIGKILL (default 10)  user=root                 ; setuid to this UNIX account to run the program  log_stdout=true  log_stderr=true             ; if true, log program stderr (def false)  logfile=/work/git/hprose-sample/logs/supervisor/go_hprose_sample.log  logfile_maxbytes=1MB        ; max # logfile bytes b4 rotation (default 50MB)  logfile_backups=10          ; # of logfile backups (default 10)  stdout_logfile_maxbytes=20MB  ; stdout 日誌文件大小,默認 50MB  stdout_logfile_backups=20     ; stdout 日誌文件備份數  stdout_logfile=/work/git/hprose-sample/logs/supervisor/go_hprose_sample.stdout.log

註:上述配置僅供參考,請務必理解配置的含義。

然後啟動任務:

supervisorctl reread  supervisorctl update  supervisorctl start go_hprose_sample

線上部署最少要2台機器,組成負載均衡。這樣當升級的時候,可以一台一台的上線,避免服務暫停。

Hprose 總結

優點:

  • 輕量級、跨語言、跨平台
  • 更少的網路傳輸量,使用二進位傳輸協議
  • 簡單,跟著官方提供的例子很快就能掌握基本的使用
  • 文檔完善

缺點:

  • 不支援IDL(介面描述語言),所以無法一鍵生成客戶端調用程式碼,需要手動維護

參考

1、Supervisor使用教程 – 飛鴻影 – 部落格園
https://www.cnblogs.com/52fhy/p/10161253.html
2、Home · hprose/hprose-golang Wiki
https://github.com/hprose/hprose-golang/wiki
3、go-ini/ini: 超贊的 Go 語言 INI 文件操作
https://ini.unknwon.io/
4、golang中os/exec包用法
https://www.cnblogs.com/vijayfly/p/6102470.html