KyungHwan's etc.

Http Multipart 및 jsp 파일 업로드 본문

Jsp And Servlet

Http Multipart 및 jsp 파일 업로드

KyungHwan_0 2018. 5. 30. 15:46

Http Multipart

Multipart는 HTTP를 통해 File을 SERVER로 전송하기 위해 사용되는 Content-type이다.

일반적으로 HTTP Request는 Body에 클라이언트가 전송하려고 하는 데이터를 넣을 수 있다. 그리고 Body에 들어가는 데이터의 타입을 HTTP Header에 명시해 줌으로써 서버가 타입에 따라 알맞게 처리하게 한다. 이 Body의 타입을 명시하는 Header가 Content-type이다.

Content-Type필드에는 엔티티본문의 MIME타입을 기술한다.


-MIME타입

HTTP가 웹에서 전송되는 객체 각각에 붙이는 데이터 포맷 라벨로써 Multipurpose Internet Mail Extensions의 약자이다.

이는 원래 각기 다른 전자메일 시스템 사이에서 메시지가 오갈 때 겪는 문제점을 해결하기 위해 설계되었다. 이는 이메일에서 워낙 잘 동작했기 때문에 HTTP에서도 멀티미디어 콘텐츠를 기술하고 라벨을 붙이기 위해 채택되었다.

웹 서버는 모든 HTTP 객체 데이터에 MIME 타입을 붙인다. 웹 브라우저는 서버로부터 객체를 돌려받을 때, 다룰 수 있는 객체인지 MIME 타입을 통해 확인한다

그런데 보통 HTTP Request의 Body는 한 종류의 타입이 대부분이고, 그래서 Content-type도 타입을 하나만 명시할 수 있다.

