Golang中RPC的相关问题求建议

Golang中RPC的相关问题求建议 我想构建一个Web应用程序,其中大厅内有多个房间可以容纳游戏。问题在于,似乎每个房间都应该独立处理自己的业务,这意味着并发处理,但我不知道如何通过RPC实现这一点。

我认为为每个房间单独开启一个RPC服务(即使用多个端口)并不理想。

但如果我只使用一个RPC服务,并在参数中解析房间ID,然后调用相应房间的函数,逻辑上就变成了顺序执行。

有什么建议吗?谢谢。

12 回复

你好,@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 表明将会有某种形式的存储。这意味着你需要某种存储机制——localStorageREDIS 或一个真正的 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
                  }
              }
          }
      }
      

在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) {
    // 房间具体的业务逻辑
    // 这里可以安全地并发执行
}

这种设计允许:

  1. 单个RPC服务端口处理所有请求
  2. 每个房间的操作在独立的goroutine中并发执行
  3. 通过互斥锁或通道保证房间内部的状态安全
  4. 房间之间完全隔离,互不阻塞

房间间的通信可以通过通道或事件总线实现,避免直接共享内存。

回到顶部