远程调用

通过 Invoke 方法同步调用

  1. Invoke(string, []reflect.Value, *InvokeSettings) ([]reflect.Value, error)

Invoke 方法有三个输入参数,两个输出参数。

第一个输入参数是远程方法名。

第二个输入参数是远程方法参数。

第三个输入参数是远程调用设置。

第一个输出参数表示返回的结果。

第二个输出参数表示返回的错误。

因为使用该方法进行远程调用时,远程方法参数和返回结果都是 reflect.Value 类型的 slice,使用较为麻烦,所以,通常不会直接使用该方法进行远程调用。

该方法的使用方式在 Hprose 服务器 AddMissingMethod 方法一节有详细介绍,这里就不在单独举例了。

通过 Go 方法异步调用

  1. Go(string, []reflect.Value, *InvokeSettings, Callback)

其中 Callback 定义如下:

  1. type Callback func([]reflect.Value, error)

Callback 的第一个参数是返回的结果,第二个参数是返回的错误。它们跟 Invoke 方法的两个输出参数相对应。该方法通常也很少会被使用。

使用 UseService 方法生成远程服务代理对象

所以上面的介绍的两种调用方法,可以实现同步调用和异步调用,但是使用起来较为复杂,所以通常不会直接被使用。

下面将要介绍的方法是最常用的,而且也很方便、灵活、直观。

  1. UseService(remoteService interface{}, namespace ...string)

UseService 方法的第一个参数是一个有效的结构体指针,或者是有效的结构体指针的指针,总之不能为 nil 指针。它所指向的结构体中定义了一系列的函数字段,这些函数字段会被 UseService 初始化为具体的远程调用。之后你就可以像调用本地方法一样去调用远程方法了。

namespace 参数表示远程服务的名称空间,该参数可以是 0 到 1 个值。当省略时,默认值为空。

同步调用

例如下面定义一个 HelloService 结构体:

  1. type HelloService struct {
  2. Hello func(string) string
  3. }

然后使用:

  1. var hello *HelloService
  2. client.UseService(&hello)

初始 hello 对象。

最后就可以使用:

  1. result := hello.Hello("World")

的方式来直接调用远程的 Hello 函数了。

上面的 HelloService 中,我们的 Hello 方法只有一个字符串返回值,在这种情况下,如果在进行 Hello 调用过程中发生错误,将会发生 panic。如果不希望发生 panic,可以像下面这样定义:

  1. type HelloService struct {
  2. Hello func(string) (string, error)
  3. }

异步调用

  1. type HelloService struct {
  2. Hello func(func(string, error), string)
  3. }

上面这个 Hello 是异步调用形式。

通过这种方式来定义异步调用时,回调函数必须是函数字段类型的第一个参数。

回调函数的输入参数形式跟同步调用的输出参数形式是相同的,最后的 error 返回值同样是可选的,当省略时,如果定义了 OnError 事件,该事件会在异步调用发生错误时被触发,否则异步调用的错误将被忽略。

回调函数没有输出参数。

同名重载

在一个用于远程调用代理的结构体中,你可以同时定义多个函数字段,甚至可以对同一个远程方法定义多个不同名称的函数字段,例如:

  1. type HelloService struct {
  2. Hello func(string) (string, error)
  3. AsyncHello func(func(string, error), string) `name:"hello"`
  4. }

上面的 AysncHello 函数跟 Hello 函数对应的都是同一个远程方法 hello,这是通过 name:"hello" 这个标记来指定的。

嵌套组合

结构体还支持嵌套组合,例如:

  1. import (
  2. "github.com/hprose/hprose-golang/io"
  3. "github.com/hprose/hprose-golang/rpc"
  4. )
  5.  
  6. type User struct {
  7. ID int `json:"id"`
  8. Name string `json:"name"`
  9. Age int `json:"age"`
  10. }
  11.  
  12. type UserService struct {
  13. Add func(user *User) (id int, error)
  14. Delete func(id int) (bool, error)
  15. Update func(id int, user *User) (bool, error)
  16. Get func(id int) (*User, error)
  17. List func(page int, count int) ([]*User, error)
  18. }
  19.  
  20. type Order struct {
  21. ID int `json:"id"`
  22. Name string `json:"name"`
  23. OrderNumber string `json:"orderNumber"`
  24. Amount int64 `json:"amount"`
  25. User *User `json:"user"`
  26. }
  27.  
  28. type OrderService struct {
  29. Add func(order *Order) (id int, error)
  30. Delete func(id int) (bool, error)
  31. Update func(id int, order *Order) (bool, error)
  32. Get func(id int) (*Order, error)
  33. List func(page int, count int) ([]*List, error)
  34. }
  35.  
  36. type MyService struct {
  37. Login func(name string, password string) (bool, error)
  38. User UserService
  39. Order OrderService
  40. }
  41.  
  42. func init() {
  43. io.Register((*User)(nil), "User", "json")
  44. io.Register((*Order)(nil), "Order", "json")
  45. }
  46.  
  47. func NewMyService(client rpc.Client) (myService *MyService) {
  48. client.UseService(&myService)
  49. return
  50. }

