手撸了一个文件传输工具
在日常的开发与运维中,文件传输工具是不可或缺的利器。无论是跨服务器传递配置文件,还是快速从一台机器下载日志文件,一个高效、可靠且简单的文件传输工具能够显著提高工作效率。今天,我想分享我自己手撸一个文件传输工具的全过程,包括需求场景、技术点分析以及实现的编码过程。
为什么要搞这个?
在我的日常学习和工作中,经常遇到以下几个需求场景:
1)跨服务器的文件共享:因为我们目前有两台服务器,有时需要在服务器之间同步配置文件或共享资源。
2)简单的文件传输:对于某些单文件传输的任务,现有工具配置较复杂,使用成本较高。希望能有一个简易的工具,不依赖复杂的配置和额外的库。
3)文件列表管理与下载:需要方便地列出文件存储目录中的内容,并支持选择性下载某些文件。
技术点分析
为了满足上述需求,我需要实现一个功能完整的文件传输工具。以下是一些关键技术点和设计思路:
TCP通信
使用 TCP 协议构建文件传输工具,确保传输的可靠性。实现客户端和服务器的双向通信,用于文件传输、列表查询等功能。
文件传输机制
通过 TCP 套接字发送和接收文件内容。客户端在上传文件时需要传递文件元信息(文件名和大小),以便服务器正确创建文件。
命令解析
支持多种命令(如 LIST
、UPLOAD
和 DOWNLOAD
),让工具更易扩展。
进度条展示
使用第三方库 pb
在终端展示传输进度条,提升用户体验。
工具开发过程
接下来,我将分享从零开始实现这个工具的完整过程。
首先的整体的机制:服务器监听一个固定的端口,接受客户端的 TCP 连接。根据客户端发送的命令执行不同的操作,在Upload操作时可以指定操作类型,比如查询文件列表等。
文件传输服务端
需要支持命令启动,而且在启动时能够指定端口号、文件保存的目录等,所以就需要使用Go语言的cobra插件。
main.go文件
var rootCmd = &cobra.Command{
Use: "",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Running myapp...")
},
}
func main() {
rootCmd.AddCommand(ClientCmd())
rootCmd.AddCommand(ServerCmd())
if err := rootCmd.Execute(); err != nil {
panic(err)
}
}
server命令代码实现
先定义命令参数和默认值,比如port, webport
代表要启动的TCP服务端口号和Web服务端口号,path, secret
代表文件的保存路径和将来可能要做安全机制的密码功能。
const (
DefaultPathDir = "tmp"
DefaultServerPort = 8088
DefaultWebPort = 8089
)
var (
port, webport int
path, secret string
)
主要方法:
func ServerCmd() *cobra.Command {
command := &cobra.Command{
Use: "server",
Run: func(cmd *cobra.Command, args []string) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
initCommand()
go func() {
runHttpServer()
runServer(port, path)
}()
<-quit
},
}
command.Flags().StringVarP(&path, "path", "", "", "path to serve")
command.Flags().StringVarP(&secret, "secret", "", "", "path to serve")
command.Flags().IntVarP(&port, "port", "", 0, "path to serve")
command.Flags().IntVarP(&webport, "webport", "", 0, "path to serve")
return command
}
支持TCP文件上传的主要逻辑:
func runServer(port int, savePath string) {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
fmt.Println("Error starting server:", err)
return
}
defer func() {
_ = listener.Close()
}()
fmt.Println("Server is listening on port", port)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Connection error:", err)
continue
}
go handleConnection(conn, savePath)
}
}
func handleConnection(conn net.Conn, savePath string) {
defer func() {
_ = conn.Close()
}()
reader := bufio.NewReader(conn)
// 读取文件元信息:文件名和文件大小
meta, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading file metadata:", err)
return
}
meta = strings.TrimSpace(meta) // 清除换行符
parts := strings.Split(meta, "|")
if len(parts) != 2 {
fmt.Println("Invalid metadata received")
return
}
fileName := parts[0]
fileSize := 0
_, err = fmt.Sscanf(parts[1], "%d", &fileSize)
if err != nil {
fmt.Println("Error parsing file size:", err)
return
}
// 确保保存路径存在
fullPath := filepath.Join(savePath, fileName)
if err = os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
fmt.Println("Error creating directories:", err)
return
}
// 创建文件
f, err := os.Create(fullPath)
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer func() {
_ = f.Close()
}()
// 创建进度条
bar := pb.Start64(int64(fileSize))
bar.Set(pb.Bytes, true)
defer bar.Finish()
// 读取数据并写入文件
proxyReader := bar.NewProxyReader(reader)
if _, err = io.Copy(f, proxyReader); err != nil {
fmt.Println("Error saving file:", err)
return
}
fmt.Println("File received:", fullPath)
}
启动服务端:
go build
./FTransferor server --path filepath --port 8081
后续会考虑支持的功能
1)文件列表功能:通过读取服务器的文件保存目录,列出所有文件。如使用 LIST
命令返回文件名列表。
2)文件下载功能:客户端通过 DOWNLOAD <filename>
命令请求文件。服务端确认文件存在后,先发送文件大小,然后传输文件内容。
文件传输客户端
客户端主要就是能够接收命令参数找到服务端、指定自己要上传的文件,然后就是支持文件上传。
参数定义:
server
为将要上传的服务端地址,file
为要进行上传的文件名次,action
参数为客户端的操作,如LIST、DOWNLOAD等,后续会进行扩展。
var (
server, file, action string
)
主要的client方法:
func ClientCmd() *cobra.Command {
command := &cobra.Command{
Use: "cli",
Run: func(cmd *cobra.Command, args []string) {
startClient(server, file)
},
}
command.Flags().StringVarP(&server, "server", "", "", "path to serve")
command.Flags().StringVarP(&file, "file", "", "", "path to serve")
command.Flags().StringVarP(&action, "action", "", "", "path to serve")
return command
}
文件上传方法:
func startClient(serverAddr string, filePath string) {
f, err := os.Open(filePath)
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer func() {
_ = f.Close()
}()
// 获取文件信息
fileInfo, err := f.Stat()
if err != nil {
fmt.Println("Error getting file info:", err)
return
}
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
fmt.Println("Error connecting to server:", err)
return
}
defer func() {
_ = conn.Close()
}()
// 发送文件元信息(文件名|文件大小)
meta := fmt.Sprintf("%s|%d\n", fileInfo.Name(), fileInfo.Size())
if _, err = conn.Write([]byte(meta)); err != nil {
fmt.Println("Error sending metadata:", err)
return
}
// 创建进度条
bar := pb.Start64(fileInfo.Size())
bar.Set(pb.Bytes, true)
defer bar.Finish()
// 发送文件数据
proxyWriter := bar.NewProxyWriter(conn)
if _, err = io.Copy(proxyWriter, f); err != nil {
fmt.Println("Error sending file:", err)
return
}
fmt.Println("File sent successfully!")
}
实现效果
1)工具编译
拿到代码后,我们需要在相关操作系统编译成可执行文件,需要go1.23以上的版本,命令:
go mod tidy
go build
2)启动服务端
最简单的启动方式就是直接使用默认参数:
./FTransferor server
当然也可以指定参数,比如指定端口号为9910,文件的保存路径为当前目录下的yankaka目录,如果没有该目录会自动创建:
./FTransferor server --port 9910 --path yankaka
3)使用客户端
使用客户端就需要输入必要的参数了,因为TCP是点对点链接,客户端必须要有一个连接目标,下面就上传一个文件看下效果:
./FTransferor cli --server yankaka.chat:9910 --file go1.23.3.linux-amd64.tar.gz
此时会显示一个进度条,就是文件上传的进度和上传速度:
服务端也会显示,并在上传成功后有提示:
上传完成后看下目标目录的相关文件是否存在:
发现是存在并且正常的,至此我们这款文件上传工具的基本功能就已经实现了!
收获与总结
通过手撸这个文件传输工具,我对TCP编程有了更深刻的理解,其实有些功能并不需要利用HTTP、RPC等上层协议进行实现,更别说各种各样的框架了,最简单的功能往往TCP协议就足够了。