본문 바로가기
Framework and Tool/gRPC and Protobuf

RPC(Remote procedure call)

by ocwokocw 2022. 3. 13.

- 출처: https://en.wikipedia.org/wiki/Remote_procedure_call#Analogues

- 출처: https://pkg.go.dev/net/rpc

- RPC란 무엇인가

gRPC에 관해 알아보기 전에 RPC란 개념부터 알아보기로 하자. RPC란 Remote procedure call 의 약자이다. 다른 주소공간(일반적으로는 같은 네트워크를 공유하는 다른 컴퓨터)의 procedure를 실행할 때, 원격 세부사항에 관한 명시적인 코딩없이 마치 local 컴퓨터의 procedure를 호출하는것처럼 사용하는 방식이다. OOP에서는 RMI(Remote method invocation)으로 표현하기도 한다.
 
Go에서는 package rpc를 제공하며 Java에서는 Java RMI를 제공한다.

- RPC 유의사항

RPC는 원격 기기의 procedure를 실행하므로 network를 통해 실행된다는점을 반드시 인지해야 하며, 이에 따라 network 문제가 발생할 수 있다. 또한 remote procedure가 수행됐는지 여부에 의존하지 않는 실패처리 등과 같은 점도 고려할 필요가 있다.

- RPC의 실행흐름

RPC는 다음과 같은 과정을 거친다.
 
  • client가 client stub을 호출
  • client stub이 parameter를 메시지로 넣고(marshalling) 시스템이 해당 메시지를 보내게 만든다.
  • client의 OS가 client에서 server로 전송한다.
  • server의 OS가 수신된 메시지를 server의 stub으로 보낸다.
  • server의 stub이 메시지로 부터 parameter를 추출한다.(unmarshalling)
  • server stub이 server의 procedure를 호출한다. (응답은 역방향으로 진행된다.)

- RPC go Example

go에서 제공하는 rpc package를 이용하여 간단한 RPC를 구현해보자. 두 int 형 인자를 곱하거나 나누는 간단한 server program을 작성해보자.
 
package server

import "errors"

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Tem 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.Tem = args.A % args.B
	return nil
}
 
client는 server와 똑같은 인자를 표현한 struct를 정의한다.
 
package client

import server2 "seminar/g_rpc/rpc/server"

type server struct {
	Args server2.Args
}

type Args struct {
	A, B int
}
 
client에서 server의 RPC를 호출하는 test 코드를 작성한다.
 
package rpc

import (
	"net"
	"net/http"
	"net/rpc"
	"testing"

	errors2 "github.com/pkg/errors"

	client2 "seminar/g_rpc/rpc/client"
	server2 "seminar/g_rpc/rpc/server"
)

const expectedResult = 56

type rpcServer struct {
	protocol string
	port     string
}

func TestSyncRpc(t *testing.T) {
	server, err := startRpcServer(t)
	if err != nil {
		t.Fatal("startRpcServer: ", err)
	}

	args := client2.Args{
		A: 7,
		B: 8,
	}
	reply, err := multiplyViaRpc(t, server, args)
	if err != nil {
		t.Fatal("rpc: ", err)
	}
	if reply != expectedResult {
		t.Errorf("expected: %d, but got: %d", expectedResult, reply)
	}
}

func startRpcServer(t testing.TB) (*rpcServer, error) {
	t.Helper()

	arith := new(server2.Arith)
	rpc.Register(arith)
	rpc.HandleHTTP()
	protocol, port := "tcp", ":1234"
	l, e := net.Listen(protocol, port)
	if e != nil {
		return nil, errors2.Wrap(e, "listen error:")
	}
	go http.Serve(l, nil)
	return &rpcServer{
		protocol: protocol,
		port:     port,
	}, nil
}

func multiplyViaRpc(t testing.TB, server *rpcServer, args client2.Args) (int, error) {
	t.Helper()

	client, err := rpc.DialHTTP(server.protocol, "localhost"+server.port)
	if err != nil {
		return 0, errors2.Wrap(err, "dialing:")
	}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
		return 0, errors2.Wrap(err, "arith error:")
	}
	return reply, nil
}
 
RPC server를 실행하는 startRpcServer 코드는 아래와 같은 동작을 한다.
 
  • server에서 RPC로 노출할 Arith struct를 rpc.Register 메소드로 등록한다.
  • rpc.HandleHTTP 메소드로 RPC 요청을 받을 수 있게 HTTP handler를 등록한다.
  • client로 부터 응답을 들을 Listen 정보인 protocol과 port를 설정하여 go http.Server 메소드로 서버를 띄운다.
 
RPC server로 요청할 multiplyViaRpc의 동작은 아래와 같다.
 
  • rpc.DialHTTP 메소드로 server의 protocol과 port 정보로 RPC 서버로의 연결을 시도한다.
  • rpc.DialHTTP로 부터 받은 client를 이용하여 client.Call 메소드로 서버의 Procedure를 실행한다. 실행할 Procedure와 인자, 반환값의 pointer를 인자로 넘겨준다.
 
 

'Framework and Tool > gRPC and Protobuf' 카테고리의 다른 글

Protocol buffer - Convention  (0) 2022.04.29
Protocol buffer - Proto3  (0) 2022.04.24
Protocol buffer  (0) 2022.03.13
gRPC with protobuf  (0) 2022.03.13

댓글