流程分析
借助TCP完成文件的傳輸,基本思路如下:
- 發送方(客戶端)向服務端發送文件名,服務端保存該文件名。
- 接收方(服務端)向客戶端返回一個消息ok,確認文件名保存成功。
- 發送方(客戶端)收到消息后,開始向服務端發送文件數據。
- 接收方(服務端)讀取文件內容,寫入到之前保存好的文件中。
由於文件傳輸需要穩定可靠的連接,所以采用TCP方式完成網絡文件傳輸功能。
首先獲取文件名。借助os包中的stat()函數來獲取文件屬性信息。在函數返回的文件屬性中包含文件名和文件大小。Stat參數name傳入的是文件訪問的絕對路徑。FileInfo中的Name()函數可以將文件名單獨提取出來。
func Stat(name string) (fi FileInfo, err error)
Stat返回一個描述name指定的文件對象的FileInfo。如果指定的文件對象是一個符號鏈接,返回的FileInfo描述該符號鏈接指向的文件的信息,本函數會嘗試跳轉該鏈接。如果出錯,返回的錯誤值為*PathError類型。
我們通過源碼可以得知FileInfo是一個接口,要實現這個接口就必須實現這個接口的如下所有方法
實現網絡文件傳輸實質上時借助了本地文件復制和TCP網絡編程相關知識,可以先看看Go語言復制文件和Go網絡編程了解相關內容。
所以關於使用TCP實現文件傳輸大致步驟可以歸結為如下步驟
接收端:
- 創建監聽 listener,程序結束時關閉。
- 阻塞等待客戶端連接 conn,程序結束時關閉conn。
- 讀取客戶端發送文件名。保存 fileName。
- 回發“ok”。
- 封裝函數 RecvFile 接收客戶端發送的文件內容。傳參 fileName 和 conn
- 按文件名 Create 文件,結束時 Close
- 循環 Read 發送端網絡文件內容,當讀到 0 說明文件讀取完畢。
- 將讀到的內容原封不動Write到創建的文件中
接收端代碼:
package main
import (
"fmt"
"io"
"net"
"os"
)
func recvFile(conn net.Conn, fileName string) {
//按照文件名創建新文件
file, err := os.Create(fileName)
if err != nil {
fmt.Printf("os.Create()函數執行錯誤,錯誤為:%v\n", err)
return
}
defer file.Close()
//從網絡中讀數據,寫入本地文件
for {
buf := make([]byte, 4096)
n, err := conn.Read(buf)
//寫入本地文件,讀多少,寫多少
file.Write(buf[:n])
if err != nil {
if err == io.EOF {
fmt.Printf("接收文件完成。\n")
} else {
fmt.Printf("conn.Read()方法執行出錯,錯誤為:%v\n", err)
}
return
}
}
}
func main() {
//1.創建監聽socket
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.Listen()函數執行錯誤,錯誤為:%v\n", err)
return
}
defer listener.Close()
//阻塞監聽
conn, err := listener.Accept()
if err != nil {
fmt.Printf("listener.Accept()方法執行錯誤,錯誤為:%v\n", err)
return
}
defer conn.Close()
//文件名的長度不能超過1024個字節
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("conn.Read()方法執行錯誤,錯誤為:%v\n", err)
return
}
fileName := string(buf[:n])
//回寫ok給發送端
conn.Write([]byte("ok"))
//獲取文件內容
recvFile(conn, fileName)
}
發送端:
- 提示用戶使用命令行參數輸入文件名。接收文件名 filepath(含訪問路徑)
- 使用 os.Stat()獲取文件屬性,得到純文件名 fileName(去除訪問路徑)
- 主動發起連接服務器請求,結束時關閉連接。
- 發送文件名到接收端 conn.Write()
- 讀取接收端回發的確認數據 conn.Read()
- 判斷是否為“ok”。如果是,封裝函數 SendFile() 發送文件內容。傳參 filePath 和 conn
- 只讀 Open 文件, 結束時Close文件
- 循環讀本地文件,讀到 EOF,讀取完畢。
- 將讀到的內容原封不動 conn.Write 給接收端(服務器)
發送端代碼:
package main
import (
"fmt"
"io"
"net"
"os"
)
func sendFile(conn net.Conn, filePath string) {
//只讀打開文件
file, err := os.Open(filePath)
if err != nil {
fmt.Printf("os.Open()函數執行出錯,錯誤為:%v\n", err)
return
}
defer file.Close()
buf := make([]byte, 4096)
for {
//從本地文件中讀數據,寫給網絡接收端。讀多少,寫多少
n, err := file.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Printf("發送文件完畢\n")
} else {
fmt.Printf("file.Read()方法執行錯誤,錯誤為:%v\n", err)
}
return
}
//寫到網絡socket中
_, err = conn.Write(buf[:n])
}
}
func main() {
//獲取命令行參數
list := os.Args
if len(list) != 2 {
fmt.Printf("格式為:go run xxx.go 文件名\n")
return
}
//提取文件的絕對路徑
path := list[1]
//獲取文件屬性
fileInfo, err := os.Stat(path)
if err != nil {
fmt.Printf("os.Stat()函數執行出錯,錯誤為:%v\n", err)
return
}
//主動發起連接請求
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Printf("net.Dial()函數執行出錯,錯誤為:%v\n", err)
return
}
defer conn.Close()
//發送文件名給接收端
_, err = conn.Write([]byte(fileInfo.Name()))
//讀取服務器回發數據
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
fmt.Printf("conn.Read(buf)方法執行出錯,錯誤為:%v\n", err)
return
}
if string(buf[:n]) == "ok" {
//寫文件內容給服務器 -- 借助conn
sendFile(conn, path)
}
}