diff --git a/library/library.iml b/library/library.iml index ac6064470..d72d61f22 100644 --- a/library/library.iml +++ b/library/library.iml @@ -61,13 +61,6 @@ - - - - - - - @@ -75,30 +68,41 @@ + + + + + + + + + + + + + + - - - - - + + \ No newline at end of file diff --git a/library/src/androidTest/java/com/qiniu/android/CancelTest.java b/library/src/androidTest/java/com/qiniu/android/CancelTest.java index 5c732ef11..056f01c32 100644 --- a/library/src/androidTest/java/com/qiniu/android/CancelTest.java +++ b/library/src/androidTest/java/com/qiniu/android/CancelTest.java @@ -44,10 +44,10 @@ public void testFile() throws Throwable { checkTemp(ts, "testFile"); } - public void testData() throws Throwable { - Temp[] ts = new Temp[]{templateData(400, 0.2), templateData(700, 0.2), templateData(1024, 0.51), templateData(4 * 1024 + 785, 0.5), templateData(4 * 1024, 0.5), templateData(8 * 1024, 0.6)}; - checkTemp(ts, "testData"); - } +// public void testData() throws Throwable { +// Temp[] ts = new Temp[]{templateData(400, 0.2), templateData(700, 0.2), templateData(1024, 0.51), templateData(4 * 1024 + 785, 0.5), templateData(4 * 1024, 0.5), templateData(8 * 1024, 0.6)}; +// checkTemp(ts, "testData"); +// } private void checkTemp(Temp[] ts, String type) { int failedCount = 0; diff --git a/library/src/androidTest/java/com/qiniu/android/DnsApiTest.java b/library/src/androidTest/java/com/qiniu/android/DnsApiTest.java new file mode 100644 index 000000000..7404a3453 --- /dev/null +++ b/library/src/androidTest/java/com/qiniu/android/DnsApiTest.java @@ -0,0 +1,215 @@ +package com.qiniu.android; + + +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.qiniu.android.collect.Config; +import com.qiniu.android.common.ZoneInfo; +import com.qiniu.android.http.DnsPrefetcher; +import com.qiniu.android.http.ResponseInfo; +import com.qiniu.android.http.custom.DnsCacheKey; +import com.qiniu.android.storage.Configuration; +import com.qiniu.android.storage.Recorder; +import com.qiniu.android.storage.UpCompletionHandler; +import com.qiniu.android.storage.UpProgressHandler; +import com.qiniu.android.storage.UploadManager; +import com.qiniu.android.storage.UploadOptions; +import com.qiniu.android.storage.persistent.DnsCacheFile; +import com.qiniu.android.utils.AndroidNetwork; +import com.qiniu.android.utils.StringUtils; + +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by jemy on 2019/8/20. + */ + +public class DnsApiTest extends InstrumentationTestCase { + public void testDns() throws Throwable { + List inetAddresses = null; + DnsPrefetcher dnsPrefetcher; +// try { +// inetAddresses = DnsPrefetcher.getDnsBySystem().lookup("upload.qiniup.com"); +// } catch (UnknownHostException e) { +// e.printStackTrace(); +// } + Log.e("qiniutest", "InetAddress: " + inetAddresses.size()); + //超耗时过程 +// for (int i = 0; i < inetAddresses.size(); i++) { +// Log.e("qiniutest", "InetAddress.getCanonicalHostName: " + inetAddresses.get(i).getCanonicalHostName()); +// +// } + for (int i = 0; i < inetAddresses.size(); i++) { + Log.e("qiniutest", "InetAddress.getHostAddress: " + inetAddresses.get(i).getHostAddress()); + } + } + + + public void testQueryDomain() { + ZoneInfo info = null; + + DnsPrefetcher dnsPrefetcher = DnsPrefetcher.getDnsPrefetcher(); + try { + info = dnsPrefetcher.init(TestConfig.uptoken_prefetch).getPreQueryZone(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + if (info == null) { + Log.e("qiniutest: ", "null"); + } + Log.e("qiniutest: ", info.toString()); + Log.e("qiniutest: ", info.upDomainsList.get(0)); + } + + + public void testLocalDomain() { + List info = null; + DnsPrefetcher dnsPrefetcher = DnsPrefetcher.getDnsPrefetcher(); + try { + info = dnsPrefetcher.init(TestConfig.uptoken_prefetch).getLocalZone(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + if (info == null) { + Log.e("qiniutest: ", "null"); + } + for (int i = 0; i < info.size(); i++) { + Log.e("qiniutest: ", info.get(i).toString()); + } + + } + + + public void testLocalIp() { + String s = AndroidNetwork.getHostIP(); + Log.e("qiniutest", s); + } + + public void testDnsPreAndcache() { + Configuration config = new Configuration.Builder().build(); + boolean needPrefetch = DnsPrefetcher.checkRePrefetchDns(TestConfig.uptoken_prefetch, config); + Log.e("qiniutest", "check:" + needPrefetch); + if (needPrefetch) { + DnsPrefetcher.startPrefetchDns(TestConfig.uptoken_prefetch, config); + } else { + testRecoverCache(); + return; + } + //预取或者recover success + List list = DnsPrefetcher.getDnsPrefetcher().getHosts(); + ConcurrentHashMap> map = DnsPrefetcher.getDnsPrefetcher().getConcurrentHashMap(); + Log.e("qiniutest: ", "list size: " + list.size()); + for (String s : list) { + Log.e("qiniutest: ", "uphost: " + s); + List list1 = map.get(s); + for (InetAddress inetAddress : + list1) { + Log.e("qiniutest: ", "ip: " + inetAddress.getHostAddress()); + } + } + + } + + //test recover + public void testRecoverCache() { + + Recorder recorder = null; + try { + recorder = new DnsCacheFile(Config.dnscacheDir); + } catch (IOException e) { + e.printStackTrace(); + } + String fileName = recorder.getFileName(); + if (fileName == null) { + Log.e("qiniutest: ", "recover file is null "); + return; + } + byte[] data = recorder.get(recorder.getFileName()); + if (data == null) { + Log.e("qiniutest: ", "recover data is null "); + return; + } + DnsPrefetcher.recoverDnsCache(data); + + + ConcurrentHashMap> map1 = DnsPrefetcher.getDnsPrefetcher().getConcurrentHashMap(); + List list = DnsPrefetcher.getDnsPrefetcher().getHosts(); + Log.e("qiniutest: ", "size for cache: " + list.size()); + for (String s : list) { + Log.e("qiniutest: ", "uphost for cache: " + s); + List list1 = map1.get(s); + for (InetAddress inetAddress : + list1) { + Log.e("qiniutest: ", "ip for cache: " + inetAddress.getHostAddress()); + } + } + } + + int time = 0; + final Object lock = new Object(); + + public void testAtomic() { + final int size = 6 * 1024; + for (int i = 0; i < 3; i++) { + new Thread(new Runnable() { + @Override + public void run() { + try { + Configuration config = new Configuration.Builder().build(); + final UploadManager uploadManager = new UploadManager(config, 3); + final String expectKey = "r=" + size + "k"; + final File f; + f = TempFile.createFile(size); + final UploadOptions uploadoption = new UploadOptions(null, null, false, new UpProgressHandler() { + public void progress(String key, double percent) { + Log.e("qiniutest", percent + ""); + } + }, null); + + uploadManager.put(f, expectKey, TestConfig.token_z0, new UpCompletionHandler() { + public void complete(String k, ResponseInfo rinfo, JSONObject response) { + Log.e("qiniutest", k + rinfo); + time += 1; + if (time == 3) { + lock.notify(); + } + } + }, uploadoption); + + + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + synchronized (lock) { + try { + lock.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + public void testSerializable() { + DnsCacheKey key = new DnsCacheKey("12321", "127.0.0.1", "akscope"); + Log.e("qiniutest", key.toString()); + DnsCacheKey key1 = DnsCacheKey.toCacheKey(key.toString()); + if (key1 == null) { + return; + } + Log.e("qiniutest", key1.getCurrentTime() + ":" + key1.getLocalIp() + ":" + key1.getAkScope()); + + } + + +} diff --git a/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java b/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java index b78cf60c4..e70791bce 100644 --- a/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java +++ b/library/src/androidTest/java/com/qiniu/android/ResumeUploadTest.java @@ -9,10 +9,12 @@ import com.qiniu.android.common.Zone; import com.qiniu.android.http.ResponseInfo; import com.qiniu.android.storage.Configuration; +import com.qiniu.android.storage.Recorder; import com.qiniu.android.storage.UpCompletionHandler; import com.qiniu.android.storage.UpProgressHandler; import com.qiniu.android.storage.UploadManager; import com.qiniu.android.storage.UploadOptions; +import com.qiniu.android.storage.persistent.FileRecorder; import junit.framework.Assert; @@ -83,7 +85,7 @@ public void progress(String key, double percent) { public void setUp() throws Exception { Configuration config = new Configuration.Builder().build(); - uploadManager = new UploadManager(config); + uploadManager = new UploadManager(config, 3); ACollectUploadInfoTest.testInit(); } @@ -120,7 +122,7 @@ public void complete(String k, ResponseInfo rinfo, JSONObject response) { // Assert.assertNotNull(resp); // String hash = resp.getString("hash"); // Assert.assertEquals(hash, Etag.file(f)); - TempFile.remove(f); + //TempFile.remove(f); // Assert.assertTrue("进度有变化,不大可能一直相同。" + getProgress(), !isProgressAllSame()); Log.d(TAG, getProgress()); ACollectUploadInfoTest.recordFileTest(); @@ -189,12 +191,12 @@ public void test4M1k2() throws Throwable { } @LargeTest - public void test4M() throws Throwable { - template(1024 * 4); + public void testResumeUploadFast() throws Throwable { + template(1024 * 8); } // @LargeTest // public void test8M1k() throws Throwable{ // template(1024*8+1); // } -} +} \ No newline at end of file diff --git a/library/src/androidTest/java/com/qiniu/android/TestConfig.java b/library/src/androidTest/java/com/qiniu/android/TestConfig.java index 2deaa2dac..b9509410c 100644 --- a/library/src/androidTest/java/com/qiniu/android/TestConfig.java +++ b/library/src/androidTest/java/com/qiniu/android/TestConfig.java @@ -9,7 +9,7 @@ public final class TestConfig { //华东上传凭证 public static final String bucket_z0 = "sdk-z0"; - public static final String token_z0 = "QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm:KUw9Fxq8NoLTeZNsKkhTjoKpsuQ=:eyJzY29wZSI6InNkay16MCIsInJldHVybmJvZHkiOiJ7XCJoYXNoXCI6XCIkKGV0YWcpXCIsXCJrZXlcIjpcIiQoa2V5KVwiLFwiZm5hbWVcIjpcIiAkKGZuYW1lKSBcIixcImZzaXplXCI6XCIkKGZzaXplKVwiLFwibWltZVR5cGVcIjpcIiQobWltZVR5cGUpXCIsXCJmb29cIjpcIiQoeDpmb28pXCIsXCJiYXJcIjpcIiQoeDpiYXIpXCJ9IiwiZGVhZGxpbmUiOjQyOTQ5NjcyOTV9"; + public static final String token_z0 = "MP_Ebql_lSsUrDr7WrXn_5vKocQDLvTPCNEFeVmp:zfCfgMtCzO8l8iab_lbm402wZY8=:eyJzY29wZSI6ImFuZHJvaWR0ZXN0IiwiZGVhZGxpbmUiOjE1Njk4NTgzMTl9"; //华北上传凭证 public static final String bucket_z1 = "sdk-z1"; public static final String token_z1 = "QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm:ILIWwUZ7_hvZeKJzbBKvpo8DpYc=:eyJzY29wZSI6InNkay16MSIsInJldHVybmJvZHkiOiJ7XCJoYXNoXCI6XCIkKGV0YWcpXCIsXCJrZXlcIjpcIiQoa2V5KVwiLFwiZm5hbWVcIjpcIiAkKGZuYW1lKSBcIixcImZzaXplXCI6XCIkKGZzaXplKVwiLFwibWltZVR5cGVcIjpcIiQobWltZVR5cGUpXCIsXCJmb29cIjpcIiQoeDpmb28pXCIsXCJiYXJcIjpcIiQoeDpiYXIpXCJ9IiwiZGVhZGxpbmUiOjQyOTQ5NjcyOTV9"; @@ -21,6 +21,9 @@ public final class TestConfig { public static final String token_na0 = "QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm:QxnYkUaYIw4fIsI133_tjFJ_03M=:eyJzY29wZSI6InNkay1uYTAiLCJyZXR1cm5ib2R5Ijoie1wiaGFzaFwiOlwiJChldGFnKVwiLFwia2V5XCI6XCIkKGtleSlcIixcImZuYW1lXCI6XCIgJChmbmFtZSkgXCIsXCJmc2l6ZVwiOlwiJChmc2l6ZSlcIixcIm1pbWVUeXBlXCI6XCIkKG1pbWVUeXBlKVwiLFwiZm9vXCI6XCIkKHg6Zm9vKVwiLFwiYmFyXCI6XCIkKHg6YmFyKVwifSIsImRlYWRsaW5lIjo0Mjk0OTY3Mjk1fQ=="; public static final String ak = "QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm"; + //dns prefetch token + public static final String uptoken_prefetch = "MP_Ebql_lSsUrDr7WrXn_5vKocQDLvTPCNEFeVmp:3KJpXCGMqm6EAYU71RF1HDmQrcE=:eyJzY29wZSI6ImFuZHJvaWR0ZXN0IiwiZGVhZGxpbmUiOjE1Njc0OTAxODF9"; + /** * 华东机房 */ diff --git a/library/src/androidTest/java/com/qiniu/android/common/AutoZoneTest.java b/library/src/androidTest/java/com/qiniu/android/common/AutoZoneTest.java index 065400496..520853c28 100644 --- a/library/src/androidTest/java/com/qiniu/android/common/AutoZoneTest.java +++ b/library/src/androidTest/java/com/qiniu/android/common/AutoZoneTest.java @@ -1,6 +1,7 @@ package com.qiniu.android.common; import android.test.AndroidTestCase; +import android.util.Log; import com.qiniu.android.TestConfig; @@ -76,10 +77,11 @@ public void onFailure(int reason) { e.printStackTrace(); } ZoneInfo info = autoZone.zoneInfo(ak, bkt); -// Log.d("zone0: ", info.toString()); + Log.d("qiniutest: ", info.toString()); ZoneInfo info2 = autoZone.zoneInfo(ak, bkt); Assert.assertSame(info, info2); } + } diff --git a/library/src/main/java/com/qiniu/android/collect/Config.java b/library/src/main/java/com/qiniu/android/collect/Config.java index 0e2707f76..8917a8405 100644 --- a/library/src/main/java/com/qiniu/android/collect/Config.java +++ b/library/src/main/java/com/qiniu/android/collect/Config.java @@ -71,6 +71,18 @@ public final class Config { */ public static int interval = 10; + /** + * dns缓存信息目录 + */ + public static String dnscacheDir = "/sdcard/dnschache"; + + /** + * preQuery host + */ + public static String preQueryHost = "uc.qbox.me"; + + public static int rePreHost = 2; + /** * 当网络切换到 wifi 下,切换到此设置 */ diff --git a/library/src/main/java/com/qiniu/android/common/AutoZone.java b/library/src/main/java/com/qiniu/android/common/AutoZone.java index ab0f120dc..fccc22d92 100644 --- a/library/src/main/java/com/qiniu/android/common/AutoZone.java +++ b/library/src/main/java/com/qiniu/android/common/AutoZone.java @@ -21,19 +21,34 @@ public final class AutoZone extends Zone { /** * 自动判断机房 */ - public static final AutoZone autoZone = new AutoZone(); - private final String ucServer; + private String ucServer; private Map zones = new ConcurrentHashMap<>(); private Client client = new Client(); + /** + * default useHttps to req autoZone + */ public AutoZone() { - this("https://uc.qbox.me"); + this(true); } - AutoZone(String ucServer) { + public AutoZone(boolean useHttps) { + if (useHttps) { + this.ucServer = "https://uc.qbox.me"; + } else { + this.ucServer = "http://uc.qbox.me"; + } + } + + //私有云可能改变ucServer + public void setUcServer(String ucServer) { this.ucServer = ucServer; } + public String getUcServer() { + return this.ucServer; + } + private void getZoneJsonAsync(ZoneIndex index, CompletionHandler handler) { String address = ucServer + "/v2/query?ak=" + index.accessKey + "&bucket=" + index.bucket; client.asyncGet(address, null, UpToken.NULL, handler); @@ -121,6 +136,7 @@ boolean preQueryIndex(final ZoneIndex index) { return success; } + @Override public synchronized String upHost(String token, boolean useHttps, String frozenDomain) { ZoneInfo info = queryByToken(token); diff --git a/library/src/main/java/com/qiniu/android/common/Constants.java b/library/src/main/java/com/qiniu/android/common/Constants.java index 55ec083dd..35ff24b6f 100644 --- a/library/src/main/java/com/qiniu/android/common/Constants.java +++ b/library/src/main/java/com/qiniu/android/common/Constants.java @@ -2,7 +2,7 @@ public final class Constants { - public static final String VERSION = "7.3.15"; + public static final String VERSION = "7.4.0"; public static final String UTF_8 = "utf-8"; } diff --git a/library/src/main/java/com/qiniu/android/common/FixedZone.java b/library/src/main/java/com/qiniu/android/common/FixedZone.java index 9d5e62162..6c89201c1 100644 --- a/library/src/main/java/com/qiniu/android/common/FixedZone.java +++ b/library/src/main/java/com/qiniu/android/common/FixedZone.java @@ -4,6 +4,7 @@ import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -16,49 +17,64 @@ public final class FixedZone extends Zone { /** * 华东机房 */ - public static final Zone zone0 = new FixedZone(new String[]{ + static String[] arrayzone0 = new String[]{ "upload.qiniup.com", "upload-jjh.qiniup.com", "upload-xs.qiniup.com", "up.qiniup.com", "up-jjh.qiniup.com", "up-xs.qiniup.com", "upload.qbox.me", "up.qbox.me" - }); + }; + public static final Zone zone0 = new FixedZone(arrayzone0); /** * 华北机房 */ - public static final Zone zone1 = new FixedZone(new String[]{ + static String[] arrayzone1 = new String[]{ "upload-z1.qiniup.com", "up-z1.qiniup.com", "upload-z1.qbox.me", "up-z1.qbox.me" - }); + }; + public static final Zone zone1 = new FixedZone(arrayzone1); /** * 华南机房 */ - public static final Zone zone2 = new FixedZone(new String[]{ + static String[] arrayzone2 = new String[]{ "upload-z2.qiniup.com", "upload-dg.qiniup.com", "upload-fs.qiniup.com", "up-z2.qiniup.com", "up-dg.qiniup.com", "up-fs.qiniup.com", "upload-z2.qbox.me", "up-z2.qbox.me" - }); + }; + public static final Zone zone2 = new FixedZone(arrayzone2); /** * 北美机房 */ - public static final Zone zoneNa0 = new FixedZone(new String[]{ + static String[] arrayzoneNa0 = new String[]{ "upload-na0.qiniup.com", "up-na0.qiniup.com", "upload-na0.qbox.me", "up-na0.qbox.me" - }); + }; + public static final Zone zoneNa0 = new FixedZone(arrayzoneNa0); /** * 新加坡机房 */ - public static final Zone zoneAs0 = new FixedZone(new String[]{ + static String[] arrayZoneAs0 = new String[]{ "upload-as0.qiniup.com", "up-as0.qiniup.com", "upload-as0.qbox.me", "up-as0.qbox.me" - }); + }; + public static final Zone zoneAs0 = new FixedZone(arrayZoneAs0); private ZoneInfo zoneInfo; + public static List getZoneInfos() { + List listZoneInfo = new ArrayList(); + listZoneInfo.add(createZoneInfo(arrayzone0)); + listZoneInfo.add(createZoneInfo(arrayzone1)); + listZoneInfo.add(createZoneInfo(arrayzone2)); + listZoneInfo.add(createZoneInfo(arrayzoneNa0)); + listZoneInfo.add(createZoneInfo(arrayZoneAs0)); + return listZoneInfo; + } + public FixedZone(ZoneInfo zoneInfo) { this.zoneInfo = zoneInfo; } @@ -77,6 +93,7 @@ public static ZoneInfo createZoneInfo(String[] upDomains) { return new ZoneInfo(0, upDomainsList, upDomainsMap); } + @Override public synchronized String upHost(String upToken, boolean useHttps, String frozenDomain) { String upHost = this.upHost(this.zoneInfo, useHttps, frozenDomain); diff --git a/library/src/main/java/com/qiniu/android/http/Client.java b/library/src/main/java/com/qiniu/android/http/Client.java index a2d4122a6..b56eab396 100644 --- a/library/src/main/java/com/qiniu/android/http/Client.java +++ b/library/src/main/java/com/qiniu/android/http/Client.java @@ -27,8 +27,6 @@ import static com.qiniu.android.http.ResponseInfo.NetworkError; -//import okhttp3.MultipartBody; - /** * Created by bailong on 15/11/12. */ @@ -47,26 +45,23 @@ public Client() { public Client(ProxyConfiguration proxy, int connectTimeout, int responseTimeout, UrlConverter converter, final Dns dns) { this.converter = converter; OkHttpClient.Builder builder = new OkHttpClient.Builder(); - if (proxy != null) { builder.proxy(proxy.proxy()); if (proxy.user != null && proxy.password != null) { builder.proxyAuthenticator(proxy.authenticator()); } } - if (dns != null) { - builder.dns(new okhttp3.Dns() { - @Override - public List lookup(String hostname) throws UnknownHostException { - try { - return dns.lookup(hostname); - } catch (Exception e) { - e.printStackTrace(); - } - return okhttp3.Dns.SYSTEM.lookup(hostname); + + builder.dns(new okhttp3.Dns() { + @Override + public List lookup(String hostname) throws UnknownHostException { + if (DnsPrefetcher.getDnsPrefetcher().getInetAddressByHost(hostname) != null) { + return DnsPrefetcher.getDnsPrefetcher().getInetAddressByHost(hostname); } - }); - } + return okhttp3.Dns.SYSTEM.lookup(hostname); + } + }); + builder.networkInterceptors().add(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { diff --git a/library/src/main/java/com/qiniu/android/http/DnsPrefetcher.java b/library/src/main/java/com/qiniu/android/http/DnsPrefetcher.java new file mode 100644 index 000000000..f8e0294e1 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/http/DnsPrefetcher.java @@ -0,0 +1,345 @@ +package com.qiniu.android.http; + +import com.qiniu.android.collect.Config; +import com.qiniu.android.common.Constants; +import com.qiniu.android.common.FixedZone; +import com.qiniu.android.common.ZoneInfo; +import com.qiniu.android.http.custom.DnsCacheKey; +import com.qiniu.android.storage.Configuration; +import com.qiniu.android.storage.Recorder; +import com.qiniu.android.storage.persistent.DnsCacheFile; +import com.qiniu.android.utils.AndroidNetwork; +import com.qiniu.android.utils.StringUtils; +import com.qiniu.android.utils.UrlSafeBase64; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + *

