RPC就是想实现函数调用模式的网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
RPC(RemoteProcedureCallProtocol)——远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
1.调用客户端句柄;执行传送参数 2.调用本地系统内核发送网络消息 3.消息传送到远程主机 4.服务器句柄得到消息并取得参数 5.执行远程过程 6.执行的过程将结果返回服务器句柄 7.服务器句柄返回结果,调用远程系统内核 8.消息传回本地主机 9.客户句柄由内核接收消息 10.客户接收句柄返回的数据Go标准包中已经提供了对RPC的支持,而且支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。
GoRPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:
函数必须是导出的(首字母大写) 必须有两个导出类型的参数, 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的 函数还要有一个返回值error举个例子,正确的RPC函数格式如下:
func(t*T)MethodName(argTypeT1,replyType*T2)errorT、T1和T2类型必须能被encoding/gob包编解码。
任何的RPC都需要通过网络来传递数据,GoRPC可以利用HTTP和TCP来传递数据,利用HTTP的好处是可以直接复用net/http里面的一些函数。详细的例子请看下面的实现
http的服务端代码实现如下:
packagemain import( "errors" "fmt" "net/http" "net/rpc" ) typeArgsstruct{ A,Bint } typeQuotientstruct{ Quo,Remint } typeArithint func(t*Arith)Multiply(args*Args,reply*int)error{ *reply=args.A*args.B returnnil } func(t*Arith)Divide(args*Args,quo*Quotient)error{ ifargs.B==0{ returnerrors.New("dividebyzero") } quo.Quo=args.A/args.B quo.Rem=args.A%args.B returnnil } funcmain(){ arith:=new(Arith) rpc.Register(arith) rpc.HandleHTTP() err:=http.ListenAndServe(":1234",nil) iferr!=nil{ fmt.Println(err.Error()) } }通过上面的例子可以看到,我们注册了一个Arith的RPC服务,然后通过rpc.HandleHTTP函数把该服务注册到了HTTP协议上,然后我们就可以利用http的方式来传递数据了。
请看下面的客户端代码:
packagemain import( "fmt" "log" "net/rpc" "os" ) typeArgsstruct{ A,Bint } typeQuotientstruct{ Quo,Remint } funcmain(){ iflen(os.Args)!=2{ fmt.Println("Usage:",os.Args[0],"server") os.Exit(1) } serverAddress:=os.Args[1] client,err:=rpc.DialHTTP("tcp",serverAddress+":1234") iferr!=nil{ log.Fatal("dialing:",err) } //Synchronouscall args:=Args{17,8} varreplyint err=client.Call("Arith.Multiply",args,&reply) iferr!=nil{ log.Fatal("aritherror:",err) } fmt.Printf("Arith:%d*%d=%d\n",args.A,args.B,reply) varquotQuotient err=client.Call("Arith.Divide",args,") iferr!=nil{ log.Fatal("aritherror:",err) } fmt.Printf("Arith:%d/%d=%dremainder%d\n",args.A,args.B,quot.Quo,quot.Rem) }我们把上面的服务端和客户端的代码分别编译,然后先把服务端开启,然后开启客户端,输入代码,就会输出如下信息:
$./http_clocalhost Arith:17*8=136 Arith:17/8=2remainder1通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为client.Call的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(注意是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。
上面我们实现了基于HTTP协议的RPC,接下来我们要实现基于TCP协议的RPC,服务端的实现代码如下所示:
packagemain import( "errors" "fmt" "net" "net/rpc" "os" ) typeArgsstruct{ A,Bint } typeQuotientstruct{ Quo,Remint } typeArithint func(t*Arith)Multiply(args*Args,reply*int)error{ *reply=args.A*args.B returnnil } func(t*Arith)Divide(args*Args,quo*Quotient)error{ ifargs.B==0{ returnerrors.New("dividebyzero") } quo.Quo=args.A/args.B quo.Rem=args.A%args.B returnnil } funcmain(){ arith:=new(Arith) rpc.Register(arith) tcpAddr,err:=net.ResolveTCPAddr("tcp",":1234") checkError(err) listener,err:=net.ListenTCP("tcp",tcpAddr) checkError(err) for{ conn,err:=listener.Accept() iferr!=nil{ continue } rpc.ServeConn(conn) } } funccheckError(errerror){ iferr!=nil{ fmt.Println("Fatalerror",err.Error()) os.Exit(1) } }上面这个代码和http的服务器相比,不同在于:在此处我们采用了TCP协议,然后需要自己控制连接,当有客户端连接上来后,我们需要把这个连接交给rpc来处理。
如果你留心了,你会发现这它是一个阻塞型的单用户的程序,如果想要实现多并发,那么可以使用goroutine来实现,我们前面在socket小节的时候已经介绍过如何处理goroutine。下面展现了TCP实现的RPC客户端:
packagemain import( "fmt" "log" "net/rpc" "os" ) typeArgsstruct{ A,Bint } typeQuotientstruct{ Quo,Remint } funcmain(){ iflen(os.Args)!=2{ fmt.Println("Usage:",os.Args[0],"server:port") os.Exit(1) } service:=os.Args[1] client,err:=rpc.Dial("tcp",service) iferr!=nil{ log.Fatal("dialing:",err) } //Synchronouscall args:=Args{17,8} varreplyint err=client.Call("Arith.Multiply",args,&reply) iferr!=nil{ log.Fatal("aritherror:",err) } fmt.Printf("Arith:%d*%d=%d\n",args.A,args.B,reply) varquotQuotient err=client.Call("Arith.Divide",args,") iferr!=nil{ log.Fatal("aritherror:",err) } fmt.Printf("Arith:%d/%d=%dremainder%d\n",args.A,args.B,quot.Quo,quot.Rem) }这个客户端代码和http的客户端代码对比,唯一的区别一个是DialHTTP,一个是Dial(tcp),其他处理一模一样。
JSONRPC是数据编码采用了JSON,而不是gob编码,其他和上面介绍的RPC概念一模一样,下面我们来演示一下,如何使用Go提供的json-rpc标准包,请看服务端代码的实现:
packagemain import( "errors" "fmt" "net" "net/rpc" "net/rpc/jsonrpc" "os" ) typeArgsstruct{ A,Bint } typeQuotientstruct{ Quo,Remint } typeArithint func(t*Arith)Multiply(args*Args,reply*int)error{ *reply=args.A*args.B returnnil } func(t*Arith)Divide(args*Args,quo*Quotient)error{ ifargs.B==0{ returnerrors.New("dividebyzero") } quo.Quo=args.A/args.B quo.Rem=args.A%args.B returnnil } funcmain(){ arith:=new(Arith) rpc.Register(arith) tcpAddr,err:=net.ResolveTCPAddr("tcp",":1234") checkError(err) listener,err:=net.ListenTCP("tcp",tcpAddr) checkError(err) for{ conn,err:=listener.Accept() iferr!=nil{ continue } jsonrpc.ServeConn(conn) } } funccheckError(errerror){ iferr!=nil{ fmt.Println("Fatalerror",err.Error()) os.Exit(1) } }通过示例我们可以看出json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。
请看客户端的实现代码:
packagemain import( "fmt" "log" "net/rpc/jsonrpc" "os" ) typeArgsstruct{ A,Bint } typeQuotientstruct{ Quo,Remint } funcmain(){ iflen(os.Args)!=2{ fmt.Println("Usage:",os.Args[0],"server:port") log.Fatal(1) } service:=os.Args[1] client,err:=jsonrpc.Dial("tcp",service) iferr!=nil{ log.Fatal("dialing:",err) } //Synchronouscall args:=Args{17,8} varreplyint err=client.Call("Arith.Multiply",args,&reply) iferr!=nil{ log.Fatal("aritherror:",err) } fmt.Printf("Arith:%d*%d=%d\n",args.A,args.B,reply) varquotQuotient err=client.Call("Arith.Divide",args,") iferr!=nil{ log.Fatal("aritherror:",err) } fmt.Printf("Arith:%d/%d=%dremainder%d\n",args.A,args.B,quot.Quo,quot.Rem) }Go已经提供了对RPC的良好支持,通过上面HTTP、TCP、JSONRPC的实现,我们就可以很方便的开发很多分布式的Web应用,我想作为读者的你已经领会到这一点。但遗憾的是目前Go尚未提供对SOAPRPC的支持,欣慰的是现在已经有第三方的开源实现了。
原文链接: