[TOC]

How does Golang use native RPC and microservices

Micro service

1. What are microservices

  • A single application is developed using a set of small services, each running in a separate process, interconnected by lightweight communication mechanisms, and deployed in an automated manner

Microservices are about design, not quantity

  • Dedicated functionality
  • That’s a lot of code
  • Architecture gets complicated

2. What are the features

  • Specific responsibilities, such as focusing on rights management
  • Lightweight communication. Communication is platform – and language-independent. HTTP, for example, is lightweight
  • Isolation, data isolation
  • They have their own data
  • Diversity of technology

3. Advantages of microservices architecture

  • independence
  • Easy for users to understand
  • Flexible technology stack
  • Efficient team

4. Deficiencies of microservices architecture

  • Extra work, service unbundling
  • Ensure data consistency
  • Increased communication costs

Microservice ecology

1. The hardware layer

  • Use Docker + K8S to solve

2. Communication layer

  • Network transmission, using RPC (Remote Procedure Call)

    • HTTP transmission, GET, POST, PUT, DELETE
    • Tcp-based, more low-level, RPC based on TCP, Dubbo (changed to support various languages at the end of 18), Grpc, Thrift
  • You need to know who to call, register and discover with services

    • Distributed data synchronization required: ETCD, Consul, ZK
  • Data transfer there can be all kinds of languages, all kinds of technologies, all kinds of transfers

Data transfer protocol selection suggestions

1. For inter-company system calls, xmL-based SOAP is a solution worth considering if the performance requirements are above 100ms.

2. Using JSON or XML can greatly improve the debugging efficiency and reduce the cost of system development in the scenario where the debugging environment is harsh.

3. Protobuf, Thrift, and Avro compete in scenarios where performance and simplicity are extremely important.

4. Protobuf and Avro are the first choice for persistence scenarios of T-level data. If persistent data is stored in a Hadoop subproject, Avro is a better choice.

Thrift is a good choice if you need to provide a complete RPC solution.

Protobuf is preferred if serialization requires support for different transport layer protocols, or for high performance scenarios that require cross-firewall access.

RPC mechanism and implementation process

1. The RPC mechanism

Lightweight remote procedure calls between services, typically using HTTP, RPC

  • HTTP invokes application layer protocols with relatively fixed structure
  • RPC’s network protocol is relatively flexible and customizable

RPC remote procedure call generally adopts C/S mode and client server mode. The client process calls the program that calls the server process. The server process returns the execution result to the client, and the client wakes up from the blocked state, receives data, and extracts data.

In the above process, the client calls the function of the server to perform the task, and it does not know whether the operation is carried out on the local operating system or through a remote procedure call.

The basic communication of RPC is as follows:

The following four issues need to be considered in RPC remote procedure call:

  • Parameter passing
  • Communication protocol mechanism
  • Error handling
  • Timeout handling

2. Parameter transfer

  • Value passed

The usual default is value passing, where you simply copy the values in the parameters to the data in the network message

  • reference

More difficult, simply pass parameters reference is completely useless, because reference address to give to the remote server, the memory address of the server is not data, the client wants not to be like this, the client must also send a copy of the data is passed to the remote server, and put them on a remote server memory, server replication reference address, Data can be read.

However, this is cumbersome and error-prone, and RPC generally does not support passing references directly

  • Data format unification problem

There needs to be a standard to encode and decode all data types, and data formats can be both implicit and explicit

  • Implicitly typed

Only the value is passed, not the name or type of the variable

  • Explicit type

Pass the type and value of the field

Common data transmission formats are:

  • ISO standard ASN.1
  • JSON
  • PROTOBUF
  • XML

3. Communication protocol mechanism

In a broad sense, the protocol stack is divided into common protocol and private protocol

  • A total of agreement

For example, HTTP, SMPP, and WEBSERVICE are all common protocols, which have advantages in public network transmission

  • A private agreement

