Tistory View

Android Develop/helper

안드로이드 HttpUrlConnection POST 전송 #2

God Dangchy What should I do? 2020. 12. 25. 20:22

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

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

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

 

 

multipart/form-data

1편에서 application/x-www-form-urlencoded방식을 설명했다. 이 방식의 가장 큰 문제 파일을 전송할 수 없다는 것이다. 파일을 전송하기위해서는multipart/form-data방식을 써야 한다. 이 방식의 전송데이터는 다음과 같이 생겼다.

POST /test HTTP/1.1
Host: foo.example
Content-Type: multipart/form-data;boundary="boundary"

--boundary
Content-Disposition: form-data; name="field1"

value1
--boundary
Content-Disposition: form-data; name="field2"; filename="example.txt"

value2
--boundary--

출처 : developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST

 

Boundary

항목과 항목을 구분하는 구분자는 boundary를 사용한다. 이 값은 Content-Type에 미리 지정하는 값이고 보내는 측에서 마음대로 설정할 수 있다.

Content-Type에 사용되는 boundary값 앞에 "--"을 붙은 "--boundary"가 실제 boundary이며, 전송데이터의 마지막에는 마지막에 "--"을 붙여서 "--boundary--"가 마지막 boundary로 데이터 전송이 끝났음을 표시한다.

정확한 boundary값은 위의 예에서는 "--boundary\r\n"값이 될 것이고, 마지막 boundary는 "--boundary--\r\n"이 된다.

 

이 Boundary값은 [값]에는 절대 나오면 안된다는 조건이 붙는다. 위의 예는 실제 값에 [--boundary]이라는 값이 들어갈 경우 문제가 발생할 수 있다. 따라서 값을 전부 파악해서 값의 일부라도 겹치치 않는 값을 만들어서 사용해야 되는 데, 이 작업은 일일이 파일을 열어서 검사를 해야되기 때문에 자원을 너무 많이 먹는 작업이다. 따라서 간단한 트릭을 이용해서 만든다.

public static String GenBoudnary() {

   SecureRandom random = new SecureRandom();

   byte[] randData = random.generateSeed(16);

   StringBuilder sb = new StringBuilder(randData.length * 2);

   for(byte b: randData )
       sb.append(String.format("%02x", b));

   return sb.toString();
}

그냥 랜덤값으로 만드는 것이다. 실제 데이터와 겹칠 확율이 아주 적다는 방식을 사용할 뿐이다.

 

 

전송할 데이터를 지정

multipart/form-data는 파일도 전송할 수 있기 때문에, 쓸만한 녀석을 만들려면 다음의 것들을 지원해줘야 할 것 같다.

1. java를 쓸 것이기 때문에 String 데이터 전송

2. binary데이터 전송

3. 파일로 binary전송 : 실제 데이터가 메모리상에 있고 이 것을 서버에서 파일로 인식하도록 전송

4. 파일이름으로 데이터전송 : 데이터가 파일로 있는 경우

 

위 프로세스의 원형은 다음 정도면 될 것이다.

1. java를 쓸 것이기 때문에 String 데이터 전송
void AddString( String name, String value, String charset );
2. binary데이터 전송
void AddBinary( String name, byte[] value );
3. 파일로 binary전송 : 실제 데이터가 메모리상에 있고 이 것을 서버에서 파일로 인식하도록 전송
void AddFile( String name, String filename, byte[] value );
4. 파일이름으로 데이터전송 : 데이터가 파일로 있는 경우
void AddFile( String name, String filename );

이 것을 위한 간단한 Class를 만들어 버리자.

import java.io.File;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

public class MultipartFormData {

    private static class DataItem {
        String name;
        byte[] data;     // 1(String) : 미리 변환된 데이터, 2(byte)일경우 입력데이터의 복사본, 3(memory file)일경우 file data
        String fileName; // 전송될 파일이름
        String filePath; // local상의 파일이름 // 4
    }

    private List<DataItem> mList = new ArrayList< DataItem >();

    // 1. java를 쓸 것이기 때문에 String 데이터 전송
    public void addString( String name, String value, String charset ) throws UnsupportedEncodingException {

        DataItem d = new DataItem();

        d.name = name;
        d.data = value.getBytes( charset );

        mList.add( d );
    }
    // 2. binary데이터 전송
    public void addBinary( String name, byte[] value ) {

        DataItem d = new DataItem();

        d.name = name;
        if( value != null ) {
            d.data = new byte[value.length];
            System.arraycopy(value, 0, d.data, 0, value.length);
        }

        mList.add( d );
    }
    // 3. 파일로 binary전송 : 실제 데이터가 메모리상에 있고 이 것을 서버에서 파일로 인식하도록 전송
    public void addFile( String name, String filename, byte[] value ) {

        DataItem d = new DataItem();

        d.name = name;
        d.fileName = filename;
        if( value != null ) {
            d.data = new byte[value.length];
            System.arraycopy(value, 0, d.data, 0, value.length);
        }
    }
    // 4. local 파일로 데이터전송 : 데이터가 파일로 있는 경우
    public void addFile( String name, String filename ) {

        DataItem d = new DataItem();
        d.name = name;
        d.fileName = new File( filename ).getName();
        d.filePath = filename;

    }
    
