본문 바로가기

C#/근웹 연대기

c#으로 근본 없는 웹서버 개발기 15 : multipart/form-data 파일전송

⚠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
url: /test/multipart
http: HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 52251
Cache-Control: max-age=0
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

가장 아래쪽에서 boundary 값을 확인할 수 있다.

 

그리고 나서 body 데이터를 확인하면.

body: ------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="fname"
 
hello
------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="lname"
 
world
------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="myfile"; filename=""
Content-Type: application/octet-stream
2
------WebKitFormBoundaryqhUDv7SMEAaZL1I0
Content-Disposition: form-data; name="myfile"; filename="tree.jpg"
Content-Type: image/jpeg
 
ÿØÿà?JFIF?????ÿÛ?C?

 
 
 %# , #&')*)-0-(0%()(ÿÛ?C
...(길어서 생략)

바운더리 값(------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