Golang中io.Copy与Chrome浏览器的问题处理

Golang中io.Copy与Chrome浏览器的问题处理 我有一个文件服务器和一个代理服务器,通过代理服务器我可以通过HTTP访问文件服务器。

简单的文件服务器:

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "tmp/1.txt") // 情况 1
        http.ServeFile(w, r, "tmp/1.mp4") // 情况 2, 3
    })
    http.ListenAndServe("localhost:3000", nil)

情况 1 - 小文件 情况 2 - 大文件

type Server struct {
    conn net.Conn
}

func (s *Server) Handle(w http.ResponseWriter, r *http.Request) error {
    if err := r.Write(s.conn); err != nil {
        log.Fatal("req.Write ", err)
    }

    resp, err := http.ReadResponse(bufio.NewReader(s.conn), r)
    if err != nil {
        return fmt.Errorf("http.ReadResponse: %s", err)
    }

    defer  resp.Body.Close()

    copyHeader(w.Header(), resp.Header)
    w.WriteHeader(resp.StatusCode)

    if _, err := io.Copy(w, resp.Body); err != nil {
        if isEPIPE(err) {
              fmt.Println("Client closed the connection, couldn't copy response")
        } else {
            fmt.Printf("copy err: %s\n", err)
        }
    }
    return nil
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := s.Handle(w, r); err != nil {
        http.Error(w, err.Error(), http.StatusBadGateway)
    }
}

func copyHeader(dst, src http.Header) {
    ....
}

func isEPIPE(err error) bool {
    var syscallErr syscall.Errno
    b := errors.As(err, &syscallErr) && syscallErr == syscall.EPIPE
    return b
}

func main() {
    conn, _ := net.Dial("tcp", "localhost:3000")
    server := &Server{conn}
    log.Fatal(http.ListenAndServe("localhost:8000", server))
}

在情况 1 中,一切工作正常。 在情况 2 中,当我使用 Safari 浏览器时,一切工作正常。 在情况 3 中,当我使用 Chrome 浏览器时,我得到错误 syscall.EPIPE。

我就是想不明白为什么客户端(Chrome 浏览器)会关闭连接???


更多关于Golang中io.Copy与Chrome浏览器的问题处理的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中io.Copy与Chrome浏览器的问题处理的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在代理服务器中使用 io.Copy 处理大文件时遇到 EPIPE 错误,这通常是因为 Chrome 浏览器在下载大文件时使用了分块传输编码(chunked encoding)或提前关闭了连接。以下是关键问题和解决方案:

问题分析

Chrome 在处理大文件下载时:

  1. 可能使用 Range 请求实现断点续传
  2. 可能提前关闭连接以优化性能
  3. 对响应头的处理比 Safari 更严格

解决方案

1. 正确处理响应头传输

确保响应头完整传递,特别是 Content-Length

func (s *Server) Handle(w http.ResponseWriter, r *http.Request) error {
    if err := r.Write(s.conn); err != nil {
        return fmt.Errorf("req.Write: %w", err)
    }

    resp, err := http.ReadResponse(bufio.NewReader(s.conn), r)
    if err != nil {
        return fmt.Errorf("http.ReadResponse: %w", err)
    }
    defer resp.Body.Close()

    // 复制所有头部
    for k, v := range resp.Header {
        w.Header()[k] = v
    }
    
    // 确保 Content-Length 正确设置
    if resp.ContentLength >= 0 {
        w.Header().Set("Content-Length", strconv.FormatInt(resp.ContentLength, 10))
    }
    
    w.WriteHeader(resp.StatusCode)

    // 使用带缓冲的复制
    _, err = io.Copy(w, resp.Body)
    if err != nil && !isEPIPE(err) {
        return fmt.Errorf("io.Copy: %w", err)
    }
    return nil
}

2. 处理 Range 请求

Chrome 可能发送 Range 头请求部分内容:

