发布服务

hprose 为发布服务提供了多个方法,这些方法可以随意组合,通过这种组合,你所发布的服务将不会局限于某一个函数,某一个方法,某一个对象,而是可以将不同的函数和方法随意重新组合成一个服务。

AddFunction 方法

  1. AddFunction(name string, function interface{}, option ...Options) Service

该方法的用于发布一个函数(命名函数或匿名函数都可以)或者一个已绑定的方法。

第一个参数 name 表示发布的函数名,它与实际的函数名或方法名无关,可以相同,也可以不同。

第二个参数是实际函数,它可以是一个函数(命名函数或匿名函数都可以)或者一个已绑定的方法,参数个数不限,返回结果个数不限,但参数和结果都必须是可序列化类型(不可序列化类型包含函数类型、chan 类型以及 unsafe.Pointer 类型),如果返回结果中包含 error 类型的结果,需要是最后一个结果参数。

第三个参数 option 是服务发布选项,类型是 Options,该类型是一个结构体,它是可选的,最多一个。一会儿,我们会详细介绍它。

该方法的返回值是服务器对象本身,目的是为了可以方便的进行链式调用。

下面来介绍一下 Options 结构体各个字段的意义。

Options 结构体

Mode 字段

该字段有 4 个取值:

  • Normal
  • Serialized
  • Raw
  • RawWithEndTag
    该字段用来指明方法调用结果的类型。

如果返回结果就是普通对象,那么不需设置该字段,也就是默认值 Normal

如果返回结果是 hprose 序列化之后的数据([]byte 类型),那么设置该字段为 Serialized 可以避免该结果被二次序列化。

如果返回结果是一个完整的响应,当这个响应结果不带 hprose 结束符时,需要将该字段设置为 Raw。如果这个响应结果带 hprose 结束符,则设置这个字段为 RawWithEndTag

这个参数主要用于存储转发的 hprose 代理服务器。通常我们不需要用到这个参数。

Simple 字段

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

Oneway 字段

该字段表示在收到调用时,是否立即返回空响应之后再执行调用。通常对于没有返回值的服务方法,而且又不需要保证跟后续调用有同步关系的情况下,可以将该参数设置为 true,这将加快客户端的响应速度。

NameSpace 字段

该字段用于设置发布方法的名称空间,它会被加在函数(方法)名前面,并且用下划线()作为分隔。客户端可以直接以 NameSpace_MethodName 方式调用,但是对于不同语言的客户端还支持使用类似 NameSpace.MethodNameNameSpace->MethodName 的方式调用。NameSpace 的值本身也可以使用下划线()分隔主次名空间。

JSONCompatible 字段

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

AddFunctions 方法

  1. AddFunctions(names []string, functions []interface{}, option ...Options) Service

该方法用于发布一组相同选项的函数或已绑定方法。

names 是发布的函数名列表。functions 是发布的函数列表。这两个列表中的函数名和函数是一一对应的,所以长度必须相等。

AddMethod 方法

  1. AddMethod(name string, obj interface{}, alias string, option ...Options) Service

该方法用于发布一个对象上的方法。

name 为对象上的方法名(注意大小写一定要跟对象上定义的方法名大小写相同,如果对象类型的定义在不同的包中,则方法必须是可导出方法),obj 是持有发布方法的对象。。alias 是远程调用使用的方法名,当其值为 "" 时,表示跟 name 一致,option 含义同上,可以省略。

AddMethods 方法

  1. AddMethods(names []string, obj interface{}, aliases []string, option ...Options) Service

该方法用于发布一个对象上的多个方法。

该方法同上面的 AddMethod 方法类似,但它可以发布多个方法。names 是方法名列表,aliases 是远程调用的函数名列表,可以为 nil,在不为 nil 的情况下,需要一一对应,长度相等。

AddInstanceMethods 方法

  1. AddInstanceMethods(obj interface{}, option ...Options) Service

跟上面几个方便相比,这是一个比较常用的用于发布服务的方法。

它会发布 obj 上的所有可导出的方法和函数字段,但是如果 obj 是一个结构体对象,对于其匿名字段上继承的方法和函数字段并不会发布。

那如果我先连继承的方法一起导出该怎么办呢?有两种方法:

第一种是,对于 obj 和它上面的匿名字段分别调用 AddInstanceMethods 方法进行发布,或者调用 AddFunctionAddMethodAddFunctionsAddMethods 来一个一个或一组一组的发布。这种方法比较繁琐。

第二种方法是使用下面的 AddAllMethods 方法,它可以让你一步到位。

AddAllMethods 方法

  1. AddAllMethods(obj interface{}, option ...Options) Service

