fasthttp使用笔记&如何实现SSE流式数据

fasthttp特性(相比net/http)

1. 极致的对象重用

​​net/http​​:每个 HTTP 请求都会在堆上分配新的 *http.Request和 *http.Response对象,请求处理完毕后,这些对象会被垃圾回收(GC)掉。在高并发下,这会创建海量的临时对象,给 GC 带来巨大压力。

​​fasthttp​​:使用一个 *fasthttp.RequestCtx对象池。当一个请求到来时,从池中获取一个 RequestCtx对象;请求处理完毕后,重置该对象并放回池中。​​这几乎完全消除了每个请求的内存分配,极大地减轻了 GC 的负担。

2. ​​避免 []byte到 string的转换​

HTTP 协议本质上是字节流([]byte)。
​​net/http​​:为了易用性和安全性,API 大量使用 string类型。例如 req.URL.Path、req.Header.Get(“User-Agent”)都返回 string。每次转换都需要分配新的内存并拷贝数据。

​​fasthttp​​:API 几乎完全基于 []byte。例如 ctx.Path()、ctx.Request.Header.Peek(“User-Agent”)都直接返回底层字节切片。​​它鼓励用户在整个请求处理周期内直接操作 []byte,避免了无数微小的内存分配和拷贝。​

3. ​​连接级别的优化​

​​net/http​​:每个连接由一个独立的 goroutine 处理。对于海量并发连接,goroutine 的调度开销虽然小,但依然存在。

​​fasthttp​​:使用了更高效的 ​​epoll(Linux)/ kqueue(BSD)​​ 事件驱动机制。它使用较少数量的 worker goroutine 来轮询多个连接,​​减少了 goroutine 的调度和上下文切换开销​​。这种模式更适合维持数十万计的并发空闲连接。

fasthttp劣势以及不适用场景

API不兼容net/http​​: 不能将基于 net/http的中间件或框架(如 Gin, Echo)直接用于 fasthttp,导致生态被割裂。

​极易误用​​:由于对象重用,如果你在请求处理函数之外保存了RequestCtx或其中任何字段(如 ctx.Request.URI()的指针,将会导致​​数据竞争和内存污染​​,这是极其危险的Bug。

​功能更简单​​:它可能不支持 net/http中一些不常用或复杂的特性以获得高性能。比如:​​Request|Response|Handler等核心对象完全重写、http.Handler接口不支持​、​​context.Context集成方式不同​(影响值传递)、Body是否允许多次读取、不支持HTTP2等

fasthttp使用示例

[]byte数据需要逃逸出当前请求处理函数的生命周期,必须进行复制

错误示例(未拷贝body,直接传递body指针到异步处理函数中):

func handler(ctx *fasthttp.RequestCtx) {
    // 这只是获取切片,数据会被重用
    body := ctx.PostBody()

    // 启动一个 goroutine 或切片地址传递给异步操作
    go handleBody(body)

    // 响应成功
    ctx.Success("", nil)

    // 此时 handler 函数返回,ctx 被重置并用于下一个请求
    // 问题:body 切片指向的内存即将被下一个请求的数据覆盖,可能导致handleBody读取数据异常!
}

正确示例(对body进行拷贝,传递副本指针到异步处理函数中):

func handler(ctx *fasthttp.RequestCtx) {
    // 这只是获取切片,数据会被重用
    body := ctx.PostBody()

    // 创建一个body副本
    bodyCopy := make([]byte, len(body))
    copy(bodyCopy, body)

    // 启动一个 goroutine 或将其传递给异步操作
    go handleBody(bodyCopy)

    // 响应成功
    ctx.Success("", nil)
}

fasthttp获取请求信息都是返回[]byte,通常使用Peek()获取

func fasthttpHandler(ctx *fasthttp.RequestCtx) {
    // 获取请求路径 - 返回 []byte
    path := ctx.Path()

    // 获取请求方式 - 返回 []byte
    method := ctx.Method()

    // 获取查询参数 - 返回 []byte,需手动转换
    nameBytes := ctx.QueryArgs().Peek("name")
    name := string(nameBytes) // 必须转换!且要注意生命周期
    
    // 获取请求头 - 返回 []byte
    userAgent := ctx.Request.Header.Peek("Customer-Header")
    
    // 读取请求体 - 直接返回 []byte,返回底层缓冲区的引用!
    body := ctx.PostBody()
    
    // 设置响应头
    ctx.Response.Header.SetContentType("application/json")
    
    // 写入响应 - 只接受 []byte
    ctx.Write([]byte(`{"name": "` + name + `"}`))
    // 更高效的写法:
    ctx.SetBodyString(`{"name": "` + name + `"}`)
}

fasthttp大量使用[]byte而不是 string来避免内存分配,所以减少直接转换,避免失去性能优势。

func handler(ctx *fasthttp.RequestCtx) {
    // 错误:每次都会分配新的字符串
    if string(ctx.Method()) == "POST" {
        
    }

    // 正确:尽可能直接使用 []byte 操作
    if bytes.Equal(ctx.Method(), []byte("POST")) {
        
    }
    
    // 或者对于确定需要字符串的情况,在必要时才转换
    if needStringRepresentation {
        pathStr := string(ctx.Path())
        // 使用 pathStr
    }
}

类似于其他 HTTP 库,请求体只能读取一次

func handler(ctx *fasthttp.RequestCtx) {
    // 错误:多次读取请求体
    body1 := ctx.PostBody()
    // ... 一些处理
    body2 := ctx.PostBody() // 可能不是期望的值
    
    // 如果中间有其他操作可能修改了上下文,会导致问题
}

fasthttp的中间件需要显式调用 ctx.Next()

// 日志中间件
func loggingMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
    return func(ctx *fasthttp.RequestCtx) {
        start := time.Now()
        path := string(ctx.Path())
        method := string(ctx.Method())
        
        // 调用下一个处理程序
        next(ctx)
        
        duration := time.Since(start)
        fmt.Printf("%s %s - %d - %v\n", method, path, ctx.Response.StatusCode(), duration)
    }
}

