Fork me on GitHub
随笔 - 215  文章 - 13  trackbacks - 0
<2024年12月>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234


专注即时通讯及网游服务端编程
------------------------------------
Openresty 官方模块
Openresty 标准模块(Opm)
Openresty 三方模块
------------------------------------
本博收藏大部分文章为转载,并在文章开头给出了原文出处,如有再转,敬请保留相关信息,这是大家对原创作者劳动成果的自觉尊重!!如为您带来不便,请于本博下留言,谢谢配合。

常用链接

留言簿(1)

随笔分类

随笔档案

相册

Awesome

Blog

Book

GitHub

Link

搜索

  •  

积分与排名

  • 积分 - 211162
  • 排名 - 118

最新评论

阅读排行榜

Golang标准库探秘(一):sync 标准库
http://www.infoq.com/cn/articles/golang-standard-library-part01

在高并发或者海量数据的生产环境中,我们会遇到很多问题,GC(garbage collection,中文译成垃圾回收)就是其中之一。说起优化GC我们首先想到的肯定是让对象可重用,这就需要一个对象池来存储待回收对象,等待下次重用,从而减少对象产生数量。

标准库原生的对象池

在Golang1.3版本便已新增了sync.Pool功能,它就是用来保存和复用临时对象,以减少内存分配,降低CG压力,下面就来讲讲sync.Pool的基本用法。

type Pool struct {
     local unsafe.Pointer
     localSize uintptr
     New func() interface{}
}

很简洁,最常用的两个函数Get/Put

   var pool = &sync.Pool{New:func()interface{}{return NewObject()}}
    pool.Put()
    Pool.Get()

对象池底层数据结构

我们选择用Golang的container标准包中的链表来做对象池的底层数据结构,它被封装在container/list标准包里:

   type Element struct {
          next, prev *Element
          list *List     
          Value interface{}
     }

这里是定义了链表中的元素,这个标准库实现的是一个双向链表,并且已经为我们封装好了各种Front/Back方法。不过Front方法的实现和我们需要的还是有点差异,它只是返回链表中的第一个元素,但这个元素依然会链接在链表里,所以我们需要自行将它从链表中删除,remove方法如下:

func (l *List) remove(e *Element) *Element {
      e.prev.next = e.next
        e.next.prev = e.prev
        e.next = nil
        e.prev = nil
        e.list = nil
        l.len--
        return e
 }

这样对象池的核心部分就完成了,但注意一下,从remove函数可以看出,container/list并不是线程安全的,所以在对象池的对象个数统计等一些功能会有问题。

原子操作并发安全

下面我们来自行解决并发安全的问题。Golang的sync标准包封装了常用的原子操作和锁操作。
sync/atomic封装了常用的原子操作。所谓原子操作就是在针对某个值进行操作的整个过程中,为了实现严谨性必须由一个独立的CPU指令完成,该过程不能被其他操作中断,以保证该操作的并发安全性。

  `type ConnPool struct {
    conns []*conn
    mu sync.Mutex // lock protected
    len int32
}`

在Golang中,我们常用的数据类型除了channel之外都不是线程安全的,所以在这里我们需要对数量(len)和切片(conns []*conn)做并发保护。至于需要几把锁做保护,取决于实际场景,合理控制锁的粒度。
接着介绍一下锁操作,我们在Golang中常用的锁——互斥锁(Lock)和读写锁(RWLock),互斥锁和读写锁的区别是:互斥锁无论是读操作还是写操作都会对目标加锁也就是说所有的操作都需要排队进行,读写锁是加锁后写操作只能排队进行但是可以并发进行读操作,要注意一点就是读的时候写操作是阻塞的,写操作进行的时候读操作是阻塞的。类型sync.Mutex/sync.RWMutex的零值表示了未被锁定的互斥量。也就是说,它是一个开箱即用的工具。只需对它进行简单声明就可以正常使用了,例如(在这里以Mutex为例,相对于RWMutex也是同理):

 var mu sync.Mutex
    mu.Lock()
    mu.Unlock()

锁操作一定要成对出现,也就是说在加锁之后操作的某一个地方一定要记得释放锁,否则再次加锁会造成死锁问题

fatal error: all goroutines are asleep - deadlock

不过在Golang里这种错误发生的几率会很少,因为有defer延时函数的存在
上面的代码可以改写为

