基於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