Golang中RPC的相关问题求建议
Golang中RPC的相关问题求建议 我想构建一个Web应用程序,其中大厅内有多个房间可以容纳游戏。问题在于,似乎每个房间都应该独立处理自己的业务,这意味着并发处理,但我不知道如何通过RPC实现这一点。
我认为为每个房间单独开启一个RPC服务(即使用多个端口)并不理想。
但如果我只使用一个RPC服务,并在参数中解析房间ID,然后调用相应房间的函数,逻辑上就变成了顺序执行。
有什么建议吗?谢谢。
你好,@Pierre_Ke,欢迎来到 Go 论坛。
你的 Web 应用程序的哪一部分需要 RPC?你是否可以仅通过请求、响应和 WebSocket 来实现你的需求?
更多关于Golang中RPC的相关问题求建议的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
但如果我只使用一个RPC服务并在参数中解析房间ID,然后调用房间的函数,逻辑就是顺序执行的。
为什么不直接为每个请求启动一个新的goroutine来处理呢? 大多数Go语言中的服务器都是这样工作的。
skillian,
非常感谢您如此详尽地回复。是的,我想我会尝试使用发布/订阅模式。
再次感谢! Pierre
当然。可以使用 WebSocket,但问题是我应该为每个房间都打开一个 Socket 吗?这会使用多个端口吗?
Pierre_Ke:
那么你所说的使用多个数据库是什么意思?
在数据库设计中有一个术语叫做“多租户”。你可以通过为每个房间创建一个新的SQL数据库来隔离每个租户(房间?)。这是最好的隔离技术。但你也可以在同一个数据库中为每个租户分配一个值。这更简单,但隔离性没那么好。据我所知,许多托管公司都使用这种方法。我猜这就是你打算做的?
我不认为为每个房间都开启一个RPC服务是好的做法,这意味着要使用多个端口。
一种方案可能是使用多个数据库。具体取决于每个房间需要执行什么操作或存储什么数据。为每个房间分配一个数据库。在需要时添加数据库房间。通过直接连接、REST API 或 RPC API 进行连接。
我的理解是,WebSocket 的工作方式与 HTTP 协议类似,多个客户端都通过同一个端口 443 进行连接,而多路复用是在 OSI 模型的较低层处理的。关于握手和其他实现细节等信息,在 WebSocket - 维基百科 文章中有更多介绍,包括 JavaScript 示例。
让我们说得更具体一些。在这款桌游中,当轮到我行动时,我选择“使用此卡”指令。在我的设计中,我想使用RPC来通知服务器我的选择。但我在想,也许在这个过程中我并不需要RPC?如果我使用REDIS并订阅一个user_id#operation频道和一个room_id#operation频道,客户端将“使用此卡”指令发送到room_id#operation频道,然后等待来自user_id#operation的回复。你认为这是一个好的实现方式吗?
假设这个房间是用来玩棋盘游戏的。在这个游戏中,玩家应该轮流选择打出一张牌或执行一些动作。
我现在正在做的是开启一个RPC服务并注册一个HALL结构体。在这个HALL结构体中,我有一个rooms切片。现在,当一个RPC请求到来时,参数类似于‘{player: Pierre; roomID: 1; action: skip}’,大厅将解析roomID然后调用这个房间的处理函数。
那么你所说的使用多个数据库是什么意思?我知道可以使用Redis的订阅与发布功能为每个房间构建一个简单的聊天系统。但我不知道它如何与RPC协同工作?
我知道可以使用
Redis的 订阅与发布 功能为每个房间构建一个简单的聊天系统。但我不知道它如何与 RPC 协同工作?
根据我的理解,这涉及到玩家数据将被存储多长时间。数据会是持久化的、按页面存储还是按会话存储?或者根本不需要存储?roomID 表明将会有某种形式的存储。这意味着你需要某种存储机制——localStorage、REDIS 或一个真正的 RDMS。
RPC 本身不进行存储。你必须使用 Redis、Postgresql 等来存储“房间数据”。RPC 只能触发一个函数或向另一个程序发出请求以利用其服务。
我想不出任何理由认为这行不通。我看到的唯一潜在缺点是这会让你绑定到Redis,但WebSocket并不特定于数据库。我查看了golang.org/x/net/websocket并观看了YouTube上的这个教程(尽管它是关于websocket package - github.com/gorilla/websocket - Go Packages的)。就我个人而言,如果要实现类似的游戏,我会采用以下方法:
- 使用Go通道,或许在服务器内为每个游戏使用一个单独的goroutine来处理游戏事件:
- 游戏看起来像这样:
type Game struct { incoming chan *GameEvent outgoing []chan<- *GameState // 以及其他你需要的游戏状态 } // ... // 订阅一个WebSocket客户端到游戏。c是一个通道,游戏将通过它向客户端发送游戏状态更新,以便客户端接收并更新浏览器。返回的通道应由客户端用来将来自Web客户端的更新发送到游戏。 func (g *Game) Subscribe(c <-chan *GameState) chan<- *GameEvent { ... } func (g *Game) run() { s := newInitialGameState() defer func() { for _, out := range g.outgoing { close(out) } }() for ev := range g.incoming { s = s.createNewStateFromEvent(ev) for _, out := range g.outgoing { out<- s } }
- 游戏看起来像这样:
- 使用WebSocket进行用户浏览器与服务器之间的交互:
- 在服务器端,对于每个WebSocket,有一个用于读取的goroutine和另一个用于写入的goroutine:
func createGameWebSocketHandler(g *Game) websocket.Handler { outgoingFromGame := make(chan *GameState) incomingToGame := g.Subscribe(outgoingFromGame) return func(ws *websocket.Conn) { go func() { for { ev := &GameEvent{} if err := websocket.JSON.Receive(ws, ev); err != nil { wsErr(ws, err) return } incomingToGame <- ev } }() for st := range outgoingFromGame { if err := websocket.JSON.Send(ws, st); err != nil { wsErr(ws, err) return } } } }
- 在服务器端,对于每个WebSocket,有一个用于读取的goroutine和另一个用于写入的goroutine:
在Golang中实现多房间并发处理,可以通过单个RPC服务配合goroutine和并发安全的数据结构来实现。以下是一个使用标准库net/rpc的示例:
package main
import (
"net"
"net/rpc"
"sync"
)
// 房间结构体
type Room struct {
ID string
mu sync.RWMutex
state map[string]interface{}
}
// 房间管理器
type RoomManager struct {
mu sync.RWMutex
rooms map[string]*Room
}
// RPC服务结构体
type GameService struct {
manager *RoomManager
}
// RPC方法:处理房间操作
func (s *GameService) HandleRoom(req Request, resp *Response) error {
// 获取房间
room := s.manager.GetRoom(req.RoomID)
if room == nil {
room = s.manager.CreateRoom(req.RoomID)
}
// 并发处理房间逻辑
go func() {
room.mu.Lock()
defer room.mu.Unlock()
// 处理房间业务逻辑
room.state[req.Action] = req.Data
// ... 其他游戏逻辑
}()
return nil
}
// 请求和响应结构
type Request struct {
RoomID string
Action string
Data interface{}
}
type Response struct {
Success bool
Message string
}
func main() {
// 初始化服务
service := &GameService{
manager: NewRoomManager(),
}
rpc.Register(service)
rpc.HandleHTTP()
// 启动单个RPC服务
listener, _ := net.Listen("tcp", ":8080")
for {
conn, _ := listener.Accept()
go rpc.ServeConn(conn)
}
}
对于更复杂的场景,可以使用sync.Map来管理房间:
type ConcurrentRoomManager struct {
rooms sync.Map
}
func (m *ConcurrentRoomManager) ProcessRoom(roomID string, fn func(*Room)) {
room, _ := m.rooms.LoadOrStore(roomID, &Room{
ID: roomID,
state: make(map[string]interface{}),
})
// 每个房间独立处理,互不阻塞
go func(r *Room) {
r.mu.Lock()
defer r.mu.Unlock()
fn(r)
}(room.(*Room))
}
如果使用gRPC,可以这样实现:
// gRPC服务实现
func (s *gameServer) HandleRoom(ctx context.Context, req *pb.RoomRequest) (*pb.RoomResponse, error) {
// 每个请求启动独立的goroutine处理
go s.processRoom(req.RoomId, req.Action)
return &pb.RoomResponse{
Success: true,
}, nil
}
func (s *gameServer) processRoom(roomID string, action string) {
// 房间具体的业务逻辑
// 这里可以安全地并发执行
}
这种设计允许:
- 单个RPC服务端口处理所有请求
- 每个房间的操作在独立的goroutine中并发执行
- 通过互斥锁或通道保证房间内部的状态安全
- 房间之间完全隔离,互不阻塞
房间间的通信可以通过通道或事件总线实现,避免直接共享内存。