+ * Created by jemy on 2019/8/20. + */ + +public class DnsPrefetcher { + + public static DnsPrefetcher dnsPrefetcher = null; + private static String token; + + private static ConcurrentHashMap> mConcurrentHashMap = new ConcurrentHashMap>(); + private static List mHosts = new ArrayList(); + + private DnsPrefetcher() { + + } + + public static DnsPrefetcher getDnsPrefetcher() { + if (dnsPrefetcher == null) { + synchronized (DnsPrefetcher.class) { + if (dnsPrefetcher == null) { + dnsPrefetcher = new DnsPrefetcher(); + } + } + } + return dnsPrefetcher; + } + + public DnsPrefetcher init(String token) throws UnknownHostException { + this.token = token; + preHosts(); + preFetch(); + return this; + } + + public void setConcurrentHashMap(ConcurrentHashMap> mConcurrentHashMap) { + this.mConcurrentHashMap = mConcurrentHashMap; + } + + //use for test + public List getHosts() { + return this.mHosts; + } + + public void setHosts(List mHosts) { + this.mHosts = mHosts; + } + + //use for test + public ConcurrentHashMap> getConcurrentHashMap() { + return this.mConcurrentHashMap; + } + + //use for test + public void setToken(String token) { + this.token = token; + } + + public List getInetAddressByHost(String host) { + return mConcurrentHashMap.get(host); + } + + private void preHosts() { + HashSet set = new HashSet(); + + //preQuery sync + ZoneInfo zoneInfo = getPreQueryZone(); + if (zoneInfo != null) { + for (String host : zoneInfo.upDomainsList) { + if (set.add(host)) + mHosts.add(host); + } + } + //local + List listZoneinfo = getLocalZone(); + for (ZoneInfo zone : listZoneinfo) { + for (String host : zone.upDomainsList) { + if (set.add(host)) + mHosts.add(host); + } + } + if (set.add(Config.preQueryHost)) + mHosts.add(Config.preQueryHost); + } + + + private void preFetch() { + List rePreHosts = new ArrayList(); + for (String host : mHosts) { + List inetAddresses = null; + try { + inetAddresses = okhttp3.Dns.SYSTEM.lookup(host); + mConcurrentHashMap.put(host, inetAddresses); + } catch (UnknownHostException e) { + e.printStackTrace(); + rePreHosts.add(host); + } + } + rePreFetch(rePreHosts, null); + } + + /** + * 对hosts预取失败对进行重新预取,deafult retryNum = 2 + * + * @param rePreHosts 用于重试的hosts + * @param customeDns 是否自定义dns + */ + private void rePreFetch(List rePreHosts, Dns customeDns) { + for (String host : rePreHosts) { + int rePreNum = 0; + while (rePreNum < Config.rePreHost) { + rePreNum += 1; + if (rePreFetch(host, customeDns)) + break; + } + } + } + + private boolean rePreFetch(String host, Dns customeDns) { + List inetAddresses = null; + try { + if (customeDns == null) { + inetAddresses = okhttp3.Dns.SYSTEM.lookup(host); + } else { + inetAddresses = customeDns.lookup(host); + } + mConcurrentHashMap.put(host, inetAddresses); + return true; + } catch (UnknownHostException e) { + e.printStackTrace(); + return false; + } + } + + /** + * 自定义dns预取 + * + * @param dns + * @return + * @throws UnknownHostException + */ + public void dnsPreByCustom(Dns dns) { + List rePreHosts = new ArrayList(); + for (String host : mHosts) { + List inetAddresses = null; + try { + inetAddresses = dns.lookup(host); + mConcurrentHashMap.put(host, inetAddresses); + } catch (UnknownHostException e) { + e.printStackTrace(); + rePreHosts.add(host); + } + } + rePreFetch(rePreHosts, dns); + } + + /** + * look local host + */ + public List getLocalZone() { + List listZoneInfo = FixedZone.getZoneInfos(); + return listZoneInfo; + } + + + /** + * query host sync + */ + public ZoneInfo getPreQueryZone() { + DnsPrefetcher.ZoneIndex index = DnsPrefetcher.ZoneIndex.getFromToken(token); + ZoneInfo zoneInfo = preQueryIndex(index); + return zoneInfo; + } + + ZoneInfo preQueryIndex(DnsPrefetcher.ZoneIndex index) { + ZoneInfo info = null; + try { + ResponseInfo responseInfo = getZoneJsonSync(index); + info = ZoneInfo.buildFromJson(responseInfo.response); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + return info; + } + + ResponseInfo getZoneJsonSync(DnsPrefetcher.ZoneIndex index) { + Client client = new Client(); + String address = "http://" + Config.preQueryHost + "/v2/query?ak=" + index.accessKey + "&bucket=" + index.bucket; + return client.syncGet(address, null); + } + + + static class ZoneIndex { + final String accessKey; + final String bucket; + + ZoneIndex(String accessKey, String bucket) { + this.accessKey = accessKey; + this.bucket = bucket; + } + + static DnsPrefetcher.ZoneIndex getFromToken(String token) { + String[] strings = token.split(":"); + String ak = strings[0]; + String policy = null; + try { + policy = new String(UrlSafeBase64.decode(strings[2]), Constants.UTF_8); + JSONObject obj = new JSONObject(policy); + String scope = obj.getString("scope"); + String bkt = scope.split(":")[0]; + return new DnsPrefetcher.ZoneIndex(ak, bkt); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public int hashCode() { + return accessKey.hashCode() * 37 + bucket.hashCode(); + } + + public boolean equals(Object obj) { + return obj == this || !(obj == null || !(obj instanceof DnsPrefetcher.ZoneIndex)) + && ((DnsPrefetcher.ZoneIndex) obj).accessKey.equals(accessKey) && ((DnsPrefetcher.ZoneIndex) obj).bucket.equals(bucket); + } + } + + /** + *

+ * ip changed, the network has changed + * ak:scope变化,prequery(v2)自动获取域名接口发生变化,存储区域可能变化 + * cacheTime>config.cacheTime(默认24H) + *

+ * + * @return true:重新预期并缓存, false:不需要重新预取和缓存 + */ + public static boolean checkRePrefetchDns(String token, Configuration config) { + Recorder recorder = null; + try { + recorder = new DnsCacheFile(Config.dnscacheDir); + } catch (IOException e) { + e.printStackTrace(); + return true; + } + String dnscache = recorder.getFileName(); + if (dnscache == null) + return true; + + byte[] data = recorder.get(dnscache); + if (data == null) + return true; + + DnsCacheKey cacheKey = DnsCacheKey.toCacheKey(dnscache); + if (cacheKey == null) + return true; + + String currentTime = String.valueOf(System.currentTimeMillis()); + String localip = AndroidNetwork.getHostIP(); + String akScope = StringUtils.getAkAndScope(token); + + long cacheTime = (Long.parseLong(currentTime) - Long.parseLong(cacheKey.getCurrentTime())) / 1000; + if (!localip.equals(cacheKey.getLocalIp()) || cacheTime > config.dnsCacheTimeMs || !akScope.equals(cacheKey.getAkScope())) { + return true; + } + + return recoverDnsCache(data); + } + + /** + * start preFetchDns: Time-consuming operation, in a thread + * + * @param token + */ + public static void startPrefetchDns(String token, Configuration config) { + String currentTime = String.valueOf(System.currentTimeMillis()); + String localip = AndroidNetwork.getHostIP(); + String akScope = StringUtils.getAkAndScope(token); + String cacheKey = new DnsCacheKey(currentTime, localip, akScope).toString(); + Recorder recorder = null; + DnsPrefetcher dnsPrefetcher = null; + try { + recorder = new DnsCacheFile(Config.dnscacheDir); + dnsPrefetcher = DnsPrefetcher.getDnsPrefetcher().init(token); + } catch (IOException e) { + e.printStackTrace(); + } + if (config.dns != null) { + DnsPrefetcher.getDnsPrefetcher().dnsPreByCustom(config.dns); + } + ConcurrentHashMap> concurrentHashMap = dnsPrefetcher.getConcurrentHashMap(); + byte[] dnscache = StringUtils.toByteArray(concurrentHashMap); + + recorder.set(cacheKey, dnscache); + } + + /** + * @param data + * @return + */ + public static boolean recoverDnsCache(byte[] data) { + ConcurrentHashMap> concurrentHashMap = (ConcurrentHashMap>) StringUtils.toObject(data); + if (concurrentHashMap == null) { + return true; + } + DnsPrefetcher.getDnsPrefetcher().setConcurrentHashMap(concurrentHashMap); + + ArrayList list = new ArrayList(); + Iterator iter = concurrentHashMap.keySet().iterator(); + while (iter.hasNext()) { + String tmpkey = (String) iter.next(); + if (tmpkey == null || tmpkey.length() == 0) + continue; + list.add(tmpkey); + } + DnsPrefetcher.getDnsPrefetcher().setHosts(list); + return false; + } +} diff --git a/library/src/main/java/com/qiniu/android/http/ResponseInfo.java b/library/src/main/java/com/qiniu/android/http/ResponseInfo.java index 47d738ff8..aad0e258d 100644 --- a/library/src/main/java/com/qiniu/android/http/ResponseInfo.java +++ b/library/src/main/java/com/qiniu/android/http/ResponseInfo.java @@ -200,7 +200,8 @@ public static ResponseInfo fileError(Exception e, final UpToken upToken) { } public static ResponseInfo networkError(int code, UpToken upToken) { - return create(null, code, "", "", "", "", "", "", 80, 0, 0, "Network error during preQuery", upToken, 0); + return create(null, code, "", "", "", "", "", "", 80, 0, 0, "Network error during preQuery, Please check your network or " + + "use http try again", upToken, 0); } public static boolean isStatusCodeForBrokenNetwork(int code) { diff --git a/library/src/main/java/com/qiniu/android/http/custom/DnsCacheKey.java b/library/src/main/java/com/qiniu/android/http/custom/DnsCacheKey.java new file mode 100644 index 000000000..b148a462a --- /dev/null +++ b/library/src/main/java/com/qiniu/android/http/custom/DnsCacheKey.java @@ -0,0 +1,65 @@ +package com.qiniu.android.http.custom; + + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by jemy on 2019/9/23. + */ + +public class DnsCacheKey { + public String currentTime; + public String localIp; + public String akScope; + + public DnsCacheKey() { + + } + + public DnsCacheKey(String currentTime, String localIp, String akScope) { + this.currentTime = currentTime; + this.localIp = localIp; + this.akScope = akScope; + } + + public String getCurrentTime() { + return currentTime; + } + + public String getAkScope() { + return akScope; + } + + public String getLocalIp() { + return localIp; + } + + public void setAkScope(String akScope) { + this.akScope = akScope; + } + + public void setCurrentTime(String currentTime) { + this.currentTime = currentTime; + } + + public void setLocalIp(String localIp) { + this.localIp = localIp; + } + + public static DnsCacheKey toCacheKey(String key) { + try { + JSONObject object = new JSONObject(key); + return new DnsCacheKey(object.getString("currentTime"), object.getString("localIp"), object.getString("akScope")); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + } + + @Override + public String toString() { + return "{\"currentTime\":\"" + currentTime + "\", \"localIp\":\"" + localIp + "\", \"akScope\":\"" + akScope + "\"}"; + + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/Configuration.java b/library/src/main/java/com/qiniu/android/storage/Configuration.java index 0e8e3734b..1c65c2496 100644 --- a/library/src/main/java/com/qiniu/android/storage/Configuration.java +++ b/library/src/main/java/com/qiniu/android/storage/Configuration.java @@ -65,6 +65,11 @@ public final class Configuration { */ public boolean useHttps; + /** + * dns预取缓存时间 + */ + public long dnsCacheTimeMs; + private Configuration(Builder builder) { useHttps = builder.useHttps; @@ -82,9 +87,14 @@ private Configuration(Builder builder) { proxy = builder.proxy; + dnsCacheTimeMs = builder.dnsCacheTimeMs; + urlConverter = builder.urlConverter; + AutoZone autoZone = null; - zone = builder.zone == null ? AutoZone.autoZone : builder.zone; + autoZone = new AutoZone(builder.useHttps); + + zone = builder.zone == null ? autoZone : builder.zone; dns = builder.dns; } @@ -114,6 +124,7 @@ public static class Builder { private int retryMax = 3; private UrlConverter urlConverter = null; private Dns dns = null; + private long dnsCacheTimeMs = 60 * 60 * 24 * 1000; public Builder zone(Zone zone) { this.zone = zone; @@ -176,6 +187,11 @@ public Builder useHttps(boolean useHttps) { return this; } + public Builder dnsCacheTimeMs(long dnsCacheTimeMs) { + this.dnsCacheTimeMs = dnsCacheTimeMs; + return this; + } + public Configuration build() { return new Configuration(this); } diff --git a/library/src/main/java/com/qiniu/android/storage/Recorder.java b/library/src/main/java/com/qiniu/android/storage/Recorder.java index 493a708cb..9bd218cb2 100644 --- a/library/src/main/java/com/qiniu/android/storage/Recorder.java +++ b/library/src/main/java/com/qiniu/android/storage/Recorder.java @@ -27,4 +27,6 @@ public interface Recorder { * @param key 持久化的键 */ void del(String key); + + String getFileName(); } diff --git a/library/src/main/java/com/qiniu/android/storage/ResumeUploaderFast.java b/library/src/main/java/com/qiniu/android/storage/ResumeUploaderFast.java new file mode 100644 index 000000000..40108f04c --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/ResumeUploaderFast.java @@ -0,0 +1,564 @@ +package com.qiniu.android.storage; + +import com.qiniu.android.http.Client; +import com.qiniu.android.http.CompletionHandler; +import com.qiniu.android.http.ProgressHandler; +import com.qiniu.android.http.ResponseInfo; +import com.qiniu.android.utils.AndroidNetwork; +import com.qiniu.android.utils.Crc32; +import com.qiniu.android.utils.StringMap; +import com.qiniu.android.utils.StringUtils; +import com.qiniu.android.utils.UrlSafeBase64; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static java.lang.String.format; + +/** + * Created by jemy on 2019/7/9. + */ + +public class ResumeUploaderFast implements Runnable { + + /** + * 文件总大小 + */ + private final long totalSize; + + private final String key; + + private final UpCompletionHandler completionHandler; + + private final UploadOptions options; + + private final Client client; + + private final Configuration config; + /** + * 保存每一块上传返回的ctx + */ + private final String[] contexts; + /** + * headers + */ + private final StringMap headers; + /** + * 修改时间 + */ + private final long modifyTime; + /** + * 断点续传记录文件 + */ + private final String recorderKey; + /** + * 文件对象 + */ + private RandomAccessFile file; + /** + * 待上传文件 + */ + private File f; + /** + * 上传token + */ + private UpToken token; + /** + * 分块数据 + */ + private Map blockInfo; + /** + * 上传域名 + */ + AtomicReference upHost = new AtomicReference(); + /** + * 总块数 + */ + AtomicInteger tblock; + /** + * 重传域名数 + */ + AtomicInteger retried = new AtomicInteger(0); + /** + * 单域名检测次数 + */ + AtomicInteger singleDomainRetry = new AtomicInteger(0); + /** + * 线程数量 + */ + private int multithread; + /** + * 第一个任务 + */ + private boolean isFirstTask = true; + /** + * 每块偏移位子 + * use 断点续传 + */ + private Long[] offsets; + /** + * 已上传块 + */ + private int upBlock = 0; + private final int domainRetry = 3; + /** + * 避免多个任务同时回调 + */ + private boolean isInterrupted = false; + + ResumeUploaderFast(Client client, Configuration config, File f, String key, UpToken token, + final UpCompletionHandler completionHandler, UploadOptions options, String recorderKey, int multithread) { + this.client = client; + this.config = config; + this.f = f; + this.recorderKey = recorderKey; + this.totalSize = f.length(); + this.key = key; + this.headers = new StringMap().put("Authorization", "UpToken " + token.token); + this.file = null; + this.multithread = multithread; + this.completionHandler = new UpCompletionHandler() { + @Override + public void complete(String key, ResponseInfo info, JSONObject response) { + if (file != null) { + try { + file.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + //第一个回调出来之后通知其他线程停止这个事件的回调 + synchronized (this) { + if (!isInterrupted) { + isInterrupted = true; + completionHandler.complete(key, info, response); + } else { + return; + } + } + } + }; + this.options = options != null ? options : UploadOptions.defaultOptions(); + tblock = new AtomicInteger((int) (totalSize + Configuration.BLOCK_SIZE - 1) / Configuration.BLOCK_SIZE); + this.offsets = new Long[tblock.get()]; + contexts = new String[tblock.get()]; + modifyTime = f.lastModified(); + this.token = token; + this.blockInfo = new LinkedHashMap<>(); + } + + @Override + public void run() { + try { + file = new RandomAccessFile(f, "r"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + completionHandler.complete(key, ResponseInfo.fileError(e, token), null); + return; + } + putBlockInfo(); + + upHost.set(config.zone.upHost(token.token, config.useHttps, null)); + if (blockInfo.size() < multithread) { + multithread = blockInfo.size(); + } + for (int i = 0; i < multithread; i++) { + BlockElement mblock = getBlockInfo(); + new UploadThread(mblock.getOffset(), mblock.getBlocksize(), upHost.get().toString()).start(); + } + } + + + /** + * cut file to blockInfo + */ + private void putBlockInfo() { + Long[] offs = recoveryFromRecord(); + int lastBlock = tblock.get() - 1; + if (offs == null) { + for (int i = 0; i < lastBlock; i++) { + blockInfo.put((long) i * Configuration.BLOCK_SIZE, Configuration.BLOCK_SIZE); + } + blockInfo.put((long) lastBlock * Configuration.BLOCK_SIZE, (int) (totalSize - lastBlock * Configuration.BLOCK_SIZE)); + } else { + HashSet set = new HashSet(Arrays.asList(offs)); + for (int i = 0; i < lastBlock; i++) { + Long offset = (long) i * Configuration.BLOCK_SIZE; + if (!set.contains(offset)) { + blockInfo.put(offset, Configuration.BLOCK_SIZE); + } else { + offsets[i] = offset; + upBlock += 1; + } + } + Long offset = (long) lastBlock * Configuration.BLOCK_SIZE; + if (!set.contains(offset)) { + blockInfo.put(offset, (int) (totalSize - lastBlock * Configuration.BLOCK_SIZE)); + } else { + offsets[lastBlock] = offset; + upBlock += 1; + } + } + } + + /** + * get next block use to upload + * + * @return BlockElement + */ + private synchronized BlockElement getBlockInfo() { + Iterator> it = blockInfo.entrySet().iterator(); + long offset = 0; + int blockSize = 0; + if (it.hasNext()) { + Map.Entry entry = it.next(); + offset = entry.getKey(); + blockSize = entry.getValue(); + blockInfo.remove(offset); + } + return new BlockElement(offset, blockSize); + } + + class UploadThread extends Thread { + private long offset; + private int blockSize; + private String upHost; + + UploadThread(long offset, int blockSize, String upHost) { + this.offset = offset; + this.blockSize = blockSize; + this.upHost = upHost; + } + + @Override + public void run() { + super.run(); + mkblk(offset, blockSize, upHost); + } + } + + /** + * 创建块,并上传内容 + * + * @param upHost 上传主机 + * @param offset 本地文件偏移量 + * @param blockSize 分块的块大小 + */ + private void mkblk(long offset, int blockSize, String upHost) { + String path = format(Locale.ENGLISH, "/mkblk/%d", blockSize); + byte[] chunkBuffer = new byte[blockSize]; + synchronized (this) { + try { + //多线程时file.read可能会在seek时被篡改,导致上传buffer紊乱 + file.seek(offset); + file.read(chunkBuffer, 0, blockSize); + } catch (IOException e) { + completionHandler.complete(key, ResponseInfo.fileError(e, token), null); + return; + } + } + + long crc32 = Crc32.bytes(chunkBuffer, 0, blockSize); + String postUrl = String.format("%s%s", upHost, path); + + post(postUrl, chunkBuffer, 0, blockSize, getProgressHandler(), + getCompletionHandler(offset, blockSize, crc32), options.cancellationSignal); + } + + private void makeFile(String upHost, CompletionHandler _completionHandler, UpCancellationSignal c) { + String mime = format(Locale.ENGLISH, "/mimeType/%s/fname/%s", + UrlSafeBase64.encodeToString(options.mimeType), UrlSafeBase64.encodeToString(f.getName())); + + String keyStr = ""; + if (key != null) { + keyStr = format("/key/%s", UrlSafeBase64.encodeToString(key)); + } + + String paramStr = ""; + if (options.params.size() != 0) { + String str[] = new String[options.params.size()]; + int j = 0; + for (Map.Entry i : options.params.entrySet()) { + str[j++] = format(Locale.ENGLISH, "%s/%s", i.getKey(), UrlSafeBase64.encodeToString(i.getValue())); + } + paramStr = "/" + StringUtils.join(str, "/"); + } + String path = format(Locale.ENGLISH, "/mkfile/%d%s%s%s", totalSize, mime, keyStr, paramStr); + String bodyStr = StringUtils.join(contexts, ","); + byte[] data = bodyStr.getBytes(); + String postUrl = String.format("%s%s", upHost, path); + post(postUrl, data, 0, data.length, null, _completionHandler, c); + } + + private void post(String upHost, byte[] data, int offset, int dataSize, ProgressHandler progress, + CompletionHandler completion, UpCancellationSignal c) { + client.asyncPost(upHost, data, offset, dataSize, headers, token, totalSize, progress, completion, c); + } + + private ProgressHandler getProgressHandler() { + return new ProgressHandler() { + @Override + public void onProgress(long bytesWritten, long totalSize) { + long size = 0; + for (Long offset : offsets) { + if (offset != null && offset > 0) { + size += 1; + } + } + double percent = (double) size * Configuration.BLOCK_SIZE / totalSize; + if (percent > 0.95) { + percent = 0.95; + } + options.progressHandler.progress(key, percent); + } + }; + + } + + private CompletionHandler getMkfileCompletionHandler() { + return new CompletionHandler() { + @Override + public void complete(ResponseInfo info, JSONObject response) { + if (info.isNetworkBroken() && !AndroidNetwork.isNetWorkReady()) { + options.netReadyHandler.waitReady(); + if (!AndroidNetwork.isNetWorkReady()) { + completionHandler.complete(key, info, response); + return; + } + } + + if (info.isOK()) { + removeRecord(); + options.progressHandler.progress(key, 1.0); + completionHandler.complete(key, info, response); + return; + } + + // mkfile ,允许多重试一次,这里不需要重试时,成功与否都complete回调给客户端 + if (info.needRetry() && retried.get() < config.retryMax + 1) { + makeFile(upHost.get().toString(), getMkfileCompletionHandler(), options.cancellationSignal); + retried.addAndGet(1); + return; + } + completionHandler.complete(key, info, response); + } + }; + } + + + private CompletionHandler getCompletionHandler(final long offset, final int blockSize, final long crc32) { + return new CompletionHandler() { + @Override + public void complete(ResponseInfo info, JSONObject response) { + //网络断开或者无状态,检查3s,直接返回 + if (info.isNetworkBroken() && !AndroidNetwork.isNetWorkReady()) { + options.netReadyHandler.waitReady(); + if (!AndroidNetwork.isNetWorkReady()) { + completionHandler.complete(key, info, response); + return; + } + } + + //取消 + if (info.isCancelled()) { + completionHandler.complete(key, info, response); + return; + } + + //上传失败:重试 + //701: ctx不正确或者已经过期 + //checkRetried之后应该立即updateRetried,否则每个线程进来checkRetried判定都是跟第一个一样 + if (!isChunkOK(info, response)) { + if (info.statusCode == 701 && checkRetried()) { + updateRetried(); + mkblk(offset, blockSize, upHost.get().toString()); + return; + } + if (upHost != null + && ((isNotChunkToQiniu(info, response) || info.needRetry()) + && checkRetried())) { + updateRetried(); + mkblk(offset, blockSize, upHost.get().toString()); + return; + } + + completionHandler.complete(key, info, response); + return; + } + + //上传成功(伪成功):检查response + String context = null; + if (response == null && checkRetried()) { + updateRetried(); + mkblk(offset, blockSize, upHost.get().toString()); + return; + } + long crc = 0; + Exception tempE = null; + try { + context = response.getString("ctx"); + crc = response.getLong("crc32"); + } catch (Exception e) { + tempE = e; + e.printStackTrace(); + } + if ((context == null || crc != crc32) && checkRetried()) { + updateRetried(); + mkblk(offset, blockSize, upHost.get().toString()); + return; + } + if (context == null) { + String error = "get context failed."; + if (tempE != null) { + error += "\n"; + error += tempE.getMessage(); + } + ResponseInfo info2 = ResponseInfo.errorInfo(info, ResponseInfo.UnknownError, error); + completionHandler.complete(key, info2, response); + return; + } + if (crc != crc32) { + String error = "block's crc32 is not match. local: " + crc32 + ", remote: " + crc; + ResponseInfo info2 = ResponseInfo.errorInfo(info, ResponseInfo.Crc32NotMatch, error); + completionHandler.complete(key, info2, response); + return; + } + + synchronized (this) { + contexts[(int) (offset / Configuration.BLOCK_SIZE)] = context; + offsets[(int) (offset / Configuration.BLOCK_SIZE)] = offset; + record(offsets); + upBlock += 1; + if (upBlock == tblock.get()) { + makeFile(upHost.get().toString(), getMkfileCompletionHandler(), options.cancellationSignal); + return; + } + } + + if (blockInfo.size() > 0) { + BlockElement mblock = getBlockInfo(); + if (mblock.getOffset() != 0 && mblock.getBlocksize() != 0) + new UploadThread(mblock.getOffset(), mblock.getBlocksize(), upHost.get().toString()).start(); + } + } + }; + } + + private synchronized void updateRetried() { + if (singleDomainRetry.get() < config.retryMax) { + singleDomainRetry.getAndAdd(1); + } else if (retried.get() < domainRetry) { + singleDomainRetry.getAndSet(1); + retried.getAndAdd(1); + upHost.getAndSet(config.zone.upHost(token.token, config.useHttps, upHost.get().toString())); + } + + } + + private boolean checkRetried() { + return retried.get() < domainRetry; + } + + private boolean isChunkOK(ResponseInfo info, JSONObject response) { + return info.statusCode == 200 && info.error == null && (info.hasReqId() || isChunkResOK(response)); + } + + private boolean isChunkResOK(JSONObject response) { + try { + // getXxxx 若获取不到值,会抛出异常 + response.getString("ctx"); + response.getLong("crc32"); + } catch (Exception e) { + return false; + } + return true; + } + + private boolean isNotChunkToQiniu(ResponseInfo info, JSONObject response) { + return info.statusCode < 500 && info.statusCode >= 200 && (!info.hasReqId() && !isChunkResOK(response)); + } + + private Long[] recoveryFromRecord() { + + if (config.recorder == null) { + return null; + } + byte[] data = config.recorder.get(recorderKey); + if (data == null) { + return null; + } + String jsonStr = new String(data); + JSONObject obj; + try { + obj = new JSONObject(jsonStr); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + JSONArray offsetsArray = obj.optJSONArray("offsets"); + long modify = obj.optLong("modify_time", 0); + long fSize = obj.optLong("size", 0); + JSONArray array = obj.optJSONArray("contexts"); + if (offsetsArray.length() == 0 || modify != modifyTime || fSize != totalSize || array == null || array.length() == 0) { + return null; + } + for (int i = 0; i < array.length(); i++) { + contexts[i] = array.optString(i); + } + for (int i = 0; i < array.length(); i++) { + String offset = offsetsArray.optString(i); + if (offset != null && !offset.equals("null")) { + offsets[i] = Long.parseLong(offset); + } + } + return offsets; + } + + private void removeRecord() { + if (config.recorder != null) { + config.recorder.del(recorderKey); + } + } + + private void record(Long[] offsets) { + if (config.recorder == null || offsets.length == 0) { + return; + } + String data = format(Locale.ENGLISH, "{\"size\":%d,\"offsets\":[%s], \"modify_time\":%d, \"contexts\":[%s]}", + totalSize, StringUtils.jsonJoin(offsets), modifyTime, StringUtils.jsonJoin(contexts)); + config.recorder.set(recorderKey, data.getBytes()); + } + + class BlockElement { + private long offset; + private int blocksize; + + BlockElement(long offset, int blocksize) { + this.offset = offset; + this.blocksize = blocksize; + } + + public long getOffset() { + return offset; + } + + public int getBlocksize() { + return blocksize; + } + } + +} diff --git a/library/src/main/java/com/qiniu/android/storage/UploadManager.java b/library/src/main/java/com/qiniu/android/storage/UploadManager.java index 517b4c678..78b6f0da9 100644 --- a/library/src/main/java/com/qiniu/android/storage/UploadManager.java +++ b/library/src/main/java/com/qiniu/android/storage/UploadManager.java @@ -4,6 +4,7 @@ import com.qiniu.android.collect.UploadInfoCollector; import com.qiniu.android.common.Zone; import com.qiniu.android.http.Client; +import com.qiniu.android.http.DnsPrefetcher; import com.qiniu.android.http.ResponseInfo; import com.qiniu.android.utils.AsyncRun; import com.qiniu.android.utils.StringUtils; @@ -11,6 +12,7 @@ import org.json.JSONObject; import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; /** * 七牛文件上传管理器 @@ -21,25 +23,52 @@ public final class UploadManager { private final Configuration config; private final Client client; + private int multithreads = 1; + private static int DEF_THREAD_NUM = 3; + /** + * 保证代码只执行一次,防止多个uploadManager同时开始预取dns + */ + static AtomicBoolean atomicStruct = new AtomicBoolean(false); + /** + * default 3 Threads + */ public UploadManager() { - this(new Configuration.Builder().build()); + this(new Configuration.Builder().build(), DEF_THREAD_NUM); } + /** + * @param config Configuration, default 1 Thread + */ public UploadManager(Configuration config) { this.config = config; this.client = new Client(config.proxy, config.connectTimeout, config.responseTimeout, config.urlConverter, config.dns); } - public UploadManager(Recorder recorder, KeyGenerator keyGen) { - this(new Configuration.Builder().recorder(recorder, keyGen).build()); + public UploadManager(Configuration config, int multitread) { + this.config = config; + this.multithreads = multitread >= 1 ? multitread : DEF_THREAD_NUM; + this.client = new Client(config.proxy, config.connectTimeout, config.responseTimeout, + config.urlConverter, config.dns); } public UploadManager(Recorder recorder) { this(recorder, null); } + public UploadManager(Recorder recorder, KeyGenerator keyGen) { + this(new Configuration.Builder().recorder(recorder, keyGen).build()); + } + + public UploadManager(Recorder recorder, int multitread) { + this(recorder, null, multitread); + } + + public UploadManager(Recorder recorder, KeyGenerator keyGen, int multitread) { + this(new Configuration.Builder().recorder(recorder, keyGen).build(), multitread); + } + private static boolean areInvalidArg(final String key, byte[] data, File f, String token, UpToken decodedToken, final UpCompletionHandler complete) { if (complete == null) { @@ -113,6 +142,16 @@ public void put(final byte[] data, final String key, final String token, return; } + if (atomicStruct.compareAndSet(false, true)) { + if (DnsPrefetcher.checkRePrefetchDns(token, config)) { + new Thread(new Runnable() { + @Override + public void run() { + DnsPrefetcher.startPrefetchDns(token, config); + } + }).start(); + } + } Zone z = config.zone; z.preQuery(token, new Zone.QueryHandler() { @Override @@ -145,6 +184,7 @@ public void put(String filePath, String key, String token, UpCompletionHandler c put(new File(filePath), key, token, completionHandler, options); } + /** * 上传文件 * @@ -154,13 +194,23 @@ public void put(String filePath, String key, String token, UpCompletionHandler c * @param complete 上传完成的后续处理动作 * @param options 上传数据的可选参数 */ - public void put(final File file, final String key, String token, final UpCompletionHandler complete, + public void put(final File file, final String key, final String token, final UpCompletionHandler complete, final UploadOptions options) { final UpToken decodedToken = UpToken.parse(token); if (areInvalidArg(key, null, file, token, decodedToken, complete)) { return; } + if (atomicStruct.compareAndSet(false, true)) { + if (DnsPrefetcher.checkRePrefetchDns(token, config)) { + new Thread(new Runnable() { + @Override + public void run() { + DnsPrefetcher.startPrefetchDns(token, config); + } + }).start(); + } + } Zone z = config.zone; z.preQuery(token, new Zone.QueryHandler() { @Override @@ -172,10 +222,15 @@ public void onSuccess() { } String recorderKey = config.keyGen.gen(key, file); final WarpHandler completionHandler = warpHandler(complete, file != null ? file.length() : 0); - ResumeUploader uploader = new ResumeUploader(client, config, file, key, - decodedToken, completionHandler, options, recorderKey); - - AsyncRun.runInMain(uploader); + if (multithreads == 1) { + ResumeUploader uploader = new ResumeUploader(client, config, file, key, + decodedToken, completionHandler, options, recorderKey); + AsyncRun.runInMain(uploader); + } else { + ResumeUploaderFast uploader = new ResumeUploaderFast(client, config, file, key, + decodedToken, completionHandler, options, recorderKey, multithreads); + AsyncRun.runInMain(uploader); + } } @Override @@ -279,4 +334,5 @@ public void run() { } } + } diff --git a/library/src/main/java/com/qiniu/android/storage/persistent/DnsCacheFile.java b/library/src/main/java/com/qiniu/android/storage/persistent/DnsCacheFile.java new file mode 100644 index 000000000..710f4a308 --- /dev/null +++ b/library/src/main/java/com/qiniu/android/storage/persistent/DnsCacheFile.java @@ -0,0 +1,127 @@ +package com.qiniu.android.storage.persistent; + +import com.qiniu.android.storage.Recorder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Created by jemy on 2019/9/17. + */ + +public class DnsCacheFile implements Recorder { + + public String directory; + + public DnsCacheFile(String directory) throws IOException { + this.directory = directory; + File f = new File(directory); + if (!f.exists()) { + boolean r = f.mkdirs(); + if (!r) { + throw new IOException("mkdir failed"); + } + return; + } + if (!f.isDirectory()) { + throw new IOException("does not mkdir"); + } + } + + /** + * 设置DNS缓存 + * + * @param key 缓存文件明 + * @param data 缓存数据 + */ + @Override + public void set(String key, byte[] data) { + File file = new File(directory); + File[] fs = file.listFiles(); + if (fs.length > 0) { + for (int i = 0; i < fs.length; i++) { + del(fs[i].getName()); + } + } + + File f = new File(directory, key); + FileOutputStream fo = null; + try { + fo = new FileOutputStream(f); + fo.write(data); + } catch (IOException e) { + e.printStackTrace(); + } + if (fo != null) { + try { + fo.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 获取缓存 + * + * @param key 缓存文件名 + */ + @Override + public byte[] get(String key) { + File f = new File(directory, key); + FileInputStream fi = null; + byte[] data = null; + int read = 0; + try { + data = new byte[(int) f.length()]; + fi = new FileInputStream(f); + read = fi.read(data); + } catch (IOException e) { + e.printStackTrace(); + } + if (fi != null) { + try { + fi.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (read == 0) { + return null; + } + return data; + } + + //f.delete()=false时才会有fs.length>1的情况 + public String getFileName() { + File file = new File(directory); + File[] fs = file.listFiles(); + if (fs.length == 1) { + return fs[0].getName(); + } else if (fs.length > 1) { + String fileName = null; + long cachetime = 0; + for (int i = 1; i < fs.length; i++) { + String key = fs[i].getName(); + long time = Long.parseLong(key.split(":")[0]); + if (time > cachetime) { + del(fileName); + cachetime = time; + fileName = key; + } + } + return fileName; + } + return null; + } + + @Override + public void del(String key) { + if (key != null) { + File f = new File(directory, key); + f.delete(); + } + } +} diff --git a/library/src/main/java/com/qiniu/android/storage/persistent/FileRecorder.java b/library/src/main/java/com/qiniu/android/storage/persistent/FileRecorder.java index 8cc99dfde..6d91e8ae3 100644 --- a/library/src/main/java/com/qiniu/android/storage/persistent/FileRecorder.java +++ b/library/src/main/java/com/qiniu/android/storage/persistent/FileRecorder.java @@ -121,4 +121,9 @@ public void del(String key) { File f = new File(directory, hash(key)); f.delete(); } -} + + @Override + public String getFileName() { + return null; + } +} \ No newline at end of file diff --git a/library/src/main/java/com/qiniu/android/utils/AndroidNetwork.java b/library/src/main/java/com/qiniu/android/utils/AndroidNetwork.java index c920070ae..511669877 100644 --- a/library/src/main/java/com/qiniu/android/utils/AndroidNetwork.java +++ b/library/src/main/java/com/qiniu/android/utils/AndroidNetwork.java @@ -4,6 +4,12 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + /** * Created by bailong on 16/9/7. */ @@ -22,4 +28,35 @@ public static boolean isNetWorkReady() { return true; } } + + /** + * 获取ip地址 + * 使用DNS解析某地址时,可能会同时返回IPv4和IPv6的地址。 + * 如果同时拥有IPv4和IPv6的地址,是会默认优先上报IPv6的地址 + * + * @return + */ + public static String getHostIP() { + String hostIp = null; + try { + Enumeration nis = NetworkInterface.getNetworkInterfaces(); + InetAddress ia = null; + while (nis.hasMoreElements()) { + NetworkInterface ni = (NetworkInterface) nis.nextElement(); + Enumeration ias = ni.getInetAddresses(); + while (ias.hasMoreElements()) { + ia = ias.nextElement(); + if (!ia.isLinkLocalAddress() && !ia.isLoopbackAddress()) { + hostIp = ia.getHostAddress(); + break; + } + continue; + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + return hostIp; + } + } diff --git a/library/src/main/java/com/qiniu/android/utils/StringUtils.java b/library/src/main/java/com/qiniu/android/utils/StringUtils.java index 2b4f57ba7..1441e3e45 100644 --- a/library/src/main/java/com/qiniu/android/utils/StringUtils.java +++ b/library/src/main/java/com/qiniu/android/utils/StringUtils.java @@ -1,7 +1,17 @@ package com.qiniu.android.utils; +import android.util.Log; + import com.qiniu.android.common.Constants; +import com.qiniu.android.http.DnsPrefetcher; + +import org.json.JSONObject; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.UnsupportedEncodingException; /** @@ -69,6 +79,9 @@ public static String join(String[] array, String sep) { */ public static String jsonJoin(String[] array) { int arraySize = array.length; + if (array[0] == null) { + array[0] = ""; + } int bufSize = arraySize * (array[0].length() + 3); StringBuilder buf = new StringBuilder(bufSize); for (int i = 0; i < arraySize; i++) { @@ -83,6 +96,25 @@ public static String jsonJoin(String[] array) { return buf.toString(); } + + public static String jsonJoin(Long[] array) { + return jsonJoin(longToString(array)); + } + + public static String[] longToString(Long longArray[]) { + String stringArray[] = new String[longArray.length]; + for (int i = 0; i < stringArray.length; i++) { + try { + stringArray[i] = String.valueOf(longArray[i]); + } catch (NumberFormatException e) { + stringArray[i] = "null"; + continue; + } + } + return stringArray; + + } + public static byte[] utf8Bytes(String data) { try { return data.getBytes(Constants.UTF_8); @@ -109,4 +141,65 @@ public static String strip(String s) { } return b.toString(); } + + /** + * 对象转数组 + * + * @param obj + * @return + */ + public static byte[] toByteArray(Object obj) { + byte[] bytes = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + ObjectOutputStream oos = new ObjectOutputStream(bos); + oos.writeObject(obj); + oos.flush(); + bytes = bos.toByteArray(); + oos.close(); + bos.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + return bytes; + } + + /** + * 数组转对象 + * + * @param bytes + * @return + */ + public static Object toObject(byte[] bytes) { + Object obj = null; + try { + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bis); + obj = ois.readObject(); + ois.close(); + bis.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } catch (ClassNotFoundException ex) { + ex.printStackTrace(); + } + return obj; + } + + public static String getAkAndScope(String token) { + String[] strings = token.split(":"); + String ak = strings[0]; + String policy = null; + try { + policy = new String(UrlSafeBase64.decode(strings[2]), Constants.UTF_8); + JSONObject obj = new JSONObject(policy); + String scope = obj.getString("scope"); + String bkt = scope.split(":")[0]; + return ak + bkt; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + }