- Building Microservices with Go
- Nic Jackson
- 1152字
- 2021-07-15 17:28:05
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.
- Implementing VMware Horizon 7(Second Edition)
- 微信公眾平臺與小程序開發:從零搭建整套系統
- 基于免疫進化的算法及應用研究
- Building Mobile Applications Using Kendo UI Mobile and ASP.NET Web API
- Mastering Kali Linux for Web Penetration Testing
- PLC編程及應用實戰
- Spring快速入門
- Getting Started with React Native
- Xcode 6 Essentials
- Android應用開發實戰
- 實驗編程:PsychoPy從入門到精通
- Android初級應用開發
- 少兒編程輕松學(全2冊)
- Python無監督學習
- 前端架構設計