Golang并发处理HTTP请求

huangapple go评论87阅读模式
英文:

golang concurrent http request handling

问题

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

  1. 并发请求处理

我创建了一个服务器,并使用s.ListenAndServe()来处理请求。根据我的理解,请求是并发处理的。我使用一个简单的处理程序来进行检查:

func ServeHTTP(rw http.ResponseWriter, request *http.Request) {  
    fmt.Println("1")  
    time.Sleep(1 * time.Second)       //第二阶段删除此行
    fmt.Fprintln(rw, "Hello, world.")
    fmt.Println("2")  
}

我发现,如果我发送多个请求,我会看到所有的"1"都出现了,只有在一秒钟后才会出现所有的"2"。但是,如果我删除Sleep行,我发现程序在完成前一个请求之前不会启动新的请求(输出为1 2 1 2 1 2 ...)。所以我不明白,它们是否真的是并发的。如果是并发的,我希望在输出中看到一些混乱的情况...

  1. 真实的问题

在真实的处理程序中,我将请求发送到另一个服务器,并将答案返回给用户(对请求和答案进行了一些更改,但在概念上它类似于代理)。当然,所有这些都需要时间,从我添加到处理程序中的一些打印信息来看,请求是一个接一个地处理的,它们之间没有并发性(我的打印信息显示一个请求开始,经过所有步骤,结束,然后才看到一个新的开始...)。我该怎么做才能使它们真正并发呢?
将处理函数作为goroutine放置会出现错误,提示请求的主体已经关闭。而且,如果已经是并发的,添加更多的goroutine只会使情况变得更糟。

谢谢!

英文:

I am creating a simple http server using golang. I have two questions, one is more theoretic and another one about the real program.

  1. Concurrent request handling

I create a server and use s.ListenAndServe() to handle the requests.
As much as I understand the requests served concurrently. I use a simple handler to check it:

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

I see that if I send several requests, I will see all the "1" appear and only after a second all the "2" appear.
But if I delete the Sleep line, I see that program never start a request before it finishes with the previous one (the output is 1 2 1 2 1 2 ...).
So I don't understand, if they are concurrent or not really. If they are I would expect to see some mess in prints...

  1. The real life problem

In the real handler, I send the request to another server and return the answer to the user (with some changes to request and the answer but in idea it is kind of a proxy). All this of course takes time and from what can see (by adding some prints to the handler), the requests are handled one by one, with no concurrency between them (my prints show me that a request starts, go through all the steps, ends and only then I see a new start....).
What can I do to make them really concurrent?
Putting the handler function as goroutine gives an error, that body of the request is already closed. Also if it is already concurrent adding more goroutines will make things only worse.

Thank you!

答案1

得分: 6

你的示例让人很难理解正在发生的事情。

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

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
英文:

Your example makes it very hard to tell what is happening.

The below example will clearly illustrate that the requests are run in parallel.

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

Make one request to http://localhost:8000

Make another request to http://localhost:8000?case-two=true within 5 seconds

console output will be

case one start
case two
case one end

答案2

得分: 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)
	}
}

通过多次运行,可以很容易地看出发送请求的goroutine的完成顺序是完全随机的,并且由于通道是先进先出的,我们可以总结出服务器以并发方式处理了请求,无论HandleFunc是否休眠。(假设所有请求大致同时开始)。

除此之外,如果在HandleFunc中休眠一秒钟,完成所有10个goroutine所需的时间始终是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

通过在没有同步的情况下打印来分析并发性几乎总是不确定的。

英文:

It does serve requests in a concurrent fashion as can be seen here in the source https://golang.org/src/net/http/server.go#L2293.

Here is a contrived example:

package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;sync&quot;
	&quot;time&quot;
)

func main() {
	go startServer()
	sendRequest := func() {
		resp, _ := http.Get(&quot;http://localhost:8000/&quot;)
		defer resp.Body.Close()
	}
	start := time.Now()
	var wg sync.WaitGroup
	ch := make(chan int, 10)
	for i := 0; i &lt; 10; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			sendRequest()
			ch &lt;- n
		}(i)
	}
	go func() {
		wg.Wait()
		close(ch)
	}()

	fmt.Printf(&quot;completion sequence :&quot;)
	for routineNumber := range ch {
		fmt.Printf(&quot;%d &quot;, routineNumber)
	}
	fmt.Println()
	fmt.Println(&quot;time:&quot;, time.Since(start))
}

func startServer() {
	http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(1 * time.Second)
	})
	if err := http.ListenAndServe(&quot;:8000&quot;, nil); err != nil {
		log.Fatal(err)
	}
}

Over several runs it is easy to visualize that the completion ordering of the go routines which send the requests are completely random and given the fact that channels are fifo, we can summarize that the server handled the requests in a concurrent fashion, irrespective of whether HandleFunc sleeps or not.
(Assumption being all the requests start at about the same time).

In addition to the above, if you did sleep for a second in HandleFunc, the time it takes to complete all 10 routines is consistently 1.xxx seconds which further shows that the server handled the requests concurrently as else the total time to complete all requests should have been 10+ seconds.

Example:

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

Analyzing concurrency by printing without synchronization is almost always indeterminate.

答案3

得分: 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请求会受到缓存的影响,如果尝试GET相同的URL,缓存可能(实际上会)影响到向服务器发出的请求。POST请求不会被缓存,所以如果将上面的示例中的"GET"更改为"POST",Go服务器代码将显示/slowhandler将会并发触发(你会看到打印出"1 1 1 [...pause...] 2 2 2")。

英文:

While Go serves requests concurrently, the client might actually be blocking (wait for the first request to complete before sending the second), and then one will see exactly the behavior reported initially.

I was having the same problem, and my client code was sending a "GET" request via XMLHttpRequest to a slow handler (I used a similar handler code as posted above, with 10 sec timeout). In turns out that such requests block each other. Example JavaScript client code:

for (var i = 0; i &lt; 3; i++) {
  var xhr = new XMLHttpRequest();
  xhr.open(&quot;GET&quot;, &quot;/slowhandler&quot;);
  xhr.send();
}

Note that xhr.send() will return immediately, since this is an asynchronous call, but that doesn't guarantee that the browser will send the actual "GET" request right away.

GET requests are subject to caching, and if one tries to GET the same URL, caching might (in fact, will) affect how the requests to the server are made. POST requests are not cached, so if one changes &quot;GET&quot; to &quot;POST&quot; in the example above, the Go server code will show that the /slowhandler will be fired concurrently (you will see "1 1 1 [...pause...] 2 2 2" printed).

huangapple
  • 本文由 发表于 2016年11月15日 20:56:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/40610398.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定