Облегченная реализация RPC

Go gRPC

предисловие

В последнее время компания занимается распределенными вещами, и ей нужно использовать RPC. Раньше я мало что знал о RPC, и я прочитал много статей в Интернете, и обнаружил, что после прочтения мое понимание не углубилось. Службы или процесс, вызывающий службу, предоставляемую другим процессом, не более того. . Я также использовал официальную библиотеку golang net/rpc для достижения этой цели, но я не знаю нижнего уровня net/rpc (только знаю, как его использовать) Я сам реализовал RPC, поэтому у меня есть эта статья.

что такое РПЦ

Википедия объясняет RPC следующим образом:

In distributed computing, a remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in a different address space (commonly on another computer on a shared network), which is coded as if it were a normal (local) procedure call, without the programmer explicitly coding the details for the remote interaction.

Не буду переводить, вообще говоря, программа А вызывает программу Б, а А и Б не находятся в одном адресном пространстве (обычно А и Б не на одном компьютере), но звонить Б из А - это то же самое, что звонить локальная программа.Для этой детали удаленного взаимодействия не требуется никакого дополнительного программирования.

Здесь есть два ключевых слова, которые я выделил жирным шрифтом.распределенный: Указывает сценарий приложения RPC, который обычно используется на нескольких компьютерах, а не на одном компьютере.в разных адресных пространствах: указывает, что есть как минимум два процесса,Один серверный процесс, один клиентский процесс. Серверный процесс предоставляет службу (открывая некоторые интерфейсы), а клиентский процесс вызывает службу. Конечно, при тестировании серверную и клиентскую программы можно записать в файл, также можно написать серверную программу в основном потоке, предоставить интерфейс, а затем открыть новый поток для написания клиентской программы и вызова интерфейс.

Схема RPC

Какие проблемы необходимо решить для внедрения RPC

Из принципиальной схемы RPC видно, что серверная сторона является поставщиком услуг, а клиентская сторона — вызывающей стороной. Поскольку серверная часть может предоставлять услуги, она должна быть реализована в первую очередь.регистрация услуг, клиент может вызывать только зарегистрированную службу. Как упоминалось ранее, клиентская и серверная стороны, как правило, не находятся в одном и том же процессе или даже на одном компьютере, поэтому связь между ними должна передаваться по сети, что включает в себясетевой протокол передачи, если говорить более прямо, это то, как безопасно передать данные на стороне клиента (обычно имя вызываемой службы и соответствующие параметры) на сторону сервера, а серверная сторона может и получить их полностью. На стороне клиента и на стороне сервера данные обычно существуют в виде объектов, и объекты не могут быть переданы по сети.Перед передачей по сети нам необходимо сериализовать объекты в байтовые потоки, а затем передать эти байтовые потоки.Сервер сторона После получения этих потоков байтов выполните десериализацию, чтобы получить исходный объект, которыйСериализация и десериализация. Подводя итог, для реализации RPC необходимо решить эти три проблемы, а именно

  1. регистрация услуг
  2. передача по сети
  3. Сериализация и десериализация

форма вызова

Прежде чем объяснять конкретную реализацию, давайте посмотрим, как используется наш testrpc.server.go

package main

import (
	"log"
	"net"
	"testrpc"
)

type Args struct {
	A, B int
}

type Arith int

func (t *Arith) Multiply(args Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}

func main() {
	// 创建一个rpc server对象
	newServer := testrpc.NewServer()

	// 向rpc server对象注册一个Arith对象,注册后,client就可以调用Arith的Multiply方法
	arith := new(Arith)
	newServer.Register(arith)

	// 监听本机的1234端口
	l, e := net.Listen("tcp", "127.0.0.1:1234")
	if e != nil {
		log.Fatalf("net.Listen tcp :0: %v", e)
	}

	for {
		// 阻塞直到从1234端口收到一个网络连接
		conn, e := l.Accept()
		if e != nil {
			log.Fatalf("l.Accept: %v", e)
		}

		//开始工作
		go newServer.ServeConn(conn)
	}
}

Код относительно прост и есть комментарии.Вот краткое описание процесса:сначала мы создаем объект сервера rpc,а затем регистрируем объект Arith на сервере.После регистрации клиент может вызывать все методы выставленные Arith, здесь только Multiply. Затем слушаем порт 1234 локальной машины, и в цикле for ждем соединения с порта 1234. Когда приходит соединение, вызываем метод ServeConn для обработки соединения в новой горутине, после чего продолжаем ждать для нового подключения.. так неоднократно.

client.go

package main

import (
	"log"
	"net"
	"os"
	"testrpc"
)

type Args struct {
	A, B int
}

