Golang-RPC(一):golang中的rpc实现

标砖库提供了 net/rpc 包用来实现基础的rpc调用。

net/rpc库使用encoding/gob进行编解码,支持tcphttp数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc库实现的RPC方法没办法进行跨语言调用。

主要有服务端和客户端。

首先是提供方法暴露的一方–服务器。

一、服务定义及暴露

在编程实现过程中,服务器端需要注册结构体对象,然后通过对象所属的方法暴露给调用者,从而提供服务,该方法称之为输出方法,此输出方法可以被远程调用。当然,在定义输出方法时,能够被远程调用的方法需要遵循一定的规则。我们通过代码进行讲解:

func (t *T) MethodName(request T1,response *T2) error

上述代码是go语言官方给出的对外暴露的服务方法的定义标准,其中包含了主要的几条规则,分别是:

1、对外暴露的方法有且只能有两个参数,这个两个参数只能是输出类型或内建类型,两种类型中的一种。
2、方法的第二个参数必须是指针类型。
3、方法的返回类型为error。
4、方法的类型是可输出的。
5、方法本身也是可输出的。
package repo

import (
	"errors"
)

type Order struct {
     }

type OrderInfo struct {
     
	Id string
	Price float64
	Status int
}

func (o *Order) GetOne(orderId string, orderInfo *OrderInfo) error {
     
	if orderId == "" {
     
		return errors.New("orderId is invalid")
	}

	*orderInfo = OrderInfo{
     
		Id: orderId,
		Price: 100.00,
		Status: 1,
	}

	return nil
}

正常情况下,方法的返回值为是error,为nil。如果遇到异常或特殊情况,则error将作为一个字符串返回给调用者,此时,resp参数就不会再返回给调用者。

至此为止,已经实现了服务端的功能定义,接下来就是让服务代码生效,需要将服务进行注册,并启动请求处理。

二、注册服务及监听请求 - HTTP

net/rpc包为我们提供了注册服务和处理请求的一系列方法,结合本案例实现注册及处理逻辑,如下所示:

// 调用net/rpc包的功能将服务对象进行注册
err := rpc.Register(new(repo.Order))
if err != nil {
     
	log.Fatal(err)
}

// 通过该函数把Order中提供的服务注册到HTTP协议上,方便调用者可以利用http的方式进行数据传递
rpc.HandleHTTP()

// 在特定的端口进行监听
l, err := net.Listen("tcp", ":8100")
if err != nil {
     
	log.Fatal(err)
}

err = http.Serve(l, nil)
if err != nil {
     
	log.Fatal(err)
}

经过服务注册和监听处理,RPC调用过程中的服务端实现就已经完成了。接下来需要实现的是客户端请求代码的实现。

当然,你可以注册多个服务到同一个端口。

三、客户端调用 - HTTP

在服务端是通过Http的端口监听方式等待连接的,因此在客户端就需要通过http连接,首先与服务端实现连接。

package main

import (
	"fmt"
	"log"
	"net/rpc"

	"demo1/go-rpc/repo"
)

func main() {
     
	client, err := rpc.DialHTTP("tcp", "127.0.0.1:8100")
	if err != nil {
     
		log.Fatal("dialing:", err)
	}

	orderId := "aaaa"
	var orderInfo repo.OrderInfo

	err = client.Call("Order.GetOne", orderId, &orderInfo)
	if err != nil {
     
		log.Fatal("Order error:", err)
	}

	fmt.Println(orderInfo)
}

打印信息

{
     aaaa 100 1}

当然,客户端和服务端同时使用了 repo.OrderInfo 对象,所以应该将其放在公共的 pkg 中方便管理。

上述的Call方法调用实现的方式是同步的调用,除此之外,还有一种异步的方式可以实现调用。异步调用代码实现如下:

package main

import (
	"fmt"
	"log"
	"net/rpc"

	"demo1/go-rpc/repo"
)

func main() {
     
	client, err := rpc.DialHTTP("tcp", "127.0.0.1:8100")
	if err != nil {
     
		log.Fatal("dialing:", err)
	}

	orderId := "aaaa"
	var orderInfo repo.OrderInfo

	syncCall := client.Go("Order.GetOne", orderId, &orderInfo, nil)
	<-syncCall.Done
	fmt.Println(orderInfo)
}

同时,net/rpc 还提供了一个简易的统计页供查看:
http://127.0.0.1:8100/debug/rpc

Golang-RPC(一):golang中的rpc实现_第1张图片

