Xây dựng một ứng dụng chat sử dụng gRPC và React.

Post on: 2023-05-10 23:39:10 | in: Golang
Để xây dựng ứng dụng chat sử dụng gRPC và React, bạn có thể làm theo các bước sau

Trong hướng dẫn này, chúng tôi sẽ hướng dẫn cách sử dụng gRPC bằng cách xây dựng một ứng dụng trò chuyện. Chúng tôi sẽ mô tả chi tiết cách bạn có thể thiết lập một dịch vụ gRPC và sau đó giao tiếp với nó từ một ứng dụng React. Việc gửi nhận các tin nhắn trò chuyện sẽ được xử lý bởi một dịch vụ gRPC. Hãy xem cách thực hiện điều này, nhưng trước tiên, chúng ta hãy tìm hiểu gRPC là gì.

gRPC qua một cái nhìn tổng quan

gRPC là một công nghệ truyền thông giữa các tiến trình được sử dụng để thực thi các phân-routines từ xa trong không gian địa chỉ khác nhau. Nó sử dụng khái niệm truyền thông tin để gửi tín hiệu cho một phân-routine đang tồn tại trong một hệ thống khác để thực thi. gRPC rất tuyệt vời vì nó tạo ra một dịch vụ máy chủ bằng một ngôn ngữ và dịch vụ này có thể được sử dụng từ một nền tảng khác bằng một ngôn ngữ khác. Dịch vụ máy chủ này chứa các phương thức có thể được gọi bởi các khách hàng gRPC từ bất kỳ đâu trên thế giới từ bất kỳ máy tính hoặc nền tảng nào.

gRPC có ba thành phần: Protocol Buffer, máy chủ và khách hàng. Protocol Buffer là một công cụ serialization mã nguồn mở được xây dựng bởi Google. gRPC sử dụng nó để mã hóa định dạng tin nhắn yêu cầu và phản hồi giữa máy chủ và khách hàng. Đây cũng là nơi xác định giao diện dịch vụ giữa máy chủ và khách hàng. Theo Kasun Indrasiri và Danesh Kuruppu trong gRPC: Up and Running, định nghĩa giao diện dịch vụ chứa thông tin về cách dịch vụ của bạn có thể được sử dụng bởi người tiêu dùng, các phương thức bạn cho phép người tiêu dùng gọi từ xa, các tham số và định dạng tin nhắn để sử dụng khi gọi những phương thức đó và vân vân.

Máy chủ, như chúng ta đã biết, chứa các phương thức/phân-routines/thủ tục, bất cứ điều gì mà bạn chọn để gọi nó. Những phương thức này thực hiện một hành động trên máy chủ. Khách hàng là nơi mà các phương thức trong máy chủ gRPC được gọi từ đó. Chúng ta đã biết gRPC là gì và làm thế nào nó hoạt động, bây giờ chúng ta tiếp tục xây dựng ứng dụng trò chuyện của chúng ta.

Ứng dụng chat

Ứng dụng trò chuyện của chúng ta sẽ là một ứng dụng nhóm giống như Discord, nơi mọi người có thể tham gia và thảo luận với các thành viên khác. Giao diện người dùng sẽ có dạng như sau

Chat app UI

Đây là trang tham gia, nơi người dùng sẽ phải nhập tên người dùng của mình trước khi tham gia vào cuộc trò chuyện.

Chat login Page

Đây là giao diện trò chuyện nơi nhóm có thể đọc và gửi tin nhắn. Giao diện người dùng đơn giản và dự án cũng vậy. Mục tiêu của chúng ta là giới thiệu cách chúng ta có thể sử dụng gRPC để tạo ra một ứng dụng trò chuyện. Hãy trước tiên tạo máy chủ. Máy chủ sẽ được xây dựng trên Nodejs.

gRPC - Cài đặt dịch vụ trò chuyện

Trước khi chúng ta bắt đầu tạo các dự án và chỉnh sửa các tệp, hãy xem mô hình và cấu trúc của ứng dụng trò chuyện của chúng ta. Điều này sẽ được thực hiện trong tệp Protobuf. Chúng ta sẽ tạo tệp chat.proto. Chúng ta sẽ cần các phương thức để:

  • người dùng sẽ gọi để tham gia cuộc trò chuyện nhóm
  • một phương thức sẽ được gọi bởi một người dùng để gửi một tin nhắn đến nhóm.
  • một phương thức sẽ mở một luồng dữ liệu trên máy chủ, sau đó máy khách sẽ lắng nghe luồng dữ liệu để lấy chúng. Luồng dữ liệu này sẽ là các tin nhắn được gửi bởi người dùng trong nhóm, vì vậy chúng ta cần phải truyền chúng đến tất cả các máy khách đang lắng nghe.
  • một phương thức sẽ trả về tất cả người dùng trong nhóm.

