Невозможно обнаружить выход клиента в GO с помощью net/http, если он не на том же компьютере

Я пытаюсь создать приложение, которое периодически отправляет данные в браузер клиента, пока браузер клиента не закроется. Моя проблема в том, что я не могу обнаружить, что клиент закрывает браузер.

У меня есть следующий код, написанный на GO (минимальный код, в котором проблема актуальна):

import (
    "fmt"
    "net/http"
    "time"
)

func test(w http.ResponseWriter, r *http.Request) {
    f, f_err := w.(http.Flusher)
    if !f_err {
        fmt.Printf("%s\n", "flush error")
        return
    }
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Content-Type", "text/event-stream")
    w.WriteHeader(http.StatusOK)
    for timeout := 0; timeout < 1000; timeout++ {
        writ, err := w.Write([]byte("data: {\"result\": \"success\"}\n\n"))
        f.Flush()
        fmt.Printf("%d %d %s\n", timeout, writ, err)
        time.Sleep(time.Duration(1) * time.Second)
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
        test(w, r)
    })
    srv := &http.Server{
        Addr:    ":3001",
        Handler: mux,
    }
    err := srv.ListenAndServe()
    if err != nil {
        panic(err)
    }
}

Приведенный выше код — это мой сервер, и я подключаюсь к этому серверу через ssh-тунель (с порта 3001 на порт 3001) с помощью:

$ curl localhost:3001/test

Когда я уничтожаю завиток, сервер все еще пытается отправить данные. Через некоторое время это приводит к перегрузке сервера.

Вот что происходит на клиенте и сервере:

Сервер:

$ ./test
0 29 %!s(<nil>)
1 29 %!s(<nil>)
2 29 %!s(<nil>)
3 29 %!s(<nil>)
4 29 %!s(<nil>)
5 29 %!s(<nil>)
6 29 %!s(<nil>)
7 29 %!s(<nil>)
...
...
...

Клиент:

$ curl localhost:3001/test
data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

^C

Чего я хочу добиться, так это способности обнаруживать убийство curl.

Когда я пытаюсь запустить, а затем убить curl на той же машине, где работает сервер, я получаю:

Сервер:

$ ./test
0 29 %!s(<nil>)
1 29 %!s(<nil>)
2 29 %!s(<nil>)
3 29 %!s(<nil>)
4 29 %!s(<nil>)
5 29 %!s(<nil>)
6 29 %!s(<nil>)
7 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
8 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
9 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
10 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
11 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
12 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
13 0 write tcp 127.0.0.1:3001->127.0.0.1:51058: write: broken pipe
...
...
...

Клиент:

$ curl localhost:3001/test                                                                                                                             
data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

data: {"result": "success"}

^C

Это именно то, что мне хотелось бы иметь, когда я использую ssh-туннель.

🤔 А знаете ли вы, что...
Основополагающими принципами Go являются простота и читаемость кода.


1
54
2

Ответы:

Чтобы обнаружить отключение клиента, проверьте отмену контекста запроса и ошибки записи.

func test(w http.ResponseWriter, r *http.Request) {
    rc := http.NewResponseController(w)
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Content-Type", "text/event-stream")
    w.WriteHeader(http.StatusOK)
    t := time.NewTicker(time.Second)
    defer t.Stop()
    for timeout := 0; timeout < 1000; timeout++ {
        select {
        case <-t.C:
            _, err := w.Write([]byte("data: {\"result\": \"success\"}\n\n"))
            if err != nil {
                fmt.Println("write err", err)
                return // Exit on write error.
            }
            err = rc.Flush()
            if err != nil {
                fmt.Println("flush err", err)
                return // Exit on write error.
            }
        case <-r.Context().Done():
            fmt.Println("context canceled", r.Context().Err())
            return // Exit on context cancelation.
        }
    }
}

Решено

Согласно этой статьи (Раздел О стримингах). Go net/http не может обнаружить некорректные отключения. Таким образом, соединение через SSH-туннель, потеря Wi-Fi, отключение кабеля, сбой браузера и т. д. не могут быть обнаружены с помощью net/http.

Примечание. В будущем ситуация может измениться, поскольку базовый net.Conn способен обнаруживать ошибки такого типа.