草庐IT

我的Go gRPC之旅、03 简单控制台聊天室

小能的博客 CanAngle's Blog 2023-03-28 原文

效果

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

资料

Welcome - PTerm Docs

一口气搞懂Go sync-map 所有知识点- 阅坊 (readfog.com)

Go语言sync.Map(在并发环境中使用的map) (biancheng.net)

Golang 转换 Unix 时间戳为 date 字符串示例

有关我的Go gRPC之旅、03 简单控制台聊天室的更多相关文章

  1. Ruby Readline 在向上箭头上使控制台崩溃 - 2

    当我在Rails控制台中按向上或向左箭头时,出现此错误:irb(main):001:0>/Users/me/.rvm/gems/ruby-2.0.0-p247/gems/rb-readline-0.4.2/lib/rbreadline.rb:4269:in`blockin_rl_dispatch_subseq':invalidbytesequenceinUTF-8(ArgumentError)我使用rvm来管理我的ruby​​安装。我正在使用=>ruby-2.0.0-p247[x86_64]我使用bundle来管理我的gem,并且我有rb-readline(0.4.2)(人们推荐的最少

  2. ruby-on-rails - 带 Spring 锁的 Rails 4 控制台 - 2

    我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.

  3. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  4. ruby-on-rails - openshift 上的 rails 控制台 - 2

    我将我的Rails应用程序部署到OpenShift,它运行良好,但我无法在生产服务器上运行“Rails控制台”。它给了我这个错误。我该如何解决这个问题?我尝试更新ruby​​gems,但它也给出了权限被拒绝的错误,我也无法做到。railsc错误:Warning:You'reusingRubygems1.8.24withSpring.UpgradetoatleastRubygems2.1.0andrun`gempristine--all`forbetterstartupperformance./opt/rh/ruby193/root/usr/share/rubygems/rubygems

  5. ruby - 简单获取法拉第超时 - 2

    有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url

  6. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

    我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

  7. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  8. jquery - 我的 jquery AJAX POST 请求无需发送 Authenticity Token (Rails) - 2

    rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送

  9. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  10. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

随机推荐