Trước tiên, trong chat.proto, chúng ta sẽ có một định dạng tin nhắn cho một tin nhắn trong cuộc trò chuyện:


syntax = "proto3";

message ChatMessage {
    string from = 1;
    string msg = 2;
    string time = 3;
}

Trường from, msg và time sẽ chứa tên của người dùng trong nhóm đã gửi tin nhắn, văn bản hoặc tin nhắn đã gửi và thời gian khi tin nhắn được gửi, tương ứng. Số trong các trường là các số độc nhất. Chúng được sử dụng để xác định khi các trường được mã hóa sang định dạng nhị phân. Các số trường được sử dụng để lấy các giá trị của các trường từ định dạng nhị phân của chúng. Các số trường có thể được gán trong khoảng từ 1 đến 536,870,911. Xem thêm về định dạng nhị phân ở đây.

Tiếp theo, chúng ta sẽ có một định dạng thông điệp cho một người dùng:


message User {
    string id = 1;
    string name = 2;
}

Các trường id và name chứa định danh duy nhất của một người dùng và tên người dùng tương ứng. Chúng ta tạo một tin nhắn rỗng vì chúng ta sẽ cần nó cho các phương thức không yêu cầu đối số hoặc giá trị trả về.

message Empty {}

Vì chúng ta sẽ trả về một danh sách người dùng, vì vậy chúng ta sẽ có một định dạng tin nhắn cho nó.


message UserList {
    repeated User users = 1;
}

Từ khóa "repeated" có nghĩa là trường đó là một danh sách hoặc một mảng. Vì vậy, trường người dùng "users" sẽ là một mảng các đối tượng User. Chúng ta sẽ có một phương thức tham gia mà người dùng sẽ gọi để tham gia một nhóm, điều này sẽ có một định dạng tin nhắn phản hồi để giữ cho trạng thái của việc tham gia

message JoinResponse {
    int32 error = 1;
    string msg = 2;
}

Lỗi sẽ giữ một giá trị để chỉ ra liệu yêu cầu tham gia đã thành công hay không. msg sẽ chứa thông báo phản hồi cho yêu cầu tham gia. Bây giờ, proto của chúng ta sẽ là thế này:


syntax = "proto3";

message ChatMessage {
    string from = 1;
    string msg = 2;
    string time = 3;
}

message User {
    string id = 1;
    string name = 2;
}

message Empty {}

message UserList {
    repeated User users = 1;
}

message JoinResponse {
    int32 error = 1;
    string msg = 2;
}

message ReceiveMsgRequest {
    string user = 1;
}

service ChatService {
    rpc join(User) returns (JoinResponse) {}
    rpc sendMsg(ChatMessage) returns (Empty) {}
    rpc receiveMsg(Empty) returns (stream ChatMessage) {}
    rpc getAllUsers(Empty) returns (UserList) {}
}

Từ khóa service biểu thị dịch vụ gRPC được proto xuất. Các dịch vụ gRPC trong một tệp proto sẽ có các phương thức sẵn có cho các khách hàng.

Các phương thức này được đặt bằng từ khóa rpc. Vì vậy, ở trên chúng ta có bốn phương thức. Phương thức join sẽ lấy một đối tượng User làm tham số để lấy chi tiết người dùng từ đó và thêm người dùng vào trò chuyện nhóm. Nó sẽ trả về một đối tượng JoinResponse. Phương thức sendMsg gửi tin nhắn đến trò chuyện nhóm. Nó lấy định dạng tin nhắn ChatMessage làm đối số để có thể lấy thông tin tin nhắn và người gửi từ các trường, nó trả về một đối tượng rỗng. Phương thức receiveMsg thiết lập một luồng tin nhắn trên máy chủ. Từ khóa stream biểu thị một luồng dữ liệu được tạo và phát ra đến người dùng cuối. Ở đây, receiveMsg sẽ tạo và phát ra các luồng tin nhắn chat ChatMessage, khách hàng sẽ lắng nghe luồng để nhận các cuộc trò chuyện được phát ra đến kênh. Điều này làm cho việc nhận các văn bản được gửi bởi người dùng trở nên có thể thực hiện ngay lập tức. Phương thức getAllUsers trả về các người dùng trong trò chuyện.