var mu sync.Mutex
mu.Lock()
defer mu.Unlock()

在加锁之后马上用defer函数进行解锁操作,这样即使下面我们只关心函数逻辑而在函数退出的时候忘记Unlock操作也不会造成死锁,因为在函数退出的时候会自动执行defer延时函数释放锁。

标准库中的并发控制-WaitGroup

sync标准包还封装了其他很有用的功能,比如WaitGroup,它能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine(Golang中并发执行的协程)执行完成。文章开始我们说过,Golang是支持并发的语言,在其他goroutine异步运行的时候主协程并不知道其他协程是否运行结束,一旦主协程退出那所有的协程就会退出,这时我们需要控制主协程退出的时间,常用的方法:

1、time.Sleep()

让主协程睡一会,好方法,但是睡多久呢?不确定(最简单暴力)

2、channel

在主协程一直阻塞等待一个退出信号,在其他协程完成任务后给主协程发送一个信号,主协程收到这个信号后退出

e := make(chan bool)
 go func() {
      fmt.Println("hello")
      e <- true
 }()
 <-e

3、waitgroup

给一个类似队列似得东西初始化一个任务数量,完成一个减少一个

  var wg sync.WaitGroup
     func main() {
          wg.Add(1)
          go func() {
               fmt.Println("hello")
               wg.Done() //完成
          }()
          wg.Wait()
     }

这里要特别主要一下,如果waitGroup的add数量最终无法变成0,会造成死锁,比如上面例子我add(2)但是我自始至终只有一个Done,那剩下的任务一直存在于wg队列中,主协程会认为还有任务没有完成便会一直处于阻塞Wait状态,造成死锁。
wg.Done方法其实在底层调用的也是wg.Add方法,只是Add的是-1

func (wg *WaitGroup) Done() {
        wg.Add(-1)
}

我们看sync.WaitGroup的Add方法源码可以发现,底层的加减操作用的是我们上面提到的sync.atomic标准包来确保原子操作,所以sync.WaitGroup是并发安全的。

作者简介

郭军,奇虎360安全卫士服务端技术团队成员,关注架构设计,GO语言等互联网技术。

Golang标准库探秘(二):快速搭建HTTP服务器
http://www.infoq.com/cn/articles/golang-standard-library-part02

服务器阐述:

现在市面上有很多高并发服务器,Nginx就是一个领军人物,也是我们仰望的存在;Nginx+Lua这种组合也是高并发服务器的一个代表;PHP语言作为Nginx+FastCGI上一个优秀的解释语言也占据大半江山。而如今的Golang也作为高并发服务器语言横空出世,因其“语法简单、代码结构简明,维护成本还低,天生高并发”等特性而被广泛应用,尤其是各种云服务,还有Docker也是用Golang来做实现语言。

接着我们介绍下服务器编程模型,只从线程的角度,不谈并发模型。

单线程:从线程的角度,可以分为“单线程”,“多线程”2种。

整个进程只有一个线程,因为只有一个线程的缘故,当请求来的时候只能一个个按照顺序处理,要想实现高性能只能用“non-blocking IO + IO multiplexing”组合 (非阻塞io + io复用)。        Nginx采用的就是多进程 + 单线程( 非阻塞io+io复用)模式。

多线程:

进程有多个线程,多个线程就不好控制,还带来一些问题:锁竞争,数据污染、山下文切换带来的开销,但是可以充分利用CPU。要实现高性能也是“non-blocking IO + IO multiplexing”组合。

所以,其实不管单线程还是多线程都是要用“non-blocking IO + IO multiplexing”组合的。还有一种用户级线程,整个线程的库都是自己维护,“创建,撤销,切换”,内核是不知道用户级线程存在的,缺点是阻塞时会阻塞整个进程。

其实想实现高并发服务器最好用单线程(不处理逻辑的情况下),节省很多上下文切换开销(CPU分配时间片给任务,CPU加载上下文),但一定要采用io上“非阻塞和异步”。因为多线程很难控制,锁,数据依赖,不同场景会让多线程变成串行,控制起来相当繁琐,牺牲很多并发性能(Golang采用的抢占式调度),但正常情况下多线程还是挺不错的。下面我们说下Golang实现的高并发。

在Golang的调度器里用的也是“csp”并发模型,有3个重要的概念P、M、G。

P是Processor,G是Goroutine,M是Machine。

