目录

浅谈 Go 的 Web 框架 Echo 是如何处理 RESTful 调用的

一. 前言

虽然有很多人是比较了解计算机网络,以及一些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的一次请求处理全流程,如下所示:

sequenceDiagram participant Client participant TCP_Listener participant Goroutine_Pool participant Router participant Middleware participant Handler Client->>TCP_Listener: 建立TCP连接 TCP_Listener->>Goroutine_Pool: 分配处理协程 Goroutine_Pool->>Router: 解析请求路径 Router->>Middleware: 执行前置中间件 Middleware->>Handler: 执行业务逻辑 Handler->>Middleware: 执行后置中间件 Middleware->>Client: 返回HTTP响应

三. 总结

从上面的debug分析中我们可以初步知道了echo是如何监听8080端口,如何接收请求,如何匹配路由,以及如何发出回复的,但还是引入了其它问题,比如net/http包是如何建立tcp连接的,http中的数据是如何解析出来的,syscall.Listen是如何监听的等等问题,我们下篇再研究下。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
uhakadotcom1 小时前
RunPod:AI云计算的强大助手
后端·面试·github
Pitayafruit1 小时前
📌 Java 工程师进阶必备:Spring Boot 3 + Netty 构建高并发即时通讯服务
spring boot·后端·netty
uhakadotcom1 小时前
Google AlloyDB AI 与 PostgreSQL 的核心区别
后端·面试·github
uhakadotcom1 小时前
使用Go语言编写简单爬虫程序
后端·面试·github
梦想实现家_Z1 小时前
SpringBoot实现MCP Server实战详解
spring boot·后端·mcp
bobz9652 小时前
qemu 对于外部网卡的配置方式
后端
techdashen3 小时前
Rust主流框架性能比拼: Actix vs Axum vs Rocket
开发语言·后端·rust
普通网友3 小时前
内置AI与浏览器的开源终端Wave Terminal安装与远程连接内网服务器教程
开发语言·后端·golang
Gvemis⁹4 小时前
Scala总结(八)
开发语言·后端·scala
Asthenia04124 小时前
详细解析Canal如何解析MySQL Binlog+Json格式的细节
后端