Bây giờ, chúng ta xây dựng một dự án Node và viết chat.proto này với nội dung của nó.


mkdir grpc-chat

Bạn hãy chuyển đến thư mục đó bằng lệnh:

cd grpc-chat

Khởi tạo một môi trường Node

npm init -y

Tạo tệp chat.proto:

touch chat.proto

Hãy sao chép và dán nội dung của tệp proto mà chúng ta đã tạo trước đó vào trong thư mục vừa tạo.

Tiếp theo, chúng ta cài đặt các phụ thuộc:

  • grpc: Thư viện gRPC cho Nodejs runtime.
  • @grpc/proto-loader: Một gói tiện ích để tải tệp .proto để sử dụng với gRPC, sử dụng gói Protobuf.js mới nhất.

Chạy lệnh sau để cài đặt các phụ thuộc:

yarn add grpc @grpc/proto-loader
# or
npm i grpc @grpc/proto-load

Bây giờ chúng ta tạo tệp server.js:

touch server.js

File này sẽ chứa mã của máy chủ gRPC. Chúng ta sẽ nhập các phụ thuộc của gRPC, sau đó thiết lập đường dẫn cho chat.proto và địa chỉ URL của máy chủ để trỏ tới "0.0.0.0:9090":


const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");

const PROTO_PATH = "chat.proto";
const SERVER_URI = "0.0.0.0:9090";

Chúng ta sẽ sử dụng một cơ sở dữ liệu trong bộ nhớ sẽ là các mảng trong tương lai. Thực ra, chúng ta có thể kết nối một máy chủ gRPC với các cơ sở dữ liệu như MongoDB, MySQL, vv, nhưng điều đó không nằm trong phạm vi của bài viết này. Vì vậy, chúng ta tạo một mảng trống để chứa người dùng trong nhóm usersInchat và các quan sát viên của writable stream máy chủ người dùng. Sau đó, chúng ta tải chat.proto và tải các định nghĩa của gói.


const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");

const PROTO_PATH = "chat.proto";
const SERVER_URI = "0.0.0.0:9090";

const usersInChat = [];
const observers = [];

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);

Tiếp theo, chúng ta thiết lập các hàm xử lý cho các phương thức trong ChatService.


// we'll implement the handlers here
const join = (call, callback) => {
  const user = call.request;

  // check username already exists.
  const userExist = usersInChat.find((_user) => _user.name == user.name);
  if (!userExist) {
    usersInChat.push(user);
    callback(null, {
      error: 0,
      msg: "Success",
    });
  } else {
    callback(null, { error: 1, msg: "User already exist." });
  }
};

Đối số gọi chứa các tham số yêu cầu từ khách hàng và các tham số khác được thiết lập bởi gRPC. Các tham số được thiết lập bởi khách hàng nằm trong đối tượng yêu cầu. Vì vậy, chúng ta lấy đối tượng người dùng từ đối tượng yêu cầu và thêm nó vào mảng usersInChat.


const getAllUsers = (call, callback) => {
  callback(null, { users: usersInChat });
};

Hàm này trả về danh sách các user trong mảng usersInChat cho client. callback là một hàm được gọi để gửi phản hồi lại cho client gọi method. Tham số đầu tiên của hàm callback xác định xem có xảy ra lỗi không, tham số thứ hai là dữ liệu phản hồi. Vì vậy, ở đây, client sẽ nhận được danh sách các user trong thuộc tính users của đối tượng được trả về.


const receiveMsg = (call, callback) => {
  observers.push({
    call,
  });
};

Phương thức này được gọi để thiết lập một luồng dữ liệu trên máy chủ để khách hàng có thể lắng nghe dữ liệu được phát ra trong luồng. Ở đây, đối số gọi sẽ là một ServerWritableStream instance. Nó có một phương thức write mà chúng ta gọi để phát dữ liệu trong luồng. Điều này sẽ được thực hiện trên máy chủ ở đây vì khách hàng đã gọi phương thức này sẽ lắng nghe dữ liệu được phát ra. Do đó, khi phương thức write được gọi, dữ liệu được gửi xuống hàm xử lý "data" của khách hàng. Chúng ta đẩy đối số gọi vào mảng observers.


