hello大家好呀,我是小楼,这是系列文《Go底层原理剖析》的第二篇,依旧是分析Http模块,话不多说,开始。 从一个Demo入手 俗话说万事开头难,但用Go实现一个HttpServer真不难,简单到什么程度?起一个Server,并且能响应请求,算上包名、导入的依赖,甚至空行,也就只要15行代码:packagemainimport(ionethttp)funcmain(){http。HandleFunc(hello,hello)http。ListenAndServe(:81,nil)}funchello(responsehttp。ResponseWriter,requesthttp。Request){io。WriteString(response,helloworld)} 这么简单,能与之一战的恐怕只有Python了吧,而且Go还能编译成可执行的二进制文件,你说牛啤不牛啤? HttpServer如何处理连接? 我们从这一行代码看起http。ListenAndServe(:81,nil) 从命名来看,这个方法干了两件事,监听并且服务,从方法的单一职责上来说,我觉得不ok,一个方法怎么能干两件事?但这是大佬写的代码,就很合理。 第一个参数Addr是要监听的地址和端口,第二个参数Handler一般是nil,它是真正的逻辑处理,但我们通常用第一行代码那样来注册处理器,这代码一看就感觉是把path映射到业务逻辑上,我们先大概了解,待会再来看它http。HandleFunc(hello,hello) 如果了解过一点网络编程基础,就会知道操作系统提供了bind、listen、accept这样的系统调用,我们只要按顺序发起调用,就能组合出一个Server。 Go也是利用这些系统调用,把他们都封装在了ListenAndServe中。 Listen往下追究就是系统调用,所以我们重点看Serve: 把分支代码收起来,只看主干,发现是一个for循环里面在不停地Accept,而这个Accept在没有连接时是阻塞的,当有连接时,起一个新的协程来处理。HttpServer如何处理请求的?一些前置工作 处理请求的一行代码是,可以看出是每个连接单开了一个协程处理:goc。serve(connCtx) 这里的connCtx代入了当前的Server对象:ctx:context。WithValue(baseCtx,ServerContextKey,srv)。。。connCtx:ctx 而且还提供了修改它的hook方法srv。ConnContext,可以在每次Accept时修改原始的contextifcc:srv。ConnContext;cc!nil{connCtxcc(connCtx,rw)ifconnCtxnil{panic(ConnContextreturnednil)}} 它的定义是:ConnContextoptionallyspecifiesafunctionthatmodifiesthecontextusedforanewconnectionc。TheprovidedctxisderivedfromthebasecontextandhasaServerContextKeyvalue。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,可采取如上的方法设置,在每次连接状态改变时执行hook方法:c。setState(c。rwc,StateNew,runHooks)beforeServecanreturnConnStatespecifiesanoptionalcallbackfunctionthatiscalledwhenaclientconnectionchangesstate。SeetheConnStatetypeandassociatedconstantsfordetails。ConnStatefunc(net。Conn,ConnState)开始真正干活 为了能看清楚Accept后,serve方法到底干了什么,我们再简化一下:func(cconn)serve(ctxcontext。Context){。。。for{w,err:c。readRequest(ctx)。。。serverHandler{c。server}。ServeHTTP(w,w。req)。。。}} serve也是一个大循环,循环里面主要是读取一个请求,然后将请求交给Handler处理。 为什么是一个大循环呢?因为每个serve处理的是一个连接,一个连接可以有多次请求。 读请求就显得比较枯燥乏味,按照Http协议,读出URL,header,body等信息。 这里有个细节是在每次读取了一个请求后,还开了一个协程去读下一个请求,也算是做了优化吧。for{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的路由,则按hostpath去寻找,如果没注册带host的路由,则按path寻找路由规则匹配以完全匹配优先,如果注册的路由规则最后一个字符是,则除了完全匹配外,还会以前缀查找 举几个例子来理解一下:带host的匹配规则 注册路由为http。HandleFunc(hello,hello)http。HandleFunc(127。0。0。1hello,hello2) 此时如果执行curlhttp:127。0。0。1:81hello 则会匹配到hello2,但如果执行curlhttp:localhost:81hello 就匹配的是hello前缀匹配 如果注册路由为http。HandleFunc(hello,hello)http。HandleFunc(127。0。0。1hello,hello2) 注意第二个最后还有个,此时如果执行curlhttp:127。0。0。1:81helloroshi 也能匹配到hello2,怎么样,是不是理解了? 找到路由之后就直接调用我们开头注册的方法,如果我们往Response中写入数据,就能返回给客户端,这样一个请求就处理完成了。总结 最后我们回忆下GoHttpServer的要点:用Go起一个HttpServer非常简单GoHttpServer本质是一个大循环,每当有一个新连接时,会起一个新的协程来处理每个连接的处理也是一个大循环,这个循环里做了读取请求、寻找路由、执行逻辑三件大事 感谢能抽空看到这里,如果你能点赞、在看、分享,我会更加感激不尽 搜索关注捉虫大师,后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践