【Golang开发】Gin框架学习笔记——服务器的运行机制

引言

Gin框架作为Go语言中最受欢迎的Web框架之一,以其高性能和易用性著称,非常适合用于构建高并发的Web服务。如果你不太熟悉Web框架,可以通过阅读Gin源码,分析Gin框架下HTTP服务器的运行机制,大致就能了解大部分Web框架的原理了。通过这篇文章,你将学习:

  1. 基于socket网络编程的基本知识
  2. Gin框架利用Goroutine支持高并发的原理
  3. Gin框架下HTTP服务器的运行机制

一、预备知识

1.1 socket编程

在学习Web开发之前,首先需要具备socket网络编程的基础知识。因为HTTP是应用层协议,其在传输层使用的是TCP协议,那么在编程过程中需要使用操作系统提供socket接口来实现应用层与底层TCP/IP协议的通信,通过读写socket来完成HTTP报文的解析和回复。

基本的客户端------服务端socket通信过程如下图所示:

服务端编程步骤

  1. 创建套接字(socket):使用socket()函数创建一个新的套接字。

  2. 绑定套接字(bind):通过bind()函数将套接字与特定的IP地址和端口号关联起来。

  3. 监听连接(listen):使用listen()函数使服务器套接字监听来自客户端的连接请求。

  4. 接受连接(accept):当客户端请求连接时,accept()函数会接受这个连接。

  5. 读取数据(read/recv):从客户端接收数据。

  6. 发送数据(write/send):向客户端发送数据。

  7. 关闭套接字(close):完成数据传输后,关闭连接。

代码示例:

c++ 复制代码
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>

int main() {
    // 1. 创建Socket
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        std::cerr << "Failed to create socket\n";
        return 1;
    }

    // 2. 绑定IP和端口
    sockaddr_in server_addr{};
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
    server_addr.sin_port = htons(8080);       // 端口8080

    if (bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Failed to bind\n";
        close(server_fd);
        return 1;
    }

    // 3. 监听连接
    if (listen(server_fd, 5) == -1) { // 最多5个等待连接
        std::cerr << "Failed to listen\n";
        close(server_fd);
        return 1;
    }

    std::cout << "Server listening on port 8080...\n";

    // 4. 接受客户端连接
    sockaddr_in client_addr{};
    socklen_t client_len = sizeof(client_addr);
    int client_fd = accept(server_fd, (sockaddr*)&client_addr, &client_len);  // 阻塞,直到有连接请求
    if (client_fd == -1) {
        std::cerr << "Failed to accept connection\n";
        close(server_fd);
        return 1;
    }

    std::cout << "Client connected!\n";

    // 5. 接收和发送数据
    char buffer[1024] = {0};
    ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
    if (bytes_read == -1) {
        std::cerr << "Failed to read from client\n";
    } else {
        std::cout << "Received: " << buffer << std::endl;
        const char* response = "Hello from server!";
        send(client_fd, response, strlen(response), 0);
    }

    // 6. 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}

客户端编程步骤

  1. 创建套接字(socket):同服务端。

  2. 发起连接(connect):使用connect()函数向服务器发起连接请求。

  3. 发送数据(write/send):向服务器发送数据。

  4. 读取数据(read/recv):从服务器接收数据。

  5. 关闭套接字(close):完成数据传输后,关闭连接。

代码示例:

c++ 复制代码
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

int main() {
    // 1. 创建Socket
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd == -1) {
        std::cerr << "Failed to create socket\n";
        return 1;
    }

    // 2. 连接服务端
    sockaddr_in server_addr{};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);                  // 服务端端口
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); // 服务端IP

    if (connect(client_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Failed to connect to server\n";
        close(client_fd);
        return 1;
    }

    std::cout << "Connected to server!\n";

    // 3. 发送和接收数据
    const char* message = "Hello from client!";
    send(client_fd, message, strlen(message), 0);

    char buffer[1024] = {0};
    ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
    if (bytes_read == -1) {
        std::cerr << "Failed to read from server\n";
    } else {
        std::cout << "Received: " << buffer << std::endl;
    }

    // 4. 关闭连接
    close(client_fd);
    return 0;
}

分析

让我们来关注一下服务器端的程序,上述的例子只是一个最简单的示例,实际的服务器程序远比这个复杂。示例中,服务器调用accept时会阻塞,直到有客户端发起connect调用,此时会完成TCP的三次握手

步骤 操作 说明
1 客户端调用 connect() 内核发起第一次握手(SYN)
2 服务端内核响应 SYN-ACK 服务端未调用 accept()前已完成第二次握手
3 客户端内核响应 ACK 握手完成,connect()返回成功
4 服务端调用 accept() 从已建立的连接队列中取出客户端 Socket
plaintext 复制代码
        客户端                         服务端
          |                              |
          |           SYN                |
          |--------------------------->  | 内核完成第二次握手(SYN-ACK)
          |                              | (此时 accept() 可能仍在阻塞)
          |         ACK                  |
          |<---------------------------  | 握手完成
          |                              |
connect() 返回成功                       accept() 返回 client_fd
          |                              |

连接建立后,服务端调用recv等待客户端发送数据,这一步也是阻塞,直到客户端调用send,然后服务端读取数据,并调用send给客户端回复,最后close关闭socket。

这个简单的服务端程序无法支持多个客户端的并发连接,实际项目中,需结合多线程或 I/O 多路复用处理并发连接。

1.2 I/O 多路复用

之前写过一篇【Linux开发】浅析select/poll/epoll与IO多路复用,具体介绍了什么是IO多路复用、为什么要使用IO多路复用、怎样使用IO多路复用,这里就不展开了赘述了。

如果读者还不清楚相关的概念,只需要知道:任何服务端程序要想支持高并发,必须使用操作系统提供的IO多路复用机制,在 Linux 系统上,使用epoll;在 FreeBSD 和 Mac OS X 上,使用kqueue;在 Windows 上,使用IOCP。这些机制允许操作系统在一个线程中同时监控多个 socket 的读写事件,从而实现高效的并发处理。

Go 语言的net/http包在处理网络连接时,默认使用了操作系统提供的 IO 多路复用机制。


二、一图看懂Gin的运行机制

虽然上一章节的socket编程示例跟实际项目相差甚远,但是它描述了所有服务器程序的必须步骤。因此我们可以寻找一下Gin框架中这些步骤的身影。

上图大致揭示了Gin框架的运行机制,首先再回顾一下服务端的基本步骤:

plaintext 复制代码
服务端: socket() → bind() → listen() → accept() → send()/recv() → close()
  1. bind()对应图中创建Server实例,address就是服务器监听的地址
  2. listen()对应图中ServerListen,开启监听
  3. accept()对应图中Accept,并且是在一个循环中不断接收新连接
  4. send()/recv()对应图中蓝色部分,创建一个连接对象扔到Goroutine中进行读写,不阻塞Accpet,这一步是Gin支持高并发的关键
  5. 具体怎么处理的?那就要看这个框架注册的路由和处理函数、中间件等东西了,简单说就是预先对不同的HTTP请求和不同的URL的指定相应的处理方法

基本上所有的Web框架都是类似的运行机制,而Gin的优点就在于直接利用Go语言的Goroutine来实现并发编程,不用手动编写许多复杂的代码。并且,其他语言都得使用一个线程处理一个客户端的读写,而Go语言的协程比线程更加轻量级,一个线程可以处理多个客户端的读写,所以Go语言编写的服务端程序天然能支持更多的并发。这就是为什么Go语言特别适合用来做服务端开发的原因。


三、 Gin源码赏析

上一章介绍了Gin框架的运行机制,如果读者对源码感兴趣,可以继续扒一扒Gin的源码。

Gin使用示例

以下是一个简单的 Gin 框架示例,展示了如何创建基本的路由、处理请求参数、使用中间件以及进行 JSON 响应:

go 复制代码
package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