// 认证中间件
func authMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
    return func(ctx *fasthttp.RequestCtx) {
        token := string(ctx.Request.Header.Peek("X-Auth-Token"))
        if token != "my-secret-token" {
            ctx.Error("Unauthorized", fasthttp.StatusUnauthorized)
            return // 重要:认证失败时直接返回,不调用 next
        }
        next(ctx)
    }
}

// 链式使用中间件
func main() {
    handler := func(ctx *fasthttp.RequestCtx) {
        ctx.WriteString("Protected content!")
    }
    
    // 中间件链:先认证,后记录日志
    wrappedHandler := loggingMiddleware(authMiddleware(handler))
    
    fasthttp.ListenAndServe(":8080", wrappedHandler)
}

fasthttp客户端内置了连接池,需要正确配置

package main

import (
    "fmt"
    "time"

    "github.com/valyala/fasthttp"
)

func main() {
    // 配置高性能客户端
    client := &fasthttp.Client{
        Name: "MyClient",
        MaxConnsPerHost: 100, // 每个主机的最大连接数
        
        // 读写超时
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 5 * time.Second,
        
        // 长连接配置
        MaxIdleConnDuration: 5 * time.Minute,
    }
    
    // 使用客户端(注意重用 Request 和 Response 对象)
    req := fasthttp.AcquireRequest()
    resp := fasthttp.AcquireResponse()
    defer func() {
        fasthttp.ReleaseRequest(req)
        fasthttp.ReleaseResponse(resp)
    }()
    
    req.SetRequestURI("http://127.0.0.1/api")
    req.Header.SetMethod("GET")
    
    if err := client.Do(req, resp); err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    
    fmt.Printf("Status: %d\n", resp.StatusCode())
    fmt.Printf("Body: %s\n", resp.Body())
}

fasthttp流式返回数据

func streamResponseHandler(ctx *fasthttp.RequestCtx) {
    // 这只是获取切片,数据会被重用
    body := ctx.PostBody()

    // 创建一个body副本
    bodyCopy := make([]byte, len(body))
    copy(bodyCopy, body)

    // 使用SetBodyStreamWriter返回流式数据
    ctx.Response.SetBodyStreamWriter(sendStreamRequest(bodyCopy))
}

func sendStreamRequest(body byte[]) func(w *bufio.Writer) {
	return func(w *bufio.Writer) {
        var err error

        // 业务处理.....

		// 返回错误信息
        if err != nil {
            // 拼接SSE格式失败信息
            fmt.Fprintf(w, "data: [ERROR] %s\n\n", fmt.Sprintf("请求后端失败:%s", err.Error()))
            w.Flush()
            return
        }
		
        // 写入数据
		w.Write([]byte(fmt.Sprintf("data: %s\n\n", string("xxxx"))))
		w.Flush()
	}
}

