⚠WARNING
ASP.NET에 대한 포스팅이 아닙니다
나가실 문은 오른쪽 하단입니다
multipart/form-data는 명칭에서 유추해볼 수 있듯이 여러 개의 파일이나 텍스트를 form에 쟁여서 전송이 필요할 때 사용하게 되는 Content-type이다.
전송 데이터를 확인하기 위하여 다음과 같이 html form을 작성하였다.
<body>
<form action="/test/multipart" method="post" enctype="multipart/form-data">
<label for="fname">First name:</label>
<input type="text" id="fname" name="fname" value="hello"><br><br>
<label for="lname">Last name:</label>
<input type="text" id="lname" name="lname" value="world"><br><br>
<label for="myfile">file:</label>
<input type="file" id="myfile" name="myfile"><br><br>
<label for="multiple">file:</label>
<input multiple="multiple" id="multiple" name="myfile" type="file"/><br><br>
<input type="submit" value="Submit">
</form>
</body>
그리고 다음과 같이 입력을 하고.
Submit을 누르면 콘솔 창에서 아래와 같이 헤더 값이 나온다.
method: POST
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
가장 아래쪽에서 boundary 값을 확인할 수 있다.
그리고 나서 body 데이터를 확인하면.
body: ------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="fname"
------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="lname"
------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="myfile"; filename=""
Content-Type: application/octet-stream
------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="myfile"; filename="tree.jpg"
바운더리 값(------WebKitF...)을 기준으로 데이터가 분리되어 있는 것을 알 수 있다.
그리고 추가로 확장자가 없는 것은 타입이 octet-stream이 된다는 것도 알 수 있다.
그리고 파일이 아닌 것은 타입이 없다는 것을 확인할 수 있다.
데이터에서 이미지 파일을 추출하기 위해 Request 클래스에 다음과 같이 메서드를 작성하였다.
JO makeMultipart(){
string boundary = "\r\n--" + parseColon(this["Content-Type"])["boundary"];
string data = "\r\n" + body;
var jo = new JO();
int idx = 0;
while((idx = data.IndexOf(boundary,idx)) != -1){
idx += boundary.Length + 2;
if(data.Length <= idx + 2) break;
if(data.Substring(idx,20) != "Content-Disposition:"){
this.err = "bad format";
return null;
}
idx += 20;
int idx2 = data.IndexOf(";", idx);
if (idx2 == -1)
{
this.err = "bad format";
return null;
}
JO newObj = J.O(("Content-Disposition", data[idx..idx2].Trim()));
idx = idx2;
idx2 = data.IndexOf("\r\n",idx);
var str = data[idx..idx2];
var tdic = parseColon(str);
string name = tdic.GetD("name").Replace("\"","");
if(name==null){
this.err = "bad format";
return null;
}
jo[name] ??= new JL();
(jo[name] as JL).Add(newObj);
foreach(var tem in tdic) newObj[tem.Key] = tem.Value;
do{
idx = idx2 + 2;
idx2 = data.IndexOf("\r\n",idx);
if(idx2==-1){
this.err = "bad format";
return null;
}
str = data[idx..idx2];
if(str.Trim().Length>0){
var sp = str.Split(':');
newObj.Add(sp.First(), sp.LastOrDefault());
}else break;
}while(true);
idx = idx2 + 2;
idx2 = data.IndexOf(boundary,idx);
if(idx2 - idx < 0){
this.err = "bad format";
return null;
}
var substr = data[idx..idx2];
var bytes = Util.GetBytes(substr);
newObj["data"] = newObj.Has("filename") ? bytes : Encoding.UTF8.GetString(bytes);
}
return jo;
}
참고로 JO, JL 은 전편에서 만든 클래스로, 스스로 베타테스터가 되기 위해 억지로 사용하였다.
여기서 치명적인 단점은 String을 byte로 변환할 때마다 새로 할당되므로 오버헤드가 발생된다.
변환 없이 쭉 byte로 해도 되긴 하는데 문자열일 때 쓸 수 있는 메서드를 쓰지 못하는.. 애로함이 있다.
c++ 이였으면 포인터로 간단하고 빠릿빠릿하게 연산이 가능할 터인데, C#에서는 문자열 하나가 2바이트 char타입이기에 최적화를 하기 위해서는 마샬링이 필요할 것 같다.
그렇다
싱글보드처럼 성능이 낮은 곳이나 최적화가 필요한 곳에는 c#은 답이 아니다.
그리고 마지막으로 파일을 저장하는 부분을 작성하였다.
case "/test/multipart":
foreach(J tem in (req.jo["myfile"] as JL).getList())
{
byte[] bytes = tem["data"];
if(bytes?.Length is null or 0) continue;
string fanme = tem["filename"].Trim('"');
Util.saveFile("public/upload/" + fanme, bytes);
}
string fname = req.jo["fname"][0]["data"];
string lname = req.jo["lname"][0]["data"];
resText(client, fname +" " + lname); return;
테스트 결과: 무난하게 tree.jpg 파일이 저장되었다.
tree.jpg