The internal protocol has many disadvantages, but it can be highly customized, improve performance, reduce costs, and improve flexibility and efficiency. Private protocol development is often used within enterprises

The following five aspects should be considered in the formulation of the agreement:

  • Protocol design

What issues need to be considered

  • Private protocol codec

The need for business-specific codec methods, as shown in the following cases

  • Command definition and command processor selection

There are two kinds of protocols

  1. The load command

Transmit business specific data, such as request parameters, response results of the command

  1. Control command

Generally, they are function management commands, such as heartbeat commands

  • Protocol of command

Serialization protocol is generally used. Different protocols have different encoding efficiency and transmission efficiency, such as

  • Communication mode
  1. Oneway — The request thread will not be blocked without caring about the response
  2. Sync – the call will be blocked until the result is returned
  3. Future – The county thread will not be blocked when called, and will be blocked when the result is fetched
  4. Callback – Asynchronous calls that do not block the thread

Error handling and timeout handling

Remote procedure calls are more likely to fail than local procedure calls, so various scenarios of call failure need to be considered:

  • What can I do if a server error occurs
  • If an error occurs or a timeout occurs when a client requests services, a proper retry mechanism must be set

4. Simple GO language native RPC

It can be roughly divided into the following four steps:

  • Design data structures and methods

  • Implementation method

  • The registration service

  • The client connects to the server and invokes the methods of the server

    See below for an example of how Golang uses native RPC

RPC calls and service monitoring

  • RPC related Content
    • Data transfer: JSON Protobuf Thrift
    • Load: Random algorithm polling consistent hash weighting
    • Abnormal fault tolerance: health detection fuse current limiting
  • Service monitoring
    • Log collection
    • Some sample

1. Introduction of RPC

  • Remote Procedure Call (RPC) is a computer communication protocol
  • The protocol allows a program running on one computer to call a subroutine on another without the programmer having to program for this interaction
  • If the software involved uses object-oriented programming, remote procedure calls can also be called remote calls or remote method calls

2. RPC call process

Normally, we would call the function code directly locally. In microservice architecture, we need to run this function as a separate service, which is called by the client over the network

  • In microservice architecture, data interaction is generally internal RPC and external REST
  • Split the business into microservices by functional modules,Has the following advantages
    • Improve the efficiency of project collaboration
    • Reduce module coupling
    • Improve system availability
  • It has the following disadvantages:
    • The development threshold is relatively high, such as the use of RPC framework, later service monitoring and other work

3. RPC Golang native processing mode

The simplest use of Golang native RPC

The official GOLang NET/RPC library uses Encoding/GOB for codec and supports TCP and HTTP data transmission

server1.go

package main

import (
   "log"
   "net/http"
   "net/rpc"
)

type Happy struct{}

/ / happy
func (r *Happy) CalHappy(num int, ret *int) error {
   *ret = num * 10
   return nil
}

/ / the main function
func main(a) {
   // New a service
   ha := new(Happy)
   // Register a Happy service
   rpc.Register(ha)
   // Service processing is bound to the HTTP protocol
   rpc.HandleHTTP()
   // Listen to the service
   err := http.ListenAndServe(": 9999".nil)
   iferr ! =nil {
      log.Panicln(err)
   }
}
Copy the code

client1.go

package main

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

/ / the main function
func main(a) {
   // Connect to the remote RPC service
   conn, err := rpc.DialHTTP("tcp".": 9999")
   iferr ! =nil {
      log.Fatal(err)
   }
   // Call the server method
   ret := 0
   err2 := conn.Call("Happy.CalHappy".10, &ret)
   iferr2 ! =nil {
      log.Fatal(err2)
   }
   fmt.Println("Happiness index:", ret)
}
Copy the code

The results of

Use golang jsonrpc

Jsonrpc uses JSON to encode and decode data and supports cross-language invocation. Jsonrpc library is implemented based on TCP protocol and does not support HTTP transmission

server2.go

package main

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

type Happy struct{}