    public int computeContentLength() {
        int ret = 0;
        // ...
        return ret;
    }
    
    public void Write( OutputStream out ) {
        // ...
    }
}

ContentLength 계산

public int computeContentLength( String boundary ) {

   int ret = 0;

   int boundarylen = boundary.length();

   if( mList.size() == 0 ) {
       return 0;
   }

   for( DataItem d : mList ) {
       //--{boundary}RN
       //12{boundary}12
       ret += 2 + boundarylen + 2;

       // Content-Disposition: form-data; name="{name}"
       //          1         2         3
       // 12345678901234567890123456789012345678{name}1
       //
       ret += 38 + d.name.length + 1;

       if( d.fileName == null ) {
           ret += 4; // \r\n\r\n
           ret += d.data.length;
       } else {

           // ; filename="{filename}"
           // 123456789012{filename}1
           ret += 12 + d.fileName.length + 1;
           ret += 4; // \r\n\r\n
           if( d.data != null ) {
               ret += d.data.length;
           } else {
               // case 4
               ret += new File(d.filePath).length();
           }
       }
       ret += 2;  // "\r\n"
   }

   // --boundary--RN
   // 12        1234
   ret += 2 + boundarylen + 4;

   return ret;
}

업로드할 데이터를 Stream으로 보내는 코드

public int write( String boundary, OutputStream out ) throws IOException {

   int ret = 0;

   byte[] buf = new byte[1024];

   int boundarylen = boundary.length();
   byte[] dd    = { '-', '-' };
   byte[] rn    = { '\r', '\n' };
   byte[] cdf   = ("Content-Disposition: form-data; name=\"").getBytes();
   byte[] fn    = "\"; filename=\"".getBytes();
   byte[] q     = {'"'};
   
   byte[] bound = boundary.getBytes( "UTF-8" );

   if( mList.size() == 0 ) {
       return 0;
   }

   for( DataItem d : mList ) {

   
       ret += 2 + boundarylen + 2;
       out.write( dd ); out.write( bound ); out.write( rn );

       if( d.fileName == null ) {

           out.write( cdf ); out.write( d.name ); out.write( '"' ); out.write( rn );
           ret += 38 + d.name.length + 1 + 2;

           out.write( rn );
           ret += 2; // \r\n

           out.write( d.data );
           ret += d.data.length;

       } else {

           out.write( cdf );
           out.write( d.name );
           out.write( fn );
           out.write( d.fileName );
           out.write( '"' );
           out.write( rn );

           ret += 38 + d.name.length + 13 + d.fileName.length + 1 + 2;

           out.write( rn );
           ret += 2; // \r\n


           if( d.data != null ) {
               out.write( d.data );
               ret += d.data.length;

           } else {
               // case 4

               File f = new File(d.filePath);

               FileInputStream fo = new FileInputStream( f );

               BufferedInputStream is = new BufferedInputStream( fo );

               while( true ) {
                   int nread = is.read( buf );
                   if( nread <= 0  ) {
                       break;
                   }
                   out.write( buf, 0, nread );
               }


               fo.close();

               ret += f.length();
           }
       }

       out.write( rn );
       ret += 2;  // "\r\n"
   }

   // --boundary--RN
   // 12        1234
   out.write( dd );
   out.write( bound );
   out.write( dd );
   out.write( rn );
   ret += 2 + boundarylen + 4;

   return ret;
}

예제 코드

MultipartFormData formData = new MultipartFormData();

try {
   formData.addString("h\"i", "값1", "UTF-8");
   formData.addString("hi2", "값2", "UTF-8");
   formData.addBinary( "hi3", new byte[]{'1', '2' } );
   formData.addFile( "filefrom메모리", "file이름.txt", new byte[]{'3', '4', '5', '6' } );
   formData.addFile( "filePath", "/storage/emulated/0/~~~~~8.jpg" );
} catch( Exception e ) {

}



.
.
String boundary=GenBoundary();
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
urlConnection.setRequestProperty("Content-Type", "multipart/form-data; boundary=\"" + boundary + "\"" );


//urlConnection.setFixedLengthStreamingMode( contentLength );
urlConnection.setChunkedStreamingMode(0);

out = new BufferedOutputStream( urlConnection.getOutputStream() );

int written = formData.write( boundary, out );

out.close();
out = null;

.
.
.

 

 

바운더리에 각각의 항목도 기본 Header/Body를 구분하는 것처럼 Header/Body로 구분된다.

--boundary
Content-Disposition: ~~~~[줄넘김]        // header 
Content-Type: image/jpeg[줄넘김1]        // header
[줄넘김2]                                // <-------- header와 body의 구분자
[내용][내용][내용]                       // body 
[내용][내용][내용]                       // body

[줄넘김1]과 [줄넘김2]가 합쳐져서 두번의 줄넘김으로 보이는 것이다.

Replies
Reply Write