简述:M是执行G的机器线程,跟P绑定才可以执行,P存放G的队列。看到这里大家会问到刚刚不是说多线程切换上下文开销很大吗?其实每个M都有一个g0栈内存,用来执行运行时管理命令。调度时候M的g0会从每个G栈取出栈(现场),调度之后再保存到G,这样不同的M就可以接着调度了。所有上下文都是自己在切换,省去了内核带来的开销,而且Golang会观察,长时间不调度的G会被其他G抢占(抢占调度其实就是一个标记)。

采用异步的方式运行G,这样就实现了并发(M可不止一个啊,感兴趣看下Go并发实战

看到上面估计大家可能稍微了解点Golang的优势了吧。不要担心GC问题,选择场景问题。

实战

现在我们进入实战部分,手把手教你实现CGI,FastCGI,HTTP服务器,主要是用Golang的HTTP包。TCP实战就不在这次说了,TCP其实是块难啃的骨头,简单的几乎话说不清楚,如果是简单写一个“hello world”的例子,让大家似懂非懂的,不如单独开篇讲解一下,从Tcp 到 Protobuf 再到RPC,然后写一个稍微复杂点的tcp服务器,我们也可以处理下“粘包,丢包”等问题(Protobuf解决或者做一个分包算法),如果简单的demo可能会导致你丢失兴趣的。

首先了解什么是CGI?CGI和FastCGI的区别是什么?

CGI:全拼(Common Gateway Interface)是能让web服务器和CGI脚本共同处理客户的请求的协议。Web服务器把请求转成CGI脚本,CGI脚本执行回复Web服务器,Web服务回复给客户端。

CGI fork一个新的进程来执行,读取参数,处理数据,然后就结束生命期。

FastCGI采用tcp链接,不用fork新的进程,因为程序启动的时候就已经开启了,等待数据的到来,处理数据。

看出来差距在哪里了吧?就是CGI每次都要fork进程,这个开销很大的。(感兴趣的看下linux进程相关知识)。

现在我们来做我们的CGI服务器

CGI服务器

需要用到的包:

"net/http/cgi"
"net/http"

简单的2个包就可以实现CGI服务器了。“高秀敏:准备好了吗?希望别看到老头子他又错了的场景啊”。我们按照“代码->讲解”的流程,先运行在讲解。

 package main;
    import (
       "net/http/cgi"
       "fmt"
       "net/http"
    )
    
    funcmain() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
            handler := new(cgi.Handler);
            handler.Path = "/usr/local/go/bin/go";
            script := "/Users/liujinlong/cgi-script" + r.URL.Path;
            fmt.Println(handler.Path);
            handler.Dir = "/Users/liujinlong/cgi-script";
            args := []string{"run", script};
            handler.Args = append(handler.Args, args...);
            fmt.Println(handler.Args);
    
            handler.ServeHTTP(w, r);
       });
    http.ListenAndServe(":8989",nil);
    select {}//阻塞进程
    }
    test.go
    package main
    
    import(
        "fmt"
    )
    
    funcinit() {
        fmt.Print("Content-Type: text/plain;charset=utf-8\n\n");
    }
    
    funcmain() {
        fmt.Println("hello!!!!")
    }

看来我们成功了。来看下net/http/cgi的包。

先看host.go,这里有一个重要的结构Handler。

  // Handler runs an executable in a subprocess with a CGI environment.
    type Handler struct{
       Path string // 执行程序
       Root string // 处理url的根,为空的时候“/”
       Dir string         //目录
       Env        []string    // 环境变量
       InheritEnv []string    //集成环境变量
       Logger     *log.Logger// 日志
       Args       []string    //参数
       PathLocationHandlerhttp.Handler //http包的handler宿主
    }

    func(h *Handler) ServeHTTP(rwhttp.ResponseWriter, req *http.Request)

它也实现了ServeHttp,所有请求都会调用这个,这个后面分析HTTP源码的时候回详细讲解它是做什么的。Handler是在子程序中执行CGI脚本的。

funcRequest() (*http.Request, error)
funcServe(handler http.Handler)

先是将前端CGI请求转换成net包的HTTP请求,然后执行Handler,然后处理response。

接下来是FastCGI服务器,

用到的包:

"net"
    "net/http"
    "net/http/fcgi"

