GO_网络编程---文件传输实战

一、传输整体流程

发送端 (客户端) ↔ 接收端 (服务端) 自定义简易传输协议:

  1. 客户端先发送文件名
  2. 服务端收到文件名,回复 ok
  3. 客户端收到 ok 后,分片读取本地文件,发送二进制文件内容
  4. 服务端循环接收二进制数据,写入新文件,传输完成

二、核心前置 API 说明

  1. os.Stat(path):获取文件元信息,可拿到文件名、文件大小,校验文件是否存在
  2. os.Open():只读打开本地文件(发送端使用)
  3. os.Create():创建新文件(接收端使用)
  4. file.Read(buf):读取文件到缓冲区,返回实际读取字节数,读到文件末尾返回io.EOF
  5. conn.Read/conn.Write:TCP 连接读写,网络数据传输
  6. 缓冲区分片传输:使用 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
		}
	}
}

五、运行测试步骤

  1. 先运行接收端 recv.go,启动监听服务
  2. 新开终端运行发送端 send.go
  3. 输入本地文件路径(图片、txt、压缩包均可)
  4. 传输完成后,服务端同级目录生成同名文件

六、重点复习 & 踩坑总结

  1. 必须分片读写文件,大文件一次性读取会造成内存溢出,统一使用 4KB 缓冲
  2. 读写缓冲区时,必须截取有效长度 buf[:n],否则缓冲区残留脏数据,文件损坏
  3. 传输流程不能颠倒:先发文件名→回 ok→传文件内容,自定义协议避免粘包问题
  4. io.EOF 是文件正常读取结束标识,其他 error 属于异常
  5. 文件、TCP 连接必须搭配defer Close(),避免文件句柄、网络资源泄漏
  6. 仅支持单客户端单次文件传输,拓展可使用 goroutine 实现并发多客户端上传
相关推荐
tiantianuser2 小时前
NVME-oF IP 设计9 :为什么选RDMA传输?
网络·网络协议·tcp/ip
@LuckY BoY2 小时前
mtu详解
网络
happy_0x3f2 小时前
前端应用的离线暂停更新策略
开发语言·前端·php
Yang96112 小时前
多场景电磁干扰排查方案,鼎讯信通 DXL-400E 适配野外与室内运维
网络
北冥you鱼3 小时前
Go 语言新手扫盲:指针 * 和 & 使用场景详解
开发语言·后端·golang
gongzhxu3 小时前
DumpAny —— 一款支持 HTTP / gRPC / MySQL / Redis 的全能网络调试工具
网络·测试工具·http·rpc
三川6983 小时前
深入浅出SSD 09:NVMe介绍
网络
超级赛博搬砖工4 小时前
并发、多线程和HTTP连接之间有什么关系?
网络·网络协议·http
wWYy.5 小时前
RPC详解
qt·rpc·php