This library implements a simple, custom RPC protocol via os/exex and stdin and stdout. Both server and client comes in a raw ([]byte) and strongly typed variant (using Go generics).
A strongly typed client may look like this:
// Define the request, message and receipt types for the RPC call.client, err:=execrpc.StartClient(
client, err:= execrpc.StartClient(
execrpc.ClientOptions[model.ExampleConfig, model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]{
ClientRawOptions: execrpc.ClientRawOptions{
Version: 1,
Cmd: "go",
Dir: "./examples/servers/typed",
Args: []string{"run", "."},
Env: env,
Timeout: 30*time.Second,
},
Config: model.ExampleConfig{},
Codec: codec,
},
)
iferr!=nil {
log.Fatal(err)
}
// Consume standalone messages (e.g. log messages) in its own goroutine.gofunc() {
formsg:=rangeclient.MessagesRaw() {
fmt.Println("got message", string(msg.Body))
}
}()
// Execute the request.result:= client.Execute(model.ExampleRequest{Text: "world"})
// Check for errors.iferr:=result.Err(); err!=nil {
log.Fatal(err)
}
// Consume the messages.form:=rangeresult.Messages() {
fmt.Println(m)
}
// Wait for the receipt.receipt:=<-result.Receipt()
// Check again for errors.iferr:=result.Err(); err!=nil {
log.Fatal(err)
}
fmt.Println(receipt.Text)
// Close the client.iferr:=client.Close(); err!=nil {
log.Fatal(err)
}To get the best performance you should keep the client open as long as its needed – and store it as a shared object; it's safe and encouraged to call Execute from multiple goroutines.
And the server side of the above:
funcmain() {
log.SetFlags(0)
log.SetPrefix("readme-example: ")
varclientConfig model.ExampleConfigserver, err:=execrpc.NewServer(
execrpc.ServerOptions[model.ExampleConfig, model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]{
// Optional function to provide a hasher for the ETag.GetHasher: func() hash.Hash {
returnfnv.New64a()
},
// Allows you to delay message delivery, and drop// them after reading the receipt (e.g. the ETag matches the ETag seen by client).DelayDelivery: false,
// Optional function to initialize the server// with the client configuration.// This will be called once on server start.Init: func(cfg model.ExampleConfig) error {
clientConfig=cfgreturnclientConfig.Init()
},
// Handle the incoming call.Handle: func(c*execrpc.Call[model.ExampleRequest, model.ExampleMessage, model.ExampleReceipt]) {
// Raw messages are passed directly to the client,// typically used for log messages.c.SendRaw(
execrpc.Message{
Header: execrpc.Header{
Version: 32,
Status: 150,
},
Body: []byte("log message"),
},
)
// Enqueue one or more messages.c.Enqueue(
model.ExampleMessage{
Hello: "Hello 1!",
},
model.ExampleMessage{
Hello: "Hello 2!",
},
)
c.Enqueue(
model.ExampleMessage{
Hello: "Hello 3!",
},
)
// Wait for the framework generated receipt.receipt:=<-c.Receipt()
// ETag provided by the framework.// A hash of all message bodies.// fmt.Println("Receipt:", receipt.ETag)// Modify if needed.receipt.Size=uint32(123)
receipt.Text="echoed: "+c.Request.Text// Close the message stream and send the receipt.// Pass true to drop any queued messages,// this is only relevant if DelayDelivery is enabled.c.Close(false, receipt)
},
},
)
iferr!=nil {
handleErr(err)
}
iferr:=server.Start(); err!=nil {
handleErr(err)
}
}
funchandleErr(errerror) {
log.Fatalf("error: failed to start typed echo server: %s", err)
}The server can generate an ETag for the messages. This is a hash of all message bodies.
To enable this:
- Provide a
GetHasherfunction to the server options. - Have the
Receiptimplement the TagProvider interface.
Note that there are three different optional E-interfaces for the Receipt:
- TagProvider for the ETag.
- SizeProvider for the size.
- LastModifiedProvider for the last modified timestamp.
A convenient struct that can be embedded in your Receipt that implements all of these is the Identity.
The status codes in the header between 1 and 99 are reserved for the system. This will typically be used to catch decoding/encoding errors on the server.