[英]How to broadcast in gRPC from server to client?
我現在正在 gRPC 中創建一個小型聊天應用程序,我遇到了一個問題,如果用戶想作為客戶端連接到 gRPC 服務器,我想向所有其他連接的人廣播該事件已經發生客戶。
我正在考慮使用某種觀察者,但我對服務器如何知道誰已連接以及我如何將事件廣播給所有客戶端而不僅僅是一兩個客戶端感到困惑。
我知道使用流是答案的一部分,但是因為每個客戶端都在服務器上創建自己的 stream,所以我不確定它如何訂閱其他服務器-客戶端流。
是的,除了保持包含所有已連接流的全局數據結構並遍歷它們的全局數據結構,並告訴它們剛剛發生的偶數,我沒有其他方法。
另一種選擇是使用長輪詢方法。 可以像下面這樣嘗試(Python代碼,因為這是我最熟悉的,但是go應該非常相似)。 這未經測試,只是為了讓您了解如何在gRPC中進行長輪詢:
.PROTO defs
-------------------------------------------------
service Updater {
rpc GetUpdates(GetUpdatesRequest) returns (GetUpdatesResponse);
}
message GetUpdatesRequest {
int64 last_received_update = 1;
}
message GetUpdatesResponse {
repeated Update updates = 1;
int64 update_index = 2;
}
message Update {
// your update structure
}
SERVER
-----------------------------------------------------------
class UpdaterServer(UpdaterServicer):
def __init__(self):
self.condition = threading.Condition()
self.updates = []
def post_update(self, update):
"""
Used whenever the clients should be updated about something. It will
trigger their long-poll calls to return
"""
with self.condition:
# TODO: You should probably remove old updates after some time
self.updates.append(updates)
self.condition.notify_all()
def GetUpdates(self, req, context):
with self.condition:
while self.updates[req.last_received_update + 1:] == []:
self.condition.wait()
new_updates = self.updates[req.last_received_update + 1:]
response = GetUpdatesResponse()
for update in new_updates:
response.updates.add().CopyFrom(update)
response.update_index = req.last_received_update + len(new_updates)
return response
SEPARATE THREAD IN THE CLIENT
----------------------------------------------
request = GetUpdatesRequest()
request.last_received_update = -1
while True:
stub = UpdaterStub(channel)
try:
response = stub.GetUpdates(request, timeout=60*10)
handle_updates(response.updates)
request.last_received_update = response.update_index
except grpc.FutureTimeoutError:
pass
需要一個全局的map
結構,可以為每個連接創建一個新的chan
。 我想出的是一個處理全局map
結構的中間通道。
服務器流式傳輸的示例:
func (s *server) Subscribe(req *pb.SubscribeRequest, srv pb.SubscribeServer) error {
//get trace id or generated a random string or whatever you want to indicate this goroutine
ID:="randomString"
//create a chan to receive response message
conn := make(chan *pb.SubscribeResponse)
//an intermediate channel which has the ownership of the `map`
s.broadcast <- &broadcastPayload {
//an unique identifier
ID: ID
//the chan corresponse to the ID
Conn: conn
//event to indicate add, remove or send message to broadcast channel
Event: EventEnum.AddConnection
}
for {
select {
case <-srv.Context().Done():
s.broadcast <- &entity.BroadcastPayload{
ID: ID,
Event: EventEnum.RemoveConnection
}
return nil
case response := <-conn:
if status, ok := status.FromError(srv.Send(response)); ok {
switch status.Code() {
case codes.OK:
//noop
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded:
return nil
default:
return nil
}
}}
}
}
對於broadcast
go例程:
//this goroutine has the ownership of the map[string]chan *pb.SubscribeResponse
go func(){
for v:=range s.broadcast {
//do something based on the event
switch v.Event {
//add the ID and conn to the map
case EventEnum.AddConnection:
...
//delete map key and close conn channel here
case EventEnum.RemoveConnection:
...
//receive message from business logic, send the message to suiteable conn in the map as you like
case EventEnum.ReceiveResponse:
...
}
}
}
我在這里放一些細節
Go示例中使用 gRPC 實現的簡單聊天服務器/客戶端
所有客戶端都存儲在map[string]chan *chat.StreamResponse
type server struct {
Host, Password string
Broadcast chan *chat.StreamResponse
ClientNames map[string]string
ClientStreams map[string]chan *chat.StreamResponse
namesMtx, streamsMtx sync.RWMutex
}
並向所有客戶端廣播消息
func (s *server) broadcast(_ context.Context) {
for res := range s.Broadcast {
s.streamsMtx.RLock()
for _, stream := range s.ClientStreams {
select {
case stream <- res:
// noop
default:
ServerLogf(time.Now(), "client stream full, dropping message")
}
}
s.streamsMtx.RUnlock()
}
}
// send messages in individual client
func (s *server) sendBroadcasts(srv chat.Chat_StreamServer, tkn string) {
stream := s.openStream(tkn)
defer s.closeStream(tkn)
for {
select {
case <-srv.Context().Done():
return
case res := <-stream:
if s, ok := status.FromError(srv.Send(res)); ok {
switch s.Code() {
case codes.OK:
// noop
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded:
DebugLogf("client (%s) terminated connection", tkn)
return
default:
ClientLogf(time.Now(), "failed to send to client (%s): %v", tkn, s.Err())
return
}
}
}
}
}
另一種方法是在客戶端也生成grpc服務器。 在應用程序級別,您需要從客戶端到服務器進行一些握手,以交換客戶端grpc-server ip和端口。 您現在可能要為此地址創建一個客戶端,並將該客戶端存儲在列表中。
現在,您可以使用默認的一元RPC調用將消息從列表中推送到客戶端。 不需要[bidi]流。 優點:
缺點:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.