【问题标题】:Close sent on server side but client side has still connection关闭在服务器端发送,但客户端仍然有连接
【发布时间】:2019-04-24 06:40:43
【问题描述】:

我有一个通过 API 和 websocket 进行通信的应用程序。 如果在数据库中发生更改,websocket 用于将更新的用户数据发布到客户端 - 这非常有效,除了在某些情况下 websocket 没有接收任何数据的情况。几秒钟后,websocket 又开始工作了。

服务器日志(首先websocket不工作,重新开始工作)

msg="向 Websocket 写入数据失败:websocket: 关闭发送"

msg="向客户端发送 Ping 消息"

msg="无法将 ping 消息写入 Websocket: websocket: close sent"

msg="向客户端发送 Ping 消息"

msg="向客户端发送 Ping 消息"

msg="向客户端发送 Ping 消息"

msg="向客户端发送 Ping 消息"

客户端代码:

<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>

Go 代码(通过 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
            }
        }
    }
}

基本上,当更新时,我们会将当前数据发布到新的 Websocket 连接 - 这总是有效的。之后,如果数据库发生变化,它会将更新的用户列表发布到通道中——然后 websocket 应该将其发布到更新列表的客户端。我们还发送 ping 消息 - 失败(如上面的日志所示)。客户端本身不会记录任何错误或关闭 websocket。

【问题讨论】:

    标签: go websocket


    【解决方案1】:

    websocket: close sent 错误表示服务器向客户端发送了关闭消息。因为应用程序服务器代码不发送消息,所以消息一定是由连接发送的,以响应来自客户端的关闭消息。

    关闭消息作为 websocket 读取方法的错误返回。因为没有记录任何消息,客户端必须发送“离开”关闭消息(唯一没有记录的错误)。

    当websocket连接返回错误时,读写goroutines关闭连接并返回。连接未保持打开状态。

    读写 goroutine 不会检测到对方已经关闭了连接,直到连接上的方法调用返回错误。读取 goroutine 快速检测到关闭的连接,因为它一直在读取,但是写入 goroutine 可能会有延迟。这可能是应用程序的问题

    为了让正在编写的 goroutine 快速退出,请使用通道向正在编写的 goroutine 发出信号。 dataChan 可能用于此目的,但我不确定,因为该问题不包括有关如何管理频道的信息。假设通道可以使用,阅读 goroutine 应该关闭dataChan。 writer 应该检测到关闭的 channel 并退出 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
            }
            ...
    

    这是Gorilla Chat Example使用的方法。

    如果无法使用dataChan,请为此目的引入一个新频道。在 handler 中创建通道,并将通道传递给读写 goroutine:

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

    从阅读 goroutine 返回时关闭通道:

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

    在写作gorountine中选择频道:

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

    这会导致写入 goroutine 在读取 goroutine 退出后快速退出。

    这是Gorilla Command Example使用的方法。

    这两种方法都降低了在连接上写入返回websocket: close sent 错误的可能性,但它们并没有消除这种可能性。该错误是意料之中的,因为读取 goroutine 可以在写入 goroutine 写入消息之前关闭连接。

    无论如何,证据是客户端正在关闭连接。未关闭的连接不是问题所在。

    【讨论】:

    • 所以基本上,一些Websockets失败的原因是未关闭的Websockets? Datachan 在数据库中的某些数据发生更改时获取数据 - 因此它仅在数据为数据时发送数据。只要有一个活动的 websocket 连接,它就应该是打开的——以防万一发生变化。
    • 所有证据表明 websocket 连接已关闭,因为用户导航离开或关闭了网页。发布的代码中的任何内容都不会通知dataChan 的发件人连接已关闭。
    • 我在上面测试了你的方法,它似乎有效 - 非常感谢你的帮助!
    猜你喜欢
    • 2018-06-03
    • 1970-01-01
    • 1970-01-01
    • 2011-03-03
    • 2013-05-18
    • 2010-12-19
    • 1970-01-01
    • 1970-01-01
    • 2021-12-04
    相关资源
    最近更新 更多