上面已经讲过,它是TCP的方式实现的,需要借助其他服务器来做转发,这里我们只提供代码,demo的截图讲解TCP的时候在加上。

需要使用Nginx,我电脑上没有。各位自己测试一下

server {
            listen 80;
 server_name ****;
            ...
            location *... {
                    include         fastcgi.conf;
    fastcgi_pass    127.0.0.1:9001;
            }
            ...
    }//…是省略,自己去写一个server。(具体谷歌)
    package main
    
    import (
       "net"
       "net/http"
       "net/http/fcgi"
    )
    
    type FastCGIstruct{}
    
    func(s *FastCGI) ServeHTTP(resphttp.ResponseWriter, req *http.Request) {
    resp.Write([]byte("Hello, fastcgi"))
    }
    
    funcmain() {
       listener, _ := net.Listen("tcp", "127.0.0.1:8989")
       srv := new(FastCGI)
       fcgi.Serve(listener, srv)
    select {
    
       }
    }

HTTP服务器

接下来就是重点了,我们的HTTP服务器,这个大家都不陌生,HTTP是最常用的方式之一,通用性很强,跨团队协作上也比较受到推荐,排查问题也相对来说简单。

我们接下来以3种方式来展现Golang的HTTP服务器的简洁和强大。

  1. 写一个简单的HTTP服务器
  2. 写一个稍微复杂带路由的HTTP服务器
  3. 分析源码,然后实现一个自定义Handler的服务器

然后我们对照net/http包来进行源码分析,加强对http包的理解。

1、写一个简单的HTTP服务器:

package main;
    
    import (
        "net/http"
    )
    
    funchello(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello"))
    }
    funcsay(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello"))
    }
    funcmain() {
        http.HandleFunc("/hello", hello);
        http.Handle("/handle",http.HandlerFunc(say));
        http.ListenAndServe(":8001", nil);
        select{};//阻塞进程
    }

是不是很简单,我用2种方式演示了这个例子,HandleFunc和Handle方式不同,却都能实现一个路由的监听,其实很简单,但是很多人看到这都会有疑惑,别着急,咱们源码分析的时候你会看到。

2、写一个稍微复杂带路由的HTTP服务器:

对着上面的例子想一个问题,我们在开发中会遇到很多问题,比如handle/res,handle/rsa…等等路由,这两个路由接受的参数都不一样,我们应该怎么写。我先来个图展示下运行结果。

是不是挺惊讶的,404了,路由没有匹配到。可是我们写handle这个路由了。

问题:

  1. 什么原因导致的路由失效
  2. 如何解决这种问题,做一个可以用Controller来控制的路由

问题1:

我们在源码阅读分析的时候会解决。

问题2:

我们可以设定一个控制器Handle,它有2个action,我们的执行handle/res对应的结果是调用Handle的控制器下的res方法。这样是不是很酷。

来我们先上代码:

静态目录:

  1. css
  2. js
  3. image

静态目录很好实现,只要一个函数http.FileServer(),这个函数从文字上看就是文件服务器,他需要传递一个目录,我们常以http.Dir("Path")来传递。

其他目录大家自己实现下,我们来实现问题2,一个简单的路由。

我们来看下代码

package main;
    
    import (
       "net/http"
       "strings"
       "reflect"
       "fmt"
    )
    
    funchello(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello"));
    }
    
    
    type Handlers struct{
    
    }
    
    func(h *Handlers) ResAction(w http.ResponseWriter, req *http.Request)  {
        fmt.Println("res");
        w.Write([]byte("res"));
    }
    funcsay(w http.ResponseWriter, req *http.Request) {
       pathInfo := strings.Trim(req.URL.Path, "/");
       parts := strings.Split(pathInfo, "/");
       varaction = "";
       fmt.Println(strings.Join(parts,"|"));
       if len(parts) >1 {
          action = strings.Title(parts[1]) + "Action";
       }
       fmt.Println(action);
       handle := &Handlers{};
       controller := reflect.ValueOf(handle);
       method := controller.MethodByName(action);
       r := reflect.ValueOf(req);
       wr := reflect.ValueOf(w);
       method.Call([]reflect.Value{wr, r});
    }
    funcmain() {
        http.HandleFunc("/hello", hello);
        http.Handle("/handle/",http.HandlerFunc(say));
        http.ListenAndServe(":8081", nil);
    select{};//阻塞进程
    }

上面代码就可以实现handle/res,handle/rsa等路由监听,把前缀相同的路由业务实现放在一个文件里,这样也可以解耦合,是不是清爽多了。其实我们可以在做的更加灵活些。在文章最后我们放出来一个流程图,按照流程图做你们就能写出一个简单的mvc路由框架。接下来看运行之后的结果。

如下图: 

(点击放大图像)

3、分析源码,然后实现一个自定义Handler的服务器

现在我们利用这个例子来分析下http包的源码(只是服务器相关的,Request我们此期不讲,简单看看就行。)

其实使用Golang做web服务器的方式有很多,TCP也是一种,net包就可以实现,不过此期我们不讲,因为HTTP服务器如果不懂,TCP会让你更加不明白。

我们从入口开始,首先看main方法里的http.HandleFunc和http.Handle这个绑定路由的方法,上面一直没解释有啥区别。现在我们来看一下。

// HandleFunc registers the handler function for the given pattern
    // in the DefaultServeMux.
    // The documentation for ServeMux explains how patterns are matched.
    funcHandleFunc(pattern string, handler func(ResponseWriter, *Request)) 
    funcHandle(pattern string, handler Handler)

Handle 和HandleFunc都是注册路由,从上面也能看出来这两个函数都是绑定注册路由函数的。如何绑定的呢?我们来看下。

上面2个函数通过DefaultServeMux.handle,DefaultServeMux.handleFunc把pattern和HandleFunc绑定到ServeMux的Handle上。

为什么DefaultServeMux会把路由绑定到ServeMux上呢?

// DefaultServeMux is the default ServeMux used by Serve.
    varDefaultServeMux = NewServeMux()

因为DefaultServeMux就是ServeMux的实例对象。导致我们就把路由和执行方法绑注册好了。不过大家请想下handle/res的问题?

从上面的分析我们要知道几个重要的概念。

HandlerFunc

    // The HandlerFunc type is an adapter to allow the use of
    // ordinary functions as HTTP handlers.  If f is a function
    // with the appropriate signature, HandlerFunc(f) is a
    // Handler object that calls f.
    type HandlerFuncfunc(ResponseWriter, *Request)
    
    // ServeHTTP calls f(w, r).
    func(f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
       f(w, r)
    }

上面的大概意思是,定义了一个函数适配器(可以理解成函数指针)HandleFunc,通过HandlerFunc(f)来进行适配。其实调用的实体是f本身。

package main
    import "fmt"
    type A func(int, int)
    func(f A)Serve() {
        fmt.Println("serve2")
    }
    funcserve(int,int) {
        fmt.Println("serve1")
    }
    funcmain() {
       a := A(serve)
       a(1,2)//这行输出的结果是serve1
       a.Serve()//这行输出的结果是serve2
    }

上面结果是serve1,serve2

Golang的源码里用了很多HandleFunc这个适配器。

接下来我们看第二个,ServeMux结构,最终我们是绑定它,也是通过它来解析。

type ServeMuxstruct{
       mu    sync.RWMutex//读写锁
       m     map[string]muxEntry//路由map,pattern->HandleFunc
       hosts bool//是否包含hosts        
    }
    
    type muxEntrystruct{
       explicit bool//是否精确匹配,这个在Golang实现里是ture
       h        Handler //这个路由表达式对应哪个handler
       pattern  string//路由
    }

看到explicit的时候是不是就明白为啥handle/res不能用handle来监听了?原来如此。大致绑定流程大家看明白了吗?如果不理解可以回去再看一遍。

接下来我们来看实现“启动/监听/触发”服务器的代码。

http.ListenAndServe(":8081", nil);

上面这句就是,”:8081”是监听的端口,也是socket监听的端口,第二个参数就是我们的Handler,这里我们写nil。

 funcListenAndServe(addr string, handler Handler) error {
       server := &Server{Addr: addr, Handler: handler}
       return server.ListenAndServe()
    }

从这个代码看出来,Server这个结构很重要。我们来看看他是什么。

type Server struct {
    Addr           string        // 监听的地址和端口
    Handler        Handler       // 所有请求需要调用的Handler(
    ReadTimeouttime.Duration // 读的最大Timeout时间
    WriteTimeouttime.Duration // 写的最大Timeout时间
    MaxHeaderBytesint           // 请求头的最大长度
    TLSConfig      *tls.Config   // 配置TLS
        ...   //结构太长我省略些,感兴趣大家自己看下
    }

Server提供的方法有:

 func(srv *Server) Serve(l net.Listener) error   //对某个端口进行监听,里面就是调用for进行accept的处理了
 func(srv *Server) ListenAndServe() error  //开启http server服务
 func(srv *Server) ListenAndServeTLS(certFile, keyFile string) error //开启https server服务

Server的ListenAndServe方法通过TCP的方式监听端口,然后调用Serve里的实现等待client来accept,然后开启一个协程来处理逻辑(go c.serve)。

它的格式

func(srv *Server) ListenAndServe() error 

看到这里我们要了解几个重要的概念。

ResponseWriter:生成Response的接口

Handler:处理请求和生成返回的接口

ServeMux:路由,后面会说到ServeMux也是一种Handler

Conn : 网络连接

这几个概念看完之后我们下面要用。

type conn struct

这个结构是一个网络间接。我们暂时忽略。

这个c.serve里稍微有点复杂,它有关闭这次请求,读取数据的,刷新缓冲区的等实现。这里我们主要关注一个c.readRequest(),通过redRequest可以得到Response,就是输出给客户端数据的一个回复者。

它里面包含request。如果要看懂这里的实现就要搞懂三个接口。

ResponseWriter, Flusher, Hijacker

    // ResponseWriter的作用是被Handler调用来组装返回的Response的
    type ResponseWriter interface {
        // 这个方法返回Response返回的Header供读写
        Header() Header
    
        // 这个方法写Response的Body
        Write([]byte) (int, error)
    
        // 这个方法根据HTTP State Code来写Response的Header
    
        WriteHeader(int)
    }
    
    // Flusher的作用是被Handler调用来将写缓存中的数据推给客户端
    type Flusher interface {
        // 刷新缓冲区
        Flush()
    }
    
    // Hijacker的作用是被Handler调用来关闭连接的
    type Hijacker interface {
        Hijack() (net.Conn, *bufio.ReadWriter, error)
    
    }

而我们这里的w也就是ResponseWriter了。而调用了下面这句方法,就可以利用它的Write方法输出内容给客户端了。

serverHandler{c.server}.ServeHTTP(w, w.req)

这句就是触发路由绑定的方法了。要看这个触发器我们还要知道几个接口。

具体我们先看下如何实现这三个接口的,因为后面我们要看触发路由执行逻辑片段。实现这三个接口的结构是response

response
    // response包含了所有server端的HTTP返回信息
    type response struct {
        conn          *conn         // 保存此次HTTP连接的信息
        req           *Request // 对应请求信息
        chunking      bool     // 是否使用chunk
        wroteHeaderbool     // header是否已经执行过写操作
        wroteContinuebool     // 100 Continue response was written
        header        Header   // 返回的HTTP的Header
        written       int64    // Body的字节数
        contentLength int64    // Content长度
        status        int      // HTTP状态
        needSniffbool  
//是否需要使用sniff。(当没有设置Content-Type的时候,开启sniff能根据HTTP body来确定Content-Type)
        closeAfterReplybool    
//是否保持长链接。如果客户端发送的请求中connection有keep-alive,这个字段就设置为false。
        requestBodyLimitHitbool 
//是否requestBody太大了(当requestBody太大的时候,response是会返回411状态的,并把连接关闭)
    }

在response中是可以看到

func(w *response) Header() Header 
func(w *response) WriteHeader(code int) 
func(w *response) Write(data []byte) (n int, err error) 
func(w *response) WriteString(data string) (n int, err error) 
// either dataB or dataS is non-zero.
func(w *response) write(lenDataint, dataB []byte, dataS string) (n int, err error) 
func(w *response) finishRequest()
func(w *response) Flush() 
func(w *response) Hijack() (rwcnet.Conn, buf *bufio.ReadWriter, err error)

我简单罗列一些,从上面可以看出,response实现了这3个接口。

接下来我们请求真正的触发者也就是serverHandle要触发路由(hijacked finishRequest暂且不提)。先看一个接口。

Handler
    
    type Handler interface {
    ServeHTTP(ResponseWriter, *Request)  // 具体的逻辑函数
    }

实现了handler接口,就意味着往server端添加了处理请求的逻辑函数。

