基于hprose-golang创建RPC微服务

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)
        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,'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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


类型转换 1、int转string 2、string转int 3、string转float 4、用户结构类型转换
package main import s &quot;strings&quot; import &quot;fmt&quot; var p = fmt.Println func main() { p(&quot;Contains: &quot;, s.Contains(&quot;test&quo
类使用:实现一个people中有一个sayhi的方法调用功能,代码如下: 接口使用:实现上面功能,代码如下:
html代码: beego代码:
1、读取文件信息: 2、读取文件夹下的所有文件: 3、写入文件信息 4、删除文件,成功返回true,失败返回false
配置环境:Windows7+推荐IDE:LiteIDEGO下载地址:http://www.golangtc.com/downloadBeego开发文档地址:http://beego.me/docs/intro/ 安装步骤: 一、GO环境安装 二、配置系统变量 三、Beego安装 一、GO环境安装 根
golang获取程序运行路径:
Golang的文档和社区资源:为什么它可以帮助开发人员快速上手?
Golang:AI 开发者的实用工具
Golang的标准库:为什么它可以大幅度提高开发效率?
Golang的部署和运维:如何将应用程序部署到生产环境中?
高性能AI开发:Golang的优势所在
本篇文章和大家了解一下go语言开发优雅得关闭协程的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。1.简介本文将介绍首先为什么需要主...
这篇文章主要介绍了Go关闭goroutine协程的方法,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。1.简介本文将介绍首先为什么需要主动关闭gor...
本篇文章和大家了解一下go关闭GracefulShutdown服务的几种方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。目录Shutdown方法Regi...
这篇文章主要介绍了Go语言如何实现LRU算法的核心思想和实现过程,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。GO实现Redis的LRU例子常
今天小编给大家分享的是Go简单实现多租户数据库隔离的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会...
这篇“Linux系统中怎么安装NSQ的Go语言客户端”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希
本文小编为大家详细介绍“怎么在Go语言中实现锁机制”,内容详细,步骤清晰,细节处理妥当,希望这篇“怎么在Go语言中实现锁机制”文章能帮助大家解决疑惑,下面...
今天小编给大家分享一下Go语言中interface类型怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考