简体   繁体   English

如何优雅地重新连接websocket?

[英]How to gracefully re-connect websocket?

I'm building a singe-page application using HTTP and Websockets.我正在使用 HTTP 和 Websockets 构建一个单页应用程序。 The user submits a form and I stream a response to the client.用户提交表单,我 stream 向客户端响应。 Below is a snippet client.下面是一个片段客户端。

var html = `<!DOCTYPE html>

<meta charset="utf-8">
<head>
</head>
<body>

<script>
var ws = new WebSocket("ws://localhost:8000/ws")
ws.onmessage = function(e) {
  document.getElementById("output").innerHTML += e.data + "<br>"
}

function submitFunction() {
  document.getElementById("output").innerHTML += ""
  return false
}
</script>

<form
enctype="multipart/x-www-form-urlencoded"
action="http://localhost:8000/"
method="post"
>`

This is the server.这是服务器。 If the request is not a POST, I write/render the html (parseAndExecute) which establishes a new websocket connection.如果请求不是 POST,我会编写/呈现 html (parseAndExecute),它会建立一个新的 websocket 连接。 If the request is a POST (from the form), then I start processing and eventually write to the websocket.如果请求是 POST(来自表单),那么我开始处理并最终写入 websocket。

func (c *Config) ServeHtml(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        //process
        channel <- data
    }

    c.parseAndExecute(w)
}

func (sh *SocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        w.Write([]byte(fmt.Sprintf("", err)))
        return
    }
    //defer ws.Close()

    // discard received messages
    go func(c *websocket.Conn) {
        for {
            if _, _, err := c.NextReader(); err != nil {
                c.Close()
                break
            }
        }
    }(ws)

    data <- channel

Everything works as I expect only if I do not refresh the page.只有当我不刷新页面时,一切都按我的预期工作。 If I don't refresh, I can keep submitting forms and see the different outputs come in line by line.如果我不刷新,我可以继续提交 forms 并逐行查看不同的输出。 To clarify, it actually only works if the page is already up so that parseAndExecute is never called.澄清一下,它实际上仅在页面已经启动时才有效,因此parseAndExecute永远不会被调用。 This function parses and executes html/template creating a new websocket client.这个 function 解析并执行 html/模板创建一个新的 websocket 客户端。

Any refresh of the page or initially browsing localhost:8000 would cause websocket: close sent on the server.任何刷新页面或最初浏览 localhost:8000 都会导致websocket: close sent

I'm not sure how to resolve this.我不确定如何解决这个问题。 Does the server to need to gracefully handle disconnections and allow re-connects?服务器是否需要优雅地处理断开连接并允许重新连接? Or does the client need to do something?还是客户需要做点什么? It seems like the server should upgrade any connection at /ws so it shouldn't matter how many new websocket clients are made but obviously my understanding is wrong.似乎服务器应该升级/ws的任何连接,所以创建多少新的 websocket 客户端并不重要,但显然我的理解是错误的。

I'm not closing the websocket connection on the server because it should be up for as long as the program is running.我没有关闭服务器上的 websocket 连接,因为只要程序正在运行,它就应该启动。 When the user stops the program, I assume it'll be automatically closed.当用户停止程序时,我认为它会自动关闭。

Full SocketHandler code:完整的 SocketHandler 代码:

func (sh *SocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        w.Write([]byte(fmt.Sprintf("", err)))
        return
    }

    // discard received messages
    go func(c *websocket.Conn) {
        for {
            if _, _, err := c.NextReader(); err != nil {
                c.Close()
                break
            }
        }
    }(ws)

    cmd := <-sh.cmdCh

    log.Printf("Executing")
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        w.Write([]byte(err.Error()))
        return
    }
    defer stdout.Close()

    stderr, err := cmd.StderrPipe()
    if err != nil {
        w.Write([]byte(err.Error()))
        return
    }
    defer stderr.Close()

    if err := cmd.Start(); err != nil {
        w.Write([]byte(err.Error()))
        return
    }

    s := bufio.NewScanner(io.MultiReader(stdout, stderr))
    for s.Scan() {
        err := ws.WriteMessage(1, s.Bytes())
        if err != nil {
            log.Printf("Error writing to client: %v", err)
            ws.Close()
        }

    }

    if err := cmd.Wait(); err != nil {
        w.Write([]byte(err.Error()))
        return
    }

    log.Println("Done")
}

Websocket server applications must handle errors on the connection by closing the connection and releasing resources associated with the connection. Websocket 服务器应用程序必须通过关闭连接并释放与连接关联的资源来处理连接上的错误。

Browsers close websocket connections when the containing page is refreshed.刷新包含页面时,浏览器会关闭 websocket 连接。 The server will eventually get a read or write error after the browser closes the connection.浏览器关闭连接后,服务器最终会出现读取或写入错误。

Connection close is one of several possible errors that the server might encounter.连接关闭是服务器可能遇到的几个可能错误之一。 The server application should handle all errors on the connection the same way: close the connection and release resources associated with the connection.服务器应用程序应该以相同的方式处理连接上的所有错误:关闭连接并释放与连接相关的资源。

The typical application design is for the client to connect on page load and to reconnect (with backoff) after an error.典型的应用程序设计是让客户端在页面加载时连接并在出现错误后重新连接(带回退)。 The server assumes that clients will connect and disconnect over time.服务器假定客户端将随着时间的推移连接和断开连接。

The JS code can be improved by adding an onerror handler that reconnects with backoff. JS 代码可以通过添加一个使用退避重新连接的 onerror 处理程序来改进。 Depending on the application, you may also want to display UI indicating the connection status.根据应用程序,您可能还希望显示指示连接状态的 UI。

The Go code does not close the connection in all scenarios. Go代码并非在所有场景下都关闭连接。 The running command is the resource associated with the connection.运行命令是与连接关联的资源。 The application does not kill this program on connection error.该应用程序不会在连接错误时终止该程序。 Here are some fixes:以下是一些修复:

Add defer ws.Close() after successful upgrade.升级成功后添加defer ws.Close() Remove other direct calls to ws.Close() from SocketHandler.ServeHTTP .SocketHandler.ServeHTTP中删除对ws.Close()的其他直接调用。 This ensures that ws.Close() is called in all scenarios.这可确保在所有场景中都调用ws.Close()

Kill the command on exit from the read and write pumps.从读写泵退出时终止命令。 Move the read pump to after the command is started.将读取泵移至命令启动后。 Kill on return.回来就杀。

go func(c *websocket.Conn, cmd *exec.Command) {
    defer c.Close()
    defer cmd.Process.Kill()
    for {
        if _, _, err := c.NextReader(); err != nil {
            break
        }
    }
}(ws, cmd)

Kill the command on exit from the write pump:从写泵退出时终止命令:

s := bufio.NewScanner(io.MultiReader(stdout, stderr))
for s.Scan() {
    err := ws.WriteMessage(1, s.Bytes())
    if err != nil {
        break
    }
}
cmd.Process.Kill()

I have not run or tested this code.我没有运行或测试过这段代码。 It's possible that some of the details are wrong, but this outlines the general approach of closing the connection and releasing the resource.有些细节可能是错误的,但这概述了关闭连接和释放资源的一般方法。

Take a look at the Gorilla command example .看一看Gorilla 命令示例 The example shows how to pump stdin and stdout to a websocket.该示例显示了如何将标准输入和标准输出泵送到 websocket。 The example also handles more advanced features like checking the health of the connection with PING/PONG.该示例还处理更高级的功能,例如使用 PING/PONG 检查连接的健康状况。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM