RPC explained by writing simple RPC framework in 300 lines of pure Golang.

Overview

Simple GoRPC

Learning RPC basic building blocks by building a simple RPC framework in Golang from scratch.

RPC

In Simple Term Service A wants to call Service B functions. But those two services are not in the same memory space. So it cannot be called directly.

So, in order to make this call happen, we need to express the semantics of how to call and also how to pass the communication through the network.

Let's think what we do when we call function in the same memory space (local call)

type User struct {
	Name string
	Age int
}

var userDB = map[int]User{
	1: User{"Ankur", 85},
	9: User{"Anand", 25},
	8: User{"Ankur Anand", 27},
}


func QueryUser(id int) (User, error) {
	if u, ok := userDB[id]; ok {
		return u, nil
	}

	return User{}, fmt.Errorf("id %d not in user db", id)
}


func main() {
	u , err := QueryUser(8)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("name: %s, age: %d \n", u.Name, u.Age)
}

Now, how do we do the same function call over the network?

Client will call QueryUser(id int) function over the network and there will be one server which will Serve the Call to this function and return the Response User{"Name", id}, nil.

Network Transmission Data format.

Simple-gorpc will do TLV (fixed-length header + variable-length message body) encoding scheme to regulate the transmission of data, over the tcp. More on this later

Before we send our data over the network we need to define the structure how we are going to send the data over the network.

This helps us to define a common protocol that, the client and server both can understand. (protobuf IDL define what both server and client understand).

So data received by the server needs to have:

  • the name of the function to be called
  • list of parameters to be passed to that function

Also let's agree that the second return value is of type error, indicating the RPC call result.

// RPCdata transmission format
type RPCdata struct {
	Name string        // name of the function
	Args []interface{} // request's or response's body expect error.
	Err  string        // Error any executing remote server
}

So now that we have a format, we need to serialize this so that we can send it over the network. In our case we will use the go default binary serialization protocol for encoding and decoding.