const sendMsg = (call, callback) => {
  const chatObj = call.request;
  observers.forEach((observer) => {
    observer.call.write(chatObj);
  });
  callback(null, {});
};

Phương thức này gửi một tin nhắn đến nhóm chat. Nó trích xuất đối tượng tin nhắn từ đối tượng call.request và lặp qua mảng observers. Mảng observers chứa các trường hợp ServerWritableStream từ lời gọi receiveMsg.

Bây giờ, với mỗi ServerWritableStream trong mảng observers, ta sẽ gọi phương thức write với tham số là đối tượng chat message trong chatObj. Điều này sẽ khiến ServerWritableStream phát ra message của cuộc trò chuyện, vì vậy các client đang lắng nghe sự kiện phát ra sẽ nhận được tin nhắn. Ở phần frontend, client sẽ nhận được tin nhắn phát ra và ta sẽ sử dụng nó để cập nhật giao diện người dùng để xem tin nhắn và người gửi nó.

Chúng ta đã hoàn tất các trình xử lý phương thức ChatService. Bây giờ, chúng ta sẽ thiết lập máy chủ gRPC

const server = new grpc.Server();

Máy chủ chứa một thể hiện grpc.Server. Tiếp theo, chúng ta thêm dịch vụ ChatService vào máy chủ bằng cách sử dụng phương thức addService(). Tham số đầu tiên của addService là mô tả dịch vụ, trong trường hợp này, chúng ta sẽ lấy mô tả dịch vụ ChatService từ protoDescriptor. Tham số thứ hai sẽ là một bản đồ tên phương thức đến cài đặt phương thức cho dịch vụ được cung cấp.


server.addService(protoDescriptor.ChatService.service, {
  join,
  sendMsg,
  getAllUsers,
  receiveMsg,
});

Tiếp theo, chúng ta sẽ ràng buộc và bắt đầu máy chủ của chúng ta trên cổng 9090.

server.bind(SERVER_URI, grpc.ServerCredentials.createInsecure());

server.start();
console.log("Server is running!");

Đó là tất cả. Bây giờ chúng ta có thể chạy máy chủ:

$ node server
Server is running!

Đây là mã đầy đủ của máy chủ:


const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");

const PROTO_PATH = "chat.proto";
const SERVER_URI = "0.0.0.0:9090";

const usersInChat = [];
const observers = [];

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);

// we'll implement the handlers here
const join = (call, callback) => {
  const user = call.request;

  // check username already exists.
  const userExiist = usersInChat.find((_user) => _user.name == user.name);
  if (!userExiist) {
    usersInChat.push(user);
    callback(null, {
      error: 0,
      msg: "Success",
    });
  } else {
    callback(null, { error: 1, msg: "user already exist." });
  }
};

const sendMsg = (call, callback) => {
  const chatObj = call.request;
  observers.forEach((observer) => {
    observer.call.write(chatObj);
  });
  callback(null, {});
};

const getAllUsers = (call, callback) => {
  callback(null, { users: usersInChat });
};

const receiveMsg = (call, callback) => {
  observers.push({
    call,
  });
};

const server = new grpc.Server();

server.addService(protoDescriptor.ChatService.service, {
  join,
  sendMsg,
  getAllUsers,
  receiveMsg,
});

server.bind(SERVER_URI, grpc.ServerCredentials.createInsecure());

server.start();
console.log("Server is running!");

Ứng dụng của chúng ta sẽ là một ứng dụng React.js, điều này có nghĩa là nó sẽ chạy trong trình duyệt.

Trình duyệt phải kết nối với dịch vụ gRPC thông qua một proxy đặc biệt. Proxy này là một quá trình có thể gửi các cuộc gọi HTTP/2. Vì vậy, chúng ta gửi một cuộc gọi HTTP 1.1 đến proxy từ trình duyệt, proxy nhận được và gọi máy chủ gRPC thông qua HTTP/2, gửi URL yêu cầu và tham số kèm theo. Sau đó, nó nhận được phản hồi từ máy chủ gRPC thông qua HTTP/2, phản hồi này được gửi đến khách hàng thông qua HTTP 1.1 bởi proxy. Quá trình proxy lý tưởng cho điều này là Envoy.

Cài đặt proxy Envoy