比如上面这段程序中,UserOrder 是两个用于传递数据的结构体,它们在 init 函数中被注册。

UserServiceOrderService 是两个用于远程服务代理的结构体,MyService 也是,但 MyService 中还包含了 UserServiceOrderService 这两个服务。

那么现在用 NewMyService 方法创建的 myService 对象上就包含了所有这些服务,嵌套的结构体名称会自动作为名称空间附加在调用的方法名之前,例如:

  1. myService.User.Delete(1)

当调用上面这个方法时,其实调用的是 User_Delete 这个远程方法。

上面这个嵌套结构体只有一层,实际上你还可以定义更复杂的多层嵌套,这里就不再举例说明了。

InvokeSettings 结构体

前面介绍 InvokeGo 方法时,它们的最后一个输入参数都是一个 InvokeSettings 结构体指针。因为该设置涉及到的内容很多,故而放在此处单独介绍,而且它里面的选项跟使用远程服务代理对象调用方式也有密切的对应关系,我们会在这里一并介绍。

InvokeSettings 结构体定义如下:

  1. type InvokeSettings struct {
  2. ByRef bool
  3. Simple bool
  4. Idempotent bool
  5. Failswitch bool
  6. Oneway bool
  7. JSONCompatible bool
  8. Retry int
  9. Mode ResultMode
  10. Timeout time.Duration
  11. ResultTypes []reflect.Type
  12. }

我们下面就对该结构体中的几个字段进行详细说明。

ByRef 字段

该字段表示调用是否为引用参数传递,默认值为 false,即不使用引用参数传递。当使用代理对象调用时,它使用 byref 标记来定义,例如:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5.  
  6. "github.com/hprose/hprose-golang/rpc"
  7. )
  8.  
  9. func swap(a, b *int) {
  10. *b, *a = *a, *b
  11. }
  12.  
  13. type RO struct {
  14. Swap func(a, b *int) error `byref:"true"`
  15. }
  16.  
  17. func main() {
  18. server := rpc.NewTCPServer("")
  19. server.AddFunction("swap", swap)
  20. server.Handle()
  21. client := rpc.NewClient(server.URI())
  22. var ro *RO
  23. client.UseService(&ro)
  24. a := 1
  25. b := 2
  26. ro.Swap(&a, &b)
  27. fmt.Println(a, b)
  28. client.Close()
  29. server.Close()
  30. }

运行该程序,执行结果为:


  1. 2 1

如果去掉 byref 标记,或者将 byref 标记的值设为 "false",结果为


  1. 1 2

如果仅仅将 byref 标记设置为 "true",而方法的参数不是指针类型,而是值类型,那么虽然调用会以引用参数传递的方式调用,但是 a, b 的值并不会被交换。

所以引用参数传递必须满足两个条件,参数为指针类型,设置了 byref 标记为 "true"

Simple 字段

该字段表示调用所返回的结果是否为简单数据。简单数据是指:nil、数字(包括有符号和无符号的各种长度的整数类型、浮点数类型,big.Int, big.Rat, *big.Float)、bool 值、字符串、二进制数据、日期时间等基本类型的数据或者不包含引用的数组、slice, map 和结构体对象。当该字段设置为 true 时,在进行序列化操作时,将忽略引用处理,加快序列化速度。但如果数据不是简单类型的情况下,将该字段设置为 true,可能会因为死循环导致堆栈溢出的错误。简单的讲,用 JSON 可以表示的数据都是简单数据。

该字段默认值为 false,即不使用简单模式序列化请求参数。当使用代理对象调用时,它使用 simple 标记来定义,例如:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5.  
  6. "github.com/hprose/hprose-golang/rpc"
  7. )
  8.  
  9. func hello(name string) string {
  10. return "Hello " + name + "!"
  11. }
  12.  
  13. // Stub is ...
  14. type Stub struct {
  15. Hello func(string) (string, error) `simple:"true"`
  16. }
  17.  
  18. func main() {
  19. server := rpc.NewTCPServer("")
  20. server.AddFunction("hello", hello)
  21. server.Handle()
  22. client := rpc.NewClient(server.URI())
  23. var stub *Stub
  24. client.UseService(&stub)
  25. fmt.Println(stub.Hello("World"))
  26. client.Close()
  27. server.Close()
  28. }

