13 RPC 远程过程调用
Introduction
介绍
Socket and HTTP programming use a message-passing paradigm. A client sends a message to a server which usually sends a message back. Both sides are responsible for creating messages in a format understood by both sides, and in reading the data out of those messages.
Socket 和 HTTP 编程 使用的是一种消息传递模式. 一个客户端发送了一个消息给服务器,通常会等回一个响应消息。两边都要创建出一种双方可理解的格式,然后从里面读出数据的实体。
However, most standalone applications do not make so much use of message passing techniques. Generally the preferred mechanism is that of the function
(or method or procedure) call. In this style, a program will call a function with a list of parameters, and on completion of the function call will have a set of return values. These values may be the function value, or if addresses have been passed as parameters then the contents of those addresses might have been changed.
然而,大多数独立主机应用不会做太多的消息传递技术。一般来说,函数调用
(或者被称作method/procedure)的使用更为普遍。在函数风格下,程序会调用函数时会传入一系列参数,然后函数调用完毕后会返回一系列返回值。这些返回值会成为函数的值,或者传递进函数的是参数的地址引用,那么参数值可能最后会被修改。
The remote procedure call is an attempt to bring this style of programming into the network world. Thus a client will make what looks to it like a normal procedure call. The client-side will package this into a network message and transfer it to the server. The server will unpack this and turn it back into a procedure call on the server side. The results of this call will be packaged up for return to the client.
远程过程调用的初衷就是把这种风格带入网络世界。客户调用时候会让这一切看起来像是函数调用,而客户端会打包这些数据成为消息,然后传递到远端服务器。服务器再拆解包,然后把它变成在服务器端的过程调用,而最后的返回结果会被打包传回给客户。
Diagrammatically it looks like
where the steps are
用图示表示的话,看起来就会是这个样子
经历如下几个步骤
- The client calls the local stub procedure. The stub packages up the parameters into a network message. This is called marshalling.
- Networking functions in the O/S kernel are called by the stub to send the message.
- The kernel sends the message(s) to the remote system. This may be connection-oriented or connectionless.
- A server stub unmarshals the arguments from the network message.
- The server stub executes a local procedure call.
- The procedure completes, returning execution to the server stub.
- The server stub marshals the return values into a network message.
- The return messages are sent back.
- The client stub reads the messages using the network functions.
- The message is unmarshalled. and the return values are set on the stack for the local process.
- 客户调用本地存根节点过程, 存根节点会把参数打包成网络消息,这个过程被称为编组
- OS内核里的网络通信函数会被存根节点调用来发送消息。
- 内核把消息传递给远端系统。这个可以使面向连接的或者是无连接传输模式。
- 服务器端的存根节点会把参数从网络消息中拆解出来。
- 服务器端的存根节点会执行一个本地过程调用
- 等到过程完成,返回之行结果给服务器端的存根节点。
- 服务器存根节点会把返回值编组成网络消息。
- 消息被返回
- 客户端存根节点用网络通信函数读取消息
- 消息被拆解。然后返回值被放到本地程序的堆栈内。
There are two common styles for implementing RPC. The first is typified by Sun's RPC/ONC and by CORBA. In this, a specification of the service is given in some abstract language such as CORBA IDL (interface definition language). This is then compiled into code for the client and for the server. The client then writes a normal program containing calls to a procedure/function/method which is linked to the generated client-side code. The server-side code is actually a server itself, which is linked to the procedure implementation that you write.
远程过程调用有两种普遍使用的风格。第一个是以SUN开发的CORBA的RPC/ONC为代表。这里,服务的描述被某种像CORBA IDL(接口定义语言)抽象语言提供,然后编译成可执行代码分别部署在client端和server端。客户接着就可以写一个常规的程序去连接那个生成出来的方法,而server端的代码实际上就是server服务的实体,然后连接到你实现的程序。
In this way, the client-side code is almost identical in appearance to a normal procedure call. Generally there is a little extra code to locate the server. In Sun's ONC, the address of the server must be known; in CORBA a naming service is called to find the address of the server; In Java RMI, the IDL is Java itself and a naming service is used to find the address of the service.
这样,客户端代码就基本上跟一个普通的程序调用没什么区别了。一般来说,在server端部署的代码量会有点多。在SUN开发的的ONC上,server端的地址必须是公开的。在CORBA里面,一个命名服务会启动去寻找服务器的地址。而在JAVA RMI中,IDL由Java类库实现,然后命名服务会被调用去寻找服务器地址。
In the second style, you have to make use of a special client API. You hand the function name and its parameters to this library on the client side. On the server side, you have to explicitly write the server yourself, as well as the remote procedure implementation.
在第二种风格中,你会用到一些特别的client端API,这些API,包括函数名,和参数是在生成的client代码中的。与此不同的是,在server端,你必须用你的手把代码敲出来,包括这些远程函数的实现。
This approach is used by many RPC systems, such as Web Services. It is also the approach used by Go's RPC.
很多RPC系统都采用了这种方法,比如Web Services. 当然,Go的PRC也采用了这样的方法。
Go RPC
Go RPC
Go's RPC is so far unique to Go. It is different to the other RPC systems, so a Go client will only talk to a Go server. It uses the Gob serialisation system discussed in chapter X, which defines the data types which can be used.
Go的RPC是非常独特的。它与别的RPC系统不同,所以Go的client只能跟Go的server对话。它被用在第十章讨论的Gob序列化系统里面,用来定义可被使用的数据类型。
RPC systems generally make some restrictions on the functions that can be called across the network. This is so that the RPC system can properly determine what are value arguments to be sent, what are reference arguments to receive answers, and how to signal errors.
RPC系统一般来说是对远程的函数调用的一些限定。这也就是为什么RPC系统可以恰当地决定哪些参数要被传递,哪些引用参数来接受数据,以及如何做错误警报。
- the function must be public (begin with a capital letter);
- have exactly two arguments, the first is a pointer to value data to be received by the function from the client, and the second is a pointer to hold the answers to be returned to the client; and
- have a return value of type
os.Error
- 函数必须是公共的(也就是首字母大写)
- 有且仅有2个指针参数,第一个指向 “接收器”——接受从client端发过来的数据值,第二个指向 "发送器"——存放向client端发送的返回值。
- 有一个
os.Error
类型返回值
For example, a valid function is
比方说,一个合法的函数应该是如下这样的
F(&T1, &T2) os.Error
The restriction on arguments means that you typically have to define a structure type. Go's RPC uses the gob
package for marshalling and unmarshalling data, so the argument types have to follow the rules of gob
as discussed in an earlier chapter.
所谓的对参数的限定指的是你只需要定义数据类型。Go的RPC会用gob
包来编组和解编组数据,所以对于参数类型,你只需要按照之前讨论过的gob
的规则定义就可以。
We shall follow the example given in the Go documentation, as this illustrates the important points. The server performs two operations which are trivial - they do not require the "grunt" of RPC, but are simple to understand. The two operations are to multiply two integers, and the second is to find the quotient and remainder after dividing the first by the second.
我们应该参考Go的官方文档的例子,因为这些例子展示了一些关键点。Server端执行2种操作,这些操作看起来非常浅显易懂,这里没用RPC的那些难懂的细节,而是非常易于理解。 第一种操作是两个整数相乘,第二个则是第一个数字除以第二个数字然后求商取余。
The two values to be manipulated are given in a structure:
2个操作数被放在了一个结构体里:
type Values struct {
X, Y int
}
The sum is just an int
, while the quotient/remainder is another structure
两数之和是一个 int
, 而商数和余数则在另一个结构体里
type Quotient struct {
Quo, Rem int
}
We will have two functions, multiply and divide to be callable on the RPC server. These functions will need to be registered with the RPC system. The function Register
takes a single parameter, which is an interface. So we need a type with these two functions:
我们会把这两个程序,也就是乘法和除法, 部署在RPC的server端等待调用。这些函数过会儿会被注册到RPC系统里去。函数Register
会带一个interface类型的参数。 所以我们要给这两个函数定义一个类型。
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) os.Error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) os.Error {
if args.B == 0 {
return os.ErrorString("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
The underlying type of Arith
is given as int
. That doesn't matter - any type could have done.
Arith
背后的实际类型是 int
. 这不要紧 - 任何类型都可以。
An object of this type can now be registered using Register
, and then its methods can be called by the RPC system.
这个类型的对象现在可以用Register
函数来注册, 之后,RPC系统就可以调用这个方法了。
HTTP RPC Server
HTTP RPC 服务器
Any RPC needs a transport mechanism to get messages across the network. Go can use HTTP or TCP. The advantage of the HTTP mechanism is that it can leverage off the HTTP suport library. You need to add an RPC handler to the HTTP layer which is done using HandleHTTP
and then start an HTTP server. The complete code is
任何RPC系统都需要一个传输机制来跨网络地传递消息。Go可以用HTTP或TCP。用HTTP机制的优势就是可以借助HTTP来支持库文件。 你需要通过HandleHTTP
在HTTP层上加一个RPC处理器, 然后启动一个HTTP 服务器。完整的代码是这样
/**
* ArithServer
*/
package main
import (
"fmt"
"net/rpc"
"errors"
"net/http"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
err := http.ListenAndServe(":1234", nil)
if err != nil {
fmt.Println(err.Error())
}
}
HTTP RPC client
HTTP RPC 客户端
The client needs to set up an HTTP connection to the RPC server. It needs to prepare a structure with the values to be sent, and the address of a variable to store the results in. Then it can make a Call
with arguments:
客户端需要设置一个HTTP连接,来连接RPC服务器。客户端需要发起一个对RPC服务器的连接。它需要准备一个包含要发送数据的结构体, 以及一个接受返回值的变量地址。之后,它就可以用参数来 调用
了,参数如下
- The name of the remote function to execute
- The values to be sent
- The address of a variable to store the result in
- 想要调用的远程函数的名字
- 被发送的数据结构体
- 储存返回值的变量地址
A client that calls both functions of the arithmetic server is
一个调用在远端服务器上的这两个计算函数的客户端是这样的
/**
* ArithClient
*/
package main
import (
"net/rpc"
"fmt"
"log"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
TCP RPC server
TCP RPC 服务端
A version of the server that uses TCP sockets is
一个使用TCP socket的服务器是这样的
/**
* TCPArithServer
*/
package main
import (
"fmt"
"net/rpc"
"errors"
"net"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
/* This works:
rpc.Accept(listener)
*/
/* and so does this:
*/
for {
conn, err := listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
Note that the call to Accept
is blocking, and just handles client connections. If the server wishes to do other work as well, it should call this in a goroutine.
留心一点,对于Accept
的调用是阻塞式的,用来处理客户端连接。如果服务端希望也做点别的事情,那么就应该在goroutine中调用它。
TCP RPC client
TCP RPC 客户端
A client that uses the TCP server and calls both functions of the arithmetic server is
一个使用TCP连接,调用在远端计算服务器的两个函数的客户端是这样的。
/**
* TCPArithClient
*/
package main
import (
"net/rpc"
"fmt"
"log"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
service := os.Args[1]
client, err := rpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
Matching values
数据值匹配
We note that the types of the value arguments are not the same on the client and server. In the server, we have used Values
while in the client we used Args
. That doesn't matter, as we are following the rules of gob
serialisation, and the names an types of the two structures' fields match. Better programming practise would say that the names should be the same!*
我们注意到在server端和client端的数据类型并不相同。在服务器端,我们用的是Values
而在客户端我们用了 Args
。这并不成问题,因为我们按照了gob
串行化规则,而且在两个结构体字段中的名称能匹配。但更好的编程实践却告诉我们,名字也应该相同。*
However, this does point out a possible trap in using Go RPC. If we change the structure in the client to be, say,
然而,这指出了go中可能存在的陷阱的可能性。要是我们改变了client端的结构体,比方说,
type Values struct {
C, B int
}
then gob
has no problems: on the server-side the unmarshalling will ignore the value of C
given by the client, and use the default zero value for A
.
而这对于 gob
来说却没有什么疑问: 在server端解编组的时候会忽略来自client的C
,然后将默认值零值赋给A
.
Using Go RPC will require a rigid enforcement of the stability of field names and types by the programmer. We note that there is no version control mechanism to do this, and no mechanism in gob
to signal any possible mismatches.
用Go RPC会要求对字段名称和类型的一致性都进行严格加强。我们注意到,没有任何的版本控制机制或是gob
本身,都没有任何提示数据不匹配的保护机制。
JSON
JSON
This section adds nothing new to the earlier concepts. It just uses a different "wire" format for the data, JSON instead of gob
. As such, clients or servers could be written in other language that understand sockets and JSON.
这部分每增加什么新的概念。只是用了另一种数据的 "电报" 格式, 用JSON来代替gob
。由于这样做了,那么client端和server端要用另一种语言来来理解socket和JSON。
JSON RPC client
JSON RPC 客户端
A client that calls both functions of the arithmetic server is
客户端调用计算服务器的两个函数如下
/* JSONArithCLient
*/
package main
import (
"net/rpc/jsonrpc"
"fmt"
"log"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
log.Fatal(1)
}
service := os.Args[1]
client, err := jsonrpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
JSON RPC server
JSON RPC 服务器
A version of the server that uses JSON encoding is
JSON版的服务器代码如下
/* JSONArithServer
*/
package main
import (
"fmt"
"net/rpc"
"net/rpc/jsonrpc"
"os"
"net"
"errors"
)
//import ("fmt"; "rpc"; "os"; "net"; "log"; "http")
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
/* This works:
rpc.Accept(listener)
*/
/* and so does this:
*/
for {
conn, err := listener.Accept()
if err != nil {
continue
}
jsonrpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
Conclusion
总结
RPC is a popular means of distributing applications. Several ways of doing it have been presented here. What is missing from Go is support for the currently fashionable (but extremely badly enginereed) SOAP RPC mechanism.
RPC 是一个流行的分布应用的方法。这里展示了许多实现它的方法。Go所不支持的实现下很火的(却也是实现地很不好的) SOAP RPC 机制。