/ / happy
func (r *Happy) CalHappy(num int, ret *int) error {
   *ret = num * 10
   return nil
}

/ / the main function
func main(a) {
   // New a service
   ha := new(Happy)
   // Register a Happy service
   rpc.Register(ha)
   // Listen to the service
   listen, err := net.Listen("tcp".": 9999")
   iferr ! =nil {
      log.Panicln(err)
   }
   // Process the request
   for {
      con, err := listen.Accept()
      iferr ! =nil {
         continue
      }

      // open a special coroutine to handle the request
      go func(con net.Conn) {
         fmt.Println("process new client")
         jsonrpc.ServeConn(con)
      }(con)
   }
}
Copy the code

client2.go

package main

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

/ / the main function
func main(a) {
   // Connect to the remote RPC service
   conn, err := jsonrpc.Dial("tcp".": 9999")
   iferr ! =nil {
      log.Fatal(err)
   }
   // Call the server method
   ret := 0
   err2 := conn.Call("Happy.CalHappy".10, &ret)
   iferr2 ! =nil {
      log.Fatal(err2)
   }
   fmt.Println("Happiness index:", ret)
}
Copy the code

Golang Native RPC custom protocol

For example, in our custom protocol, a piece of data, ** the first two bytes are the data header, and the following bytes are the real data, such as:

  • Now that we have a custom protocol, we need to follow our protocol when we send and read data, otherwise there will be problems

  • So when we do data transmission will involve encoding and decoding, we also need to encapsulate the encoding and decoding function

Function encapsulation for writing and reading data
// Write data
func MyWriteData(con net.Conn, data []byte) (int, error) {
	if con == nil {
		log.Fatal("con is nil")
	}

	buf := make([]byte.2+len(data))
	// Write the length of the real data in the header first
	binary.BigEndian.PutUint16(buf[:2].uint16(len(data)))
	// Write data again
	copy(buf[2:], data)
	n, err := con.Write(buf)
	iferr ! =nil {
		log.Fatal("Write error", err)
	}
	return n, nil
}

// Read data
func MyReadData(con net.Conn) ([]byte, error) {
	if con == nil {
		log.Fatal("con is nil")}// The first two bytes of the protocol
	myheader := make([]byte.2)
	// Reads a 2-byte protocol header
	_, err := io.ReadFull(con, myheader)
	iferr ! =nil {
		log.Fatal("ReadFull error", err)
	}
	// Read real data
	// The length of the real data to read from the top
	len := binary.BigEndian.Uint16(myheader)
	data := make([]byte.len)
	_, err = io.ReadFull(con, data)
	iferr ! =nil {
		log.Fatal("ReadFull error", err)
	}
	return data, nil
}
Copy the code
Write function packages for encoding and decoding

We designed the string command to bind to the specific function called, so as to lay a foundation for the implementation of server3.go RPC

// A concrete data structure
type MyData struct {
	Name   string
	MyArgs []interface{} // Parameter list
}

/ / encryption
func MyEncode(data *MyData) ([]byte, error) {
	if data == nil {
		log.Fatal("con is nil")}var bb bytes.Buffer
	buf := gob.NewEncoder(&bb)
	iferr := buf.Encode(data); err ! =nil {
		log.Fatal("Encode error ", err)
	}
	return bb.Bytes(), nil
}

/ / decryption
func MyDecode(data []byte) (MyData, error) {
	if data == nil {
		log.Fatal("con is nil")
	}
	buf := bytes.NewBuffer(data)
	myDe := gob.NewDecoder(buf)
	var res MyData
	iferr := myDe.Decode(&res); err ! =nil {
		log.Fatal("Decode error ", err)
	}
	return res, nil
}
Copy the code
My_server. go:
package main

import (
	"bytes"
	"encoding/binary"
	"encoding/gob"
	"fmt"
	"io"
	"log"
	"net"
	"reflect"
)