服务器端在发布服务时也可以设置服务函数(方法)为 Simple 模式,服务器端的设置只影响服务的返回结果,客户端的设置只影响客户端发送的参数,所以这两个设置是独立的,客户端和服务器端不必同时设置,但可以同时设置。

Idempotent 字段

该字段表示调用是否为幂等性调用,幂等性调用表示不论该调用被重复几次,对服务器的影响都是相同的。幂等性调用在因网络原因调用失败时,会自动重试。如果 Failswitch 字段同时被设置为 true,并且客户端设置了多个服务地址,在重试时还会自动切换地址。

该字段默认值为 false,即表示当前调用是非幂等性调用,出错不进行重试。当使用代理对象调用时,它使用 idempotent 标记来定义。

Failswitch 字段

该字段表示当前调用在因网络原因失败时是否自动切换服务地址。

该字段默认值为 false,即表示当出错时不自动切换服务地址。当使用代理对象调用时,它使用 failswitch 标记来定义。

Retry 字段

该字段表示幂等性调用在因网络原因调用失败后的重试次数。只有 Idempotent 字段为 true 时,该设置才有作用。

该字段默认值为 0,表示重试次数使用客户端的默认重试次数。当使用代理对象调用时,它使用 retry 标记来定义。例如:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5.  
  6. "github.com/hprose/hprose-golang/rpc"
  7. )
  8.  
  9. func hello(name string) string {
  10. return "Hello " + name + "!"
  11. }
  12.  
  13. // Stub is ...
  14. type Stub struct {
  15. Hello func(string) (string, error) `simple:"true" idempotent:"true" retry:"30"`
  16. }
  17.  
  18. func main() {
  19. server := rpc.NewTCPServer("")
  20. server.AddFunction("hello", hello)
  21. server.Handle()
  22. client := rpc.NewClient(server.URI())
  23. var stub *Stub
  24. client.UseService(&stub)
  25. fmt.Println(stub.Hello("World"))
  26. client.Close()
  27. server.Close()
  28. }

这些标记都是各自独立的,可以同时使用多个标记。

Oneway 字段

该字段表示当前调用是否不等待返回值。当该字段设置为 true 时,请求发送之后,并不等待服务器返回结果,而是直接向下继续执行。

该字段默认值为 false,即表示调用需要等待返回值。当使用代理对象调用时,它使用 oneway 标记来定义。

Timeout 字段

该字段表示当前调用的超时时间,如果调用超过该时间后仍然没有返回,则会以超时错误返回。

该字段默认值为 0,即表示使用客户端默认的超时时间。当使用代理对象调用时,它使用 timeout 标记来定义。

需要注意,因为是 time.Duration 类型的数据,所以它的单位是纳秒。

JSONCompatible 字段

该字段为 true 时,返回结果中的 map 类型的默认映射类型改为 map[string]interface{},否则默认映射类型为 map[interface{}]interface{},当你确认你的 map 数据是 JSON 兼容的,即 mapkey 一定是 string 类型或者可以转换为 string 类型的数据时,可以将该字段设置为 true

该字段默认为 false。当使用代理对象调用时,它使用 jsoncompat 标记来定义。

ResultTypes 字段

该字段用来设置返回值的类型,当该值为 nil 时,表示没有返回值。因为 go 支持多结果返回,因此可以设置多个返回值类型。最后的错误类型不包含在这个字段中。

该字段默认为 nil。当使用代理对象调用时,它直接通过代理对象结构体中函数字段的返回值类型或回调函数的参数类型来定义。

SetUserData 方法

  1. func (settings *InvokeSettings) SetUserData(data map[string]interface{})

InvokeSettings 结构体指针上还定义了一个 SetUserData 方法,它用来设置 context 上的 UserData 的初始值。当使用代理对象调用时,它使用 userdata 标记来定义,userdata 的标记值使用 JSON 格式表示。比如:

  1. type HelloService struct {
  2. Hello func(string) (string, error) `simple:"true" userdata:"{\"cache\":true}"`
  3. }

userdata 的设置值是用户自己设置的,它本身没有什么特殊含义,但是用户可以通过它跟 Hprose 中间件或者 Hprose 过滤器结合,来实现功能强大的插件。我们在后面介绍 Hprose 中间件Hprose 过滤器时,会举例介绍它的用法。