├── .gitignore ├── LICENSE ├── README.md ├── common_functions.go ├── consts.go ├── data_type.go ├── file_struct_info.go ├── go.mod ├── send_receive_interface.go ├── storage_tcp_delete_file.go ├── storage_tcp_download.go ├── storage_tcp_get_fileinfo.go ├── storage_tcp_uploadfile.go ├── tcp_conn_pool.go ├── tcp_header.go ├── tcp_protocal_detail.md ├── test └── fdfscClient_test.go ├── tracker_storage_tcp_common.go ├── tracker_storage_tcp_delete_file.go ├── tracker_storage_tcp_download.go ├── tracker_storage_tcp_get_fileinfo.go ├── tracker_storage_tcp_uploadfile.go └── tracker_tcp_conn.go /.gitignore: -------------------------------------------------------------------------------- 1 | ./.idea 2 | ./.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 张奇峰 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## fastdfs_client_go 2 | 3 | ### 1.概述 4 | 5 | - `FastDFS` 采用二进制 `TCP` 通信协议. 6 | - 开发本包的重点与核心主要是实现二进制通讯协议. 7 | - [点击了解 fastdfs 分布式文件存储系统](https://github.com/happyfish100/fastdfs) . 8 | 9 | ### 2.FastDFS二进制通讯协议细节 10 | 11 | [点击查看GO 实现过程](./tcp_protocal_detail.md) 12 | 13 | ### 3.安装本包 14 | 15 | ```code 16 | // 请在本仓库的 gitTag 中查看最新版本,永远建议大家使用最新版本. 17 | // 查看地址:https://github.com/qifengzhang007/fastdfs_client_go/tags 18 | go get github.com/qifengzhang007/fastdfs_client_go@v1.0.5 19 | 20 | ``` 21 | 22 | ### 4. 已封装的函数列表 23 | 24 | - 1.0.0 版本我们提供了核心功能,基本上可以解决绝大部分的需求,同时提供了非常详细的二进制协议对接细节、go 示例代码, 其他开发者可以仿照我们的项目结构自己扩展不常用功能. 25 | - 关于其他未实现的不常用功能,如果您需要,可以提 issue , 我们会在下个版本更新进去. 26 | - 在使用中出现的其他问题,都可以提 issue ,我们会在第一时间处理. 27 | 28 | #### 4.1 文件上传(指定文件名) 29 | 30 | ```code 31 | // 设置 trackerServer 配置参数 32 | var conf = &fastdfs_client_go.TrackerStorageServerConfig{ 33 | // 替换为自己的 storagerServer ip 和端口即可,保证在开发阶段外网可访问 34 | TrackerServer: []string{"192.168.10.10:22122"}, 35 | // tcp 连接池最大允许的连接数(trackerServer 和 storageServer 连接池共用该参数) 36 | MaxConns: 128, 37 | } 38 | # 文件上传核心函数 39 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 40 | fileId, err := fdfsClient.UploadByFileName(curDir + fileName) 41 | 42 | ``` 43 | 44 | #### 4.2 文件上传(传递二进制) 45 | 46 | ```code 47 | // 设置 trackerServer 配置参数 48 | var conf = &fastdfs_client_go.TrackerStorageServerConfig{ 49 | // 替换为自己的 storagerServer ip 和端口即可,保证在开发阶段外网可访问 50 | TrackerServer: []string{"192.168.10.10:22122"}, 51 | // tcp 连接池最大允许的连接数(trackerServer 和 storageServer 连接池共用该参数) 52 | MaxConns: 128, 53 | } 54 | # 文件上传核心函数 55 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 56 | // 直接传递二进制上传文件,适合文件比较小的场景使用 57 | fileId, err := fdfsClient.UploadByBuffer([]byte("测试文本数据转为二进制直接上传"), 58 | 59 | ``` 60 | 61 | #### 4.3 文件下载 62 | 63 | ```code 64 | 65 | // 设置 trackerServer 配置参数 66 | var conf = &fastdfs_client_go.TrackerStorageServerConfig{ 67 | // 替换为自己的 storagerServer ip 和端口即可,保证在开发阶段外网可访问 68 | TrackerServer: []string{"192.168.10.10:22122"}, 69 | // tcp 连接池最大允许的连接数(trackerServer 和 storageServer 连接池共用该参数) 70 | MaxConns: 128, 71 | } 72 | // 指定需要被下载的文件id (fileId) 73 | fileId := "group1/M00/00/01/MeiRdmISDUiAaURaAsRMrFnLJoE317.wav" // 大小 46419116,约 46M 左右 74 | // 创建 fdfs 客户端 75 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 76 | 77 | // 指定需要下载的文件id(fileId),最终的保存路径,开始下载 78 | fdfsClient.DownloadFileByFileId(fileId, "E:/音乐文件夹/下载测试-俩俩相忘.wav") 79 | 80 | ``` 81 | 82 | #### 4.4 文件删除 83 | 84 | ```code 85 | // 设置 trackerServer 配置参数 86 | var conf = &fastdfs_client_go.TrackerStorageServerConfig{ 87 | // 替换为自己的 storagerServer ip 和端口即可,保证在开发阶段外网可访问 88 | TrackerServer: []string{"192.168.10.10:22122"}, 89 | // tcp 连接池最大允许的连接数(trackerServer 和 storageServer 连接池共用该参数) 90 | MaxConns: 128, 91 | } 92 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 93 | // 指定需要删除的文件Id 94 | fileId := "group1/M00/00/01/MeiRdmISSbuAZwwSAAAAD_Q4O2U879.txt" 95 | // 指定删除命令 96 | err = fdfsClient.DeleteFile(fileId); 97 | 98 | ``` 99 | 100 | #### 4.5 获取远程文件信息 101 | 102 | ```code 103 | // 设置 trackerServer 配置参数 104 | var conf = &fastdfs_client_go.TrackerStorageServerConfig{ 105 | // 替换为自己的 storagerServer ip 和端口即可,保证在开发阶段外网可访问 106 | TrackerServer: []string{"192.168.10.10:22122"}, 107 | // tcp 连接池最大允许的连接数(trackerServer 和 storageServer 连接池共用该参数) 108 | MaxConns: 128, 109 | } 110 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 111 | // 指定需要查询的远程文件Id 112 | fileId := "group1/M00/00/01/MeiRdmISSbuAZwwSAAAAD_Q4O2U879.txt" 113 | // 查询远程文件信息,返回一个包含文件信息的结构体 114 | remoteFileInfo, err := fdfsClient.GetRemoteFileInfo(fileId) 115 | 116 | ``` 117 | 118 | #### 以上命令的使用示例,[点击查看单元测试详情](./test/fdfscClient_test.go) 119 | 120 | 121 | #### 5.最后一些说明 122 | - 5.1 `fastdfs` 分布式文件系统应该部署在内网环境, 整个系统原则上是不对互联网直接开放访问权限的(除了开发调试之外). 123 | - 5.2 基于以上原因,开发者可以将用户上传的文件,首先保存在临时目录,然后调用本客户端将临时目录的文件上传到 `fastdfs` 文件系统, 获取可访问的`文件id(fileId)(格式:group1/M00/00/01/MeiRdmISDUiAaURaAsRMrFnLJoE317.wav)`,最终返回给用户访问地址(建议通过nginx代理访问资源) 124 | 125 | -------------------------------------------------------------------------------- /common_functions.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | "net" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | //IntToBytes 整形转换成字节 16 | // 注意: 8字节整数整形转换为 字节 17 | // @n 需要转换的整数 18 | //func IntToBytes(n int64) []byte { 19 | // bytesBuffer := bytes.NewBuffer([]byte{}) 20 | // _ = binary.Write(bytesBuffer, binary.BigEndian, n) 21 | // return bytesBuffer.Bytes() 22 | //} 23 | 24 | //bytesToInt 字节转换成整形 25 | // @bys 需要转换的字节 26 | func bytesToInt(bys []byte) int64 { 27 | bytesBuffer := bytes.NewBuffer(bys) 28 | // 注意:这里转换的结果是 : 8字节整数 29 | var x int64 30 | _ = binary.Read(bytesBuffer, binary.BigEndian, &x) 31 | return x 32 | } 33 | 34 | // getBytesByPosition 截取指定长度的字节切片 35 | // @bys 原始字节切片 36 | // @start 开始位置, 37 | // @num 截取的字节数目 38 | func getBytesByPosition(bys []byte, start, num int) []byte { 39 | var newBytes = bys[start:] 40 | endPosition := bytes.IndexByte(newBytes, 0x0) 41 | if endPosition > 0 { 42 | num = endPosition 43 | } 44 | return newBytes[:num] 45 | } 46 | 47 | // getFileExtNameStr 获取文件扩展名的文本 48 | // @fileName 文件的完整名 49 | func getFileExtNameStr(fileName string) string { 50 | index := strings.LastIndexByte(fileName, '.') 51 | if index != -1 { 52 | return fileName[index+1:] 53 | } 54 | return "" 55 | } 56 | 57 | // specialFileExtNameConvBytes 指定的文件扩展名转换为二进制 58 | // @specialFileExtName 指定文件的扩展名,例如:png、tar.gz,最开始位置不包括点(.) 59 | func specialFileExtNameConvBytes(specialFileExtName string) []byte { 60 | var fileExtName = make([]byte, FILE_EXTNAME_FIX_LEN) 61 | copy(fileExtName, specialFileExtName[:]) 62 | return fileExtName 63 | } 64 | 65 | //groupNameConvBytes storage server 组名转为二进制 66 | // @groupName storage server 文件存储组名 67 | func groupNameConvBytes(groupName string) []byte { 68 | var gName = make([]byte, FDFS_GROUP_NAME_FIX_LEN) 69 | copy(gName, groupName[:]) 70 | return gName 71 | } 72 | 73 | //sendBytesByFilePtr 通过文件指针以及tcp连接将数据发送出去 74 | // 注释事项:这里对于文件指针只是使用,开发者必须在文件打开的地方,记得调用 defer 关闭 75 | // @filePtr 文件指针 76 | // @tcpConn tcp 连接 77 | func sendBytesByFilePtr(filePtr *os.File, tcpConn net.Conn) (int64, error) { 78 | fInfo, err := filePtr.Stat() 79 | if err != nil { 80 | return 0, err 81 | } 82 | totalSize := fInfo.Size() 83 | var oneReadBuf = make([]byte, TCP_READ_BUFFER_SIZE) 84 | var remainSize int64 = 1 85 | var actualReadSize = 0 86 | var alreadySendSize int64 = 0 87 | _ = tcpConn.SetWriteDeadline(time.Time{}) 88 | for ; remainSize > 0; remainSize = totalSize - alreadySendSize { 89 | if actualReadSize, err = filePtr.Read(oneReadBuf); err != nil { 90 | if err == io.EOF { 91 | return alreadySendSize, err 92 | } else { 93 | return alreadySendSize, err 94 | } 95 | } else { 96 | alreadySendSize += int64(actualReadSize) 97 | //fmt.Printf("tcp发送的数据: %#+v\n", oneReadBuf[:actualReadSize]) 98 | if _, tcpErr := tcpConn.Write(oneReadBuf[:actualReadSize]); tcpErr != nil { 99 | return alreadySendSize, tcpErr 100 | } 101 | } 102 | } 103 | return alreadySendSize, nil 104 | } 105 | 106 | //writeBufferFromTcpConn 从tcp连接的内核缓冲区把数据写到内存缓冲区 107 | // @conn tcp连接 108 | // @writer 内存缓冲区io 109 | // @totalSize 待接收的二进制总长度 110 | func writeBufferFromTcpConn(conn net.Conn, bufWriter *bufio.Writer, totalSize int64) (err error) { 111 | // 单次读取的字节 112 | var oneReadSize int 113 | // 假设有剩余字节需要读取 114 | var remainSize int64 = 1 115 | // 已经接受到的字节 116 | var alreadyReceivedSize int64 = 0 117 | // 初始化一个临时存储缓冲区 118 | buf := make([]byte, TCP_RECEIVE_BUFFER_SIZE) 119 | _ = conn.SetReadDeadline(time.Time{}) 120 | var i = 1 //记录数据读取的次数 121 | for ; remainSize > 0; remainSize = totalSize - alreadyReceivedSize { 122 | i++ 123 | //最大每次读取 4096 个字节 124 | if remainSize > TCP_RECEIVE_BUFFER_SIZE { 125 | remainSize = TCP_RECEIVE_BUFFER_SIZE 126 | } 127 | //fmt.Printf("从tcp内核缓冲区读取的字节数目:%d, 二进制:%#+v\n", oneReadSize, buf[:remainSize]) 128 | oneReadSize, err = conn.Read(buf[:remainSize]) 129 | if err != nil { 130 | return err 131 | } 132 | // 从 tcp 内核的缓冲区写入开发者定义的接受变量对应的内存缓冲区 133 | _, err = bufWriter.Write(buf[:oneReadSize]) 134 | if err != nil { 135 | return err 136 | } 137 | alreadyReceivedSize += int64(oneReadSize) 138 | // 假设每次从 tcp 内核缓冲区读取的内容都是最大值 4096 字节,那么 1000 次大概是 4M 左右 139 | // 每隔 4M 左右将内存缓冲区数据写入到底层的硬盘, 确保大文下载件时,内存占用始终处于低位 140 | if i%1000 == 0 { 141 | //fmt.Printf("%d - 每隔大约 4M 左右的数据,刷新到硬盘,已经接受的字节量:%d\n", i, alreadyReceivedSize) 142 | if err = bufWriter.Flush(); err != nil { 143 | return errors.New(ERROR_STORAGE_SERVER_DOWN_FILE_WRITE_FLUSH + err.Error()) 144 | } 145 | } 146 | } 147 | if err = bufWriter.Flush(); err != nil { 148 | return errors.New(ERROR_STORAGE_SERVER_DOWN_FILE_WRITE_FLUSH + err.Error()) 149 | } else { 150 | return nil 151 | } 152 | } 153 | 154 | // splitStorageServerFileId 分割 group 和 文件存储路径使用 155 | // @fileId 服务器存储的文件Id (fileId) 156 | func splitStorageServerFileId(fileId string) (string, string, error) { 157 | pos1 := strings.IndexByte(fileId, '/') 158 | // 如果文件的id(fileId) 以 / 开头, 就把最开始的 / 删除 159 | if pos1 == 0 { 160 | fileId = fileId[1:] 161 | } 162 | str := strings.SplitN(fileId, "/", 2) 163 | if len(str) != 2 { 164 | return "", "", errors.New(ERROR_STORAGE_SERVER_FILE_NAME_FORMAT2) 165 | } 166 | return str[0], str[1], nil 167 | } 168 | -------------------------------------------------------------------------------- /consts.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import "time" 4 | 5 | // fastdfs通信协议参考地址(作者2019年发布): https://mp.weixin.qq.com/s/lpWEv3NCLkfKmtzKJ5lGzQ 6 | // fastdfs通信协议参考地址(作者2009年发布):http://bbs.chinaunix.net/thread-2001015-1-1.html 7 | // FastDFS采用二进制TCP通信协议。一个数据包由 包头(header)和包体(body)组成。 8 | 9 | // 程序配置常量 10 | const ( 11 | // 包头固定长度10个字节 12 | TCP_HEADER_LEN = 10 13 | //tracker 响应码 14 | TRACKER_PROTO_CMD_RESP = 100 15 | //获取一个storage server用来存储文件(指定组名 16 | TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE = 101 17 | // 获取一个 storage server 用来下载文件 18 | TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE = 102 19 | // 上传文件 20 | STORAGE_PROTO_CMD_UPLOAD_FILE = 11 21 | // 删除文件 22 | STORAGE_PROTO_CMD_DELETE_FILE = 12 23 | // 下载文件 24 | STORAGE_PROTO_CMD_DOWNLOAD_FILE = 14 25 | // 获取文件信息 26 | STORAGE_PROTO_CMD_QUERY_FILE_INFO = 22 27 | 28 | // 激活测试,通常用于检测连接是否有效 29 | // 客户端使用连接池的情况下,建立连接后发送一次active test即可和server端保持长连接。 30 | FDFS_PROTO_CMD_ACTIVE_TEST = 111 31 | 32 | // groupName 长度常量 33 | FDFS_GROUP_NAME_FIX_LEN = 16 34 | 35 | // TCP连接池最小连接数 36 | TCP_CONNS_MIN_NUM = 3 37 | // tcp 连接超时时间 38 | TCP_CONN_TIMEOUT = time.Second * 10 39 | // tcp 连接最大空闲时间(秒) 40 | TCP_CONN_IDLE_TIMEOUT float64 = 15 41 | // tcp 心跳的秒数 42 | HEART_BEAT_SECOND = time.Second * 10 43 | 44 | // 文件的扩展名长度常量 45 | FILE_EXTNAME_FIX_LEN = 6 46 | 47 | // TCP 从内核读取数据到内存中转缓冲区的大小 48 | TCP_RECEIVE_BUFFER_SIZE = 4096 49 | 50 | // TCP 从内核读取数据到内存中转缓冲区的大小 51 | TCP_READ_BUFFER_SIZE = 512 52 | 53 | // 使用进程的状态常量,模拟 tcp 连接状态 54 | 55 | // 执行状态(从连接池取后的状态) 56 | TCP_STATUS_RUNNING = 'R' 57 | // 可中断的休眠状态(使用后归还到连接池的状态) 58 | TCP_STATUS_INTERRUPTIBLE = 'S' 59 | // 不可中断的睡眠状态(刚创建后的状态) 60 | TCP_STATUS_UNINTERRUPTIBLE = 'D' 61 | ) 62 | 63 | // 错误常量 64 | const ( 65 | ERROR_CONN_POOL_OVER_MAX = "连接池超过已设置的最大连接数 , 当前最大连接数:" 66 | ERROR_GET_TCP_CONN_FAIL = "从连接池获取一个 tcp 连接失败 , 出错明细:" 67 | ERROR_TCP_SERVER_RESPONSE_NOT_ZERO = "tcp 服务器返回的status状态值不为0" 68 | ERROR_FILE_FILENAME_IS_EMPTY = "待上传的文件名不允许为空" 69 | ERROR_FILE_SIZE_IS_ZERO = "待上传的文件大小不允许为0字节" 70 | ERROR_FILE_DOWNLOAD_RELA_FILENAME_NOT_EMPTY = "下载文件时, 接受数据对应的文件名不能已经存在,否则可能会影响已经存在的文件数据" 71 | ERROR_FILE_EXT_NAME_IS_EMPTY = "通过文件流(二进制)上传文件时,必须手动指定文件扩展名" 72 | ERROR_CONN_POOL_NO_ACTIVE_CONN = "tcp 连接池中没有有效对象" 73 | ERROR_HEADER_RECEV_STATUS_NOT_ZERO = "收到的消息头(receive header)中 status 值不为0" 74 | ERROR_HEADER_RECEV_ERROR = "收到的消息头(receive header)中有错误" 75 | ERROR_HEADER_RECEV_LEN_LT16_ERROR = "收到的消息头(receive header)长度必须 > 16" 76 | ERROR_STORAGE_SERVER_FILE_NAME_FORMAT2 = "storage server 文件名格式不正确, 文件Id(fileId) 中必须至少存在一个斜杠( /),不能不能在开头位置" 77 | ERROR_STORAGE_SERVER_DOWN_HEADER = "storage server 下载时获取服务器响应头出错: " 78 | ERROR_STORAGE_SERVER_DOWN_IS_EMPTY = "storage server 被下载的文件在服务器端不存在(或文件内容为空)" 79 | ERROR_STORAGE_SERVER_DOWN_FILENAME_EMPTY = "storage server 下载文件时,必须指定文件名才能保存" 80 | ERROR_STORAGE_SERVER_DOWN_RECEIVE = "storage server 下载时接受文件出错: " 81 | ERROR_STORAGE_SERVER_DOWN_FILE_RECEIVE = "storage server 下载的文件在读取数据过程中出错:" 82 | ERROR_STORAGE_SERVER_DOWN_FILE_WRITE_FLUSH = "storage server 下载的文件写出到硬盘出错:" 83 | ERROR_STORAGE_SERVER_FILE_UPLOAD_SEND_BYTES = "storage server 上传文件在通过tcp连接发送二进制文件时出错:" 84 | ERROR_STORAGE_SERVER_GET_FILEINFO = "storage server 查询文件信息时出错:" 85 | ERROR_STORAGE_SERVER_GET_FILEINFO_BODY_LEN = "storage server 查询文件信息时获取响应 body 长度不符合长度为 40 字节的标准" 86 | ERROR_TCP_CONN_ASSERT_FAIL = "从连接池中获取的 tcp 连接断言为结构体 tcpConnBaseInfo 失败" 87 | ) 88 | -------------------------------------------------------------------------------- /data_type.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | // TrackerStorageServerConfig fast dfs 服务端参数配置 4 | type TrackerStorageServerConfig struct { 5 | TrackerServer []string 6 | MaxConns int 7 | } 8 | 9 | // RemoteFileInfo 查询远程服务器的文件信息 10 | type RemoteFileInfo struct { 11 | fileSize int64 12 | createTimestamp int64 13 | crc32 int64 14 | SourceIpAddr string 15 | } 16 | 17 | // storageServerInfo 服务器信息(需要通过 tracker server获取) 18 | type storageServerInfo struct { 19 | addrPort string 20 | storagePathIndex byte 21 | } 22 | -------------------------------------------------------------------------------- /file_struct_info.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | ) 7 | 8 | // 文件结构信息(通过文件名上传、或者通过字节上传文件使用) 9 | type fileInfo struct { 10 | filePtr *os.File // 文件指针 11 | fileExtName string // 文件扩展名 12 | fileSize int64 // 文件大小 13 | buffer []byte // 文件字节(文件内容本身) 14 | } 15 | 16 | // 通过文件名获取文件信息 17 | func getFileInfoByFileName(fileName string) (*fileInfo, error) { 18 | if fileName != "" { 19 | file, err := os.OpenFile(fileName, os.O_RDONLY, 0755) 20 | if err != nil { 21 | return nil, err 22 | } 23 | stat, err := file.Stat() 24 | if err != nil { 25 | return nil, err 26 | } 27 | if int(stat.Size()) == 0 { 28 | return nil, errors.New(fileName + ERROR_FILE_SIZE_IS_ZERO) 29 | } 30 | 31 | return &fileInfo{ 32 | fileSize: stat.Size(), 33 | filePtr: file, 34 | buffer: nil, 35 | fileExtName: getFileExtNameStr(fileName), 36 | }, nil 37 | } else { 38 | return nil, errors.New(ERROR_FILE_FILENAME_IS_EMPTY) 39 | } 40 | } 41 | 42 | // 通过文件字节获取文件信息 43 | func getFileInfoByFileByte(buffer []byte, fileExtName string) (*fileInfo, error) { 44 | if len(buffer) == 0 { 45 | return nil, errors.New(ERROR_FILE_SIZE_IS_ZERO) 46 | } 47 | if len(fileExtName) == 0 { 48 | return nil, errors.New(ERROR_FILE_EXT_NAME_IS_EMPTY) 49 | } 50 | return &fileInfo{ 51 | filePtr: nil, 52 | fileSize: int64(len(buffer)), 53 | buffer: buffer, 54 | fileExtName: fileExtName, 55 | }, nil 56 | } 57 | 58 | // 关闭文件 59 | func (c *fileInfo) Close() { 60 | if c.filePtr != nil { 61 | _ = c.filePtr.Close() 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qifengzhang007/fastdfs_client_go 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /send_receive_interface.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // tcpSendReceive 发送、接受消息必须通过 tcp 连接,定义一个统一的接口 8 | type tcpSendReceive interface { 9 | Send(tcpConn net.Conn) error 10 | Receive(tcpConn net.Conn) error 11 | } 12 | -------------------------------------------------------------------------------- /storage_tcp_delete_file.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | ) 7 | 8 | // storageDeleteHeaderBody 文件删除 9 | type storageDeleteHeaderBody struct { 10 | header 11 | groupName string 12 | remoteFilename string 13 | } 14 | 15 | //Send 发送删除文件命令 16 | // @tcpConn tcp连接 17 | func (s *storageDeleteHeaderBody) Send(tcpConn net.Conn) error { 18 | // 设置删除文件时的 header 参数 19 | //@group_name:16字节字符串,组名 20 | //@filename:不定长字符串,文件名 21 | s.header.pkgLen = int64(len(s.remoteFilename) + 16) 22 | s.header.cmd = STORAGE_PROTO_CMD_DELETE_FILE 23 | s.header.status = 0 24 | 25 | if err := s.header.sendHeader(tcpConn); err != nil { 26 | return err 27 | } 28 | buffer := new(bytes.Buffer) 29 | buffer.Write(groupNameConvBytes(s.groupName)) 30 | buffer.WriteString(s.remoteFilename) 31 | 32 | if _, err := tcpConn.Write(buffer.Bytes()); err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | // Receive 接受删除命令发送服务端的响应头 39 | // @tcpConn tcp连接 40 | func (s *storageDeleteHeaderBody) Receive(tcpConn net.Conn) error { 41 | return s.header.receiveHeader(tcpConn) 42 | } 43 | -------------------------------------------------------------------------------- /storage_tcp_download.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "net" 9 | "os" 10 | ) 11 | 12 | type storageDownloadHeaderBody struct { 13 | header 14 | // body 参数,发送使用 15 | groupName string 16 | remoteFilename string 17 | offset int64 18 | downloadBytes int64 19 | //保存的文件名 20 | saveFileName string 21 | } 22 | 23 | // Send 下载文件 body 参数 24 | // @tcpConn tcp连接 25 | func (s *storageDownloadHeaderBody) Send(tcpConn net.Conn) error { 26 | // 构建 header 头参数 27 | // 32 = body 下载参数的前3个参数二进制总长度 28 | // @file_offset 8字节整数,文件偏移量,从指定的位置开始下载 29 | // @download_bytes:8字节整数,需要下载字节数 30 | // @group name:16字节字符串,组名 31 | // @filename:不定长字符串,文件名 32 | s.header.pkgLen = int64(len(s.remoteFilename) + 32) 33 | s.header.cmd = STORAGE_PROTO_CMD_DOWNLOAD_FILE 34 | s.header.status = 0 35 | 36 | if err := s.header.sendHeader(tcpConn); err != nil { 37 | return err 38 | } 39 | buffer := new(bytes.Buffer) 40 | if err := binary.Write(buffer, binary.BigEndian, s.offset); err != nil { 41 | return err 42 | } 43 | if err := binary.Write(buffer, binary.BigEndian, s.downloadBytes); err != nil { 44 | return err 45 | } 46 | buffer.Write(groupNameConvBytes(s.groupName)) 47 | buffer.WriteString(s.remoteFilename) 48 | if _, err := tcpConn.Write(buffer.Bytes()); err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | // Receive 通过tcp连接接受数据 55 | // @tcpConn tcp连接 56 | func (s *storageDownloadHeaderBody) Receive(tcpConn net.Conn) error { 57 | if err := s.header.receiveHeader(tcpConn); err != nil { 58 | return errors.New(ERROR_STORAGE_SERVER_DOWN_HEADER + err.Error()) 59 | } 60 | if s.header.pkgLen == 0 { 61 | return errors.New(ERROR_STORAGE_SERVER_DOWN_IS_EMPTY) 62 | } 63 | if s.saveFileName != "" { 64 | if err := s.receiveToFile(tcpConn); err != nil { 65 | return errors.New(ERROR_STORAGE_SERVER_DOWN_RECEIVE + err.Error()) 66 | } 67 | } else { 68 | return errors.New(ERROR_STORAGE_SERVER_DOWN_FILENAME_EMPTY) 69 | } 70 | return nil 71 | } 72 | 73 | //receiveToFile 接受下载的数据流到指定的本地文件(设置文件名接受数据流),注意:指定的文件名必须不能事先存在,避免对已经存在文件产生影响 74 | // @tcpConn tcp连接 75 | func (s *storageDownloadHeaderBody) receiveToFile(tcpConn net.Conn) error { 76 | file, err := os.OpenFile(s.saveFileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND|os.O_EXCL, 0755) 77 | defer func() { 78 | _ = file.Close() 79 | }() 80 | if err != nil { 81 | return err 82 | } 83 | writerBuf := bufio.NewWriter(file) 84 | if err = writeBufferFromTcpConn(tcpConn, writerBuf, s.pkgLen); err != nil { 85 | return errors.New(ERROR_STORAGE_SERVER_DOWN_FILE_RECEIVE + err.Error()) 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /storage_tcp_get_fileinfo.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "net" 7 | ) 8 | 9 | type storageGetFileInfoHeaderBody struct { 10 | header 11 | // header 额外参数,发送使用 12 | groupName string 13 | remoteFilename string 14 | // 响应信息 15 | fileSize int64 16 | createTimestamp int64 17 | crc32 int64 18 | SourceIpAddr string 19 | } 20 | 21 | // Send 获取文件信息 body 参数 22 | // @tcpConn tcp连接 23 | func (s *storageGetFileInfoHeaderBody) Send(tcpConn net.Conn) error { 24 | // 构建 header 头参数 25 | // 16 = body 下载参数的前3个参数二进制总长度 26 | // @group name:16字节字符串,组名 27 | // @remoteFilename:不定长字符串,文件名 28 | s.header.pkgLen = int64(len(s.remoteFilename) + 16) 29 | s.header.cmd = STORAGE_PROTO_CMD_QUERY_FILE_INFO 30 | s.header.status = 0 31 | 32 | if err := s.header.sendHeader(tcpConn); err != nil { 33 | return err 34 | } 35 | buffer := new(bytes.Buffer) 36 | buffer.Write(groupNameConvBytes(s.groupName)) 37 | buffer.WriteString(s.remoteFilename) 38 | if _, err := tcpConn.Write(buffer.Bytes()); err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | 44 | // Receive 通过tcp连接接受数据 45 | // @tcpConn tcp连接 46 | // 查询文件信息,服务端响body,总计 40 字节长度(固定值) 47 | // @file_size 文件大小,8字节 48 | // @create_timestamp 创建时间戳,8字节 49 | // @crc32 文件内容CRC32校验码,8字节 50 | // @source_ip_addr 16字节字符串,源storage server IP地址 51 | func (s *storageGetFileInfoHeaderBody) Receive(tcpConn net.Conn) error { 52 | if err := s.header.receiveHeader(tcpConn); err != nil { 53 | return errors.New(ERROR_STORAGE_SERVER_GET_FILEINFO + err.Error()) 54 | } 55 | if s.header.pkgLen != 40 { 56 | return errors.New(ERROR_STORAGE_SERVER_GET_FILEINFO_BODY_LEN) 57 | } 58 | buf := make([]byte, 40) 59 | if _, err := tcpConn.Read(buf); err != nil { 60 | return err 61 | } 62 | s.fileSize = bytesToInt(getBytesByPosition(buf, 0, 8)) 63 | s.createTimestamp = bytesToInt(getBytesByPosition(buf, 8, 8)) 64 | s.crc32 = bytesToInt(getBytesByPosition(buf, 16, 8)) 65 | s.SourceIpAddr = string(getBytesByPosition(buf, 24, 16)) 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /storage_tcp_uploadfile.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "net" 8 | ) 9 | 10 | type storageServerUploadHeaderBody struct { 11 | header 12 | fileInfo *fileInfo 13 | storagePathIndex byte 14 | // 文件id (fileId) 接受使用 15 | fileId string 16 | } 17 | 18 | // Send 文件上传 body 参数详情 19 | // @tcpConn tcp连接 20 | func (s *storageServerUploadHeaderBody) Send(tcpConn net.Conn) (err error) { 21 | // @store_path_index , byte 1字节 ,存储目录序号 22 | // @fileSize int64,8字节, 传输的二进制数据的字节数目 23 | // @file_ext_name 文件扩展名6个字节 24 | // @file_content:file_size 字节二进制内容,文件内容 25 | // 15 的组成 :@store_path_index (1字节整数) + @fileSize ( 8字节整数 ) + @file_ext_name (6字节字符串) 26 | s.header.pkgLen = s.fileInfo.fileSize + 15 27 | s.header.cmd = STORAGE_PROTO_CMD_UPLOAD_FILE 28 | s.header.status = 0 29 | 30 | if err = s.header.sendHeader(tcpConn); err != nil { 31 | return err 32 | } 33 | // 创建 body 数据发送时的缓冲区 34 | buffer := new(bytes.Buffer) 35 | 36 | // @store_path_index (1字节整数) 37 | buffer.WriteByte(s.storagePathIndex) 38 | 39 | //@file_size:8字节整数 40 | if err = binary.Write(buffer, binary.BigEndian, s.fileInfo.fileSize); err != nil { 41 | return err 42 | } 43 | // 文件扩展名 6 字节,必须要写满6个字节 44 | buffer.Write(specialFileExtNameConvBytes(s.fileInfo.fileExtName)) 45 | if _, err = tcpConn.Write(buffer.Bytes()); err != nil { 46 | return err 47 | } 48 | 49 | //发送文件内容本身的二进制数据 50 | // 1.如果文件结构信息对应的指针不为空,表示该文件是通过文件名方式打开操作的,那么就根据文件指针读取数据,发送出去 51 | // 2.如果文件结构信息中的文件指针为空,表示该文件需要通过字节流发送出去 52 | // 3.最后将文件真正的内容发出去,其实 tcp 底层会对大文件分块多次发送,服务器端会按照收到的报文头读取对应的数量字节才结束. 53 | if s.fileInfo.filePtr != nil { 54 | if _, err = sendBytesByFilePtr(s.fileInfo.filePtr, tcpConn); err != nil { 55 | return errors.New(ERROR_STORAGE_SERVER_FILE_UPLOAD_SEND_BYTES + err.Error()) 56 | } 57 | } else { 58 | _, err = tcpConn.Write(s.fileInfo.buffer) 59 | } 60 | 61 | if err != nil { 62 | return err 63 | } 64 | return nil 65 | } 66 | 67 | // Receive 发送文件上传命令之后接受服务器的响应头 68 | // @tcpConn tcp连接 69 | func (s *storageServerUploadHeaderBody) Receive(tcpConn net.Conn) error { 70 | // @group_name:16字节字符串,组名 71 | // @filename:不定长字符串,文件名 72 | if err := s.header.receiveHeader(tcpConn); err != nil { 73 | return err 74 | } 75 | if s.pkgLen <= 16 { 76 | return errors.New(ERROR_HEADER_RECEV_LEN_LT16_ERROR) 77 | } 78 | 79 | buf := make([]byte, s.pkgLen) 80 | if _, err := tcpConn.Read(buf); err != nil { 81 | return err 82 | } 83 | s.fileId = string(getBytesByPosition(buf, 0, 16)) + "/" + string(getBytesByPosition(buf, 16, int(s.pkgLen)-16)) 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /tcp_conn_pool.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "container/list" 5 | "errors" 6 | "net" 7 | "strconv" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // tcpConnPool 连接池 13 | type tcpConnPool struct { 14 | conns *list.List 15 | addrPort string 16 | maxConns int 17 | count int 18 | lock *sync.Mutex 19 | isQuit chan bool 20 | } 21 | 22 | // tcpConnBaseInfo 连接应该具有的基本特征 23 | type tcpConnBaseInfo struct { 24 | // 参见常量 TCP_STATUS 开头的相关值 25 | status byte 26 | // 最后一次归还到连接池的时间点 27 | putTime time.Time 28 | // 一个 tcp 连接 29 | net.Conn 30 | } 31 | 32 | // 初始化一个tcp连接池,最少数量为 3 33 | func initTcpConnPool(addrPort string, maxConns int) (*tcpConnPool, error) { 34 | if maxConns < TCP_CONNS_MIN_NUM { 35 | maxConns = TCP_CONNS_MIN_NUM 36 | } 37 | connPool := &tcpConnPool{ 38 | conns: list.New(), 39 | addrPort: addrPort, 40 | maxConns: maxConns, 41 | lock: &sync.Mutex{}, 42 | isQuit: make(chan bool), 43 | } 44 | connPool.lock.Lock() 45 | defer connPool.lock.Unlock() 46 | for i := 0; i < TCP_CONNS_MIN_NUM; i++ { 47 | if err := connPool.CreateTcpConn(); err != nil { 48 | return nil, err 49 | } 50 | } 51 | // 开启一个周期性心跳包 52 | go func() { 53 | ticker := time.NewTicker(HEART_BEAT_SECOND) 54 | for { 55 | select { 56 | case isQuit := <-connPool.isQuit: 57 | if isQuit { 58 | ticker.Stop() 59 | return 60 | } 61 | case <-ticker.C: 62 | _ = connPool.checkTcpConnPool() 63 | } 64 | } 65 | }() 66 | return connPool, nil 67 | } 68 | 69 | // Destroy tcp 连接关闭 70 | // 1.首先给管道发送退出消息,结束心跳任务 71 | // 2.关闭一个tcp连接对应的连接池中的全部连接 72 | func (t *tcpConnPool) Destroy() { 73 | t.isQuit <- true 74 | for tcpConn := t.conns.Front(); tcpConn != nil; tcpConn = tcpConn.Next() { 75 | conn := tcpConn.Value.(*tcpConnBaseInfo) 76 | _ = conn.Close() 77 | t.conns.Remove(tcpConn) 78 | t.count-- 79 | } 80 | } 81 | 82 | // checkTcpConnPool 检测连接池中所有的 tcp 连接是否有效,把失效的tcp连接删除 83 | func (t *tcpConnPool) checkTcpConnPool() (err error) { 84 | t.lock.Lock() 85 | defer t.lock.Unlock() 86 | var isOk bool 87 | var conn *tcpConnBaseInfo 88 | for tcpConn := t.conns.Front(); tcpConn != nil; tcpConn = tcpConn.Next() { 89 | if conn, isOk = tcpConn.Value.(*tcpConnBaseInfo); isOk { 90 | // 1.首先检查休眠状态超时的 tcp 连接,直接从连接池删除 91 | if conn.status == TCP_STATUS_INTERRUPTIBLE && time.Now().Sub(conn.putTime).Seconds() > TCP_CONN_IDLE_TIMEOUT { 92 | _ = conn.Close() 93 | t.conns.Remove(tcpConn) 94 | t.count-- 95 | } 96 | // 2.其次,检查没有超时,但是不可用的连接,从连接池删除 97 | if isOk, err = t.CheckSpecialTcpConnIsActive(conn); !isOk { 98 | t.conns.Remove(tcpConn) 99 | t.count-- 100 | } 101 | } 102 | } 103 | return err 104 | } 105 | 106 | // CheckSpecialTcpConnIsActive 检查特定的 tcp 连接是否有效 107 | // @tcpConn tcp连接 108 | func (t *tcpConnPool) CheckSpecialTcpConnIsActive(tcpConn *tcpConnBaseInfo) (bool, error) { 109 | tmpHeader := &header{ 110 | pkgLen: 0, 111 | cmd: FDFS_PROTO_CMD_ACTIVE_TEST, 112 | status: 0, 113 | } 114 | err1 := tmpHeader.sendHeader(tcpConn) 115 | err2 := tmpHeader.receiveHeader(tcpConn) 116 | 117 | if err1 == nil && err2 == nil && tmpHeader.status == 0 { 118 | return true, nil 119 | } else { 120 | if tmpHeader.status != 0 { 121 | err3 := ERROR_TCP_SERVER_RESPONSE_NOT_ZERO 122 | return false, errors.New(err1.Error() + err2.Error() + err3) 123 | } 124 | // 这里的错误可能发生在发送时、接受时,其中一个或者两处全部都出错 125 | var tempErr string 126 | if err1 != nil { 127 | tempErr = err1.Error() 128 | } 129 | if err2 != nil { 130 | tempErr += err2.Error() 131 | } 132 | return false, errors.New(tempErr) 133 | } 134 | } 135 | 136 | // CreateTcpConn 初始化一个tcp连接,主要用于连接到 fastdfs 的tracker server、storage server 137 | func (t *tcpConnPool) CreateTcpConn() error { 138 | conn, err := net.DialTimeout("tcp", t.addrPort, TCP_CONN_TIMEOUT) 139 | if err != nil { 140 | return err 141 | } 142 | var tcpBaseInfo = &tcpConnBaseInfo{ 143 | status: TCP_STATUS_UNINTERRUPTIBLE, 144 | Conn: conn, 145 | } 146 | t.conns.PushBack(tcpBaseInfo) 147 | t.count++ 148 | return nil 149 | } 150 | 151 | // get 获取一个 tcp 连接 152 | func (t *tcpConnPool) get() (tcpConn *tcpConnBaseInfo, err error) { 153 | t.lock.Lock() 154 | defer t.lock.Unlock() 155 | var isOk bool 156 | var okTcp *tcpConnBaseInfo 157 | for { 158 | conn := t.conns.Front() 159 | if conn == nil { 160 | if t.count > t.maxConns { 161 | err = errors.New(ERROR_CONN_POOL_OVER_MAX + strconv.Itoa(t.maxConns)) 162 | return nil, err 163 | } 164 | if err = t.CreateTcpConn(); err == nil { 165 | continue 166 | } else { 167 | break 168 | } 169 | } 170 | // 获取一个 tcp 连接 171 | if okTcp, isOk = conn.Value.(*tcpConnBaseInfo); isOk { 172 | if isOk, err = t.CheckSpecialTcpConnIsActive(okTcp); isOk { 173 | t.conns.Remove(conn) 174 | // 取出后的 tcp 状态设置为 运行态 175 | okTcp.status = TCP_STATUS_RUNNING 176 | return okTcp, nil 177 | } else { 178 | t.conns.Remove(conn) 179 | continue 180 | } 181 | } else { 182 | return nil, errors.New(ERROR_TCP_CONN_ASSERT_FAIL) 183 | } 184 | } 185 | return nil, errors.New(ERROR_GET_TCP_CONN_FAIL + err.Error()) 186 | } 187 | 188 | // put 将使用完毕的tcp连接放回连接池 189 | // @tcpConn tcp 连接 190 | func (t *tcpConnPool) put(tcpConn *tcpConnBaseInfo) { 191 | t.lock.Lock() 192 | defer t.lock.Unlock() 193 | 194 | tcpConn.status = TCP_STATUS_INTERRUPTIBLE 195 | tcpConn.putTime = time.Now() 196 | t.conns.PushBack(tcpConn) 197 | } 198 | -------------------------------------------------------------------------------- /tcp_header.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "net" 8 | ) 9 | 10 | // 本页面代码功能介绍: 11 | // header 头是所有tcp连接通讯必须组合使用的公共结构体 12 | // 如果 body 参数为空,那么可以直接使用 header 结构体以及提供的方法进行网络通讯 13 | 14 | // header(包头) 组成结构: 15 | // pkg_len:8字节整数,body长度,不包含header,只是body的长度 16 | // cmd: 1字节整数,命令码 17 | // status:1字节整数,状态码,0表示成功,非0失败(UNIX错误码) 18 | type header struct { 19 | pkgLen int64 // 占8字节 20 | cmd byte // 占1字节 21 | status byte // 占1字节 22 | } 23 | 24 | //sendHeader 发送header消息 25 | // @tcpConn tcp连接 26 | func (h *header) sendHeader(tcpConn net.Conn) error { 27 | buffer := new(bytes.Buffer) 28 | // c.pkgLen 整数型(4字节或者8字节)写入到二进制缓冲区 29 | //将整数类型采用网络字节序(Big-Endian),包括4字节整数(int32)和8字节整数(int64) 30 | if err := binary.Write(buffer, binary.BigEndian, h.pkgLen); err != nil { 31 | return err 32 | } 33 | buffer.WriteByte(h.cmd) 34 | buffer.WriteByte(h.status) 35 | 36 | if _, err := tcpConn.Write(buffer.Bytes()); err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | //receiveHeader 接受 header 头 43 | // @tcpConn tcp连接 44 | func (h *header) receiveHeader(tcpConn net.Conn) error { 45 | buf := make([]byte, TCP_HEADER_LEN) 46 | if _, err := tcpConn.Read(buf); err != nil { 47 | return err 48 | } 49 | buffer := bytes.NewBuffer(buf) 50 | // 读取已接受字节的实际长度,赋值给 pkgLen,pkgLen 按照协议长度必须=10 51 | if err := binary.Read(buffer, binary.BigEndian, &h.pkgLen); err != nil || h.pkgLen != TCP_HEADER_LEN { 52 | return err 53 | } else { 54 | h.cmd = (buf[8:9])[0] 55 | h.status = (buf[9:10])[0] 56 | if h.status != 0 { 57 | return errors.New(ERROR_HEADER_RECEV_STATUS_NOT_ZERO + ", ErrorMsg: " + string(h.status)) 58 | } 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /tcp_protocal_detail.md: -------------------------------------------------------------------------------- 1 | ### fastdfs_client_for_go 2 | 3 | ### 1.TCP 通信协议详情 4 | 本篇文档在编写过程中遇到了几个的错误,主要是因为作者在 2009年 和 2019年 分别发布了两份协议参数,但是两份协议参数不尽相同,有些协议的对接反而是旧版本成功了,而按照新版本的协议参数对接却始终失败. 5 | 参考地址(作者2009年发布):` http://bbs.chinaunix.net/thread-2001015-1-1.html ` 6 | 参考地址(作者2019年发布):` https://mp.weixin.qq.com/s/lpWEv3NCLkfKmtzKJ5lGzQ ` 7 | 本篇文档是综合了以上两份协议参数编写而成,有时候按照新版本协议对接失败,请切换为旧版本参数对接调试即可 . 8 | 9 | ### 2.header 和 body 组成 10 | 11 | FastDFS 采用二进制 TCP 通信协议。一个数据包由 包头(header)和包体(body)组成。 12 | 13 | #### 2.1 包头(header)由10个字节,格式如下: 14 | 15 | ```code 16 | 17 | @ pkg_len:8字节整数,body体长度,不包含header, 只是body的长度 18 | @ cmd:1字节整数,命令码 19 | @ status:1字节整数,状态码,0表示成功,非0失败(UNIX错误码) 20 | 21 | ``` 22 | 发送header 头参数核心函数 23 | 24 | ```code 25 | //sendHeader 发送header消息 26 | func (h *header) sendHeader(conn net.Conn) error { 27 | 28 | // 创建一个发送数据前的字节缓冲区 29 | buffer := new(bytes.Buffer) 30 | 31 | // c.pkgLen 整数型(4字节或者8字节)写入到二进制缓冲区 32 | //将整数类型采用网络字节序(Big-Endian),包括4字节整数(int32)和8字节整数(int64) 33 | if err := binary.Write(buffer, binary.BigEndian,h.pkgLen); err != nil { 34 | return err 35 | } 36 | // 单字节写入缓冲区 37 | buffer.WriteByte(h.cmd) 38 | buffer.WriteByte(h.status) 39 | 40 | // 通过 tcp 连接将缓冲区的全部字节数据发送出去 41 | if _, err := conn.Write(buffer.Bytes()); err != nil { 42 | return err 43 | } 44 | return nil 45 | } 46 | 47 | ``` 48 | 包头参数解释: 49 | 1.整数类型采用网络字节序(Big-Endian), 包括4字节整数和8字节整数. 50 | 2.字节整数不存在字节序问题 , 在 GO 中直接映射为 byte 类型,C/C++ 中为 char 类型. 51 | 3.固定长度的字符串类型以 ASCII 码 0 结尾 , 对于固定长度字符串, 开发者必须在取出的二进制文件中,找到 `0x0` 然后截取前面的字符串. 52 | 4.变长字符串的长度可以直接拿到或者根据包长度计算出来, 不以 ASCII 0 结尾. 53 | 54 | #### 2.2 包体(body)组成 55 | 不同的命令,`body` 体发出去的参数不同, 这就相当于调用不同的函数,传递的参数不同是一样的道理,但是我们只要按照官方公布的 `tcp` 协议格式传递参数就可以实现相应的功能. 56 | 下面我们将以具体的命令为例来说明 `body` 参数的使用. 57 | 58 | ### 3.命令列表 59 | 60 | #### 3.1 文件上传 - STORAGE_PROTO_CMD_UPLOAD_FILE 61 | > 通过指定已经存在的文件名上传到服务器 62 | - 1.`header` 头命令 63 | ```code 64 | @pkg_len : int64 ,8字节 ,body 体的总字节数目,不包括 header 任何部分 65 | @cmd: byte , 1字节 , 上传命令常量 STORAGE_PROTO_CMD_UPLOAD_FILE 66 | @status: byte,1字节 ,该参数主要用于返回判断状态,发送时默认为 0 即可 67 | ``` 68 | `header` 头参数组装成二进制核心函数 69 | ```code 70 | s.header.status = 0 // 主要用于返回做判断,发送时位置为0即可 71 | s.header.cmd = STORAGE_PROTO_CMD_UPLOAD_FILE // 具体的协议常量值 72 | 73 | // pkgLen 表示 body 体的总字节长度,计算依据请参考后面 body 体的参数组成 74 | // 15 的组成 :@store_path_index (1字节整数) + @fileSize ( 8字节整数 ) + @file_ext_name (6字节字符串) 75 | s.header.pkgLen = s.fileInfo.fileSize + 15 76 | if err = s.sendHeader(conn); err != nil { 77 | return err 78 | } 79 | ``` 80 | 81 | - 2.`body` 体发送参数 82 | ```code 83 | # body 发送参数构成 84 | @store_path_index : byte ,基于 0 的存储路径顺序号 ,服务器存储目录有很多个,0 表示存储目录的序号,具体值由 tracker server 返回 85 | @file_size : int64 ,被上传的文件大小(总字节量) 86 | @file_ext_name string ,最大6字节,不包括小数点的文件扩展名,例如 jpeg、tar.gz 87 | @file_content []byte , 字节切片,不定长 88 | ``` 89 | `body` 体参数组装成二进制核心过程 90 | ```code 91 | 92 | // 创建 body 数据发送时的缓冲区 93 | buffer := new(bytes.Buffer) 94 | 95 | // @store_path_index (1字节整数) 96 | buffer.WriteByte(s.storagePathIndex) 97 | 98 | //@file_size:8字节整数 99 | if err = binary.Write(buffer, binary.BigEndian, s.fileInfo.fileSize); err != nil { 100 | return err 101 | } 102 | // 文件扩展名 6 字节 103 | buffer.Write(GetFileExtNameBytes(s.fileInfo.fileName)) 104 | 105 | // 这里首先把报文头发给服务器,服务器就会根据报文头,一直接受完毕约定的字节量 106 | if _, err = conn.Write(buffer.Bytes()); err != nil { 107 | return err 108 | } 109 | 110 | //发送文件内容本身的二进制数据 111 | // 1.如果文件结构信息对应的指针不为空,表示该文件是通过文件名方式打开操作的,那么就根据文件指针读取数据,发送出去 112 | // 2.如果文件结构信息中的文件指针为空,表示该文件需要通过字节流发送出去 113 | // 3.最后将文件真正的内容发出去,其实 tcp 底层会对大文件分块多次发送,服务器端会按照收到的报文头读取对应的数量字节才结束. 114 | if s.fileInfo.filePtr != nil { 115 | _, err = conn.(*net.TCPConn).ReadFrom(s.fileInfo.filePtr) 116 | } else { 117 | _, err = conn.Write(s.fileInfo.buffer) 118 | } 119 | ``` 120 | 121 | - 3.`body` 体接受参数 122 | ```code 123 | @ group_name : string,16字节字符串,组名 124 | @ filename: string 不定长字符串,文件名 125 | ``` 126 | 首先接受 `header` 头参数, 先行判断服务器响应的状态码、数据长度必须符合协议约定,然后再读取 `body` 体的内容. 127 | 响应的body体协议规定的格式如下: 128 | ```code 129 | #body 响应格式 130 | @ group_name:16字节字符串,组名 131 | @ filename:不定长字符串,文件名 132 | ``` 133 | 根据 `body` 体的参数,那么 `header` 的响应就必须满足如下格式 134 | ```code 135 | # 响应 header 头参数 136 | pkgLen int64 // 长度 必须>16 137 | cmd byte // 发送命令的参数,接受时忽略即可 138 | status byte // 状态值必须是0,其他值表示有错误 139 | 140 | ``` 141 | 142 | #### 3.2 文件下载 - STORAGE_PROTO_CMD_DOWNLOAD_FILE 143 | - 1.`header` 头命令 144 | ```code 145 | @pkg_len : int64 ,8字节 ,body 体的总字节数目,不包括 header 任何部分 146 | @cmd: byte , 1字节 , 上传命令常量 STORAGE_PROTO_CMD_UPLOAD_FILE 147 | @status: byte,1字节 ,该参数主要用于返回判断状态,发送时默认为 0 即可 148 | ``` 149 | `header` 下载命令头参数赋值 150 | ```code 151 | // 构建 header 头参数 152 | s.header.status = 0 153 | s.header.cmd = STORAGE_PROTO_CMD_DOWNLOAD_FILE 154 | // 32 = body 下载参数的前3个参数二进制总长度 155 | s.header.pkgLen = int64(len(s.remoteFilename) + 32) 156 | 157 | if err := s.sendHeader(conn); err != nil { 158 | return err 159 | } 160 | 161 | ``` 162 | - 2.`body` 参数 163 | ```code 164 | // Send 下载文件 body 参数 165 | // @file_offset 8字节整数,文件偏移量,从指定的位置开始下载 166 | // @download_bytes:8字节整数,需要下载字节数 167 | // @group name:16字节字符串,组名 168 | // @filename:不定长字符串,文件名 169 | ``` 170 | `body` 参数赋值 171 | ```code 172 | 173 | // 构建 body 头参数 174 | buffer := new(bytes.Buffer) 175 | if err := binary.Write(buffer, binary.BigEndian, s.offset); err != nil { 176 | return err 177 | } 178 | if err := binary.Write(buffer, binary.BigEndian, s.downloadBytes); err != nil { 179 | return err 180 | } 181 | buffer.Write(groupNameConvBytes(s.groupName)) 182 | buffer.WriteString(s.remoteFilename) 183 | if _, err := conn.Write(buffer.Bytes()); err != nil { 184 | return err 185 | } 186 | 187 | ``` 188 | 189 | 190 | #### 3.3 文件删除 - STORAGE_PROTO_CMD_DELETE_FILE 191 | - 1.`header` 头命令格式 192 | ```code 193 | @pkg_len : int64 ,8字节 ,body 体的总字节数目,不包括 header 任何部分 194 | @cmd: byte , 1字节 , 上传命令常量 STORAGE_PROTO_CMD_UPLOAD_FILE 195 | @status: byte,1字节 ,该参数主要用于返回判断状态,发送时默认为 0 即可 196 | ``` 197 | `header` 删除命令头参数赋值 198 | ```code 199 | // 设置删除文件时的 header 参数 200 | s.header.status = 0 201 | s.header.cmd = STORAGE_PROTO_CMD_DELETE_FILE 202 | s.header.pkgLen = int64(len(s.remoteFilename) + 16) 203 | 204 | if err := s.sendHeader(conn); err != nil { 205 | return err 206 | } 207 | 208 | ``` 209 | - 2.`body` 参数 210 | ```code 211 | //Send 发送删除文件命令 212 | //@group_name:16字节字符串,组名 213 | //@filename:不定长字符串,文件名 214 | ``` 215 | `body` 参数赋值 216 | ```code 217 | // 写入body参数 218 | buffer := new(bytes.Buffer) 219 | buffer.Write(groupNameConvBytes(s.groupName)) 220 | buffer.WriteString(s.remoteFilename) 221 | // 发送body参数 222 | if _, err := conn.Write(buffer.Bytes()); err != nil { 223 | return err 224 | } 225 | 226 | ``` 227 | 228 | ### 命令常量列表 229 | 230 | | 常量含义 | 常量代码 | 相关值 | 231 | |:----------------------|:--------------------------------------------------------|:----| 232 | | tracker 正确响应码 | TRACKER_PROTO_CMD_RESP | 100 | 233 | | storage 正确响应码 | STORAGE_PROTO_CMD_RESP | 100 | 234 | | 激活测试,通常用于检测连接是否有效 | FDFS_PROTO_CMD_ACTIVE_TEST | 111 | 235 | | 待补充 | TRACKER_PROTO_CMD_SERVER_LIST_ONE_GROUP | 90 | 236 | | 获取组列表 | TRACKER_PROTO_CMD_SERVER_LIST_ALL_GROUPS | 91 | 237 | | 不需要组名获取一个存储节点 | TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE | 101 | 238 | | 获取下载节点QUERY_FETCH_ONE | TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE | 102 | 239 | | 获取更新节点QUERY_UPDATE | TRACKER_PROTO_CMD_SERVICE_QUERY_UPDATE | 103 | 240 | | 按组获取存储节点 | TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITH_GROUP_ONE | 104 | 241 | | 待补充 | TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ALL | 105 | 242 | | 待补充 | TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ALL | 106 | 243 | | 待补充 | TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITH_GROUP_ALL | 10 | 244 | | 文件上传 | STORAGE_PROTO_CMD_UPLOAD_FILE | 11 | 245 | | 删除文件 | STORAGE_PROTO_CMD_DELETE_FILE | 12 | 246 | | 设置文件元数据 | STORAGE_PROTO_CMD_SET_METADATA | 13 | 247 | | 文件下载 | STORAGE_PROTO_CMD_DOWNLOAD_FILE | 14 | 248 | | 获取文件元数据 | STORAGE_PROTO_CMD_GET_METADATA | 15 | 249 | | 上传附属文件 | STORAGE_PROTO_CMD_UPLOAD_SLAVE_FILE | 21 | 250 | | 查询文件信息 | STORAGE_PROTO_CMD_QUERY_FILE_INFO | 22 | 251 | | 创建支持断点续传的文件 | STORAGE_PROTO_CMD_UPLOAD_APPENDER_FILE | 23 | 252 | | 断点续传 | STORAGE_PROTO_CMD_APPEND_FILE | 24 | 253 | | 文件修改 | STORAGE_PROTO_CMD_MODIFY_FILE | 34 | 254 | | 清除文件 | STORAGE_PROTO_CMD_TRUNCATE_FILE | 36 | 255 | | 待补充 | STORAGE_PROTO_CMD_REGENERATE_APPENDER_FILENAME | 38 | 256 | -------------------------------------------------------------------------------- /test/fdfscClient_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/qifengzhang007/fastdfs_client_go" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | ) 9 | 10 | var conf = &fastdfs_client_go.TrackerStorageServerConfig{ 11 | // 替换为自己的 storagerServer ip 和端口即可,保证在开发阶段外网可访问 12 | TrackerServer: []string{"192.168.10.10:22122"}, 13 | // tcp 连接池最大允许的连接数(trackerServer 和 storageServer 连接池共用该参数) 14 | MaxConns: 128, 15 | } 16 | 17 | // 设置测试文件的根目录,测试使用 18 | //var curDir = "E:/Project/2020/fastdfs_client_go/" 19 | //var fileName = "1024.txt" 20 | 21 | var curDir = "E:/音乐资源/" 22 | var fileName = "15 俩俩相忘.mp3" // 9M 左右 23 | 24 | // 通过文件名上传文件 25 | func TestUploadByFileName(t *testing.T) { 26 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 27 | if err != nil { 28 | t.Log("单元测试失败,创建TCP连接出错:" + err.Error()) 29 | return 30 | } 31 | defer fdfsClient.Destroy() 32 | fileId, err := fdfsClient.UploadByFileName(curDir + fileName) 33 | if err != nil { 34 | t.Errorf("单元测试失败,上传文件出错:%s", err.Error()) 35 | return 36 | } else { 37 | t.Logf("单元测试成功,成功上传文件:%s", fileId) 38 | } 39 | } 40 | 41 | // 通过二进制上传文件 42 | func TestUploadByBytes(t *testing.T) { 43 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 44 | if err != nil { 45 | t.Log("单元测试失败,创建TCP连接出错:" + err.Error()) 46 | return 47 | } 48 | defer fdfsClient.Destroy() 49 | var wg sync.WaitGroup 50 | wg.Add(10) 51 | for i := 0; i < 10; i++ { 52 | go func(no int) { 53 | defer wg.Done() 54 | if fileId, err := fdfsClient.UploadByBuffer([]byte(strconv.Itoa(no+1)+" - 二进制直接上传"), "txt"); err != nil { 55 | t.Error("通过二进制文件流上传文件失败, ERROR:" + err.Error()) 56 | } else { 57 | t.Log("通过二进制文件流上传文件成功!文件名:" + fileId) 58 | } 59 | }(i) 60 | } 61 | wg.Wait() 62 | } 63 | 64 | // 下载文件测试 65 | func TestDownLoadFile(t *testing.T) { 66 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 67 | if err != nil { 68 | t.Log("单元测试失败,创建TCP连接出错:" + err.Error()) 69 | return 70 | } 71 | defer fdfsClient.Destroy() 72 | // 通过指定 文件id 下载文件 73 | //fileId := "group1/M00/00/01/MeiRdmISCRuASW3BABrBPcp7oMo520.jpg" 74 | fileId := "group1/M00/00/01/MeiRdmISDUiAaURaAsRMrFnLJoE317.wav" // 大小 9451392 75 | if err = fdfsClient.DownloadFileByFileId(fileId, curDir+"下载测试-橄榄树.wav"); err != nil { 76 | t.Error("下载文件单元测试出错, ERROR:" + err.Error()) 77 | } else { 78 | t.Log("下载文件单元测试成功 !") 79 | } 80 | } 81 | 82 | // 删除文件 83 | func TestDeleteFile(t *testing.T) { 84 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 85 | if err != nil { 86 | t.Error("单元测试失败,创建TCP连接出错:" + err.Error()) 87 | return 88 | } 89 | defer fdfsClient.Destroy() 90 | // 通过指定 文件id(fileId) 删除文件 91 | fileId := "group1/M00/00/01/MeiRdmISSbuAZwwSAAAAD_Q4O2U879.txt" 92 | if err = fdfsClient.DeleteFile(fileId); err != nil { 93 | t.Error("单元测试失败,删除文件出错:" + err.Error()) 94 | } else { 95 | t.Log("删除文件 - 单元测试成功!") 96 | } 97 | } 98 | 99 | // 查询远程文件信息 100 | func TestQueryRemoteFileInfo(t *testing.T) { 101 | fdfsClient, err := fastdfs_client_go.CreateFdfsClient(conf) 102 | if err != nil { 103 | t.Error("单元测试失败,创建TCP连接出错:" + err.Error()) 104 | return 105 | } 106 | defer fdfsClient.Destroy() 107 | // 通过指定 文件id(fileId) 删除文件 108 | fileId := "group1/M00/00/01/MeiRdmIbNnqAMnUuAAAAGej0Rfc623.txt" 109 | if remoteFileInfo, err := fdfsClient.GetRemoteFileInfo(fileId); err != nil { 110 | t.Error("单元测试失败,删除文件出错:" + err.Error()) 111 | } else { 112 | t.Logf("远程文件查询结果:%#+v\n", remoteFileInfo) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tracker_storage_tcp_common.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "sync" 7 | ) 8 | 9 | func CreateFdfsClient(trackerServerOptions *TrackerStorageServerConfig) (*trackerServerTcpClient, error) { 10 | 11 | tcpClient := &trackerServerTcpClient{ 12 | trackerServerConfig: trackerServerOptions, 13 | trackerPools: make(map[string]*tcpConnPool), 14 | storagePoolLock: &sync.Mutex{}, 15 | storagePools: make(map[string]*tcpConnPool), 16 | } 17 | for _, addr := range trackerServerOptions.TrackerServer { 18 | trackerServerPool, err := initTcpConnPool(addr, trackerServerOptions.MaxConns) 19 | if err != nil { 20 | return nil, err 21 | } 22 | tcpClient.trackerPools[addr] = trackerServerPool 23 | } 24 | 25 | return tcpClient, nil 26 | } 27 | 28 | // trackerServerTcpClient 创建一个go语言连接 fastdfs 服务的 tcp 客户端 29 | // 一个客户端可以同时连接到 tracker server 和 storage server 30 | type trackerServerTcpClient struct { 31 | trackerServerConfig *TrackerStorageServerConfig 32 | trackerPools map[string]*tcpConnPool 33 | storagePools map[string]*tcpConnPool 34 | storagePoolLock *sync.Mutex 35 | } 36 | 37 | // getTrackerConn 从连接池获取一个 trackerServer 的 tcp 连接 38 | // @ 参数 :无 39 | // 返回参数解释: 40 | // tcpConnPool 连接池地址 41 | // tcpConnBaseInfo 从连接池中获取的tcp连接 42 | // error 可能的错误 43 | func (c *trackerServerTcpClient) getTrackerConn() (*tcpConnPool, *tcpConnBaseInfo, error) { 44 | // 连接池地址 45 | var trackerPool *tcpConnPool 46 | // 从连接池获取的tcp连接 47 | var trackerConn *tcpConnBaseInfo 48 | var err error 49 | var getOne bool 50 | for _, trackerPool = range c.trackerPools { 51 | trackerConn, err = trackerPool.get() 52 | if err == nil { 53 | getOne = true 54 | break 55 | } 56 | } 57 | if getOne { 58 | // 返回连接池地址、连接池地址获取的tcp连接对象、错误 59 | return trackerPool, trackerConn, nil 60 | } 61 | if err == nil { 62 | return nil, nil, errors.New(ERROR_CONN_POOL_NO_ACTIVE_CONN) 63 | } 64 | return nil, nil, err 65 | } 66 | 67 | // Destroy 整个客户端销毁时,关闭连接池中的所有tcp连接(包括 trackerServer 和 storageServer) 68 | func (c *trackerServerTcpClient) Destroy() { 69 | for _, pool := range c.trackerPools { 70 | pool.Destroy() 71 | } 72 | for _, pool := range c.storagePools { 73 | pool.Destroy() 74 | } 75 | } 76 | 77 | // getStorageInfoByTracker 主要通过 tracker server 获取 storage server 服务的ip、端口等信息,然后通过 storage server 传输文件 78 | // @ body 参数 : 不需要 79 | func (c *trackerServerTcpClient) getStorageInfoByTracker(cmd byte, groupName string, remoteFilename string) (*storageServerInfo, error) { 80 | trackerSendParmas := &trackerTcpConn{} 81 | 82 | // 将命令参数设置在 header 头部分 83 | trackerSendParmas.header.pkgLen = 0 84 | trackerSendParmas.header.cmd = cmd 85 | trackerSendParmas.header.status = 0 86 | trackerSendParmas.groupName = groupName 87 | trackerSendParmas.remoteFilename = remoteFilename 88 | 89 | if err := c.sendHeaderByTrackerServer(trackerSendParmas); err != nil { 90 | return nil, err 91 | } 92 | return &storageServerInfo{ 93 | addrPort: trackerSendParmas.storageInfo.ipAddr + ":" + strconv.FormatInt(trackerSendParmas.storageInfo.port, 10), 94 | storagePathIndex: trackerSendParmas.storageInfo.storePathIndex, 95 | }, nil 96 | } 97 | 98 | // sendHeaderByTrackerServer 通过trackerServer 的header 头参数发送特定命令获取 storageServer 服务器 99 | // @trackerTcpConn trackerServer 的 tcp连接 100 | func (c *trackerServerTcpClient) sendHeaderByTrackerServer(trackerTcpConn tcpSendReceive) error { 101 | trackerTcpPoolPtr, trackerTcp, err := c.getTrackerConn() 102 | if err != nil { 103 | return err 104 | } 105 | defer func() { 106 | trackerTcpPoolPtr.put(trackerTcp) 107 | }() 108 | if err = trackerTcpConn.Send(trackerTcp); err != nil { 109 | return err 110 | } 111 | if err = trackerTcpConn.Receive(trackerTcp); err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | // getStorageConn 通过 trackerServer 获取的参数,创建 StorageServer 的tcp连接 118 | // @storageServInfo trackerServer 获取的 storageServer 参数 119 | // 返回参数解释: 120 | // storageTcpConnPool 连接池地址 121 | // tcpConnBaseInfo 从连接池中获取的tcp连接 122 | // err 可能的错误 123 | func (c *trackerServerTcpClient) getStorageConn(storageServInfo *storageServerInfo) (storageTcpConnPool *tcpConnPool, tcpConnBaseInfo *tcpConnBaseInfo, err error) { 124 | c.storagePoolLock.Lock() 125 | defer c.storagePoolLock.Unlock() 126 | var isOk bool 127 | storageTcpConnPool, isOk = c.storagePools[storageServInfo.addrPort] 128 | if isOk { 129 | tcpConnBaseInfo, err = storageTcpConnPool.get() 130 | if err == nil { 131 | c.storagePools[storageServInfo.addrPort] = storageTcpConnPool 132 | } 133 | return 134 | } 135 | storageTcpConnPool, err = initTcpConnPool(storageServInfo.addrPort, c.trackerServerConfig.MaxConns) 136 | if err == nil { 137 | tcpConnBaseInfo, err = storageTcpConnPool.get() 138 | if err == nil { 139 | c.storagePools[storageServInfo.addrPort] = storageTcpConnPool 140 | } 141 | return 142 | } 143 | return nil, nil, err 144 | } 145 | 146 | // sendCmdToStorageServer 给 storageServer 发送具体的业务命令 147 | // @headerBody 实现了 tcpSendReceive 接口的 header 和 body 参数组装的结构体 148 | // @storageInfo storageServer 的服务器信息,用于创建到 storageServer 的tcp连接 149 | func (c *trackerServerTcpClient) sendCmdToStorageServer(headerBody tcpSendReceive, storageInfo *storageServerInfo) error { 150 | storageTcpPool, storageTcpConn, err := c.getStorageConn(storageInfo) 151 | if err != nil { 152 | return err 153 | } 154 | defer func() { 155 | storageTcpPool.put(storageTcpConn) 156 | }() 157 | 158 | if err = headerBody.Send(storageTcpConn); err != nil { 159 | return err 160 | } 161 | if err = headerBody.Receive(storageTcpConn); err != nil { 162 | return err 163 | } 164 | 165 | return nil 166 | } 167 | -------------------------------------------------------------------------------- /tracker_storage_tcp_delete_file.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | //DeleteFile 删除存储在 storage server 服务器的文件 4 | // @fileId storage server 服务器存储的完整文件名,例如:group1/M00/00/01/MeiRdmIQ7N-AOJmJAAAAD1y-ivQ067.png 5 | func (c *trackerServerTcpClient) DeleteFile(fileId string) error { 6 | groupName, remoteFilename, err := splitStorageServerFileId(fileId) 7 | if err != nil { 8 | return err 9 | } 10 | 11 | storageServInfo, err := c.getStorageInfoByTracker(TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE, groupName, remoteFilename) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | del := &storageDeleteHeaderBody{} 17 | del.groupName = groupName 18 | del.remoteFilename = remoteFilename 19 | 20 | return c.sendCmdToStorageServer(del, storageServInfo) 21 | } 22 | -------------------------------------------------------------------------------- /tracker_storage_tcp_download.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | //DownloadFileByFileId 通过文件id下载文件 8 | // @fileId 文件id,格式:group1/M00/00/01/MeiRdmISSSqASUsRAJA3gI2KXVQ867.mp3 9 | // @saveFileName 指定下载后保存的名称 10 | // @offset 偏移字节数量 11 | // @downloadBytes 指定需要下载的字节大小,超过此字节不会下载 12 | func (c *trackerServerTcpClient) DownloadFileByFileId(fileId string, saveFileName string) error { 13 | // 下载文件时,首先必须确保被下载的文件不存在 14 | if _, err := getFileInfoByFileName(saveFileName); err == nil { 15 | return errors.New(ERROR_FILE_DOWNLOAD_RELA_FILENAME_NOT_EMPTY) 16 | } 17 | groupName, remoteFilename, err := splitStorageServerFileId(fileId) 18 | if err != nil { 19 | return err 20 | } 21 | storageServInfo, err := c.getStorageInfoByTracker(TRACKER_PROTO_CMD_SERVICE_QUERY_FETCH_ONE, groupName, remoteFilename) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | down := &storageDownloadHeaderBody{} 27 | down.groupName = groupName 28 | down.remoteFilename = remoteFilename 29 | down.offset = 0 //offset 偏移的字节数,设置0,从头开始下载 30 | down.downloadBytes = 0 // downloadBytes 需要下载的字节数,设置为0,下载整个文件 31 | down.saveFileName = saveFileName 32 | 33 | return c.sendCmdToStorageServer(down, storageServInfo) 34 | } 35 | -------------------------------------------------------------------------------- /tracker_storage_tcp_get_fileinfo.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | // GetRemoteFileInfo 获取远程服务器的文件信息 4 | // @remoteFileId 远程服务器的文件Id 5 | func (c *trackerServerTcpClient) GetRemoteFileInfo(remoteFileId string) (remoteFileInfo RemoteFileInfo, err error) { 6 | groupName, remoteFilename, err := splitStorageServerFileId(remoteFileId) 7 | if err != nil { 8 | return remoteFileInfo, err 9 | } 10 | 11 | storageServInfo, err := c.getStorageInfoByTracker(TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE, "", "") 12 | if err != nil { 13 | return remoteFileInfo, err 14 | } 15 | queryRemoteFile := &storageGetFileInfoHeaderBody{} 16 | queryRemoteFile.groupName = groupName 17 | queryRemoteFile.remoteFilename = remoteFilename 18 | 19 | if err = c.sendCmdToStorageServer(queryRemoteFile, storageServInfo); err != nil { 20 | return remoteFileInfo, err 21 | } 22 | remoteFileInfo.fileSize = queryRemoteFile.fileSize 23 | remoteFileInfo.createTimestamp = queryRemoteFile.createTimestamp 24 | remoteFileInfo.crc32 = queryRemoteFile.crc32 25 | remoteFileInfo.SourceIpAddr = queryRemoteFile.SourceIpAddr 26 | 27 | return remoteFileInfo, nil 28 | } 29 | -------------------------------------------------------------------------------- /tracker_storage_tcp_uploadfile.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | // UploadByFileName 创建fdfs客户端, 上传文件 4 | // @fileName 指定已经存在的文件名 5 | func (c *trackerServerTcpClient) UploadByFileName(fileName string) (string, error) { 6 | file, err := getFileInfoByFileName(fileName) 7 | defer file.Close() 8 | if err != nil { 9 | return "", err 10 | } 11 | 12 | storageServInfo, err := c.getStorageInfoByTracker(TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE, "", "") 13 | if err != nil { 14 | return "", err 15 | } 16 | 17 | uploadServ := &storageServerUploadHeaderBody{} 18 | uploadServ.fileInfo = file 19 | uploadServ.storagePathIndex = storageServInfo.storagePathIndex 20 | 21 | if err = c.sendCmdToStorageServer(uploadServ, storageServInfo); err != nil { 22 | return "", err 23 | } 24 | return uploadServ.fileId, nil 25 | } 26 | 27 | // UploadByBuffer 创建fdfs客户端, 上传文件 28 | // @buffer 二进制数据,适合小文件一次性发送 29 | // @fileExtName 指定文件在服务器端保存时的文件名 30 | func (c *trackerServerTcpClient) UploadByBuffer(buffer []byte, fileExtName string) (string, error) { 31 | tmpFileInfo, err := getFileInfoByFileByte(buffer, fileExtName) 32 | defer tmpFileInfo.Close() 33 | if err != nil { 34 | return "", err 35 | } 36 | storageServInfo, err := c.getStorageInfoByTracker(TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE, "", "") 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | uploadServ := &storageServerUploadHeaderBody{} 42 | uploadServ.fileInfo = tmpFileInfo 43 | uploadServ.storagePathIndex = storageServInfo.storagePathIndex 44 | 45 | if err = c.sendCmdToStorageServer(uploadServ, storageServInfo); err != nil { 46 | return "", err 47 | } 48 | return uploadServ.fileId, nil 49 | } 50 | -------------------------------------------------------------------------------- /tracker_tcp_conn.go: -------------------------------------------------------------------------------- 1 | package fastdfs_client_go 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "net" 8 | ) 9 | 10 | // fastdfs 设计原理: 11 | // 1.客户端首先连接到 trackerServer,通过发送指定的命令(cmd常量值)获取 storageServer 的信息 12 | // 2.客户端根据第一步获取的 storageServer =继续建立长连接 13 | // 3.最后,所有的文件上传、下载都是通过第二步中获取的 storageServer 连接继续发送具体命令实现的 14 | 15 | // 本页面的代码主要功能: 16 | // 1.与 trackerServer 实现网络通讯 17 | 18 | // trackerTcpConn trackerServer的tcp连接信息 19 | type trackerTcpConn struct { 20 | // send 参数 ↓ 21 | header 22 | groupName string 23 | remoteFilename string 24 | // receive 接受返回结果 ↓ 25 | storageInfo storageInfo 26 | } 27 | 28 | // trackerStorageInfo 通过 trackerServer 获取的 storageServer 信息 29 | type storageInfo struct { 30 | ipAddr string 31 | port int64 32 | groupName string 33 | storePathIndex byte 34 | } 35 | 36 | // Send 向对方发送数据 37 | // @tcpConn tcp 连接 38 | func (t *trackerTcpConn) Send(tcpConn net.Conn) error { 39 | if t.groupName == "" { 40 | if err := t.header.sendHeader(tcpConn); err != nil { 41 | return err 42 | } 43 | } else { 44 | t.header.pkgLen = int64(FDFS_GROUP_NAME_FIX_LEN + len(t.remoteFilename)) 45 | buffer := new(bytes.Buffer) 46 | if err := binary.Write(buffer, binary.BigEndian, t.pkgLen); err != nil { 47 | return err 48 | } 49 | buffer.WriteByte(t.header.cmd) 50 | buffer.WriteByte(t.header.status) 51 | buffer.Write(groupNameConvBytes(t.groupName)) 52 | buffer.WriteString(t.remoteFilename) 53 | if _, err := tcpConn.Write(buffer.Bytes()); err != nil { 54 | return err 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | // Receive 接受对方返回结果 61 | func (t *trackerTcpConn) Receive(tcpConn net.Conn) error { 62 | if err := t.receiveHeader(tcpConn); err != nil { 63 | return errors.New(ERROR_HEADER_RECEV_ERROR + err.Error()) 64 | } 65 | buf := make([]byte, t.pkgLen) 66 | if _, err := tcpConn.Read(buf); err != nil { 67 | return err 68 | } 69 | // 通信协议详情地址: https://mp.weixin.qq.com/s/lpWEv3NCLkfKmtzKJ5lGzQ 70 | // 响应body: 71 | //@group_name:16字节字符串,组名 72 | //@ip_addr:15字节字符串, storage server IP地址 73 | //@port:8字节整数,storage server端口号 74 | //@store_path_index:1字节整数,基于0的存储路径顺序号 75 | t.groupName = string(getBytesByPosition(buf, 0, 15)) 76 | t.storageInfo.ipAddr = string(getBytesByPosition(buf, 16, 15)) 77 | t.storageInfo.port = bytesToInt(getBytesByPosition(buf, 31, 8)) 78 | switch t.header.cmd { 79 | // 后面的参数需要根据具体的命令去设置 80 | case STORAGE_PROTO_CMD_UPLOAD_FILE: 81 | t.storageInfo.storePathIndex = getBytesByPosition(buf, 39, 1)[0] 82 | default: 83 | // 84 | } 85 | return nil 86 | } 87 | --------------------------------------------------------------------------------