├── .github └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── CHANGELOG_zh.md ├── LICENSE ├── Makefile ├── Makefile.cross-compiles ├── README.md ├── client ├── recv.go ├── send.go └── service.go ├── cmd ├── bandwidth-test │ ├── main.go │ ├── process.go │ ├── recv.go │ ├── root.go │ └── send.go ├── fft │ ├── main.go │ └── root.go ├── ffts │ ├── main.go │ └── root.go └── fftw │ ├── main.go │ └── root.go ├── go.mod ├── go.sum ├── hack └── run-e2e.sh ├── package.sh ├── pkg ├── io │ ├── io.go │ └── limit.go ├── log │ └── log.go ├── msg │ ├── ctl.go │ └── msg.go ├── receiver │ └── receiver.go ├── sender │ ├── frame.go │ ├── sender.go │ └── transfer.go └── stream │ ├── frame.go │ └── stream.go ├── server ├── client.go ├── match.go ├── service.go └── worker.go ├── test └── e2e │ ├── basic │ └── basic.go │ ├── e2e.go │ ├── e2e_test.go │ ├── framework │ ├── flags.go │ ├── framework.go │ ├── process.go │ └── util.go │ ├── go.mod │ ├── go.sum │ └── pkg │ └── process │ └── process.go ├── version └── version.go └── worker ├── match.go ├── register.go ├── service.go └── traffic.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | pull_request: {} 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: '1.23' 20 | 21 | - name: Check Format 22 | run: | 23 | unformatted=$(gofmt -l .) 24 | if [ -n "$unformatted" ]; then 25 | echo "The following files are not formatted with go fmt:" 26 | echo "$unformatted" 27 | exit 1 28 | fi 29 | 30 | - name: Build 31 | run: go build ./... 32 | 33 | - name: Vet 34 | run: go vet ./... 35 | 36 | - name: Test 37 | run: go test ./... 38 | 39 | e2e: 40 | runs-on: ubuntu-latest 41 | needs: build 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - name: Set up Go 46 | uses: actions/setup-go@v5 47 | with: 48 | go-version: '1.23' 49 | 50 | - name: Build binaries 51 | run: make build 52 | 53 | - name: Run e2e tests 54 | run: make e2e 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Self 14 | bin/ 15 | packages/ 16 | 17 | # Cache 18 | *.swp 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.2.0 2 | 3 | - Support bandwidth limit on fftw. 4 | - Support limit the max traffic fftw can used every day. 5 | 6 | ### v0.1.0 7 | 8 | - Support upload and download process bar. 9 | - Enable TLS between all components. 10 | - Support ACK message and retry missing data. 11 | - Add `-n` to set frame size of sender. Add `-c` to set cache frame count. 12 | 13 | ### v0.0.1 14 | 15 | - Init. 16 | -------------------------------------------------------------------------------- /CHANGELOG_zh.md: -------------------------------------------------------------------------------- 1 | ### v0.2.0 2 | 3 | - fftw 支持限速。 4 | - fftw 支持限制每天使用的流量,超过限制后会自动从服务器端注销,第二天恢复。 5 | 6 | ### v0.1.0 7 | 8 | - 支持上传下载进度条显示。 9 | - 所有组件启用 TLS 加密传输。 10 | - 发送方和接收方支持 ACK 确认消息,支持重传。 11 | - 发送方和接收方支持设置传输帧和缓冲区大小。 12 | 13 | ### v0.0.1 14 | 15 | - 初始可用版本。 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: fmt build 2 | 3 | build: fft fftw ffts bandwidth-test 4 | 5 | fmt: 6 | go fmt ./... 7 | 8 | fft: 9 | go build -ldflags "-s -w" -o bin/fft ./cmd/fft 10 | 11 | fftw: 12 | go build -ldflags "-s -w" -o bin/fftw ./cmd/fftw 13 | 14 | ffts: 15 | go build -ldflags "-s -w" -o bin/ffts ./cmd/ffts 16 | 17 | bandwidth-test: 18 | go build -ldflags "-s -w" -o bin/bandwidth-test ./cmd/bandwidth-test 19 | 20 | e2e: 21 | ./hack/run-e2e.sh 22 | -------------------------------------------------------------------------------- /Makefile.cross-compiles: -------------------------------------------------------------------------------- 1 | export PATH := $(GOPATH)/bin:$(PATH) 2 | LDFLAGS := -s -w 3 | 4 | all: build 5 | 6 | build: app 7 | 8 | app: 9 | env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fft_darwin_amd64 ./cmd/fft 10 | env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fftw_darwin_amd64 ./cmd/fftw 11 | env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./ffts_darwin_amd64 ./cmd/ffts 12 | env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fft_freebsd_amd64 ./cmd/fft 13 | env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fftw_freebsd_amd64 ./cmd/fftw 14 | env CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./ffts_freebsd_amd64 ./cmd/ffts 15 | env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./fft_linux_386 ./cmd/fft 16 | env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_386 ./cmd/fftw 17 | env CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_386 ./cmd/ffts 18 | env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fft_linux_amd64 ./cmd/fft 19 | env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_amd64 ./cmd/fftw 20 | env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_amd64 ./cmd/ffts 21 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "$(LDFLAGS)" -o ./fft_linux_arm ./cmd/fft 22 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_arm ./cmd/fftw 23 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_arm ./cmd/ffts 24 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ./fft_linux_arm64 ./cmd/fft 25 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_arm64 ./cmd/fftw 26 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_arm64 ./cmd/ffts 27 | env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fft_windows_amd64.exe ./cmd/fft 28 | env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./fftw_windows_amd64.exe ./cmd/fftw 29 | env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ./ffts_windows_amd64.exe ./cmd/ffts 30 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./fft_linux_mips64 ./cmd/fft 31 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_mips64 ./cmd/fftw 32 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips64 go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_mips64 ./cmd/ffts 33 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "$(LDFLAGS)" -o ./fft_linux_mips64le ./cmd/fft 34 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_mips64le ./cmd/fftw 35 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips64le go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_mips64le ./cmd/ffts 36 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./fft_linux_mips ./cmd/fft 37 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_mips ./cmd/fftw 38 | env CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_mips ./cmd/ffts 39 | env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./fft_linux_mipsle ./cmd/fft 40 | env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./fftw_linux_mipsle ./cmd/fftw 41 | env CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags "$(LDFLAGS)" -o ./ffts_linux_mipsle ./cmd/ffts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fft 2 | 3 | `fft` is a distributed file transfer tool designed to accelerate the transfer of large files. It achieves this by utilizing multiple relay nodes in parallel, effectively overcoming the bandwidth limitations of a single server. 4 | 5 | ## Purpose 6 | 7 | Transferring large files reliably between two machines, especially when they are behind NATs or firewalls, often requires a relay server with high bandwidth. However, high-bandwidth servers can be expensive. 8 | 9 | On the other hand, there are many low-bandwidth servers (e.g., 1MBps) that often sit idle, their resources underutilized. 10 | 11 | The goal of `fft` is to leverage these underutilized low-bandwidth servers. By enabling senders and receivers to transfer files through multiple relay nodes simultaneously, `fft` aggregates the bandwidth of these nodes, allowing for faster transfer speeds than would be possible with a single relay. This approach avoids the bottleneck of a single server's bandwidth capacity. 12 | 13 | `fft` focuses specifically on the task of file transfer, which typically involves a unidirectional flow of a large amount of data. Once this project matures, the plan is to integrate its capabilities into [frp](https://github.com/fatedier/frp), a fast reverse proxy, to enhance its services by removing bandwidth limitations imposed by a single relay server. 14 | 15 | ## Architecture 16 | 17 | The `fft` system consists of three main components: 18 | 19 | * **`ffts` (Server Control Node):** This is the central coordinator of the system. 20 | * It manages the registration of `fftw` worker nodes. 21 | * It facilitates the matching of sending and receiving `fft` clients. 22 | * When a sender initiates a file transfer, it contacts `ffts`. 23 | * When a receiver wants to download a file, it also contacts `ffts` using a transfer ID provided by the sender. 24 | * `ffts` then assigns available `fftw` worker nodes to the transfer, enabling the parallel data streams. 25 | * `ffts` does not handle any of_the actual file data_ itself; it only manages control signals and metadata. 26 | 27 | * **`fftw` (Worker Node):** These nodes are responsible for relaying the actual file data. 28 | * Multiple `fftw` instances can be deployed on various servers. 29 | * Each `fftw` registers itself with the `ffts` server, making itself available for relaying transfers. 30 | * Upon instruction from `ffts`, an `fftw` node will accept data from a sending `fft` client and forward it to the receiving `fft` client. 31 | * The more `fftw` nodes available and assigned to a transfer, the higher the potential aggregate bandwidth and thus faster transfer speeds. 32 | 33 | * **`fft` (Client):** This is the command-line tool used by end-users to send or receive files. 34 | * **Sender:** When sending a file, the `fft` client: 35 | 1. Contacts the `ffts` server to announce a new transfer and receives a unique transfer ID. 36 | 2. Communicates this transfer ID to the intended recipient (e.g., via email, messaging). 37 | 3. Upon `ffts` matching it with a receiver and assigning `fftw` nodes, the client splits the file data and sends parts of it in parallel to the assigned `fftw` nodes. 38 | * **Receiver:** When receiving a file, the `fft` client: 39 | 1. Contacts the `ffts` server using the transfer ID obtained from the sender. 40 | 2. `ffts` matches the receiver with the sender and provides the list of `fftw` nodes involved in the transfer. 41 | 3. The client then receives data in parallel from these `fftw` nodes and reassembles the original file. 42 | 43 | The overall interaction is as follows: 44 | 1. `fftw` nodes start up and register with `ffts`. 45 | 2. An `fft` client (sender) initiates a transfer by contacting `ffts`. `ffts` provides a transfer ID. 46 | 3. The sender shares this transfer ID with another `fft` client (receiver). 47 | 4. The `fft` client (receiver) contacts `ffts` with the transfer ID. 48 | 5. `ffts` matches the sender and receiver and allocates a set of registered `fftw` nodes for the transfer. It informs both clients about these worker nodes. 49 | 6. The `fft` client (sender) then starts sending file data in parallel streams to the allocated `fftw` nodes. 50 | 7. The `fftw` nodes relay this data to the `fft` client (receiver). 51 | 8. The `fft` client (receiver) reassembles the data from the parallel streams to reconstruct the original file. 52 | 53 | This architecture allows `fft` to achieve high-speed file transfers by distributing the load across multiple relay servers (`fftw`s), orchestrated by the central `ffts` controller. 54 | 55 | ## Development Status 56 | 57 | `fft` is currently in the early stages of development. Features are still being added, and it is primarily intended for testing purposes. 58 | 59 | The `master` branch is used for stable releases, while the `dev` branch is for ongoing development. You can try the latest release for testing. 60 | 61 | **The current communication protocol may change at any time and backward compatibility is not guaranteed. Please check the release notes when upgrading to a new version.** 62 | 63 | ## Usage Example 64 | 65 | * `ffts`: The server control node. Deploy one instance of this. 66 | * `fftw`: Worker nodes that relay traffic. Deploy multiple instances of these. More worker nodes can increase file transfer speed. 67 | * `fft`: The client, used for sending and receiving files. 68 | 69 | Each program's usage parameters can be viewed by running it with the `-h` flag. 70 | 71 | `ffts` and `fftw` need to be deployed on machines with public IP addresses, and their respective ports must be open for `fft` clients to access. 72 | 73 | By default, `fftw` and `fft` will attempt to connect to the `ffts` service at `fft.gofrp.org:7777`. If you wish to use your own `ffts` instance, you can specify its address using the `-s {server-addr}` option. 74 | 75 | ### Sending a File 76 | 77 | `./fft -i 123 -l ./filename` 78 | 79 | * `-i 123`: Specifies the transfer request ID. This should be a unique custom value. The receiver will use this ID to accept the file. 80 | * `-l ./filename`: Specifies the path to the local file to be transferred. 81 | 82 | ### Receiving a File 83 | 84 | `./fft -i 123 -t ./` 85 | 86 | * `-i 123`: Specifies the ID of the transfer request to accept. 87 | * `-t ./`: Specifies the local path to save the received file. If it's a directory, the sender's original filename will be used and the file saved in that directory. Otherwise, a new file will be created with the specified name. 88 | -------------------------------------------------------------------------------- /client/recv.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | "sync" 10 | "time" 11 | 12 | fio "github.com/fatedier/fft/pkg/io" 13 | "github.com/fatedier/fft/pkg/msg" 14 | "github.com/fatedier/fft/pkg/receiver" 15 | "github.com/fatedier/fft/pkg/stream" 16 | 17 | "github.com/cheggaaa/pb" 18 | ) 19 | 20 | func (svc *Service) recvFile(id string, filePath string) error { 21 | isDir := false 22 | finfo, err := os.Stat(filePath) 23 | if err == nil && finfo.IsDir() { 24 | isDir = true 25 | } 26 | 27 | conn, err := net.Dial("tcp", svc.serverAddr) 28 | if err != nil { 29 | return err 30 | } 31 | conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) 32 | defer conn.Close() 33 | 34 | msg.WriteMsg(conn, &msg.ReceiveFile{ 35 | ID: id, 36 | CacheCount: int64(svc.cacheCount), 37 | }) 38 | 39 | conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 40 | raw, err := msg.ReadMsg(conn) 41 | if err != nil { 42 | return err 43 | } 44 | conn.SetReadDeadline(time.Time{}) 45 | 46 | m, ok := raw.(*msg.ReceiveFileResp) 47 | if !ok { 48 | return fmt.Errorf("get send file response format error") 49 | } 50 | if m.Error != "" { 51 | return fmt.Errorf(m.Error) 52 | } 53 | 54 | if len(m.Workers) == 0 { 55 | return fmt.Errorf("no available workers") 56 | } 57 | 58 | fmt.Printf("Recv filename: %s Size: %s\n", m.Name, pb.Format(m.Fsize).To(pb.U_BYTES).String()) 59 | if svc.debugMode { 60 | fmt.Printf("Workers: %v\n", m.Workers) 61 | } 62 | 63 | realPath := filePath 64 | if isDir { 65 | realPath = filepath.Join(filePath, m.Name) 66 | } 67 | f, err := os.Create(realPath) 68 | if err != nil { 69 | return err 70 | } 71 | defer f.Close() 72 | 73 | var wait sync.WaitGroup 74 | count := m.Fsize 75 | bar := pb.New(int(count)) 76 | bar.ShowSpeed = true 77 | bar.SetUnits(pb.U_BYTES) 78 | 79 | if !svc.debugMode { 80 | bar.Start() 81 | } 82 | 83 | callback := func(n int) { 84 | bar.Add(n) 85 | } 86 | 87 | recv := receiver.NewReceiver(0, fio.NewCallbackWriter(f, callback)) 88 | 89 | if svc.enableUnorderedProcessing { 90 | if svc.debugMode { 91 | fmt.Printf("Enabling unordered frame processing with buffer size: %d\n", svc.bufferSize) 92 | } 93 | recv.EnableUnorderedProcessing(svc.bufferSize) 94 | } 95 | 96 | for _, worker := range m.Workers { 97 | wait.Add(1) 98 | go func(addr string) { 99 | newRecvStream(recv, id, addr, svc.debugMode) 100 | wait.Done() 101 | }(worker) 102 | } 103 | 104 | recvDoneCh := make(chan struct{}) 105 | streamCloseCh := make(chan struct{}) 106 | go func() { 107 | recv.Run() 108 | close(recvDoneCh) 109 | }() 110 | go func() { 111 | wait.Wait() 112 | close(streamCloseCh) 113 | }() 114 | 115 | select { 116 | case <-recvDoneCh: 117 | case <-streamCloseCh: 118 | select { 119 | case <-recvDoneCh: 120 | case <-time.After(2 * time.Second): 121 | } 122 | } 123 | 124 | if !svc.debugMode { 125 | bar.Finish() 126 | } 127 | return nil 128 | } 129 | 130 | func newRecvStream(recv *receiver.Receiver, id string, addr string, debugMode bool) { 131 | conn, err := net.Dial("tcp", addr) 132 | if err != nil { 133 | log(debugMode, "[%s] %v", addr, err) 134 | return 135 | } 136 | conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) 137 | 138 | msg.WriteMsg(conn, &msg.NewReceiveFileStream{ 139 | ID: id, 140 | }) 141 | 142 | conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 143 | raw, err := msg.ReadMsg(conn) 144 | if err != nil { 145 | conn.Close() 146 | log(debugMode, "[%s] %v", addr, err) 147 | return 148 | } 149 | conn.SetReadDeadline(time.Time{}) 150 | m, ok := raw.(*msg.NewReceiveFileStreamResp) 151 | if !ok { 152 | conn.Close() 153 | log(debugMode, "[%s] read NewReceiveFileStreamResp format error", addr) 154 | return 155 | } 156 | 157 | if m.Error != "" { 158 | conn.Close() 159 | log(debugMode, "[%s] new recv file stream error: %s", addr, m.Error) 160 | return 161 | } 162 | 163 | s := stream.NewFrameStream(conn) 164 | for { 165 | frame, err := s.ReadFrame() 166 | if err != nil { 167 | return 168 | } 169 | recv.RecvFrame(frame) 170 | err = s.WriteAck(&stream.Ack{ 171 | FileID: frame.FileID, 172 | FrameID: frame.FrameID, 173 | }) 174 | if err != nil { 175 | return 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /client/send.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | fio "github.com/fatedier/fft/pkg/io" 12 | "github.com/fatedier/fft/pkg/msg" 13 | "github.com/fatedier/fft/pkg/sender" 14 | "github.com/fatedier/fft/pkg/stream" 15 | 16 | "github.com/cheggaaa/pb" 17 | ) 18 | 19 | func (svc *Service) sendFile(id string, filePath string) error { 20 | conn, err := net.Dial("tcp", svc.serverAddr) 21 | if err != nil { 22 | return err 23 | } 24 | conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) 25 | defer conn.Close() 26 | 27 | f, err := os.Open(filePath) 28 | if err != nil { 29 | return err 30 | } 31 | defer f.Close() 32 | 33 | finfo, err := f.Stat() 34 | if err != nil { 35 | return err 36 | } 37 | if finfo.IsDir() { 38 | return fmt.Errorf("send file can't be a directory") 39 | } 40 | 41 | msg.WriteMsg(conn, &msg.SendFile{ 42 | ID: id, 43 | Name: finfo.Name(), 44 | Fsize: finfo.Size(), 45 | CacheCount: int64(svc.cacheCount), 46 | }) 47 | 48 | fmt.Printf("Wait receiver...\n") 49 | conn.SetReadDeadline(time.Now().Add(120 * time.Second)) 50 | raw, err := msg.ReadMsg(conn) 51 | if err != nil { 52 | return err 53 | } 54 | conn.SetReadDeadline(time.Time{}) 55 | 56 | m, ok := raw.(*msg.SendFileResp) 57 | if !ok { 58 | return fmt.Errorf("get send file response format error") 59 | } 60 | if m.Error != "" { 61 | return fmt.Errorf(m.Error) 62 | } 63 | 64 | if len(m.Workers) == 0 { 65 | return fmt.Errorf("no available workers") 66 | } 67 | svc.cacheCount = int(m.CacheCount) 68 | fmt.Printf("ID: %s\n", m.ID) 69 | if svc.debugMode { 70 | fmt.Printf("Workers: %v\n", m.Workers) 71 | } 72 | 73 | var wait sync.WaitGroup 74 | count := finfo.Size() 75 | bar := pb.New(int(count)) 76 | bar.ShowSpeed = true 77 | bar.SetUnits(pb.U_BYTES) 78 | if !svc.debugMode { 79 | bar.Start() 80 | } 81 | 82 | callback := func(n int) { 83 | bar.Add(n) 84 | } 85 | 86 | s, err := sender.NewSender(0, fio.NewCallbackReader(f, callback), svc.frameSize, svc.cacheCount) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if svc.dynamicAllocation { 92 | if svc.debugMode { 93 | fmt.Println("Enabling dynamic frame allocation") 94 | } 95 | s.EnableDynamicAllocation() 96 | } 97 | 98 | for _, worker := range m.Workers { 99 | wait.Add(1) 100 | go func(addr string) { 101 | newSendStream(s, m.ID, addr, svc.debugMode) 102 | wait.Done() 103 | }(worker) 104 | } 105 | go s.Run() 106 | wait.Wait() 107 | 108 | if !svc.debugMode { 109 | bar.Finish() 110 | } 111 | return nil 112 | } 113 | 114 | func newSendStream(s *sender.Sender, id string, addr string, debugMode bool) { 115 | conn, err := net.Dial("tcp", addr) 116 | if err != nil { 117 | log(debugMode, "[%s] %v", addr, err) 118 | return 119 | } 120 | conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) 121 | 122 | msg.WriteMsg(conn, &msg.NewSendFileStream{ 123 | ID: id, 124 | }) 125 | 126 | conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 127 | raw, err := msg.ReadMsg(conn) 128 | if err != nil { 129 | conn.Close() 130 | log(debugMode, "[%s] %v", addr, err) 131 | return 132 | } 133 | conn.SetReadDeadline(time.Time{}) 134 | m, ok := raw.(*msg.NewSendFileStreamResp) 135 | if !ok { 136 | conn.Close() 137 | log(debugMode, "[%s] read NewSendFileStreamResp format error", addr) 138 | return 139 | } 140 | 141 | if m.Error != "" { 142 | conn.Close() 143 | log(debugMode, "[%s] new send file stream error: %s", addr, m.Error) 144 | return 145 | } 146 | 147 | s.HandleStream(stream.NewFrameStream(conn)) 148 | } 149 | -------------------------------------------------------------------------------- /client/service.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Options struct { 8 | ServerAddr string 9 | ID string 10 | SendFile string 11 | FrameSize int 12 | CacheCount int 13 | RecvFile string 14 | DebugMode bool 15 | EnableUnorderedProcessing bool 16 | DynamicAllocation bool 17 | BufferSize int 18 | } 19 | 20 | func (op *Options) Check() error { 21 | if op.SendFile == "" && op.RecvFile == "" { 22 | return fmt.Errorf("send_file or recv_file is required") 23 | } 24 | 25 | if op.SendFile != "" { 26 | if op.FrameSize <= 0 { 27 | return fmt.Errorf("frame_size should be greater than 0") 28 | } 29 | } 30 | 31 | if op.CacheCount <= 0 { 32 | return fmt.Errorf("cache_count should be greater than 0") 33 | } 34 | return nil 35 | } 36 | 37 | type Service struct { 38 | debugMode bool 39 | serverAddr string 40 | frameSize int 41 | cacheCount int 42 | enableUnorderedProcessing bool 43 | dynamicAllocation bool 44 | bufferSize int 45 | 46 | runHandler func() error 47 | } 48 | 49 | func NewService(options Options) (*Service, error) { 50 | if err := options.Check(); err != nil { 51 | return nil, err 52 | } 53 | 54 | svc := &Service{ 55 | debugMode: options.DebugMode, 56 | serverAddr: options.ServerAddr, 57 | frameSize: options.FrameSize, 58 | cacheCount: options.CacheCount, 59 | enableUnorderedProcessing: options.EnableUnorderedProcessing, 60 | dynamicAllocation: options.DynamicAllocation, 61 | bufferSize: options.BufferSize, 62 | } 63 | 64 | if options.SendFile != "" { 65 | svc.runHandler = func() error { 66 | return svc.sendFile(options.ID, options.SendFile) 67 | } 68 | } else { 69 | svc.runHandler = func() error { 70 | return svc.recvFile(options.ID, options.RecvFile) 71 | } 72 | } 73 | return svc, nil 74 | } 75 | 76 | func (svc *Service) Run() error { 77 | err := svc.runHandler() 78 | if err != nil && svc.debugMode { 79 | fmt.Println(err) 80 | } 81 | return err 82 | } 83 | 84 | func log(debugMode bool, foramt string, v ...interface{}) { 85 | if debugMode { 86 | fmt.Printf(foramt+"\n", v...) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/bandwidth-test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func main() { 8 | if err := rootCmd.Execute(); err != nil { 9 | os.Exit(1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cmd/bandwidth-test/process.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "sync" 11 | ) 12 | 13 | type Process struct { 14 | cmd *exec.Cmd 15 | cancel context.CancelFunc 16 | errorOutput *bytes.Buffer 17 | stdOutput *bytes.Buffer 18 | mutex sync.Mutex 19 | stopped bool 20 | name string // Process name for logging 21 | showOutput bool // Whether to show output in real-time 22 | } 23 | 24 | type OutputWriter struct { 25 | buffer *bytes.Buffer 26 | prefix string 27 | } 28 | 29 | func (w *OutputWriter) Write(p []byte) (n int, err error) { 30 | n, err = w.buffer.Write(p) 31 | if err != nil { 32 | return n, err 33 | } 34 | 35 | fmt.Print(w.prefix) 36 | fmt.Print(string(p)) 37 | 38 | return n, nil 39 | } 40 | 41 | func NewProcess(name, path string, params []string, showOutput bool) *Process { 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | cmd := exec.CommandContext(ctx, path, params...) 44 | p := &Process{ 45 | cmd: cmd, 46 | cancel: cancel, 47 | name: name, 48 | showOutput: showOutput, 49 | } 50 | p.errorOutput = bytes.NewBufferString("") 51 | p.stdOutput = bytes.NewBufferString("") 52 | 53 | if showOutput { 54 | stdoutWriter := &OutputWriter{ 55 | buffer: p.stdOutput, 56 | prefix: fmt.Sprintf("[%s] ", name), 57 | } 58 | stderrWriter := &OutputWriter{ 59 | buffer: p.errorOutput, 60 | prefix: fmt.Sprintf("[%s ERROR] ", name), 61 | } 62 | cmd.Stdout = stdoutWriter 63 | cmd.Stderr = stderrWriter 64 | } else { 65 | cmd.Stdout = p.stdOutput 66 | cmd.Stderr = p.errorOutput 67 | } 68 | 69 | return p 70 | } 71 | 72 | func (p *Process) Start() error { 73 | return p.cmd.Start() 74 | } 75 | 76 | func (p *Process) Stop() error { 77 | p.mutex.Lock() 78 | defer p.mutex.Unlock() 79 | 80 | if p.stopped { 81 | return nil 82 | } 83 | 84 | p.stopped = true 85 | p.cancel() 86 | return p.cmd.Wait() 87 | } 88 | 89 | func (p *Process) ErrorOutput() string { 90 | p.mutex.Lock() 91 | defer p.mutex.Unlock() 92 | return p.errorOutput.String() 93 | } 94 | 95 | func (p *Process) StdOutput() string { 96 | p.mutex.Lock() 97 | defer p.mutex.Unlock() 98 | return p.stdOutput.String() 99 | } 100 | 101 | func GetExecutablePath(name string) string { 102 | currentDirPath := "./" + name 103 | if _, err := os.Stat(currentDirPath); err == nil { 104 | return currentDirPath 105 | } 106 | 107 | if _, err := os.Stat(name); err == nil { 108 | return name 109 | } 110 | 111 | cwd, err := os.Getwd() 112 | if err == nil { 113 | binPath := filepath.Join(cwd, "bin", name) 114 | if _, err := os.Stat(binPath); err == nil { 115 | return binPath 116 | } 117 | } 118 | 119 | path, err := exec.LookPath(name) 120 | if err == nil { 121 | return path 122 | } 123 | 124 | return name 125 | } 126 | -------------------------------------------------------------------------------- /cmd/bandwidth-test/recv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func recvFile(serverAddr, id, filePath string, cacheCount int, callback func(n int)) error { 4 | return nil 5 | } 6 | -------------------------------------------------------------------------------- /cmd/bandwidth-test/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/fatedier/fft/version" 14 | 15 | "github.com/cheggaaa/pb" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var ( 20 | showVersion bool 21 | fileSize int64 22 | duration int 23 | tempDir string 24 | workers string 25 | verbose bool // Whether to show detailed output from external processes 26 | enableUnorderedProcessing bool // Enable out-of-order frame processing 27 | dynamicAllocation bool // Enable dynamic frame allocation 28 | bufferSize int // Maximum buffer size for out-of-order frames 29 | ) 30 | 31 | func init() { 32 | rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of bandwidth test tool") 33 | rootCmd.PersistentFlags().Int64VarP(&fileSize, "file-size", "s", 0, "test file size in KB, 0 means auto calculate based on duration") 34 | rootCmd.PersistentFlags().IntVarP(&duration, "duration", "d", 25, "expected test duration in seconds, used to calculate file size if not specified") 35 | rootCmd.PersistentFlags().StringVarP(&tempDir, "temp-dir", "t", os.TempDir(), "directory to store temporary files") 36 | rootCmd.PersistentFlags().StringVarP(&workers, "workers", "w", "100KB,500KB", "worker bandwidth configuration, comma-separated list of bandwidth limits (e.g., '200KB' for one worker, '200KB,200KB,300KB' for three workers)") 37 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "", false, "show detailed output from external processes") 38 | 39 | rootCmd.PersistentFlags().BoolVar(&enableUnorderedProcessing, "enable-unordered-processing", false, "enable out-of-order frame processing to improve efficiency with unbalanced workers") 40 | rootCmd.PersistentFlags().BoolVar(&dynamicAllocation, "dynamic-allocation", false, "enable dynamic frame allocation based on worker performance") 41 | rootCmd.PersistentFlags().IntVar(&bufferSize, "buffer-size", 1000, "maximum buffer size for out-of-order frames (only used with --enable-unordered-processing)") 42 | } 43 | 44 | var rootCmd = &cobra.Command{ 45 | Use: "bandwidth-test", 46 | Short: "bandwidth-test is a tool to test bandwidth aggregation in fft (https://github.com/fatedier/fft)", 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | if showVersion { 49 | fmt.Println(version.Full()) 50 | return nil 51 | } 52 | 53 | return runBandwidthTest() 54 | }, 55 | } 56 | 57 | func parseWorkerBandwidths(workersStr string) ([]int, error) { 58 | if workersStr == "" { 59 | return []int{100, 500}, nil // Default: 100KB/s and 500KB/s 60 | } 61 | 62 | parts := strings.Split(workersStr, ",") 63 | rates := make([]int, 0, len(parts)) 64 | 65 | for _, part := range parts { 66 | part = strings.TrimSpace(part) 67 | 68 | part = strings.TrimSuffix(part, "KB") 69 | 70 | rate, err := strconv.Atoi(part) 71 | if err != nil { 72 | return nil, fmt.Errorf("invalid bandwidth format '%s': %v", part, err) 73 | } 74 | 75 | if rate < 50 { 76 | return nil, fmt.Errorf("bandwidth must be at least 50KB/s, got %dKB/s", rate) 77 | } 78 | 79 | rates = append(rates, rate) 80 | } 81 | 82 | if len(rates) == 0 { 83 | return nil, fmt.Errorf("no valid worker bandwidths specified") 84 | } 85 | 86 | return rates, nil 87 | } 88 | 89 | func setupTestEnvironment() (string, []int, int, error) { 90 | fmt.Println("Starting bandwidth aggregation test...") 91 | 92 | testDir := filepath.Join(tempDir, fmt.Sprintf("fft-bandwidth-test-%d", time.Now().UnixNano())) 93 | err := os.MkdirAll(testDir, 0755) 94 | if err != nil { 95 | return "", nil, 0, fmt.Errorf("failed to create test directory: %v", err) 96 | } 97 | 98 | workerRates, err := parseWorkerBandwidths(workers) 99 | if err != nil { 100 | return "", nil, 0, fmt.Errorf("failed to parse worker configuration: %v", err) 101 | } 102 | 103 | var totalBandwidth int 104 | for _, rate := range workerRates { 105 | totalBandwidth += rate 106 | } 107 | 108 | return testDir, workerRates, totalBandwidth, nil 109 | } 110 | 111 | func startServer() (string, *Process, error) { 112 | serverPort, err := allocPort() 113 | if err != nil { 114 | return "", nil, fmt.Errorf("failed to allocate server port: %v", err) 115 | } 116 | serverAddr := fmt.Sprintf("127.0.0.1:%d", serverPort) 117 | 118 | fftsPath := GetExecutablePath("ffts") 119 | serverArgs := []string{ 120 | "--bind-addr", serverAddr, 121 | } 122 | 123 | serverProcess := NewProcess("Server", fftsPath, serverArgs, verbose) 124 | err = serverProcess.Start() 125 | if err != nil { 126 | return "", nil, fmt.Errorf("failed to start server process: %v", err) 127 | } 128 | 129 | fmt.Printf("Server started on %s\n", serverAddr) 130 | time.Sleep(1 * time.Second) 131 | 132 | if !verbose { 133 | if output := serverProcess.ErrorOutput(); len(output) > 0 { 134 | fmt.Printf("Server startup warnings/errors: %s\n", output) 135 | } 136 | } 137 | 138 | return serverAddr, serverProcess, nil 139 | } 140 | 141 | func startWorkers(serverAddr string, workerRates []int) ([]*Process, error) { 142 | workerProcesses := make([]*Process, 0, len(workerRates)) 143 | fftwPath := GetExecutablePath("fftw") 144 | 145 | for i, rate := range workerRates { 146 | workerPort, err := allocPort() 147 | if err != nil { 148 | return nil, fmt.Errorf("failed to allocate worker%d port: %v", i+1, err) 149 | } 150 | workerAddr := fmt.Sprintf("127.0.0.1:%d", workerPort) 151 | 152 | workerArgs := []string{ 153 | "--server-addr", serverAddr, 154 | "--bind-addr", workerAddr, 155 | "--advice-public-ip", "127.0.0.1", 156 | "--rate", fmt.Sprintf("%d", rate), 157 | } 158 | 159 | workerName := fmt.Sprintf("Worker%d", i+1) 160 | workerProcess := NewProcess(workerName, fftwPath, workerArgs, verbose) 161 | err = workerProcess.Start() 162 | if err != nil { 163 | return nil, fmt.Errorf("failed to start worker%d process: %v", i+1, err) 164 | } 165 | workerProcesses = append(workerProcesses, workerProcess) 166 | 167 | fmt.Printf("%s started on %s with bandwidth limit %dKB/s\n", workerName, workerAddr, rate) 168 | } 169 | 170 | time.Sleep(2 * time.Second) 171 | 172 | if !verbose { 173 | for i, process := range workerProcesses { 174 | if output := process.ErrorOutput(); len(output) > 0 { 175 | fmt.Printf("Worker%d startup warnings/errors: %s\n", i+1, output) 176 | } 177 | } 178 | } 179 | 180 | time.Sleep(2 * time.Second) 181 | return workerProcesses, nil 182 | } 183 | 184 | func prepareTestFile(testDir string, totalBandwidth int) (string, string, string, int64, error) { 185 | calculatedFileSize := fileSize 186 | if calculatedFileSize == 0 { 187 | expectedSpeed := float64(totalBandwidth) * 1024 * 0.4 // KB/s to bytes/sec with efficiency factor 188 | calculatedFileSize = int64(expectedSpeed * float64(duration) / 1024) // Convert bytes to KB 189 | fmt.Printf("Auto-calculated file size: %d KB (%.2f MB) for %d seconds test\n", 190 | calculatedFileSize, float64(calculatedFileSize)/1024, duration) 191 | rates, _ := parseWorkerBandwidths(workers) 192 | fmt.Printf("Based on total bandwidth of %dKB/s across %d workers\n", 193 | totalBandwidth, len(rates)) 194 | } 195 | 196 | fileSizeBytes := calculatedFileSize * 1024 197 | 198 | testFilePath := filepath.Join(testDir, "test-file") 199 | err := createTestFile(testFilePath, fileSizeBytes) 200 | if err != nil { 201 | return "", "", "", 0, fmt.Errorf("failed to create test file: %v", err) 202 | } 203 | 204 | recvDir := filepath.Join(testDir, "recv") 205 | err = os.MkdirAll(recvDir, 0755) 206 | if err != nil { 207 | return "", "", "", 0, fmt.Errorf("failed to create receive directory: %v", err) 208 | } 209 | 210 | transferID := fmt.Sprintf("bandwidth-test-%d", time.Now().UnixNano()) 211 | return testFilePath, recvDir, transferID, fileSizeBytes, nil 212 | } 213 | 214 | func startSender(serverAddr, transferID, testFilePath string) (*Process, chan error, *pb.ProgressBar, error) { 215 | finfo, err := os.Stat(testFilePath) 216 | if err != nil { 217 | return nil, nil, nil, fmt.Errorf("failed to stat test file: %v", err) 218 | } 219 | 220 | bar := pb.New(int(finfo.Size())) 221 | bar.ShowSpeed = true 222 | bar.SetUnits(pb.U_BYTES) 223 | bar.Start() 224 | 225 | senderDoneCh := make(chan error, 1) 226 | fftPath := GetExecutablePath("fft") 227 | senderArgs := []string{ 228 | "--server-addr", serverAddr, 229 | "--id", transferID, 230 | "--send-file", testFilePath, 231 | "--frame-size", fmt.Sprintf("%d", 5*1024), 232 | "--cache-count", "512", 233 | } 234 | 235 | if verbose { 236 | senderArgs = append(senderArgs, "--debug") 237 | } 238 | 239 | if dynamicAllocation { 240 | senderArgs = append(senderArgs, "--dynamic-allocation") 241 | fmt.Println("Dynamic frame allocation enabled") 242 | } 243 | 244 | senderProcess := NewProcess("Sender", fftPath, senderArgs, verbose) 245 | fmt.Println("Starting sender...") 246 | 247 | go func() { 248 | err := senderProcess.Start() 249 | if err != nil { 250 | senderDoneCh <- fmt.Errorf("failed to start sender process: %v", err) 251 | return 252 | } 253 | 254 | err = senderProcess.cmd.Wait() 255 | if err != nil { 256 | senderDoneCh <- fmt.Errorf("sender process error: %v", err) 257 | return 258 | } 259 | 260 | if output := senderProcess.ErrorOutput(); strings.Contains(output, "error") { 261 | senderDoneCh <- fmt.Errorf("sender error: %s", output) 262 | return 263 | } 264 | 265 | bar.Finish() 266 | senderDoneCh <- nil 267 | }() 268 | 269 | return senderProcess, senderDoneCh, bar, nil 270 | } 271 | 272 | func startReceiver(serverAddr, transferID, recvDir string, fileSizeBytes int64) (*Process, chan error, *pb.ProgressBar, time.Time, error) { 273 | recvBar := pb.New(int(fileSizeBytes)) 274 | recvBar.ShowSpeed = true 275 | recvBar.SetUnits(pb.U_BYTES) 276 | recvBar.Start() 277 | 278 | receiverDoneCh := make(chan error, 1) 279 | fftPath := GetExecutablePath("fft") 280 | receiverArgs := []string{ 281 | "--server-addr", serverAddr, 282 | "--id", transferID, 283 | "--recv-file", recvDir, 284 | "--cache-count", "512", 285 | } 286 | 287 | if verbose { 288 | receiverArgs = append(receiverArgs, "--debug") 289 | } 290 | 291 | if enableUnorderedProcessing { 292 | receiverArgs = append(receiverArgs, "--enable-unordered-processing") 293 | if bufferSize > 0 { 294 | receiverArgs = append(receiverArgs, "--buffer-size", fmt.Sprintf("%d", bufferSize)) 295 | } 296 | fmt.Printf("Unordered frame processing enabled with buffer size: %d\n", bufferSize) 297 | } 298 | 299 | receiverProcess := NewProcess("Receiver", fftPath, receiverArgs, verbose) 300 | fmt.Println("Starting receiver...") 301 | 302 | startTime := time.Now() 303 | 304 | go func() { 305 | err := receiverProcess.Start() 306 | if err != nil { 307 | receiverDoneCh <- fmt.Errorf("failed to start receiver process: %v", err) 308 | return 309 | } 310 | 311 | err = receiverProcess.cmd.Wait() 312 | if err != nil { 313 | receiverDoneCh <- fmt.Errorf("receiver process error: %v", err) 314 | return 315 | } 316 | 317 | if output := receiverProcess.ErrorOutput(); strings.Contains(output, "error") { 318 | receiverDoneCh <- fmt.Errorf("receiver error: %s", output) 319 | return 320 | } 321 | 322 | recvBar.Finish() 323 | receiverDoneCh <- nil 324 | }() 325 | 326 | return receiverProcess, receiverDoneCh, recvBar, startTime, nil 327 | } 328 | 329 | func displayResults(testFilePath, recvDir string, startTime time.Time, totalBandwidth int, workerRates []int) error { 330 | endTime := time.Now() 331 | duration := endTime.Sub(startTime) 332 | 333 | receivedFilePath := filepath.Join(recvDir, filepath.Base(testFilePath)) 334 | receivedInfo, err := os.Stat(receivedFilePath) 335 | if err != nil { 336 | return fmt.Errorf("failed to stat received file: %v", err) 337 | } 338 | totalBytes := receivedInfo.Size() 339 | 340 | bytesPerSecond := float64(totalBytes) / duration.Seconds() 341 | kbPerSecond := bytesPerSecond / 1024 342 | 343 | fmt.Printf("\nBandwidth Test Results:\n") 344 | fmt.Printf("Total transferred: %d bytes (%.2f KB)\n", totalBytes, float64(totalBytes)/1024) 345 | fmt.Printf("Transfer duration: %.2f seconds\n", duration.Seconds()) 346 | fmt.Printf("Average transfer speed: %.2f KB/s\n", kbPerSecond) 347 | 348 | fmt.Printf("Worker configuration: ") 349 | for i, rate := range workerRates { 350 | if i > 0 { 351 | fmt.Printf(", ") 352 | } 353 | fmt.Printf("%dKB/s", rate) 354 | } 355 | fmt.Printf("\n") 356 | 357 | fmt.Printf("Expected combined speed: %d KB/s\n", totalBandwidth) 358 | fmt.Printf("Efficiency: %.2f%%\n", (kbPerSecond/float64(totalBandwidth))*100) 359 | 360 | fmt.Println("File transfer completed successfully!") 361 | return nil 362 | } 363 | 364 | func runBandwidthTest() error { 365 | testDir, workerRates, totalBandwidth, err := setupTestEnvironment() 366 | if err != nil { 367 | return err 368 | } 369 | defer os.RemoveAll(testDir) 370 | 371 | serverAddr, serverProcess, err := startServer() 372 | if err != nil { 373 | return err 374 | } 375 | defer serverProcess.Stop() 376 | 377 | workerProcesses, err := startWorkers(serverAddr, workerRates) 378 | if err != nil { 379 | return err 380 | } 381 | for _, process := range workerProcesses { 382 | defer process.Stop() 383 | } 384 | 385 | testFilePath, recvDir, transferID, fileSizeBytes, err := prepareTestFile(testDir, totalBandwidth) 386 | if err != nil { 387 | return err 388 | } 389 | 390 | fmt.Println("Starting file transfer...") 391 | 392 | _, senderDoneCh, bar, err := startSender(serverAddr, transferID, testFilePath) 393 | if err != nil { 394 | return err 395 | } 396 | 397 | time.Sleep(2 * time.Second) 398 | 399 | _, receiverDoneCh, _, startTime, err := startReceiver(serverAddr, transferID, recvDir, fileSizeBytes) 400 | if err != nil { 401 | return err 402 | } 403 | 404 | senderErr := <-senderDoneCh 405 | if senderErr != nil { 406 | return fmt.Errorf("sender error: %v", senderErr) 407 | } 408 | 409 | select { 410 | case receiverErr := <-receiverDoneCh: 411 | if receiverErr != nil { 412 | return fmt.Errorf("receiver error: %v", receiverErr) 413 | } 414 | case <-time.After(5 * time.Second): 415 | fmt.Println("Receiver timeout - this is expected as sender has completed") 416 | } 417 | 418 | bar.Finish() 419 | 420 | return displayResults(testFilePath, recvDir, startTime, totalBandwidth, workerRates) 421 | } 422 | 423 | func allocPort() (int, error) { 424 | l, err := net.Listen("tcp", "127.0.0.1:0") 425 | if err != nil { 426 | return 0, err 427 | } 428 | defer l.Close() 429 | 430 | return l.Addr().(*net.TCPAddr).Port, nil 431 | } 432 | 433 | func createTestFile(path string, size int64) error { 434 | f, err := os.Create(path) 435 | if err != nil { 436 | return err 437 | } 438 | defer f.Close() 439 | 440 | const bufSize = 64 * 1024 441 | buf := make([]byte, bufSize) 442 | 443 | rand.Read(buf) 444 | 445 | remaining := size 446 | for remaining > 0 { 447 | writeSize := bufSize 448 | if remaining < bufSize { 449 | writeSize = int(remaining) 450 | } 451 | 452 | n, err := f.Write(buf[:writeSize]) 453 | if err != nil { 454 | return err 455 | } 456 | 457 | remaining -= int64(n) 458 | } 459 | 460 | return nil 461 | } 462 | -------------------------------------------------------------------------------- /cmd/bandwidth-test/send.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func sendFile(serverAddr, id, filePath string, frameSize, cacheCount int, callback func(n int)) error { 4 | return nil 5 | } 6 | -------------------------------------------------------------------------------- /cmd/fft/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func main() { 8 | if err := rootCmd.Execute(); err != nil { 9 | os.Exit(1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cmd/fft/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatedier/fft/client" 8 | "github.com/fatedier/fft/version" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | showVersion bool 15 | options client.Options 16 | ) 17 | 18 | func init() { 19 | rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of fft client") 20 | rootCmd.PersistentFlags().StringVarP(&options.ServerAddr, "server-addr", "s", version.DefaultServerAddr(), "remote fft server address") 21 | rootCmd.PersistentFlags().StringVarP(&options.ID, "id", "i", "", "specify a special id to transfer file") 22 | rootCmd.PersistentFlags().StringVarP(&options.SendFile, "send-file", "l", "", "specify which file to send to another client") 23 | rootCmd.PersistentFlags().IntVarP(&options.FrameSize, "frame-size", "n", 5*1024, "each frame size, it's only for sender, default(5*1024 B)") 24 | rootCmd.PersistentFlags().IntVarP(&options.CacheCount, "cache-count", "c", 512, "how many frames be cached, it will be set to the min value between sender and receiver") 25 | rootCmd.PersistentFlags().StringVarP(&options.RecvFile, "recv-file", "t", "", "specify local file path to store received file") 26 | rootCmd.PersistentFlags().BoolVarP(&options.DebugMode, "debug", "g", false, "print more debug info") 27 | 28 | rootCmd.PersistentFlags().BoolVar(&options.EnableUnorderedProcessing, "enable-unordered-processing", false, "enable out-of-order frame processing to improve efficiency with unbalanced workers") 29 | rootCmd.PersistentFlags().BoolVar(&options.DynamicAllocation, "dynamic-allocation", false, "enable dynamic frame allocation based on worker performance") 30 | rootCmd.PersistentFlags().IntVar(&options.BufferSize, "buffer-size", 1000, "maximum buffer size for out-of-order frames (only used with --enable-unordered-processing)") 31 | } 32 | 33 | var rootCmd = &cobra.Command{ 34 | Use: "fft", 35 | Short: "fft is the client of fft (https://github.com/fatedier/fft)", 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | if showVersion { 38 | fmt.Println(version.Full()) 39 | return nil 40 | } 41 | 42 | svc, err := client.NewService(options) 43 | if err != nil { 44 | fmt.Printf("new fft client error: %v\n", err) 45 | os.Exit(1) 46 | } 47 | 48 | err = svc.Run() 49 | if err != nil { 50 | fmt.Printf("fft run error: %v\n", err) 51 | os.Exit(1) 52 | } 53 | return nil 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /cmd/ffts/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func main() { 8 | if err := rootCmd.Execute(); err != nil { 9 | os.Exit(1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cmd/ffts/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatedier/fft/server" 8 | "github.com/fatedier/fft/version" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | showVersion bool 15 | options server.Options 16 | ) 17 | 18 | func init() { 19 | rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of fft server") 20 | rootCmd.PersistentFlags().StringVarP(&options.BindAddr, "bind-addr", "b", "0.0.0.0:7777", "bind address") 21 | rootCmd.PersistentFlags().StringVarP(&options.LogFile, "log-file", "", "console", "log file path") 22 | rootCmd.PersistentFlags().StringVarP(&options.LogLevel, "log-level", "", "info", "log level") 23 | rootCmd.PersistentFlags().Int64VarP(&options.LogMaxDays, "log-max-days", "", 3, "log file reserved max days") 24 | } 25 | 26 | var rootCmd = &cobra.Command{ 27 | Use: "ffts", 28 | Short: "ffts is the server of fft (https://github.com/fatedier/fft)", 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | if showVersion { 31 | fmt.Println(version.Full()) 32 | return nil 33 | } 34 | 35 | svc, err := server.NewService(options) 36 | if err != nil { 37 | fmt.Printf("new fft server error: %v", err) 38 | os.Exit(1) 39 | } 40 | 41 | svc.Run() 42 | return nil 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /cmd/fftw/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func main() { 8 | if err := rootCmd.Execute(); err != nil { 9 | os.Exit(1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cmd/fftw/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatedier/fft/version" 8 | "github.com/fatedier/fft/worker" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | showVersion bool 15 | options worker.Options 16 | ) 17 | 18 | func init() { 19 | rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of fft worker") 20 | rootCmd.PersistentFlags().StringVarP(&options.ServerAddr, "server-addr", "s", version.DefaultServerAddr(), "remote fft server address") 21 | rootCmd.PersistentFlags().StringVarP(&options.BindAddr, "bind-addr", "b", "0.0.0.0:7778", "bind address") 22 | rootCmd.PersistentFlags().StringVarP(&options.AdvicePublicIP, "advice-public-ip", "p", "", "fft worker's advice public ip") 23 | rootCmd.PersistentFlags().IntVarP(&options.RateKB, "rate", "", 4096, "max bandwidth fftw will provide, unit is KB, default is 4096KB and min value is 50KB") 24 | rootCmd.PersistentFlags().IntVarP(&options.MaxTrafficMBPerDay, "max-traffic-per-day", "", 0, "max traffic fftw can use every day, 0 means no limit, unit is MB, default is 0MB and min value is 128MB") 25 | 26 | rootCmd.PersistentFlags().StringVarP(&options.LogFile, "log-file", "", "console", "log file path") 27 | rootCmd.PersistentFlags().StringVarP(&options.LogLevel, "log-level", "", "info", "log level") 28 | rootCmd.PersistentFlags().Int64VarP(&options.LogMaxDays, "log-max-days", "", 3, "log file reserved max days") 29 | } 30 | 31 | var rootCmd = &cobra.Command{ 32 | Use: "fftw", 33 | Short: "fftw is the worker of fft (https://github.com/fatedier/fft)", 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | if showVersion { 36 | fmt.Println(version.Full()) 37 | return nil 38 | } 39 | 40 | svc, err := worker.NewService(options) 41 | if err != nil { 42 | fmt.Printf("new fft worker error: %v\n", err) 43 | os.Exit(1) 44 | } 45 | 46 | err = svc.Run() 47 | if err != nil { 48 | fmt.Printf("fft worker runner exit: %v\n", err) 49 | os.Exit(1) 50 | } 51 | return nil 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fatedier/fft 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/cheggaaa/pb v1.0.29 7 | github.com/fatedier/golib v0.5.1 8 | github.com/spf13/cobra v1.9.1 9 | golang.org/x/time v0.11.0 10 | ) 11 | 12 | require ( 13 | github.com/golang/snappy v1.0.0 // indirect 14 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 15 | github.com/mattn/go-runewidth v0.0.16 // indirect 16 | github.com/rivo/uniseg v0.4.7 // indirect 17 | github.com/spf13/pflag v1.0.6 // indirect 18 | golang.org/x/crypto v0.38.0 // indirect 19 | golang.org/x/sys v0.33.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= 2 | github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M= 7 | github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= 8 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 9 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 10 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 11 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 12 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 13 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 14 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 15 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 16 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 17 | github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= 18 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 19 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 20 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 21 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 25 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 26 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 27 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 28 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 29 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 30 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 31 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 32 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 33 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 34 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 35 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 36 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 39 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 40 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 41 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 42 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /hack/run-e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SCRIPT=$(readlink -f "$0") 4 | ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd) 5 | 6 | if ! command -v ginkgo >/dev/null 2>&1; then 7 | echo "ginkgo not found, try to install..." 8 | go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 9 | fi 10 | 11 | debug=false 12 | if [ "x${DEBUG}" = "xtrue" ]; then 13 | debug=true 14 | fi 15 | logLevel=info 16 | if [ "${LOG_LEVEL}" ]; then 17 | logLevel="${LOG_LEVEL}" 18 | fi 19 | 20 | fftPath=${ROOT}/bin/fft 21 | if [ "${FFT_PATH}" ]; then 22 | fftPath="${FFT_PATH}" 23 | fi 24 | fftsPath=${ROOT}/bin/ffts 25 | if [ "${FFTS_PATH}" ]; then 26 | fftsPath="${FFTS_PATH}" 27 | fi 28 | fftwPath=${ROOT}/bin/fftw 29 | if [ "${FFTW_PATH}" ]; then 30 | fftwPath="${FFTW_PATH}" 31 | fi 32 | concurrency="4" 33 | if [ "${CONCURRENCY}" ]; then 34 | concurrency="${CONCURRENCY}" 35 | fi 36 | 37 | echo "Building fft binaries..." 38 | make -C ${ROOT} build 39 | 40 | echo "Running e2e tests..." 41 | ginkgo -nodes=${concurrency} --poll-progress-after=60s ${ROOT}/test/e2e -- -fft-path=${fftPath} -ffts-path=${fftsPath} -fftw-path=${fftwPath} -log-level=${logLevel} -debug=${debug} 42 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | # compile for version 2 | make 3 | if [ $? -ne 0 ]; then 4 | echo "make error" 5 | exit 1 6 | fi 7 | 8 | fft_version=`./bin/fft --version` 9 | echo "build version: $fft_version" 10 | 11 | # cross_compiles 12 | make -f ./Makefile.cross-compiles 13 | 14 | rm -rf ./packages 15 | mkdir ./packages 16 | 17 | os_all='linux windows darwin freebsd' 18 | arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle' 19 | 20 | for os in $os_all; do 21 | for arch in $arch_all; do 22 | fft_dir_name="fft_${fft_version}_${os}_${arch}" 23 | fft_path="./packages/fft_${fft_version}_${os}_${arch}" 24 | 25 | if [ "x${os}" = x"windows" ]; then 26 | if [ ! -f "./fft_${os}_${arch}.exe" ]; then 27 | continue 28 | fi 29 | if [ ! -f "./fftw_${os}_${arch}.exe" ]; then 30 | continue 31 | fi 32 | if [ ! -f "./ffts_${os}_${arch}.exe" ]; then 33 | continue 34 | fi 35 | mkdir ${fft_path} 36 | mv ./fft_${os}_${arch}.exe ${fft_path}/fft.exe 37 | mv ./fftw_${os}_${arch}.exe ${fft_path}/fftw.exe 38 | mv ./ffts_${os}_${arch}.exe ${fft_path}/ffts.exe 39 | else 40 | if [ ! -f "./fft_${os}_${arch}" ]; then 41 | continue 42 | fi 43 | if [ ! -f "./fftw_${os}_${arch}" ]; then 44 | continue 45 | fi 46 | if [ ! -f "./ffts_${os}_${arch}" ]; then 47 | continue 48 | fi 49 | mkdir ${fft_path} 50 | mv ./fft_${os}_${arch} ${fft_path}/fft 51 | mv ./fftw_${os}_${arch} ${fft_path}/fftw 52 | mv ./ffts_${os}_${arch} ${fft_path}/ffts 53 | fi 54 | cp ./LICENSE ${fft_path} 55 | 56 | # packages 57 | cd ./packages 58 | if [ "x${os}" = x"windows" ]; then 59 | zip -rq ${fft_dir_name}.zip ${fft_dir_name} 60 | else 61 | tar -zcf ${fft_dir_name}.tar.gz ${fft_dir_name} 62 | fi 63 | cd .. 64 | rm -rf ${fft_path} 65 | done 66 | done 67 | -------------------------------------------------------------------------------- /pkg/io/io.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type CallbackReader struct { 8 | r io.Reader 9 | callback func(n int) 10 | } 11 | 12 | func NewCallbackReader(r io.Reader, callback func(n int)) *CallbackReader { 13 | return &CallbackReader{ 14 | r: r, 15 | callback: callback, 16 | } 17 | } 18 | 19 | func (cr *CallbackReader) Read(p []byte) (n int, err error) { 20 | n, err = cr.r.Read(p) 21 | cr.callback(n) 22 | return 23 | } 24 | 25 | type CallbackWriter struct { 26 | w io.Writer 27 | callback func(n int) 28 | } 29 | 30 | func NewCallbackWriter(w io.Writer, callback func(n int)) *CallbackWriter { 31 | return &CallbackWriter{ 32 | w: w, 33 | callback: callback, 34 | } 35 | } 36 | 37 | func (cw *CallbackWriter) Write(p []byte) (n int, err error) { 38 | n, err = cw.w.Write(p) 39 | cw.callback(n) 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /pkg/io/limit.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "golang.org/x/time/rate" 8 | ) 9 | 10 | type RateReader struct { 11 | underlying io.Reader 12 | limiter *rate.Limiter 13 | } 14 | 15 | func NewRateReader(r io.Reader, limiter *rate.Limiter) *RateReader { 16 | return &RateReader{ 17 | underlying: r, 18 | limiter: limiter, 19 | } 20 | } 21 | 22 | func (rr *RateReader) Read(p []byte) (n int, err error) { 23 | burst := rr.limiter.Burst() 24 | if len(p) > burst { 25 | p = p[:burst] 26 | } 27 | 28 | n, err = rr.underlying.Read(p) 29 | if err != nil { 30 | return 31 | } 32 | 33 | err = rr.limiter.WaitN(context.Background(), n) 34 | if err != nil { 35 | return 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/fatedier/golib/log" 7 | ) 8 | 9 | var ( 10 | TraceLevel = log.TraceLevel 11 | DebugLevel = log.DebugLevel 12 | InfoLevel = log.InfoLevel 13 | WarnLevel = log.WarnLevel 14 | ErrorLevel = log.ErrorLevel 15 | ) 16 | 17 | var defaultLogger *log.Logger 18 | 19 | func init() { 20 | defaultLogger = log.New( 21 | log.WithCaller(true), 22 | log.AddCallerSkip(1), 23 | log.WithLevel(log.InfoLevel), 24 | ) 25 | } 26 | 27 | func InitLog(logWay string, logFile string, logLevel string, maxdays int64) { 28 | SetLogFile(logWay, logFile, maxdays) 29 | SetLogLevel(logLevel) 30 | } 31 | 32 | // logWay: file or console 33 | func SetLogFile(logWay string, logFile string, maxdays int64) { 34 | options := []log.Option{} 35 | if logWay == "console" { 36 | options = append(options, 37 | log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{ 38 | Colorful: true, 39 | }, os.Stdout)), 40 | ) 41 | } else { 42 | writer := log.NewRotateFileWriter(log.RotateFileConfig{ 43 | FileName: logFile, 44 | Mode: log.RotateFileModeDaily, 45 | MaxDays: int(maxdays), 46 | }) 47 | writer.Init() 48 | options = append(options, log.WithOutput(writer)) 49 | } 50 | defaultLogger = defaultLogger.WithOptions(options...) 51 | } 52 | 53 | // value: error, warning, info, debug, trace 54 | func SetLogLevel(logLevel string) { 55 | level, err := log.ParseLevel(logLevel) 56 | if err != nil { 57 | level = log.WarnLevel // default to warning 58 | } 59 | defaultLogger = defaultLogger.WithOptions(log.WithLevel(level)) 60 | } 61 | 62 | // wrap log 63 | 64 | func Error(format string, v ...interface{}) { 65 | defaultLogger.Errorf(format, v...) 66 | } 67 | 68 | func Warn(format string, v ...interface{}) { 69 | defaultLogger.Warnf(format, v...) 70 | } 71 | 72 | func Info(format string, v ...interface{}) { 73 | defaultLogger.Infof(format, v...) 74 | } 75 | 76 | func Debug(format string, v ...interface{}) { 77 | defaultLogger.Debugf(format, v...) 78 | } 79 | 80 | func Trace(format string, v ...interface{}) { 81 | defaultLogger.Tracef(format, v...) 82 | } 83 | 84 | // Logger 85 | type Logger interface { 86 | AddLogPrefix(string) 87 | GetPrefixStr() string 88 | GetAllPrefix() []string 89 | ClearLogPrefix() 90 | Error(string, ...interface{}) 91 | Warn(string, ...interface{}) 92 | Info(string, ...interface{}) 93 | Debug(string, ...interface{}) 94 | Trace(string, ...interface{}) 95 | } 96 | 97 | type PrefixLogger struct { 98 | prefix string 99 | allPrefix []string 100 | } 101 | 102 | func NewPrefixLogger(prefix string) *PrefixLogger { 103 | logger := &PrefixLogger{ 104 | allPrefix: make([]string, 0), 105 | } 106 | logger.AddLogPrefix(prefix) 107 | return logger 108 | } 109 | 110 | func (pl *PrefixLogger) AddLogPrefix(prefix string) { 111 | if len(prefix) == 0 { 112 | return 113 | } 114 | 115 | pl.prefix += "[" + prefix + "] " 116 | pl.allPrefix = append(pl.allPrefix, prefix) 117 | } 118 | 119 | func (pl *PrefixLogger) GetPrefixStr() string { 120 | return pl.prefix 121 | } 122 | 123 | func (pl *PrefixLogger) GetAllPrefix() []string { 124 | return pl.allPrefix 125 | } 126 | 127 | func (pl *PrefixLogger) ClearLogPrefix() { 128 | pl.prefix = "" 129 | pl.allPrefix = make([]string, 0) 130 | } 131 | 132 | func (pl *PrefixLogger) Error(format string, v ...interface{}) { 133 | defaultLogger.Errorf(pl.prefix+format, v...) 134 | } 135 | 136 | func (pl *PrefixLogger) Warn(format string, v ...interface{}) { 137 | defaultLogger.Warnf(pl.prefix+format, v...) 138 | } 139 | 140 | func (pl *PrefixLogger) Info(format string, v ...interface{}) { 141 | defaultLogger.Infof(pl.prefix+format, v...) 142 | } 143 | 144 | func (pl *PrefixLogger) Debug(format string, v ...interface{}) { 145 | defaultLogger.Debugf(pl.prefix+format, v...) 146 | } 147 | 148 | func (pl *PrefixLogger) Trace(format string, v ...interface{}) { 149 | defaultLogger.Tracef(pl.prefix+format, v...) 150 | } 151 | -------------------------------------------------------------------------------- /pkg/msg/ctl.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "io" 5 | 6 | jsonMsg "github.com/fatedier/golib/msg/json" 7 | ) 8 | 9 | type Message = jsonMsg.Message 10 | 11 | var ( 12 | msgCtl *jsonMsg.MsgCtl 13 | ) 14 | 15 | func init() { 16 | msgCtl = jsonMsg.NewMsgCtl() 17 | for typeByte, msg := range msgTypeMap { 18 | msgCtl.RegisterMsg(typeByte, msg) 19 | } 20 | } 21 | 22 | func ReadMsg(c io.Reader) (msg Message, err error) { 23 | return msgCtl.ReadMsg(c) 24 | } 25 | 26 | func ReadMsgInto(c io.Reader, msg Message) (err error) { 27 | return msgCtl.ReadMsgInto(c, msg) 28 | } 29 | 30 | func WriteMsg(c io.Writer, msg interface{}) (err error) { 31 | return msgCtl.WriteMsg(c, msg) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/msg/msg.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | const ( 4 | TypeRegisterWorker = 'a' 5 | TypeRegisterWorkerResp = 'b' 6 | TypeSendFile = 'c' 7 | TypeSendFileResp = 'd' 8 | TypeReceiveFile = 'e' 9 | TypeReceiveFileResp = 'f' 10 | TypeNewSendFileStream = 'g' 11 | TypeNewSendFileStreamResp = 'h' 12 | TypeNewReceiveFileStream = 'i' 13 | TypeNewReceiveFileStreamResp = 'j' 14 | 15 | TypePing = 'y' 16 | TypePong = 'z' 17 | ) 18 | 19 | var ( 20 | msgTypeMap = map[byte]interface{}{ 21 | TypeRegisterWorker: RegisterWorker{}, 22 | TypeRegisterWorkerResp: RegisterWorkerResp{}, 23 | TypeSendFile: SendFile{}, 24 | TypeSendFileResp: SendFileResp{}, 25 | TypeReceiveFile: ReceiveFile{}, 26 | TypeReceiveFileResp: ReceiveFileResp{}, 27 | TypeNewSendFileStream: NewSendFileStream{}, 28 | TypeNewSendFileStreamResp: NewSendFileStreamResp{}, 29 | TypeNewReceiveFileStream: NewReceiveFileStream{}, 30 | TypeNewReceiveFileStreamResp: NewReceiveFileStreamResp{}, 31 | 32 | TypePing: Ping{}, 33 | TypePong: Pong{}, 34 | } 35 | ) 36 | 37 | type RegisterWorker struct { 38 | Version string `json:"version"` 39 | BindPort int64 `json:"bind_port"` 40 | PublicIP string `json:"public_ip"` 41 | } 42 | 43 | type RegisterWorkerResp struct { 44 | Error string `json:"error"` 45 | } 46 | 47 | type SendFile struct { 48 | ID string `json:"id"` 49 | Fsize int64 `json:"fsize"` 50 | Name string `json:"name"` 51 | CacheCount int64 `json:"cache_count"` 52 | } 53 | 54 | type SendFileResp struct { 55 | ID string `json:"id"` 56 | Workers []string `json:"workers"` 57 | CacheCount int64 `json:"cache_count"` 58 | Error string `json:"error"` 59 | } 60 | 61 | type ReceiveFile struct { 62 | ID string `json:"id"` 63 | CacheCount int64 `json:"cache_count"` 64 | } 65 | 66 | type ReceiveFileResp struct { 67 | Name string `json:"name"` 68 | Fsize int64 `json:"fsize"` 69 | Workers []string `json:"workers"` 70 | CacheCount int64 `json:"cache_count"` 71 | Error string `json:"error"` 72 | } 73 | 74 | type NewSendFileStream struct { 75 | ID string `json:"id"` 76 | } 77 | 78 | type NewSendFileStreamResp struct { 79 | Error string `json:"error"` 80 | } 81 | 82 | type NewReceiveFileStream struct { 83 | ID string `json:"id"` 84 | } 85 | 86 | type NewReceiveFileStreamResp struct { 87 | Error string `json:"error"` 88 | } 89 | 90 | type Ping struct { 91 | } 92 | 93 | type Pong struct { 94 | } 95 | -------------------------------------------------------------------------------- /pkg/receiver/receiver.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sort" 7 | "sync" 8 | 9 | "github.com/fatedier/fft/pkg/stream" 10 | ) 11 | 12 | type Receiver struct { 13 | fileID uint32 14 | nextFrameID uint32 15 | dst io.Writer 16 | frames []*stream.Frame 17 | framesIDMap map[uint32]struct{} 18 | notifyCh chan struct{} 19 | 20 | unorderedEnabled bool 21 | orderedBuffer map[uint32]*stream.Frame // Buffer for out-of-order frames 22 | maxBufferSize int // Maximum number of frames to buffer 23 | 24 | mu sync.RWMutex 25 | } 26 | 27 | func NewReceiver(fileID uint32, dst io.Writer) *Receiver { 28 | return &Receiver{ 29 | fileID: fileID, 30 | nextFrameID: 0, 31 | dst: dst, 32 | frames: make([]*stream.Frame, 0), 33 | framesIDMap: make(map[uint32]struct{}), 34 | notifyCh: make(chan struct{}, 1), 35 | unorderedEnabled: false, 36 | orderedBuffer: make(map[uint32]*stream.Frame), 37 | maxBufferSize: 1000, // Default buffer size 38 | } 39 | } 40 | 41 | func (r *Receiver) EnableUnorderedProcessing(maxBufferSize int) { 42 | r.mu.Lock() 43 | defer r.mu.Unlock() 44 | 45 | r.unorderedEnabled = true 46 | if maxBufferSize > 0 { 47 | r.maxBufferSize = maxBufferSize 48 | } 49 | } 50 | 51 | func (r *Receiver) RecvFrame(frame *stream.Frame) { 52 | r.mu.Lock() 53 | 54 | if frame.FrameID < r.nextFrameID { 55 | r.mu.Unlock() 56 | return 57 | } 58 | 59 | if _, ok := r.framesIDMap[frame.FrameID]; ok { 60 | r.mu.Unlock() 61 | return 62 | } 63 | 64 | r.framesIDMap[frame.FrameID] = struct{}{} 65 | 66 | if r.unorderedEnabled { 67 | if frame.FrameID == r.nextFrameID { 68 | r.frames = append(r.frames, frame) 69 | } else { 70 | r.orderedBuffer[frame.FrameID] = frame 71 | } 72 | } else { 73 | r.frames = append(r.frames, frame) 74 | sort.Slice(r.frames, func(i, j int) bool { 75 | return r.frames[i].FrameID < r.frames[j].FrameID 76 | }) 77 | } 78 | r.mu.Unlock() 79 | 80 | select { 81 | case r.notifyCh <- struct{}{}: 82 | default: 83 | } 84 | } 85 | 86 | func (r *Receiver) Run() { 87 | for { 88 | _, ok := <-r.notifyCh 89 | if !ok { 90 | return 91 | } 92 | 93 | buffer := bytes.NewBuffer(nil) 94 | ii := 0 95 | finished := false 96 | r.mu.Lock() 97 | 98 | for i, frame := range r.frames { 99 | if r.nextFrameID == frame.FrameID { 100 | ii = i + 1 101 | delete(r.framesIDMap, frame.FrameID) 102 | // it's last frame 103 | if len(frame.Buf) == 0 { 104 | finished = true 105 | break 106 | } 107 | 108 | buffer.Write(frame.Buf) 109 | r.nextFrameID++ 110 | } else { 111 | ii = i 112 | break 113 | } 114 | } 115 | r.frames = r.frames[ii:] 116 | 117 | if r.unorderedEnabled { 118 | continueProcessing := true 119 | for continueProcessing { 120 | if frame, ok := r.orderedBuffer[r.nextFrameID]; ok { 121 | delete(r.orderedBuffer, r.nextFrameID) 122 | delete(r.framesIDMap, r.nextFrameID) 123 | 124 | // Check if it's the last frame 125 | if len(frame.Buf) == 0 { 126 | finished = true 127 | break 128 | } 129 | 130 | buffer.Write(frame.Buf) 131 | r.nextFrameID++ 132 | } else { 133 | continueProcessing = false 134 | } 135 | } 136 | } 137 | r.mu.Unlock() 138 | 139 | buf := buffer.Bytes() 140 | if len(buf) != 0 { 141 | r.dst.Write(buf) 142 | } 143 | 144 | if finished { 145 | break 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /pkg/sender/frame.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/fatedier/fft/pkg/stream" 8 | ) 9 | 10 | type SendFrame struct { 11 | frame *stream.Frame 12 | tr *Transfer 13 | sendTime time.Time 14 | retryTimes int 15 | hasAck bool 16 | transferID int // ID of the transfer this frame is assigned to 17 | 18 | mu sync.Mutex 19 | } 20 | 21 | func NewSendFrame(frame *stream.Frame) *SendFrame { 22 | return &SendFrame{ 23 | frame: frame, 24 | transferID: -1, // -1 means not assigned to any specific transfer 25 | } 26 | } 27 | 28 | func (sf *SendFrame) UpdateSendTime() { 29 | sf.mu.Lock() 30 | sf.sendTime = time.Now() 31 | sf.mu.Unlock() 32 | } 33 | 34 | func (sf *SendFrame) FrameID() uint32 { 35 | return sf.frame.FrameID 36 | } 37 | 38 | func (sf *SendFrame) Frame() *stream.Frame { 39 | return sf.frame 40 | } 41 | 42 | func (sf *SendFrame) HasAck() bool { 43 | return sf.hasAck 44 | } 45 | 46 | func (sf *SendFrame) SetAck() { 47 | sf.hasAck = true 48 | } 49 | 50 | func (sf *SendFrame) SetTransferID(id int) { 51 | sf.transferID = id 52 | } 53 | 54 | func (sf *SendFrame) GetTransferID() int { 55 | return sf.transferID 56 | } 57 | -------------------------------------------------------------------------------- /pkg/sender/sender.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/fatedier/fft/pkg/stream" 11 | 12 | "github.com/fatedier/golib/control/shutdown" 13 | ) 14 | 15 | type AckWaitingObj struct { 16 | Frame *stream.Frame 17 | HasAck bool 18 | LastSendTime time.Time 19 | } 20 | 21 | type Sender struct { 22 | id uint32 23 | 24 | // each frame size 25 | frameSize int 26 | 27 | // send src to remote Receiver 28 | src io.Reader 29 | 30 | frameCh chan *SendFrame 31 | 32 | // get each ack message from ackCh 33 | ackCh chan *stream.Ack 34 | 35 | dynamicAllocationEnabled bool 36 | transfers map[int]*Transfer 37 | totalThroughput float64 38 | allocationRatios map[int]float64 39 | transfersMu sync.RWMutex 40 | 41 | retryFrames []*SendFrame 42 | 43 | maxBufferCount int 44 | limiter chan struct{} 45 | waitAcks map[uint32]*SendFrame 46 | bufferFrames []*SendFrame 47 | 48 | // 1 means all frames has been sent 49 | sendAll bool 50 | mu sync.Mutex 51 | sendShutdown *shutdown.Shutdown 52 | ackShutdown *shutdown.Shutdown 53 | 54 | count uint32 55 | } 56 | 57 | func NewSender(id uint32, src io.Reader, frameSize int, maxBufferCount int) (*Sender, error) { 58 | if !stream.IsValidFrameSize(frameSize) { 59 | return nil, fmt.Errorf("invalid frameSize") 60 | } 61 | if maxBufferCount <= 0 { 62 | maxBufferCount = 100 63 | } 64 | 65 | s := &Sender{ 66 | id: id, 67 | frameSize: frameSize, 68 | src: src, 69 | frameCh: make(chan *SendFrame), 70 | ackCh: make(chan *stream.Ack), 71 | dynamicAllocationEnabled: false, 72 | transfers: make(map[int]*Transfer), 73 | totalThroughput: 0, 74 | allocationRatios: make(map[int]float64), 75 | maxBufferCount: maxBufferCount, 76 | retryFrames: make([]*SendFrame, 0), 77 | limiter: make(chan struct{}, maxBufferCount), 78 | waitAcks: make(map[uint32]*SendFrame), 79 | bufferFrames: make([]*SendFrame, 0), 80 | sendShutdown: shutdown.New(), 81 | ackShutdown: shutdown.New(), 82 | } 83 | for i := 0; i < maxBufferCount; i++ { 84 | s.limiter <- struct{}{} 85 | } 86 | return s, nil 87 | } 88 | 89 | func (sender *Sender) EnableDynamicAllocation() { 90 | sender.transfersMu.Lock() 91 | defer sender.transfersMu.Unlock() 92 | 93 | sender.dynamicAllocationEnabled = true 94 | } 95 | 96 | func (sender *Sender) HandleStream(s *stream.FrameStream) { 97 | sender.mu.Lock() 98 | if sender.sendAll { 99 | sender.mu.Unlock() 100 | s.Close() 101 | return 102 | } 103 | sender.mu.Unlock() 104 | 105 | id := atomic.AddUint32(&sender.count, 1) 106 | trBufferCount := sender.maxBufferCount / 2 107 | if trBufferCount <= 0 { 108 | trBufferCount = 1 109 | } 110 | tr := NewTransfer(int(id), trBufferCount, s, sender.frameCh, sender.ackCh) 111 | 112 | if sender.dynamicAllocationEnabled { 113 | sender.transfersMu.Lock() 114 | sender.transfers[int(id)] = tr 115 | totalTransfers := len(sender.transfers) 116 | if totalTransfers > 0 { 117 | equalRatio := 1.0 / float64(totalTransfers) 118 | for transferID := range sender.transfers { 119 | sender.allocationRatios[transferID] = equalRatio 120 | } 121 | } 122 | sender.transfersMu.Unlock() 123 | } 124 | 125 | // block until transfer exit 126 | noAckFrames := tr.Run() 127 | 128 | if sender.dynamicAllocationEnabled { 129 | sender.transfersMu.Lock() 130 | delete(sender.transfers, int(id)) 131 | delete(sender.allocationRatios, int(id)) 132 | sender.transfersMu.Unlock() 133 | } 134 | 135 | if len(noAckFrames) > 0 { 136 | sender.mu.Lock() 137 | sender.retryFrames = append(sender.retryFrames, noAckFrames...) 138 | sender.mu.Unlock() 139 | for i := 0; i < len(noAckFrames); i++ { 140 | sender.limiter <- struct{}{} 141 | } 142 | } 143 | } 144 | 145 | func (sender *Sender) Run() { 146 | go sender.ackHandler() 147 | go sender.loopSend() 148 | 149 | sender.sendShutdown.WaitDone() 150 | sender.ackShutdown.WaitDone() 151 | } 152 | 153 | func (sender *Sender) updateAllocationRatios() { 154 | sender.transfersMu.RLock() 155 | defer sender.transfersMu.RUnlock() 156 | 157 | if len(sender.transfers) <= 1 { 158 | for id := range sender.transfers { 159 | sender.allocationRatios[id] = 1.0 160 | } 161 | return 162 | } 163 | 164 | // Calculate total throughput across all transfers 165 | totalThroughput := 0.0 166 | for _, transfer := range sender.transfers { 167 | if transfer.currentThroughput > 0 { 168 | totalThroughput += transfer.currentThroughput 169 | } else { 170 | totalThroughput += 1.0 171 | } 172 | } 173 | 174 | if totalThroughput > 0 { 175 | for id, transfer := range sender.transfers { 176 | throughput := transfer.currentThroughput 177 | if throughput <= 0 { 178 | throughput = 1.0 // Default value if no data yet 179 | } 180 | sender.allocationRatios[id] = throughput / totalThroughput 181 | } 182 | } else { 183 | equalRatio := 1.0 / float64(len(sender.transfers)) 184 | for id := range sender.transfers { 185 | sender.allocationRatios[id] = equalRatio 186 | } 187 | } 188 | } 189 | 190 | func (sender *Sender) loopSend() { 191 | defer sender.sendShutdown.Done() 192 | 193 | var count uint32 194 | for { 195 | <-sender.limiter 196 | 197 | // retry first 198 | var retryFrame *SendFrame 199 | sender.mu.Lock() 200 | if len(sender.retryFrames) > 0 { 201 | retryFrame = sender.retryFrames[0] 202 | sender.retryFrames = sender.retryFrames[1:] 203 | } 204 | sender.mu.Unlock() 205 | 206 | if retryFrame != nil { 207 | sender.frameCh <- retryFrame 208 | continue 209 | } 210 | 211 | // don't need get frames from src 212 | if sender.sendAll { 213 | continue 214 | } 215 | 216 | // no retry frames, get a new frame from src 217 | buf := make([]byte, sender.frameSize) 218 | n, err := sender.src.Read(buf) 219 | if err == io.EOF { 220 | // send last frame and it's buffer is nil 221 | f := stream.NewFrame(sender.id, count, nil) 222 | sf := NewSendFrame(f) 223 | 224 | sender.mu.Lock() 225 | sender.sendAll = true 226 | sender.waitAcks[sf.FrameID()] = sf 227 | sender.bufferFrames = append(sender.bufferFrames, sf) 228 | sender.mu.Unlock() 229 | 230 | sender.frameCh <- sf 231 | return 232 | } 233 | if err != nil { 234 | close(sender.frameCh) 235 | return 236 | } 237 | buf = buf[:n] 238 | 239 | f := stream.NewFrame(0, count, buf) 240 | sf := NewSendFrame(f) 241 | sender.mu.Lock() 242 | sender.waitAcks[sf.FrameID()] = sf 243 | sender.bufferFrames = append(sender.bufferFrames, sf) 244 | 245 | if sender.dynamicAllocationEnabled { 246 | if count%10 == 0 { 247 | sender.updateAllocationRatios() 248 | } 249 | 250 | if len(sender.transfers) > 1 { 251 | var maxThroughput, minThroughput float64 252 | maxThroughput = 0 253 | minThroughput = float64(^uint(0) >> 1) // Max int value 254 | 255 | for _, transfer := range sender.transfers { 256 | if transfer.currentThroughput > maxThroughput { 257 | maxThroughput = transfer.currentThroughput 258 | } 259 | if transfer.currentThroughput > 0 && transfer.currentThroughput < minThroughput { 260 | minThroughput = transfer.currentThroughput 261 | } 262 | } 263 | 264 | if minThroughput == float64(^uint(0)>>1) || maxThroughput == 0 || 265 | maxThroughput/minThroughput < 1.5 { 266 | sf.SetTransferID(-1) 267 | } else { 268 | var fastestWorkerID int 269 | for id, transfer := range sender.transfers { 270 | if transfer.currentThroughput == maxThroughput { 271 | fastestWorkerID = id 272 | break 273 | } 274 | } 275 | sf.SetTransferID(fastestWorkerID) 276 | } 277 | } 278 | } 279 | sender.mu.Unlock() 280 | 281 | sender.frameCh <- sf 282 | count++ 283 | } 284 | } 285 | 286 | func (sender *Sender) ackHandler() { 287 | defer sender.ackShutdown.Done() 288 | 289 | for { 290 | ack, ok := <-sender.ackCh 291 | if !ok { 292 | return 293 | } 294 | 295 | finished := false 296 | sender.mu.Lock() 297 | waitSendFrame, ok := sender.waitAcks[ack.FrameID] 298 | if ok { 299 | waitSendFrame.SetAck() 300 | delete(sender.waitAcks, ack.FrameID) 301 | 302 | // if all frames has been sent and no waiting acks, we are success 303 | if sender.sendAll && len(sender.waitAcks) == 0 { 304 | finished = true 305 | } 306 | 307 | removeCount := 0 308 | // remove all continuous buffer frames with ack 309 | for _, sf := range sender.bufferFrames { 310 | if sf.HasAck() { 311 | removeCount++ 312 | sender.limiter <- struct{}{} 313 | } else { 314 | break 315 | } 316 | } 317 | sender.bufferFrames = sender.bufferFrames[removeCount:] 318 | } 319 | sender.mu.Unlock() 320 | 321 | if finished { 322 | close(sender.ackCh) 323 | close(sender.frameCh) 324 | close(sender.limiter) 325 | return 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /pkg/sender/transfer.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | "time" 7 | 8 | "github.com/fatedier/fft/pkg/stream" 9 | 10 | "github.com/fatedier/golib/control/limit" 11 | "github.com/fatedier/golib/control/shutdown" 12 | ) 13 | 14 | type Transfer struct { 15 | id int 16 | maxBufferCount int 17 | inSlowStart bool 18 | waitAcks map[uint32]*SendFrame 19 | 20 | framesSent uint64 21 | bytesTransferred uint64 22 | startTime time.Time 23 | lastMetricTime time.Time 24 | currentThroughput float64 // bytes per second 25 | 26 | s *stream.FrameStream 27 | limiter *limit.Limiter 28 | frameCh chan *SendFrame 29 | ackCh chan *stream.Ack 30 | mu sync.Mutex 31 | sendShutdown *shutdown.Shutdown 32 | recvShutdown *shutdown.Shutdown 33 | } 34 | 35 | func NewTransfer(id int, maxBufferCount int, s *stream.FrameStream, 36 | frameCh chan *SendFrame, ackCh chan *stream.Ack) *Transfer { 37 | 38 | if maxBufferCount <= 0 { 39 | maxBufferCount = 10 40 | } 41 | now := time.Now() 42 | t := &Transfer{ 43 | id: id, 44 | maxBufferCount: maxBufferCount, 45 | inSlowStart: true, 46 | waitAcks: make(map[uint32]*SendFrame), 47 | framesSent: 0, 48 | bytesTransferred: 0, 49 | startTime: now, 50 | lastMetricTime: now, 51 | currentThroughput: 0, 52 | s: s, 53 | limiter: limit.NewLimiter(int64(1)), 54 | frameCh: frameCh, 55 | ackCh: ackCh, 56 | sendShutdown: shutdown.New(), 57 | recvShutdown: shutdown.New(), 58 | } 59 | 60 | t.limiter.SetLimit(int64(1)) 61 | return t 62 | } 63 | 64 | // Block until all frames sended 65 | func (t *Transfer) Run() (noAckFrames []*SendFrame) { 66 | go t.ackReceiver() 67 | go t.frameSender() 68 | 69 | t.recvShutdown.WaitDone() 70 | t.sendShutdown.WaitDone() 71 | 72 | for _, f := range t.waitAcks { 73 | noAckFrames = append(noAckFrames, f) 74 | } 75 | if len(noAckFrames) > 0 { 76 | sort.Slice(noAckFrames, func(i, j int) bool { 77 | return noAckFrames[i].FrameID() < noAckFrames[j].FrameID() 78 | }) 79 | } 80 | 81 | t.limiter.Close() 82 | return 83 | } 84 | 85 | func (t *Transfer) frameSender() { 86 | defer t.sendShutdown.Done() 87 | 88 | for { 89 | // block by limiter 90 | n := int(t.limiter.LimitNum()) 91 | err := t.limiter.Acquire(time.Second) 92 | if err != nil { 93 | if err == limit.ErrTimeout { 94 | if n/2 == 0 { 95 | n = 1 96 | } 97 | t.limiter.SetLimit(int64(n)) 98 | continue 99 | } else { 100 | return 101 | } 102 | } 103 | 104 | if n < t.maxBufferCount { 105 | if t.inSlowStart { 106 | n = 2 * n 107 | } else { 108 | n++ 109 | } 110 | 111 | if n > t.maxBufferCount { 112 | t.inSlowStart = false 113 | n = t.maxBufferCount 114 | } 115 | t.limiter.SetLimit(int64(n)) 116 | } 117 | 118 | sf, ok := <-t.frameCh 119 | if !ok { 120 | t.s.Close() 121 | return 122 | } 123 | 124 | if sf.GetTransferID() != -1 && sf.GetTransferID() != t.id { 125 | select { 126 | case t.frameCh <- sf: 127 | default: 128 | sf.SetTransferID(t.id) 129 | } 130 | continue 131 | } 132 | 133 | t.mu.Lock() 134 | t.waitAcks[sf.FrameID()] = sf 135 | t.mu.Unlock() 136 | 137 | err = t.s.WriteFrame(sf.Frame()) 138 | if err != nil { 139 | return 140 | } 141 | } 142 | } 143 | 144 | func (t *Transfer) ackReceiver() { 145 | defer t.recvShutdown.Done() 146 | 147 | for { 148 | ack, err := t.s.ReadAck() 149 | if err != nil { 150 | t.limiter.Close() 151 | return 152 | } 153 | 154 | t.mu.Lock() 155 | sf, ok := t.waitAcks[ack.FrameID] 156 | if ok { 157 | t.framesSent++ 158 | if sf.Frame().Buf != nil { 159 | t.bytesTransferred += uint64(len(sf.Frame().Buf)) 160 | } 161 | 162 | // Calculate throughput every 10 frames or at least once per second 163 | now := time.Now() 164 | if t.framesSent%10 == 0 || now.Sub(t.lastMetricTime) > time.Second { 165 | elapsedSeconds := now.Sub(t.lastMetricTime).Seconds() 166 | if elapsedSeconds > 0 { 167 | // Calculate bytes per second 168 | t.currentThroughput = float64(t.bytesTransferred) / elapsedSeconds 169 | 170 | currentLimit := t.limiter.LimitNum() 171 | if t.currentThroughput > 0 { 172 | newLimit := currentLimit 173 | if t.inSlowStart { 174 | newLimit = currentLimit * 2 175 | } else { 176 | newLimit = currentLimit + (currentLimit / 10) 177 | } 178 | 179 | // Cap at maxBufferCount 180 | if newLimit > int64(t.maxBufferCount) { 181 | newLimit = int64(t.maxBufferCount) 182 | t.inSlowStart = false 183 | } 184 | 185 | t.limiter.SetLimit(newLimit) 186 | } 187 | 188 | t.lastMetricTime = now 189 | t.bytesTransferred = 0 190 | } 191 | } 192 | 193 | delete(t.waitAcks, ack.FrameID) 194 | } 195 | t.mu.Unlock() 196 | if ok { 197 | t.limiter.Release() 198 | } 199 | 200 | t.ackCh <- ack 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /pkg/stream/frame.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | func IsValidFrameSize(frameSize int) bool { 4 | if frameSize <= 0 || frameSize > 65535 { 5 | return false 6 | } 7 | return true 8 | } 9 | 10 | type Frame struct { 11 | Version uint8 12 | FileID uint32 13 | FrameID uint32 14 | Buf []byte // if len(Buf) == 0 , is last frame 15 | } 16 | 17 | func NewFrame(fileID uint32, frameID uint32, buf []byte) *Frame { 18 | return &Frame{ 19 | Version: 0, 20 | FileID: fileID, 21 | FrameID: frameID, 22 | Buf: buf, 23 | } 24 | } 25 | 26 | type Ack struct { 27 | Version uint8 28 | FileID uint32 29 | FrameID uint32 30 | } 31 | 32 | func NewAck(fileID uint32, frameID uint32) *Ack { 33 | return &Ack{ 34 | Version: 0, 35 | FileID: fileID, 36 | FrameID: frameID, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/stream/stream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "net" 9 | ) 10 | 11 | /* 12 | Sender -> Frame -> Receiver 13 | Sender <- Ack <- Receiver 14 | */ 15 | 16 | type FrameStream struct { 17 | conn net.Conn 18 | } 19 | 20 | func NewFrameStream(conn net.Conn) *FrameStream { 21 | return &FrameStream{ 22 | conn: conn, 23 | } 24 | } 25 | 26 | func (fs *FrameStream) WriteFrame(frame *Frame) error { 27 | buffer := bytes.NewBuffer(nil) 28 | binary.Write(buffer, binary.BigEndian, uint8(frame.Version)) 29 | binary.Write(buffer, binary.BigEndian, uint32(frame.FileID)) 30 | binary.Write(buffer, binary.BigEndian, uint32(frame.FrameID)) 31 | binary.Write(buffer, binary.BigEndian, uint16(len(frame.Buf))) 32 | _, err := fs.conn.Write(buffer.Bytes()) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | if len(frame.Buf) > 0 { 38 | _, err = io.Copy(fs.conn, bytes.NewBuffer(frame.Buf)) 39 | if err != nil { 40 | return err 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func (fs *FrameStream) ReadFrame() (*Frame, error) { 47 | f := &Frame{} 48 | err := binary.Read(fs.conn, binary.BigEndian, &f.Version) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | err = binary.Read(fs.conn, binary.BigEndian, &f.FileID) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | err = binary.Read(fs.conn, binary.BigEndian, &f.FrameID) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | var length uint16 64 | err = binary.Read(fs.conn, binary.BigEndian, &length) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | // last frame 70 | if length == 0 { 71 | return f, nil 72 | } 73 | 74 | f.Buf = make([]byte, length) 75 | n, err := io.ReadFull(fs.conn, f.Buf) 76 | if err != nil { 77 | return nil, err 78 | } 79 | f.Buf = f.Buf[:n] 80 | 81 | if uint16(n) != length { 82 | return nil, fmt.Errorf("error frame length") 83 | } 84 | return f, nil 85 | } 86 | 87 | func (fs *FrameStream) WriteAck(ack *Ack) error { 88 | buffer := bytes.NewBuffer(nil) 89 | binary.Write(buffer, binary.BigEndian, uint8(ack.Version)) 90 | binary.Write(buffer, binary.BigEndian, uint32(ack.FileID)) 91 | binary.Write(buffer, binary.BigEndian, uint32(ack.FrameID)) 92 | _, err := fs.conn.Write(buffer.Bytes()) 93 | if err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | 99 | func (fs *FrameStream) ReadAck() (*Ack, error) { 100 | ack := &Ack{} 101 | err := binary.Read(fs.conn, binary.BigEndian, &ack.Version) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | err = binary.Read(fs.conn, binary.BigEndian, &ack.FileID) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | err = binary.Read(fs.conn, binary.BigEndian, &ack.FrameID) 112 | if err != nil { 113 | return nil, err 114 | } 115 | return ack, nil 116 | } 117 | 118 | func (fs *FrameStream) Close() error { 119 | return fs.conn.Close() 120 | } 121 | -------------------------------------------------------------------------------- /server/client.go: -------------------------------------------------------------------------------- 1 | package server 2 | -------------------------------------------------------------------------------- /server/match.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type SendConn struct { 11 | id string 12 | conn net.Conn 13 | filename string 14 | fsize int64 15 | cacheCount int64 16 | 17 | recvConnCh chan *RecvConn 18 | } 19 | 20 | func NewSendConn(id string, conn net.Conn, filename string, fsize int64, cacheCount int64) *SendConn { 21 | return &SendConn{ 22 | id: id, 23 | conn: conn, 24 | filename: filename, 25 | fsize: fsize, 26 | cacheCount: cacheCount, 27 | recvConnCh: make(chan *RecvConn), 28 | } 29 | } 30 | 31 | type RecvConn struct { 32 | id string 33 | conn net.Conn 34 | cacheCount int64 35 | } 36 | 37 | func NewRecvConn(id string, conn net.Conn, cacheCount int64) *RecvConn { 38 | return &RecvConn{ 39 | id: id, 40 | conn: conn, 41 | cacheCount: cacheCount, 42 | } 43 | } 44 | 45 | type MatchController struct { 46 | senders map[string]*SendConn 47 | 48 | mu sync.Mutex 49 | } 50 | 51 | func NewMatchController() *MatchController { 52 | return &MatchController{ 53 | senders: make(map[string]*SendConn), 54 | } 55 | } 56 | 57 | // block until there is a same ID recv conn or timeout 58 | func (mc *MatchController) DealSendConn(sc *SendConn, timeout time.Duration) (cacheCount int64, err error) { 59 | mc.mu.Lock() 60 | if _, ok := mc.senders[sc.id]; ok { 61 | mc.mu.Unlock() 62 | err = fmt.Errorf("id is repeated") 63 | return 64 | } 65 | mc.senders[sc.id] = sc 66 | mc.mu.Unlock() 67 | 68 | select { 69 | case rc := <-sc.recvConnCh: 70 | cacheCount = rc.cacheCount 71 | case <-time.After(timeout): 72 | mc.mu.Lock() 73 | if tmp, ok := mc.senders[sc.id]; ok && tmp == sc { 74 | delete(mc.senders, sc.id) 75 | } 76 | mc.mu.Unlock() 77 | err = fmt.Errorf("timeout waiting recv conn") 78 | return 79 | } 80 | return 81 | } 82 | 83 | func (mc *MatchController) DealRecvConn(rc *RecvConn) (filename string, fsize int64, cacheCount int64, err error) { 84 | mc.mu.Lock() 85 | sc, ok := mc.senders[rc.id] 86 | if ok { 87 | delete(mc.senders, rc.id) 88 | } 89 | mc.mu.Unlock() 90 | 91 | if !ok { 92 | err = fmt.Errorf("no target sender") 93 | return 94 | } 95 | filename = sc.filename 96 | fsize = sc.fsize 97 | cacheCount = sc.cacheCount 98 | 99 | select { 100 | case sc.recvConnCh <- rc: 101 | default: 102 | err = fmt.Errorf("no target sender") 103 | return 104 | } 105 | return 106 | } 107 | -------------------------------------------------------------------------------- /server/service.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "fmt" 10 | "math/big" 11 | "net" 12 | "time" 13 | 14 | "github.com/fatedier/fft/pkg/log" 15 | "github.com/fatedier/fft/pkg/msg" 16 | ) 17 | 18 | type Options struct { 19 | BindAddr string 20 | 21 | LogFile string 22 | LogLevel string 23 | LogMaxDays int64 24 | } 25 | 26 | func (op *Options) Check() error { 27 | if op.LogMaxDays <= 0 { 28 | op.LogMaxDays = 3 29 | } 30 | return nil 31 | } 32 | 33 | type Service struct { 34 | l net.Listener 35 | workerGroup *WorkerGroup 36 | matchController *MatchController 37 | 38 | tlsConfig *tls.Config 39 | } 40 | 41 | func NewService(options Options) (*Service, error) { 42 | if err := options.Check(); err != nil { 43 | return nil, err 44 | } 45 | 46 | logway := "file" 47 | if options.LogFile == "console" { 48 | logway = "console" 49 | } 50 | log.InitLog(logway, options.LogFile, options.LogLevel, options.LogMaxDays) 51 | 52 | l, err := net.Listen("tcp", options.BindAddr) 53 | if err != nil { 54 | return nil, err 55 | } 56 | log.Info("ffts listen on: %s", l.Addr().String()) 57 | 58 | return &Service{ 59 | l: l, 60 | workerGroup: NewWorkerGroup(), 61 | matchController: NewMatchController(), 62 | tlsConfig: generateTLSConfig(), 63 | }, nil 64 | } 65 | 66 | func (svc *Service) Run() error { 67 | // Debug ======== 68 | go func() { 69 | for { 70 | time.Sleep(10 * time.Second) 71 | log.Info("worker addrs: %v", svc.workerGroup.GetAvailableWorkerAddrs()) 72 | } 73 | }() 74 | // Debug ======== 75 | 76 | for { 77 | conn, err := svc.l.Accept() 78 | if err != nil { 79 | return err 80 | } 81 | conn = tls.Server(conn, svc.tlsConfig) 82 | 83 | go svc.handleConn(conn) 84 | } 85 | } 86 | 87 | func (svc *Service) handleConn(conn net.Conn) { 88 | var ( 89 | rawMsg msg.Message 90 | err error 91 | ) 92 | 93 | conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 94 | if rawMsg, err = msg.ReadMsg(conn); err != nil { 95 | conn.Close() 96 | return 97 | } 98 | conn.SetReadDeadline(time.Time{}) 99 | 100 | switch m := rawMsg.(type) { 101 | case *msg.RegisterWorker: 102 | err = svc.handleRegisterWorker(conn, m) 103 | if err != nil { 104 | msg.WriteMsg(conn, &msg.RegisterWorkerResp{ 105 | Error: err.Error(), 106 | }) 107 | conn.Close() 108 | } 109 | case *msg.SendFile: 110 | if err = svc.handleSendFile(conn, m); err != nil { 111 | msg.WriteMsg(conn, &msg.SendFileResp{ 112 | Error: err.Error(), 113 | }) 114 | conn.Close() 115 | } 116 | case *msg.ReceiveFile: 117 | if err = svc.handleRecvFile(conn, m); err != nil { 118 | msg.WriteMsg(conn, &msg.ReceiveFileResp{ 119 | Error: err.Error(), 120 | }) 121 | conn.Close() 122 | } 123 | default: 124 | conn.Close() 125 | return 126 | } 127 | } 128 | 129 | func (svc *Service) handleRegisterWorker(conn net.Conn, m *msg.RegisterWorker) error { 130 | log.Debug("get register worker: remote addr [%s] port [%d], advice public IP [%s]", 131 | conn.RemoteAddr().String(), m.BindPort, m.PublicIP) 132 | w := NewWorker(m.BindPort, m.PublicIP, conn) 133 | err := w.DetectPublicAddr() 134 | if err != nil { 135 | log.Warn("detect [%s] public address error: %v", conn.RemoteAddr().String(), err) 136 | return err 137 | } else { 138 | msg.WriteMsg(conn, &msg.RegisterWorkerResp{Error: ""}) 139 | } 140 | 141 | svc.workerGroup.RegisterWorker(w) 142 | log.Info("[%s] new worker register", w.PublicAddr()) 143 | return nil 144 | } 145 | 146 | func (svc *Service) handleSendFile(conn net.Conn, m *msg.SendFile) error { 147 | if m.ID == "" || m.Name == "" { 148 | return fmt.Errorf("id and file name is required") 149 | } 150 | log.Debug("new SendFile id [%s], filename [%s] size [%d]", m.ID, m.Name, m.Fsize) 151 | 152 | sc := NewSendConn(m.ID, conn, m.Name, m.Fsize, m.CacheCount) 153 | cacheCount, err := svc.matchController.DealSendConn(sc, 120*time.Second) 154 | if err != nil { 155 | log.Warn("deal send conn error: %v", err) 156 | return err 157 | } 158 | 159 | msg.WriteMsg(conn, &msg.SendFileResp{ 160 | ID: m.ID, 161 | Workers: svc.workerGroup.GetAvailableWorkerAddrs(), 162 | CacheCount: cacheCount, 163 | }) 164 | return nil 165 | } 166 | 167 | func (svc *Service) handleRecvFile(conn net.Conn, m *msg.ReceiveFile) error { 168 | if m.ID == "" { 169 | return fmt.Errorf("id is required") 170 | } 171 | log.Debug("new ReceiveFile id [%s]", m.ID) 172 | 173 | rc := NewRecvConn(m.ID, conn, m.CacheCount) 174 | filename, fsize, cacheCount, err := svc.matchController.DealRecvConn(rc) 175 | if err != nil { 176 | log.Warn("deal recv conn error: %v", err) 177 | return err 178 | } 179 | 180 | msg.WriteMsg(conn, &msg.ReceiveFileResp{ 181 | Name: filename, 182 | Fsize: fsize, 183 | Workers: svc.workerGroup.GetAvailableWorkerAddrs(), 184 | CacheCount: cacheCount, 185 | }) 186 | return nil 187 | } 188 | 189 | // Setup a bare-bones TLS config for the server 190 | func generateTLSConfig() *tls.Config { 191 | key, err := rsa.GenerateKey(rand.Reader, 1024) 192 | if err != nil { 193 | panic(err) 194 | } 195 | template := x509.Certificate{SerialNumber: big.NewInt(1)} 196 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 197 | if err != nil { 198 | panic(err) 199 | } 200 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 201 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 202 | 203 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 204 | if err != nil { 205 | panic(err) 206 | } 207 | return &tls.Config{Certificates: []tls.Certificate{tlsCert}} 208 | } 209 | -------------------------------------------------------------------------------- /server/worker.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "sync" 9 | "time" 10 | 11 | "github.com/fatedier/fft/pkg/log" 12 | "github.com/fatedier/fft/pkg/msg" 13 | ) 14 | 15 | var ( 16 | ErrPublicAddr = errors.New("no public address") 17 | ) 18 | 19 | type Worker struct { 20 | conn net.Conn 21 | port int64 22 | advicePublicIP string 23 | publicAddr string 24 | } 25 | 26 | func NewWorker(port int64, advicePublicIP string, conn net.Conn) *Worker { 27 | return &Worker{ 28 | port: port, 29 | advicePublicIP: advicePublicIP, 30 | conn: conn, 31 | } 32 | } 33 | 34 | func (w *Worker) PublicAddr() string { 35 | return w.publicAddr 36 | } 37 | 38 | func (w *Worker) DetectPublicAddr() error { 39 | host, _, err := net.SplitHostPort(w.conn.RemoteAddr().String()) 40 | if err != nil { 41 | return fmt.Errorf("parse worker address error: %v", err) 42 | } 43 | 44 | ip := w.advicePublicIP 45 | if ip == "" { 46 | ip = host 47 | } 48 | detectAddr := net.JoinHostPort(ip, fmt.Sprintf("%d", w.port)) 49 | log.Debug("worker detect address: %s", detectAddr) 50 | 51 | detectConn, err := net.Dial("tcp", detectAddr) 52 | if err != nil { 53 | log.Warn("dial worker public address error: %v", err) 54 | return ErrPublicAddr 55 | } 56 | detectConn = tls.Client(detectConn, &tls.Config{InsecureSkipVerify: true}) 57 | defer detectConn.Close() 58 | 59 | msg.WriteMsg(detectConn, &msg.Ping{}) 60 | 61 | detectConn.SetReadDeadline(time.Now().Add(5 * time.Second)) 62 | m, err := msg.ReadMsg(detectConn) 63 | if err != nil { 64 | log.Warn("read pong from detectConn error: %v", err) 65 | return ErrPublicAddr 66 | } 67 | if _, ok := m.(*msg.Pong); !ok { 68 | return ErrPublicAddr 69 | } 70 | 71 | w.publicAddr = detectAddr 72 | return nil 73 | } 74 | 75 | func (w *Worker) RunKeepAlive(closeCallback func()) { 76 | defer func() { 77 | if closeCallback != nil { 78 | closeCallback() 79 | } 80 | }() 81 | 82 | for { 83 | w.conn.SetReadDeadline(time.Now().Add(30 * time.Second)) 84 | m, err := msg.ReadMsg(w.conn) 85 | if err != nil { 86 | w.conn.Close() 87 | return 88 | } 89 | 90 | if _, ok := m.(*msg.Ping); !ok { 91 | w.conn.Close() 92 | return 93 | } 94 | msg.WriteMsg(w.conn, &msg.Pong{}) 95 | } 96 | } 97 | 98 | type WorkerGroup struct { 99 | workers map[string]*Worker 100 | 101 | mu sync.RWMutex 102 | } 103 | 104 | func NewWorkerGroup() *WorkerGroup { 105 | return &WorkerGroup{ 106 | workers: make(map[string]*Worker), 107 | } 108 | } 109 | 110 | func (wg *WorkerGroup) RegisterWorker(w *Worker) { 111 | closeCallback := func() { 112 | wg.mu.Lock() 113 | delete(wg.workers, w.PublicAddr()) 114 | wg.mu.Unlock() 115 | } 116 | 117 | wg.mu.Lock() 118 | wg.workers[w.PublicAddr()] = w 119 | go w.RunKeepAlive(closeCallback) 120 | wg.mu.Unlock() 121 | } 122 | 123 | func (wg *WorkerGroup) GetAvailableWorkerAddrs() []string { 124 | addrs := make([]string, 0) 125 | 126 | wg.mu.RLock() 127 | defer wg.mu.RUnlock() 128 | for addr, _ := range wg.workers { 129 | addrs = append(addrs, addr) 130 | } 131 | return addrs 132 | } 133 | -------------------------------------------------------------------------------- /test/e2e/basic/basic.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | 11 | "github.com/fatedier/fft/test/e2e/framework" 12 | ) 13 | 14 | var _ = ginkgo.Describe("[Feature: Basic]", func() { 15 | f := framework.NewDefaultFramework() 16 | 17 | ginkgo.Describe("File Transfer", func() { 18 | ginkgo.It("Should transfer a small file successfully", func() { 19 | testContent := make([]byte, 10*1024) 20 | for i := range testContent { 21 | testContent[i] = byte(i % 256) 22 | } 23 | 24 | testFilePath, err := f.CreateTestFile(testContent) 25 | framework.ExpectNoError(err) 26 | 27 | serverPort := f.AllocPort() 28 | serverAddr := fmt.Sprintf("127.0.0.1:%d", serverPort) 29 | 30 | _, serverOutput, err := f.RunServer("--bind-addr", serverAddr) 31 | framework.ExpectNoError(err) 32 | ginkgo.GinkgoWriter.Printf("Server started on %s\n", serverAddr) 33 | ginkgo.GinkgoWriter.Printf("Server output: %s\n", serverOutput) 34 | 35 | workerPort := f.AllocPort() 36 | workerAddr := fmt.Sprintf("127.0.0.1:%d", workerPort) 37 | _, workerOutput, err := f.RunWorker("--server-addr", serverAddr, "--bind-addr", workerAddr) 38 | framework.ExpectNoError(err) 39 | ginkgo.GinkgoWriter.Printf("Worker connected to server\n") 40 | ginkgo.GinkgoWriter.Printf("Worker output: %s\n", workerOutput) 41 | 42 | transferID := fmt.Sprintf("test-%d", time.Now().UnixNano()) 43 | 44 | recvDir := filepath.Join(f.TempDirectory, "recv") 45 | err = os.MkdirAll(recvDir, 0755) 46 | framework.ExpectNoError(err) 47 | 48 | ginkgo.GinkgoWriter.Printf("Starting sender with ID: %s\n", transferID) 49 | go func() { 50 | _, senderOutput, err := f.RunClient( 51 | "-s", serverAddr, 52 | "-i", transferID, 53 | "-l", testFilePath, 54 | "-g", "true", // debug mode 55 | ) 56 | if err != nil { 57 | ginkgo.GinkgoWriter.Printf("Sender error: %v\n", err) 58 | } 59 | ginkgo.GinkgoWriter.Printf("Sender output: %s\n", senderOutput) 60 | }() 61 | 62 | time.Sleep(2 * time.Second) 63 | 64 | ginkgo.GinkgoWriter.Printf("Starting receiver with ID: %s\n", transferID) 65 | _, receiverOutput, err := f.RunClient( 66 | "-s", serverAddr, 67 | "-i", transferID, 68 | "-t", recvDir, 69 | "-g", "true", // debug mode 70 | ) 71 | framework.ExpectNoError(err) 72 | ginkgo.GinkgoWriter.Printf("Receiver output: %s\n", receiverOutput) 73 | 74 | time.Sleep(5 * time.Second) 75 | 76 | receivedFilePath := filepath.Join(recvDir, filepath.Base(testFilePath)) 77 | err = f.VerifyFileContent(receivedFilePath, testContent) 78 | framework.ExpectNoError(err, "File content verification failed") 79 | }) 80 | 81 | ginkgo.It("Should transfer a medium file successfully", func() { 82 | testContent := make([]byte, 1024*1024) 83 | for i := range testContent { 84 | testContent[i] = byte(i % 256) 85 | } 86 | 87 | testFilePath, err := f.CreateTestFile(testContent) 88 | framework.ExpectNoError(err) 89 | 90 | serverPort := f.AllocPort() 91 | serverAddr := fmt.Sprintf("127.0.0.1:%d", serverPort) 92 | 93 | _, serverOutput, err := f.RunServer("--bind-addr", serverAddr) 94 | framework.ExpectNoError(err) 95 | ginkgo.GinkgoWriter.Printf("Server started on %s\n", serverAddr) 96 | ginkgo.GinkgoWriter.Printf("Server output: %s\n", serverOutput) 97 | 98 | workerPort := f.AllocPort() 99 | workerAddr := fmt.Sprintf("127.0.0.1:%d", workerPort) 100 | _, workerOutput, err := f.RunWorker("--server-addr", serverAddr, "--bind-addr", workerAddr) 101 | framework.ExpectNoError(err) 102 | ginkgo.GinkgoWriter.Printf("Worker connected to server\n") 103 | ginkgo.GinkgoWriter.Printf("Worker output: %s\n", workerOutput) 104 | 105 | transferID := fmt.Sprintf("test-%d", time.Now().UnixNano()) 106 | 107 | recvDir := filepath.Join(f.TempDirectory, "recv-medium") 108 | err = os.MkdirAll(recvDir, 0755) 109 | framework.ExpectNoError(err) 110 | 111 | ginkgo.GinkgoWriter.Printf("Starting sender with ID: %s\n", transferID) 112 | go func() { 113 | _, senderOutput, err := f.RunClient( 114 | "-s", serverAddr, 115 | "-i", transferID, 116 | "-l", testFilePath, 117 | "-g", "true", // debug mode 118 | ) 119 | if err != nil { 120 | ginkgo.GinkgoWriter.Printf("Sender error: %v\n", err) 121 | } 122 | ginkgo.GinkgoWriter.Printf("Sender output: %s\n", senderOutput) 123 | }() 124 | 125 | time.Sleep(2 * time.Second) 126 | 127 | ginkgo.GinkgoWriter.Printf("Starting receiver with ID: %s\n", transferID) 128 | _, receiverOutput, err := f.RunClient( 129 | "-s", serverAddr, 130 | "-i", transferID, 131 | "-t", recvDir, 132 | "-g", "true", // debug mode 133 | ) 134 | framework.ExpectNoError(err) 135 | ginkgo.GinkgoWriter.Printf("Receiver output: %s\n", receiverOutput) 136 | 137 | time.Sleep(10 * time.Second) 138 | 139 | receivedFilePath := filepath.Join(recvDir, filepath.Base(testFilePath)) 140 | err = f.VerifyFileContent(receivedFilePath, testContent) 141 | framework.ExpectNoError(err, "File content verification failed") 142 | }) 143 | 144 | ginkgo.It("Should transfer a file with custom frame size and cache count", func() { 145 | testContent := make([]byte, 100*1024) 146 | for i := range testContent { 147 | testContent[i] = byte(i % 256) 148 | } 149 | 150 | testFilePath, err := f.CreateTestFile(testContent) 151 | framework.ExpectNoError(err) 152 | 153 | serverPort := f.AllocPort() 154 | serverAddr := fmt.Sprintf("127.0.0.1:%d", serverPort) 155 | 156 | _, serverOutput, err := f.RunServer("--bind-addr", serverAddr) 157 | framework.ExpectNoError(err) 158 | ginkgo.GinkgoWriter.Printf("Server started on %s\n", serverAddr) 159 | ginkgo.GinkgoWriter.Printf("Server output: %s\n", serverOutput) 160 | 161 | workerPort := f.AllocPort() 162 | workerAddr := fmt.Sprintf("127.0.0.1:%d", workerPort) 163 | _, workerOutput, err := f.RunWorker("--server-addr", serverAddr, "--bind-addr", workerAddr) 164 | framework.ExpectNoError(err) 165 | ginkgo.GinkgoWriter.Printf("Worker connected to server\n") 166 | ginkgo.GinkgoWriter.Printf("Worker output: %s\n", workerOutput) 167 | 168 | transferID := fmt.Sprintf("test-%d", time.Now().UnixNano()) 169 | 170 | recvDir := filepath.Join(f.TempDirectory, "recv-custom") 171 | err = os.MkdirAll(recvDir, 0755) 172 | framework.ExpectNoError(err) 173 | 174 | ginkgo.GinkgoWriter.Printf("Starting sender with ID: %s\n", transferID) 175 | go func() { 176 | _, senderOutput, err := f.RunClient( 177 | "-s", serverAddr, 178 | "-i", transferID, 179 | "-l", testFilePath, 180 | "-n", "10240", // 10KB frame size 181 | "-c", "256", // 256 frames cache 182 | "-g", "true", // debug mode 183 | ) 184 | if err != nil { 185 | ginkgo.GinkgoWriter.Printf("Sender error: %v\n", err) 186 | } 187 | ginkgo.GinkgoWriter.Printf("Sender output: %s\n", senderOutput) 188 | }() 189 | 190 | time.Sleep(2 * time.Second) 191 | 192 | ginkgo.GinkgoWriter.Printf("Starting receiver with ID: %s\n", transferID) 193 | _, receiverOutput, err := f.RunClient( 194 | "-s", serverAddr, 195 | "-i", transferID, 196 | "-t", recvDir, 197 | "-c", "256", // 256 frames cache 198 | "-g", "true", // debug mode 199 | ) 200 | framework.ExpectNoError(err) 201 | ginkgo.GinkgoWriter.Printf("Receiver output: %s\n", receiverOutput) 202 | 203 | time.Sleep(5 * time.Second) 204 | 205 | receivedFilePath := filepath.Join(recvDir, filepath.Base(testFilePath)) 206 | err = f.VerifyFileContent(receivedFilePath, testContent) 207 | framework.ExpectNoError(err, "File content verification failed") 208 | }) 209 | }) 210 | }) 211 | -------------------------------------------------------------------------------- /test/e2e/e2e.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/v2" 7 | "github.com/onsi/gomega" 8 | 9 | "github.com/fatedier/fft/test/e2e/framework" 10 | ) 11 | 12 | var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { 13 | setupSuite() 14 | return nil 15 | }, func(data []byte) { 16 | setupSuitePerGinkgoNode() 17 | }) 18 | 19 | var _ = ginkgo.SynchronizedAfterSuite(func() { 20 | CleanupSuite() 21 | }, func() { 22 | AfterSuiteActions() 23 | }) 24 | 25 | func RunE2ETests(t *testing.T) { 26 | gomega.RegisterFailHandler(framework.Fail) 27 | 28 | suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration() 29 | suiteConfig.EmitSpecProgress = true 30 | suiteConfig.RandomizeAllSpecs = true 31 | 32 | ginkgo.RunSpecs(t, "fft e2e suite", suiteConfig, reporterConfig) 33 | } 34 | 35 | func setupSuite() { 36 | } 37 | 38 | func setupSuitePerGinkgoNode() { 39 | } 40 | 41 | func CleanupSuite() { 42 | framework.RunCleanupActions() 43 | } 44 | 45 | func AfterSuiteActions() { 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | _ "github.com/onsi/ginkgo/v2" 10 | 11 | _ "github.com/fatedier/fft/test/e2e/basic" 12 | "github.com/fatedier/fft/test/e2e/framework" 13 | ) 14 | 15 | func handleFlags() { 16 | framework.RegisterCommonFlags(flag.CommandLine) 17 | flag.Parse() 18 | } 19 | 20 | func TestMain(m *testing.M) { 21 | handleFlags() 22 | 23 | if err := framework.ValidateTestContext(&framework.TestContext); err != nil { 24 | fmt.Println(err) 25 | os.Exit(1) 26 | } 27 | 28 | os.Exit(m.Run()) 29 | } 30 | 31 | func TestE2E(t *testing.T) { 32 | RunE2ETests(t) 33 | } 34 | -------------------------------------------------------------------------------- /test/e2e/framework/flags.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func RegisterCommonFlags(flags *flag.FlagSet) { 11 | flags.StringVar(&TestContext.FFTPath, "fft-path", "", "Path to fft binary") 12 | flags.StringVar(&TestContext.FFTSPath, "ffts-path", "", "Path to ffts binary") 13 | flags.StringVar(&TestContext.FFTWPath, "fftw-path", "", "Path to fftw binary") 14 | flags.StringVar(&TestContext.LogLevel, "log-level", "info", "Log level") 15 | flags.BoolVar(&TestContext.Debug, "debug", false, "Debug mode") 16 | } 17 | 18 | func ValidateTestContext(testContext *struct { 19 | FFTPath string 20 | FFTSPath string 21 | FFTWPath string 22 | LogLevel string 23 | Debug bool 24 | }) error { 25 | if testContext.FFTPath == "" { 26 | testContext.FFTPath = filepath.Join(os.Getenv("HOME"), "repos", "fft", "bin", "fft") 27 | } 28 | if testContext.FFTSPath == "" { 29 | testContext.FFTSPath = filepath.Join(os.Getenv("HOME"), "repos", "fft", "bin", "ffts") 30 | } 31 | if testContext.FFTWPath == "" { 32 | testContext.FFTWPath = filepath.Join(os.Getenv("HOME"), "repos", "fft", "bin", "fftw") 33 | } 34 | 35 | if _, err := os.Stat(testContext.FFTPath); os.IsNotExist(err) { 36 | return fmt.Errorf("fft binary not found at %s", testContext.FFTPath) 37 | } 38 | if _, err := os.Stat(testContext.FFTSPath); os.IsNotExist(err) { 39 | return fmt.Errorf("ffts binary not found at %s", testContext.FFTSPath) 40 | } 41 | if _, err := os.Stat(testContext.FFTWPath); os.IsNotExist(err) { 42 | return fmt.Errorf("fftw binary not found at %s", testContext.FFTWPath) 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/framework/framework.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | ) 11 | 12 | var ( 13 | TestContext struct { 14 | FFTPath string 15 | FFTSPath string 16 | FFTWPath string 17 | LogLevel string 18 | Debug bool 19 | } 20 | 21 | RunID = fmt.Sprintf("%d", ginkgo.GinkgoRandomSeed()) 22 | ) 23 | 24 | type Framework struct { 25 | TempDirectory string 26 | 27 | usedPorts map[string]int 28 | 29 | allocatedPorts []int 30 | 31 | cleanupHandle CleanupActionHandle 32 | 33 | beforeEachStarted bool 34 | 35 | serverConfPath string 36 | serverProcess *Process 37 | 38 | workerConfPaths []string 39 | workerProcesses []*Process 40 | 41 | clientConfPaths []string 42 | clientProcesses []*Process 43 | 44 | configFileIndex int64 45 | 46 | osEnvs []string 47 | 48 | mutex sync.Mutex 49 | } 50 | 51 | func NewDefaultFramework() *Framework { 52 | f := &Framework{ 53 | usedPorts: make(map[string]int), 54 | } 55 | 56 | ginkgo.BeforeEach(f.BeforeEach) 57 | ginkgo.AfterEach(f.AfterEach) 58 | return f 59 | } 60 | 61 | func (f *Framework) BeforeEach() { 62 | f.beforeEachStarted = true 63 | 64 | f.cleanupHandle = AddCleanupAction(f.AfterEach) 65 | 66 | dir, err := os.MkdirTemp(os.TempDir(), "fft-e2e-test-*") 67 | ExpectNoError(err) 68 | f.TempDirectory = dir 69 | } 70 | 71 | func (f *Framework) AfterEach() { 72 | if !f.beforeEachStarted { 73 | return 74 | } 75 | 76 | RemoveCleanupAction(f.cleanupHandle) 77 | 78 | if f.serverProcess != nil { 79 | _ = f.serverProcess.Stop() 80 | if TestContext.Debug || ginkgo.CurrentSpecReport().Failed() { 81 | fmt.Println(f.serverProcess.ErrorOutput()) 82 | fmt.Println(f.serverProcess.StdOutput()) 83 | } 84 | } 85 | for _, p := range f.workerProcesses { 86 | _ = p.Stop() 87 | if TestContext.Debug || ginkgo.CurrentSpecReport().Failed() { 88 | fmt.Println(p.ErrorOutput()) 89 | fmt.Println(p.StdOutput()) 90 | } 91 | } 92 | for _, p := range f.clientProcesses { 93 | _ = p.Stop() 94 | if TestContext.Debug || ginkgo.CurrentSpecReport().Failed() { 95 | fmt.Println(p.ErrorOutput()) 96 | fmt.Println(p.StdOutput()) 97 | } 98 | } 99 | f.serverProcess = nil 100 | f.workerProcesses = nil 101 | f.clientProcesses = nil 102 | 103 | os.RemoveAll(f.TempDirectory) 104 | f.TempDirectory = "" 105 | f.serverConfPath = "" 106 | f.workerConfPaths = []string{} 107 | f.clientConfPaths = []string{} 108 | 109 | for _, port := range f.usedPorts { 110 | ReleasePort(port) 111 | } 112 | f.usedPorts = make(map[string]int) 113 | 114 | for _, port := range f.allocatedPorts { 115 | ReleasePort(port) 116 | } 117 | f.allocatedPorts = make([]int, 0) 118 | 119 | f.osEnvs = make([]string, 0) 120 | } 121 | 122 | func (f *Framework) AllocPort() int { 123 | port := AllocPort() 124 | ExpectTrue(port > 0, "alloc port failed") 125 | f.allocatedPorts = append(f.allocatedPorts, port) 126 | return port 127 | } 128 | 129 | func (f *Framework) PortByName(name string) int { 130 | return f.usedPorts[name] 131 | } 132 | 133 | func (f *Framework) SetEnvs(envs []string) { 134 | f.osEnvs = envs 135 | } 136 | 137 | func (f *Framework) WriteTempFile(name string, content string) string { 138 | filePath := filepath.Join(f.TempDirectory, name) 139 | err := os.WriteFile(filePath, []byte(content), 0o600) 140 | ExpectNoError(err) 141 | return filePath 142 | } 143 | 144 | func (f *Framework) GenerateConfigFile(content string) string { 145 | f.configFileIndex++ 146 | path := filepath.Join(f.TempDirectory, fmt.Sprintf("fft-e2e-config-%d", f.configFileIndex)) 147 | err := os.WriteFile(path, []byte(content), 0o600) 148 | ExpectNoError(err) 149 | return path 150 | } 151 | -------------------------------------------------------------------------------- /test/e2e/framework/process.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | ) 9 | 10 | func (f *Framework) RunServer(args ...string) (*Process, string, error) { 11 | p := NewWithEnvs(TestContext.FFTSPath, args, f.osEnvs) 12 | f.serverProcess = p 13 | err := p.Start() 14 | if err != nil { 15 | return p, p.StdOutput(), err 16 | } 17 | time.Sleep(2 * time.Second) 18 | return p, p.StdOutput(), nil 19 | } 20 | 21 | func (f *Framework) RunWorker(args ...string) (*Process, string, error) { 22 | p := NewWithEnvs(TestContext.FFTWPath, args, f.osEnvs) 23 | f.workerProcesses = append(f.workerProcesses, p) 24 | err := p.Start() 25 | if err != nil { 26 | return p, p.StdOutput(), err 27 | } 28 | time.Sleep(2 * time.Second) 29 | return p, p.StdOutput(), nil 30 | } 31 | 32 | func (f *Framework) RunClient(args ...string) (*Process, string, error) { 33 | p := NewWithEnvs(TestContext.FFTPath, args, f.osEnvs) 34 | f.clientProcesses = append(f.clientProcesses, p) 35 | err := p.Start() 36 | if err != nil { 37 | return p, p.StdOutput(), err 38 | } 39 | time.Sleep(1 * time.Second) 40 | return p, p.StdOutput(), nil 41 | } 42 | 43 | type Process struct { 44 | cmd *Cmd 45 | errorOutput *Buffer 46 | stdOutput *Buffer 47 | stopped bool 48 | } 49 | 50 | func NewWithEnvs(path string, params []string, envs []string) *Process { 51 | cmd := NewCmd(path, params...) 52 | cmd.Env = envs 53 | p := &Process{ 54 | cmd: cmd, 55 | } 56 | p.errorOutput = NewBuffer() 57 | p.stdOutput = NewBuffer() 58 | cmd.Stderr = p.errorOutput 59 | cmd.Stdout = p.stdOutput 60 | return p 61 | } 62 | 63 | func (p *Process) Start() error { 64 | return p.cmd.Start() 65 | } 66 | 67 | func (p *Process) Stop() error { 68 | if p.stopped { 69 | return nil 70 | } 71 | defer func() { 72 | p.stopped = true 73 | }() 74 | return p.cmd.Stop() 75 | } 76 | 77 | func (p *Process) ErrorOutput() string { 78 | return p.errorOutput.String() 79 | } 80 | 81 | func (p *Process) StdOutput() string { 82 | return p.stdOutput.String() 83 | } 84 | 85 | func (f *Framework) CreateTestFile(content []byte) (string, error) { 86 | f.mutex.Lock() 87 | defer f.mutex.Unlock() 88 | 89 | filename := fmt.Sprintf("test-file-%d", time.Now().UnixNano()) 90 | filepath := filepath.Join(f.TempDirectory, filename) 91 | 92 | err := os.WriteFile(filepath, content, 0644) 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | return filepath, nil 98 | } 99 | 100 | func (f *Framework) VerifyFileContent(path string, expected []byte) error { 101 | content, err := os.ReadFile(path) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if len(content) != len(expected) { 107 | return fmt.Errorf("file content length mismatch: got %d, expected %d", len(content), len(expected)) 108 | } 109 | 110 | for i := range content { 111 | if content[i] != expected[i] { 112 | return fmt.Errorf("file content mismatch at position %d: got %d, expected %d", i, content[i], expected[i]) 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /test/e2e/framework/util.go: -------------------------------------------------------------------------------- 1 | package framework 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net" 7 | "os/exec" 8 | "sync" 9 | "time" 10 | 11 | "github.com/onsi/ginkgo/v2" 12 | "github.com/onsi/gomega" 13 | ) 14 | 15 | type Buffer struct { 16 | buffer bytes.Buffer 17 | mutex sync.Mutex 18 | } 19 | 20 | func (b *Buffer) Write(p []byte) (n int, err error) { 21 | b.mutex.Lock() 22 | defer b.mutex.Unlock() 23 | return b.buffer.Write(p) 24 | } 25 | 26 | func (b *Buffer) String() string { 27 | b.mutex.Lock() 28 | defer b.mutex.Unlock() 29 | return b.buffer.String() 30 | } 31 | 32 | func NewBuffer() *Buffer { 33 | return &Buffer{} 34 | } 35 | 36 | type Cmd struct { 37 | *exec.Cmd 38 | ctx context.Context 39 | cancel context.CancelFunc 40 | } 41 | 42 | func NewCmd(path string, args ...string) *Cmd { 43 | ctx, cancel := context.WithCancel(context.Background()) 44 | cmd := exec.CommandContext(ctx, path, args...) 45 | return &Cmd{ 46 | Cmd: cmd, 47 | ctx: ctx, 48 | cancel: cancel, 49 | } 50 | } 51 | 52 | func (c *Cmd) Stop() error { 53 | c.cancel() 54 | return c.Wait() 55 | } 56 | 57 | type CleanupActionHandle int 58 | 59 | var ( 60 | cleanupActionsLock sync.Mutex 61 | cleanupActions = map[CleanupActionHandle]func(){} 62 | nextCleanupAction CleanupActionHandle = 0 63 | ) 64 | 65 | func AddCleanupAction(fn func()) CleanupActionHandle { 66 | cleanupActionsLock.Lock() 67 | defer cleanupActionsLock.Unlock() 68 | handle := nextCleanupAction 69 | nextCleanupAction++ 70 | cleanupActions[handle] = fn 71 | return handle 72 | } 73 | 74 | func RemoveCleanupAction(handle CleanupActionHandle) { 75 | cleanupActionsLock.Lock() 76 | defer cleanupActionsLock.Unlock() 77 | delete(cleanupActions, handle) 78 | } 79 | 80 | func RunCleanupActions() { 81 | cleanupActionsLock.Lock() 82 | defer cleanupActionsLock.Unlock() 83 | for _, fn := range cleanupActions { 84 | fn() 85 | } 86 | cleanupActions = map[CleanupActionHandle]func(){} 87 | } 88 | 89 | func Fail(message string, callerSkip ...int) { 90 | skip := 1 91 | if len(callerSkip) > 0 { 92 | skip = callerSkip[0] 93 | } 94 | ginkgo.Fail(message, skip) 95 | } 96 | 97 | func ExpectNoError(err error, explain ...interface{}) { 98 | gomega.ExpectWithOffset(1, err).NotTo(gomega.HaveOccurred(), explain...) 99 | } 100 | 101 | func ExpectTrue(actual interface{}, explain ...interface{}) { 102 | gomega.ExpectWithOffset(1, actual).To(gomega.BeTrue(), explain...) 103 | } 104 | 105 | func ExpectEqual(actual, expected interface{}, explain ...interface{}) { 106 | gomega.ExpectWithOffset(1, actual).To(gomega.Equal(expected), explain...) 107 | } 108 | 109 | func AllocPort() int { 110 | l, err := net.Listen("tcp", "127.0.0.1:0") 111 | if err != nil { 112 | return 0 113 | } 114 | defer l.Close() 115 | return l.Addr().(*net.TCPAddr).Port 116 | } 117 | 118 | func ReleasePort(port int) { 119 | time.Sleep(100 * time.Millisecond) // Small delay to ensure port is fully released 120 | } 121 | -------------------------------------------------------------------------------- /test/e2e/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fatedier/fft/test/e2e 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/onsi/ginkgo/v2 v2.23.4 7 | github.com/onsi/gomega v1.37.0 8 | ) 9 | 10 | require ( 11 | github.com/go-logr/logr v1.4.2 // indirect 12 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 13 | github.com/google/go-cmp v0.7.0 // indirect 14 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 15 | go.uber.org/automaxprocs v1.6.0 // indirect 16 | golang.org/x/net v0.37.0 // indirect 17 | golang.org/x/sys v0.32.0 // indirect 18 | golang.org/x/text v0.23.0 // indirect 19 | golang.org/x/tools v0.31.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /test/e2e/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 4 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 5 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 6 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 7 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 8 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 9 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 10 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 11 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 12 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 16 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 17 | github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= 18 | github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 22 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 23 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 24 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 25 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 26 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 27 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 28 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 29 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 30 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 31 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 32 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 33 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 34 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 35 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 36 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 39 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 41 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | -------------------------------------------------------------------------------- /test/e2e/pkg/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os/exec" 7 | "sync" 8 | ) 9 | 10 | type Process struct { 11 | cmd *exec.Cmd 12 | cancel context.CancelFunc 13 | errorOutput *bytes.Buffer 14 | stdOutput *bytes.Buffer 15 | mutex sync.Mutex 16 | stopped bool 17 | } 18 | 19 | func New(path string, params []string) *Process { 20 | return NewWithEnvs(path, params, nil) 21 | } 22 | 23 | func NewWithEnvs(path string, params []string, envs []string) *Process { 24 | ctx, cancel := context.WithCancel(context.Background()) 25 | cmd := exec.CommandContext(ctx, path, params...) 26 | cmd.Env = envs 27 | p := &Process{ 28 | cmd: cmd, 29 | cancel: cancel, 30 | } 31 | p.errorOutput = bytes.NewBufferString("") 32 | p.stdOutput = bytes.NewBufferString("") 33 | cmd.Stderr = p.errorOutput 34 | cmd.Stdout = p.stdOutput 35 | return p 36 | } 37 | 38 | func (p *Process) Start() error { 39 | return p.cmd.Start() 40 | } 41 | 42 | func (p *Process) Stop() error { 43 | p.mutex.Lock() 44 | defer p.mutex.Unlock() 45 | 46 | if p.stopped { 47 | return nil 48 | } 49 | 50 | p.stopped = true 51 | p.cancel() 52 | return p.cmd.Wait() 53 | } 54 | 55 | func (p *Process) ErrorOutput() string { 56 | p.mutex.Lock() 57 | defer p.mutex.Unlock() 58 | return p.errorOutput.String() 59 | } 60 | 61 | func (p *Process) StdOutput() string { 62 | p.mutex.Lock() 63 | defer p.mutex.Unlock() 64 | return p.stdOutput.String() 65 | } 66 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var version string = "0.2.0" 4 | 5 | func Full() string { 6 | return version 7 | } 8 | 9 | var defaultServerAddr string = "fft.gofrp.org:7777" 10 | 11 | func DefaultServerAddr() string { 12 | return defaultServerAddr 13 | } 14 | -------------------------------------------------------------------------------- /worker/match.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | fio "github.com/fatedier/fft/pkg/io" 11 | "github.com/fatedier/fft/pkg/log" 12 | "github.com/fatedier/fft/pkg/msg" 13 | 14 | gio "github.com/fatedier/golib/io" 15 | 16 | "golang.org/x/time/rate" 17 | ) 18 | 19 | type TransferConn struct { 20 | isSender bool 21 | id string 22 | conn net.Conn 23 | 24 | pairConnCh chan *TransferConn 25 | } 26 | 27 | func NewTransferConn(id string, conn net.Conn, isSender bool) *TransferConn { 28 | return &TransferConn{ 29 | isSender: isSender, 30 | id: id, 31 | conn: conn, 32 | pairConnCh: make(chan *TransferConn), 33 | } 34 | } 35 | 36 | type MatchController struct { 37 | conns map[string]*TransferConn 38 | 39 | rateLimit *rate.Limiter 40 | statFunc func(int) 41 | mu sync.Mutex 42 | } 43 | 44 | func NewMatchController(rateByte int, statFunc func(int)) *MatchController { 45 | if rateByte < 50*1024 { 46 | rateByte = 50 * 1024 47 | } 48 | return &MatchController{ 49 | conns: make(map[string]*TransferConn), 50 | rateLimit: rate.NewLimiter(rate.Limit(float64(rateByte)), 32*1024), 51 | statFunc: statFunc, 52 | } 53 | } 54 | 55 | // block until there is a same ID transfer conn or timeout 56 | func (mc *MatchController) DealTransferConn(tc *TransferConn, timeout time.Duration) error { 57 | mc.mu.Lock() 58 | pairConn, ok := mc.conns[tc.id] 59 | if !ok { 60 | mc.conns[tc.id] = tc 61 | } else { 62 | delete(mc.conns, tc.id) 63 | } 64 | mc.mu.Unlock() 65 | 66 | if !ok { 67 | select { 68 | case pairConn := <-tc.pairConnCh: 69 | var sender, receiver io.ReadWriteCloser 70 | if tc.isSender { 71 | wrapReader := fio.NewCallbackReader(fio.NewRateReader(tc.conn, mc.rateLimit), mc.statFunc) 72 | sender = gio.WrapReadWriteCloser(wrapReader, tc.conn, func() error { 73 | return tc.conn.Close() 74 | }) 75 | receiver = pairConn.conn 76 | } else { 77 | wrapReader := fio.NewCallbackReader(fio.NewRateReader(pairConn.conn, mc.rateLimit), mc.statFunc) 78 | sender = gio.WrapReadWriteCloser(wrapReader, pairConn.conn, func() error { 79 | return pairConn.conn.Close() 80 | }) 81 | receiver = tc.conn 82 | } 83 | msg.WriteMsg(sender, &msg.NewSendFileStreamResp{}) 84 | msg.WriteMsg(receiver, &msg.NewReceiveFileStreamResp{}) 85 | 86 | go func() { 87 | gio.Join(sender, receiver) 88 | log.Info("ID [%s] join pair connections closed", tc.id) 89 | }() 90 | case <-time.After(timeout): 91 | mc.mu.Lock() 92 | if tmp, ok := mc.conns[tc.id]; ok && tmp == tc { 93 | delete(mc.conns, tc.id) 94 | } 95 | mc.mu.Unlock() 96 | return fmt.Errorf("timeout waiting pair connection") 97 | } 98 | } else { 99 | select { 100 | case pairConn.pairConnCh <- tc: 101 | default: 102 | return fmt.Errorf("no target pair connection") 103 | } 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /worker/register.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/fatedier/fft/pkg/log" 11 | "github.com/fatedier/fft/pkg/msg" 12 | "github.com/fatedier/fft/version" 13 | ) 14 | 15 | type Register struct { 16 | port int64 17 | advicePublicIP string 18 | serverAddr string 19 | conn net.Conn 20 | 21 | closed bool 22 | mu sync.Mutex 23 | } 24 | 25 | func NewRegister(port int64, advicePublicIP string, serverAddr string) (*Register, error) { 26 | conn, err := net.Dial("tcp", serverAddr) 27 | if err != nil { 28 | return nil, err 29 | } 30 | conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) 31 | 32 | return &Register{ 33 | port: port, 34 | advicePublicIP: advicePublicIP, 35 | serverAddr: serverAddr, 36 | conn: conn, 37 | closed: false, 38 | }, nil 39 | } 40 | 41 | func (r *Register) Register() error { 42 | msg.WriteMsg(r.conn, &msg.RegisterWorker{ 43 | Version: version.Full(), 44 | PublicIP: r.advicePublicIP, 45 | BindPort: r.port, 46 | }) 47 | 48 | r.conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 49 | m, err := msg.ReadMsg(r.conn) 50 | if err != nil { 51 | log.Warn("read RegisterWorkerResp error: %v", err) 52 | return err 53 | } 54 | r.conn.SetReadDeadline(time.Time{}) 55 | 56 | resp, ok := m.(*msg.RegisterWorkerResp) 57 | if !ok { 58 | return fmt.Errorf("read RegisterWorkerResp format error") 59 | } 60 | 61 | if resp.Error != "" { 62 | return fmt.Errorf(resp.Error) 63 | } 64 | return nil 65 | } 66 | 67 | func (r *Register) RunKeepAlive() { 68 | var err error 69 | for { 70 | // send ping and read pong 71 | for { 72 | // in case it is closed before 73 | if r.conn == nil { 74 | break 75 | } 76 | 77 | msg.WriteMsg(r.conn, &msg.Ping{}) 78 | 79 | _, err = msg.ReadMsg(r.conn) 80 | if err != nil { 81 | r.conn.Close() 82 | break 83 | } 84 | 85 | time.Sleep(10 * time.Second) 86 | } 87 | 88 | for { 89 | r.mu.Lock() 90 | closed := r.closed 91 | r.mu.Unlock() 92 | if r.closed { 93 | return 94 | } 95 | 96 | conn, err := net.Dial("tcp", r.serverAddr) 97 | if err != nil { 98 | time.Sleep(10 * time.Second) 99 | continue 100 | } 101 | conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) 102 | 103 | r.mu.Lock() 104 | closed = r.closed 105 | if closed { 106 | conn.Close() 107 | r.mu.Unlock() 108 | return 109 | } 110 | r.conn = conn 111 | r.mu.Unlock() 112 | 113 | err = r.Register() 114 | if err != nil { 115 | r.conn.Close() 116 | time.Sleep(10 * time.Second) 117 | continue 118 | } 119 | 120 | break 121 | } 122 | } 123 | } 124 | 125 | func (r *Register) Close() { 126 | r.mu.Lock() 127 | defer r.mu.Unlock() 128 | r.closed = true 129 | r.conn.Close() 130 | } 131 | 132 | // Reset can be only called after Close 133 | func (r *Register) Reset() { 134 | r.closed = false 135 | r.conn = nil 136 | } 137 | -------------------------------------------------------------------------------- /worker/service.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "fmt" 10 | "math/big" 11 | "net" 12 | "strconv" 13 | "time" 14 | 15 | "github.com/fatedier/fft/pkg/log" 16 | "github.com/fatedier/fft/pkg/msg" 17 | ) 18 | 19 | type Options struct { 20 | ServerAddr string 21 | BindAddr string 22 | AdvicePublicIP string 23 | RateKB int // xx KB/s 24 | MaxTrafficMBPerDay int // xx MB, 0 is no limit 25 | 26 | LogFile string 27 | LogLevel string 28 | LogMaxDays int64 29 | } 30 | 31 | func (op *Options) Check() error { 32 | if op.LogMaxDays <= 0 { 33 | op.LogMaxDays = 3 34 | } 35 | if op.RateKB < 50 { 36 | return fmt.Errorf("rate should be greater than 50KB") 37 | } 38 | if op.MaxTrafficMBPerDay < 128 && op.MaxTrafficMBPerDay != 0 { 39 | return fmt.Errorf("max_traffic_per_day should be greater than 128MB") 40 | } 41 | return nil 42 | } 43 | 44 | type Service struct { 45 | serverAddr string 46 | advicePublicIP string 47 | rateKB int 48 | maxTrafficMBPerDay int 49 | 50 | l net.Listener 51 | matchCtl *MatchController 52 | register *Register 53 | trafficLimiter *TrafficLimiter 54 | tlsConfig *tls.Config 55 | 56 | stopCh chan struct{} 57 | } 58 | 59 | func NewService(options Options) (*Service, error) { 60 | if err := options.Check(); err != nil { 61 | return nil, err 62 | } 63 | 64 | logway := "file" 65 | if options.LogFile == "console" { 66 | logway = "console" 67 | } 68 | log.InitLog(logway, options.LogFile, options.LogLevel, options.LogMaxDays) 69 | 70 | l, err := net.Listen("tcp", options.BindAddr) 71 | if err != nil { 72 | return nil, err 73 | } 74 | log.Info("fftw listen on: %s", l.Addr().String()) 75 | 76 | _, portStr, err := net.SplitHostPort(l.Addr().String()) 77 | if err != nil { 78 | return nil, fmt.Errorf("get bind port error, bind address: %v", l.Addr().String()) 79 | } 80 | port, err := strconv.Atoi(portStr) 81 | if err != nil { 82 | return nil, fmt.Errorf("get bind port error: %v", err) 83 | } 84 | 85 | register, err := NewRegister(int64(port), options.AdvicePublicIP, options.ServerAddr) 86 | if err != nil { 87 | return nil, fmt.Errorf("new register error: %v", err) 88 | } 89 | 90 | svc := &Service{ 91 | serverAddr: options.ServerAddr, 92 | advicePublicIP: options.AdvicePublicIP, 93 | rateKB: options.RateKB, 94 | maxTrafficMBPerDay: options.MaxTrafficMBPerDay, 95 | 96 | l: l, 97 | register: register, 98 | tlsConfig: generateTLSConfig(), 99 | 100 | stopCh: make(chan struct{}), 101 | } 102 | 103 | svc.trafficLimiter = NewTrafficLimiter(uint64(options.MaxTrafficMBPerDay*1024*1024), func() { 104 | svc.register.Close() 105 | log.Info("reach traffic limit %dMB one day, unregister from server", options.MaxTrafficMBPerDay) 106 | }, func() { 107 | svc.register.Reset() 108 | go svc.register.RunKeepAlive() 109 | log.Info("restore from traffic limit since it's a new day") 110 | }) 111 | 112 | svc.matchCtl = NewMatchController(options.RateKB*1024, func(n int) { 113 | svc.trafficLimiter.AddCount(uint64(n)) 114 | }) 115 | return svc, nil 116 | } 117 | 118 | func (svc *Service) Run() error { 119 | go svc.worker() 120 | go svc.trafficLimiter.Run() 121 | 122 | err := svc.register.Register() 123 | if err != nil { 124 | return fmt.Errorf("register worker to server error: %v", err) 125 | } 126 | log.Info("register to server success") 127 | 128 | svc.register.RunKeepAlive() 129 | <-svc.stopCh 130 | return nil 131 | } 132 | 133 | func (svc *Service) worker() error { 134 | for { 135 | conn, err := svc.l.Accept() 136 | if err != nil { 137 | return err 138 | } 139 | conn = tls.Server(conn, svc.tlsConfig) 140 | go svc.handleConn(conn) 141 | } 142 | } 143 | 144 | func (svc *Service) handleConn(conn net.Conn) { 145 | var ( 146 | rawMsg msg.Message 147 | err error 148 | ) 149 | 150 | conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 151 | if rawMsg, err = msg.ReadMsg(conn); err != nil { 152 | conn.Close() 153 | return 154 | } 155 | conn.SetReadDeadline(time.Time{}) 156 | 157 | switch m := rawMsg.(type) { 158 | case *msg.NewSendFileStream: 159 | log.Debug("new send file stream [%s]", m.ID) 160 | tc := NewTransferConn(m.ID, conn, true) 161 | if err = svc.matchCtl.DealTransferConn(tc, 20*time.Second); err != nil { 162 | msg.WriteMsg(conn, &msg.NewSendFileStreamResp{ 163 | Error: err.Error(), 164 | }) 165 | conn.Close() 166 | } 167 | case *msg.NewReceiveFileStream: 168 | log.Debug("new recv file stream [%s]", m.ID) 169 | tc := NewTransferConn(m.ID, conn, false) 170 | if err = svc.matchCtl.DealTransferConn(tc, 20*time.Second); err != nil { 171 | msg.WriteMsg(conn, &msg.NewReceiveFileStreamResp{ 172 | Error: err.Error(), 173 | }) 174 | conn.Close() 175 | } 176 | case *msg.Ping: 177 | log.Debug("return pong to server ping") 178 | msg.WriteMsg(conn, &msg.Pong{}) 179 | conn.Close() 180 | return 181 | default: 182 | conn.Close() 183 | return 184 | } 185 | } 186 | 187 | // Setup a bare-bones TLS config for the server 188 | func generateTLSConfig() *tls.Config { 189 | key, err := rsa.GenerateKey(rand.Reader, 1024) 190 | if err != nil { 191 | panic(err) 192 | } 193 | template := x509.Certificate{SerialNumber: big.NewInt(1)} 194 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) 195 | if err != nil { 196 | panic(err) 197 | } 198 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) 199 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 200 | 201 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) 202 | if err != nil { 203 | panic(err) 204 | } 205 | return &tls.Config{Certificates: []tls.Certificate{tlsCert}} 206 | } 207 | -------------------------------------------------------------------------------- /worker/traffic.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "math" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | type TrafficLimiter struct { 10 | count uint64 11 | maxCountPerDay uint64 12 | 13 | exceedCh chan struct{} 14 | exceedLimitCallback func() 15 | restoreCallback func() 16 | } 17 | 18 | func NewTrafficLimiter(maxCountPerDay uint64, exceedLimitCallback func(), restoreCallback func()) *TrafficLimiter { 19 | if maxCountPerDay == 0 { 20 | maxCountPerDay = math.MaxUint64 21 | } 22 | 23 | return &TrafficLimiter{ 24 | count: 0, 25 | maxCountPerDay: maxCountPerDay, 26 | 27 | exceedCh: make(chan struct{}), 28 | exceedLimitCallback: exceedLimitCallback, 29 | restoreCallback: restoreCallback, 30 | } 31 | } 32 | 33 | func (tl *TrafficLimiter) AddCount(count uint64) { 34 | newCount := atomic.AddUint64(&tl.count, count) 35 | if newCount-count < tl.maxCountPerDay && newCount >= tl.maxCountPerDay { 36 | tl.exceedCh <- struct{}{} 37 | } 38 | } 39 | 40 | func (tl *TrafficLimiter) Run() { 41 | go tl.restoreWorker() 42 | } 43 | 44 | // change count to 0 every day 45 | func (tl *TrafficLimiter) restoreWorker() { 46 | now := time.Now() 47 | lastRestoreTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 48 | exceed := false 49 | 50 | for { 51 | select { 52 | case <-tl.exceedCh: 53 | exceed = true 54 | tl.exceedLimitCallback() 55 | case now := <-time.After(5 * time.Second): 56 | nowDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 57 | days := int(nowDay.Sub(lastRestoreTime).Hours() / 24) 58 | if days > 0 { 59 | atomic.StoreUint64(&tl.count, 0) 60 | lastRestoreTime = nowDay 61 | if exceed { 62 | exceed = false 63 | tl.restoreCallback() 64 | } 65 | } 66 | } 67 | } 68 | } 69 | --------------------------------------------------------------------------------