doumei1772 2018-11-21 16:22
浏览 711
已采纳

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

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条回答 默认 最新

  • doubi12138 2018-11-21 20:01
    关注

    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.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥30 Matlab打开默认名称带有/的光谱数据
  • ¥50 easyExcel模板 动态单元格合并列
  • ¥15 res.rows如何取值使用
  • ¥15 在odoo17开发环境中,怎么实现库存管理系统,或独立模块设计与AGV小车对接?开发方面应如何设计和开发?请详细解释MES或WMS在与AGV小车对接时需完成的设计和开发
  • ¥15 CSP算法实现EEG特征提取,哪一步错了?
  • ¥15 游戏盾如何溯源服务器真实ip?需要30个字。后面的字是凑数的
  • ¥15 vue3前端取消收藏的不会引用collectId
  • ¥15 delphi7 HMAC_SHA256方式加密
  • ¥15 关于#qt#的问题:我想实现qcustomplot完成坐标轴
  • ¥15 下列c语言代码为何输出了多余的空格