一. 前言
虽然有很多人是比较了解计算机网络,以及一些web框架是如何做TCP协议解析的,但是对于我个人来说,这方面的知识还是有所欠缺的,正好今天别人问我postman上的请求是如何进入你本地跑的服务的?当时我就有点懵,但是没有什么问题是debug解决不了的,所以在这我们就来看看它是如何处理restful调用的。
二. Echo框架
做golang后端web开发的应该都了解gin、echo、beego等框架的,简单说他们都是处理http请求的框架,都有数据绑定与验证、丰富的中间件支持、路由分组、高效的json处理等能力。但是echo相对更轻量、极简,也是我平常工作使用的框架,所以我就以此为示例来讲解一下echo框架是如何处理restful调用的。
下面是一段echo的代码,功能很简单就是注册了一个路由,当请求到/user/:id时,会调用对应的函数,并返回一个json对象。
go
package main
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
// 创建一个新的 Echo 实例
e := echo.New()
// 注册路由
e.GET("/user/:id", func(c echo.Context) error {
id := c.Param("id")
fmt.Println("User ID:", id)
return c.JSON(http.StatusOK, map[string]string{"id": id})
})
// 启动服务器
e.Logger.Fatal(e.Start(":8080")) // 在 8080 端口启动服务器
}
接下来我们仔细看看这个代码是如何接受restful调用的,首先debug启动服务将断点打在e := echo.New(),进入New方法的内部,可以看到它主要初始化了一个router
go
func New() (e *Echo) {
e = &Echo{
filesystem: createFilesystem(),
Server: new(http.Server),
TLSServer: new(http.Server),
// ...省略其他代码...
ListenerNetwork: "tcp",
}
// ...省略其他代码...
e.router = NewRouter(e)
e.routers = map[string]*Router{}
return
}
继续执行代码进入e.GET方法的内部,往深debug几步,可以看到这里是将GET方法注册到router中,并返回一个Route对象
go
func (e *Echo) add(host, method, path string, handler HandlerFunc, middlewares ...MiddlewareFunc) *Route {
router := e.findRouter(host)
//FIXME: when handler+middleware are both nil ... make it behave like handler removal
name := handlerName(handler)
route := router.add(method, path, name, func(c Context) error {
h := applyMiddleware(handler, middlewares...)
return h(c)
})
// ...省略其他代码...
return route
}
回到main函数中继续debug,去看e.Start(":8080")是如何监听端口的,首先可以看到它将地址信息传给了e.Server.Addr,然后调用e.configureServer方法,最后调用e.Server.Serve(e.Listener)方法,启动服务器,并监听端口。
go
func (e *Echo) Start(address string) error {
e.startupMutex.Lock()
e.Server.Addr = address
if err := e.configureServer(e.Server); err != nil {
e.startupMutex.Unlock()
return err
}
e.startupMutex.Unlock()
return e.Server.Serve(e.Listener)
}
ok到这肯定还看不出它到底是如何监听8080端口的请求的,也看不出来请求是在哪一步进来的,更看不出它是如何找到我们注册的路由将请求发送过来的,那么就继续debug,先从e.configureServer方法进去,看它内部在干嘛,看到一个关键的函数l, err := newListener(s.Addr, e.ListenerNetwork),继续进入newListener,再往里debug,一直走到头,原来就是这个listenFunc在执行监听的逻辑的。
go
listenFunc func(syscall.Handle, int) error = syscall.Listen
go
func (fd *netFD) listenStream(ctx context.Context, laddr sockaddr, backlog int, ctrlCtxFn func(context.Context, string, string, syscall.RawConn) error) error {
var err error
if err = setDefaultListenerSockopts(fd.pfd.Sysfd); err != nil {
return err
}
var lsa syscall.Sockaddr
// ...省略其他代码...
if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
return os.NewSyscallError("bind", err)
}
if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
return os.NewSyscallError("listen", err)
}
// ...省略其他代码...
fd.setAddr(fd.addrFunc()(lsa), nil)
return nil
}
上面找到监听的的函数了,但是外部的请求是直接到这被监听到的吗?显然不是,它是在e.Server.Serve(e.Listener)这段函数中的,继续进入这段函数,可以看到它调用了l.Accept()方法,点进去可以看到它的注释上写的就是监听请求,但是继续debug你会发现它不执行了,而且服务以及启动了。所以这又带来了一个疑问,为什么下面的err后的逻辑没有被执行,而是直接结束了。
go
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
// ...省略其他代码...
for {
rw, err := l.Accept()
if err != nil {
// ...省略其他代码...
return err
}
// ...省略其他代码...
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
带着上面的疑问我们使用postman来发起请求,而且请求立马到了l.Accept()处的逻辑,这里当有新的请求进来,就会再开一个协程来处理请求,进入c.serve方法中,这个方法很大,下面示例代码做了很多减化,这里面有c.readRequest方法,这个方法主要是解析请求,如何继续执行,直到走到serverHandler{c.server}.ServeHTTP(w, w.req)这里,进入其中,基本上就明朗了。
go
func (c *conn) serve(ctx context.Context) {
if ra := c.rwc.RemoteAddr(); ra != nil {
c.remoteAddr = ra.String()
}
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
var inFlightResponse *response
defer func() {
// ...省略其他代码...
}()
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
// ...省略其他代码...
if err := tlsConn.HandshakeContext(ctx); err != nil {
// ...省略其他代码...
}
// Restore Conn-level deadlines.
// ...省略其他代码...
}
// HTTP/1.x from here on.
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
// ...省略其他代码...
for {
w, err := c.readRequest(ctx)
// ...省略其他代码...
// Expect 100 Continue support
req := w.req
// ...省略其他代码...
c.curReq.Store(w)
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
// ...省略其他代码...
serverHandler{c.server}.ServeHTTP(w, w.req)
// ...省略其他代码...
}
}
接着上面,我们继续执行进入ServeHTTP方法内部,可以看到先通过findRouter解析出我们一开始注册的路由(/user/:id),然后再去执行路由注册的handler方法 h(c)。
go
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Acquire context
c := e.pool.Get().(*context)
c.Reset(r, w)
var h HandlerFunc
if e.premiddleware == nil {
e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
h = c.Handler()
h = applyMiddleware(h, e.middleware...)
}
// ...省略其他代码...
// Execute chain
if err := h(c); err != nil {
e.HTTPErrorHandler(err, c)
}
// Release context
e.pool.Put(c)
}
执行完handler方法后,会执行c.Write方法,最终将数据写到客户端。
go
// Write writes the data to the connection as part of an HTTP reply.
func (r *Response) Write(b []byte) (n int, err error) {
if !r.Committed {
if r.Status == 0 {
r.Status = http.StatusOK
}
r.WriteHeader(r.Status)
}
n, err = r.Writer.Write(b)
r.Size += int64(n)
for _, fn := range r.afterFuncs {
fn()
}
return
}
RESTful的一次请求处理全流程,如下所示:
三. 总结
从上面的debug分析中我们可以初步知道了echo是如何监听8080端口,如何接收请求,如何匹配路由,以及如何发出回复的,但还是引入了其它问题,比如net/http包是如何建立tcp连接的,http中的数据是如何解析出来的,syscall.Listen是如何监听的等等问题,我们下篇再研究下。