搭建一个后台服务器, 有很种方法与模式,c/c++使用epoll, nodejs的V8等都能实现。
今天我们来看看go server的模式。
同以往其他语言底层采用epoll模式略有不同,golang采用了goroutine(协程)+channel的模式。
routine: 在线程之上封装的协程,每个都有自己的堆栈。
channel:协程间通信使用的。
创建routine的命令是
go func()
我们来举个例子。
比如创建一个http服务:
func Index(w http.ResponseWriter, r *http.Request){
log.Println("static:index_request, ip:", r.RemoteAddr)
io.WriteString(w, r.URL.Path)
}
func GetData(w http.ResponseWriter, r *http.Request){
log.Println("static:index_request, ip:", r.RemoteAddr)
vars := mux.Vars(r)
id := vars["id"]
log.Println("get data, id:", id)
io.WriteString(w, r.URL.Path)
}
func createServer() error{
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/getData/{id}", GetData)
router.HandleFunc("/", Index)
http.ListenAndServe(":12345", router)
return nil
}
router专门负责请求路由。
/请求只是负责打印请求链接。
/getData/{id}是一条查询数据请求。
看看内部对每条新请求的处理逻辑:
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
if fn := testHookServerServe; fn != nil {
fn(srv, l)
}
var tempDelay time.Duration // how long to sleep on accept failure
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
// TODO: allow changing base context? can't imagine concrete
// use cases yet.
baseCtx := context.Background()
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
for {
rw, e := l.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
可以看到最后一条语句为 go c.serve(ctx) ,意味着每条新链接都会被放进一条新routine里去执行,这样避免了各请求间的阻塞关系。
假设同一条请求有多个阻塞子请求,比如
getData调用2个请求以获得数据:
func GetDataA() (*A, error){
time.Sleep(time.Second * 1)
return &A{string("a")}, nil
}
func GetDataB() (*B, error){
time.Sleep(time.Second * 0.5)
return &B{string("b")}, nil
}
func GetData(w http.ResponseWriter, r *http.Request){
log.Println("static:index_request, ip:", r.RemoteAddr)
vars := mux.Vars(r)
id := vars["id"]
log.Println("get data, id:", id)
io.WriteString(w, r.URL.Path)
a, _ := GetDataA()
b, _ := GetDataB()
log.Println("a:", a.a, ",b:", b.b)
}
一般情况下,会将GetDataA/GetDataB顺序执行,假设A函数占用1s, B函数占用0.5秒,则GetData调用时间将超过1.5秒。
在go routine下可以很好的解决问题。
func GetDataA(ca *chan A) error{
time.Sleep(time.Second * 1)
ca <- A{string("a")}
return nil
}
func GetDataB(cb *chan B) error{
time.Sleep(time.Second * 0.5)
cb <- &B{string("b")}
return nil
}
func GetData(w http.ResponseWriter, r *http.Request){
log.Println("static:index_request, ip:", r.RemoteAddr)
vars := mux.Vars(r)
id := vars["id"]
log.Println("get data, id:", id)
io.WriteString(w, r.URL.Path)
var ca chan A
var cb chan B
go GetDataA(&ca)
go GetDataB(&cb)
a := <- ca
b := <- cb
log.Println("a:", a.a, ",b:", b.b)
}
这样GetDataA/GetDataB即可以并行执行,总共时间约1秒即可完成。
上面的 a := <- ca即表示a是接受channel的请求。
整条链路的流程:
现实工作中可能会有很多类似串行转并行的过程。
比如前端界现在流行的服务端渲染, 对于多个model数据的请求,采用routine获取,可以将时间长度最大缩短。
备注:
c++与golang高并发模式不同,之前我写过c/c++的高性能服务器文章:http://blog.csdn.net/xiaofei_hah0000/article/details/8743627