func (s *Server) Handle(w http.ResponseWriter, r *http.Request) error {
    // 传递原始请求到后端
    backendReq, err := http.NewRequest(r.Method, "http://localhost:3000"+r.URL.Path, nil)
    if err != nil {
        return fmt.Errorf("NewRequest: %w", err)
    }
    
    // 复制所有头部
    for k, v := range r.Header {
        backendReq.Header[k] = v
    }
    
    if err := backendReq.Write(s.conn); err != nil {
        return fmt.Errorf("req.Write: %w", err)
    }

    resp, err := http.ReadResponse(bufio.NewReader(s.conn), backendReq)
    if err != nil {
        return fmt.Errorf("http.ReadResponse: %w", err)
    }
    defer resp.Body.Close()

    // 复制响应头
    copyHeader(w.Header(), resp.Header)
    w.WriteHeader(resp.StatusCode)

    // 使用带错误处理的复制
    _, err = io.Copy(w, resp.Body)
    if err != nil {
        // EPIPE 错误可以忽略,表示客户端提前关闭连接
        if !isEPIPE(err) && !errors.Is(err, net.ErrClosed) {
            return fmt.Errorf("io.Copy: %w", err)
        }
    }
    return nil
}

3. 改进连接管理

使用连接池而不是单一连接:

type Server struct {
    addr string
}

func (s *Server) Handle(w http.ResponseWriter, r *http.Request) error {
    // 为每个请求创建新连接
    conn, err := net.Dial("tcp", s.addr)
    if err != nil {
        return fmt.Errorf("Dial: %w", err)
    }
    defer conn.Close()

    // 设置写超时
    conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
    if err := r.Write(conn); err != nil {
        return fmt.Errorf("req.Write: %w", err)
    }

    // 设置读超时
    conn.SetReadDeadline(time.Now().Add(30 * time.Second))
    resp, err := http.ReadResponse(bufio.NewReader(conn), r)
    if err != nil {
        return fmt.Errorf("http.ReadResponse: %w", err)
    }
    defer resp.Body.Close()

    // 复制头部
    copyHeader(w.Header(), resp.Header)
    w.WriteHeader(resp.StatusCode)

    // 使用带缓冲的复制,设置超时
    buf := make([]byte, 32*1024) // 32KB 缓冲区
    _, err = io.CopyBuffer(w, resp.Body, buf)
    
    // 忽略客户端关闭连接的错误
    if err != nil && !isEPIPE(err) && !errors.Is(err, io.EOF) {
        return fmt.Errorf("io.CopyBuffer: %w", err)
    }
    
    return nil
}

4. 完整的代理服务器示例

func main() {
    proxy := &http.Server{
        Addr: "localhost:8000",
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 连接到后端服务器
            backendConn, err := net.DialTimeout("tcp", "localhost:3000", 10*time.Second)
            if err != nil {
                http.Error(w, err.Error(), http.StatusBadGateway)
                return
            }
            defer backendConn.Close()

            // 发送请求到后端
            if err := r.Write(backendConn); err != nil {
                http.Error(w, err.Error(), http.StatusBadGateway)
                return
            }

            // 读取响应
            resp, err := http.ReadResponse(bufio.NewReader(backendConn), r)
            if err != nil {
                http.Error(w, err.Error(), http.StatusBadGateway)
                return
            }
            defer resp.Body.Close()

            // 复制响应头
            for k, v := range resp.Header {
                w.Header()[k] = v
            }
            w.WriteHeader(resp.StatusCode)

            // 复制响应体,忽略客户端关闭错误
            _, err = io.Copy(w, resp.Body)
            if err != nil && !isEPIPE(err) && !errors.Is(err, io.ErrClosedPipe) {
                log.Printf("Copy error: %v", err)
            }
        }),
        ReadTimeout:  30 * time.Second,
        WriteTimeout: 30 * time.Second,
    }
    
    log.Fatal(proxy.ListenAndServe())
}

关键点

  1. EPIPE 错误处理io.Copy 返回 EPIPE 表示客户端(Chrome)在下载完成前关闭了连接,这在 HTTP 代理中是正常情况
  2. 连接管理:为每个请求使用独立的连接,避免连接复用问题
  3. 超时设置:为读写操作设置合理的超时时间
  4. 头部传递:确保所有响应头正确传递,特别是 Content-TypeContent-Length

Chrome 浏览器对网络连接的处理比 Safari 更激进,可能会提前关闭空闲连接或使用不同的传输策略,上述代码通过正确处理这些边界情况来解决 EPIPE 错误。

回到顶部