// Book 结构体用于绑定 JSON 数据
type Book struct {
    Title  string `json:"title"`
    Author string `json:"author"`
}

// 中间件函数
func loggingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 在请求处理前记录日志
        c.Next()
        // 在请求处理后记录日志
    }
}

func main() {
    // 创建一个默认的 Gin 引擎
    r := gin.Default()

    // 使用中间件
    r.Use(loggingMiddleware())

    // 定义 GET 请求路由,用于获取所有书籍(简单模拟)
    r.GET("/books", func(c *gin.Context) {
        books := []Book{
            {Title: "Book1", Author: "Author1"},
            {Title: "Book2", Author: "Author2"},
        }
        c.JSON(http.StatusOK, books)
    })

    // 定义 GET 请求路由,带有参数
    r.GET("/book/:title", func(c *gin.Context) {
        title := c.Param("title")
        // 这里可以根据 title 从数据库或其他数据源获取书籍信息
        // 简单模拟返回一个包含 title 的响应
        c.JSON(http.StatusOK, gin.H{"title": title})
    })

    // 定义 POST 请求路由,用于创建新书籍
    r.POST("/books", func(c *gin.Context) {
        var newBook Book
        if err := c.ShouldBindJSON(&newBook); err!= nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        // 这里可以将 newBook 保存到数据库
        c.JSON(http.StatusCreated, newBook)
    })

    // 启动服务器,监听 8080 端口
    r.Run(":8080")
}

在这个示例中:

  1. 路由定义
    • 使用 r.GET 定义了两个 GET 请求路由,一个用于获取所有书籍,另一个用于根据书籍标题获取特定书籍。
    • 使用 r.POST 定义了一个 POST 请求路由,用于创建新的书籍。
  2. 参数处理
    • /book/:title 路由中,使用 c.Param("title") 获取 URL 中的参数。
    • /books 的 POST 请求中,使用 c.ShouldBindJSON 将请求体中的 JSON 数据绑定到 Book 结构体。
  3. 中间件使用
    • 定义了一个简单的 loggingMiddleware 中间件,并通过 r.Use(loggingMiddleware()) 将其应用到所有路由上。该中间件在请求处理前后都可以执行一些逻辑,这里只是简单示例,实际应用中可以记录请求的详细信息。
  4. JSON 响应
    • 使用 c.JSON 方法返回 JSON 格式的响应。对于 GET 请求,返回书籍列表或特定书籍信息;对于 POST 请求,返回创建的新书籍信息。如果请求处理过程中出现错误,也以 JSON 格式返回错误信息。

源码跟踪

Engine

在 Gin 框架中,Engine 是一个核心结构体,它承载了整个 Web 应用的配置、路由信息以及请求处理逻辑,起到了中枢的作用。以下是Engine的定义:

go 复制代码
// gin.go