Chúng ta sẽ thiết lập proxy Envoy trong một hình ảnh Docker. Nhưng trước khi làm điều đó, hãy thiết lập dự án React của chúng ta vì đó là nơi các tệp cấu hình Envoy sẽ nằm.

Tạo dự án React

Chạy lệnh dưới đây để tạo một dự án React:


create-react-app grpc-chat-react

Cài đặt các phụ thuộc:

  • grpc-web: cung cấp một thư viện JavaScript cho phép khách hàng trình duyệt truy cập vào dịch vụ gRPC.
  • google-protobuf: chứa thư viện thời gian chạy Protocol Buffers JavaScript.

yarn add grpc-web google-protobuf
# or
npm i grpc-web google-protobuf

Bây giờ, sao chép chat.proto từ dự án grpc-chat của máy chủ vào dự án React.js này. Sao chép nó vào thư mục src/. Chúng ta cần tệp này vì chúng ta sẽ cần nó để tạo ra các tệp .js khách hàng gRPC để chúng ta có thể sử dụng chúng để gọi các phương thức gRPC. Đầu tiên, hãy tạo một envoy.yaml trong thư mục gốc của thư mục React.js này. Đây là tệp cấu hình của Envoy mà Docker sẽ sử dụng để cài đặt một quá trình Envoy đang chạy. Trong tệp envoy.yaml, dán mã cấu hình sau đây


admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 8080 }
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                codec_type: auto
                stat_prefix: ingress_http
                stream_idle_timeout: 0s
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route:
                            cluster: chat_service
                            max_grpc_timeout: 0s
                            max_stream_duration:
                              grpc_timeout_header_max: 0s
                      cors:
                        allow_origin_string_match:
                          - prefix: "*"
                        allow_methods: GET, PUT, DELETE, POST, OPTIONS
                        allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                        max_age: "1728000"
                        expose_headers: custom-header-1,grpc-status,grpc-message
                http_filters:
                  - name: envoy.filters.http.grpc_web
                  - name: envoy.filters.http.cors
                  - name: envoy.filters.http.router
  clusters:
    - name: chat_service
      connect_timeout: 0.25s
      type: logical_dns
      http2_protocol_options: {}
      lb_policy: round_robin
      # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
      load_assignment:
        cluster_name: cluster_0
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: host.docker.internal
                      port_value: 9090

Đó là rất nhiều cấu hình, chúng ta sẽ xem xét các cấu hình cần thiết.


admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

Điều này thiết lập địa chỉ URL và cổng của hình ảnh Docker.


static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 8080 }

Điều này thiết lập địa chỉ URL mà khách hàng trình duyệt gRPC sẽ điều hướng các cuộc gọi HTTP/1.1 của mình. Ví dụ, để gọi phương thức join, URL sẽ là như sau:


localhost:8080/ChatService/join

Vì vậy, 0.0.0.0:8080 là địa chỉ của quá trình Envoy đang chạy trong hình ảnh Docker.


clusters:
  - name: chat_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
    load_assignment:
      cluster_name: cluster_0
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: host.docker.internal
                    port_value: 9090

Điều này thiết lập địa chỉ URL của máy chủ gRPC nơi tiến trình lập pháp Envoy sẽ điều hướng các cuộc gọi HTTP/2 của mình. Hãy xem xét rằng cổng địa chỉ là giống như cổng máy chủ gRPC của chúng ta đang chạy trên 9090. Địa chỉ được thiết lập thành host.docker.internal vì Docker sẽ đặt địa chỉ của nó cho máy chủ gRPC để chúng ta sử dụng địa chỉ Docker. Vì vậy, hình ảnh tư duy của các cuộc gọi HTTP sẽ là như sau:

  • trình duyệt gọi các phương thức ChatService trên cổng 8080 thông qua HTTP 1.1.
  • Docker đang chạy trên 9091 nhận cuộc gọi và thực hiện cuộc gọi HTTP/2 đến gRPC đang chạy trên 9090 truyền địa chỉ URL và các thông số yêu cầu.
  • máy chủ gRPC gửi phản hồi cho Envoy thông qua HTTP/2 trên 9091, Envoy gửi phản hồi cho khách hàng trình duyệt qua HTTP 1.1 trên cổng 3000.

Hãy tạo một Dockerfile ở cùng một vị trí với envoy.yaml.


