題外話:今天偶爾來逛逛,發現我真是懶到家了。居然有半年前的留言我都沒有來看過,真對不起留言的同學,希望他的問題已經解決了。
這兩三天一直被亞馬遜S3上傳文件的問題困擾着,直到昨天晚上終於搞定了,工作群里一片歡騰,從客戶端到服務器數位工程師卡在這個問題上抓耳撓腮了好幾天,終於解決了,這就是所謂“光明總出現在最黑暗的時刻”吧,嘿嘿,非常開心,程序員真是容易滿足啊。
途中搜索了很多互聯上的方案,最終在stackoverflow的一個帖子里找到有價值的參考。而我們國內的站點上關於這方面的信息非常少,所以我來寫這個隨筆,希望為大家增加一點參考吧。
------------------------------------------------------------正式開始的分割線----------------------------------------------------------------------------
一開始我們的代碼是這樣,上傳一個測試的mp4,我們的需求是文件size不超過10M:
1 File file = new File(Environment.getExternalStorageDirectory().getPath() + "/0.mp4"); 2 3 if(!file.exists()) 4 return; 5 if(!file.isFile()) 6 return; 7 try { 8 URL url = new URL("https://secv.s3.amazonaws.com/familytest10099/DFG092833/2016/01/27/10-38-07/0.mp4?AWSAccessKeyId=AKIAIGMMMZARXIK3ZDBA&Expires=1453948689&Signature=uJvHirNhOlCuB5z20NPkYc73qV8%3D");//從自家server簽出的S3 地址 9 HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); 10 connection.setDoOutput(true); 11 connection.setRequestMethod("PUT"); 12 BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(connection.getOutputStream()); 13 FileInputStream fileInputStream = new FileInputStream(file);// 讀取文件的數據。 14 byte[] bufer = new byte[1024]; 15 int len = 0, i = 0; 16 while ((len = fileInputStream.read(bufer)) != -1) { 17 bufferedOutputStream.write(bufer, 0, len); 18 } 19 20 bufferedOutputStream.flush(); 21 bufferedOutputStream.close(); 22 23 fileInputStream.close(); 24 25 int responseCode = connection.getResponseCode(); 26 Log.d("s3_ssltest","Service returned response code " + responseCode); 27 } catch (MalformedURLException e) { 28 // TODO Auto-generated catch block 29 e.printStackTrace(); 30 } catch (IOException e) { 31 // TODO Auto-generated catch block 32 e.printStackTrace(); 33 }
運行結果拋出異常:
javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x54b61708: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0x52732cfc:0x00000000)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:448)
at com.android.okhttp.Connection.upgradeToTls(Connection.java:146)
at com.android.okhttp.Connection.connect(Connection.java:107)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:294)
at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:255)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:296)
at com.android.okhttp.internal.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:503)
at com.android.okhttp.internal.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:136)
at com.spde.switchbox.manager.secu.SecuThread.a(Unknown Source)
at com.spde.switchbox.manager.secu.SecuThread.run(Unknown Source)
Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x54b61708: Failure in SSL library, usually a protocol error
error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0x52732cfc:0x00000000)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:405)
... 11 more
出錯原因是亞馬遜s3 服務器端禁用了SSLv3,解決方法是自己extends一個SSLSocketFactory把SSLv3從默認的protocol中remove掉。重寫的SSLSocketFactory見附件NoSSLv3Factory.java。
在創建connection之前先調用:
HttpsURLConnection.setDefaultSSLSocketFactory(new NoSSLv3Factory());
這樣修改之后SSL的問題解決了,但又拋出一個異常
javax.net.ssl.SSLException: Write error: ssl=0x5ab6f5f8: I/O error during system call, Connection reset by peer
at org.apache.harmony.xnet.provider.jsse.NativeCrypto.SSL_write(Native Method)
at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl$SSLOutputStream.write(OpenSSLSocketImpl.java:693)
at java.io.ByteArrayOutputStream.writeTo(ByteArrayOutputStream.java:231)
at libcore.net.http.RetryableOutputStream.writeToSocket(RetryableOutputStream.java:70)
at libcore.net.http.HttpEngine.readResponse(HttpEngine.java:806)
at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:274)
at libcore.net.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:486)
at libcore.net.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:134)
at com.example.test.MainActivity$SSLTestRunnable.run(MainActivity.java:73)
at java.lang.Thread.run(Thread.java:856)
原因是http header中沒有指定content-type,傳輸格式是stream
connection.setRequestProperty("Content-Type", "application/octet-stream");
這個異常也解決了。訪問成功,但是返回403 Forbidden,網上搜到很多解釋說是客戶端的時區或者系統時間不對,因為S3上簽出的url是有expire date的。但我們出問題的原因是因為服務器簽出這個url的時候沒有指定Content-type , 又是Content-type的問題,修改簽出地址的代碼如下:
public static String createUploadUrl(String bucket, String objKey, Date expire){
String url = s3Client.generatePresignedUrl(new GeneratePresignedUrlRequest(bucket, objKey).
withMethod(HttpMethod.PUT).
withContentType("application/octet-stream").
withExpiration(expire)
).toString();
return url;
}
ok,這次終於收到回復200 ok了,視頻也上傳成功。
困擾三天的問題就這樣解決了。總結一下:一個復雜迷茫各種想不通的問題,最終解決的時候往往修改不了幾行代碼。
額外的說明:我們出問題的平台android版本是4.4.3,openSSL lib的版本是1.0.1e。網上搜集到的信息,SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure 這種問題通常出現在Android 4.x上。
最后是 NoSSLv3Factory.java 的源碼