// be sent over the network.
func Encode(data RPCdata) ([]byte, error) {
	var buf bytes.Buffer
	encoder := gob.NewEncoder(&buf)
	if err := encoder.Encode(data); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// Decode the binary data into the Go struct
func Decode(b []byte) (RPCdata, error) {
	buf := bytes.NewBuffer(b)
	decoder := gob.NewDecoder(buf)
	var data RPCdata
	if err := decoder.Decode(&data); err != nil {
		return Data{}, err
	}
	return data, nil
}

Network Transmission

The reason for choosing the TLV protocol is due to the fact that it's very simple to implement, and it also fullfills our need over identification of the length of data to read, as we need to identify the number of bytes to read for this request over the stream of incoming request. Send and Receive does the same

// Transport will use TLV protocol
type Transport struct {
	conn net.Conn // Conn is a generic stream-oriented network connection.
}

// NewTransport creates a Transport
func NewTransport(conn net.Conn) *Transport {
	return &Transport{conn}
}

// Send TLV data over the network
func (t *Transport) Send(data []byte) error {
	// we will need 4 more byte then the len of data
	// as TLV header is 4bytes and in this header
	// we will encode how much byte of data
	// we are sending for this request.
	buf := make([]byte, 4+len(data))
	binary.BigEndian.PutUint32(buf[:4], uint32(len(data)))
	copy(buf[4:], data)
	_, err := t.conn.Write(buf)
	if err != nil {
		return err
	}
	return nil
}

// Read TLV sent over the wire
func (t *Transport) Read() ([]byte, error) {
	header := make([]byte, 4)
	_, err := io.ReadFull(t.conn, header)
	if err != nil {
		return nil, err
	}
	dataLen := binary.BigEndian.Uint32(header)
	data := make([]byte, dataLen)
	_, err = io.ReadFull(t.conn, data)
	if err != nil {
		return nil, err
	}
	return data, nil
}

Now that we have the DataFormat and Transport protocol defined. We need and RPC Server and RPC CLient

RPC SERVER

RPC Server will receive the RPCData which will have an function Name. So we need to maintain and map that contains an function name to actual function mapping

// RPCServer ...
type RPCServer struct {
	addr string
	funcs map[string] reflect.Value
}

// Register the name of the function and its entries
func (s *RPCServer) Register(fnName string, fFunc interface{}) {
	if _,ok := s.funcs[fnName]; ok {
		return
	}

	s.funcs[fnName] = reflect.ValueOf(fFunc)
}

Now that we have the func registered, when we receive the request we will check if the name of func passed during the execution of the function is present or not. and then will execute it accordingly

// Execute the given function if present
func (s *RPCServer) Execute(req RPCdata) RPCdata {
	// get method by name
	f, ok := s.funcs[req.Name]
	if !ok {
		// since method is not present
		e := fmt.Sprintf("func %s not Registered", req.Name)
		log.Println(e)
		return RPCdata{Name: req.Name, Args: nil, Err: e}
	}

	log.Printf("func %s is called\n", req.Name)
	// unpackage request arguments
	inArgs := make([]reflect.Value, len(req.Args))
	for i := range req.Args {
		inArgs[i] = reflect.ValueOf(req.Args[i])
	}

	// invoke requested method
	out := f.Call(inArgs)
	// now since we have followed the function signature style where last argument will be an error
	// so we will pack the response arguments expect error.
	resArgs := make([]interface{}, len(out) - 1)
	for i := 0; i < len(out) - 1; i ++ {
		// Interface returns the constant value stored in v as an interface{}.
		resArgs[i] = out[i].Interface()
	}

	// pack error argument
	var er string
	if e, ok := out[len(out) - 1].Interface().(error); ok {
		// convert the error into error string value
		er = e.Error()
	}
	return RPCdata{Name: req.Name, Args: resArgs, Err: er}
}

RPC CLIENT

Since the concrete implementation of the function is on the server side, the client only has the prototype of the function, so we need complete prototype of the calling function, so that we can call it.

func (c *Client) callRPC(rpcName string, fPtr interface{}) {
	container := reflect.ValueOf(fPtr).Elem()
	f := func(req []reflect.Value) []reflect.Value {
		cReqTransport := NewTransport(c.conn)
		errorHandler := func(err error) []reflect.Value {
			outArgs := make([]reflect.Value, container.Type().NumOut())
			for i := 0; i < len(outArgs)-1; i++ {
				outArgs[i] = reflect.Zero(container.Type().Out(i))
			}
			outArgs[len(outArgs)-1] = reflect.ValueOf(&err).Elem()
			return outArgs
		}

		// Process input parameters
		inArgs := make([]interface{}, 0, len(req))
		for _, arg := range req {
			inArgs = append(inArgs, arg.Interface())
		}

		// ReqRPC
		reqRPC := RPCdata{Name: rpcName, Args: inArgs}
		b, err := Encode(reqRPC)
		if err != nil {
			panic(err)
		}
		err = cReqTransport.Send(b)
		if err != nil {
			return errorHandler(err)
		}
		// receive response from server
		rsp, err := cReqTransport.Read()
		if err != nil { // local network error or decode error
			return errorHandler(err)
		}
		rspDecode, _ := Decode(rsp)
		if rspDecode.Err != "" { // remote server error
			return errorHandler(errors.New(rspDecode.Err))
		}

		if len(rspDecode.Args) == 0 {
			rspDecode.Args = make([]interface{}, container.Type().NumOut())
		}
		// unpackage response arguments
		numOut := container.Type().NumOut()
		outArgs := make([]reflect.Value, numOut)
		for i := 0; i < numOut; i++ {
			if i != numOut-1 { // unpackage arguments (except error)
				if rspDecode.Args[i] == nil { // if argument is nil (gob will ignore "Zero" in transmission), set "Zero" value
					outArgs[i] = reflect.Zero(container.Type().Out(i))
				} else {
					outArgs[i] = reflect.ValueOf(rspDecode.Args[i])
				}
			} else { // unpackage error argument
				outArgs[i] = reflect.Zero(container.Type().Out(i))
			}
		}

		return outArgs
	}
	container.Set(reflect.MakeFunc(container.Type(), f))
}

Testing our framework

package main

import (
	"encoding/gob"
	"fmt"
	"net"
)

type User struct {
	Name string
	Age  int
}

var userDB = map[int]User{
	1: User{"Ankur", 85},
	9: User{"Anand", 25},
	8: User{"Ankur Anand", 27},
}

func QueryUser(id int) (User, error) {
	if u, ok := userDB[id]; ok {
		return u, nil
	}

	return User{}, fmt.Errorf("id %d not in user db", id)
}

func main() {
	// new Type needs to be registered
	gob.Register(User{})
	addr := "localhost:3212"
	srv := NewServer(addr)

	// start server
	srv.Register("QueryUser", QueryUser)
	go srv.Run()

	// wait for server to start.
	time.Sleep(1 * time.Second)

	// start client
	conn, err := net.Dial("tcp", addr)
	if err != nil {
		panic(err)
	}
	cli := NewClient(conn)

	var Query func(int) (User, error)
	cli.callRPC("QueryUser", &Query)

	u, err := Query(1)
	if err != nil {
		panic(err)
	}
	fmt.Println(u)

	u2, err := Query(8)
	if err != nil {
		panic(err)
	}
	fmt.Println(u2)
}

Output

2019/07/23 20:26:18 func QueryUser is called
{Ankur 85}
2019/07/23 20:26:18 func QueryUser is called
{Ankur Anand 27}

go run main.go

Owner
Ankur Anand
We have Traveled a long road since 80's
Ankur Anand
Kitex byte-dance internal Golang microservice RPC framework with high performance and strong scalability, customized extensions for byte internal.

Kitex 字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点,针对字节内部做了定制扩展。

CloudWeGo 4.7k Aug 1, 2022
Solution & Framework for JSON-RPC over HTTP

JROH Solution & Framework for JSON-RPC over HTTP Why not OpenAPI? OpenAPI addresses the definition of RESTful APIs, when it comes to JSON-RPCs, some i

Go Toolkit 11 Mar 13, 2022
Starter code for writing web services in Go

Ultimate Service Copyright 2018, 2019, 2020, 2021, Ardan Labs [email protected] Ultimate Service 2.0 Video If you are watching the Ultimate Service v

Ardan Labs 2.4k Aug 2, 2022
Golang client for Ethereum and Flashbots JSON-RPC API calls.

Flashbots RPC client Fork of ethrpc with additional Flashbots RPC methods: FlashbotsGetUserStats FlashbotsCallBundle FlashbotsSendBundle FlashbotsSimu

Chris Hager 79 Aug 3, 2022
A code generator that turns plain old Go services into RPC-enabled (micro)services with robust HTTP APIs.

Frodo is a code generator and runtime library that helps you write RPC-enabled (micro) services and APIs.

Monadic 19 Jul 22, 2022
Automatic Service Mesh and RPC generation for Go micro services, it's a humble alternative to gRPC with Istio.

Mesh RPC MeshRPC provides automatic Service Mesh and RPC generation for Go micro services, it's a humble alternative to gRPC with Istio. In a nutshell

AstraNet Toolkit 68 Apr 19, 2022
stack-rpc 快速开发包

Micro 快速开发工具包 项目进行中 本仓库旨在提供面向 stack-rpc 生产环境的快速开发包。 目录 快速开始示例 控制台示例 以最常见的登录流程为例,实现一个场景简单,但包含微服务各种治理能力的示例 Hipster Shop示例 参考GoogleCloudPlatform/microser

Stack Labs 383 Jul 26, 2022
This project implements p11-kit RPC server protocol, allowing Go programs to act as a PKCS #11 module without the need for cgo

PKCS #11 modules in Go without cgo This project implements p11-kit RPC server protocol, allowing Go programs to act as a PKCS #11 module without the n

Google 42 Jun 28, 2022
🦄🌈 YoyoGo is a simple, light and fast , dependency injection based micro-service framework written in Go.

???? YoyoGo is a simple, light and fast , dependency injection based micro-service framework written in Go. Support Nacos ,Consoul ,Etcd ,Eureka ,kubernetes.

YoyoFx 549 Aug 6, 2022
😈 Simple micro-front-end framework.

Development and Maintenance Status RancherOS 1.x is no longer being actively maintained. There are two significant reasons behind this product decisio

Matt D 0 Jan 5, 2022
Kratos is a microservice-oriented governance framework implements by golang

Kratos is a microservice-oriented governance framework implements by golang, which offers convenient capabilities to help you quickly build a bulletproof application from scratch.

Kratos 18.5k Jul 30, 2022
Modern microservice web framework of golang

gogo gogo is an open source, high performance RESTful api framework for the Golang programming language. It also support RPC api, which is similar to

null 40 May 23, 2022
Kratos is a microservice-oriented governance framework implements by golang,

Kratos is a microservice-oriented governance framework implements by golang, which offers convenient capabilities to help you quickly build a bulletproof application from scratch.

Kratos 18.5k Aug 4, 2022
Nano - Lightweight, facility, high performance golang based game server framework

Nano Nano is an easy to use, fast, lightweight game server networking library fo

Lonng 2.1k Aug 4, 2022
Go Micro is a framework for distributed systems development

Go Micro Go Micro is a framework for distributed systems development. Overview Go Micro provides the core requirements for distributed systems develop

Asim Aslam 18.8k Aug 6, 2022
Micro-service framework in Go

Kite Micro-Service Framework Kite is a framework for developing micro-services in Go. Kite is both the name of the framework and the micro-service tha

Koding, Inc. 3.2k Aug 6, 2022
Sample full stack micro services application built using the go-Micro framework.

goTemp goTemp is a full stack Golang microservices sample application built using go-micro. The application is built as a series of services that prov

null 61 Jul 11, 2022
NewSQL distributed storage database based on micro service framework

QLite 是基于微服务的 NewSQL 型数据库系统,与传统的一体化数据库不同,该系统将本该内置的多种数据结构(STL)拆分成多个服务模块,每个模块都是独立的一个节点,每个节点都与其主网关进行连接,从而形成分布式存储结构。

null 36 Jun 19, 2022
a microservice framework for rapid development of micro services in Go with rich eco-system

中文版README Go-Chassis is a microservice framework for rapid development of microservices in Go. it focus on helping developer to deliver cloud native a

null 2.5k Aug 1, 2022