list) {
50 | return isEmpty(list) ? null : list.get(0);
51 | }
52 |
53 | /**
54 | * 给 ByteBuf 写入定长字符串
55 | *
56 | * 若字符串长度大于定长,则截取定长字节;若小于定长,则补零
57 | *
58 | * @param buf
59 | * @param content
60 | * @param length
61 | */
62 | public static void writeFixLength(ByteBuf buf, String content, int length) {
63 | byte[] bytes = content.getBytes(FastdfsConstants.UTF_8);
64 | int blen = bytes.length;
65 | int wlen = Math.min(blen, length);
66 | buf.writeBytes(bytes, 0, wlen);
67 | if (wlen < length) {
68 | buf.writeZero(length - wlen);
69 | }
70 | }
71 |
72 | /**
73 | * 读取固定长度的字符串(修剪掉补零的字节)
74 | *
75 | * @param in
76 | * @param length
77 | * @return
78 | */
79 | public static String readString(ByteBuf in, int length) {
80 | byte[] bytes = new byte[length];
81 | in.readBytes(bytes);
82 | return new String(bytes, FastdfsConstants.UTF_8).trim();
83 | }
84 |
85 | /**
86 | * 读取字符串(修剪掉补零的字节)
87 | *
88 | * @param in
89 | * @return
90 | */
91 | public static String readString(ByteBuf in) {
92 | return in.toString(FastdfsConstants.UTF_8);
93 | }
94 |
95 | /**
96 | * 获取文件扩展名
97 | *
98 | * @param filename
99 | * @return
100 | */
101 | public static String getFileExt(String filename, String defaultExt) {
102 | String fileExt = getFileExt(filename);
103 | return isEmpty(fileExt) || !fileExt.matches(WORDS) ? defaultExt : fileExt;
104 | }
105 |
106 | /**
107 | * 获取文件扩展名
108 | *
109 | * @param filename
110 | * @return
111 | */
112 | private static String getFileExt(String filename) {
113 | if (filename == null) {
114 | return "";
115 | }
116 | int idx = filename.lastIndexOf('.');
117 | return idx == -1 ? "" : filename.substring(idx + 1).toLowerCase();
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/java/fastdfs/client/TrackerClient.java:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | package fastdfs.client;
5 |
6 | import fastdfs.client.codec.*;
7 |
8 | import java.io.Closeable;
9 | import java.io.IOException;
10 | import java.net.InetSocketAddress;
11 | import java.util.List;
12 | import java.util.concurrent.CompletableFuture;
13 |
14 |
15 | final class TrackerClient implements Closeable {
16 |
17 | private final FastdfsExecutor executor;
18 | private final TrackerSelector selector;
19 | private final TrackerMonitor monitor;
20 |
21 | TrackerClient(FastdfsExecutor executor, FastdfsScheduler scheduler, TrackerSelector selector, List servers, int fall, int rise, long checkTimeout, long checkInterval) {
22 | this.executor = executor;
23 | this.selector = servers.size() == 1 ? TrackerSelector.FIRST : selector;
24 | this.monitor = new TrackerMonitor(executor, scheduler, servers, fall, rise, checkTimeout, checkInterval);
25 | }
26 |
27 | private CompletableFuture trackerSelect() {
28 | CompletableFuture promise = new CompletableFuture<>();
29 | try {
30 | promise.complete(monitor.trackerSelect(selector).toInetAddress());
31 | } catch (Exception e) {
32 | promise.completeExceptionally(e);
33 | }
34 | return promise;
35 | }
36 |
37 | /**
38 | * @return
39 | */
40 | CompletableFuture uploadStorageGet() {
41 | return uploadStorageGet(null);
42 | }
43 |
44 | /**
45 | * @param group
46 | * @return
47 | */
48 | CompletableFuture uploadStorageGet(String group) {
49 | return trackerSelect().thenCompose(addr -> executor.execute(addr, new UploadStorageGetEncoder(group), StorageServerDecoder.INSTANCE));
50 | }
51 |
52 | /**
53 | * @param fileId
54 | * @return
55 | */
56 | CompletableFuture downloadStorageGet(FileId fileId) {
57 | CompletableFuture> result = trackerSelect().thenCompose((addr -> executor.execute(addr, new DownloadStorageGetEncoder(fileId), StorageServerListDecoder.INSTANCE)));
58 | return result.thenApply(FastdfsUtils::first);
59 | }
60 |
61 | /**
62 | * 获取更新存储服务器地址
63 | *
64 | * @param fileId
65 | */
66 | CompletableFuture updateStorageGet(FileId fileId) {
67 | CompletableFuture> result = trackerSelect().thenCompose(addr -> executor.execute(addr, new UpdateStorageGetEncoder(fileId), StorageServerListDecoder.INSTANCE));
68 | return result.thenApply(FastdfsUtils::first);
69 | }
70 |
71 | /**
72 | * @param fileId
73 | * @return
74 | */
75 | CompletableFuture> downloadStorageList(FileId fileId) {
76 | return trackerSelect().thenCompose(addr -> executor.execute(addr, new DownloadStorageListEncoder(fileId), StorageServerListDecoder.INSTANCE));
77 | }
78 |
79 | @Override
80 | public void close() throws IOException {
81 | try {
82 | monitor.close();
83 | } catch (Exception e) {
84 | // do nothing.
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/test/java/fastdfs/client/FastdfsClientTest.java:
--------------------------------------------------------------------------------
1 | package fastdfs.client;
2 |
3 | import org.junit.After;
4 | import org.junit.Before;
5 | import org.junit.Test;
6 |
7 | import java.io.File;
8 | import java.io.FileOutputStream;
9 | import java.util.concurrent.CompletableFuture;
10 |
11 | /**
12 | * @author siuming
13 | */
14 | public class FastdfsClientTest {
15 |
16 | FastdfsClient client;
17 |
18 | @Before
19 | public void setUp() throws Exception {
20 | client = FastdfsClient.newBuilder()
21 | .connectTimeout(3000)
22 | .readTimeout(100)
23 | .maxThreads(100)
24 | .tracker("172.16.1.25", 22222)
25 | .build();
26 |
27 | }
28 |
29 | @Test
30 | public void testUpload() throws Exception {
31 | long current = System.currentTimeMillis();
32 | CompletableFuture path = client.upload(null, new File("/tmp/test.dmg"));
33 | System.out.println(path.get());
34 | System.out.println("==========");
35 | System.out.println(System.currentTimeMillis() - current + " ms");
36 | }
37 |
38 | @Test
39 | public void testUploadAppend() throws Exception {
40 | }
41 |
42 | @Test
43 | public void testDownload() throws Exception {
44 |
45 | long current = System.currentTimeMillis();
46 | for (int i = 0; i < 100; i++) {
47 | File file = new File("/tmp/logo2.png");
48 | FileId path = FileId.fromString("group1/M00/00/00/ZfvfZlbz1EyAC4FPAAAWNZ1l3ec600.png");
49 | try (FileOutputStream outputStream = new FileOutputStream(file)) {
50 | CompletableFuture promise = client.download(path, outputStream);
51 | promise.get();
52 | }
53 | }
54 | System.out.println(System.currentTimeMillis() - current + " ms");
55 | }
56 |
57 | @Test
58 | public void testSetMetadata() throws Exception {
59 | FileId path = FileId.fromString("group1/M00/00/00/ZfvfZlbz1EyAC4FPAAAWNZ1l3ec600.png");
60 | FileMetadata metadata = FileMetadata.newBuilder().put("test", "test1").build();
61 | CompletableFuture promise = client.metadataSet(path, metadata);
62 | promise.get();
63 | }
64 |
65 | @Test
66 | public void testGetMetadata() throws Exception {
67 | FileId path = FileId.fromString("group1/M00/00/00/ZfvfZlbz1EyAC4FPAAAWNZ1l3ec600.png");
68 | CompletableFuture promise = client.metadataGet(path);
69 | System.out.println(promise.get().values());
70 | }
71 |
72 | @Test
73 | public void testGetInfo() throws Exception {
74 | FileId path = FileId.fromString("group1/M00/00/00/ZfvfZlbz6VuAPdosAARXBcPHPhU268.log");
75 | CompletableFuture promise = client.infoGet(path);
76 | System.out.println(promise.get());
77 | }
78 |
79 | @Test
80 | public void testDelete() throws Exception {
81 | FileId path = FileId.fromString("group1/M00/00/00/ZfvfZlbz7TaAeyUeAeJOH39coH0381.dmg");
82 | client.delete(path).get();
83 | }
84 |
85 | @After
86 | public void tearDown() throws Exception {
87 | client.close();
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/fastdfs/client/FastdfsPool.java:
--------------------------------------------------------------------------------
1 | package fastdfs.client;
2 |
3 | import io.netty.bootstrap.Bootstrap;
4 | import io.netty.channel.Channel;
5 | import io.netty.channel.ChannelPipeline;
6 | import io.netty.channel.pool.ChannelPool;
7 | import io.netty.channel.pool.ChannelPoolHandler;
8 | import io.netty.channel.pool.FixedChannelPool;
9 | import io.netty.handler.stream.ChunkedWriteHandler;
10 | import io.netty.handler.timeout.IdleStateHandler;
11 | import io.netty.util.concurrent.Future;
12 | import io.netty.util.concurrent.Promise;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | import java.util.concurrent.TimeUnit;
17 |
18 | /**
19 | * @author siuming
20 | */
21 | final class FastdfsPool implements ChannelPool {
22 |
23 | private static final Logger LOG = LoggerFactory.getLogger(FastdfsPoolGroup.class);
24 | private final ChannelPool channelPool;
25 |
26 | FastdfsPool(Bootstrap bootstrap, long readTimeout, long idleTimeout, int maxConnPerHost, int maxPendingRequests) {
27 | this.channelPool = new FixedChannelPool(
28 | bootstrap,
29 | new FastdfsPoolHandler(readTimeout, idleTimeout),
30 | maxConnPerHost,
31 | maxPendingRequests
32 | );
33 | }
34 |
35 | public Future acquire() {
36 | return channelPool.acquire();
37 | }
38 |
39 | public Future release(Channel channel, Promise promise) {
40 | return channelPool.release(channel, promise);
41 | }
42 |
43 | public Future acquire(Promise promise) {
44 | return channelPool.acquire(promise);
45 | }
46 |
47 | public void close() {
48 | channelPool.close();
49 | }
50 |
51 | public Future release(Channel channel) {
52 | return channelPool.release(channel);
53 | }
54 |
55 | private static class FastdfsPoolHandler implements ChannelPoolHandler {
56 | final long readTimeout;
57 | final long idleTimeout; // 最大闲置时间(秒)
58 |
59 | FastdfsPoolHandler(long readTimeout, long idleTimeout) {
60 | this.readTimeout = readTimeout;
61 | this.idleTimeout = idleTimeout;
62 | }
63 |
64 | public void channelReleased(Channel channel) throws Exception {
65 | if (LOG.isDebugEnabled()) {
66 | LOG.debug("channel released : {}", channel.toString());
67 | }
68 |
69 | channel.attr(FastdfsHandler.OPERATION_KEY).set(null);
70 | }
71 |
72 | public void channelAcquired(Channel channel) throws Exception {
73 | if (LOG.isDebugEnabled()) {
74 | LOG.debug("channel acquired : {}", channel.toString());
75 | }
76 |
77 | channel.attr(FastdfsHandler.OPERATION_KEY).set(null);
78 | }
79 |
80 | public void channelCreated(Channel channel) throws Exception {
81 | if (LOG.isInfoEnabled()) {
82 | LOG.info("channel created : {}", channel.toString());
83 | }
84 |
85 | ChannelPipeline pipeline = channel.pipeline();
86 | pipeline.addLast(new IdleStateHandler(readTimeout, 0, idleTimeout, TimeUnit.MILLISECONDS));
87 | pipeline.addLast(new ChunkedWriteHandler());
88 | pipeline.addLast(new FastdfsHandler());
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/java/fastdfs/client/codec/FileOperationEncoder.java:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | package fastdfs.client.codec;
5 |
6 | import fastdfs.client.exchange.Requestor;
7 | import io.netty.buffer.ByteBuf;
8 | import io.netty.buffer.ByteBufAllocator;
9 | import io.netty.buffer.CompositeByteBuf;
10 | import io.netty.buffer.Unpooled;
11 | import io.netty.channel.DefaultFileRegion;
12 | import io.netty.handler.stream.ChunkedNioStream;
13 | import io.netty.handler.stream.ChunkedStream;
14 |
15 | import java.io.File;
16 | import java.io.InputStream;
17 | import java.nio.channels.ReadableByteChannel;
18 | import java.util.LinkedList;
19 | import java.util.List;
20 |
21 | import static fastdfs.client.FastdfsConstants.ERRNO_OK;
22 | import static fastdfs.client.FastdfsConstants.FDFS_HEAD_LEN;
23 |
24 | /**
25 | * 抽象文件请求
26 | *
27 | * @author liulongbiao
28 | */
29 | abstract class FileOperationEncoder implements Requestor.Encoder {
30 |
31 | private final Object content;
32 | private final long size;
33 |
34 | FileOperationEncoder(File file) {
35 | long length = file.length();
36 | this.content = new DefaultFileRegion(file, 0, length);
37 | this.size = length;
38 | }
39 |
40 | FileOperationEncoder(Object content, long size) {
41 | this.content = toContent(content);
42 | this.size = size;
43 | }
44 |
45 | private static Object toContent(Object content) {
46 |
47 | if (content instanceof File) {
48 | File file = (File) content;
49 | return new DefaultFileRegion(file, 0, file.length());
50 | }
51 |
52 | if (content instanceof InputStream) {
53 | return new ChunkedStream((InputStream) content);
54 | }
55 |
56 | if (content instanceof ReadableByteChannel) {
57 | return new ChunkedNioStream((ReadableByteChannel) content);
58 | }
59 |
60 | if (content instanceof byte[]) {
61 | return Unpooled.wrappedBuffer((byte[]) content);
62 | }
63 |
64 | throw new IllegalArgumentException("unknown content type : " + content.getClass().getName());
65 | }
66 |
67 | @Override
68 | public List