golang 并发 http 请求处理

问题描述 投票:0回答:4

我正在使用 golang 创建一个简单的 http 服务器。我有两个问题,一个是理论上的问题,另一个是关于实际程序的问题。

  1. 并发请求处理

我创建一个服务器并使用 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 ...)。 所以我不明白它们是否是并发的。如果是的话,我会看到打印中出现一些混乱......

  1. 现实生活中的问题

在真正的处理程序中,我将请求发送到另一台服务器并将答案返回给用户(对请求和答案进行了一些更改,但实际上它是一种代理)。当然,所有这些都需要时间,并且从所见(通过向处理程序添加一些打印)来看,请求是一一处理的,它们之间没有并发性(我的打印显示请求开始,完成所有步骤,结束了,然后我才看到一个新的开始......)。 我该怎么做才能使它们真正并发?
将处理程序函数作为 goroutine 会出现错误,请求正文已关闭。另外,如果它已经是并发的,添加更多的 goroutine 只会让事情变得更糟。

谢谢!

multithreading go concurrency httpserver
4个回答
7
投票

你的例子让人很难判断发生了什么。

下面的示例将清楚地说明请求是并行运行的。

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

http://localhost:8000

发出一个请求

在 5 秒内向 http://localhost:8000?case-two=true 再次发出请求

控制台输出将是

case one start
case two
case one end

1
投票

它确实以并发方式服务请求,如源代码中所示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

通过打印而不同步来分析并发几乎总是不确定的。


0
投票

虽然 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" 印刷)。


0
投票

Go标准库中的HTTP服务器是高并发的。如果你查看它的代码,你会看到类似这样的东西(简化版本):

    l, _ := net.Listen("tcp", addr)
    for {
        rw, _ := l.Accept()
        conn := &conn{
            server: srv,
            rwc:    rwc,
        }
        go s.serve(conn)

简而言之:服务器在简单的

for
循环中侦听连接,但在接受连接后,它会在单独的 goroutine 中为其提供服务。

处理几万并发连接确实没有问题。

在您的处理程序中,可以随意访问数据库、进行长时间运行的计算等。唯一的限制是您为 HTTP 服务器选择的超时(以及部署到生产时的负载平衡等)。

© www.soinside.com 2019 - 2024. All rights reserved.