Tistory View

1. 안드로이드 http 다운로드하기( HttpURLConnection + SSL )

2. 안드로이드 HttpUrlConnection 서버로 전송 #1 (기본편, x-www-form-urlencoded )

3. 안드로이드 HttpUrlConnection 서버로 전송 #2 ( multipart/form-data )

 

 

 

목차

1. 서론

2. 준비사항

3. 파일로 다운로드

4. Self-Signed 인증서

5. String으로 받기

 

 

서론

안드로이드에서 web의 파일 등을 다운로드하려면 DownloadManager를 쓰면 쉽게 되지만 문제는 알림이 뜨게 된다. 여러 개를 다운로드하면 정신이 없을 정도다. 그냥 프로그램상에서 받았으면 할 경우가 있는 데, 이 때 사용할 것이 바로 HttpURLConnection이다.

 

 

준비사항

일단 사용에 앞서 필요한 권한을 manifest.xml파일에 미리 넣어둔다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

중요한 주의 사항이 있는 데, 안드로이드는 HttpURLConnection이 MainThread에서는 동작하지 않기 때문에 다른 쓰레드를 써야 한다.ASyncTask를 쓰면 좋다.

 

파일로 다운로드

일단 파일로 다운로드하는 함수를 한개 만들면서 시작해 보자.

 

이 함수는 성공시 저장된 바이트크기를 리턴하고 실패시 -1을 리턴한다.

   uri : 가지올 페이지 주소 예) "http://www.....com/..."

   filePath : 저장할 파일경로

public static int DownloadFile( String uri, String filePath ) {

   int ret = -1;

   byte[] buf = new byte[4096];
   int nread;
   int totalRead = 0;

   int contentLength;

   URL               url  = null;
   HttpURLConnection conn = null;
   InputStream       is   = null;

   File             outFile = null;
   FileOutputStream os      = null;


   try {
       url = new URL(uri );

       // 서버에 접속한다.
       conn = (HttpURLConnection)url.openConnection();

       if( 200 != conn.getResponseCode() ) {
           throw new Exception( "Not Ok : " + conn.getResponseCode() );
       }

       // contentLength : -1 == chuncked
       contentLength = conn.getContentLength();
       Log.d( TAG, "Content-Length:" + conn.getContentLength() );


       // BufferedInputStream을 쓰지 않으면 느리기 때문에 쓰는 것이다.
       is = new BufferedInputStream(conn.getInputStream());

       outFile = new File( filePath );
       os = new FileOutputStream( outFile );

       while( true ) {
           nread = is.read( buf );
           if( nread <= 0 ) {
               break;
           }
           os.write( buf, 0, nread );
           totalRead += nread;
       }

       // chunck가 아닐 경우 보내준 크기와 같은 지를 검사한다.
       if( contentLength >= 0 ) {
           if( contentLength != totalRead ) {
               throw new Exception( "read size mismatch to content Length readsize=" + totalRead + " content-length=" + contentLength );
           }
       }

       // all success
       // clean up
       is.close(); is = null;
       os.close(); os = null;
       conn.disconnect(); conn = null;

       ret = totalRead;

   } catch( Exception e ) {
       String msg = e.getMessage();
       Log.e(TAG, msg != null ? msg : "(null)" );
       e.printStackTrace();
   }
   finally {

       if( os != null ) {
           try { os.close(); } catch( Exception e ){}
       }

       if (is != null) {
           try { is.close(); } catch( Exception e ){}
       }

       if (conn != null) {
           try { conn.disconnect(); } catch ( Exception e ){}
       }
   }

   return ret;
}

 

접속 후 서버가 200[성공]을 리턴하면 해당 콘텐츠를 가지고 와서 저장한다. 그리 어려운 내용이 아니니 한번 차근차근 보면 이해 할 수 있을 것이다.

 

이제 이 함수 그대로 HTTPS 로 시도를 해보자.

놀랍게도 SSL이 적용된 서버에도 문제 없이 알아서 SSL로 처리하여 데이터를 받아올 수 있다.

 

 

Self-Signed 인증서

위에서 작성한 코드만으로도 http인지 https인지 구분하여 처리를 해주지만 문제는 서버의 인증서가 문제가 없어야 한다는 것이다.

인증서가 아직 발급되지 않았거나, 지금 열심히 서버프로그램들을 작성하고 있다면 물론 보안상에 문제가 있지만 Self-Signed 인증서를 임시로 써야 한다.

Self-Signed 인증서를 쓰는 서버에 위 코드로 접속을 하면 다음과 같은 로그캣 메세지를 뱄으며, 처리가 되지 않는다.

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found

말그대로 신뢰할 수 없는 연결이라는 것이다.

만약 인증서가 정상이고 브라우저에서도 잘 되는 것이라면, 주소가 틀렸는지 확인해보길 바란다.

 

그래서 self-Signed로 처리한 경우 SSL에서 검사를 무력화 시키는 방법은 다음과 같다.

 

만약 selfSigned인 서버를 테스트한다면 isSelfSigned를 true로 넣으면 된다.

 

