官术网_书友最值得收藏!

Simple RPC example

In this simple example, we will see how we can use the standard RPC package to create a client and server that use a shared interface to communicate over RPC. We will follow the typical Hello World example that we ran through when learning the net/http package and see just how easy it is to build an RPC-based API in go:

rpc/server/server.go:

34 type HelloWorldHandler struct{} 
35
36 func (h *HelloWorldHandler) HelloWorld(args *contract.HelloWorldRequest, reply *contract.HelloWorldResponse) error {
37 reply.Message = "Hello " + args.Name
38 return nil
39 }

Like our example on creating REST APIs using the standard library for RPC, we will also define a handler. The difference between this handler and http.Handler is that it does not need to conform to an interface; as long as we have a struct field with methods on it we can register this with the RPC server:

func Register(rcvr interface{}) error 

The Register function, which is in the rpc package, publishes the methods that are part of the given interface to the default server and allows them to be called by clients connecting to the service. The name of the method uses the name of the concrete type, so in our instance if my client wanted to call the HelloWorld method, we would access it using HelloWorldHandler.HelloWorld. If we do not wish to use the concrete types name, we can register it with a different name using the RegisterName function, which uses the provided name instead:

func RegisterName(name string, rcvr interface{}) error 

This would enable me to keep the name of the struct field to whatever is meaningful to my code; however, for my client contract I might decide to use something different such as Greet:

19 func StartServer() { 
20 helloWorld := &HelloWorldHandler{}
21 rpc.Register(helloWorld)
22
23 l, err := net.Listen("("tcp", fmt.Sprintf(":%(":%v", port))
24 if err != nil {
25 log.Fatal(fmt.Sprintf("("Unable to listen on given port: %s", err))
26 }
27
28 for {
29 conn, _ := l.Accept()
30 go rpc.ServeConn(conn)
31 }
32 }

In the StartServer function, we first create a new instance of our handler and then we register this with the default RPC server.

Unlike the convenience of net/http where we can just create a server with ListenAndServe, when we are using RPC we need to do a little more manual work. In line 23, we are creating a socket using the given protocol and binding it to the IP address and port. This gives us the capability to specifically select the protocol we would like to use for the server, tcp, tcp4, tcp6, unix, or unixpacket:

func Listen(net, laddr string) (Listener, error) 

The Listen() function returns an instance that implements the Listener interface:

type Listener interface { 
// Accept waits for and returns the next connection to the listener.
Accept() (Conn, error)

// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
Close() error

// Addr returns the listener's network address.
Addr() Addr
}

To receive connections, we must call the Accept method on the listener. If you look at line 29, you will see that we have an endless for loop, this is because unlike ListenAndServe which blocks for all connections, with an RPC server we handle each connection individually and as soon as we deal with the first connection we need to continue to again call Accept to handle subsequent connections or the application would exit. Accept is a blocking method so if there are no clients currently attempting to connect to the service then Accept will block until one does. Once we receive a connection then we need to call the Accept method again to process the next connection. If you look at line 30 in our example code, you will see we are calling the ServeConn method:

func ServeConn(conn io.ReadWriteCloser) 

The ServeConn method runs the DefaultServer method on the given connection, and will block until the client completes. In our example, we are using the go statement before running the server so that we can immediately process the next waiting connection without blocking for the first client to close its connection.

In terms of communication protocol, ServeConn uses the gob wire format https://golang.org/pkg/encoding/gob/, we will see when we look at JSON-RPC how we can use a different encoding.

The gob format was specifically designed to facilitate Go to Go-based communication and was designed around the idea of something easier to use and possibly more efficient than the likes of protocol buffers, this comes at a cost of cross language communication.

With gobs, the source and destination values and types do not need to correspond exactly, when you send struct, if a field is in the source but not in the receiving struct, then the decoder will ignore this field and the processing will continue without error. If a field is present in the destination that is not in the source, then again the decoder will ignore this field and will successfully process the rest of the message. Whilst this seems like a minor benefit, it is a huge advancement over the RPC messages of old such as JMI where the exact same interface must be present on both the client and server. This level of inflexibility with JMI introduced tight coupling between the two code bases and caused no end of complexity when it was required to deploy an update to our application.

To make a request to our client we can no longer simply use curl as we are no longer are using the HTTP protocol and the message format is no longer JSON. If we look at the example in rpc/client/client.go we can see how to implement a connecting client:

13 func CreateClient() *rpc.Client {
14 client, err := rpc.Dial("tcp", fmt.Sprintf("localhost:%v", port))
15 if err != nil {
16 log.Fatal("dialing:", err)
17 }
18
19 return client
20 }

 

The previous block shows how we need to setup rpc.Client, the first thing we need to do on line 14 is to create the client itself using the Dial() function in the rpc package:

func Dial(network, address string) (*Client, error)

We then use this returned connection to make a request to the server:

22 func PerformRequest(client *rpc.Client) 
contract.HelloWorldResponse {
23 args := &contract.HelloWorldRequest{Name: "World"}
24 var reply contract.HelloWorldResponse
25
26 err := client.Call("HelloWorldHandler.HelloWorld", args, &reply)
27 if err != nil {
28 log.Fatal("error:", err)
29 }
30
31 return reply
32 }

 

In line 26, we are using the Call() method on the client to invoke the named function on the server:

func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error

Call is a blocking function which waits until the server sends a reply writing the response assuming there is no error to the reference of our HelloWorldResponse passed to the method and if an error occurs when processing the request this is returned and can be handled accordingly.

主站蜘蛛池模板: 张家川| 青铜峡市| 习水县| 哈尔滨市| 长沙县| 东丽区| 绥棱县| 石台县| 通城县| 马山县| 古蔺县| 耒阳市| 舒城县| 柳河县| 西吉县| 株洲县| 同江市| 香格里拉县| 江华| 湘潭县| 阳曲县| 合水县| 鄄城县| 新疆| 苗栗县| 吴旗县| 当阳市| 岢岚县| 文昌市| 綦江县| 如东县| 合阳县| 洛浦县| 元氏县| 唐河县| 长葛市| 巴东县| 天峨县| 嘉义市| 油尖旺区| 河曲县|