Отключение клиента Go GRPC завершает работу сервера Go

#go #network-programming #grpc-go

#Вперед #сетевое программирование #grpc-go

Вопрос:

Немного новичок как в Go, так и в GRPC, так что потерпите меня.

Используя версию go go1.14.4 для Windows / amd64, proto3 и последнюю версию grpc (я думаю, 1.31). Я пытаюсь настроить потоковое соединение bidi, которое, вероятно, будет открыто в течение более длительных периодов времени. Все работает локально, за исключением того, что если я завершу работу клиента (или одного из них), он также отключит сервер со следующей ошибкой:

Ошибка rpc при обмене данными: code = Cancelled desc = контекст отменен

Эта ошибка возникает из-за этого кода на стороне сервера

 func (s *exchangeserver) Trade(stream proto.ExchageService_TradeServer) error {

    endchan := make(chan int)
    defer close(endchan)

    go func() {
        for {
            req, err := stream.Recv()
            if err == io.EOF {
                break
            }
            if err != nil {
                log.Fatal("Unable to trade data ", err)
                break
            }

            fmt.Println("Got ", req.GetNumber())
        }

        endchan <- 1
    }()

    go func() {
        for {
            resp := amp;proto.WordResponse{Word: "Hello again "}
            err := stream.Send(resp)
            if err != nil {
                log.Fatal("Unable to send from server ", err)
                break
            }

            time.Sleep(time.Duration(500 * time.Millisecond))
        }

        endchan <- 1
    }()

    <-endchan
    return nil
}
  

И RPC Trade() настолько прост, что не стоит публиковать .proto.
Ошибка явно возникает из-за вызова Recv (), но этот вызов блокируется до тех пор, пока не появится сообщение, например, об отключении клиента, и в этот момент я бы ожидал, что он остановит поток, а не весь процесс. Я попытался добавить обработчик службы с помощью HandleConn(контекст, статистика.ConnStats) и он фиксирует отключение до того, как сервер умрет, но я ничего не могу с этим поделать. Я даже пытался создать глобальный канал, в который обработчик обслуживания вводит значение при обработке PC (контекст, статистика.Вызывается RPCStats) и позволяет вызывать Recv() только тогда, когда в канале есть значение, но это не может быть правильным, это похоже на блокировку функции блокировки для безопасности, и это все равно не сработало.

Это, должно быть, одна из тех действительно глупых ошибок, которые допускают новички. Какая польза была бы от GPRC, если бы он не мог обрабатывать отключение клиента без смерти? Тем не менее, я прочитал, вероятно, триллион (иш) сообщений со всех уголков Интернета, и ни у кого больше нет этой проблемы. Напротив, более популярная версия этого вопроса — «Мой клиентский поток остается открытым после отключения». Я ожидал бы этой проблемы. Не этот.

Ответ №1:

Я не уверен на 100%, как это должно себя вести, но я отмечаю, что вы запускаете отдельные программы приема и отправки одновременно. Это может быть допустимым, но не является типичным подходом. Вместо этого вы обычно получаете то, что хотите обработать, а затем запускаете вложенный цикл для обработки ответа.

Смотрите пример типичной реализации двунаправленной потоковой передачи отсюда:https://grpc.io/docs/languages/go/basics /

 func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
    for {
        in, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        key := serialize(in.Location)
                ... // look for notes to be sent to client
        for _, note := range s.routeNotes[key] {
            if err := stream.Send(note); err != nil {
                return err
            }
        }
    }
}
  

одновременная отправка и получение могут быть допустимы для вашего варианта использования, но если это то, что вы пытаетесь сделать, то я считаю, что ваша обработка каналов неправильная. В любом случае, пожалуйста, читайте дальше, чтобы понять проблему, поскольку она является распространенной в go.

У вас есть единственный канал, который блокируется только до тех пор, пока не получит одно сообщение, как только он разблокируется, функция завершается, и канал закрывается (с помощью defer).