// Write data
func MyWriteData(con net.Conn, data []byte) (int, error) {
	if con == nil {
		log.Fatal("con is nil")
	}

	buf := make([]byte.2+len(data))
	// Write the length of the real data in the header first
	binary.BigEndian.PutUint16(buf[:2].uint16(len(data)))
	// Write data again
	copy(buf[2:], data)
	n, err := con.Write(buf)
	iferr ! =nil {
		log.Fatal("Write error", err)
	}
	return n, nil
}

// Read data
func MyReadData(con net.Conn) ([]byte, error) {
	if con == nil {
		log.Fatal("con is nil")}// The first two bytes of the protocol
	myheader := make([]byte.2)
	// Reads a 2-byte protocol header
	_, err := io.ReadFull(con, myheader)
	iferr ! =nil {
		log.Fatal("ReadFull error", err)
	}
	// Read real data
	// The length of the real data to read from the top
	len := binary.BigEndian.Uint16(myheader)
	data := make([]byte.len)
	_, err = io.ReadFull(con, data)
	iferr ! =nil {
		log.Fatal("ReadFull error", err)
	}
	return data, nil
}

// A concrete data structure
type MyData struct {
	Name   string
	MyArgs []interface{} // Parameter list
}

/ / encryption
func MyEncode(data *MyData) ([]byte, error) {
	if data == nil {
		log.Fatal("con is nil")}var bb bytes.Buffer
	buf := gob.NewEncoder(&bb)
	iferr := buf.Encode(data); err ! =nil {
		log.Fatal("Encode error ", err)
	}
	return bb.Bytes(), nil
}

/ / decryption
func MyDecode(data []byte) (MyData, error) {
	if data == nil {
		log.Fatal("con is nil")
	}
	buf := bytes.NewBuffer(data)
	myDe := gob.NewDecoder(buf)
	var res MyData
	iferr := myDe.Decode(&res); err ! =nil {
		log.Fatal("Decode error ", err)
	}
	return res, nil
}

// a global map of commands and functions
var myFun = make(map[string]reflect.Value)

// Register the command binding function
func MyRegister(name string, fn interface{}) {
	if _, ok := myFun[name]; ok { // Indicates that the command has been bound with functions
		return
	}
	myFun[name] = reflect.ValueOf(fn)
	log.Println("reflect.ValueOf(fn) == ", myFun[name])
}

// The method executed by the server
func MyRun(addr string) {
	listen, err := net.Listen("tcp", addr)
	iferr ! =nil {
		log.Fatal("Listen is nil")
	}
	log.Println("Start server....")
	// Start blocking the connection waiting for the client
	for {
		con, err := listen.Accept()
		iferr ! =nil {
			log.Println("Accept is nil")
			return
		}
		// Read data
		b, err := MyReadData(con)
		iferr ! =nil {
			log.Println("MyReadData error ", err)
			return
		}
		log.Println("MyReadData =============== ")
		// Parse the data
		my, err := MyDecode(b)
		iferr ! =nil {
			log.Println("MyDecode =============== ")
			log.Println("MyDecode error ", err)
			return
		}
		f, ok := myFun[my.Name]
		if! ok { fmt.Printf("Command %s does not bind function \n", my.Name)
			return
		}
		// Get parameters
		args := make([]reflect.Value, 0.len(my.MyArgs))
		for _, arg := range my.MyArgs {
			args = append(args, reflect.ValueOf(arg))
			log.Println("reflect.ValueOf(arg) - ", reflect.ValueOf(arg))
		}

		/ / reflection
		res := f.Call(args)
		log.Println("f.Call(args) == ", res)
		// Wrap the result data to the client
		out := make([]interface{}, 0.len(res))
		for _, arg := range res {
			log.Println("arg == ", arg)
			out = append(out, arg.Interface())
		}
		log.Println("out == ", out)
		// Encode data
		bb, err := MyEncode(&MyData{Name: my.Name, MyArgs: out})
		iferr ! =nil {
			log.Println("MyEncode error ", err)
			return
		}
		// Write data to the client
		_, err = MyWriteData(con, bb)
		iferr ! =nil {
			log.Println("MyWriteData ======== ")
			log.Println("MyWriteData error ", err)
			return}}}// The client invokes the function with a command
