如何中断 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 处理程序中被调用,就没有办法取消它。
如何以这种异步方式最好地处理 运行 程序在很大程度上取决于流程本身的组织方式,但我会这样滚动:
在处理程序中,准备进程,然后使用 exec.Cmd.Start
(而不是 Run
)启动它。
检查返回的错误值 Start
:如果是 nil
该过程已成功启动。否则以某种方式将失败传达给客户端并退出处理程序。
一旦知道进程已经启动,exec.Cmd
值
它的某些字段填充了与流程相关的信息;
特别感兴趣的是 Process
类型的字段
os.Process
:该类型具有Kill
方法,可用于强制关闭进程。
启动一个 goroutine 并传递 exec.Cmd
值和一些合适类型的通道(见下文)。
那个 goroutine 应该调用 Wait
并且一旦它 returns,
它应该通过该通道将该事实传达回原始 goroutine。
究竟要交流什么,这是一个悬而未决的问题,因为它取决于
关于您是否要收集进程写入其标准的内容
输出和错误流 and/or 可能是与进程相关的一些其他数据' activity.
发送数据后,该 goroutine 退出。
主 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检查是否取消,或者关闭它,刷新它,断开它,或者...,取消。
假设我有一个这样的 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 处理程序中被调用,就没有办法取消它。
如何以这种异步方式最好地处理 运行 程序在很大程度上取决于流程本身的组织方式,但我会这样滚动:
在处理程序中,准备进程,然后使用
exec.Cmd.Start
(而不是Run
)启动它。检查返回的错误值
Start
:如果是nil
该过程已成功启动。否则以某种方式将失败传达给客户端并退出处理程序。一旦知道进程已经启动,
exec.Cmd
值 它的某些字段填充了与流程相关的信息; 特别感兴趣的是Process
类型的字段os.Process
:该类型具有Kill
方法,可用于强制关闭进程。启动一个 goroutine 并传递
exec.Cmd
值和一些合适类型的通道(见下文)。那个 goroutine 应该调用
Wait
并且一旦它 returns, 它应该通过该通道将该事实传达回原始 goroutine。究竟要交流什么,这是一个悬而未决的问题,因为它取决于 关于您是否要收集进程写入其标准的内容 输出和错误流 and/or 可能是与进程相关的一些其他数据' activity.
发送数据后,该 goroutine 退出。
主 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检查是否取消,或者关闭它,刷新它,断开它,或者...,取消。