四、注册服务及监听请求 - TCP

package main

import (
	"log"
	"net"
	"net/rpc"

	"demo1/go-rpc/repo"
)

func main() {
     
	err := rpc.Register(new(repo.Order))
	if err != nil {
     
		log.Fatal(err)
	}

	l, err := net.Listen("tcp", ":8100")
	if err != nil {
     
		log.Fatal(err)
	}

	for {
     
		conn, e := l.Accept()
		if e != nil {
     
			continue
		}
		go rpc.ServeConn(conn)
	}
}

五、客户端调用 - TCP

package main

import (
	"fmt"
	"log"
	"net/rpc"

	"demo1/go-rpc/repo"
)

func main() {
     
	client, err := rpc.Dial("tcp", "127.0.0.1:8100")
	if err != nil {
     
		log.Fatal("dialing:", err)
	}

	orderId := "aaaa"
	var orderInfo repo.OrderInfo

	err = client.Call("Order.GetOne", orderId, &orderInfo)
	if err != nil {
     
		log.Fatal("Order error:", err)
	}

	fmt.Println(orderInfo)
}

六、原理解析

首先注册了两个路由与handle,这是 rpc.HandleHTTP() 做的事情。

func (server *Server) HandleHTTP(rpcPath, debugPath string) {
     
	http.Handle(rpcPath, server)
	http.Handle(debugPath, debugHTTP{
     server})
}

func HandleHTTP() {
     
	DefaultServer.HandleHTTP(DefaultRPCPath, DefaultDebugPath)
}


其中

DefaultRPCPath   = "/_goRPC_"
DefaultDebugPath = "/debug/rpc"

而且 /debug/rpc 在上面还用到过;
/_goRPC_ 则是rpc服务的路由,然后通过传递的参数去调用指定对象的方法。

再就是注册服务,其实就是通过反射导出对象的可调用的方法存入一个map。这是 rpc.Register(new(repo.Order)) 做的事情。

func (server *Server) register(rcvr interface{
     }, name string, useName bool) error {
     
	s := new(service)
	s.typ = reflect.TypeOf(rcvr)
	s.rcvr = reflect.ValueOf(rcvr)
	sname := reflect.Indirect(s.rcvr).Type().Name()
	if useName {
     
		sname = name
	}
	if sname == "" {
     
		s := "rpc.Register: no service name for type " + s.typ.String()
		log.Print(s)
		return errors.New(s)
	}
	if !token.IsExported(sname) && !useName {
     
		s := "rpc.Register: type " + sname + " is not exported"
		log.Print(s)
		return errors.New(s)
	}
	s.name = sname

	// Install the methods
	s.method = suitableMethods(s.typ, true)

	if len(s.method) == 0 {
     
		str := ""

		// To help the user, see if a pointer receiver would work.
		method := suitableMethods(reflect.PtrTo(s.typ), false)
		if len(method) != 0 {
     
			str = "rpc.Register: type " + sname + " has no exported methods of suitable type (hint: pass a pointer to value of that type)"
		} else {
     
			str = "rpc.Register: type " + sname + " has no exported methods of suitable type"
		}
		log.Print(str)
		return errors.New(str)
	}

	if _, dup := server.serviceMap.LoadOrStore(sname, s); dup {
     
		return errors.New("rpc: service already defined: " + sname)
	}
	return nil
}

func suitableMethods(typ reflect.Type, reportErr bool) map[string]*methodType {
     
	methods := make(map[string]*methodType)
	for m := 0; m < typ.NumMethod(); m++ {
     
		method := typ.Method(m)
		mtype := method.Type
		mname := method.Name
		...
		...
		methods[mname] = &methodType{
     method: method, ArgType: argType, ReplyType: replyType}
	}
	return methods
}

为了更直观的理解,可以看下面的例子。

package main

import (
	"fmt"
	"reflect"
)

type Peaple struct {
     
}

func (p *Peaple) Eat() {
     

}
func (p *Peaple) Drink() {
     

}

func main() {
     
	a := new(Peaple)
	getType := reflect.TypeOf(a)
	getValue := reflect.ValueOf(a)

	obj := reflect.Indirect(getValue).Type().Name()

	num := getType.NumMethod()

	for i := 0; i < num; i++ {
     
		m := getType.Method(i)
		fmt.Println(obj + "." + m.Name)
	}
}

输出结果

Peaple.Drink
Peaple.Eat

也就是客户端调用的第一个参数。

你可能感兴趣的:(golang)