func CallRPCFun(con net.Conn, rpcName string, args interface{}) {
	// Get the uninitialized function prototype of ARGS by reflection
	fn := reflect.ValueOf(args).Elem()
	log.Println("fn == ", fn)
	// We need another function that operates on the first function argument
	f := func(args []reflect.Value) []reflect.Value {
		// Process parameters
		inArgs := make([]interface{}, 0.len(args))
		for _, arg := range args {
			inArgs = append(inArgs, arg.Interface())
		}
		/ / the connection
		// Encode data
		reqRPC := &MyData{Name: rpcName, MyArgs: inArgs}
		b, err := MyEncode(reqRPC)
		iferr ! =nil {
			log.Println("MyEncode =============== ")
			log.Println("MyEncode error ", err)
		}
		/ / write data
		_, err = MyWriteData(con, b)
		iferr ! =nil {
			log.Println("MyWriteData =============== ")
			log.Fatal("MyWriteData error ", err)
		}
		// The server sends the return value, which should be read and parsed
		respBytes, err := MyReadData(con)
		iferr ! =nil {
			log.Fatal("MyReadData error ", err)
		}
		/ / decoding
		res, err := MyDecode(respBytes)
		iferr ! =nil {
			log.Println("MyDecode =============== ")
			log.Fatal("MyDecode error ", err)
		}
		// Process the data returned by the server
		outArgs := make([]reflect.Value, 0.len(res.MyArgs))
		for i, arg := range res.MyArgs {
			// Nil conversions must be made
			if arg == nil {
				// reflect.zero () returns a value of type Zero
				//.out() returns the type of argument output by the function
				outArgs = append(outArgs, reflect.Zero(fn.Type().Out(i)))
				continue
			}
			outArgs = append(outArgs, reflect.ValueOf(arg))
		}
		return outArgs
	}

	v := reflect.MakeFunc(fn.Type(), f)

	// Assign a value to f
	fn.Set(v)
}

// Define a user object
type Data struct {
	CmdName string
	Param   string
}

// The method used to test user queries
func GetData(id int) (Data, error) {
	data := make(map[int]Data)
	/ / false data
	data[0] = Data{"PullInfo"."xiaoxiong"}
	data[1] = Data{"PutInfo"."daxiong"}

	/ / query
	if u, ok := data[id]; ok {
		return u, nil
	}
	return Data{}, fmt.Errorf("%d err", id)
}

/ / the main function
func main(a) {
	// Simply set the log parameter
	log.SetFlags(log.Lshortfile | log.LstdFlags)
	
    // RPC server
	// register when one of the fields in the encoding is interface{}
	gob.Register(Data{})
	addr := "127.0.0.1:9999"
	// Create a server
	// Register the server method
	MyRegister("GetData", GetData)

	// The server waits for the call
	go MyRun(addr)
	
    //------------- I am the splitter -----------
    
	// The RPC client gets the connection
	conn, err := net.Dial("tcp", addr)
	iferr ! =nil {
		fmt.Println("Dial err")
		return
	}
	log.Println("The client dialed successfully, started calling the function...")
	// Create a client object
	// The function prototype needs to be declared
	var getdata func(int) (Data, error)

	CallRPCFun(conn, "GetData", &getdata)
	// Get the query result
	u, err := getdata(1)
	iferr ! =nil {
		fmt.Println("getdata err")
		return
	}
	log.Println(u)
	select{}}Copy the code

Results:

All the above are learned, if there are errors please correct

Technology is open, and so should our mindsets. Embrace change and move forward bravely on the road ahead. Come on, everybody!

I am Nezha, welcome to ridicule and communication