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,会得到文件结束错误。我实在无法理解这个问题。

Screenshot%20from%202018-09-03%2015-07-32


更多关于Golang中TCP服务器/客户端使用io.CopyN()卡住的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

我看到很多人关注了这个问题,但为什么没有人回复呢……是不是我的问题需要修改?如果需要的话请告诉我,我真的很想解决这个问题。

更多关于Golang中TCP服务器/客户端使用io.CopyN()卡住的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在你发布的屏幕截图中,你是否看到了来自客户端的错误信息,提示由于向 strconv.Atoi() 传递了错误数据而导致程序崩溃?在调用 strconv.Atoi() 之前,请尝试检查传递给它的数据,并在出现问题时让程序输出更详细的信息。这样当发生错误时,它就能直接定位到出问题的具体代码行。

我复制了你帖子中的客户端和服务器代码并运行。发现大约有三分之一的情况下,客户端会因同样的错误信息而失败。(我从未遇到过"卡住"的情况,只有这个错误。)

问题出现在客户端第90行的 strconv.Atoi() 调用处。它得到的不是一个包含整数的字符串(如"835474"),而是一个充满垃圾字符的超长字符串,无法解析。

顺便说一句,你发布的客户端代码末尾缺少一个’}’。在发布代码时,你可以通过将代码复制回.go文件来检查发布的代码是否正确,确保它能编译/运行。不要发布无法编译的代码,然后疑惑为什么没有人回复。

感谢您的回复,并对您提到的所有问题表示歉意。我会牢记在心。实际上,我在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()操作可能从同一个缓冲读取器中消费数据,导致数据不匹配或阻塞。

问题分析:

  1. 协议设计缺陷:服务器先发送两个字符串(块数和剩余字节数),然后发送文件数据。客户端使用bufio.Reader读取头部,但文件数据读取时切换到了原始连接(io.Reader(conn)),这可能导致缓冲读取器中残留未消费的数据,破坏同步。
  2. 缓冲读取器影响bufio.Reader会预读取更多数据到缓冲区。当客户端读取头部后,文件数据的一部分可能已被预读到缓冲区中,但后续io.CopyN()从原始连接读取时,会跳过这些缓冲数据,造成死锁或数据丢失。
  3. 错误处理不完整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)验证性能。如果问题持续,检查网络延迟或系统资源限制。

回到顶部