предисловие
В последнее время компания занимается распределенными вещами, и ей нужно использовать 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 необходимо решить эти три проблемы, а именно
- регистрация услуг
- передача по сети
- Сериализация и десериализация
форма вызова
Прежде чем объяснять конкретную реализацию, давайте посмотрим, как используется наш 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, код очень простой и пояснять не будем.
Сторона сервера обрабатывает сетевые соединения
- Создает объект Transfer, представляющий сетевой перевод
- Метод ReadData Call Transfer из сетевого подключениячитать данные
- Вызовите метод декодирования EdCode для преобразования данныхдесериализоватьв общий объект
- Получить десериализованные данные, такие как имя метода, параметры
- Найти ServiceMap по имени метода,Получить соответствующую услугу
- перечислитьСоответствующий сервис, получите результат
- приведет кСериализация
- Использование метода WriteData Transferнаписать ответв сетевое соединение
Выше показан процесс обработки сервером после получения клиентского запроса, который мы инкапсулируем в методе ServeConn. Код довольно длинный, поэтому я не буду его здесь выкладывать, если вам интересно, вы можете зайти на github и посмотреть его.
Клиент инициирует сетевое подключение
- Создает объект Transfer, представляющий сетевой перевод
- Имя службы и вызываемые параметрыСериализация
- Используйте метод WriteData класса Transfer, чтобы преобразовать сериализованные данные внаписатьв сетевое соединение
- Блокировать, пока серверная сторона не рассчитает
- Метод ReadData Call Transfer из сетевого подключениячитатьрасчетный результат
- приведет кдесериализоватьЗатем вернитесь к клиенту код показывает, как показано ниже:
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
Вы также можете обратить внимание на мой личный технический публичный аккаунт и надеяться на совместный прогресс.