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
更多关于Golang中io.Copy与Chrome浏览器的问题处理的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在代理服务器中使用 io.Copy 处理大文件时遇到 EPIPE 错误,这通常是因为 Chrome 浏览器在下载大文件时使用了分块传输编码(chunked encoding)或提前关闭了连接。以下是关键问题和解决方案:
问题分析
Chrome 在处理大文件下载时:
- 可能使用
Range请求实现断点续传 - 可能提前关闭连接以优化性能
- 对响应头的处理比 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())
}
关键点
- EPIPE 错误处理:
io.Copy返回EPIPE表示客户端(Chrome)在下载完成前关闭了连接,这在 HTTP 代理中是正常情况 - 连接管理:为每个请求使用独立的连接,避免连接复用问题
- 超时设置:为读写操作设置合理的超时时间
- 头部传递:确保所有响应头正确传递,特别是
Content-Type和Content-Length
Chrome 浏览器对网络连接的处理比 Safari 更激进,可能会提前关闭空闲连接或使用不同的传输策略,上述代码通过正确处理这些边界情况来解决 EPIPE 错误。