func main() {

	// 连接本机的1234端口,返回一个net.Conn对象
	conn, err := net.Dial("tcp", "127.0.0.1:1234")
	if err != nil {
		log.Println(err.Error())
		os.Exit(-1)
	}

	// main函数退出时关闭该网络连接
	defer conn.Close()

	// 创建一个rpc client对象
	client := testrpc.NewClient(conn)
	// main函数退出时关闭该client
	defer client.Close()

	// 调用远端Arith.Multiply函数
	args := Args{7, 8}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		log.Fatal("arith error:", err)
	}
	log.Println(reply)
}

Мы сначала подключаемся к порту 1234 локальной машины (прослушивается порт server.go), получаем объект net.Conn, а затем используем этот объект для создания rpc-клиента, а затем вызываем предоставленный сервером метод Multiply через этот клиент для расчета После этого сохраните результат для ответа. Код очень простой, говорить особо не о чем.

Это использование относится к официальной библиотеке net/rpc golang.Заинтересованные читатели также могут узнать, как использовать net/rpc.

Принцип реализации

Определение сервера

Как упоминалось ранее, проблемы, которые необходимо решить на стороне сервера:регистрация услуг, то есть метод Register, то сервер должен уметь хранить эти сервисы, поэтому определение сервера может быть следующим:

type Service struct {
	Method    reflect.Method
	ArgType   reflect.Type
	ReplyType reflect.Type
}

type Server struct {
	ServiceMap  map[string]map[string]*Service
	serviceLock sync.Mutex
}

Объект службы соответствует службе, а служба включает в себя методы, типы параметров и типы возвращаемых значений. Сервер имеет два атрибута: ServiceMap и serviceLock. ServiceMap представляет собой набор из серии сервисов. Причина, по которой он представлен в виде карты, заключается в облегчении поиска. ServiceLock предназначен для защиты ServiceMap и гарантирует, что только одна горутина может записать ServiceMap в то же время.

регистрация услуг

func (server *Server) Register(obj interface{}) error {
	server.serviceLock.Lock()
	defer server.serviceLock.Unlock()

	//通过obj得到其各个方法,存储在servicesMap中
	tp := reflect.TypeOf(obj)
	val := reflect.ValueOf(obj)
	serviceName := reflect.Indirect(val).Type().Name()
	if _, ok := server.ServiceMap[serviceName]; ok {
		return errors.New(serviceName + " already registed.")
	}

	s := make(map[string]*Service)
	numMethod := tp.NumMethod()
	for m := 0; m < numMethod; m++ {
		service := new(Service)
		method := tp.Method(m)
		mtype := method.Type
		mname := method.Name

		service.ArgType = mtype.In(1)
		service.ReplyType = mtype.In(2)
		service.Method = method
		s[mname] = service
	}
	server.ServiceMap[serviceName] = s
	server.ServerType = reflect.TypeOf(obj)
	return nil
}

Может быть, будет понятнее взглянуть на предыдущий код, который вызывает Register здесь.

type Arith int

func (t *Arith) Multiply(args Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}

...
newServer := testrpc.NewServer()
newServer.Register(new(Arith))
...

Примерная логика регистра таковаПолучите открытые методы obj (параметр Register) (есть только один Multiply), а затем сохраните их в ServiceMap сервера.. Здесь в основном используется рефлекс Голанга, и если вы не понимаете рефлексию, все равно сложно прочитать код регистрации. В Интернете есть много статей, объясняющих рефлексию, поэтому читателям, которые не понимают рефлексию, рекомендуется сначала ознакомиться с ней, поэтому я не буду говорить об этом здесь. После регистрации ServiceMap выглядит так

{
	"Arith": {"Multiply":&{Method:Multiply, ArgType:main.Args, ReplyType:*int}}	
}

передача по сети

Сетевая передача testrpc основана на net.Conn, предоставляемом golang.Этот net.Conn предоставляет два метода: чтение и запись. Чтение означает чтение данных из сетевого подключения, а запись означает запись данных в сетевое подключение. Мы реализуем нашу сетевую передачу на основе этих двух методов.Код выглядит следующим образом:

const (
	EachReadBytes = 500
)

type Transfer struct {
	conn net.Conn
}

func NewTransfer(conn net.Conn) *Transfer {
	return &Transfer{conn: conn}
}

func (trans *Transfer) ReadData() ([]byte, error) {
	finalData := make([]byte, 0)
	for {
		data := make([]byte, EachReadBytes)
		i, err := trans.conn.Read(data)
		if err != nil {
			return nil, err
		}
		finalData = append(finalData, data[:i]...)
		if i < EachReadBytes {
			break
		}
	}
	return finalData, nil
}

func (trans *Transfer) WriteData(data []byte) (int, error) {
	num, err := trans.conn.Write(data)
	return num, err
}

ReadData этоЧтение данных из сетевого подключения, считывать 500 байт каждый раз (указывается EveryReadBytes), пока не завершится, а затем возвращать прочитанные данные; WriteDataзапись данных в сетевое соединение.

Сериализация и десериализация