FROM envoyproxy/envoy-dev:latest
COPY envoy.yaml /etc/envoy/envoy.yaml
RUN chmod go+r /etc/envoy/envoy.yaml

Tiếp theo, chúng ta sẽ tạo ảnh Docker bằng lệnh sau:


docker build -t grpc-web-react .

Lệnh trên tạo một ảnh Docker với tên grpc-web-react. Bây giờ, chúng ta chạy Docker bằng lệnh sau:


docker run -d --name grpc-web-react -p 8080:8080 -p 9901:9901 grpc-web-react

Chúng ta sẽ thấy ảnh Docker của mình đang chạy:

Docker image

Đặt gRPC web protoc

Bây giờ chúng ta sẽ phải biên dịch tệp chat.proto bằng trình biên dịch proto. Việc này để tạo mã JavaScript tương đương với mã Protobuf chúng ta có trong chat.proto, điều này được thực hiện để chúng ta có thể truy cập các loại tin nhắn, dịch vụ và gọi các phương thức bằng JavaScript. Mỗi ngôn ngữ có trình biên dịch proto riêng của nó, chúng ta có thể sử dụng nó để tạo mã nguồn gRPC của ngôn ngữ đó. Chúng ta cần cài đặt plugin Proto và trình biên dịch Protoc cho JavaScript.

Để biết thêm thông tin về cách cài đặt các công cụ Proto, hãy truy cập trang GitHub của grpc-web. Sau khi hoàn tất tất cả, chúng ta sẽ có trình biên dịch protoc được truy cập toàn cầu từ Terminal của chúng ta.

Vì vậy, chúng ta biên dịch tệp chat.proto, chạy lệnh sau:


protoc -I=. src/chat.proto --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.

Điều này sẽ biên dịch tệp chat.proto và tạo các tệp:

  • Điều này sẽ biên dịch tệp chat.proto và tạo các tệp:
  • chat_grpc_web_pb.js: Tệp này sẽ xuất ChatServiceClient. Các dịch vụ trong tệp proto được xuất ở đây. Các dịch vụ được xuất với Client được đính kèm vào tên của chúng.

Bây giờ, chúng ta sẽ build các component UI.

Build the UI

Chúng ta sẽ có các thành phần trình bày và trang. Các thành phần trình bày là các thành phần ngu dốt mà chỉ hiển thị dữ liệu được truyền vào. Các thành phần trang là các thành phần được gắn vào một tuyến đường, chúng được hiển thị khi tuyến đường mà chúng được gắn vào được điều hướng đến trên trình duyệt.

Các thành phần trình bày:

  • Header: Đây sẽ hiển thị tiêu đề.
  • Chat: Thành phần này sẽ hiển thị các tin nhắn trò chuyện.
  • UsersList: Đây sẽ hiển thị danh sách người dùng trong nhóm.

Các thành phần trang:

  • ChatPage: Trang này sẽ hiển thị các thành phần để nhập, gửi và hiển thị tin nhắn.

Hãy tạo các thư mục và tệp cho các thành phần:


mkdir src/pages
mkdir src/pages/ChatPage

touch src/pages/ChatPage/index.js
touch src/pages/ChatPage.css

mkdir src/pages/Join
touch src/pages/Join/index.js

mkdir src/components

mkdir src/components/Header
touch src/pages/Header/index.js
touch src/pages/Header/Header.css

mkdir src/components/Chat
touch src/pages/Chat/index.js
touch src/pages/Chat/Chat.css

mkdir src/components/UsersList
touch src/pages/UsersList/index.js
touch src/pages/UsersList/UsersList.css

Chúng ta bắt đầu từ component cơ bản App.js.

import "./App.css";
import Header from "./components/Header";

import { User, JoinResponse } from "./chat_pb";
import { ChatServiceClient } from "./chat_grpc_web_pb";
import ChatPage from "./pages/ChatPage";
import { useState, useRef } from "react";

const client = new ChatServiceClient("http://localhost:8080", null, null);

