Golang中TCP服务器/客户端使用io.CopyN()卡住的问题
Golang中TCP服务器/客户端使用io.CopyN()卡住的问题 我正在尝试制作一个用于传输文件的TCP服务器。我使用io.CopyN进行读写操作。从服务器端,我将文件发送给客户端,服务器端可以完美发送所有字节,但客户端在读取大约1000000字节后会卡住。有时工作正常,有时会卡住。我使用300MB的PDF文件进行测试。以下是相关代码和输出:
//server
package main
import (
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"strings"
)
func main() {
fmt.Println("In server \n")
ls, err := net.Listen("tcp", ":1234")
defer ls.Close()
if err != nil {
panic(err)
}
i := 1
for {
conn, _ := ls.Accept()
var check string
var check1 string
defer conn.Close()
for {
fmt.Println("Do you want to send more : ")
fmt.Scanf("%s", &check1)
if check == "n" {
break
}
file, err := os.Open(strings.TrimSpace("./" + "Mag" + ".pdf"))
if err != nil {
fmt.Println("inside error 111 : ", i)
log.Fatal(err)
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
fmt.Println("in stat")
}
size := fileInfo.Size()
numberOfTime := size / 1000000
leftByte := size - numberOfTime*1000000
numberOfTimeString := strconv.Itoa(int(numberOfTime))
leftByteString := strconv.Itoa(int(leftByte))
fmt.Println("1000000 times : ", numberOfTimeString)
fmt.Println("Left Bytes : ", leftByteString)
sizeSent, err := fmt.Fprintf(conn, numberOfTimeString+"\n")
if err != nil {
panic(err)
}
sizeSent, err = fmt.Fprintf(conn, leftByteString+"\n")
if err != nil {
panic(err)
}
fmt.Println(sizeSent)
fileWriter := io.Writer(conn)
n, err := io.CopyN(fileWriter, file, size)
if err == io.EOF {
fmt.Println(err, n)
}
fmt.Println(n, "bytes sent")
if err != nil {
log.Println(err)
}
file.Close()
}
}
fmt.Println("We are done with server")
}
//client
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
)
func main() {
c := make(chan os.Signal, 15)
signal.Notify(c, syscall.SIGINT)
go func() {
for {
s := <-c
switch s {
case syscall.SIGINT:
removeDir()
os.Exit(1)
}
}
}()
defer removeDir()
conn, err := net.Dial("tcp", ":1234")
if err != nil {
panic(err)
}
defer conn.Close()
connReadWrite := bufio.NewReader(io.Reader(conn))
var i int
var filename string
for {
i++
if i > 15 {
break
}
nu := strconv.Itoa(i)
os.MkdirAll("./"+nu+"/", os.ModePerm)
filename = "./" + nu + "/image" + nu + ".pdf"
file, err := os.Create(filename)
defer file.Close()
if err != nil {
panic(err)
}
l1, err := connReadWrite.ReadString('\n')
if err != nil {
println(err)
}
println("\n1000000 times :", l1)
l1 = strings.TrimSuffix(l1, "\n")
nb1, err := strconv.Atoi(l1)
if err != nil {
panic(err)
}
l2, err := connReadWrite.ReadString('\n')
if err != nil {
println(err)
}
println("Left Bytes :", l2)
l2 = strings.TrimSuffix(l2, "\n")
nb2, err := strconv.Atoi(l2)
if err != nil {
panic(err)
}
fmt.Println("After convert in Num :", nb1, nb2)
newFileWriter := io.Writer(file)
newFileReader := io.Reader(conn)
for i := 0; i < nb1; i++ {
n, err := io.CopyN(newFileWriter, newFileReader, 1000000)
if i >= 31 {
errFun(err, n)
}
}
n, err := io.CopyN(newFileWriter, newFileReader, int64(nb2))
errFun(err, n)
file.Close()
}
fmt.Println("We are done with client ")
}
func errFun(err error, n int64) {
if err == io.EOF {
fmt.Println("End of file : ", n)
return
} else if n == 0 {
fmt.Println("n is : ", n)
return
} else if err != nil {
fmt.Println(err)
return
}
fmt.Println(err, " : ", n)
}
func removeDir() {
var dir []string
files, err := ioutil.ReadDir("./")
if err != nil {
log.Fatal(err)
}
for _, f := range files {
dir = append(dir, f.Name())
}
for i, dname := range dir {
if i < 1 {
continue
}
file, err := os.Open("./" + dname)
fileInfo, err := file.Stat()
if !fileInfo.IsDir() {
file.Close()
continue
}
err = os.RemoveAll(dname)
if err != nil {
log.Fatal(err)
}
file.Close()
}
输入/输出
从服务器端,当按下Y时发送数据,客户端获取需要读取的字节数,然后我发送文件,客户端进行读取。在图片中,我能够发送两次,但第三次会卡住,有时第一次也会卡住。当卡住时如果按下Ctrl+C,会得到文件结束错误。我实在无法理解这个问题。

更多关于Golang中TCP服务器/客户端使用io.CopyN()卡住的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
我看到很多人关注了这个问题,但为什么没有人回复呢……是不是我的问题需要修改?如果需要的话请告诉我,我真的很想解决这个问题。
更多关于Golang中TCP服务器/客户端使用io.CopyN()卡住的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
感谢您的回复,并对您提到的所有问题表示歉意。我会牢记在心。实际上,我在Stack Overflow上发布了这个问题并得到了解决方案。 https://stackoverflow.com/questions/52233610/client-stuck-when-trying-to-read-with-io-copyn-in-golang/52239522#52239522
关于程序卡住的问题,您可能无法直接看到,但请允许我解释:当客户端成功读取后,会进行第二次读取,而此时服务器仍在持续逐条发送数据。关键点在于我使用了io.copyN(),该函数在读取到N字节或EOF之前不会返回。在我的程序中,有时会在读取最后几个字节时卡住,此时既未收到EOF也未读满N字节。但由于服务器持续发送数据,新数据会进入缓冲区,使得io.copyN()最终读取到数据并返回,这样服务器发送的文件大小就能被io.copyN()正确读取,之后文件内容会被以下代码读取:
l1, err := connReadWrite.ReadString('\n')
此时它获取到了无法转换的数据导致程序恐慌。我删除了一些代码,因为Stack Overflow上有人建议我移除冗余代码,这就是为什么您无法直接看到卡住的具体情况 🙂 我也尝试过其他读取器如ReadFull()、ReadAtLeast()等,但都会在那里卡住…
问题并不在于strconv(),真正的问题是我对conn进行了多次封装。首先我创建了:
connReadWrite := bufio.NewReader(io.Reader(conn))
在使用两次后,我又为conn创建了新的读取器封装:
newFileWriter := io.Writer(file)
newFileReader := io.Reader(conn)
用于读取实际文件,这是错误的做法。删除新的封装器后,我改用原有的读取器:
n, err := io.CopyN(file, connReadWrite , 1000000)
这样就成功解决了问题。
在您的代码中,TCP服务器和客户端使用io.CopyN()时卡住的主要原因是协议同步问题和缓冲读取器未正确处理数据流。具体来说,服务器发送文件数据后,客户端通过bufio.Reader读取头部信息(如块数),但后续的io.CopyN()操作可能从同一个缓冲读取器中消费数据,导致数据不匹配或阻塞。
问题分析:
- 协议设计缺陷:服务器先发送两个字符串(块数和剩余字节数),然后发送文件数据。客户端使用
bufio.Reader读取头部,但文件数据读取时切换到了原始连接(io.Reader(conn)),这可能导致缓冲读取器中残留未消费的数据,破坏同步。 - 缓冲读取器影响:
bufio.Reader会预读取更多数据到缓冲区。当客户端读取头部后,文件数据的一部分可能已被预读到缓冲区中,但后续io.CopyN()从原始连接读取时,会跳过这些缓冲数据,造成死锁或数据丢失。 - 错误处理不完整:
io.CopyN()在遇到错误时未妥善处理,如非EOF错误可能导致循环卡住。
解决方案:
- 统一数据读取源:客户端应始终使用同一个读取器(如
bufio.Reader)来读取所有数据,包括头部和文件内容,避免混合使用缓冲和原始连接。 - 改进协议同步:确保头部和文件数据被完整消费,无残留。
- 优化错误处理:检查
io.CopyN()的返回值,并对非EOF错误进行中断。
以下是修改后的客户端代码示例,关键调整包括:
- 使用
bufio.Reader读取所有数据。 - 移除不必要的类型转换(如
io.Reader(conn))。 - 简化循环逻辑,确保数据流一致。
修改后的客户端代码:
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
"os"
"strconv"
"strings"
)
func main() {
conn, err := net.Dial("tcp", ":1234")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
reader := bufio.NewReader(conn) // 使用同一个bufio.Reader读取所有数据
for i := 1; i <= 15; i++ {
filename := fmt.Sprintf("./%d/image%d.pdf", i, i)
err := os.MkdirAll(fmt.Sprintf("./%d/", i), os.ModePerm)
if err != nil {
log.Printf("Failed to create directory: %v", err)
continue
}
file, err := os.Create(filename)
if err != nil {
log.Printf("Failed to create file: %v", err)
continue
}
// 读取块数
chunkStr, err := reader.ReadString('\n')
if err != nil {
log.Printf("Error reading chunk count: %v", err)
file.Close()
break
}
chunkStr = strings.TrimSpace(chunkStr)
chunks, err := strconv.Atoi(chunkStr)
if err != nil {
log.Printf("Error converting chunk count: %v", err)
file.Close()
break
}
// 读取剩余字节数
remainStr, err := reader.ReadString('\n')
if err != nil {
log.Printf("Error reading remaining bytes: %v", err)
file.Close()
break
}
remainStr = strings.TrimSpace(remainStr)
remain, err := strconv.Atoi(remainStr)
if err != nil {
log.Printf("Error converting remaining bytes: %v", err)
file.Close()
break
}
// 使用io.CopyN从同一个reader读取文件数据
for j := 0; j < chunks; j++ {
_, err := io.CopyN(file, reader, 1000000)
if err != nil {
log.Printf("Error copying chunk %d: %v", j, err)
file.Close()
return
}
}
if remain > 0 {
_, err := io.CopyN(file, reader, int64(remain))
if err != nil {
log.Printf("Error copying remaining bytes: %v", err)
file.Close()
return
}
}
file.Close()
fmt.Printf("File %s received successfully\n", filename)
}
fmt.Println("Client done")
}
服务器代码调整建议:
服务器代码基本正确,但建议在每次循环后重置文件读取位置(如重新打开文件),并移除不必要的defer file.Close()(因为已在循环内关闭)。示例调整:
// 在服务器循环中,确保文件在每次迭代时从头读取
file, err := os.Open("./Mag.pdf")
if err != nil {
log.Fatal(err)
}
// 直接使用file.Stat()和io.CopyN,无需调整
通过以上修改,客户端和服务器将保持协议同步,避免因缓冲读取器导致的卡住问题。测试时,确保网络稳定,并使用大文件(如300MB PDF)验证性能。如果问题持续,检查网络延迟或系统资源限制。


