关闭已在服务器端发送,但客户端仍保持连接

I have an application that communicates over an API and a websocket. The websocket is used to publish updated userdata to the client if it is changed in the database - this works perfectly, except for the case that the websocket does not receive any data in some cases. After a few seconds, the websocket starts working again.

The server log (first, the websocket does not work and starts working again)

msg="Failed to write data to Websocket: websocket: close sent"

msg="Sending Ping Message to Client"

msg="Failed to write ping message to Websocket: websocket: close sent"

msg="Sending Ping Message to Client"

msg="Sending Ping Message to Client"

msg="Sending Ping Message to Client"

msg="Sending Ping Message to Client"

The client-side code:

<html>
<body>
<p id="data"></p>
</body>
<script>
var ws = new WebSocket("wss://example.com/ws");

function unloadPage() {
    toggleLoader();
    ws.onclose = function () {};
    ws.close();
}

ws.onopen = function () {
    ws.send('Ping');
};
ws.onerror = function (error) {
    console.log('WebSocket Error ' + error);
    var d = document.getElementById("data");
    d.innerHTML += "<tr><td>Failed to connect to Server.</td></tr>"
};
ws.onmessage = function (e) {
    console.log(e);
    var data = e.data;
    var d = document.getElementById("data");
    var parsedjson = JSON.parse(data);
    d.innerHTML = "";
    for (var i = 0; i < parsedjson.length; i++) {
        d.innerHTML += parsedjson;
    }
};
ws.onclose = function () {
    console.log("Websocket has been closed");
};
window.addEventListener("beforeunload", unloadPage);
</script>
</html>

The Go Code (routed through gorilla mux):

var (
    upgrader = websocket.Upgrader{
        ReadBufferSize:  1024,
        WriteBufferSize: 1024,
        CheckOrigin:     func(r *http.Request) bool { return true },
    }
    pingPeriod = (pongPeriod * 9) / 10
    pongPeriod = 60 * time.Second
    writeWait  = 10 * time.Second
)


func PingResponse(ws *websocket.Conn) {
    conf := storage.GetConfig()
    defer ws.Close()
    ws.SetReadLimit(512)
    ws.SetReadDeadline(time.Now().Add(pongPeriod))
    ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongPeriod)); return nil })
    for {
        _, _, err := ws.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
                conf.Log.Debugf("Websocket Ping Read Failed: %v", err)
            }

            return
        } else {
            conf.Log.Debugf("Received message from Websocket client")
        }
    }
}

func ServeAllUsersWebsocket(datachan chan *[]storage.UserResponse) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        conf := storage.GetConfig()
        ws, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            conf.Log.Debugf("Failed to upgrade data to Websocket: %v", err)
            return
        }

        go allUserWebsocketWriter(ws, datachan)
        go PingResponse(ws)
    })
}


func allUserWebsocketWriter(ws *websocket.Conn, datachan chan *[]storage.UserResponse) {
    conf := storage.GetConfig()
    pingticker := time.NewTicker(pingPeriod)
    defer func() {
        pingticker.Stop()
        ws.Close()
    }()

    userresponse, err := conf.Database.GetAllUsers()
    if err != nil {
        conf.Log.Errorf("Failed to query users from database: %v", err)
        return
    }

    ws.SetWriteDeadline(time.Now().Add(writeWait))
    err = ws.WriteJSON(&userresponse)
    if err != nil {
        conf.Log.Debugf("Failed to write initial user response: %v", err)
        return
    }

    for {
        select {
        case data := <-datachan:
            ws.SetWriteDeadline(time.Now().Add(writeWait))
            err := ws.WriteJSON(&data)
            if err != nil {
                conf.Log.Debugf("Failed to write data to Websocket: %v", err)
                return
            }
        case <-pingticker.C:
            ws.SetWriteDeadline(time.Now().Add(writeWait))
            conf.Log.Debugf("Sending Ping Message to Client")
            if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
                conf.Log.Debugf("Failed to write ping message to Websocket: %v", err)
                return
            }
        }
    }
}

Basically, we are posting the current data to the new Websocket Connection, when it is updated - this does always work. Afterwards, if the database changes, it posts the updated userlist into the channel - the websocket should then post it to the client which updates the list. We are also sending ping messages - which fail (as seen in the log above). The client itself does not log any errors or closes the websocket.

1个回答

The websocket: close sent error indicates that the server sent a close message to the client. Because the application server code does not send the message, the message must have been sent by the connection in response to a close message from the client.

The close message is returned as an error from the websocket read methods. Because there are no messages logged, the client must have sent the 'going away' close message (the only error not logged).

When the websocket connection returns an error, the reading and writing goroutines close the connection and return. The connection is not left open.

The reading and writing goroutines do not detect that the other has closed the connection until an error is returned from a method call on the connection. The reading goroutine quickly detects the closed connection because it's always reading, but there can be a delay for the writing goroutine. This may be a problem for the application

To get the writing goroutine to exit quickly, signal writing goroutine using a channel. It's possible that dataChan can be used for this purpose, but I don't know for sure because the question does not include information about how the channel is managed. Assuming that the channel can be used, the reading goroutine should close dataChan. The writer should detect the closed channel and exit the goroutine:

...
for {
    select {
    case data, ok := <-datachan:
        if !ok {
           // Done writing, return
           return
        }
        ws.SetWriteDeadline(time.Now().Add(writeWait))
        err := ws.WriteJSON(&data)
        if err != nil {
            conf.Log.Debugf("Failed to write data to Websocket: %v", err)
            return
        }
        ...

This is the approach used by the Gorilla Chat Example.

If dataChan cannot be used, introduce a new channel just for this purpose. Create the channel in the handler and pass the channel to the reading and writing goroutines:

 done := make(chan struct{})
 go allUserWebsocketWriter(ws, stop, datachan)
 go PingResponse(ws, stop)

Close the channel on return from the reading goroutine:

func PingResponse(ws *websocket.Conn, done chan struct{}) {
    defer close(done)
    conf := storage.GetConfig()
    ...

Select on the channel in the writing gorountine:

...
for {
    select {
    case <-done:
        return
    case data := <-datachan:
        ws.SetWriteDeadline(time.Now().Add(writeWait))
        err := ws.WriteJSON(&data)
        ...

This causes the writing goroutine to quickly exit after the reading goroutine exits.

This is the approach used by the Gorilla Command Example.

Both of these approaches reduce the likelihood that write on the connection returns the websocket: close sent error, but they do not eliminate the possibility. The error is expected because the reading goroutine can close the connection just before the writing goroutine writes a message.

In any case, the evidence is that the client is closing the connection. Unclosed connections are not whatever the issue is.

duancha1065
duancha1065 我已经在上面测试了您的方法,并且似乎可行-非常感谢您的帮助!
一年多之前 回复
down101102
down101102 所有证据表明,由于用户导航或关闭了网页,因此websocket连接已关闭。 发布的代码中的任何内容都不会通知发送方dataChan连接已关闭。
一年多之前 回复
douxian0279
douxian0279 因此,基本上,某些Websocket失败的原因是未封闭的Websocket? 当数据库中的某些数据更改时,Datachan会获取数据-因此,仅当它是数据时才发送数据。 只要有一个活动的Websocket连接,它就应该打开-以防万一。
一年多之前 回复
Csdn user default icon
上传中...
上传图片
插入图片
抄袭、复制答案,以达到刷声望分或其他目的的行为,在CSDN问答是严格禁止的,一经发现立刻封号。是时候展现真正的技术了!
立即提问
相关内容推荐