export default function App() {
  const inputRef = useRef(null);
  const [submitted, setSubmitted] = useState(null);

  function joinHandler() {
    const _username = inputRef.current.value;

    const user = new User();
    user.setId(Date.now());
    user.setName(_username);

    client.join(user, null, (err, response) => {
      if (err) return console.log(err);
      const error = response.getError();
      const msg = response.getMsg();

      if (error === 1) {
        setSubmitted(true);
        return;
      }
      window.localStorage.setItem("username", _username.toString());
      setSubmitted(true);
    });
  }

  function renderChatPage() {
    return <ChatPage client={client} />;
  }

  function renderJoinPage() {
    return (
      <div>
        <div>
          <h1>Join Chat As...</h1>
        </div>
        <div style={{ padding: "10px 0" }}>
          <input
            style={{ fontSize: "1.3rem" }}
            type="text"
            ref={inputRef}
            placeholder="Your username..."
          />
        </div>
        <div>
          <button
            onClick={joinHandler}
            style={{
              padding: "7px 38px",
              fontSize: "1.2em",
              boxSizing: "content-box",
              borderRadius: "4px",
            }}
          >
            Join
          </button>
        </div>
      </div>
    );
  }

  return (
    <>
      <head>
        <title>ChatApp</title>
        <link rel="icon" href="/favicon.ico" />
      </head>
      <Header />
      <div className="container">
        <main className="main">
          {submitted ? renderChatPage() : renderJoinPage()}
        </main>
      </div>
    </>
  );
}

 

Trước tiên, chúng ta sẽ import các module cần thiết. Ta import styling trong file App.css. Tiếp theo, ta import Header component, sau đó, message type User được định nghĩa trong file proto được export từ chat_pb.js. Ta cũng import ChatServiceClient từ chat_grpc_web_pb.js. Tiếp theo, ta import component ChatPage và hook useState.

Chúng ta thiết lập một client gRPC bằng cách khởi tạo ChatServiceClient và truyền localhost:8080 vào hàm tạo. Thể hiện được lưu trong biến client. Đây là điều chúng ta sẽ sử dụng để gọi các phương thức trong ChatService. Bên trong component App, ta có một state submitted, nó giữ trạng thái boolean để chỉ ra liệu yêu cầu tham gia có thành công hay không. Chúng ta có hai phương thức renderChatPage và renderJoinPage. Phương thức renderChatPage render giao diện cho trang chat, trong khi đó, phương thức renderJoinPage sẽ render giao diện để nhận đầu vào cho việc tham gia nhóm.

Bạn có thể thấy rằng trong giao diện renderJoinPage, có một hộp nhập sẽ lấy tên của người dùng, nút Join khi được nhấn sẽ gọi hàm joinHandler. Hàm joinHandler sẽ gọi join() trong client. Điều này sẽ kích hoạt phương thức join trong máy chủ gRPC. Bạn có thể thấy rằng User instance với id và name được thiết lập được truyền làm tham số cho cuộc gọi join(). Trong hàm callback, submitted state được thiết lập nếu thành công và component ChatPage được render.

Mở file App.css, xóa nội dung và lưu lại. Bây giờ, chúng ta sẽ xem xét component ChatPage.

ChatPage

Mở file ChatPage/index.js và dán đoạn mã sau:


import Chat from "./../../components/Chat";
import UsersList from "./../../components/UsersList";
import "./ChatPage.css";
import { ChatMessage, ReceiveMsgRequest, Empty } from "./../../chat_pb";
import { useEffect, useState } from "react";

export default function ChatPage({ client }) {
  const [users, setUsers] = useState([]);
  const [msgList, setMsgList] = useState([]);
  const username = window.localStorage.getItem("username");

  useEffect(() => {
    const strRq = new ReceiveMsgRequest();
    strRq.setUser(username);

    var chatStream = client.receiveMsg(strRq, {});
    chatStream.on("data", (response) => {
      const from = response.getFrom();
      const msg = response.getMsg();
      const time = response.getTime();

      if (from === username) {
        setMsgList((oldArray) => [
          ...oldArray,
          { from, msg, time, mine: true },
        ]);
      } else {
        setMsgList((oldArray) => [...oldArray, { from, msg, time }]);
      }
    });

    chatStream.on("status", function (status) {
      console.log(status.code, status.details, status.metadata);
    });

    chatStream.on("end", () => {
      console.log("Stream ended.");
    });
  }, []);

  useEffect(() => {
    getAllUsers();
  }, []);

  function getAllUsers() {
    client.getAllUsers(new Empty(), null, (err, response) => {
      let usersList = response?.getUsersList() || [];
      usersList = usersList
        .map((user) => {
          return {
            id: user.array[0],
            name: user.array[1],
          };
        })
        .filter((u) => u.name !== username);
      setUsers(usersList);
    });
  }

  function sendMessage(message) {
    const msg = new ChatMessage();
    msg.setMsg(message);
    msg.setFrom(username);
    msg.setTime(new Date().toLocaleString());

    client.sendMsg(msg, null, (err, response) => {
      console.log(response);
    });
  }
  return (
    <div className="chatpage">
      <div className="userslist-section">
        <div
          style={{ paddingBottom: "4px", borderBottom: "1px solid darkgray" }}
        >
          <div>
            <button onClick={getAllUsers}>REFRESH</button>
          </div>
          <div>
            <span>
              Logged in as <b>{username}</b>
            </span>
          </div>
        </div>
        <UsersList users={users} />
      </div>
      <div className="chatpage-section">
        <Chat msgList={msgList} sendMessage={sendMessage} />
      </div>
    </div>
  );
}
 