该方法不但会发布 obj 上的所有可导出的方法和函数字段。而且会发布 obj 所有字段上的方法和函数字段,对于非匿名字段,还会自动添加名空间。

下面我们来举例说明 AddInstanceMethodsAddAllMethods 具体使用方法和区别。

假设我们已经定义了一下数据结构:

  1. // Foo ...
  2. type Foo int
  3.  
  4. // MethodA1 ...
  5. func (Foo) MethodA1() {}
  6.  
  7. // MethodA2 ...
  8. func (*Foo) MethodA2() {}
  9.  
  10. // Bar ...
  11. type Bar struct {
  12. Foo
  13. FuncB func()
  14. }
  15.  
  16. // MethodB1 ...
  17. func (Bar) MethodB1() {}
  18.  
  19. // MethodB2 ...
  20. func (*Bar) MethodB2() {}
  21.  
  22. // Foobar ...
  23. type Foobar struct {
  24. FooField Foo
  25. BarField *Bar
  26. }
  27.  
  28. // MethodC1 ...
  29. func (Foobar) MethodC1() {}
  30.  
  31. // MethodC2 ...
  32. func (*Foobar) MethodC2() {}

现在我们用 http 方式来发布:

  1. func main() {
  2. service := rpc.NewHTTPService()
  3. service.AddInstanceMethods(Foobar{})
  4. http.ListenAndServe(":8080", service)
  5. }

现在我们打开浏览器,输入:

  1. http://127.0.0.1:8080/

