Skip to content

Latest commit

 

History

History
393 lines (300 loc) · 9.61 KB

File metadata and controls

393 lines (300 loc) · 9.61 KB

#8.4 RPC In previous sections we talked about how to write network applications based on Sockets and HTTP, we learned that both of them are using "information exchange" model, which clients send requests and servers response. This kind of data exchange are based on certain format so both sides are able to understand. However, many independence applications do not use this model, but call services just like call normal functions.

RPC was intended to achieve the function call mode networking. Clients like calling native functions, and then packaged these parameters after passing through the network to the server, the server unpacked process execution, and executes the results back to the client.

In computer science, a remote procedure call (RPC) is an inter-process communication that allows a computer program to cause a subroutine or procedure to execute in another address space (commonly on another computer on a shared network) without the programmer explicitly coding the details for this remote interaction. That is, the programmer writes essentially the same code whether the subroutine is local to the executing program, or remote. When the software in question uses object-oriented principles, RPC is called remote invocation or remote method invocation.

##RPC working principle

Figure 8.8 RPC working principle.

Normally, a RPC call from the client to the server has following ten steps:

    1. Call the client handle, execute transfer arguments.
    1. Call local system kernel to send network messages.
    1. Send messages to remote hosts.
    1. The server receives handle and arguments.
    1. Execute remote processes.
    1. Return execute result to corresponding handle.
    1. The server handle calls remote system kernel.
    1. Messages sent back to local system kernel.
    1. The client handle receives messages from system kernel.
    1. The client gets results from corresponding handle.

##Go RPC Go has official support for RPC in standard library with three levels which are TCP, HTTP and JSON RPC. Note that Go RPC is not like other traditional RPC systems, it requires you to use Go applications on both sides of clients and servers because it encodes content through Gob.

Functions of Go RPC have to follow following rules for remote access, otherwise corresponding calls will be ignored.

  • Functions are exported(capitalize).
  • Functions have to have two arguments with exported types.
  • The first argument is for receiving from the client, and the second one has to be pointer type and is for replying to the client.
  • Functions have to have a return value of error type.

For example:

func (t *T) MethodName(argType T1, replyType *T2) error

Where T, T1 and T2 must be able to encoded by package encoding/gob.

Any kind of RPC have to through network to transfer data, Go RPC can either use HTTP or TCP, the benefits of using HTTP is that you can reuse some function in package net/http.

###HTTP RPC HTTP server side code:

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/rpc"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

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
}

func main() {

	arith := new(Arith)
	rpc.Register(arith)
	rpc.HandleHTTP()

	err := http.ListenAndServe(":1234", nil)
	if err != nil {
		fmt.Println(err.Error())
	}
}

We registered a RPC service of Arith, then registered this service on HTTP through rpc.HandleHTTP. After that, we are able to transfer data through HTTP.

Client side code:

package main

import (
	"fmt"
	"log"
	"net/rpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}


func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: ", os.Args[0], "server")
		os.Exit(1)
	}
	serverAddress := os.Args[1]

	client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 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\n", args.A, args.B, reply)

	var quot Quotient
	err = client.Call("Arith.Divide", args, &quot)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

We compile the client and the server side code separately, start server and start client, then you'll have something similar as follows after you input some data.

$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1

As you can see, we defined a struct for return type, we use it as type of function argument in server side, and use as type of the second and third arguments in the client client.Call. This call is very important, it has three arguments, where the first one the name of function that is going to be called, and the second is the argument you want to pass, the last one is the return value(pointer type). So far we see that it's easy to implement RPC in Go.

###TCP RPC Let's try the RPC that is based on TCP, here is the serer side code:

package main

import (
	"errors"
	"fmt"
	"net"
	"net/rpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

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
}

func main() {

	arith := new(Arith)
	rpc.Register(arith)

	tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
	checkError(err)

	listener, err := net.ListenTCP("tcp", tcpAddr)
	checkError(err)

	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		rpc.ServeConn(conn)
	}

}

func checkError(err error) {
	if err != nil {
		fmt.Println("Fatal error ", err.Error())
		os.Exit(1)
	}
}

The different between HTTP RPC and TCP RPC is that we have to control connections by ourselves if we use TCP RPC, then pass connections to RPC for processing.

As you may guess, this is a blocking pattern application, you are free to use goroutine to extend this application for more advanced experiment.

The client side code:

package main

import (
	"fmt"
	"log"
	"net/rpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: ", os.Args[0], "server:port")
		os.Exit(1)
	}
	service := os.Args[1]

	client, err := rpc.Dial("tcp", service)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 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\n", args.A, args.B, reply)

	var quot Quotient
	err = client.Call("Arith.Divide", args, &quot)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

The only difference in client side code is that HTTP client uses DialHTTP where TCP client uses Dial(TCP).

###JSON RPC JSON RPC encodes data to JSON instead of gob, let's see an example of Go JSON RPC server side code sample:

package main

import (
	"errors"
	"fmt"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

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
}

func main() {

	arith := new(Arith)
	rpc.Register(arith)

	tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
	checkError(err)

	listener, err := net.ListenTCP("tcp", tcpAddr)
	checkError(err)

	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		jsonrpc.ServeConn(conn)
	}

}

func checkError(err error) {
	if err != nil {
		fmt.Println("Fatal error ", err.Error())
		os.Exit(1)
	}
}

JSON RPC is based on TCP, it hasn't support HTTP yet.

The client side code:

package main

import (
	"fmt"
	"log"
	"net/rpc/jsonrpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

func main() {
	if len(os.Args) != 2 {
		fmt.Println("Usage: ", os.Args[0], "server:port")
		log.Fatal(1)
	}
	service := os.Args[1]

	client, err := jsonrpc.Dial("tcp", service)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 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\n", args.A, args.B, reply)

	var quot Quotient
	err = client.Call("Arith.Divide", args, &quot)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

##Summary Go has good support of HTTP, TPC, JSON RPC implementation, we can easily develop distributed web applications; however, it is regrettable that Go hasn't support for SOAP RPC which some third-party packages did it on open source.

##Links