ex8.2:要求實現一個ftp服務器,支持cd,ls,put,get等命令,目前實現了用戶身份簡單確認,獲取家目錄後能夠進行cd,ls,mkdir以及上傳和下載文件。linux
TODO:windows
1)未實現輸入密碼時不回顯(相似C裏的getpass函數);數組
2)不支持文件夾的上傳與下載;服務器
3)未實現與linux用戶權限管理保持一致。app
目錄結構:tcp
----go工做目錄/gobook/ch8/ex8.2
--------client
------------ftp
----------------ftp.go
------------client.go
--------ftp
------------ftp.go
--------server
------------ftp
----------------ftp.go
------------server.go
代碼實現:ide
ftp/ftp.go函數
package ftp import ( "encoding/binary" "net" "unsafe" ) var Commands = map[string]uint8{ "cd": uint8(1), "ls": uint8(2), "exit": uint8(3), "mkdir": uint8(4), "put": uint8(5), "get": uint8(6), } type FtpConn struct { Con net.Conn Cwd string Home string Exit bool } func (ftpCon *FtpConn) Write(content []byte) error { var length uint32 length = uint32(len(content)) if length == 0 { return binary.Write(ftpCon.Con, binary.LittleEndian, &length) } length = length + uint32(binary.Size(length)) err := binary.Write(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, content) if err != nil { return err } return nil } // string轉[]byte // 利用string原本的底層數組 func Str2sbyte(s string) (b []byte) { *(*string)(unsafe.Pointer(&b)) = s *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*unsafe.Sizeof(&b))) = len(s) return } // []byte轉string // 利用[]byte原本的底層數組 func Sbyte2str(b []byte) string { return *(*string)(unsafe.Pointer(&b)) }
client/client.goui
package main import ( "bufio" "encoding/binary" "errors" "fmt" "log" "net" "os" "strings" "gobook/ch8/ex8.2/client/ftp" "gobook/ch8/ex8.2/ftp" ) func printHelp() { log.Println("Help:\t[command] [args]\ncd [path]\n") } func handleCommand(ftpCon *client.FtpClient, command string, args []string) (err error) { cmdid, ok := ftp.Commands[command] if !ok { return errors.New("unsupported command\n") } err = ftpCon.WriteCommand(cmdid, args) if err != nil { return err } if cmdid == ftp.Commands["get"] { err = ftpCon.HandleGet(args[0]) if err != nil { return err } } var length uint32 err = binary.Read(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } if length == 0 { fmt.Printf("\n%s:", ftpCon.Cwd) return nil } res := make([]byte, length-uint32(binary.Size(length))) err = binary.Read(ftpCon.Con, binary.LittleEndian, res) if err != nil { return err } if cmdid == ftp.Commands["cd"] { ftpCon.Cwd = ftp.Sbyte2str(res) fmt.Printf("\n%s:", ftpCon.Cwd) return nil } if cmdid == ftp.Commands["exit"] { ftpCon.Exit = true fmt.Printf("%s\n", ftp.Sbyte2str(res)) return nil } fmt.Printf("%s\n%s:", ftp.Sbyte2str(res), ftpCon.Cwd) return } func main() { // 獲取用戶身份信息與ftp服務器host信息 if len(os.Args) < 2 { fmt.Println("沒法經過身份認證") return } arg := os.Args[1] if !strings.Contains(arg, "@") { fmt.Println("沒法經過身份認證") return } args := strings.Split(arg, "@") user := args[0] host := args[1] fmt.Print("Password:") var pwd string input := bufio.NewScanner(os.Stdin) if input.Scan() { pwd = input.Text() } // 鏈接到ftp服務器 con, err := net.Dial("tcp", host) if err != nil { fmt.Println(err) return } defer con.Close() ftpCon := ftp.FtpConn{ Con: con, } ftpClient := client.FtpClient{ ftpCon, } // 身份驗證 err = ftpClient.Write(ftp.Str2sbyte(user)) if err != nil { fmt.Println(err) return } err = ftpClient.Write(ftp.Str2sbyte(pwd)) if err != nil { fmt.Println(err) return } var res uint32 err = binary.Read(con, binary.LittleEndian, &res) if err != nil { fmt.Println(err) return } if res == 0 { fmt.Println("認證失敗") return } cwd := make([]byte, res) err = binary.Read(con, binary.LittleEndian, cwd) if err != nil { fmt.Println(err) return } ftpClient.Cwd = ftp.Sbyte2str(cwd) ftpClient.Home = ftpCon.Cwd fmt.Println(ftpClient.Cwd, ":") // 監聽命令行輸入 for input.Scan() && !ftpClient.Exit { argstr := input.Text() args := strings.Split(strings.TrimSpace(argstr), " ") if len(args) == 0 { printHelp() continue } command := args[0] if len(args) > 1 { args = args[1:] } else { args = nil } err = handleCommand(&ftpClient, command, args) if err != nil { log.Println(err) } } }
client/ftp/ftp.gospa
package client import ( "bufio" "encoding/binary" "errors" "io" "os" "path" "strings" "gobook/ch8/ex8.2/ftp" ) type FtpClient struct { ftp.FtpConn } func (ftpCon *FtpClient) WriteCommand(cmdid uint8, args []string) error { if cmdid == ftp.Commands["put"] { return ftpCon.WritePut(cmdid, args[0]) } var length uint32 argstr := strings.Join(args, "") length = uint32(binary.Size(length)+binary.Size(cmdid)) + uint32(len(argstr)) err := binary.Write(ftpCon.Con, binary.LittleEndian, length) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, cmdid) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, ftp.Str2sbyte(argstr)) if err != nil { return err } return nil } func (ftpCon *FtpClient) WritePut(cmdid uint8, filePath string) error { filePath = strings.Replace(filePath, "\\", "/", -1) f, err := os.Open(filePath) if err != nil { return err } defer f.Close() // 發送命令與文件名 var length uint32 fileName := ftp.Str2sbyte(path.Base(filePath)) length = uint32(binary.Size(length)+binary.Size(cmdid)) + uint32(len(fileName)) err = binary.Write(ftpCon.Con, binary.LittleEndian, length) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, cmdid) if err != nil { return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, fileName) if err != nil { return err } // 發送文件長度 fileInfo, err := f.Stat() if err != nil { return err } if fileInfo.IsDir() { return errors.New("put 命令不支持發送文件夾,請嘗試putdir命令") } else { err = binary.Write(ftpCon.Con, binary.LittleEndian, fileInfo.Size()) if err != nil { return err } } // 發送文件內容 buf := make([]byte, 4096) bufReader := bufio.NewReader(f) for { n, err := bufReader.Read(buf) if err != nil { if err == io.EOF { break } return err } err = binary.Write(ftpCon.Con, binary.LittleEndian, buf[0:n]) if err != nil { return err } } return nil } func (ftpCon *FtpClient) HandleGet(filePath string) error { fileName := path.Base(filePath) f, err := os.Create(fileName) if err != nil { if os.IsExist(err) { err = f.Truncate(0) if err != nil { return err } } else { return err } } defer f.Close() var length int64 err = binary.Read(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } var total, bufSize int64 if length > 4096 { bufSize = 4096 } else { bufSize = length } buf := make([]byte, bufSize) for total < length { err = binary.Read(ftpCon.Con, binary.LittleEndian, buf) if err != nil { return err } n, err := f.Write(buf) if err != nil { return err } total += int64(n) if length-total < bufSize { buf = buf[0 : length-total] } } return nil }
server/server.go
// ftp server package main import ( "encoding/binary" "log" "net" "gobook/ch8/ex8.2/ftp" "gobook/ch8/ex8.2/server/ftp" ) func handleFunc(con net.Conn) { defer con.Close() // 身份驗證 // 讀取用戶名 var length uint32 err := binary.Read(con, binary.LittleEndian, &length) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } user := make([]byte, length-uint32(binary.Size(length))) err = binary.Read(con, binary.LittleEndian, user) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } // 讀取密碼 err = binary.Read(con, binary.LittleEndian, &length) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } pwd := make([]byte, length-uint32(binary.Size(length))) err = binary.Read(con, binary.LittleEndian, pwd) if err != nil { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } // 驗證用戶名密碼獲取家目錄 validated, cwd := server.Validate(ftp.Sbyte2str(user), ftp.Sbyte2str(pwd)) if !validated { err = binary.Write(con, binary.LittleEndian, uint32(0)) if err != nil { log.Println(err) } return } home := ftp.Str2sbyte(cwd) err = binary.Write(con, binary.LittleEndian, uint32(binary.Size(home))) if err != nil { log.Println(err) return } err = binary.Write(con, binary.LittleEndian, home) if err != nil { log.Println(err) return } ftpCon := ftp.FtpConn{ Con: con, Home: cwd, Cwd: cwd, } ftpServer := server.FtpServer{ ftpCon, } // 循環監聽命令請求 for !ftpServer.Exit { var length uint32 err = binary.Read(con, binary.LittleEndian, &length) if err != nil { log.Println(err) return } var cmdid uint8 err = binary.Read(con, binary.LittleEndian, &cmdid) if err != nil { log.Println(err) return } args := make([]byte, length-uint32(binary.Size(cmdid))-uint32(binary.Size(length))) err = binary.Read(con, binary.LittleEndian, args) if err != nil { log.Println(err) return } switch cmdid { case ftp.Commands["cd"]: err = ftpServer.HandleCd(args) case ftp.Commands["ls"]: err = ftpServer.HandleLs(args) case ftp.Commands["exit"]: err = ftpServer.HandleExit(args) case ftp.Commands["mkdir"]: err = ftpServer.HandleMkdir(args) case ftp.Commands["put"]: err = ftpServer.HandlePut(args) case ftp.Commands["get"]: err = ftpServer.HandleGet(args) default: err = ftpServer.Write([]byte("no command handler.")) } if err != nil { log.Println(err) } } } func main() { listener, err := net.Listen("tcp", "localhost:5900") if err != nil { log.Fatal(err) } for { con, err := listener.Accept() if err != nil { log.Println(err) continue } go handleFunc(con) } }
server/ftp/ftp.go
package server import ( "bufio" "encoding/binary" "fmt" "io" "log" "os" "path" "runtime" "strings" "sync" "gobook/ch8/ex8.2/ftp" ) var Commands = map[string]uint8{ "cd": uint8(1), "ls": uint8(2), "exit": uint8(3), "mkdir": uint8(4), "put": uint8(5), "get": uint8(6), } var DefaultDir = map[string]string{ "windows": "C:/Users/Kylin/workspace/go/src/gobook/ch8", "unix": "home/www", } type userInfo struct { name string pwd string home string } var lock sync.Once // 初始化users一次 var users []userInfo func init() { lock.Do(initUsers) } type FtpServer struct { ftp.FtpConn } func (ftpCon *FtpServer) HandleCd(args []byte) error { cwd := ftp.Sbyte2str(args) if strings.HasPrefix(cwd, "/") { cwd = path.Join(ftpCon.Cwd, cwd) } f, err := os.Open(cwd) if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } defer f.Close() finfo, err := f.Stat() if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } if !finfo.IsDir() { ftpCon.Write(ftp.Str2sbyte("cd parameter must be directory.")) return nil } ftpCon.Cwd = cwd return ftpCon.Write(ftp.Str2sbyte(cwd)) } func (ftpCon *FtpServer) HandleLs(args []byte) error { cwd := ftp.Sbyte2str(args) if strings.HasPrefix(cwd, "/") { cwd = path.Join(ftpCon.Cwd, cwd) } f, err := os.Open(cwd) if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } finfo, err := f.Stat() if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) return nil } if finfo.IsDir() { finfos, err := f.Readdir(0) if err != nil { ftpCon.Write(ftp.Str2sbyte(err.Error())) } var res string res = fmt.Sprintf("Total:%d\n", len(finfos)) for _, info := range finfos { res = res + fmt.Sprintf("%.30s\t%.10d\t%s\n", info.Name(), info.Size(), info.ModTime()) } err = ftpCon.Write(ftp.Str2sbyte(res)) } else { res := fmt.Sprintf("%.30s\t%.10d\t%s\n", finfo.Name(), finfo.Size(), finfo.ModTime()) err = ftpCon.Write(ftp.Str2sbyte(res)) } if err != nil { err = ftpCon.Write(ftp.Str2sbyte(err.Error())) } return err } func (ftpCon *FtpServer) HandleExit(args []byte) error { ftpCon.Exit = true ftpCon.Write(ftp.Str2sbyte("Byebye.")) return nil } func (ftpCon *FtpServer) HandleMkdir(args []byte) error { dir := ftp.Sbyte2str(args) if strings.HasPrefix(dir, "/") { dir = path.Join(ftpCon.Home, dir) } else { dir = path.Join(ftpCon.Cwd, dir) } err := os.Mkdir(dir, os.ModePerm) if err != nil { return err } return ftpCon.Write(ftp.Str2sbyte("Ok")) } func (ftpCon *FtpServer) HandlePut(args []byte) error { fileName := ftp.Sbyte2str(args) f, err := os.Create(path.Join(ftpCon.Cwd, fileName)) if err != nil { return err } defer f.Close() var length int64 err = binary.Read(ftpCon.Con, binary.LittleEndian, &length) if err != nil { return err } var total, bufSize int64 if length > 4096 { bufSize = 4096 } else { bufSize = length } buf := make([]byte, bufSize) for total < length { err = binary.Read(ftpCon.Con, binary.LittleEndian, buf) if err != nil { return err } n, err := f.Write(buf) if err != nil { return err } total += int64(n) if (length - total) < bufSize { buf = buf[0 : length-total] } } ftpCon.Write(ftp.Str2sbyte("Ok.")) return nil } func (ftpCon *FtpServer) HandleGet(args []byte) error { filePath := ftp.Sbyte2str(args) if strings.HasPrefix(filePath, "/") { filePath = path.Join(ftpCon.Home, filePath) } else { filePath = path.Join(ftpCon.Cwd, filePath) } f, err := os.Open(filePath) if err != nil { return err } defer f.Close() finfo, err := f.Stat() if err != nil { return err } // TODO 暫不支持下載文件夾 if finfo.IsDir() { return binary.Write(ftpCon.Con, binary.LittleEndian, int64(0)) } err = binary.Write(ftpCon.Con, binary.LittleEndian, finfo.Size()) if err != nil { return err } bufReader := bufio.NewReader(f) buf := make([]byte, 4096) for { n, err := bufReader.Read(buf) if err != nil { if err == io.EOF { break } else { return err } } err = binary.Write(ftpCon.Con, binary.LittleEndian, buf[0:n]) if err != nil { return err } } ftpCon.Write(ftp.Str2sbyte("Ok.")) return nil } func initUsers() { cwd, ok := DefaultDir[runtime.GOOS] if !ok { log.Fatal("Unsupported system.") } // TODO 打開相對路徑的問題 f, err := os.Open("C:/Users/Kylin/workspace/go/src/gobook/ch8/ex8.2/server/ftp/users") if err != nil { log.Fatal("failed to load users' information.", err) } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() userinfo := strings.Split(line, ";;") if len(userinfo) < 3 { continue } home := path.Join(cwd, userinfo[2]) f, err := os.Open(home) if err != nil && os.IsNotExist(err) { err = os.Mkdir(home, os.ModePerm) if err != nil { log.Fatal("failed to make directory", home) } } else { f.Close() } users = append(users, userInfo{userinfo[0], userinfo[1], home}) } } // 驗證用戶名和密碼,返回驗證結果true/false和驗證經過後的用戶家目錄 func Validate(name string, pwd string) (pass bool, home string) { if len(users) <= 0 { return } for _, info := range users { if info.name == name && info.pwd == pwd { return true, info.home } } return }