Tistory View
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);
}
mList.add( d );
}
// 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;
mList.add( d );
}
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]가 합쳐져서 두번의 줄넘김으로 보이는 것이다.
'Android Develop > helper' 카테고리의 다른 글
안드로이드 byte배열을 String 한글 변환(+charsetDecoder) (0) | 2020.12.30 |
---|---|
Buffer(ByteBuffer, CharBuffer...) flip, compact, clear사용법 (0) | 2020.12.30 |
안드로이드 HttpUrlConnection POST 전송 #1 (0) | 2020.12.24 |
안드로이드 http 다운로드하기( HttpURLConnection + SSL ) (0) | 2020.12.18 |
안드로이드 Bitmap 로드하기 (0) | 2019.09.08 |
- Total
- Today
- Yesterday
- OpenGL ES
- 텍스처
- 재태크
- 에어컨
- 블로그
- 재테크
- 에어콘
- 공유 컨텍스트
- 예금
- 애드핏
- 금리
- 사용료
- TTS
- gpgpu
- 전기요금
- 적금
- Android
- 경제보복
- 애드센스
- 컴퓨트셰이더
- 안드로이드
- ComputeShader
- texture
- 아끼는 법
- 전기료
- OpenGLes
- 티스토리
- choreographer
- 컴퓨트쉐이더
- 전기세
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |