我的Go gRPC之旅、03 簡單控制台聊天室

来源:https://www.cnblogs.com/linxiaoxu/archive/2022/09/20/16712548.html
-Advertisement-
Play Games

我的gRPC之旅。使用gRPC一元通信模式和雙向流通信模式寫一個簡單的控制台聊天室。實現創建用戶和實時聊天兩個功能,不考慮高性能。複習了記憶體同步訪問Sync包的使用。用切片緩存聊天記錄,新用戶可以同步聊天記錄。 ...


效果

使用gRPC一元通信模式和雙向流通信模式寫一個簡單的控制台聊天室。實現創建用戶實時聊天兩個功能,不考慮高性能。複習了記憶體同步訪問Sync包的使用。用切片緩存聊天記錄,新用戶可以同步聊天記錄。

image-20220920204845525

image-20220920204908759

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})
	}
}

資料

Welcome - PTerm Docs

一口氣搞懂Go sync-map 所有知識點- 閱坊 (readfog.com)

Go語言sync.Map(在併發環境中使用的map) (biancheng.net)

Golang 轉換 Unix 時間戳為 date 字元串示例


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大部分高級編程語言雖然語法不同,編譯器不同,學習它們的小哥哥小姐姐們不同,但有一點卻是出奇地一致:編程邏輯! 有些剛入行或剛入門的童鞋可能連編程是啥意思都沒弄懂,一下子又來了個「邏輯」,那是什麼?這裡說的邏輯,廣義上指的是抽象思維能力,也就是能思考那些客觀世界不存在的東西的能力。狹義上來說,就是明確 ...
  • 概要 設計模式類型:創建型 目標問題:創建對象時,參數設置的靈活性問題。(具體看案例) 接下來我們看一個需要改進的案例。 對象創建的優化 現在有個Employee類,你能預想到在開發中可能會出現的問題嗎?不一定是業務方面的問題哦。 最初版 public class Employee { privat ...
  • 這篇博客是我在B站看韓順平老師的數據結構和演算法的約瑟夫問題後的學習筆記,記錄一下,防止忘記,也希望能幫到各位小伙伴。 問題引入:設編號為 1,2,… n 的 n 個人圍坐一圈,約定編號為 k(1<=k<=n)的人從 1 開始報數,數 到 m 的那個人出列,它的下一位又從 1 開始報數,數到 m 的那 ...
  • 左值引用用於一級指針,特別是將它們和const關鍵字三者聯合使用時,有不同於普通左值引用的性質,主要表現在初始化方面,下麵總結一下。 ...
  • 下麵這段代碼是使用MatPlotLib繪製數據隨時間變化的趨勢。 import datetime as dt import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib.pylab ...
  • Java面向對象 1.類和對象 1.1 類和對象的概念: 類是抽象的集合,對象是具體的實例。 類可以想象為製作蛋糕的模具,對象就是做出來的蛋糕。 類中包含屬性(欄位)和方法(操作) 1.2 類的定義: Class ClassName { 屬性1 屬性2 ··· 構造器1 構造器2(如果不寫,系統會默 ...
  • 寫程式之前要瞭解兩個概念 1.什麼是進程 2.什麼是線程 搞清楚這兩個概念之後 才能寫好一個合適而不會太抽象的程式 對進程和線程的理解見鏈接: https://blog.csdn.net/new_teacher/article/details/51469241 https://www.cnblogs ...
  • form表單內容序列化 form表單自帶兩種方法serialize()方法和serialize()方法 1.serialize()方法 描述:序列化表單內容為字元串(不包括文件),用於Ajax請求。 格式:var data = $('#form').serialize(); 2.serializeA ...
一周排行
    -Advertisement-
    Play Games
  • 經常看到有群友調侃“為什麼搞Java的總在學習JVM調優?那是因為Java爛!我們.NET就不需要搞這些!”真的是這樣嗎?今天我就用一個案例來分析一下。 昨天,一位學生問了我一個問題:他建了一個預設的ASP.NET Core Web API的項目,也就是那個WeatherForecast的預設項目模 ...
  • 很多軟體工程師都認為MD5是一種加密演算法,然而這種觀點是不對的。作為一個 1992 年第一次被公開的演算法,到今天為止已經被髮現了一些致命的漏洞。本文討論MD5在密碼保存方面的一些問題。 ...
  • Maven可以使我們在構建項目時需要用到很多第三方類jar包,如下一些常用jar包 而maven的出現可以讓我們避免手動導入jar包出現的某些問題,它可以自動下載那須所需要的jar包 我們只需要在創建的maven項目自動生成的pom.xml中輸入如下代碼 <dependencies> <!--ser ...
  • 來源:https://developer.aliyun.com/article/694020 非同步調用幾乎是處理高併發Web應用性能問題的萬金油,那麼什麼是“非同步調用”? “非同步調用”對應的是“同步調用”,同步調用指程式按照定義順序依次執行,每一行程式都必須等待上一行程式執行完成之後才能執行;非同步調 ...
  • 1.面向對象 面向對象編程是在面向過程編程的基礎上發展來的,它比面向過程編程具有更強的靈活性和擴展性,所以可以先瞭解下什麼是面向過程編程: 面向過程編程的核心是過程,就是分析出實現需求所需要的步驟,通過函數一步一步實現這些步驟,接著依次調用即可,再簡單理解就是程式 從上到下一步步執行,從頭到尾的解決 ...
  • 10瓶毒藥其中只有一瓶有毒至少需要幾隻老鼠可以找到有毒的那瓶 身似浮雲,心如飛絮,氣若游絲。 用二分查找和二進位位運算的思想都可以把死亡的老鼠降到最低。 其中,二進位位運算就是每一隻老鼠代表一個二進位0或1,0就代表老鼠存活,1代表老鼠死亡;根據數學運算 23 = 8、24 = 16,那麼至少需要四 ...
  • 一、Kafka存在哪些方面的優勢 1. 多生產者 可以無縫地支持多個生產者,不管客戶端在使用單個主題還是多個主題。 2. 多消費者 支持多個消費者從一個單獨的消息流上讀取數據,而且消費者之間互不影響。 3. 基於磁碟的數據存儲 支持消費者非實時地讀取消息,由於消息被提交到磁碟,根據設置的規則進行保存 ...
  • 大家好,我是陶朱公Boy。 前言 上一篇文章《關於狀態機的技術選型,最後一個真心好》我跟大家聊了一下關於”狀態機“的話題。從眾多技術選型中我也推薦了一款阿裡開源的狀態機—“cola-statemachine”。 於是就有小伙伴私信我,自己項目也考慮引入這款狀態機,但網上資料實在太少,能不能系統的介紹 ...
  • 使用腳本自動跑實驗(Ubuntu),將實驗結果記錄在文件中,併在實驗結束之後將結果通過郵件發送到郵箱,最後在windows端自動解析成excel表格。 ...
  • 話說在前面,我不是小黑子~ 我是超級大黑子😏 表弟大周末的跑來我家,沒事幹天天騷擾我,搞得我都不能跟小姐姐好好聊天了,於是為了打發表弟,我決定用Python做一個小游戲來消耗一下他的精力,我思來想去,決定把他變成小黑子,於是做了一個坤坤打籃球的游戲,沒想到他還挺愛玩的~ 終於解放了,於是我把游戲寫 ...