fasthttp作为代理客户端转发请求,接收并返回流式数据

func streamResponseHandler(ctx *fasthttp.RequestCtx) {
    // 这只是获取切片,数据会被重用
    body := ctx.PostBody()

    // 创建一个body副本
    bodyCopy := make([]byte, len(body))
    copy(bodyCopy, body)

    ctx.Response.SetBodyStreamWriter(sendStreamRequest(bodyCopy))
}

func sendStreamRequest(body byte[]) func(w *bufio.Writer) {
	return func(w *bufio.Writer) {
        // 构建请求client
		client := &fasthttp.Client{
			StreamResponseBody:  true, // 流式需要开启该选项,这里一定要开启,否则不能流式响应
			Dial:                fasthttp.Dial,
			ReadTimeout:         time.Duration(5) * time.Second,
			WriteTimeout:        time.Duration(5) * time.Second,
			MaxIdleConnDuration: time.Duration(5) * time.Second,
		}

		// 构建请求对象
		req := fasthttp.AcquireRequest()
		defer fasthttp.ReleaseRequest(req)

		// 设置请求参数
		req.SetBodyRaw(body)
        // 设置请求方法
        req.Header.SetMethod("POST")
        // 设置请求地址
        req.SetRequestURI("http://127.0.0.1:9090/api/test")
        // 设置请求头
        // req.Header.Set(key, value)

        // 创建响应对象来接收后端响应
		resp := fasthttp.AcquireResponse()
		defer fasthttp.ReleaseResponse(resp) // 确保释放资源

		// 向后端服务器发起请求
		if err := client.Do(req, resp); err != nil {
			log.Errorf("请求后端失败: %v", err)
			fmt.Fprintf(w, "data: [ERROR] %s\n\n", fmt.Sprintf("请求后端失败:%s", err.Error()))
			w.Flush()
			return
		}

		// 接收后端SSE响应数据
		bodyStream := resp.BodyStream()
		reader := bufio.NewReader(bodyStream)
		for {
			if line, rErr := reader.ReadBytes('\n'); rErr != nil {
				log.Errorf("请求后端失败: %v", rErr)
				break
			} else if len(line) > 0 {
				w.Write(line)
				w.Flush()
			}
		}
	}
}

fasthttp不兼容net/http部分

// 1. 获取参数方式不同, Context上下文对象不同
// ========== net/http ==========
func netHttpHandler(w http.ResponseWriter, r *http.Request) {
    // 获取查询参数 - 返回 string
    name := r.URL.Query().Get("name")
    
    // 获取请求头 - 返回 string
    userAgent := r.Header.Get("User-Agent")
    
    // 读取请求体 - 返回 []byte, 但通常需要错误处理
    body, _ := io.ReadAll(r.Body)
    
    // 设置响应头
    w.Header().Set("Content-Type", "application/json")
    
    // 写入响应 - 接受 string 或 []byte
    fmt.Fprintf(w, `{"name": "%s"}`, name)
}

// ========== fasthttp ==========
func fasthttpHandler(ctx *fasthttp.RequestCtx) {
    // 获取查询参数 - 返回 []byte,需手动转换
    nameBytes := ctx.QueryArgs().Peek("name")
    name := string(nameBytes) // 必须转换!且要注意生命周期
    
    // 获取请求头 - 返回 []byte
    userAgent := ctx.Request.Header.Peek("User-Agent")
    
    // 读取请求体 - 直接返回 []byte
    body := ctx.PostBody()
    
    // 设置响应头
    ctx.Response.Header.SetContentType("application/json")
    
    // 写入响应 - 只接受 []byte
    ctx.Write([]byte(`{"name": "` + name + `"}`)) // 字符串拼接性能差
    // 更高效的写法:
    ctx.SetBodyString(`{"name": "` + name + `"}`)
}

// 2. 中间件生态完全割裂
// ========== net/http ==========
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println(r.URL.Path)
        next.ServeHTTP(w, r) // 调用下一个处理器
    })
}

// ========== fasthttp ==========
func loggingMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
    return func(ctx *fasthttp.RequestCtx) {
        log.Printf("%s", ctx.Path())
        next(ctx) // 调用下一个处理器
    }
}

// 3. http.Handler接口不支持, fasthttp未实现net/http的Handler接口,其他框架实现比如Gin等
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

// 4. fasthttp默认不支持http2协议,需要安装其他模块,比如http2

目前只踩过这些大坑,后续使用过程中遇到的大坑,再继续更新!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QuietJiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值