type Engine struct {
    // 继承 RouterGroup 的所有字段和方法(如路由前缀 prefix、中间件 handlers 等),使 Engine 具备路由注册、分组管理的能力(如 GET()、Group() 方法)
    RouterGroup
    // 控制是否自动重定向 "尾部带斜杠" 的路由到 "不带斜杠" 的版本(或反之)
    // 例如:若为 true,访问 /api/ 会自动重定向到 /api(或根据路由定义反向重定向)
    RedirectTrailingSlash bool
    // 当路由匹配失败时,是否尝试通过 "修复路径"(如移除重复斜杠、替换编码字符)后再次匹配
    // 例如:访问 /api//v1 会尝试修复为 /api/v1 后匹配路由
    RedirectFixedPath bool
    // 当请求的 HTTP 方法(如 POST)不被路由支持时,是否返回 405 Method Not Allowed 错误
    // 若为 false,则会返回 404 Not Found
    HandleMethodNotAllowed bool
    //是否从请求头(如 X-Forwarded-For 或 X-Real-IP)中获取客户端真实 IP,而非直接使用连接的 IP 地址(适用于反向代理场景)
    ForwardedByClientIP bool
    // AppEngine 已弃用
    // 是否运行在 Google App Engine 环境中,用于适配该环境的特殊处理(如请求代理)
    AppEngine bool
    // 是否使用原始未解码的 URL 路径进行路由匹配(默认使用解码后的路径)
    // 例如:%2F 会被解码为 /,若开启此选项,则按 %2F 匹配
    UseRawPath bool
    // 是否对路径参数进行 URL 解码(如将 %20 转为空格)。默认开启,确保参数取值符合预期
    UnescapePathValues bool
    // 是否自动移除 URL 中的重复斜杠(如将 // 合并为 /)。与 RedirectFixedPath 配合使用,用于标准化路径。
    RemoveExtraSlash bool
    // 用于提取客户端真实 IP 的请求头列表,默认包含 X-Forwarded-For 和 X-Real-IP
    RemoteIPHeaders []string
    // 指定信任的平台(如 X-Appengine-Remote-Addr),用于从特定平台的请求头中获取客户端 IP
    TrustedPlatform string
    // 解析 multipart/form-data 表单时的最大内存限制(默认 32MB),超过此限制的内容会写入临时文件
    MaxMultipartMemory int64
    // 是否启用 HTTP/2 明文模式(H2C),允许在非 HTTPS 环境下使用 HTTP/2 协议
    UseH2C bool
    // 当 c.Request.Context() 不存在时,是否使用自定义的 fallback 上下文(兼容旧版本 Go 的上下文机制)
    ContextWithFallback bool
    // HTML 模板的分隔符(默认是 {{ 和 }}),可自定义为其他符号(如 [[ 和 ]])避免与前端模板冲突
    delims           render.Delims
    // 为 SecureJSON() 方法添加的前缀(默认是 ")]}'\n"),用于防御 JSON 劫持攻击(避免浏览器将 JSON 解析为脚本)
    secureJSONPrefix string
    // HTML 模板渲染器,用于 c.HTML() 方法渲染模板文件(可自定义实现,如集成 html/template 或第三方模板引擎)
    HTMLRender       render.HTMLRender
    // 注册到 HTML 模板中的自定义函数映射(如 {{ len .Name }} 中的 len 函数),用于扩展模板功能
    FuncMap          template.FuncMap
    // 内部使用,存储合并了全局中间件的 404/405 处理链(全局中间件 + 自定义 noRoute/noMethod 处理函数)
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    // 自定义 "404 路由未找到" 的处理函数链(中间件 + 业务逻辑)
    // 例如:通过 engine.NoRoute(custom404Handler) 设置自定义 404 页面
    noRoute          HandlersChain
    // 自定义 "405 方法不允许" 的处理函数链
    // 例如:通过 engine.NoMethod(custom405Handler) 设置自定义 405 响应
    noMethod         HandlersChain
    // Context 对象的对象池,用于复用 Context 实例(避免每次请求创建新对象,减少内存分配和 GC 压力)
    pool             sync.Pool
    // 存储所有 HTTP 方法(GET/POST/PUT 等)的路由树(基于 httprouter 的前缀树实现),是路由匹配的核心数据结构
    trees            methodTrees
    // 路由路径中允许的最大参数数量(如 /user/:id/post/:pid 包含 2 个参数),用于限制复杂路由的性能损耗
    maxParams        uint16
    // 路由路径中允许的最大 "段数"(以 / 分隔的部分),例如 /a/b/c 包含 3 个段,用于限制过长路径
    maxSections      uint16
    // 信任的代理服务器 IP 列表,用于在 ForwardedByClientIP 开启时,仅从信任的代理中提取客户端 IP
    trustedProxies   []string
    // 信任的代理服务器 IP 网段(CIDR 格式),功能与 trustedProxies 类似,但支持网段批量配置
    trustedCIDRs     []*net.IPNet
}

Run

Engine 结构体的 Run 方法,启动一个 HTTP 服务器并开始监听指定地址的请求

go 复制代码
// gin.go

func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
	}
	engine.updateRouteTrees()
	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine.Handler())
	return
}

