基于hprosegolang创建RPC微服务

coding

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:%v\n", err)

return

}

fmt.Printf("server is running at %s\n", 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:%v\n", 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 = \Hprose\Socket\Client::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 = \Hprose\Socket\Client::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

原文出处:https://www.cnblogs.com/52fhy/p/11185895.html

以上是 基于hprosegolang创建RPC微服务 的全部内容, 来源链接: utcz.com/z/509803.html

回到顶部