Chúng ta có những state để lưu trữ tất cả người dùng trong nhóm và các tin nhắn đã gửi. Tên của người dùng được lấy từ localStorage và được lưu trữ trong biến username.

Phương thức callback trong useEffect hook gọi phương thức receiveMsg, vì phương thức này trả về một stream từ server. Chúng ta lắng nghe cho stream dữ liệu được phát ra từ server bằng cách gọi phương thức then on() và truyền chuỗi "data" vào tham số đầu tiên và một hàm callback vào tham số tiếp theo. Điều này lắng nghe cho dữ liệu trên server-side streaming, dữ liệu được phát ra bởi server được lấy ra từ đối số response trong hàm callback. Đối tượng response này sẽ là một ChatMessage, vì vậy chúng ta lấy người gửi tin nhắn, nội dung tin nhắn và thời gian gửi bằng cách gọi lần lượt getFrom(), getMsg() và getTime(). Để phân biệt một tin nhắn của người dùng với các người dùng khác trong cuộc trò chuyện, chúng ta kiểm tra from so sánh với người dùng hiện tại trong username và thêm thuộc tính mine vào đối tượng, sau đó, chúng ta thiết lập đối tượng tin nhắn cho mảng state msgList.

Hàm getAllUsers lấy tất cả người dùng trong cuộc trò chuyện, xem nó gọi hàm getAllUsers() trong dịch vụ gRPC và đặt phản hồi vào trạng thái msgList. Hàm sendMessage gửi tin nhắn chat đến máy chủ. Đối số message chứa nội dung tin nhắn sẽ được gửi. Nó tạo đối tượng ChatMessage của mình và gọi sendMsg với nó. Điều này kích hoạt phương thức sendMsg trên máy chủ gRPC. Giao diện người dùng hiển thị một nút mà chúng ta có thể nhấp để làm mới danh sách người dùng, hiển thị tên người dùng và vẽ các thành phần UsersList và Chat, chuyển các người dùng đến thành phần UsersList thông qua đầu vào users và đối số msgList và hàm sendMessage cho thành phần Chat như các props đầu vào.

Thêm các style vào ChatPage.css.


.chatpage {
  display: flex;
  border: 1px solid darkgrey;
  height: 100%;
}

.userslist-section {
  flex: 1;
}

.chatpage-section {
  flex: 2;
  border: 1px solid darkgrey;
}

UsersList

Mở tệp UsersList/index.js và dán đoạn mã sau:


import "./UsersList.css";

export default function UsersList({ users = [] }) {
  return (
    <div className="userslist">
      {users?.map((user, i) => {
        return <UserCard user={user} key={i} />;
      })}
    </div>
  );
}

function UserCard({ user }) {
  return (
    <div className="usercard">
      <div className="usercard-img"></div>
      <div>
        <div className="usercard-name">
          <h3>{user?.name || "No Username"}</h3>
        </div>
      </div>
    </div>
  );
}

Nó giải cấu trúc mảng người dùng từ các props của nó. Sau đó, nó vẽ nó. Thêm kiểu dáng vào UsersList.css:


.usercard {
  display: flex;
  align-items: center;
  margin: 4px;
  padding: 4px;
  border-bottom: 1px solid darkgray;
  cursor: pointer;
}

.usercard-img {
  width: 52px;
  height: 52px;
  background-color: darkgray;
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
  margin-right: 9px;
  /*flex: 1 100%;*/
  border-radius: 50%;
}

.usercard-name h3 {
  margin: 2px;
}
Xây dựng một ứng dụng chat sử dụng gRPC và React phần 2
Tag: Golang nâng cao