0

Bài 4: Định nghĩa gRPC Service và Triển khai mã nguồn Server/Client trong Golang

Ở Bài 3, chúng ta đã biên dịch thành công file user.proto sang hai tệp mã nguồn Go: user.pb.go (chứa cấu trúc dữ liệu) và user_grpc.pb.go (chứa các interface giao tiếp gRPC).

Trong bài học này, chúng ta sẽ sử dụng những đoạn code do protoc sinh ra để hiện thực hóa một gRPC Server (nhận request, xử lý logic và trả về kết quả) và một gRPC Client (gửi request từ phía máy khách) hoàn chỉnh bằng Golang.

1. Giải phẫu các Interface được sinh ra

Trước khi viết code, hãy nhìn vào file user_grpc.pb.go do plugin tạo ra. Bạn sẽ thấy hai thành phần cốt lõi:

  1. UserServiceServer (Interface cho Server): Định nghĩa tập hợp các hàm RPC mà server phải cài đặt.

  2. UserServiceClient (Interface cho Client): Cung cấp các phương thức để gọi từ xa giống như gọi hàm cục bộ.

2. Triển khai gRPC Server (server/main.go)

Đầu tiên, chúng ta tạo một ứng dụng Server lắng nghe trên cổng TCP 50051, đăng ký service UserService và xử lý yêu cầu tìm kiếm người dùng.

Tạo thư mục server/main.go:

Go

package main

import (
	"context"
	"fmt"
	"log"
	"net"

	// Nhớ thay thế "github.com/your-username/my-grpc-project" bằng module path thực tế trong go.mod của bạn
	"github.com/your-username/my-grpc-project/pb"

	"google.golang.org/grpc"
)

// 1. Định nghĩa struct Server kế thừa từ UnimplementedUserServiceServer 
// (Bắt buộc phải nhúng struct này để đảm bảo tính tương thích ngược khi proto file mở rộng)
type server struct {
	pb.UnimplementedUserServiceServer
}

// 2. Hiện thực hóa (Implement) phương thức GetUser đã định nghĩa trong file .proto
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
	log.Printf("[Server] Nhận yêu cầu tìm kiếm User ID: %d", req.GetId())

	// Giả lập logic nghiệp vụ tìm kiếm trong cơ sở dữ liệu
	// Trong thực tế, bạn sẽ gọi vào Database layer ở đây
	return &pb.UserResponse{
		Id:    req.GetId(),
		Name:  "Hiếu Backend",
		Email: "hieu.backend@example.com",
	}, nil
}

func main() {
	// Lắng nghe kết nối TCP tại cổng 50051
	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("Không thể khởi động TCP listener trên cổng 50051: %v", err)
	}

	// Khởi tạo một gRPC Server mới
	grpcServer := grpc.NewServer()

	// Đăng ký struct server của chúng ta vào gRPC Server tổng
	pb.RegisterUserServiceServer(grpcServer, &server{})

	log.Println("[Server] gRPC Server đang chạy và sẵn sàng nhận request tại port 50051...")
	
	// Bắt đầu phục vụ các request đến
	if err := grpcServer.Serve(listener); err != nil {
		log.Fatalf("Lỗi khi chạy gRPC Server: %v", err)
	}
}

3. Triển khai gRPC Client (client/main.go)

Bây giờ, chúng ta viết một đoạn mã Client để kết nối tới server vừa tạo, gửi đi một request chứa id = 123 và nhận về thông tin user.

Tạo thư mục client/main.go:

Go

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/your-username/my-grpc-project/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	// 1. Kết nối tới gRPC Server (Sử dụng insecure credentials vì môi trường dev chưa bật TLS)
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Không thể kết nối tới gRPC server: %v", err)
	}
	defer conn.Close()

	// 2. Khởi tạo gRPC Client từ connection
	client := pb.NewUserServiceClient(conn)

	// 3. Tạo context có thời gian timeout (1 giây) để tránh request bị treo vô tận
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	// 4. Chuẩn bị request và gọi hàm GetUser từ xa như gọi hàm cục bộ
	req := &pb.GetUserRequest{Id: 123}
	res, err := client.GetUser(ctx, req)
	if err != nil {
		log.Fatalf("Lỗi khi gọi RPC GetUser: %v", err)
	}

	// 5. In kết quả nhận được từ Server
	fmt.Println("--------------------------------------------------")
	fmt.Println("[Client] Nhận phản hồi thành công từ gRPC Server:")
	fmt.Printf("-> User ID   : %d\n", res.GetId())
	fmt.Printf("-> Full Name : %s\n", res.GetName())
	fmt.Printf("-> Email     : %s\n", res.GetEmail())
	fmt.Println("--------------------------------------------------")
}

4. Chạy và Kiểm thử ứng dụng

Để kiểm thử, bạn mở hai cửa sổ Terminal riêng biệt trên máy tính:

  • Terminal 1 (Chạy Server):

    Bash

    go run server/main.go
    
    

    (Bạn sẽ thấy thông báo server đang lắng nghe tại port 50051).

  • Terminal 2 (Chạy Client):

    Bash

    go run client/main.go
    
    

    (Bạn sẽ thấy client kết nối thành công, gửi request và nhận về thông tin user được in đẹp mắt).

Tổng kết

Chúng ta đã hoàn thành việc xây dựng một luồng giao tiếp gRPC khép kín từ định nghĩa (.proto), biên dịch (protoc), viết Server cho đến viết Client bằng Golang.

Tuy nhiên, trong các dự án lớn, việc gõ các lệnh protoc dài dòng hay phải chạy thủ công từng file server/client là rất bất tiện. Làm sao để tự động hóa toàn bộ quy trình biên dịch Protobuf chỉ bằng một câu lệnh duy nhất?

Đó chính là nội dung của Bài 5 tiếp theo.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí