从一个demo开始俗话说万事开头难,但是用Go实现一个HttpServer真的不难。它有多简单?要启动一个Server并响应请求,包括包名、导入的依赖项,甚至是空行,只需要15行代码:packagemainimport("io""net/http")funcmain(){http.HandleFunc("/hello",hello)http.ListenAndServe(":81",nil)}funchello(responsehttp.ResponseWriter,request*http.Request){io.WriteString(response,"helloworld")}如此简单,能与之抗衡的恐怕只有Python了,Go也可以编译成可执行的二进制文件。你觉得好还是不好?HttpServer如何处理连接?从这行代码我们可以看出http.ListenAndServe(":81",nil)从命名的角度来看,这个方法做了两件事,监听和服务。从方法的单一职责来看,我觉得是不行的,一个方法怎么能做两件事呢?但是这是老大写的代码,所以很合理。第一个参数Addr是要监听的地址和端口,第二个参数Handler一般为nil,才是真正的逻辑处理,但是我们通常使用第一行代码来注册处理器。这段代码看起来像是Map到业务逻辑的路径,我们先大致了解一下,以后再看。http.HandleFunc("/hello",hello)如果你对网络编程基础有一点了解,就会知道操作系统提供了bind、listen、accept等系统调用。我们只需要发起调用就可以组成一个Server。Go也使用了这些系统调用并将它们封装在ListenAndServe中。Listen往下是系统调用,所以我们关注Serve:把branch代码收起来,只看trunk,发现是一个for循环,一直在accept,这个accept在没有connection的时候是阻塞的,并且当有连接时,启动一个新的协程来处理它。HttpServer是如何处理请求的?一些工作前处理请求的一行代码。可以看出每个连接都会开启一个协程进行处理:goc.serve(connCtx)其中connCtx代入到当前Server对象中:ctx:=context.WithValue(baseCtx,ServerContextKey,srv)...connCtx:=ctx并且还提供了一个钩子方法srv.ConnContext对其进行修改,可以在每次Accept的时候修改原来的context。如果cc:=srv.ConnContext;cc!=nil{connCtx=cc(connCtx,rw)ifconnCtx==nil{panic("ConnContextreturnednil")}}它的定义是://ConnContext可选地指定一个函数来修改//用于新的上下文连接C.提供的ctx//派生自basecontext,有一个ServerContextKey//value.ConnContextfunc(ctxcontext.Context,cnet.Conn)context.Context但是如果按照我一开始给的代码,是不能修改的srv.ConnContext,可以改成这样来自定义:funcmain(){http.HandleFunc("/hello",hello)server:=http.Server{Addr:":81",ConnContext:func(ctxcontext.Context,cnet.Conn)context.Context{returncontext.WithValue(ctx,"hello","roshi")},}server.ListenAndServe()}同样的c.setState也提供了一个hook,可以如上设置,每次连接状态改变时执行钩子方法:c.setState(c.rwc,StateNew,runHooks)//在Serve可以返回之前//ConnState指定一个可选的回调函数,该函数//在客户端连接更改状态时调用。详情见//ConnState类型和相关常量ConnStatefunc(net.Conn,ConnState)开始真正起作用为了看清楚serve方法在Accept之后做了什么,我们再简化一下:func(c*conn)serve(ctxcontext.Context){...for{w,err:=c.readRequest(ctx)...serverHandler{c.server}.ServeHTTP(w,w.req)...}}serve也是一个大循环,主要是读取一个请求,然后将请求交给Handler处理。为什么是大循环?因为每个服务处理一个连接,所以一个连接可以有多个请求。阅读请求比较枯燥。根据Http协议,读出URL、header、body等信息。这里有一个细节,每次请求读取完毕后,再开启一个协程读取下一个请求,也算是一种优化。对于{w,err:=c.readRequest(ctx)...ifrequestBodyRemains(req.Body){registerOnHitEOF(req.Body,w.conn.r.startBackgroundRead)}else{w.conn.r.startBackgroundRead()}...}请求是如何路由的?读取一个请求后,输入这行代码:serverHandler{c.server}.ServeHTTP(w,w.req)ServeHTTP找到我们注册的Handler进行处理,如果请求的URI是*或者请求Method是OPTIONS,那么使用globalOptionsHandler,也就是说这种请求不需要手动处理,直接返回。对于我们注册的Handler,我们还需要找到路由。这条路由的规则比较简单,主要由以下三个组成:如果注册了带host的路由,则按照host+path查找;如果host所在的路由没有注册,按Path查找精确匹配优先级匹配的路由规则。如果注册的路由规则的最后一个字符是/,则除了完全匹配外,还会按前缀查找。举几个例子大家理解一下:host匹配规则的注册路由是http。HandleFunc("/hello",hello)http.HandleFunc("127.0.0.1/hello",hello2)这时候如果执行curl'http://127.0.0.1:81/hello',就会匹配到hello2,但是如果你执行curl'http://localhost:81/hello'如果注册的路由是http.HandleFunc("/hello",hello)http.HandleFunc("127.0.0.1/hello/",hello2)note第二个结尾有一个/。这时如果执行curl'http://127.0.0.1:81/hello/roshi',也可以匹配到hello2。这个怎么样?你明白吗?找到路由后,我们直接调用我们一开始注册的方法。如果我们将数据写入Response,就可以返回给客户端,这样一个请求的处理就完成了。总结最后,让我们回顾一下GoHttpServer的要点:用Go启动一个HttpServer非常简单。GoHttpServer的本质就是一个大循环。每当有新的连接时,就会启动一个新的协程来处理每个连接的处理。也是一个大循环。在这个循环中,主要做了三件事情:读请求,找路由,执行逻辑