engine.Handler()中,engine.UseH2CEngine 结构体中的一个布尔字段,用于指示是否启用 HTTP/2 明文模式(H2C)。如果 UseH2Cfalse,说明不使用 H2C,直接返回 engine 本身;反之,返回一个经过 H2C 包装的处理器,以支持 HTTP/2 明文模式的请求处理

go 复制代码
// gin.go

func (engine *Engine) Handler() http.Handler {
	if !engine.UseH2C {
		return engine
	}

	h2s := &http2.Server{}
	return h2c.NewHandler(engine, h2s)
}

使用传入的 addr(监听地址)和 handler(HTTP 请求处理器)创建一个新的 Server 结构体实例

go 复制代码
// server.go

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

Listen

调用新创建的 server 实例的 ListenAndServe 方法。ListenAndServe 方法会执行以下主要操作:

  • 创建一个 net.Listener,用于监听指定的 Addr 地址。
  • 在一个循环中,不断接受来自客户端的连接。
  • 对于每个接受的连接,创建一个新的 conn 实例,并在一个新的 goroutine 中调用 conn.serve 方法来处理该连接的请求。conn.serve 方法负责解析 HTTP 请求、调用相应的处理器处理请求并返回响应。

ListenAndServe 方法会一直阻塞,直到出现错误(例如无法监听指定端口),此时会返回错误信息。ListenAndServe 方法返回的错误会被外层的 ListenAndServe 函数返回,这样调用者可以根据返回的错误进行相应的处理。

go 复制代码
//server.go

func (s *Server) ListenAndServe() error {
	if s.shuttingDown() {
		return ErrServerClosed
	}
	addr := s.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return s.Serve(ln)
}

Accept

Serve中,for循环不断调用Accept接收客户端连接,每个连接通过一个go协程处理读写,这一步实现了高并发。

go 复制代码
// server.go

func (s *Server) Serve(l net.Listener) error {
	if fn := testHookServerServe; fn != nil {
		fn(s, l) // call hook with unwrapped listener
	}

	origListener := l
	l = &onceCloseListener{Listener: l}
	defer l.Close()

	if err := s.setupHTTP2_Serve(); err != nil {
		return err
	}

	if !s.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer s.trackListener(&l, false)

	baseCtx := context.Background()
	if s.BaseContext != nil {
		baseCtx = s.BaseContext(origListener)
		if baseCtx == nil {
			panic("BaseContext returned a nil context")
		}
	}

	var tempDelay time.Duration // how long to sleep on accept failure

	ctx := context.WithValue(baseCtx, ServerContextKey, s)
	for {
		rw, err := l.Accept()
		if err != nil {
			if s.shuttingDown() {
				return ErrServerClosed
			}
			if ne, ok := err.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return err
		}
		connCtx := ctx
		if cc := s.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		tempDelay = 0
		c := s.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)
	}
}

readRequest

for循环不断读取socket缓冲区的数据,解析成HTTP请求。

serverHandler{c.server}.ServeHTTP(w, w.req) 处理HTTP请求。

go 复制代码
// server.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())
        
	// 省略部分代码...

	for {
		w, err := c.readRequest(ctx)
		if c.r.remain != c.server.initialReadLimitSize() {
			// If we read any bytes off the wire, we're active.
			c.setState(c.rwc, StateActive, runHooks)
		}
		if c.server.shuttingDown() {
			return
		}
                
		// 省略部分代码...
                
		inFlightResponse = w
		serverHandler{c.server}.ServeHTTP(w, w.req)
		inFlightResponse = nil
		w.cancelCtx()
		if c.hijacked() {
			c.r.releaseConn()
			return
		}
		w.finishRequest()
		c.rwc.SetWriteDeadline(time.Time{})
		if !w.shouldReuseConnection() {
			if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
				c.closeWriteAndWait()
			}
			return
		}
		c.setState(c.rwc, StateIdle, runHooks)
		c.curReq.Store(nil)

		if !w.conn.server.doKeepAlives() {
			// We're in shutdown mode. We might've replied
			// to the user without "Connection: close" and
			// they might think they can send another
			// request, but such is life with HTTP/1.1.
			return
		}

		if d := c.server.idleTimeout(); d > 0 {
			c.rwc.SetReadDeadline(time.Now().Add(d))
		} else {
			c.rwc.SetReadDeadline(time.Time{})
		}

		// Wait for the connection to become readable again before trying to
		// read the next request. This prevents a ReadHeaderTimeout or
		// ReadTimeout from starting until the first bytes of the next request
		// have been received.
		if _, err := c.bufr.Peek(4); err != nil {
			return
		}

		c.rwc.SetReadDeadline(time.Time{})
	}
}