public static int DownloadFile( String uri, String filePath, boolean isSelfSigned ) {

   int ret = -1;

   byte[] buf = new byte[4096];
   int nread;
   int totalRead = 0;

   int contentLength;

   URL               url  = null;
   HttpURLConnection conn = null;
   InputStream       is   = null;

   File             outFile = null;
   FileOutputStream os      = null;



   try {

       // ssl 이며 self-signed인 경우 무력화 시키는 코드
       if( uri.startsWith( "https://" ) && isSelfSigned ) {

           try {

               HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier(){
                   public boolean verify(String hostname, SSLSession session) {
                       return true;
                   }});

               SSLContext sslContext = null;
               sslContext = SSLContext.getInstance("TLS");

               sslContext.init(null, new X509TrustManager[]{new X509TrustManager(){
                   public void checkClientTrusted(X509Certificate[] chain,
                                                  String authType) throws CertificateException {}
                   public void checkServerTrusted(X509Certificate[] chain,
                                                  String authType) throws CertificateException {}
                   public X509Certificate[] getAcceptedIssuers() {
                       return new X509Certificate[0];
                   }}}, new SecureRandom() );

               HttpsURLConnection.setDefaultSSLSocketFactory( sslContext.getSocketFactory() );

           } catch (Exception e) { // should never happen
               e.printStackTrace();
           }
       }
       /// 여기까지.. 이하는 동일
       
       
       

       url = new URL(uri );
       conn = (HttpURLConnection) url.openConnection();


       if( 200 != conn.getResponseCode() ) {
           throw new Exception( "Not Ok : " + conn.getResponseCode() );
       }

       contentLength = conn.getContentLength();
       Log.d( TAG, "Content-Length:" + conn.getContentLength() );


       // BufferedInputStream을 쓰지 않으면 느리기 때문에 쓰는 것이다.
       is = new BufferedInputStream(conn.getInputStream());

       outFile = new File( filePath );
       os = new FileOutputStream( outFile );

       while( true ) {
           nread = is.read( buf );
           if( nread <= 0 ) {
               break;
           }
           os.write( buf, 0, nread );
           totalRead += nread;
       }

       if( contentLength > 0 ) {
           if( contentLength != totalRead ) {
               throw new Exception( "read size mismatch to content Length readsize=" + totalRead + " content-length=" + contentLength );
           }
       }

       // all success
       // clean up
       is.close(); is = null;
       os.close(); os = null;
       conn.disconnect(); conn = null;

       ret = totalRead;

   } catch( Exception e ) {
       String msg = e.getMessage();
       Log.e(TAG, msg != null ? msg : "(null)" );
       e.printStackTrace();
   }
   finally {

       if( os != null ) {
           try { os.close(); } catch( Exception e ){}
       }

       if (is != null) {
           try { is.close(); } catch( Exception e ){}
       }

       if (conn != null) {
           try { conn.disconnect(); } catch ( Exception e ){}
       }
   }

   return ret;
}

이제 인증서가 도착하면 이 함수에 isSelfSigned만 false로 변경하기만 하면 된다. 뭐.. 다른 이유로 계속 쓸 수도 있겠지만....

 

이 코드의 문제점은 HttpURLConnection전체를 무력화하기 때문에 그냥 최대한 빨리 인증서를 발급받기 바란다.

[letsencypt]라는 공짜인증서도 있다.

 

String으로 받기

파일로 저장하는 것으로 준비 작업을 끝냈으니, 이제 String를 처리해보자

 

함수는 다음과 같다.

 

charset : "UTF-8", "EUC-KR" 등등 서버에 값과 동일하게 맞춰주면 된다.

Content-Type같은 것을 분석해서 처리하려고 했는 데, 생각해보니, 서버와 클라이언트가 같이 프로그래밍 되는 경우가 대부분이기에 이 코드는 넣지 않기로 했다.

public static String DownloadAsString( String uri, String charset, boolean isSelfSigned ) {
   
   String ret = null;

   char[] buf = new char[2048];
   int nread;

   URL url = null;
   HttpURLConnection urlConnection = null;
   InputStream in = null;
   InputStreamReader rdr = null;

   StringBuilder contents = new StringBuilder();

   try {

       if( uri.startsWith( "https://" ) && isSelfSigned ) {

           try {

               HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier(){
                   public boolean verify(String hostname, SSLSession session) {
                       return true;
                   }});

               SSLContext sslContext = null;
               sslContext = SSLContext.getInstance("TLS");

               sslContext.init(null, new X509TrustManager[]{new X509TrustManager(){
                   public void checkClientTrusted(X509Certificate[] chain,
                                                  String authType) throws CertificateException {}
                   public void checkServerTrusted(X509Certificate[] chain,
                                                  String authType) throws CertificateException {}
                   public X509Certificate[] getAcceptedIssuers() {
                       return new X509Certificate[0];
                   }}}, new SecureRandom() );

               HttpsURLConnection.setDefaultSSLSocketFactory( sslContext.getSocketFactory() );

           } catch (Exception e) { // should never happen
               e.printStackTrace();
           }
       }




       url = new URL(uri );
       urlConnection = (HttpURLConnection) url.openConnection();

       if( 200 != urlConnection.getResponseCode() ) {
           throw new Exception( "Not Ok : " + urlConnection.getResponseCode() );
       }

       Log.e( TAG, "Content-Length:" + urlConnection.getContentLength() );

       in = new BufferedInputStream(urlConnection.getInputStream());

       rdr = new InputStreamReader( in, charset );

       nread = rdr.read( buf );
       while ( nread > 0 ) {
           contents.append( buf, 0, nread );
           nread = rdr.read( buf );
       }

       ret = contents.toString();



   } catch( Exception e ) {
       String msg = e.getMessage();
       Log.e(TAG, msg != null ? msg : "(null)" );
       e.printStackTrace();

       ret = null; // reset for returning error
   }
   finally {

       if( rdr != null ) {
           try { rdr.close(); } catch(Exception e ) {}
       }
       if (in != null) {
           try { in.close(); } catch( Exception e ) { }
       }

       if (urlConnection != null) {
           try { urlConnection.disconnect(); }  catch( Exception e ) { }
       }

   }

   return ret;
}

 

제법 쓸만한 코드가 만들어진 것 같다.

Replies
Reply Write