如何使用通道和 goroutine 构建一个 Go Web 服务器的结构?

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

How to structure a Go web server, using channels and goroutines?

问题

我正在实现一个服务器来流式传输多个浮点数数组。我需要一些帮助来设计我的系统以实现以下目标:

  • 音频处理必须是独立的,即使没有任何请求进来,它也能正常工作。我的当前方法是使DataProcess函数等待直到有请求。
  • 由于通道只能向一个请求提供数据,如何让2个或更多的请求获取我在DataProcess中准备的数据?
  • 为了实际流式传输数据,请求处理程序不能等待整个DataProcess完成,有没有办法在DataProcess的每次迭代完成时立即将数据传递给处理程序?

感谢任何回复。这是我目前的想法:

package main

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

func main() {
	c := AudioProcess()
	handleHello := makeHello(c)

	http.HandleFunc("/", handleHello)
	http.ListenAndServe(":8000", nil)
}

func makeHello(c chan string) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		for item := range c { // this loop runs when channel c is closed
			io.WriteString(w, item)
		}
	}
}

func AudioProcess() chan string {
	c := make(chan string)
	go func() {
		for i := 0; i <= 10; i++ { // Iterate the audio file
			c <- strconv.Itoa(i) // have my frame of samples, send to channel c
			time.Sleep(time.Second)
			fmt.Println("send ", i) // logging
		}
		close(c) // done processing, close channel c
	}()
	return c
}

请注意,我只会翻译代码部分,不会回答关于翻译的问题。

英文:

I am implementing a server to stream many arrays of floats. I need some help to design my system to achieve the following:

  • The audio process must be independent and does its work even if there isn't any request coming in. My current approach make the DataProcess function to wait until there is a request.
  • Because the channel can only give data to 1 request, how can 2 or more requests get the data I've prepared in the DataProcess?
  • To actually stream data, the request handler cant wait for the whole DataProcess to finish, is there anyway to give the handler data as soon as we complete each iteration in the DataProcess?

Any reply is appreciated. This is my current thoughts:

package main

import (
&quot;fmt&quot;
&quot;io&quot;
&quot;net/http&quot;
&quot;strconv&quot;
&quot;time&quot;
)

func main() {
	c := AudioProcess()
	handleHello := makeHello(c)

	http.HandleFunc(&quot;/&quot;, handleHello)
	http.ListenAndServe(&quot;:8000&quot;, nil)
}

func makeHello(c chan string) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		for item := range c { // this loop runs when channel c is closed
			io.WriteString(w, item)
		}
	}
}

func AudioProcess() chan string {
	c := make(chan string)
	go func() {
		for i := 0; i &lt;= 10; i++ { // Iterate the audio file
			c &lt;- strconv.Itoa(i) // have my frame of samples, send to channel c
			time.Sleep(time.Second)
			fmt.Println(&quot;send &quot;, i) // logging
		}
		close(c) // done processing, close channel c
		}()
		return c
	}

答案1

得分: 1

我不完全确定这是否解决了你的问题,因为我不完全了解你的用例,但是无论如何,我已经提供了下面的解决方案。

我在HTTP路由器中使用了Gin,因为对我来说更方便,但我相信你可以根据自己的情况调整代码。我匆忙完成了这个(抱歉),所以可能存在我不知道的问题,但如果有任何问题,请告诉我。

简而言之:

  1. 我创建了一个Manager来管理多个Client。它还包含一个sync.Mutex来确保在任何给定时间只有一个“线程”在修改clients
  2. 有一个InitBackgroundTask()函数,它会生成一个随机的float64数,并将其传递给Manager中的所有clients(如果有的话)。如果没有任何clients,我们只是睡眠并继续执行...
  3. index处理程序处理添加和删除clients。通过UUID来标识clients;
  4. 现在可能发生3件事情。当clients通过&lt;-c.Writer.CloseNotify()通道断开连接时(因为该方法返回并调用defer),它们会自动被删除。我们还可以在下一个后台任务的“tick”中接收到随机的float64数。最后,如果在20秒内没有收到任何内容,我们也可以终止。

我在这里对你的需求做了几个假设(例如,后台任务每隔Y分钟返回X)。如果你想要更精细的流式传输,我建议使用WebSockets(下面的模式仍然可以使用)。

如果你有任何问题,请告诉我。

代码:

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/satori/go.uuid"
	"log"
	"math/rand"
	"net/http"
	"sync"
	"time"
)

type Client struct {
	uuid string
	out  chan float64
}

type Manager struct {
	clients map[string]*Client
	mutex   sync.Mutex
}

func NewManager() *Manager {
	m := new(Manager)
	m.clients = make(map[string]*Client)
	return m
}

func (m *Manager) AddClient(c *Client) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	log.Printf("add client: %s\n", c.uuid)
	m.clients[c.uuid] = c
}

func (m *Manager) DeleteClient(id string) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	// log.Println("delete client: %s", c.uuid)
	delete(m.clients, id)
}

