一、传输整体流程
发送端 (客户端) ↔ 接收端 (服务端) 自定义简易传输协议:
- 客户端先发送文件名
- 服务端收到文件名,回复
ok - 客户端收到 ok 后,分片读取本地文件,发送二进制文件内容
- 服务端循环接收二进制数据,写入新文件,传输完成
二、核心前置 API 说明
os.Stat(path):获取文件元信息,可拿到文件名、文件大小,校验文件是否存在os.Open():只读打开本地文件(发送端使用)os.Create():创建新文件(接收端使用)file.Read(buf):读取文件到缓冲区,返回实际读取字节数,读到文件末尾返回io.EOFconn.Read/conn.Write:TCP 连接读写,网络数据传输- 缓冲区分片传输:使用 4KB 缓冲,避免一次性加载大文件造成内存溢出
三、客户端发送代码 send.go
go
Go
package main
import (
"fmt"
"io"
"net"
"os"
)
func main() {
// 输入待传输文件路径
var path string
fmt.Println("请输入需要传输的文件路径:")
fmt.Scan(&path)
// 获取文件信息,校验文件是否存在
info, err := os.Stat(path)
if err != nil {
fmt.Println("os.Stat err =", err)
return
}
// 主动连接文件服务端
conn, err1 := net.Dial("tcp", "127.0.0.1:8000")
if err1 != nil {
fmt.Println("net.Dial err1 = ", err1)
return
}
defer conn.Close()
// 第一步:发送文件名给服务端
_, err := conn.Write([]byte(info.Name()))
if err != nil {
fmt.Println("发送文件名 err = ", err)
return
}
// 等待服务端ok回复
buf := make([]byte, 1024)
n, err1 := conn.Read(buf)
if err1 != nil {
fmt.Println("读取服务端响应 err1 = ", err1)
return
}
if string(buf[:n]) != "ok" {
fmt.Println("服务端未就绪,终止传输")
return
}
// 打开本地待发送文件
f, err := os.Open(path)
if err != nil {
fmt.Println("打开文件失败 err =", err)
return
}
defer f.Close()
// 4KB缓冲区循环分片发送文件内容
sendBuf := make([]byte, 1024*4)
for {
readNum, err := f.Read(sendBuf)
if err != nil {
if err == io.EOF {
fmt.Println("文件发送完毕")
} else {
fmt.Println("读取文件 err = ", err)
}
break
}
// 发送本次读到的有效数据
_, err = conn.Write(sendBuf[:readNum])
if err != nil {
fmt.Println("发送文件数据失败 err =", err)
return
}
}
}
四、服务端接收代码 recv.go
go
Go
package main
import (
"fmt"
"io"
"net"
"os"
)
func main() {
// 监听tcp端口
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Listen err =", err)
return
}
defer listener.Close()
fmt.Println("文件接收服务启动,监听8000端口")
// 阻塞等待客户端连接
conn, err1 := listener.Accept()
if err1 != nil {
fmt.Println("listener.Accept err =", err1)
return
}
defer conn.Close()
fmt.Println("客户端已连接")
// 先读取客户端发送的文件名
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("读取文件名 err =", err)
return
}
fileName := string(buf[:n])
fmt.Println("待接收文件名称:", fileName)
// 向客户端回复ok,表示准备接收文件内容
_, err = conn.Write([]byte("ok"))
if err != nil {
fmt.Println("发送ok响应失败 err =", err)
return
}
// 调用函数接收完整文件
RecvFile(fileName, conn)
}
// RecvFile 根据文件名创建文件,循环接收网络数据写入本地
func RecvFile(fileName string, conn net.Conn) {
f, err := os.Create(fileName)
if err != nil {
fmt.Println("os.Create 创建文件 err = ", err)
return
}
defer f.Close()
recvBuf := make([]byte, 1024*4)
for {
readNum, err := conn.Read(recvBuf)
if err != nil {
if err == io.EOF {
fmt.Println("文件接收完毕")
} else {
fmt.Println("conn.Read 读取文件数据 err =", err)
}
break
}
if readNum == 0 {
fmt.Println("文件传输结束")
break
}
// 将读到的有效数据写入本地文件
_, err = f.Write(recvBuf[:readNum])
if err != nil {
fmt.Println("写入文件失败 err =", err)
return
}
}
}
五、运行测试步骤
- 先运行接收端 recv.go,启动监听服务
- 新开终端运行发送端 send.go
- 输入本地文件路径(图片、txt、压缩包均可)
- 传输完成后,服务端同级目录生成同名文件
六、重点复习 & 踩坑总结
- 必须分片读写文件,大文件一次性读取会造成内存溢出,统一使用 4KB 缓冲
- 读写缓冲区时,必须截取有效长度
buf[:n],否则缓冲区残留脏数据,文件损坏 - 传输流程不能颠倒:先发文件名→回 ok→传文件内容,自定义协议避免粘包问题
io.EOF是文件正常读取结束标识,其他 error 属于异常- 文件、TCP 连接必须搭配
defer Close(),避免文件句柄、网络资源泄漏 - 仅支持单客户端单次文件传输,拓展可使用 goroutine 实现并发多客户端上传