我的gRPC之旅。使用gRPC一元通信模式和雙向流通信模式寫一個簡單的控制台聊天室。實現創建用戶和實時聊天兩個功能,不考慮高性能。複習了記憶體同步訪問Sync包的使用。用切片緩存聊天記錄,新用戶可以同步聊天記錄。 ...
效果
使用gRPC一元通信模式和雙向流通信模式寫一個簡單的控制台聊天室。實現創建用戶和實時聊天兩個功能,不考慮高性能。複習了記憶體同步訪問Sync包的使用。用切片緩存聊天記錄,新用戶可以同步聊天記錄。
PS C:\Users\小能喵喵喵\Desktop\Go\gRPC\chatroom> tree /f
├───client
│ │ go.mod
│ │ go.sum
│ │ main.go
│ │
│ └───chatroom
│ chat_room.pb.go
│ chat_room_grpc.pb.go
│
├───proto
│ │ chat_room.pb.go
│ │ chat_room.proto
│ │ chat_room_grpc.pb.go
│ │
│ └───google
│ └───protobuf
│ wrappers.proto
│
└───server
│ go.mod
│ go.sum
│ main.go
│ service.go
│
└───chatroom
chat_room.pb.go
chat_room_grpc.pb.go
server.code-workspace
Proto
syntax = "proto3";
import "google/protobuf/wrappers.proto";
package chatroom;
option go_package=".";
service ChatRoom{
rpc login(User) returns(google.protobuf.StringValue);
rpc chat(stream ChatMessage) returns(stream ChatMessage);
}
message User{
string id = 1;
string name = 2;
}
message ChatMessage{
string id = 1;
string name = 2;
uint64 time = 3;
string content = 4;
}
protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./chat_room.proto
Server
service.go
package main
import (
"context"
"fmt"
"sync"
"time"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
pb "wolflong.com/chatroom_server/chatroom"
)
// ^ 實現服務
type service struct {
pb.UnimplementedChatRoomServer
chatMessageCache []*pb.ChatMessage
userMap sync.Map
L sync.RWMutex
}
var (
workers map[pb.ChatRoom_ChatServer]pb.ChatRoom_ChatServer = make(map[pb.ChatRoom_ChatServer]pb.ChatRoom_ChatServer)
)
// ^ 實現login用戶註冊方法
func (s *service) Login(ctx context.Context, in *pb.User) (*wrappers.StringValue, error) {
in.Id = uuid.New().String()
if _, ok := s.userMap.Load(in.Id); ok {
return nil, status.Errorf(codes.AlreadyExists, "已有同名用戶,請換個用戶名")
}
s.userMap.Store(in.Id, in)
go s.sendMessage(nil, &pb.ChatMessage{Id: "server", Content: fmt.Sprintf("%v 加入聊天室", in.Name), Time: uint64(time.Now().Unix())})
// some work...
return &wrappers.StringValue{Value: in.Id}, status.New(codes.OK, "").Err()
}
// ^ 實現聊天室
func (s *service) Chat(stream pb.ChatRoom_ChatServer) error {
if s.chatMessageCache == nil {
s.chatMessageCache = make([]*pb.ChatMessage, 0, 1024)
}
workers[stream] = stream
for _, v := range s.chatMessageCache {
stream.Send(v)
}
s.recvMessage(stream)
return status.New(codes.OK, "").Err()
}
func (s *service) recvMessage(stream pb.ChatRoom_ChatServer) {
md, _ := metadata.FromIncomingContext(stream.Context())
for {
mes, err := stream.Recv()
if err != nil {
s.L.Lock()
delete(workers, stream)
s.L.Unlock()
s.userMap.Delete(md.Get("uuid")[0])
fmt.Println("某個用戶掉線,目前用戶線上數量", len(workers))
break
}
s.chatMessageCache = append(s.chatMessageCache, mes)
v, ok := s.userMap.Load(md.Get("uuid")[0])
if !ok {
fmt.Println("致命錯誤,用戶不存在")
return
}
mes.Name = v.(*pb.User).Name
mes.Time = uint64(time.Now().Unix())
s.sendMessage(stream, mes)
}
}
func (s *service) sendMessage(stream pb.ChatRoom_ChatServer, mes *pb.ChatMessage) {
s.L.Lock()
for _, v := range workers {
if v != stream {
err := v.Send(mes)
if err != nil {
// err handle
continue
}
}
}
s.L.Unlock()
}
main.go
package main
import (
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "wolflong.com/chatroom_server/chatroom"
)
const (
ip = "127.0.0.1"
port = "23333"
)
func main() {
lis, err := net.Listen("tcp", fmt.Sprintf("%v:%v", ip, port))
if err != nil {
log.Fatalf("無法監聽埠 %v %v", port, err)
}
s := grpc.NewServer()
// ^ 註冊服務
pb.RegisterChatRoomServer(s, &service{})
log.Println("gRPC伺服器開始監聽", port)
if err := s.Serve(lis); err != nil {
log.Fatalf("提供服務失敗: %v", err)
}
}
Client
package main
import (
"bufio"
"context"
"os"
"strings"
"time"
"github.com/pterm/pterm"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/wrapperspb"
pb "wolflong.com/chatroom_client/chatroom"
)
const (
address = "localhost:23333"
)
func main() {
/* ---------------------------------- 連接伺服器 --------------------------------- */
spinner, _ := pterm.DefaultSpinner.Start("正在連接聊天室")
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
spinner.Fail("連接失敗")
pterm.Fatal.Printfln("無法連接至伺服器: %v", err)
return
}
c := pb.NewChatRoomClient(conn)
spinner.Success("連接成功")
/* ---------------------------------- 註冊用戶名 --------------------------------- */
var val *wrapperspb.StringValue
var user *pb.User
for {
result, _ := pterm.DefaultInteractiveTextInput.Show("創建用戶名")
if strings.TrimSpace(result) == "" {
pterm.Error.Printfln("進入聊天室失敗,沒有取名字")
continue
}
user = &pb.User{Name: result}
val, err = c.Login(context.TODO(), user)
if err != nil {
pterm.Error.Printfln("進入聊天室失敗 %v", err)
continue
} else {
break
}
}
user.Id = val.Value
pterm.Success.Println("創建成功!開始聊天吧!")
/* ---------------------------------- 聊天室邏輯 --------------------------------- */
stream, _ := c.Chat(metadata.AppendToOutgoingContext(context.Background(), "uuid", user.Id))
go func(pb.ChatRoom_ChatClient) {
for {
res, _ := stream.Recv()
switch res.Id {
case "server":
pterm.Success.Printfln("(%[2]v) [伺服器] %[1]s ", res.Content, time.Unix(int64(res.Time), 0).Format(time.ANSIC))
default:
pterm.Info.Printfln("(%[3]v) %[1]s : %[2]s", res.Name, res.Content, time.Unix(int64(res.Time), 0).Format(time.ANSIC))
}
}
}(stream)
for {
inputReader := bufio.NewReader(os.Stdin)
input, _ := inputReader.ReadString('\n')
input = strings.TrimRight(input, "\r \n")
// pterm.Info.Printfln("%s : %s", user.Name, input)
stream.Send(&pb.ChatMessage{Id: user.Id, Content: input})
}
}
資料
一口氣搞懂Go sync-map 所有知識點- 閱坊 (readfog.com)
Go語言sync.Map(在併發環境中使用的map) (biancheng.net)
Golang 轉換 Unix 時間戳為 date 字元串示例