This is a creation in Article, where the information may have evolved or changed.
Go official provides an RPC library: net/rpc
. Package RPC provides the ability to access an object through the network. The server needs to register the object, exposing the service through the object's type name. The output method of this object can be called remotely after registration, which encapsulates the details of the underlying transport, including serialization. The server can register multiple objects of different types, but there is an error when registering multiple objects of the same type.
I recently wrote an ebook: Go RPC Development Guide, Introduction to the relevant technologies for Go RPC development, which is a chapter that specifically describes net/rpc
the use of the official library.
At the same time, if the object's methods are to be accessed remotely, they must satisfy certain conditions, otherwise the method of the object is ignored.
These conditions are:
- The type of the method is output (the way ' s type is exported)
- The method itself can also be output (the IS exported)
- The method must be two parameters, must be an output type, or be of the built-in type (the way has two arguments, both exported or builtin types)
- The second parameter of the method is the pointer type (the method ' s second argument is a pointer)
- Method return type is error (the way has return type error)
So the format of an output method is as follows:
1 |
Func (t *t) MethodName (Argtype T1, Replytype *t2) error |
Here T
, and T1
T2
can be encoding/gob
serialized, and even with other serialization frameworks, this demand may be weakened in the future.
The first parameter of this method represents the parameters provided by the caller (client),
The second parameter represents the result of the calculation to be returned to the caller.
The return value of the method is returned to the caller as a string if it is not empty.
If error is returned, the reply parameter is not returned to the caller.
The server processes the request by calling on ServeConn
a connection, more typically, it can create a network listener and then accept the request.
For HTTP listener, you can call HandleHTTP
and http.Serve
. The details are described below.
The client can invoke Dial
and DialHTTP
establish a connection. The client has two methods to invoke the service: Call
and Go
, the service can be invoked synchronously or asynchronously.
Of course, when calling, you need to pass the service name, method name, and parameters to the server. An asynchronous method call is Go
returned by a Done
channel notification call result.
codec
This library defaults to using packages as the serialization framework unless the settings are displayed encoding/gob
.
Simple example
The preferred introduction is a simple example.
This example provides two ways to multiply and divide two numbers.
The first step you need to define is the data structure for incoming parameters and return parameters:
1 2 3 4 5 6 7 8 9 |
Package Server Type Args struct { A, B int } Type quotient struct { Quo, Rem int } |
The second step is to define a service object that can be very simple, such as the type is int
or is interface{}
, and what is important is the method it outputs.
Here we define an arithmetic type Arith
, in fact it is an int type, but the value of this int is not used in the implementation of the following method, so it basically plays an auxiliary role.
Step three implements this type of two methods, multiplication and division:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 } |
So far, our preparations have been completed and sip tea continues under the steps below.
The fourth step implements the RPC server:
1 2 3 4 5 6 7 8 9 10 11 |
Arith: = new (Arith) Rpc. Register (Arith) Rpc. Handlehttp () L, E: = Net. Listen ("TCP", ": 1234") If E! = Nil { Log. Fatal ("Listen error:", e) } Go http. Serve (l, Nil) |
Here we generate a Arith object and use it rpc.Register
to register the service and then expose it via HTTP.
The client can see Arith
the service as well as its two methods Arith.Multiply
and Arith.Divide
.
The fifth step is to create a client and establish a client-server connection:
1 2 3 4 |
Client, err: = RPC. Dialhttp ("TCP", ServerAddress + ": 1234") If err! = Nil { Log. Fatal ("Dialing:", err) } |
The client can then make a remote call. For example, the way of synchronization:
1 2 3 4 5 6 7 8 9 |
Args: = &server. args{7,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", args. A, args. B, Reply) |
Or an asynchronous way:
1 2 3 4 |
Quotient: = new (quotient) Divcall: = client. Go ("Arith.divide", args, quotient, nil) Replycall: = <-divcall.done//would be is equal to Divcall Check errors, print, etc. |
Server Code Analysis
First, ' Net/rpc ' defines a default server, so many methods of server can be called directly, which is more convenient for a simple server implementation, but if you need to configure a different server,
such as different listening addresses or ports, you need to generate the server yourself:
1 |
var defaultserver = NewServer () |
Server has a variety of ways to listen to sockets:
1 2 3 4 5 6 |
Func (server *server) Accept (Lis net. Listener) Func (server *server) handlehttp (Rpcpath, Debugpath string) Func (server *server) Servecodec (codec Servercodec) Func (server *server) serveconn (conn io. Readwritecloser) Func (server *server) servehttp (w http. Responsewriter, req *http. Request) Func (server *server) serverequest (codec Servercodec) error |
Where the ServeHTTP
business logic for processing HTTP requests is implemented, it first processes the HTTP CONNECT
request, hijacker the connection conn after receiving it, and then calls the ServeConn
request to process the client on the connection.
It is actually implemented http.Handler
interface, we generally do not call this method directly.
' Server.handlehttp ' Sets the context path of RPC, ' rpc.handlehttp ' uses the default context path ' Defaultrpcpath ', DefaultDebugPath
.
This way, when you start an HTTP server, ' HTTP. Listenandserve ', the context set above will be used as the RPC transport, and this context request will be taught ServeHTTP
to handle.
The above is the RPC over HTTP implementation, you can see net/rpc
just using HTTP connect to establish a connection, which is not the same as the normal RESTful API.
The ' Accept ' is used to process a listener, listening to the client connection, and once the listener receives a connection, it is handed over ServeConn
to another goroutine:
1 2 3 4 5 6 7 8 9 10 11 12 |
Func (server *server) Accept (Lis net. Listener) { for { Conn, err: = Lis. Accept () If err! = Nil { Log. Print ("RPC. Serve:accept: ", err. Error ()) Return } Go server. SERVECONN (conn) } } |
It can be seen that one of the most important methods is ServeConn
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Func (server *server) serveconn (conn io. Readwritecloser) { BUF: = Bufio. Newwriter (conn) SRV: = &gobservercodec{ Rwc:conn, Dec:gob. Newdecoder (conn), Enc:gob. Newencoder (BUF), Encbuf:buf, } Server. Servecodec (SRV) } |
Connection is actually given ServerCodec
to a go processing, where the default is gobServerCodec
to deal with, this is a default codec is not output, you can use other codecs, we introduce below,
Here we can see ServeCodec
how it's implemented:
1 2 3 4 5 6 7 8 9 11 + + /+ //+ / + + + + + - - / - |
Func (server *server) Servecodec (codec Servercodec) { Sending: = new (sync. Mutex) for { Service, Mtype, req, argv, Replyv, keepreading, err: = Server.readrequest (codec) If err! = Nil { If Debuglog && err! = Io. EOF { Log. Println ("RPC:", err) } If!keepreading { Break } Send a response if we actually managed to read a header. If req! = Nil { Server.sendresponse (sending, req, Invalidrequest, codec, err. Error ()) Server.freerequest (req) } Continue } Go Service.call (server, sending, Mtype, req, argv, REPLYV, codec) } Codec. Close () } |
It actually reads the request from the connection and then invokes the go service.call
service call in a different goroutine.
We can learn from the following:
Object Reuse. Both the request and the response are reusable and compete through lock processing. This is effective in the case of large concurrency. Interested readers can refer to the implementation of Fasthttp.
Use a lot of goroutine. Unlike threads in Java, you can create very many goroutine, and the concurrency process is very good. If you use a certain number of goutine as a worker pool to handle this case, there may be some performance improvements, but more complexity. The use of goroutine has achieved very good performance.
The business process is asynchronous, and the execution of the service does not block the reading of other messages.
Note that a codec instance must be associated with a connnection because it needs to read the request and send response from the connection.
The message (Request and response) of the RPC official library of Go is simply the message header + content body (body).
The requested message header is defined as follows, including the name and serial number of the service:
1 2 3 4 5 6 7 |
Type Request struct { Servicemethod string//Format: "Service.method" Seq UInt64//sequence number chosen by client Contains filtered or unexported fields } |
The message body is the passed-in parameter.
The message headers that are returned are defined as follows:
1 2 3 4 5 6 7 |
Type Response struct { Servicemethod string//echoes that of the Request Seq UInt64//echoes that of the request Error string//error, if any. Contains filtered or unexported fields } |
The message body is the serialized value of the reply type.
The server also provides two methods for registering services:
1 2 |
Func (server *server) Register (RCVR interface{}) error Func (server *server) registername (name string, RCVR interface{}) error |
The second method has an alias for the service, otherwise the service name is named for its type, and the two underlying calls register
are registered for the service.
1 |
Func (server *server) register (RCVR interface{}, name string, usename bool) error |
Subject to the features of the go language, it is not possible to create an object based on reflection dynamically when receiving a request from the client, which is java.
So in the go language, we need to create a service map in advance, which is done at compile time:
1 |
Server.servicemap = Make (Map[string]*service) |
At the same time, each service also has a method Map:map[string]*methodtype, established through Suitablemethods:
1 |
Func suitablemethods (Typ reflect. Type, reporterr bool) Map[string]*methodtype |
So RPC in the read request header, by looking at the two map, you can get the service to invoke and its corresponding method.
Method is called:
1 2 3 4 5 6 7 8 9 One , , , , , , , , , |
Func (S *service) call (server *server, sending *sync. mutexes, Mtype *methodtype, req *request, argv, replyv reflect. Value, codec Servercodec) { Mtype. Lock () mtype.numcalls++ Mtype. Unlock () Function: = Mtype.method.Func //Invoke the method, providing a new value for the reply. Returnvalues: = function. Call ([]reflect. VALUE{S.RCVR, argv, replyv}) //The return value for the method was an error. Errinter: = Returnvalues[0]. Interface () ErrMsg: = "" if errinter! = Nil { ErrMsg = Errinter. ( Error). Error () } Server.sendresponse (sending, req, Replyv. Interface (), codec, errmsg) Server.freerequest (req) } |
Client Code Analysis
There are several ways that a client can establish a connection to a server:
1 2 3 4 5 |
Func Dial (Network, address string) (*client, error) Func dialhttp (Network, address string) (*client, error) Func Dialhttppath (Network, address, path string) (*client, error) Func newclient (conn io. Readwritecloser) *client Func Newclientwithcodec (codec Clientcodec) *client |
DialHTTP
And DialHTTPPath
is to establish the connection with the server through the HTTP way, the difference between them is whether to set the context path:
1 2 3 4 5 6 7 8 9 11 + + /+ //+ / + + + + + - - |
Func Dialhttppath (Network, address, path string) (*client, error) { var err error Conn, Err: = Net. Dial (Network, address) If err! = Nil { return nil, err } Io. WriteString (conn, "CONNECT" +path+ "http/1.0\n\n") Require Successful HTTP response Before switching to RPC protocol. RESP, err: = http. Readresponse (Bufio. Newreader (conn), &http. Request{method: "CONNECT"}) If Err = = Nil && resp. Status = = Connected { Return Newclient (conn), nil } If Err = = Nil { Err = errors. New ("Unexpected HTTP response:" + resp. Status) } Conn. Close () return nil, &net. operror{ Op: "Dial-http", Net:network + "" + Address, Addr:nil, Err:err, } } |
The CONNECT
request is sent first, if the connection succeeds, by NewClient(conn)
creating the client.
Instead, Dial
connect to the server directly via TCP:
1 2 3 4 5 6 7 8 9 10 |
Func Dial (Network, address string) (*client, error) { Conn, Err: = Net. Dial (Network, address) If err! = Nil { return nil, err } Return Newclient (conn), nil } |
Select the appropriate connection method depending on whether the service is over HTTP or over TCP.
NewClient
Create a default codec client for the GLOB serialization library:
1 2 3 4 5 |
Func newclient (conn io. Readwritecloser) *client { Encbuf: = Bufio. Newwriter (conn) Client: = &gobclientcodec{conn, gob. Newdecoder (conn), Gob. Newencoder (ENCBUF), encbuf} return Newclientwithcodec (client) } |
If you want to use a different serialized library, you can call the NewClientWithCodec
method <:></:>
1 2 3 4 5 6 7 8 9 |
Func Newclientwithcodec (codec Clientcodec) *client { Client: = &client{ Codec:codec, Pending:make (Map[uint64]*call), } Go Client.input () Return client } |
It is important that the input
method, which has a dead loop, continuously reads response from the connection and then calls the map to read the waiting Call.done channel notification to complete.
The structure of the message is consistent with the server, all in a header+body way.
The client calls have two methods: Go
and Call
. Go
method is asynchronous, it returns a call pointer object, it's done is a channel, if the service returns,
Done can get the returned object (actually the call object, which contains the reply and error information). Go
is a synchronous way of calling, which is actually called Call
implementation,
We can see how it's implemented, and you can see how asynchronous synchronization Works:
1 2 3 4 |
Func (client *client) call (Servicemethod string, args interface{}, reply interface{}) error { Call: = <-client. Go (Servicemethod, args, reply, make (Chan *call, 1)). Done Return call. Error } |
Reading an object from a channel is blocked until an object can be read, which is simple and convenient.
In fact, from the server-side code and the implementation of the client's code we can also learn the lock of a practical way, that is, release the lock as soon as possible, not until the function is executed until the defer mu.Unlock
last release, so the lock takes too long.
codec/Serialization Framework
We introduced the RPC framework by default using the GOB serialization library, in many cases we pursue better efficiency, or pursue a more general serialization format, we may use other serialization methods, such as PROTOBUF, JSON, XML and so on.
There is a requirement for the GOB serialization library that you need to register the specific implementation type for the value of the interface type:
1 2 |
Func Register (Value interface{}) Func registername (name string, value interface{}) |
The first person to use RPC is prone to make this mistake, causing serialization to be unsuccessful.
The Go official library implements JSON-RPC 1.0. JSON-RPC is an RPC specification for message transmission in JSON format, so you can make cross-language calls.
The Go net/rpc/jsonrpc
library can convert JSON-RPC requests into its own internal format, such as the processing of request headers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Func (c *servercodec) Readrequestheader (R *rpc. Request) Error { C.req.reset () If err: = C.dec.decode (&c.req); Err! = Nil { return err } R.servicemethod = C.req.method C.mutex.lock () c.seq++ C.PENDING[C.SEQ] = c.req.id C.req.id = Nil R.seq = C.seq C.mutex.unlock () return Nil } |
JSON-RPC 2.0 is supported by the official library, but there are third-party developers who provide implementations such as:
- Https://github.com/powerman/rpc-codec
- Https://github.com/dwlnetnl/generpc
Some other codec such as BSONRPC, Messagepack, Protobuf and so on.
If you use other specific serialization frameworks, you can refer to these implementations to write your own RPC codec.
For a comparison of the performance of the Go serialization library you can refer to Gosercomp.
Other
There is a proposal deprecate NET/RPC:
The package had outstanding bugs that is hard-to-fix, and cannot support for TLS without major work. So although it have a nice API and allows one to use native Go types without an IDL, it should probably is retired.
The proposal is to freeze the package, retire the many bugs filed against it, and add documentation indicating that it is Frozen and that suggests alternatives such as GRPC.
But I think net/rpc design is very good, performance is excellent, if not continue to develop it is too pity. Some of the bugs and TLS mentioned in the proposal are not irreparable, maybe the Go team lacks resources, or the developer is not interested in it. I believe there is a great objection to this proposal.
At present, the performance of ' grpc ' is far inferior net/rpc
, not only the throughput rate, but also the CPU share.
Read more about Go RPC development: Go RPC Development Guide.
Reference documents
- https://golang.org/pkg/net/rpc/
- https://golang.org/pkg/encoding/gob/
- https:// golang.org/pkg/net/rpc/jsonrpc/
- https://github.com/golang/go/issues/16844