Закрытие WebSocket-соединения в Go
Открытый канал нужно завершать явно, иначе карта клиентов хранит мертвые указатели и рассылка будет биться об закрытые сокеты. Сначала отправляют служебный кадр закрытия с типом из библиотеки, затем вызывают закрытие соединения на стороне, которая инициирует конец работы.
После закрытия канал удаляют из карты под замком, как при обычном отключении клиента. Так сервер не будет писать в соединение, которое уже завершено с его стороны.
Обработчик с нормальным закрытием и удалением из карты:
package main
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
type hub struct {
mu sync.Mutex
conns map[*websocket.Conn]struct{}
}
func (h *hub) add(conn *websocket.Conn) {
h.mu.Lock()
h.conns[conn] = struct{}{}
h.mu.Unlock()
}
func (h *hub) remove(conn *websocket.Conn) {
h.mu.Lock()
delete(h.conns, conn)
h.mu.Unlock()
}
func closeClient(h *hub, conn *websocket.Conn) {
_ = conn.WriteMessage(websocket.CloseMessage, []byte{})
conn.Close()
h.remove(conn)
}
func handleSession(h *hub, conn *websocket.Conn) {
defer closeClient(h, conn)
for {
_, _, err := conn.ReadMessage()
if err != nil {
return
}
}
}
func main() {
h := &hub{conns: make(map[*websocket.Conn]struct{})}
upgrader := websocket.Upgrader{}
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
h.add(conn)
go handleSession(h, conn)
})
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
Служебный кадр закрытия сообщает другой
стороне, что обмен завершен штатно.
Полный перечень кодов закрытия в этом
уроке не разбираем: для учебного сервера
достаточно одного пустого кадра этого
типа перед Close.
Клиентская сторона после работы закрывает канал тем же порядком:
package main
import (
"log"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/ws", nil)
if err != nil {
log.Fatal(err)
}
_ = conn.WriteMessage(websocket.CloseMessage, []byte{})
conn.Close()
}
Дано соединение в карте:
// conn already in hub.conns
Опишите порядок: служебный кадр закрытия,
вызов Close на conn,
удаление из карты под замком.
Дан фрагмент:
conn.Close()
delete(h.conns, conn)
Скажите, чем рискован такой порядок без служебного кадра и без замка при удалении из карты.
Скажите, зачем после закрытия убирать канал из карты, даже если рассылка сейчас не выполняется.