Package RPC

Remote precedure call

The package net/rpc allows Go programs to communicate each others. The package provides access to remote exported methods of an object, implementing a client-server paradigm. Methods have made visible to remote clients as a service with the name of the type of the object. Objects must be exported and registered on the server in order to be accessible. A server may register multiple objects of different types but you can not register objects of the same type.

Only the exported methods that meet certain conditions are made accessible:

  • the method has two arguments, both exported (or builtin) types
  • the method's second argument is a pointer, but usually also the first is a pointer
  • the method has return type error
The methods that meet these criteria are remotely accessible, other methods of the object are ignored and not accessible.

Communication occurs serializing the parameters through the gob format. The transport is done using a raw network connection (TCP protocol) or HTTP (HTTP over TCP protocol).
Here a non-trivial example.

The data model

These are objects used as an interchange between client and server.
package model
type Order struct {
    IdCustomer int
    Items []*Product
}
type Product struct {
    Id int
    Name string
    Quantity int
    Cost float64
}
type OrderReference struct {
    Id int
    IdCustomer int
    Total float64
}

The server

The server exposes the method RegisterOrder to store orders.
package main
import(
    "fmt"
    "log"
    "errors"
    "net"
    "net/rpc"
    "net/http"
    "github.com/maxzerbini/packagemain/rpc/model"
)
type OrderServer struct {
    endpoint string
    referenceId int
}
// Register a new order.
func (srv *OrderServer) RegisterOrder(order *model.Order, reply *model.OrderReference) (err error) {
    defer func() {
        // Executes normally even if there is a panic
        if e:= recover(); e != nil {
            log.Println("Run time panic: %v", e)
            err = errors.New("Runtime error.")
        }
    }()
    // simulate order management creating a order reference id
    // (in a true case the order will be saved on a database)
    log.Printf("Order received %v", order)
    reply.Id = srv.referenceId
    for _,item := range order.Items {
        reply.Total += item.Cost * float64(item.Quantity)
    }
    reply.IdCustomer = order.IdCustomer
    return err
}
// Start listening commands.
func (srv *OrderServer)Do(){
    rpc.Register(srv)
    rpc.HandleHTTP()
    listener, e := net.Listen("tcp", srv.endpoint)
    if e != nil {
        log.Fatal("Starting RPC-server -listen error:", e)
    }
    log.Println("Server started ...")
    http.Serve(listener, nil)
}
func NewOrderServer(endpoint string) *OrderServer{
    s := &OrderServer{endpoint:endpoint, referenceId:1}
    return s
}
func main(){
    endpoint := "localhost:9000"
    s := NewOrderServer(endpoint)
    go s.Do()
    _,_ = fmt.Scanln()  
}

The client

The client sends commands to the server and get the ID generated by the server.
package client
import(
    "log"
    "errors"
    "net/rpc"
    "github.com/maxzerbini/packagemain/rpc/model"
)
type OrderClient struct {
    endpoint string
    client *rpc.Client
}
func (nc *OrderClient) ConnectClient(){
    defer func() {
        // Println executes normally even if there is a panic
        if err := recover(); err != nil {
            log.Println("run time panic: %v", err)
        }
    }()
    client, err := rpc.DialHTTP("tcp", nc.endpoint)
    if err != nil {
        log.Printf("Error dialing server: %v \r\n", err)
    } else {
        log.Printf("Dialing server on %s done.\r\n",nc.endpoint)
        nc.client = client
    }
}
// Send an order
func (nc *OrderClient) SendOrder(order *model.Order) ( reference *model.OrderReference, err error) {
    defer func() {
        // executes normally even if there is a panic
        if e := recover(); e != nil {
            log.Printf("Error %v\r\n",e)
            reference = nil
            err = errors.New("Client runtime error.")
        }
    }()
    reference = new (model.OrderReference)
    err = nc.client.Call("OrderServer.RegisterOrder", order, ref)
    if err != nil {
        reference = nil
        log.Println("OrderServer.RegisterOrder error: ", err)
    }
    return reference, err
}
func NewOrderClient(endpoint string) *OrderClient{
    c := &OrderClient{endpoint:endpoint}
    c.ConnectClient()
    return c
}

Testing the client

This is an example of a call between clients and servers.
package main
import(
    "log"
    "github.com/maxzerbini/packagemain/rpc/model"
    "github.com/maxzerbini/packagemain/rpc/client"
)
func main(){
    endpoint := "localhost:9000"
    c := client.NewOrderClient(endpoint)
    o := &model.Order{IdCustomer:101,Items:make([]*model.Product,10)}
    for i := 0; i<10; i++ {
        o.Items[i] =&model.Product{Id:i,Name:"product",Quantity:1,Cost:5.3}
    }  
    if ref, err := c.SendOrder(o); err == nil {
        log.Printf("Order reference: %v\r\n", ref)
    } else {
        log.Printf("Error: %v", err)
    }
}

The full example code can be downloaded here:
https://github.com/maxzerbini/packagemain/tree/master/rpc


Commenti

Post popolari in questo blog

OAuth 2.0 Server & Authorization Middleware for Gin-Gonic

From the database schema to RESTful API with DinGo

OVO Key/Value Storage