我们会看到一下输出:


  1. Fa2{u#s8"MethodC1"}z

这个输出中,u# 是一个特殊的函数,它是 hprose 2.0 默认发布的一个函数,该函数的作用是生成一个唯一的标示,客户端在推送时,可以通过它来获取客户端的唯一标示。这里我们略微一提,后面在推送服务一章再对其详解。我们在这里只要知道它跟我们发布的方法没有关系就可以了。

我们从上面的输出中可以看出,我们的代码只发布了方法 MethodC1 这个方法。

稍微改一下:

  1. func main() {
  2. service := rpc.NewHTTPService()
  3. service.AddInstanceMethods(&Foobar{})
  4. http.ListenAndServe(":8080", service)
  5. }

再重新执行该服务器。打开浏览器,我们会看到内容变成下面这样:


  1. Fa3{u#s8"MethodC1"s8"MethodC2"}z

也就是说,定义在指针上的方法,只有使用指针对象才能发布,而定义在值上的方法,使用指针或值对象都可以发布。

我们再来看一下 AddAllMethods 是怎么发布的:

  1. func main() {
  2. service := rpc.NewHTTPService()
  3. service.AddAllMethods(&Foobar{})
  4. http.ListenAndServe(":8080", service)
  5. }

现在我们会看到内容变成下面这样:


  1. Fa4{u#s8"MethodC1"s8"MethodC2"s17"FooField_MethodA1"}z

FooField_MethodA1 方法也被发布了,但是 FooField_MethodA2BarField 上面的方法都没有发布。原因是,FooField 是一个值字段,所以 FooField_MethodA2 不会被发布。而 BarField 这个指针字段并没有初始化,所以也不会被发布。

那我们再来改一下:

  1. func main() {
  2. service := rpc.NewHTTPService()
  3. service.AddAllMethods(&Foobar{BarField:&Bar{}})
  4. http.ListenAndServe(":8080", service)
  5. }

现在浏览器输出变成了下面这样:


  1. Fa8{u#s8"MethodC1"s8"MethodC2"s17"FooField_MethodA1"s17"BarField_MethodA1"s17"BarField_MethodA2"s17"BarField_MethodB1"s17"BarField_MethodB2"}z

在这里,MethodC1MethodC2 被发布没有疑问。

FooField_MethodA1BarField_MethodA1 分别来自字段 FooFieldBarField,后一个还是通过匿名自动 “继承” 自 Foo 的方法。AddAllMethods 自动把字段名作为名空间将它们区别开了。

BarField 字段现在是一个已经初始化了的指针字段,所以定义在它指针和值上的方法都发布了,包括从匿名字段继承来的方法也发布了。

前面我们还说过除了方法,函数字段也会被发布,我们在 Bar 结构体上定义了 FuncB 这个函数字段,为何这个函数字段没有被发布呢?

原因是在发布之前,它没有被赋值,如果函数字段的值为 nil 则这个函数字段不会被发布。我们把上面的程序稍微修改一下,再来看一下是不是这样:

  1. func main() {
  2. service := rpc.NewHTTPService()
  3. foobar := &Foobar{}
  4. foobar.BarField = new(Bar)
  5. foobar.BarField.FuncB = func() {}
  6. service.AddAllMethods(foobar)
  7. http.ListenAndServe(":8080", service)
  8. }

现在输出变成了这样:


  1. Fa9{u#s8"MethodC1"s8"MethodC2"s17"FooField_MethodA1"s17"BarField_MethodA1"s17"BarField_MethodA2"s17"BarField_MethodB1"s17"BarField_MethodB2"s14"BarField_FuncB"}z

好了,现在 BarField_FuncB 也被发布了。

在上面关于 AddInstanceMethodsAddAllMethods 例子中,我们发布的方法既没有参数也没有结果,这仅仅是为了举例方便,而不是说 hprose 只能发布这样的方法,实际上 hprose 可以发布的方法,对参数个数没有限制,对返回值个数也没有限制。甚至连可变参数的方法都支持。

AddMissingMethod 方法

  1. type MissingMethod func(name string, args []reflect.Value, context Context) ([]reflect.Value, error)
  1. AddMissingMethod(method MissingMethod, option ...Options) Service

该方法用于发布一个用于处理客户端调用缺失服务的函数。缺失服务是指服务器端并没有明确发布的远程函数/方法。例如:

在服务器端没有发布 hello 函数时,在默认情况下,客户端调用该函数,服务器端会返回 `'Can't find this function hello().' 这样一个错误。

但是如果服务器端通过本方法发布了一个用于处理客户端调用缺失服务的 method,则服务器端会返回这个 method 函数(方法)的返回值。

该方法还可以用于做 hprose 的代理服务器,例如:

  1. package main
  2.  
  3. import (
  4. "net/http"
  5. "reflect"
  6.  
  7. "github.com/hprose/hprose-golang/rpc"
  8. )
  9.  
  10. type HproseProxy struct {
  11. client rpc.Client
  12. settings rpc.InvokeSettings
  13. }
  14.  
  15. func newHproseProxy() *HproseProxy {
  16. proxy := new(HproseProxy)
  17. proxy.client = rpc.NewClient("http://www.hprose.com/example/")
  18. proxy.settings = rpc.InvokeSettings{
  19. Mode: rpc.Raw,
  20. ResultTypes: []reflect.Type{reflect.TypeOf(([]byte)(nil))},
  21. }
  22. return proxy
  23. }
  24.  
  25. func (proxy *HproseProxy) Proxy(
  26. name string, args []reflect.Value, context rpc.Context) ([]reflect.Value, error) {
  27. return proxy.client.Invoke(name, args, &proxy.settings)
  28. }
  29.  
  30. func main() {
  31. service := rpc.NewHTTPService()
  32. service.AddMissingMethod(newHproseProxy().Proxy, rpc.Options{Mode: rpc.Raw})
  33. http.ListenAndServe(":8080", service)
  34. }

运行改程序,用浏览器打开服务地址,我们会看到:


  1. Fa2{u#u*}z

其中 u* 表示的就是这个 MissingMethod。现在如果调用 hello 方法,我们就会得到 http://www.hprose.com/example/ 这个服务地址提供的 hello 方法的返回值了。

MissingMethod 里面的进行 Invoke 调用时,我们设置了调用模式是 Raw,这样 MissingMethod 的返回值就是远程服务的原始未解析数据,省去了反序列化的过程,而我们发布的 MissingMethod 设置了调用模式是 Raw,这样就可以直接把 MissingMethod 的结果传递给客户端,省去了序列化过程。所以使用这种方式来实现 hprose 代理会更加高效。

AddNetRPCMethods 方法

  1. AddNetRPCMethods(rcvr interface{}, option ...Options) Service

该方法用于发布为 net/rpc 编写的 RPC 服务。只有满足如下标准的方法才会被当做 net/rpc 的远程服务方法,其余方法会被忽略:

  • 方法是导出的
  • 方法有两个参数,都是导出类型或内建类型
  • 方法的第二个参数是指针
  • 方法只有一个error接口类型的返回值
    事实上,方法必须看起来像这样:
  1. func (t *T) MethodName(argType T1, replyType *T2) error

通过 hprose 发布的 net/rpc 服务并不能用 net/rpc 客户端去调用,但是可以用任何语言的 hprose 客户端去调用(当然也包括 golang 的 hprose 客户端)。

调用时,方法名跟发布的方法名相同,参数只有一个,就是第一个参数,而发布的方法中的第二个参数会被作为返回值返回,服务器和客户端的参数类型不必完全一致,但需要是可相互转换的类型,这里可相互转化的含义要比 go 内置的类型转换范围要广一些,比如数字、*big.Int 和数字字符串是可以相互转换的,结构体对象和其指针类型也是可以相互转换的。而发布的方法中的 error 返回值会被作为异常返回(如果返回值为 nil,则没有异常)。

我们下面来看一个具体的例子:

  1. package main
  2.  
  3. import (
  4. "errors"
  5. "net/http"
  6.  
  7. "github.com/hprose/hprose-golang/rpc"
  8. )
  9.  
  10. type Args struct {
  11. A, B int
  12. }
  13.  
  14. type Quotient struct {
  15. Quo, Rem int
  16. }
  17.  
  18. type Arith int
  19.  
  20. func (t *Arith) Multiply(args *Args, reply *int) error {
  21. *reply = args.A * args.B
  22. return nil
  23. }
  24.  
  25. func (t *Arith) Divide(args *Args, quo *Quotient) error {
  26. if args.B == 0 {
  27. return errors.New("divide by zero")
  28. }
  29. quo.Quo = args.A / args.B
  30. quo.Rem = args.A % args.B
  31. return nil
  32. }
  33.  
  34. func main() {
  35. service := rpc.NewHTTPService()
  36. service.AddNetRPCMethods(new(Arith))
  37. http.ListenAndServe(":8080", service)
  38. }

这个例子里面,main 函数之前的内容,跟 golang 标准库文档中的 net/rpc 的例子中的内容完全一样。而发布服务则比 net/rpc 里面还要简单一些,而且你可以用任何 hprose 支持的方式来发布。

下面我们再来看一下如何在客户端调用该服务,首先我们来看在 golang 中如何调用:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "log"
  6.  
  7. "github.com/hprose/hprose-golang/rpc"
  8. )
  9.  
  10. type Args struct {
  11. A, B int
  12. }
  13.  
  14. type Quotient struct {
  15. Quo, Rem int
  16. }
  17.  
  18. type Stub struct {
  19. // Synchronous call
  20. Multiply func(args *Args) int
  21. // Asynchronous call
  22. Divide func(func(*Quotient, error), *Args)
  23. }
  24.  
  25. func main() {
  26. client := rpc.NewClient("http://127.0.0.1:8080")
  27. var stub *Stub
  28. client.UseService(&stub)
  29. fmt.Println(stub.Multiply(&Args{8, 7}))
  30. done := make(chan struct{})
  31. stub.Divide(func(result *Quotient, err error) {
  32. if err != nil {
  33. log.Fatal("arith error:", err)
  34. } else {
  35. fmt.Println(result.Quo, result.Rem)
  36. }
  37. done <- struct{}{}
  38. }, &Args{8, 7})
  39. <-done
  40. }

在这里 Multiply 方法是同步调用,在客户端的定义中,我们会看到跟服务器端定义的方式不同,参数仅仅是服务器端方法的第一个参数,而结果类型是 int,这里我们没有定义 error 类型的返回值,这样如果调用发生错误,程序会发生 panic,如果你不希望发生 panic,你可以在 int 后面再加个 error 类型的结果。

Divide 是异步调用,第一个参数是回调方法,第二个参数才是传给远程服务的参数。

根据对应关系,我想上面的客户端代码并不难理解,所以这里就不做进一步解释了。

下面我们来看一个用 js 调用上面两个方法的例子:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>hprose</title>
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <script src="hprose.js"></script>
  8. </head>
  9. <body>
  10. <script type="text/javascript">
  11. // The method name is case-insensitive
  12. var methods = ['multiply', 'divide'];
  13. var client = hprose.Client.create('http://127.0.0.1:8080/', methods);
  14. // The first letter of the field name is convert to lowercase.
  15. client.multiply({a:8, b:7}).then(function(product) {
  16. return client.divide({a:product, b:6});
  17. }).then(function(quotient) {
  18. // The first letter of the field name is convert to lowercase.
  19. console.log(quotient.quo, quotient.rem);
  20. }).catch(function(err) {
  21. // The returned err is here
  22. console.error(err);
  23. });
  24. </script>
  25. </body>
  26. </html>

在这里,调用的方法名是忽略大小写区别的,我们都用小写的方式也可以成功调用。对于参数的字段和结果中的结构体字段,首字母要改为小写。除了这两点需要注意之外,其它的内容直接参考 hprose-js 的文档就可以。

Remove 方法

  1. Remove(name string) Service

将发布的方法删除。注意这里的 name 参数是指客户端调用的方法名(包括名空间部分)。例如如果要删除下面这个方法:

  1. server.AddFunction("hello", hi, rpc.Options{NameSpace: "Test"})

需要使用:

  1. server.Remove("test_hello")

来删除,注意这里不区分大小写。