(예를 들어 text이면 text/plain, xml이면 text/xml, jpg이미지면 image/jpeg 등등

Message

[Header 부분]
POST (서버의 URI) HTTP/1.1¶
Accept-Language: ko¶
Content-Type: multipart/form-data; boundary=(일종의 예약어)¶
Host: (서버의 IP:PORT)¶
Content-Length: (Body Size)¶
Connection: Keep-Alive¶

[Body 부분]

--(헤더에서 정한 예약어)¶
Content-Disposition: form-data; name="(서버에서 받을 이름)"; filename="(보낼 파일 이름)"¶
content-Type: application/octet-stream¶ // 바이너리 파일을 보낼 경우에만 추가한다.
[보낼 데이터 부분]
//보낼 데이터 어쩌고 저쩌고
[데이터 의 끝을 표시]
--(헤더에서 정한 예약어)¶

multipart/form-data로 데이터를 보낼때의 request header와 body는 위와 같이 이뤄져 있으며 눈여겨 봐야 할 부분은 Content-Type이다.

Content-Type에 multipart/form-data로 지정이 되어 있어야 서버에서 정상적으로 데이터를 처리할 수 있으며 boundary에 지정되어 있는 문자열을 이용하여 전송되는 파일 데이터의 구분자로 사용되는 것을 확인할 수 있다.

boundary의 문자열 중 마지막의 ------WebKitFormBoundaryQGvWeNAiOE4g2VM5-- 값은 다른 값들과 다르게 마지막에 -- 가 붙은 것을 확인할 수 있는데 이는 body의 끝을 알리는 의미이다.

Multipart/form-data / Multipart/byteranges

l Multipart/form-data

브라우저에서 서버로 HTML Form의 내용을 전송 시 사용 할 수 있다. 멀티파트 문서 형식으로써, 경계(‘—‘ (이중대시)로 시작되는 문자열)로 구분되는 다른 파트들로 구성된다.

각 파트는 그 자체로 개체이며 자신만의 HTTP 헤더를 가지는데 파일 업로드 필드를 위한 헤더로는 Content-Disposition, 그리고 가장 일반적인 것 중하나인 Content-Type이 있다. (Content-Length는 경계선이 구분자로 사용되므로 무시해도 된다

<form action="http://localhost:8000/" method="post" enctype="multipart/form-data">
<input type="text" name="myTextField">
<input type="checkbox" name="myCheckBox">Check</input>
<input type="file" name="myFile">
<button>Send the file</button>
</form>

HTML form 태그
POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=---------------------------8721656041911415653955004498
Content-Length: 465

-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myTextField"

Test
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myCheckBox"

on
-----------------------------8721656041911415653955004498
Content-Disposition: form-data; name="myFile"; filename="test.txt"
Content-Type: text/plain

Simple file.
-----------------------------8721656041911415653955004498--

HTTP Form 은 다음과 같이 메시지를 전송한다.

범위 요청

HTTP는 클라이언트가 문서의 일부분이나 특정 범위만 요청할 수 있도록 할 수 있다.

이게 필요한 이유에 대해 설명하자면, HTTP를 통해서 용량이 큰 파일을 다운받고 있는데, 네트워크 문제로 연결이 끊겼다고 생각해보자.

그렇다면 보통의 생각으론 다시 연결을 시도하고 처음부터 대용량의 파일을 처음부터 받아야 할 것이다.

그런데 범위 요청을 이용하면 HTTP Client는 받다가 실패한 엔터티를 일부나 범위로 요청하여서 다운로드가 중단된 시점에서부터 다시 시작할 수 있다. (오리진 서버에서 객체를 처음 요청했을 때와 범위 요청을 했을 때 사이에 아무 변경이 없었다면)

예시

GET /bigfile.html HTTP/1.1

Range: bytes=4000-

User-Agent: Mozilla/4.61 [en] (WinNT; I)

...

이 예제에선, 클라이언트가 처음 4,000바이트 이후의 부분을 요청하고 있다. (몇바이트까지인지는 언급하지 않는데, 클라이언트가 문서의 크기가 정확히 얼마인지를 모르기 떄문이다.)

서버는 클라이언트에게 자신의 범위를 받아들일 수 있는지 응답에 Accept-Range 헤더를 포함시키는 방법으로 알려줄 수 있다. 이 헤더의 값은 측정의 단위로 주로 바이트이다.

HTTP/1.1 200 OK

Date: Fri, 05 Nov 1999 22:35:15 GMT

Server: Apache/1.2.4

Accept-Ranges: bytes

...

또한 Range 헤더는 P2P 파일 공유 클라이언트가 멀티미디어 파일의 다른 부분을 여러 다른 피어로부터 동시에 다운로드 받을 때도 널리 사용된다.

범위 요청은 객체의 특정 인스턴스를 클라이언트와 서버 사이에서 교환하는 것이기 때문에, 인스턴스 조작의 일종이라는 것에 주의해야 한다. 이는 클라이언트의 범위 요청은 오직 클라이언트와 서버가 같은 버전의 문서를 갖고 있을 때만 의미가 있음을 의미한다.

l Multipart/byteranges(**멀티파트 범위 응답)**

브라우저로 회신하는 부분적인 응답 전송의 컨텍스트 내에서 사용된다.

HTTP 메시지가 복수영역의 내용을 포함하는 경우, 이는 "multipart/byteranges" 객체에 담겨 전송된다. 이 미대어 타입은 MIME 경계로 구분된 두 개 이상의 부분을 포함하며, 각각은 자신의 Content-Type과 Content-Range 필드를 가진다.

206 Partial Content 상태코드가 전송된 경우, MIME 타입은 문서가 각각의 요청된 범위들 중 하나인 몇 가지 파트들로 구성되어 있음을 알려주기 위해 사용된다. 다른 멀티파트 타입처럼, Contetn-Type은 경계선 문자열을 정의하기 위해 boundary 디렉티브를 사용한다. 각각의 다른 파트들은 문서의 실제 타입을 가진 Content-Type 헤더와 그들이 나타내는 범위를 가진 Content-Range를 지니고 있다.

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 385

--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 100-200/1270

eta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="vieport" content
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 300-400/1270

-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: "Open Sans", "Helvetica
--3d6b6a416f9b5--

파일을 보낼 때 바이너리코드로 바뀌어서 여러 패킷에 담아 전송된다.

멀티파트를 이용한 jsp 파일업로드

파일업로드폼: fileUploadForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>파일 업로드 폼</title>
</head>
<h3>파일 업로드 폼</h3>
<center>
<!--
파일업로드를 위해선 반드시 method="post" enctype="Multipart/form-data"여야함!
-->
<form action="fileUpload.jsp" method="post" enctype="Multipart/form-data">
올린 사람 : <input type="text" name="name" /><br/>
제목 : <input type="text" name="subject" /><br/>
<!--
파일 업로드는 input type="file"로 지정한다.
-->
파일명1 : <input type="file" name="fileName1" /><br/>
<input type="submit" value="전송" />
<input type="reset" value="취소" />
</form>
</center>

이때, 파일을 저장할 디렉토리가 필요함으로 프로젝트 WebContent 하위에 uploadFile 폴더를 하나 만들자

이제 이 폴더에 업로드한 파일이 저장되도록 할 것인데, 프로젝트 내에 위치했다해서 워크스페이스 내에 있는 것이 아니다!!

실제 파일의 위치는 자신의 톰캣 서버 내의 프로젝트 폴더 내에 위치하고 있다는 점을 알아야한다.

만약 정확히 모르겠다면 jsp페이지에서 절대경로를 찍어보면 된다.

파일업로드:fileUpload.jsp

<%@page import="java.io.File"%>
<%@page import="java.util.Enumeration"%>
<%@page import="com.oreilly.servlet.multipart.DefaultFileRenamePolicy"%>
<%@page import="com.oreilly.servlet.MultipartRequest"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<%
// request.getRealPath("상대경로") 를 통해 파일을 저장할 절대 경로를 구해온다.
// 운영체제 및 프로젝트가 위치할 환경에 따라 경로가 다르기 때문에 아래처럼 구해오는게 좋음
String uploadPath = request.getRealPath("/uploadFile");
out.println("절대경로 : " + uploadPath + "<br/>");
int maxSize = 1024 * 1024 * 10; // 한번에 올릴 수 있는 파일 용량 : 10M로 제한
String name = "";
String subject = "";
String fileName1 = ""; // 중복처리된 이름
String originalName1 = ""; // 중복 처리전 실제 원본 이름
long fileSize = 0; // 파일 사이즈
String fileType = ""; // 파일 타입
MultipartRequest multi = null;
try{
// request,파일저장경로,용량,인코딩타입,중복파일명에 대한 기본 정책
multi = new MultipartRequest(request,uploadPath,maxSize,"utf-8",new DefaultFileRenamePolicy());
// form내의 input name="name" 인 녀석 value를 가져옴
name = multi.getParameter("name");
// name="subject" 인 녀석 value를 가져옴
subject = multi.getParameter("subject");
// 전송한 전체 파일이름들을 가져옴
Enumeration files = multi.getFileNames();
while(files.hasMoreElements()){
// form 태그에서 <input type="file" name="여기에 지정한 이름" />을 가져온다.
String file1 = (String)files.nextElement(); // 파일 input에 지정한 이름을 가져옴
// 그에 해당하는 실재 파일 이름을 가져옴
originalName1 = multi.getOriginalFileName(file1);
// 파일명이 중복될 경우 중복 정책에 의해 뒤에 1,2,3 처럼 붙어 unique하게 파일명을 생성하는데
// 이때 생성된 이름을 filesystemName이라 하여 그 이름 정보를 가져온다.(중복에 대한 처리)
fileName1 = multi.getFilesystemName(file1);
// 파일 타입 정보를 가져옴
fileType = multi.getContentType(file1);
// input file name에 해당하는 실재 파일을 가져옴
File file = multi.getFile(file1);
// 그 파일 객체의 크기를 알아냄
fileSize = file.length();
}
}catch(Exception e){
e.printStackTrace();
}
%>
<!--
해당 페이지는 사용자에게 보여줄 필요가 없고 해당 정보를 전달만 해주면 되기 때문에 hidden으로 했다.
-->
<form action="fileCheck.jsp" method="post" name="fileCheckFormName">
<input type="hidden" value="<%=name%>" name="name" />
<input type="hidden" value="<%=subject%>" name="subject" />
<input type="hidden" value="<%=fileName1%>" name="fileName1" />
<input type="hidden" value="<%=originalName1%>" name="originalName1" />
</form>
<!--
a태그로 클릭시 파일체크하는 jsp페이지로 이동하도록 함
javascript를 이용해서 onclick시 폼태그를 잡아와 submit()을 호출해 폼태그를 전송
-->
<a href="#" onclick="javascript:document.fileCheckFormName.submit()">업로드 파일 확인하기 :<%=fileName1 %></a>

uploadFile 폴더내에 업로드한 파일이 업로드됨.

파일체크를위한 페이지: fileCheck.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>파일체크 JSP 페이지</title>
</head>
fileCheck jsp페이지
<%
// post방식에 대한 한글 인코딩 방식 지정 get방식은 서버의 server.xml에서 Connector태그에 URIEncoding="UTF-8" 추가
request.setCharacterEncoding("UTF-8");
// input type="name" 의 value값을 가져옴
String name = request.getParameter("name");
// input type="subject" 의 value값을 가져옴
String subject = request.getParameter("subject");
// 중복방지용으로 만들어져 넘겨진 파일명을 가져옴
String fileName1 = request.getParameter("fileName1");
// 본래의 파일명을 가져옴
String originalName1 = request.getParameter("originalName1");
%>
<h3>업로드 파일 확인</h3>
올린 사람 : <%=name %><br/>
제목 : <%=subject %><br/>
<!-- 파일 다운로드 링크 클릭시 다운로드 될 수 있도록 fileDown1.jsp 페이지에서 처리 뒤에 쿼리문자열을 통해 중복 방지용 이름 fileName1 값을 같이 넘겨준다. -->
파일1 다운로드 : <a id="downA" href="#"><%=originalName1%></a>
<script type="text/javascript">
// 영문파일은 그냥 다운로드 클릭시 정상작동하지만 한글파일명을 쿼리문으로 날릴경우 인코딩 문제가 발생할 수 있다. 한글이 깨져 정상작동하지 않을 수 있음
// 따라서, 쿼리문자열에 한글을 보낼 때는 항상 인코딩을 해서 보내주도록 하자.
document.getElementById("downA").addEventListener("click", function(event) {
event.preventDefault(); // a 태그의 기본 동작을 막음
event.stopPropagation(); // 이벤트의 전파를 막음
// fileName1을 utf-8로 인코딩한다.
var fName = encodeURIComponent("<%=fileName1%>");
// 인코딩된 파일이름을 쿼리문자열에 포함시켜 다운로드 페이지로 이동
window.location.href = "fileDown1.jsp?file_name="+fName;
});
</script>

파일다운로드를 위한 페이지: fileDown1.jsp

<%@page import="java.net.URLEncoder"%>
<%@page import="java.io.FileInputStream"%>
<%@page import="java.io.File"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>파일 다운로드 페이지</title>
</head>
<%
// a태그의 href로 fileDown1.jsp?file_name="<%=fileName1 을 통해 전달한
// 중복 방지 처리한 파일명 값을 가져온다.
String fileName = request.getParameter("file_name");
// 업로드한 폴더의 위치와 업로드 폴더의 이름을 알아야 한다.
String savePath = "uploadFile"; // WebContent/uploadFile
// 위의 폴더는 상대경로이고 절대경로 기준의 진짜 경로를 구해와야한다.
String sDownPath = request.getRealPath(savePath);
System.out.println("다운로드 폴더 절대 경로 위치 : " + sDownPath);
System.out.println("fileName1 : " + fileName);
// 저장되어 있는 폴더경로/저장된 파일명 으로 풀 path를 만들어준다.
// 자바에서는 \를 표시하기 위해서는 \를 한번 더 붙여주기 때문에 \\로 해준다.
String sFilePath = sDownPath + "\\" + fileName; // ex)c:\\uploadPath\\image.jpg
System.out.println("sFilePath : " + sFilePath);
// 풀 path에 대한걸 파일 객체로 인식시킨다.
File outputFile = new File(sFilePath);
// 저장된 파일을 읽어와 저장할 버퍼를 임시로 만들고 버퍼의 용량은 이전에 한번에 업로드할 수 있는 파일크기로 지정한다.
byte[] temp = new byte[1024*1024*10]; // 10M
// 파일을 읽어와야 함으로 inputStream을 연다.(풀패스를 가지는 파일 객체를 이용해 input스트림을 형성한다.)
FileInputStream in = new FileInputStream(outputFile);
// 유형 확인 : 읽어올 경로의 파일의 유형 -> 페이지 생성할 때 타입을 설정해야 한다.
String sMimeType = getServletContext().getMimeType(sFilePath);
System.out.println("유형 : " + sMimeType);
// 지정되지 않은 유형 예외처리
if ( sMimeType == null ){
// 관례적인 표현
sMimeType = "application.octec-stream"; // 일련된 8bit 스트림 형식
// 유형이 알려지지 않은 파일에 대한 읽기 형식 지정
}
// 파일 다운로드 시작
// 유형을 지정해 준다.
response.setContentType(sMimeType); // 응답할 페이지가 text/html;charset=utf-8을
// 파일 mime 타입으로 지정해 준다.
// 업로드 파일의 제목이 깨질 수 있으므로 인코딩을 해준다.
String sEncoding = new String(fileName.getBytes("euc-kr"),"8859_1");
//String B = "utf-8";
//String sEncoding = URLEncoder.encode(A,B);
// 기타 내용을 헤더에 올려야 한다.
// 기타 내용을 보고 브라우저에서 다운로드 시 화면에 출력시켜 준다.
String AA = "Content-Disposition";
String BB = "attachment;filename="+sEncoding;
response.setHeader(AA,BB);
// 브라우저에 쓰기
ServletOutputStream out2 = response.getOutputStream();
int numRead = 0;
// 바이트 배열 temp의 0번부터 numRead번까지 브라우저로 출력
// 파일이 위치한 곳에 연결된 inputStream에서 읽되 끝(-1) 전까지 while을 돈다.
while((numRead = in.read(temp,0,temp.length)) != -1){ // temp 배열에 읽어올건데 0번째 인덱스부터 한번에 최대 temp.length 만큼 읽어온다.
// 읽어올게 더이상 없으면 -1을 리턴하면서 while문을 빠져나감
// 브라우저에 출력 : 근대 header 정보를 attachment로 해놓았음으로 다운로드가 된다.
out2.write(temp,0,numRead); // temp배열에 있는 데이터의 0번째부터 최대 numRead만큼 출력한다.
}
// 자원 해제
out2.flush();
out2.close();
in.close();
%>

공문파일업로드시 Reuqest Headers


'Jsp And Servlet' 카테고리의 다른 글

JSTL  (0) 2018.05.31
세션(Session)  (0) 2018.05.31
쿠키(Cookie)  (0) 2018.05.31
서블릿(Servlet) 과 HTTP Request/Response Header  (0) 2018.05.29
Comments