func (m *Manager) InitBackgroundTask() {
	for {
		f64 := rand.Float64()
		log.Printf("active clients: %d\n", len(m.clients))
		for _, c := range m.clients {
			c.out <- f64
		}
		log.Printf("sent output (%+v), sleeping for 10s...\n", f64)
		time.Sleep(time.Second * 10)
	}
}

func main() {
	r := gin.Default()
	m := NewManager()

	go m.InitBackgroundTask()

	r.GET("/", func(c *gin.Context) {
		cl := new(Client)
		cl.uuid = uuid.NewV4().String()
		cl.out = make(chan float64)

		defer m.DeleteClient(cl.uuid)
		m.AddClient(cl)

		select {
		case <-c.Writer.CloseNotify():
			log.Printf("%s : disconnected\n", cl.uuid)
		case out := <-cl.out:
			log.Printf("%s : received %+v\n", out)
			c.JSON(http.StatusOK, gin.H{
				"output": out,
			})
		case <-time.After(time.Second * 20):
			log.Println("timed out")
		}
	})

	r.Run()
}

注意:如果你在Chrome上进行测试,你可能需要在URL的末尾添加一个随机参数,以便实际进行请求,例如?rand=001?rand=002等等。

英文:

I'm not entirely sure if this addresses your problem as I'm not fully aware of your use case, but nevertheless, I've come up with a solution below.

I've used Gin for the HTTP router because it was more comfortable to me, but I'm pretty sure you can adapt the code to fit yours. I did this in a hurry (sorry), so there may be problems I'm not aware of, but do let me know if there are any.

In short:

  1. I created a Manager that takes care of several Client. It also contains a sync.Mutex to ensure only one thread is modifying the clients at any given time;
  2. There is an InitBackgroundTask() that will generate a random float64 number, and pass it to ALL clients in a Manager (if there are any). If there aren't any clients, we just sleep, and carry on...
  3. The index handler deals with adding, and removing clients. Clients are identified through a UUID;
  4. 3 things can happen now. Clients are automatically removed when they disconnect via the &lt;-c.Writer.CloseNotify() channel (because the method returns thereby calling the defer). We can also receive the random float64 number in the next background task tick. Finally, we can also terminate if we have not received anything in 20s.

I made several assumptions about your needs here (e.g. that the background task will return X every Y minutes). If you are looking for more fine grain streaming, I'd recommend using websockets instead (and the pattern below can still be used).

Let me know if you have any questions.

Code:

package main
import (
&quot;github.com/gin-gonic/gin&quot;
&quot;github.com/satori/go.uuid&quot;
&quot;log&quot;
&quot;math/rand&quot;
&quot;net/http&quot;
&quot;sync&quot;
&quot;time&quot;
)
type Client struct {
uuid string
out  chan float64
}
type Manager struct {
clients map[string]*Client
mutex   sync.Mutex
}
func NewManager() *Manager {
m := new(Manager)
m.clients = make(map[string]*Client)
return m
}
func (m *Manager) AddClient(c *Client) {
m.mutex.Lock()
defer m.mutex.Unlock()
log.Printf(&quot;add client: %s\n&quot;, c.uuid)
m.clients[c.uuid] = c
}
func (m *Manager) DeleteClient(id string) {
m.mutex.Lock()
defer m.mutex.Unlock()
// log.Println(&quot;delete client: %s&quot;, c.uuid)
delete(m.clients, id)
}
func (m *Manager) InitBackgroundTask() {
for {
f64 := rand.Float64()
log.Printf(&quot;active clients: %d\n&quot;, len(m.clients))
for _, c := range m.clients {
c.out &lt;- f64
}
log.Printf(&quot;sent output (%+v), sleeping for 10s...\n&quot;, f64)
time.Sleep(time.Second * 10)
}
}
func main() {
r := gin.Default()
m := NewManager()
go m.InitBackgroundTask()
r.GET(&quot;/&quot;, func(c *gin.Context) {
cl := new(Client)
cl.uuid = uuid.NewV4().String()
cl.out = make(chan float64)
defer m.DeleteClient(cl.uuid)
m.AddClient(cl)
select {
case &lt;-c.Writer.CloseNotify():
log.Printf(&quot;%s : disconnected\n&quot;, cl.uuid)
case out := &lt;-cl.out:
log.Printf(&quot;%s : received %+v\n&quot;, out)
c.JSON(http.StatusOK, gin.H{
&quot;output&quot;: out,
})
case &lt;-time.After(time.Second * 20):
log.Println(&quot;timed out&quot;)
}
})
r.Run()
}

Note: if you're testing this on Chrome, you might have to append a random parameter at the end of the URL so that the request will actually be made, e.g. ?rand=001, ?rand=002 and so on.

huangapple
  • 本文由 发表于 2016年4月7日 00:53:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/36457512.html
匿名

发表评论

匿名网友

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

确定