我正在使用 golang 创建一个简单的 http 服务器。我有两个问题,一个是理论上的问题,另一个是关于实际程序的问题。
我创建一个服务器并使用 s.ListenAndServe() 来处理请求。 据我了解同时处理的请求。我使用一个简单的处理程序来检查它:
func ServeHTTP(rw http.ResponseWriter, request *http.Request) {
fmt.Println("1")
time.Sleep(1 * time.Second) //Phase 2 delete this line
fmt.Fprintln(rw, "Hello, world.")
fmt.Println("2")
}
我发现如果我发送多个请求,我会看到所有“1”出现,并且仅在一秒钟后所有“2”出现。 但是,如果我删除 Sleep 行,我会发现该程序在完成前一个请求之前永远不会启动请求(输出为 1 2 1 2 1 2 ...)。 所以我不明白它们是否是并发的。如果是的话,我会看到打印中出现一些混乱......
在真正的处理程序中,我将请求发送到另一台服务器并将答案返回给用户(对请求和答案进行了一些更改,但实际上它是一种代理)。当然,所有这些都需要时间,并且从所见(通过向处理程序添加一些打印)来看,请求是一一处理的,它们之间没有并发性(我的打印显示请求开始,完成所有步骤,结束了,然后我才看到一个新的开始......)。
我该怎么做才能使它们真正并发?
将处理程序函数作为 goroutine 会出现错误,请求正文已关闭。另外,如果它已经是并发的,添加更多的 goroutine 只会让事情变得更糟。
谢谢!
你的例子让人很难判断发生了什么。
下面的示例将清楚地说明请求是并行运行的。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if len(r.FormValue("case-two")) > 0 {
fmt.Println("case two")
} else {
fmt.Println("case one start")
time.Sleep(time.Second * 5)
fmt.Println("case one end")
}
})
if err := http.ListenAndServe(":8000", nil); err != nil {
log.Fatal(err)
}
}
发出一个请求
在 5 秒内向 http://localhost:8000?case-two=true 再次发出请求
控制台输出将是
case one start
case two
case one end
它确实以并发方式服务请求,如源代码中所示https://golang.org/src/net/http/server.go#L2293.
这是一个人为的例子:
package main
import (
"fmt"
"log"
"net/http"
"sync"
"time"
)
func main() {
go startServer()
sendRequest := func() {
resp, _ := http.Get("http://localhost:8000/")
defer resp.Body.Close()
}
start := time.Now()
var wg sync.WaitGroup
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sendRequest()
ch <- n
}(i)
}
go func() {
wg.Wait()
close(ch)
}()
fmt.Printf("completion sequence :")
for routineNumber := range ch {
fmt.Printf("%d ", routineNumber)
}
fmt.Println()
fmt.Println("time:", time.Since(start))
}
func startServer() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(1 * time.Second)
})
if err := http.ListenAndServe(":8000", nil); err != nil {
log.Fatal(err)
}
}
经过几次运行,很容易看出发送请求的 go 例程的完成顺序是完全随机的,并且考虑到通道是 fifo,我们可以总结出服务器以并发方式处理请求,无论是否HandleFunc 是否休眠。 (假设所有请求大约在同一时间开始)。
除了上述之外,如果您确实在 HandleFunc 中睡眠了一秒钟,那么完成所有 10 个例程所需的时间始终为 1.xxx 秒,这进一步表明服务器同时处理了请求,否则完成所有例程的总时间请求应该超过 10 秒。
示例:
completion sequence :3 0 6 2 9 4 5 1 7 8
time: 1.002279359s
completion sequence :7 2 3 0 6 4 1 9 5 8
time: 1.001573873s
completion sequence :6 1 0 8 5 4 2 7 9 3
time: 1.002026465s
通过打印而不同步来分析并发几乎总是不确定的。
虽然 Go 并发地处理请求,但客户端实际上可能会阻塞(在发送第二个请求之前等待第一个请求完成),然后人们将准确地看到最初报告的行为。
我遇到了同样的问题,我的客户端代码通过 XMLHttpRequest 向慢速处理程序发送“GET”请求(我使用了与上面发布的类似的处理程序代码,超时时间为 10 秒)。事实证明,此类请求会相互阻塞。 JavaScript 客户端代码示例:
for (var i = 0; i < 3; i++) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/slowhandler");
xhr.send();
}
请注意,
xhr.send()
将立即返回,因为这是一个异步调用,但这并不能保证浏览器会立即发送实际的“GET”请求。
GET 请求会受到缓存的影响,如果尝试获取相同的 URL,缓存可能(事实上,将会)影响向服务器发出请求的方式。 POST 请求不会被缓存,因此如果将上例中的
"GET"
更改为 "POST"
,Go 服务器代码将显示 /slowhandler
将同时被触发(您将看到“1 1 1 [...pause ...] 2 2 2" 印刷)。
Go标准库中的HTTP服务器是高并发的。如果你查看它的代码,你会看到类似这样的东西(简化版本):
l, _ := net.Listen("tcp", addr)
for {
rw, _ := l.Accept()
conn := &conn{
server: srv,
rwc: rwc,
}
go s.serve(conn)
简而言之:服务器在简单的
for
循环中侦听连接,但在接受连接后,它会在单独的 goroutine 中为其提供服务。
处理几万并发连接确实没有问题。
在您的处理程序中,可以随意访问数据库、进行长时间运行的计算等。唯一的限制是您为 HTTP 服务器选择的超时(以及部署到生产时的负载平衡等)。