Я пытаюсь создать приложение, которое периодически отправляет данные в браузер клиента, пока браузер клиента не закроется. Моя проблема в том, что я не могу обнаружить, что клиент закрывает браузер.
У меня есть следующий код, написанный на 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 являются простота и читаемость кода.
Чтобы обнаружить отключение клиента, проверьте отмену контекста запроса и ошибки записи.
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
способен обнаруживать ошибки такого типа.