В предыдущем разделе мы говорили о передаче по сети.Мы знаем, что объектом передачи являетсябайтовый поток. Сериализация отвечает за превращение объекта в поток байтов, десериализация наоборот отвечает за превращение потока байтов в объект в программе. Перед передачей по сети нам нужно сначала сериализовать.Testrpc использует json в качестве метода сериализации, и в будущем могут быть добавлены другие методы сериализации, такие как gob, xml, protobuf и т. д. Давайте сначала посмотрим на код, который использует json в качестве сериализации:

type EdCode int

func (edcode EdCode) encode(v interface{}) ([]byte, error) {
	return json.Marshal(v)
}

func (edcode EdCode) decode(data []byte, v interface{}) error {
	return json.Unmarshal(data, v)
}

Здесь используется библиотека json, официально предоставленная golang, код очень простой и пояснять не будем.

Сторона сервера обрабатывает сетевые соединения

  1. Создает объект Transfer, представляющий сетевой перевод
  2. Метод ReadData Call Transfer из сетевого подключениячитать данные
  3. Вызовите метод декодирования EdCode для преобразования данныхдесериализоватьв общий объект
  4. Получить десериализованные данные, такие как имя метода, параметры
  5. Найти ServiceMap по имени метода,Получить соответствующую услугу
  6. перечислитьСоответствующий сервис, получите результат
  7. приведет кСериализация
  8. Использование метода WriteData Transferнаписать ответв сетевое соединение

Выше показан процесс обработки сервером после получения клиентского запроса, который мы инкапсулируем в методе ServeConn. Код довольно длинный, поэтому я не буду его здесь выкладывать, если вам интересно, вы можете зайти на github и посмотреть его.

Клиент инициирует сетевое подключение

  1. Создает объект Transfer, представляющий сетевой перевод
  2. Имя службы и вызываемые параметрыСериализация
  3. Используйте метод WriteData класса Transfer, чтобы преобразовать сериализованные данные внаписатьв сетевое соединение
  4. Блокировать, пока серверная сторона не рассчитает
  5. Метод ReadData Call Transfer из сетевого подключениячитатьрасчетный результат
  6. приведет кдесериализоватьЗатем вернитесь к клиенту код показывает, как показано ниже:
func (client *Client) Call(methodName string, req interface{}, reply interface{}) error {

	// 构造一个Request
	request := NewRequest(methodName, req)

	// encode
	var edcode EdCode
	data, err := edcode.encode(request)
	if err != nil {
		return err
	}

	// write
	// 构造一个Transfer
	trans := NewTransfer(client.conn)
	_, err = trans.WriteData(data)
	if err != nil {
		log.Println(err.Error())
		return err
	}

	// read
	data2, err := trans.ReadData()
	if err != nil {
		log.Println(err.Error())
		return err
	}

	// decode and assin to reply
	edcode.decode(data2, reply)

	// return
	return nil
}

Определение клиента очень простое, просто соединение, представляющее сетевое соединение, код выглядит следующим образом:

type Client struct {
	conn net.Conn
}

Почему бы не реализовать это на Python

Поскольку я обычно пишу много кода на Python, действительно быстрее реализовать этот testrpc на Python. Так зачем использовать golang? Поскольку проект, недавно выполненный нашей компанией, основан на net/rpc golang, с ним легко работать, я понимаю использование net/rpc, а также я посмотрел на базовый код net/rpc и угадал его общий принцип, Так что я подумал написать один сам, и это все. Конечно, если будет возможность в будущем, я снова реализую это на Python.

Суммировать

В этой статье в основном описывается принцип RPC и реализуется облегченный RPC. Позвольте мне рассказать о проблемах, с которыми я столкнулся при отладке: поскольку golang — язык со строгой типизацией, проблема, с которой я сталкиваюсь, заключается в том, как можно сериализовать и десериализовать объект?Хранитьего исходный тип. Например, объект типа Args (читатели могут повернуться вперед, чтобы увидеть, как определяется Args) становится типом интерфейса map[string]interface{} после сериализации и десериализации, а тип интерфейса map[string]{} равен используется как параметр типа Args, будет сообщено об ошибке.Заинтересованные читатели в этой части могут перейти к коду. У читателей может не возникнуть никаких чувств после прочтения этой статьи. Прочитав ее один раз, они ее прочитали. Она кажется полезной и не вознаграждающей. Я предлагаю загрузить код, затем запустить его на своем компьютере и отладить его. Я верю, что у вас будет глубокое понимание, будь то RPC, или отражение голанга и т. д. Например, раньше я не очень хорошо разбирался в отражении, но после написания этого testrpc я в основном освоил использование отражения.

код находится вgithubOn, вы можете поставить звезду и отправить вопрос, а также можете высказать свое собственное мнение, например, этот код написан не очень хорошо, его можно оптимизировать таким образом и т. д., вы можете общаться с я, спасибо!

адрес гитхаба: https://github.com/TanLian/testrpc

Вы также можете обратить внимание на мой личный технический публичный аккаунт и надеяться на совместный прогресс.