Вы пытаетесь отправить на этот канал как из вашего цикла отправки, так и из цикла приема.

Когда последний завершающий попытается отправить на канал, он будет закрыт (первым завершающим), и сервер запаникует. Досадно, что вы на самом деле не увидите никаких признаков этого, поскольку сервер завершит работу до того, как программа goroutine сможет сбросить панику (никаких подсказок — вероятно, почему вы приземлились здесь)

смотрите пример проблемы здесь (удален код grpc):https://play.golang.org/p/GjfgDDAWNYr Примечание: прокомментируйте последнюю паузу в main функции, чтобы перестать показывать панику надежно (как в вашем случае)

Таким образом, одним простым решением, вероятно, было бы просто создать два отдельных канала (один для отправки, один для получения) и заблокировать оба — это, однако, оставило бы цикл отправки открытым, если у вас нет возможности ответить, поэтому, вероятно, лучше структурировать, как в примере выше, если у вас нет веских оснований для чего-то другого.

Другая возможность — это какая-то путаница в контексте сервера / запроса, но я почти уверен, что вышеизложенное исправится — удалите обновление с кодом настройки вашего сервера, если у вас все еще возникают проблемы после вышеуказанных изменений

Комментарии:

1. Я никогда по-настоящему не понимал, почему в большинстве примеров отправка и получение выполняются в одной процедуре go. Разве это не означает, что вы не сможете ничего отправить, если сначала не восстановите что-либо? Я хочу, чтобы клиент (у которого также есть отдельные подпрограммы go для отправки и получения) и сервер отправляли данные всякий раз, когда они готовы, независимо от того, запрашивалось это или нет. Вероятно, я неправильно обрабатываю выходной канал. Но, честно говоря, я пробовал без канала, с никогда не открывающимся каналом, с возвратами вместо перерывов, это не имеет значения. Похоже, ничто из этого не влияет на реальную проблему.

2. Вы не можете отправлять что-либо, пока у вас не будет запроса на это, поэтому вы никогда не отправите, не получив что-либо от клиента (даже если сообщение с пустым запросом)… Как только этот первоначальный запрос получен (и у вас есть дескриптор на клиенте), вы можете отправлять клиенту несколько ответов и нежелательных сообщений, вам просто нужно подумать о том, как вы структурируете свой код для достижения этого — и это вполне может закончиться несколькими циклами — поскольку я говорю, что то, что вы делаете, не обязательно является плохим подходом, просто нетипичным и не совсем правильно реализованным, вот почему я хотел объяснить проблему 🙂

3. как я уже сказал, 2 отдельных канала или предоставление каналу пропускной способности 2 и ожидание обоих сообщений должны остановить сбой вашего примера для быстрой победы, хотя это может привести к зависанию, если вы не убедитесь, что оба канала всегда закрыты также обратите внимание: примером является приложение для чата, которое может отправлять и получать очень двунаправленным способом — каналы — это самый большой PITA в go, а также его лучшая функция

4. Да, я тоже это пробовал. Я сократил код на стороне сервера только до того, что в этом примере — никаких подпрограмм go, recv() и send () вместе в одном цикле for. То же поведение — запуск сервера, запуск клиента (или двух, или трех), закрытие одного подключенного клиента, сбой сервера, завершение работы других клиентов. Проблема, по-видимому, заключается в том, что завершающий клиент отменяет контекст потока на стороне сервера, но сервер все еще пытается прочитать из него. Я пытался защитить его с помощью шлюза в функции HandleConn обработчика сервера, но это работает только время от времени — когда я перехватываю цикл for до того, как он остановится на recv.

5. можете ли вы опубликовать обновленную конечную точку службы, а также где вы настроили и запустили сервер grpc. Если это не проблема с каналом, это может быть ошибка контекста сервера. Существует контекст уровня сервера и контекст уровня запроса — вы хотите, чтобы уровень сервера отменял уровень запроса (для плавного завершения работы), но похоже, что, возможно, каким-то образом отмена запроса отменяет контекст сервера