You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go语言TCP文件传输:服务端读取无限循环问题求助

解决TCP文件传输中服务端无限循环无法识别EOF的问题

你的问题核心在于TCP是无边界的字节流协议,服务端无法天然区分"文件内容结束"和"客户端暂时没发数据",所以io.Copy会一直阻塞等待,直到连接关闭或出错。下面是两种无需超时、停止词的Go语言解决方案:

方案1:传输文件大小作为边界标记

在发送文件名后,先传输文件的字节长度,服务端读取到长度后,只读取对应字节数的内容,就能准确判断文件结束位置,无需依赖连接关闭。

客户端代码示例

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "strings"
)

func main() {
    // 打开要发送的文件
    filePath := "test.txt"
    f, err := os.Open(filePath)
    if err != nil {
        log.Fatalf("open file failed: %v", err)
    }
    defer f.Close()

    // 获取文件信息(文件名、大小)
    fileInfo, err := f.Stat()
    if err != nil {
        log.Fatalf("get file stat failed: %v", err)
    }
    fileName := fileInfo.Name()
    fileSize := fileInfo.Size()

    // 连接服务端
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        log.Fatalf("dial server failed: %v", err)
    }
    defer conn.Close()

    // 发送文件名(用换行分隔)
    if _, err := fmt.Fprintf(conn, "%s\n", fileName); err != nil {
        log.Fatalf("send filename failed: %v", err)
    }

    // 发送文件大小(用字符串+换行简化,生产环境建议用binary包保证跨平台字节序)
    if _, err := fmt.Fprintf(conn, "%d\n", fileSize); err != nil {
        log.Fatalf("send file size failed: %v", err)
    }

    // 发送文件内容,只传输指定大小的字节
    if _, err := io.CopyN(conn, f, fileSize); err != nil && err != io.EOF {
        log.Fatalf("send file content failed: %v", err)
    }

    // 读取服务端响应
    resp, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil {
        log.Printf("read response failed: %v", err)
    }
    log.Printf("server response: %s", strings.TrimSpace(resp))
}

服务端代码示例

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "strconv"
    "strings"
)

func main() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatalf("listen failed: %v", err)
    }
    defer listener.Close()
    log.Println("server listening on :8080")

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("accept failed: %v", err)
            continue
        }
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)

    // 读取文件名
    fileName, err := reader.ReadString('\n')
    if err != nil {
        log.Printf("read filename failed: %v", err)
        fmt.Fprintf(conn, "ERR: read filename failed\n")
        return
    }
    fileName = strings.TrimSpace(fileName)

    // 读取文件大小
    sizeStr, err := reader.ReadString('\n')
    if err != nil {
        log.Printf("read file size failed: %v", err)
        fmt.Fprintf(conn, "ERR: read file size failed\n")
        return
    }
    fileSize, err := strconv.ParseInt(strings.TrimSpace(sizeStr), 10, 64)
    if err != nil {
        log.Printf("parse file size failed: %v", err)
        fmt.Fprintf(conn, "ERR: invalid file size\n")
        return
    }

    // 创建输出文件
    outFile, err := os.Create(fileName)
    if err != nil {
        errMsg := fmt.Sprintf("create file failed: %v", err)
        log.Println(errMsg)
        fmt.Fprintf(conn, "ERR: %s\n", errMsg)
        return
    }
    defer outFile.Close()

    // 读取指定大小的内容写入文件
    _, err = io.CopyN(outFile, reader, fileSize)
    if err != nil && err != io.EOF {
        errMsg := fmt.Sprintf("write file failed: %v", err)
        log.Println(errMsg)
        fmt.Fprintf(conn, "ERR: %s\n", errMsg)
        return
    }

    // 返回成功响应
    fmt.Fprintf(conn, "OK: file saved successfully\n")
    log.Printf("file %s saved, size: %d bytes", fileName, fileSize)
}

优缺点:适合多文件连续传输场景,无需断开连接;但需要提前知道文件大小,动态生成的流式内容不适用。


方案2:客户端发送完文件后关闭写端

TCP是全双工协议,客户端可以单独关闭连接的写端(CloseWrite()),此时服务端会收到EOF信号,io.Copy会自动停止读取,同时客户端仍能读取服务端的响应。

客户端代码调整(核心部分)

// 发送文件内容
if _, err := io.Copy(conn, f); err != nil {
    log.Fatalf("send file content failed: %v", err)
}

// 关闭写端,告知服务端数据已发送完毕
if err := conn.CloseWrite(); err != nil {
    log.Printf("close write failed: %v", err)
}

// 读取服务端响应
resp, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
    log.Printf("read response failed: %v", err)
}
log.Printf("server response: %s", strings.TrimSpace(resp))

服务端代码简化(核心部分)

func handleConnection(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)

    // 读取文件名
    fileName, err := reader.ReadString('\n')
    if err != nil {
        log.Printf("read filename failed: %v", err)
        fmt.Fprintf(conn, "ERR: read filename failed\n")
        return
    }
    fileName = strings.TrimSpace(fileName)

    // 创建文件
    outFile, err := os.Create(fileName)
    if err != nil {
        errMsg := fmt.Sprintf("create file failed: %v", err)
        log.Println(errMsg)
        fmt.Fprintf(conn, "ERR: %s\n", errMsg)
        return
    }
    defer outFile.Close()

    // io.Copy会在客户端关闭写端时收到EOF并停止
    _, err = io.Copy(outFile, reader)
    if err != nil && err != io.EOF {
        errMsg := fmt.Sprintf("write file failed: %v", err)
        log.Println(errMsg)
        fmt.Fprintf(conn, "ERR: %s\n", errMsg)
        return
    }

    // 返回成功响应
    fmt.Fprintf(conn, "OK: file saved\n")
    log.Printf("file %s saved", fileName)
}

优缺点:实现简单,无需额外传输元数据;但单次连接只能传输一个文件,若要传多个需重新协商边界。


内容的提问来源于stack exchange,提问作者GhostVision

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 20:39:05