serverHandle调用ServeHttp来选择触发的HandleFunc。这里面会做一个判断,如果你传递了Handler,就调用你自己的,如果没传递就用DefaultServeMux默认的。到这整体流程就结束了。

过程是:

DefaultServeMux.ServeHttp执行的简单流程.

  1. h, _ := mux.Handler(r)
  2. h.ServeHTTP(w, r)   //执行ServeHttp函数

查找路由,mux.handler函数里又调用了另外一个函数mux.handler(r.Host, r.URL.Path)。

还记得我们的ServeMux里的hosts标记吗?这个函数里会进行判断。

// Host-specific pattern takes precedence over generic ones
       if mux.hosts {
          h, pattern = mux.match(host + path)
       }
       if h == nil {
         h, pattern = mux.match(path)
       }
       if h == nil {
         h, pattern = NotFoundHandler(), ""
       }

上面就是匹配查找pattern和handler的流程了

我们来总结一下。

首先调用Http.HandleFunc

按顺序做了几件事:

  1. 调用了DefaultServerMux的HandleFunc
  2. 调用了DefaultServerMux的Handle
  3. 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则

别忘记DefaultServerMux是ServeMux的实例。其实都是围绕ServeMux,muxEntry2个结构进行操作绑定。

其次调用http.ListenAndServe(":12345", nil)

按顺序做了几件事情:

  1. 实例化Server
  2. 调用Server的ListenAndServe()
  3. 调用net.Listen("tcp", addr)监听端口,启动for循环,等待accept请求
  4. 对每个请求实例化一个Conn,并且开启一个goroutine处理请求。
  5. 如:go c.serve()
  6. 读取请求的内容w, err := c.readRequest(),也就是response的取值过程。
  7. 调用serverHandler的ServeHTTP,ServeHTTP里会判断Server的属性里的header是否为空,如果没有设置handler,handler就设置为DefaultServeMux,反之用自己的(我们后面会做一个利用自己的Handler写服务器)
  8. 调用DefaultServeMux的ServeHttp( 因为我们没有自己的Handler,所以走默认的)
  9. 通过request选择匹配的handler:

    A request匹配handler的方式。Hosts+pattern或pattern或notFound

    B 如果有路由满足,返回这个handler

    C 如果没有路由满足,返回NotFoundHandler

  10. 根据返回的handler进入到这个handler的ServeHTTP

大概流程就是这个样子,其实在net.Listen("tcp", addr)里也做了很多事,我们下期说道TCP服务器的时候回顾一下他做了哪些。

通过上面的解释大致明白了我们绑定触发的都是DefaultServeMux的Handler。现在我们来实现一个自己的Handler,这也是做框架的第一步。我们先来敲代码。

package main;
    
    import (
       "fmt"
       "net/http"
       "time"
    )
    
    type customHandlerstruct{
    
    }
    
    func(cb *customHandler) ServeHTTP( w http.ResponseWriter, r *http.Request ) {
        fmt.Println("customHandler!!");
        w.Write([]byte("customHandler!!"));
    }
    
    funcmain() {
        varserver *http.Server = &http.Server{
          Addr:           ":8080",
          Handler:        &customHandler{},
          ReadTimeout:    10 * time.Second,
          WriteTimeout:   10 * time.Second,
          MaxHeaderBytes: 1 <<20,
       }
    server.ListenAndServe();
    select {
       }
    }

是不是很酷,我们可以利用自己的handler做一个智能的路由出来。

不过还是建议使用国内Golang语言框架beego,已开源。一款非常不错的框架,谢大维护的很用心,绝对良心框架,而且文档支持,社区也很不错。

最后附上一张最早设计框架时候的一个流程图(3年前)。大家可以简单看看,当然也可以尝试的动动手。起码收获很多。

(点击放大图像)

  [1]: http://item.jd.com/11573034.html

  [2]: https://github.com/astaxie/beego

作者简介

刘金龙,艺名:金灶沐 ,go语言爱好者,2015年8月加入创业团队,负责各种“打杂”工作,之前在360电商购物小蜜java组担任java高级工程师职位,负责购物小蜜服务开发。14年开始用go语言做高并发服务并且尝试阅读go语言的源码来学习go语言的特性。

posted on 2017-08-01 17:19 思月行云 阅读(506) 评论(0)  编辑 收藏 引用 所属分类: Golang

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理