如何中断 HTTP 处理程序?

How to interrupt an HTTP handler?

假设我有一个这样的 http 处理程序:

func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
        // run code that takes a long time here
        // Executing dd command with cmd.Exec..., etc.

})

如果用户刷新页面或以其他方式终止请求而没有 运行 后续代码,我有没有办法中断此功能?我该怎么做?

我试过这样做:

notify := r.Context().Done()
go func() {
    <-notify
     println("Client closed the connection")
     s.downloadCleanup()
     return
}()

但是无论何时我中断它之后的代码仍然运行。

没有办法从任何代码 外部 强行将 goroutine 拆除到该 goroutine。

因此真正中断处理的唯一方法是定期检查客户端是否消失(或者是否有另一个停止处理的信号)。

基本上,这相当于将您的处理程序构造成这样

func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")

    done := r.Context().Done()

    // Check wheteher we're done

    // do some small piece of stuff

    // check whether we're done

    // do another small piece of stuff

    // …rinse, repeat
})

现在有一种方法可以检查是否有内容写入通道,但不阻塞操作是使用 "select with default" 惯用语:

select {
    case <- done:
        // We're done
    default:
}

当且仅当 done 被写入或被关闭(上下文就是这种情况)时,此语句才执行“//我们完成了”块中的代码,否则为空块在 default 分支中执行。

所以我们可以将其重构为

func ReallyLongFunction(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")

    done := r.Context().Done()
    closed := func () bool {
        select {
            case <- done:
                return true
            default:
                return false
        }
    }

    if closed() {
        return
    }

    // do some small piece of stuff

    if closed() {
        return
    }

    // do another small piece of stuff

    // …rinse, repeat
})

停止在 HTTP 处理程序中启动的外部进程

解决 OP 的评论…

os/exec.Cmd类型有Process字段,属于os.Process类型,该类型支持Kill方法强行带入运行进程下降。

唯一的问题是 exec.Cmd.Run 阻塞直到进程退出, 所以执行它的 goroutine 不能执行其他代码,如果 exec.Cmd.Run 在 HTTP 处理程序中被调用,就没有办法取消它。

如何以这种异步方式最好地处理 运行 程序在很大程度上取决于流程本身的组织方式,但我会这样滚动:

  1. 在处理程序中,准备进程,然后使用 exec.Cmd.Start(而不是 Run)启动它。

    检查返回的错误值 Start:如果是 nil 该过程已成功启动。否则以某种方式将失败传达给客户端并退出处理程序。

    一旦知道进程已经启动,exec.Cmd 值 它的某些字段填充了与流程相关的信息; 特别感兴趣的是 Process 类型的字段 os.Process:该类型具有Kill方法,可用于强制关闭进程。

  2. 启动一个 goroutine 并传递 exec.Cmd 值和一些合适类型的通道(见下文)。

    那个 goroutine 应该调用 Wait 并且一旦它 returns, 它应该通过该通道将该事实传达回原始 goroutine。

    究竟要交流什么,这是一个悬而未决的问题,因为它取决于 关于您是否要收集进程写入其标准的内容 输出和错误流 and/or 可能是与进程相关的一些其他数据' activity.

    发送数据后,该 goroutine 退出。

  3. 主 goroutine(执行处理程序)应该在检测到处理程序应该终止时调用 exec.Cmd.Process.Kill

    终止进程最终会解锁正在执行 Wait 相同 exec.Cmd 值作为进程退出的 goroutine。

    终止进程后,处理程序 goroutine 在通道上等待监听进程的 goroutine 的回复。处理程序对该数据做一些事情(可能是记录它或其他)然后退出。

你应该从内部取消 goroutine,所以对于一个长时间的计算任务,你可以提供检查点,停止并检查取消:

这是服务器的测试代码,例如长计算任务和检查点取消:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc(`/`, func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()

        log.Println("wait a couple of seconds ...")
        for i := 0; i < 10; i++ { // long calculation
            select {
            case <-ctx.Done():
                log.Println("Client closed the connection:", ctx.Err())
                return
            default:
                fmt.Print(".")
                time.Sleep(200 * time.Millisecond) // long calculation
            }
        }
        io.WriteString(w, `Hi`)
        log.Println("Done.")
    })
    log.Println(http.ListenAndServe(":8081", nil))
}

这里是客户端代码,超时了:

package main

import (
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

func main() {
    log.Println("HTTP GET")
    client := &http.Client{
        Timeout: 1 * time.Second,
    }
    r, err := client.Get(`http://127.0.0.1:8081/`)
    if err != nil {
        log.Fatal(err)
    }
    defer r.Body.Close()
    bs, err := ioutil.ReadAll(r.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("HTTP Done.")
    log.Println(string(bs))
}

您可以使用正常的browser检查是否取消,或者关闭它,刷新它,断开它,或者...,取消。