GNPS (Go Network Programming School)의 내용을 정리한 글입니다.
Go 언어를 활용한 네트워크 프로그래밍 - 책으로 스터디를 진행합니다.

데이터 직렬화 및 역직렬화는 다양한 서비스와 통신하고 데이터를 교환하는 데 사용된다. 애플리케이션 계층에서 선언된 데이터는 바이트로 변환되어 네트워크 전송이나 데이터 저장에 사용될 수 있다. 따라서 바이트로 구성된 데이터를 재객체화하는 메커니즘이 있다면 데이터가 어디에서 왔는지에 관계없이 형식에 맞는 데이터를 보내고 받을 수 있다.
다양한 데이터 직렬화 형식 중에서 가장 인기 있는 직렬화 형식인 JSON, 프로토콜 버퍼, Gob 형식을 살펴본다. 또한, gRPC라는 프레임워크를 사용하여 원격 노드에 있는 코드를 마치 로컬에서 실행되는 것처럼 사용하는 방법도 알아본다.
객체 직렬화하기
애플리케이션 계층에서 구조화된 데이터는 동일한 형태를 가지고 네트워크를 통해 전송할 수 없다. net.Conn의 Write() 함수는 구조체 자체를 전달할 수 없고, 바이트 슬라이스만 매개변수로 전달할 수 있다. Go 언어에서는 인코딩 및 디코딩을 위한 표준 라이브러리로 encoding 패키지를 지원한다.
package housework
type Chore struct {
Complete bool
Description string
}
집안일 객체를 직렬화할 수 있도록 Go의 구조체를 선언한다. 이 구조체를 사용하여 직렬화 및 역직렬화하는 다양한 방법에 대해서 살펴본다.
JSON
JSON은 사람이 읽을 수 있는 텍스트 기반 데이터 직렬화 형식이다. 일반적으로 키-값 쌍을 사용하며 여러 데이터 구조를 나타낼 수 있다. Go 언어에서는 encoding/json 패키지를 사용하여 JSON을 직렬화하고 역직렬화할 수 있다.
package json
import (
"encoding/json"
"io"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework"
)
func Load(r io.Reader) ([]*housework.Chore, error) {
var chore []*housework.Chore
return chore, json.NewDecoder(r).Decode(&chore)
}
func Flust(w io.Writer, chores []*housework.Chore) error {
return json.NewEncoder(w).Encode(chores)
}
Gob
Gob는 Go 네이티브 바이너리 직렬화 포맷이다. 프로토콜 버퍼의 효율성과 JSON의 쉬운 사용성을 통합하기 위해 Gob를 개발했다.
package gob
import (
"encoding/gob"
"io"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework"
)
func Load(r io.Reader) ([]*housework.Chore, error) {
var chores []*housework.Chore
return chores, gob.NewDecoder(r).Decode(&chores)
}
func Flush(w io.Writer, chores []*housework.Chore) error {
return gob.NewEncoder(w).Encode(chores)
}
앞선 JSON 부분을 전부 gob으로 바꿔서 코드를 작성했다. JSON을 사용하는 것과 마찬가지로 간편하게 사용할 수 있다. JSON보다 성능이 좋으면서 JSON보다 더 작은 스토리지를 사용하기 때문에 직렬화된 데이터를 저장하거나 전송할 때 이점이 있다. 따라서 Go 서비스가 Gob 혹은 JSON을 지원한다면 JSON보다 Gob을 선택하는 것이 좋다.
프로토콜 버퍼
프로토콜 버퍼(Protocol Buffer)는 Gob처럼 바이너리 인코딩을 사용한 다양한 플랫폼에서 데이터를 저장하고 교환하는 데 사용하는 직렬화 포맷이다. JSON보다 한참 빠르고 한참 간결한 데이터 형식을 가진다. Gob처럼 Go에 종속되어 있지 않으며 많은 프로그래밍 언어에서 지원한다.
이런 특성은 프로토콜 버퍼를 Go 기반의 서비스뿐만 아니라 다른 언어로 작성된 서비스와 통합하면서 성능적으로 이점을 가지려고 할 때 사용할 수 있게 한다.
다음 명령어를 실행하여 protobuf와 protobuf 컴파일을 위한 도구를 설치한다.
brew install protobuf
go install github.com/golang/protobuf/protoc-gen-go@latest
프로토콜 버퍼는. proto 확장자를 가지는 별도의 파일에 정의한다. 메시지는 데이터를 저장하거나 전송하기 위해 직렬화 및 역직렬화할 구조화된 데이터와 같다.
syntax = "proto3";
package housework;
option go_package = "github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework/v1/housework";
message Chore {
bool complete = 1;
string description = 2;
}
message Chores {
repeated Chore chores = 1;
}
애플리케이션에서 프로토콜 버퍼를 사용하려면. proto 파일을 컴파일하여 언어에 맞는 코드로 생성해야 한다. 생성된 코드를 사용하여 사용하는 프로그래밍 언어에서 message를 직렬화하거나 역직렬화할 수 있다.
proto3 버전 문법을 사용하는 것을 지정하고 패키지 명이 housework가 되도록 설정한다. 모듈 전체 임포트 경로를 선언하고 Chore 메시지 정의 및 여러 개의 집안일을 나타내는 Chores 메시지를 정의한다.
이후. proto 파일을 Go 코드로 컴파일한다.
protoc --go_out=. --go_opt=paths=source_relative housework/v1/housework.proto
해당 명령어를 통해 생성된 프로토콜 버퍼로 컴파일된 Go 언어 코드를 확인해 볼 수 있다. 생성된 코드는 수정하면 안 되며 수정 사항이 있는 경우. proto 파일을 수정하여 다시 컴파일이 필요하다.
생성된 Go 코드를 사용하여 프로토콜 버퍼를 적용한 직렬화 및 역직렬화 구현은 다음과 같다.
package protobuf
import (
"io"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework/v1"
"google.golang.org/protobuf/proto"
)
func Load(r io.Reader) ([]*housework.Chore, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, err
}
var chores housework.Chores
return chores.Chores, proto.Unmarshal(b, &chores)
}
func Flush(w io.Writer, chores []*housework.Chore) error {
b, err := proto.Marshal(&housework.Chores{Chores: chores})
if err != nil {
return err
}
_, err = w.Write(b)
return err
}
protoc 컴파일러로 생성한 패키지를 임포트한다. 프로토콜 버퍼는 인코더와 디코더가 아닌 마샬링 된 바이트를 io.Writer로 작성하고 io.Reader로 읽은 바이트를 언마샬링 한다.
애플리케이션 출력 확인하기
앞서 작성한 모듈을 사용하여 커맨드라인 인터페이스를 구현한다.
package cmd
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/gob"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/json"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/protobuf"
)
var dataFile string
var t string
func init() {
flag.StringVar(&dataFile, "file", "housework.db", "data file")
flag.StringVar(&t, "type", "j|g|p", "JSON|Gob|Protocol Buffer serialize formatting")
flag.Usage = func() {
fmt.Fprintf(
flag.CommandLine.Output(),
`Usage: %s [flags] [add chore, ... | complete #]
add add comma-separated chores
complete complete designated chore
Flags:
`,
filepath.Base(os.Args[0]),
)
flag.PrintDefaults()
}
}
func load() ([]*housework.Chore, error) {
if _, err := os.Stat(dataFile); os.IsNotExist(err) {
return make([]*housework.Chore, 0), nil
}
df, err := os.Open(dataFile)
if err != nil {
return nil, err
}
defer func() {
if err := df.Close(); err != nil {
fmt.Printf("closing data file: %v", err)
}
}()
switch t {
case "j":
return json.Load(df)
case "g":
return gob.Load(df)
case "p":
return protobuf.Load(df)
default:
return nil, fmt.Errorf("unsupported type: %s (use j, g, or p)", t)
}
}
func flush(chores []*housework.Chore) error {
df, err := os.Create(dataFile)
if err != nil {
return err
}
defer func() {
if err := df.Close(); err != nil {
fmt.Printf("closing data file: %v", err)
}
}()
switch t {
case "j":
return json.Flust(df, chores)
case "g":
return gob.Flush(df, chores)
case "p":
return protobuf.Flush(df, chores)
default:
return fmt.Errorf("unsupported type: %s (use j, g, or p)", t)
}
}
func list() error {
chores, err := load()
if err != nil {
return err
}
if len(chores) == 0 {
fmt.Println("You're all caught up!")
return nil
}
for i, chore := range chores {
status := " "
if chore.Complete {
status = "X"
}
fmt.Printf("[%s] %d: %s\n", status, i, chore.Description)
}
return nil
}
func add(s string) error {
chores, err := load()
if err != nil {
return err
}
descriptions := strings.Split(s, ",")
for _, desc := range descriptions {
desc = strings.TrimSpace(desc)
if desc != "" {
chores = append(chores, &housework.Chore{
Description: desc,
Complete: false,
})
}
}
return flush(chores)
}
func complete(arg string) error {
chores, err := load()
if err != nil {
return err
}
index, err := strconv.Atoi(arg)
if err != nil {
return fmt.Errorf("invalid chore number: %s", arg)
}
if index < 0 || index >= len(chores) {
return fmt.Errorf("chore number %d out of range (0-%d)", index, len(chores)-1)
}
chores[index-1].Complete = true
return flush(chores)
}
func Run() error {
flag.Parse()
if t != "j" && t != "g" && t != "p" {
return fmt.Errorf("invalid type: %s (use j for JSON, g for Gob, or p for Protobuf)", t)
}
args := flag.Args()
if len(args) == 0 {
return list()
}
switch strings.ToLower(args[0]) {
case "add":
if len(args) < 2 {
return fmt.Errorf("add requires at least one chore description")
}
return add(strings.Join(args[1:], " "))
case "complete":
if len(args) < 2 {
return fmt.Errorf("complete requires a chore number")
}
return complete(args[1])
default:
return fmt.Errorf("unknown command: %s (use 'add' or 'complete')", args[0])
}
err := list()
if err != nil {
log.Fatal(err)
}
return err
}
package main
import (
"bytes"
"fmt"
"os"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/cmd"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/gob"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/json"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/protobuf"
)
func main() {
// 테스트 모드 확인
if len(os.Args) > 1 && os.Args[1] == "test" {
runTests()
return
}
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runTests() {
fmt.Println("=== 인코딩/디코딩 타입별 테스트 시작 ===")
// 테스트 데이터 생성
testChores := []*housework.Chore{
{Description: "설거지하기", Complete: false},
{Description: "빨래하기", Complete: true},
{Description: "청소하기", Complete: false},
{Description: "요리하기", Complete: true},
{Description: "쓰레기 버리기", Complete: false},
}
fmt.Printf("원본 데이터 (%d개 항목):\n", len(testChores))
for i, chore := range testChores {
status := " "
if chore.Complete {
status = "X"
}
fmt.Printf(" [%s] %d: %s\n", status, i, chore.Description)
}
fmt.Println()
// 1. JSON 테스트
fmt.Println("--- JSON 인코딩/디코딩 테스트 ---")
testJSON(testChores)
// 2. Gob 테스트
fmt.Println("\n--- Gob 인코딩/디코딩 테스트 ---")
testGob(testChores)
// 3. Protobuf 테스트
fmt.Println("\n--- Protobuf 인코딩/디코딩 테스트 ---")
testProtobuf(testChores)
// 4. 크기 비교
fmt.Println("\n--- 인코딩 크기 비교 ---")
compareSizes(testChores)
// 5. 타입 간 호환성 테스트
fmt.Println("\n--- 타입 간 호환성 테스트 ---")
testCrossTypeCompatibility(testChores)
fmt.Println("\n=== 모든 테스트 완료 ===")
}
func testJSON(chores []*housework.Chore) {
buf := new(bytes.Buffer)
// 인코딩
if err := json.Flust(buf, chores); err != nil {
fmt.Printf(" ❌ JSON 인코딩 실패: %v\n", err)
return
}
fmt.Printf(" ✓ JSON 인코딩 성공 (크기: %d bytes)\n", buf.Len())
// 디코딩
decoded, err := json.Load(buf)
if err != nil {
fmt.Printf(" ❌ JSON 디코딩 실패: %v\n", err)
return
}
fmt.Printf(" ✓ JSON 디코딩 성공 (%d개 항목 복원)\n", len(decoded))
// 검증
if verifyChores(chores, decoded) {
fmt.Println(" ✓ 데이터 무결성 검증 성공")
} else {
fmt.Println(" ❌ 데이터 무결성 검증 실패")
}
}
func testGob(chores []*housework.Chore) {
buf := new(bytes.Buffer)
// 인코딩
if err := gob.Flush(buf, chores); err != nil {
fmt.Printf(" ❌ Gob 인코딩 실패: %v\n", err)
return
}
fmt.Printf(" ✓ Gob 인코딩 성공 (크기: %d bytes)\n", buf.Len())
// 디코딩
decoded, err := gob.Load(buf)
if err != nil {
fmt.Printf(" ❌ Gob 디코딩 실패: %v\n", err)
return
}
fmt.Printf(" ✓ Gob 디코딩 성공 (%d개 항목 복원)\n", len(decoded))
// 검증
if verifyChores(chores, decoded) {
fmt.Println(" ✓ 데이터 무결성 검증 성공")
} else {
fmt.Println(" ❌ 데이터 무결성 검증 실패")
}
}
func testProtobuf(chores []*housework.Chore) {
buf := new(bytes.Buffer)
// 인코딩
if err := protobuf.Flush(buf, chores); err != nil {
fmt.Printf(" ❌ Protobuf 인코딩 실패: %v\n", err)
return
}
fmt.Printf(" ✓ Protobuf 인코딩 성공 (크기: %d bytes)\n", buf.Len())
// 디코딩
decoded, err := protobuf.Load(buf)
if err != nil {
fmt.Printf(" ❌ Protobuf 디코딩 실패: %v\n", err)
return
}
fmt.Printf(" ✓ Protobuf 디코딩 성공 (%d개 항목 복원)\n", len(decoded))
// 검증
if verifyChores(chores, decoded) {
fmt.Println(" ✓ 데이터 무결성 검증 성공")
} else {
fmt.Println(" ❌ 데이터 무결성 검증 실패")
}
}
func compareSizes(chores []*housework.Chore) {
// JSON 크기
jsonBuf := new(bytes.Buffer)
json.Flust(jsonBuf, chores)
jsonSize := jsonBuf.Len()
// Gob 크기
gobBuf := new(bytes.Buffer)
gob.Flush(gobBuf, chores)
gobSize := gobBuf.Len()
// Protobuf 크기
protoBuf := new(bytes.Buffer)
protobuf.Flush(protoBuf, chores)
protoSize := protoBuf.Len()
fmt.Printf(" JSON: %5d bytes (100.0%%)\n", jsonSize)
fmt.Printf(" Gob: %5d bytes (%5.1f%%)\n", gobSize, float64(gobSize)/float64(jsonSize)*100)
fmt.Printf(" Protobuf: %5d bytes (%5.1f%%)\n", protoSize, float64(protoSize)/float64(jsonSize)*100)
// 가장 작은 크기 찾기
minSize := jsonSize
minType := "JSON"
if gobSize < minSize {
minSize = gobSize
minType = "Gob"
}
if protoSize < minSize {
minSize = protoSize
minType = "Protobuf"
}
fmt.Printf("\n 가장 효율적인 인코딩: %s (%d bytes)\n", minType, minSize)
}
func testCrossTypeCompatibility(chores []*housework.Chore) {
fmt.Println(" JSON으로 인코딩 후 Gob으로 디코딩 시도...")
jsonBuf := new(bytes.Buffer)
json.Flust(jsonBuf, chores)
if _, err := gob.Load(jsonBuf); err != nil {
fmt.Printf(" ✓ 예상대로 실패: %v\n", err)
} else {
fmt.Println(" ❌ 예상과 다르게 성공 (호환되지 않아야 함)")
}
fmt.Println("\n Gob으로 인코딩 후 Protobuf으로 디코딩 시도...")
gobBuf := new(bytes.Buffer)
gob.Flush(gobBuf, chores)
if _, err := protobuf.Load(gobBuf); err != nil {
fmt.Printf(" ✓ 예상대로 실패: %v\n", err)
} else {
fmt.Println(" ❌ 예상과 다르게 성공 (호환되지 않아야 함)")
}
fmt.Println("\n Protobuf으로 인코딩 후 JSON으로 디코딩 시도...")
protoBuf := new(bytes.Buffer)
protobuf.Flush(protoBuf, chores)
if _, err := json.Load(protoBuf); err != nil {
fmt.Printf(" ✓ 예상대로 실패: %v\n", err)
} else {
fmt.Println(" ❌ 예상과 다르게 성공 (호환되지 않아야 함)")
}
}
func verifyChores(original, decoded []*housework.Chore) bool {
if len(original) != len(decoded) {
return false
}
for i := range original {
if original[i].Description != decoded[i].Description {
return false
}
if original[i].Complete != decoded[i].Complete {
return false
}
}
return true
}
위 코드를 아래와 같은 명령어 세트를 통해서 테스트해 볼 수 있다.
go build -v
./encode_decode -type j
./encode_decode -type j add "Buy groceries, Clean room, Do laundry"
./encode_decode -type j
#[ ] 0: Buy groceries
#[ ] 1: Clean room
#[ ] 2: Do laundry
./encode_decode -type j complete 1
./encode_decode -type j
#[X] 0: Buy groceries
#[ ] 1: Clean room
#[ ] 2: Do laundry
혹은 아래 명령어를 사용하면 테스트할 수 있다.
go run main.go test
직렬화된 객체 전송
직렬화된 데이터는 네트워크를 통하여 전송된다. 그중 gRPC는 RPC를 구현하는 프레임워크이다. HTTP/2와 프로토콜 버퍼를 활용하여 크로스 플랫폼(여러 플랫폼을 지원하는) 프레임워크이다.
go install google.golang.org/grpc@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
위 명령어를 사용하여 여러 rpc 패키지를 추상화한 gRPC와 gRPC Go 코드를 생성하기 위한 gRPC 모듈이 포함된 프로토콜 버퍼 컴파일러를 다운로드한다.
service를 정의하고 service 하위에 RPC 메서드를 추가할 수 있다. Add, Complete, List 함수를 작성하고 필요한 요청과 응답에 대한 부분은 message로 정의한다.
// 위에서 작성한 housework.proto 파일에 이어서 작성
// ...
service RobotMaid {
rpc Add (Chores) returns (Response);
rpc Complete (CompleteRequest) returns (Response);
rpc List (Empty) returns (Chores);
}
message CompleteRequest {
int32 chore_number = 1;
}
message Empty {}
message Response{
string message = 1;
}
새로운 서비스와 메시지를 컴파일하기 위해 protoc의 go grpc 모듈이 포함되어 있는 옵션을 사용하여 컴파일한다.
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
housework/v1/housework.proto
--go_out과 --go_opt 옵션을 사용하여 message 구조체에 대한 직렬화 역직렬화 코드를 생성하고 --go-grpc_out과 --go-grpc_opt 옵션을 사용하여 서비스에 대한 클라이언트와 서버의 인터페이스 코드를 생성한다.
TLS 가 적용된 gRPC 서버 구현
gRPC는 보안 연결이 필요하며 서버에서 TLS 지원이 필요하다.
proto 파일에서 생성된 Go 코드에는 gRPC 서버와 클라이언트에 대한 인터페이스가 생성되어 있다. 해당 인터페이스를 구현하여 gRPC 서버를 작성하고 클라이언트로 요청하는 테스트를 작성할 예정이다.
먼저 gRPC 서버에 대한 코드 구현은 아래와 같다.
package main
import (
"context"
"fmt"
"sync"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework/v1"
)
type Rosie struct {
housework.UnimplementedRobotMaidServer
mu sync.Mutex
chores []*housework.Chore
}
func (r *Rosie) Add(_ context.Context, chores *housework.Chores) (*housework.Response, error) {
r.mu.Lock()
r.chores = append(r.chores, chores.Chores...)
r.mu.Unlock()
return &housework.Response{Message: "ok"}, nil
}
func (r *Rosie) Complete(_ context.Context, req *housework.CompleteRequest) (*housework.Response, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.chores == nil || req.ChoreNumber < 1 || int(req.ChoreNumber) > len(r.chores) {
return nil, fmt.Errorf("chore %d not found", req.ChoreNumber)
}
r.chores[req.ChoreNumber-1].Complete = true
return &housework.Response{Message: "ok"}, nil
}
func (r *Rosie) List(_ context.Context, _ *housework.Empty) (*housework.Chores, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.chores == nil {
r.chores = make([]*housework.Chore, 0)
}
return &housework.Chores{Chores: r.chores}, nil
}
protoc 컴파일러가 컴파일한 gRPC 서비스에 대해서 서버가 구현해야 한다. 반드시 Unimplemented로 시작하는 구조체를 임베딩하고 가지고 있어야지 인터페이스 변경에 대해 호환성을 가질 수 있다.
package main
import (
"crypto/tls"
"flag"
"fmt"
"log"
"net"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework/v1"
"google.golang.org/grpc"
)
var addr, certFn, keyFn string
func init() {
flag.StringVar(&addr, "address", "localhost:34443", "listen address")
flag.StringVar(&certFn, "cert", "cert.pem", "certificate file")
flag.StringVar(&keyFn, "key", "key.pem", "private key file")
}
func main() {
flag.Parse()
server := grpc.NewServer()
housework.RegisterRobotMaidServer(server, &Rosie{})
cert, err := tls.LoadX509KeyPair(certFn, keyFn)
if err != nil {
log.Fatal(err)
}
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Listening for TLS connections on %s ...", addr)
log.Fatal(
server.Serve(
tls.NewListener(
listener,
&tls.Config{
Certificates: []tls.Certificate{cert},
CurvePreferences: []tls.CurveID{tls.CurveP256},
MinVersion: tls.VersionTLS12,
NextProtos: []string{"h2"},
},
),
),
)
}
서버를 실행하는 main 함수에서는 gRPC 서버를 만들고, RobotMaid 서버를 등록한다. 그리고 인증서를 가지고 와 TLS 인증을 위한 TLS 리스너 형식으로 변경하면서 TLS config를 구성한다. gRPC 서버의 Serve() 함수를 호출하면서 TLS로 구성된 Listener를 넘겨 고정 인증서를 사용한 TLS 커넥션을 맺을 수 있다. 여기서 보안 정책(ALPN 강제)으로 인하여 NextProtos의 값을 명시적으로 HTTP/2로 명시하는 코드가 필수이다. 그렇지 않은 경우 에러가 발생한다.

gRPC 클라이언트 구현하여 서버 테스트
클라이언트에서 서버에 요청을 테스트하기 위해서 Go로 작성된 클라이언트를 생성한다.
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/hippo-an/tiny-go-challenges/netpro_99/encode_decode/housework/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var addr, caCertFn string
func init() {
flag.StringVar(&addr, "address", "localhost:34443", "server address")
flag.StringVar(&caCertFn, "ca-cert", "cert.pem", "CA certificate")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [flags] [add chore, ... |complete #]", filepath.Base(os.Args[0]))
flag.PrintDefaults()
}
}
func list(ctx context.Context, client housework.RobotMaidClient) error {
chores, err := client.List(ctx, new(housework.Empty))
if err != nil {
return err
}
if len(chores.Chores) == 0 {
fmt.Println("You have noting to do ")
return nil
}
fmt.Println("#\t[X]\t Description")
for i, chore := range chores.Chores {
c := " "
if chore.Complete {
c = "X"
}
fmt.Printf("%d\t[%s]\t%s\n", i+1, c, chore.Description)
}
return nil
}
func add(ctx context.Context, client housework.RobotMaidClient, s string) error {
chores := new(housework.Chores)
for _, chore := range strings.Split(s, ",") {
if desc := strings.TrimSpace(chore); desc != "" {
chores.Chores = append(chores.Chores, &housework.Chore{Description: desc})
}
}
var err error
if len(chores.Chores) > 0 {
_, err = client.Add(ctx, chores)
}
return err
}
func complete(ctx context.Context, client housework.RobotMaidClient, s string) error {
i, err := strconv.Atoi(s)
if err == nil {
_, err = client.Complete(ctx, &housework.CompleteRequest{ChoreNumber: int32(i)})
}
return err
}
func main() {
flag.Parse()
caCert, err := os.ReadFile(caCertFn)
if err != nil {
log.Fatal(err)
}
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(caCert); !ok {
log.Fatal("failed to add certificate to pool")
}
conn, err := grpc.NewClient(
addr,
grpc.WithTransportCredentials(
credentials.NewTLS(
&tls.Config{
CurvePreferences: []tls.CurveID{tls.CurveP256},
MinVersion: tls.VersionTLS12,
RootCAs: certPool,
},
),
),
)
if err != nil {
log.Fatal(err)
}
rosie := housework.NewRobotMaidClient(conn)
ctx := context.Background()
switch strings.ToLower(flag.Arg(0)) {
case "add":
err = add(ctx, rosie, strings.Join(flag.Args()[1:], " "))
case "complete":
err = complete(ctx, rosie, flag.Arg(1))
}
if err != nil {
log.Fatal(err)
}
err = list(ctx, rosie)
if err != nil {
log.Fatal(err)
}
}
클라이언트에서 호출하기 위해선 gRPC 컴파일로 생성된 코드에서 client 객체를 가지고 있어야 한다. 클라이언트 객체에서 적절한 로직에 맞게 해당하는 함수들을 호출할 수 있다.
클라이언트 연결에는 인증서를 고정하기 위해서 새로운 인증서 풀을 만들고, grpc.WithTransportCredentials 함수가 반환하는 grpc.DialOption 구조체를 이용하여 client connection을 생성할 수 있다. 이렇게 하면 고정된 인증서만 클라이언트에서 인증한다.
클라이언트 커넥션을 매개변수로 사용하여 gRPC 클라이언트를 생성하고, 함수를 호출할 수 있다.
인증서 생성
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout server/server.key \
-out server/server.crt \
-days 365 \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
-addext "extendedKeyUsage=serverAuth"
서버 실행
go run server/server.go server/rosie.go -cert server/server.crt -key server/server.key
클라이언트 실행
❯ go run client/client.go -ca-cert server/server.crt
You have noting to do
❯ go run client/client.go -ca-cert server/server.crt add Mop floors, Wash dishes
# [X] Description
1 [ ] Mop floors
2 [ ] Wash dishes
❯ go run client/client.go -ca-cert server/server.crt complete 2
# [X] Description
1 [ ] Mop floors
2 [X] Wash dishes'School > GNPS' 카테고리의 다른 글
| [GNPS] 챕터 13 로깅 & 메트릭s (0) | 2025.11.22 |
|---|---|
| [GNPS] 챕터 11 TLS 통신 보안 (0) | 2025.11.15 |
| [GNPS] 챕터 9 HTTP 서비스 작성! (0) | 2025.11.12 |
| [GNPS] 챕터 8 HTTP 클라이언트 작성 (0) | 2025.11.11 |
| [GNPS] 챕터 7 유닉스 도메인 소켓! (0) | 2025.11.08 |