ServeHTTP

调用Server对象的Handler处理HTTP请求

上面解释过,handler要么是engine本身,要么是支持h2cHandler,二者都实现http.Handler接口的ServerHTTP方法

go 复制代码
// server.go

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}

	handler.ServeHTTP(rw, req)
}
go 复制代码
// server.go

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
go 复制代码
// gin.go

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}
go 复制代码
// h2c.go

func (s h2cHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Handle h2c with prior knowledge (RFC 7540 Section 3.4)
	if r.Method == "PRI" && len(r.Header) == 0 && r.URL.Path == "*" && r.Proto == "HTTP/2.0" {
		if http2VerboseLogs {
			log.Print("h2c: attempting h2c with prior knowledge.")
		}
		conn, err := initH2CWithPriorKnowledge(w)
		if err != nil {
			if http2VerboseLogs {
				log.Printf("h2c: error h2c with prior knowledge: %v", err)
			}
			return
		}
		defer conn.Close()
		s.s.ServeConn(conn, &http2.ServeConnOpts{
			Context:          r.Context(),
			BaseConfig:       extractServer(r),
			Handler:          s.Handler,
			SawClientPreface: true,
		})
		return
	}
	// Handle Upgrade to h2c (RFC 7540 Section 3.2)
	if isH2CUpgrade(r.Header) {
		conn, settings, err := h2cUpgrade(w, r)
		if err != nil {
			if http2VerboseLogs {
				log.Printf("h2c: error h2c upgrade: %v", err)
			}
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		defer conn.Close()
		s.s.ServeConn(conn, &http2.ServeConnOpts{
			Context:        r.Context(),
			BaseConfig:     extractServer(r),
			Handler:        s.Handler,
			UpgradeRequest: r,
			Settings:       settings,
		})
		return
	}
	s.Handler.ServeHTTP(w, r)
	return
}

总结

本文介绍了Web开发的基础知识,描述了Gin框架的运行机制,并且结合部分Gin框架的源码,让读者对Gin框架有一个初步的认识。重点在于结合基础的socket通信过程,在Gin框架中寻找对应的步骤,这样就找到了Gin框架的骨架,之后可以继续探究Gin的其他特性。

相关推荐
雨绸缪7 小时前
Excel上传失败,在剪切板有大量信息。是否保存其内容...
后端
二闹7 小时前
Python中那个看似神秘的if __name__ == __main__,一次给你讲明白
后端·python
用户8356290780517 小时前
C# 转换 Word 文档为图片:解锁文档处理的新维度
后端·c#
HABuo7 小时前
【C++进阶篇】学习C++就看这篇--->多态超详解
c语言·开发语言·c++·后端·学习
前端老鹰8 小时前
Node.js 命令行交互王者:inquirer 模块实战指南
后端·node.js
Java微观世界8 小时前
static在Java中到底怎么用?5分钟搞懂变量、方法、代码块与内部类
后端·面试
他日若遂凌云志9 小时前
深度剖析 Fantasy 框架的多协议网络通信(一):TCP 可靠传输与连接管控
后端
开心就好20259 小时前
Fiddler 抓不到包怎么办?从排查到替代方案的全流程指南
后端
知其然亦知其所以然9 小时前
面试官最爱问的坑:MySQL 中 FLOAT 和 DOUBLE 你真懂吗?
后端·mysql·面试