zoukankan      html  css  js  c++  java
  • 基于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
    ", err)
    		return
    	}
    
    	fmt.Printf("server is running at %s
    ", 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
    ", 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

  • 相关阅读:
    矩阵乘法与邻接矩阵
    矩阵加速 学习笔记
    P5596 【XR-4】题
    P1842 奶牛玩杂技
    CF449B Jzzhu and Cities
    小球与盒子
    [JZOJ5279]香港记者题解--最短路图
    [学习笔记]二分图匹配与匈牙利算法
    [NOIP2018模拟赛10.25]瞎搞报告
    luogu2657-Windy数题解--数位DP
  • 原文地址:https://www.cnblogs.com/52fhy/p/11185895.html
Copyright © 2011-2022 走看看