星期四, 7月 14, 2011

[C#] 實作 Multipart/Form-data 上傳檔案

以下範例只能支援小檔案,大檔會把StringBuilder 撐爆,需加以改良
可參考前一篇文章的教學:[JAVA] Simulator to POST multipart/form-data using HttpURLConnection
其實整個POST流程只要了解用任何語言都很好實作:)



class MultiPartFormOutputStream
    {
        private Stream dataOutputStream;

        private StringBuilder sbRequBody = new StringBuilder();
       
        // unique random value
     private string boundary = null;
     // each Parameter line separate 
     private string CRLF = "\r\n";
     // optional prefix char
     private string PREFIX = "--";
        
        public MultiPartFormOutputStream(Stream outputSteam, string boundary)
        {
            this.boundary = boundary;
            this.dataOutputStream = outputSteam;
        }

        public void writeFormFieldData() { 
        
        }

        public void writeFormFileData(string fieldName,string fileName,string miniType, byte[] filedata){
        
        /*
        --boundary\r\n
        Content-Disposition: form-data; name="<fieldname>"; filename="<filename>"\r\n
        Content-Type: <mime-type>\r\n
        \r\n
        <file-data>\r\n
        */
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.boundary);
            this.sbRequBody.Append(this.CRLF);
            this.sbRequBody.Append("Content-Disposition: form-data; name=\"");
            this.sbRequBody.Append(fieldName);
            this.sbRequBody.Append("\"; filename=\"");
            this.sbRequBody.Append(fileName);
            this.sbRequBody.Append("\"");
            this.sbRequBody.Append(this.CRLF);
            if (miniType != null)
            {
                this.sbRequBody.Append("Content-Type:");
                this.sbRequBody.Append(miniType);
                this.sbRequBody.Append(this.CRLF);
                this.sbRequBody.Append(this.CRLF);
            }

            // file data
            string fileStr = Encoding.UTF8.GetString(filedata);
            this.sbRequBody.Append(fileStr);

            this.sbRequBody.Append(this.CRLF);
        }

        public void close() {
            // important!! write final boundary
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.boundary);
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.CRLF);

            String requestBody = this.sbRequBody.ToString();
            #region show console
#if DEBUG
            //Console.WriteLine("--multipart/form-data request body--");
            //Console.WriteLine(requestBody);
#endif
            #endregion

            byte[] sendRequBody = Encoding.UTF8.GetBytes(requestBody);

            this.dataOutputStream.Write(sendRequBody, 0, sendRequBody.Length);
            //this.dataOutputStream.Flush();
            //this.dataOutputStream.Close();

            this.sbRequBody.Clear();
        }
    }

改良的寫法,直接以Stream.write逐步寫入串流,不使用Stringbuilder串接所有Post Data,寫入的Buffer會將它切割成指定的block size(請設定fileBlockSize變數),避免傳送太大的buffer而造成timeout。
class MultiPartFormOutputStream
    {
        public delegate void uploadProgressDelegate(int percent);
        public event uploadProgressDelegate uploadProgress;

       
        //request stream
        private Stream postStream;

        //post header parameter data to stream 
        private StringBuilder sbRequBody = new StringBuilder();
   
        // unique random value
     private string boundary = null;

     // each Parameter line separate 
     private string CRLF = "\r\n";

     // optional prefix char
     private string PREFIX = "--";

        private int fileChunk = 4096;

        public MultiPartFormOutputStream(Stream outputSteam, string boundary)
        {
            this.boundary = boundary;
            this.postStream = outputSteam;
        }

        public void writeFormFieldData(string fieldName,string fieldValue) {

            #region debug info
#if DEBUG
            Console.WriteLine("-------------ThreadName:{0} writeFormFieldData start-------------", Thread.CurrentThread.Name);
#endif
            #endregion

           /*
            --boundary/r/n 
            Content-Disposition: form-data; name=""/r/n 
            /r/n 
            /r/n 
           */
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.boundary);
            this.sbRequBody.Append(this.CRLF);
            this.sbRequBody.Append("Content-Disposition: form-data; name=\"");
            this.sbRequBody.Append(fieldName);
            this.sbRequBody.Append("\"");
            this.sbRequBody.Append(this.CRLF);
            this.sbRequBody.Append(this.CRLF);
            this.sbRequBody.Append(fieldValue);
            this.sbRequBody.Append(this.CRLF);
            #region debug info
#if DEBUG
            Console.WriteLine(this.sbRequBody.ToString(), Thread.CurrentThread.Name);
#endif
            #endregion
            byte[] postFieldBoundary = Encoding.UTF8.GetBytes(this.sbRequBody.ToString());
            this.postStream.Write(postFieldBoundary, 0, postFieldBoundary.Length);
            this.sbRequBody.Clear();

            #region debug info
#if DEBUG
            Console.WriteLine("-------------ThreadName:{0} writeFormFieldData end-------------", Thread.CurrentThread.Name);
#endif
            #endregion
        }

        public void writeFromFileData(string fieldName, string fileName, string mimeType, String path)
        {
            #region debug info
#if DEBUG
            Console.WriteLine("-------------ThreadName:{0} writeFormFileData(path) start-------------", Thread.CurrentThread.Name);
#endif
            #endregion

            #region pre header
            /*
                --boundary\r\n
                Content-Disposition: form-data; name=""; filename=""\r\n
                Content-Type: \r\n
                \r\n
                \r\n
            */
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.boundary);
            this.sbRequBody.Append(this.CRLF);
            this.sbRequBody.Append("Content-Disposition: form-data; name=\"");
            this.sbRequBody.Append(fieldName);
            this.sbRequBody.Append("\"; filename=\"");
            this.sbRequBody.Append(fileName);
            this.sbRequBody.Append("\"");
            this.sbRequBody.Append(this.CRLF);
            if (mimeType != null)
            {
                this.sbRequBody.Append("Content-Type:");
                this.sbRequBody.Append(mimeType);
                this.sbRequBody.Append(this.CRLF);
                this.sbRequBody.Append(this.CRLF);
            }
            #region debug info
#if DEBUG
            Console.WriteLine(this.sbRequBody.ToString(), Thread.CurrentThread.Name);
#endif
            #endregion
            byte[] postFileBoundary = Encoding.UTF8.GetBytes(this.sbRequBody.ToString());
            this.postStream.Write(postFileBoundary, 0, postFileBoundary.Length);
            this.sbRequBody.Clear();
            #endregion

            #region chunk
            //chunk buffer
            byte[] filebuffer = new byte[this.fileChunk];
            long offset = 0;
            int readByteCount = 0;
            int percent = 0;
            long fileStreamLen = 0;
            #region stream write
            FileStream oFile = null;
            try
            {
                #region read chunk

                oFile = new FileStream(path, FileMode.Open);

                fileStreamLen = oFile.Length;

                do
                {
                    readByteCount = oFile.Read(filebuffer, 0, this.fileChunk);

                    //send current buffer
                    this.postStream.Write(filebuffer, 0, readByteCount);

                    #region progress bar
                    if (this.uploadProgress != null && fileStreamLen > 0)
                    {
                        //current offset
                        offset += readByteCount;
                        var currentPercent = (int)(((double)offset) / fileStreamLen * 100);
                        Console.WriteLine("Current = " + currentPercent);
                        Console.WriteLine(currentPercent + "% File Position:{0}", oFile.Position);

                        //pass current upload percent
                        this.uploadProgress(currentPercent);
                    }
                    #endregion

                } while (readByteCount > 0);

                oFile.Flush();
                oFile.Close();
                oFile.Dispose();
                #endregion
            }
            catch (Exception ex)
            {
#if DEBUG
                Console.WriteLine(ex.Message);
#endif
            }
            #endregion

            #endregion

            #region end header
            //this.sbRequBody.Append(this.CRLF);
            #region debug info
#if DEBUG
            Console.WriteLine(this.CRLF, Thread.CurrentThread.Name);
#endif
            #endregion
            byte[] CRLFByte = Encoding.UTF8.GetBytes(this.CRLF);
            this.postStream.Write(CRLFByte, 0, CRLFByte.Length);
            #endregion
#if DEBUG
            Console.WriteLine("-------------ThreadName:{0} writeFormFileData(path) end-------------", Thread.CurrentThread.Name);
#endif
        }

        public void writeFormFileData(string fieldName,string fileName,string miniType, Stream filedata)
        {
            #region debug info
#if DEBUG
            Console.WriteLine("-------------ThreadName:{0} writeFormFileData start-------------", Thread.CurrentThread.Name);
#endif
            #endregion

            #region pre header
            /*
                --boundary\r\n
                Content-Disposition: form-data; name=""; filename=""\r\n
                Content-Type: \r\n
                \r\n
                \r\n
            */
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.boundary);
            this.sbRequBody.Append(this.CRLF);
            this.sbRequBody.Append("Content-Disposition: form-data; name=\"");
            this.sbRequBody.Append(fieldName);
            this.sbRequBody.Append("\"; filename=\"");
            this.sbRequBody.Append(fileName);
            this.sbRequBody.Append("\"");
            this.sbRequBody.Append(this.CRLF);
            if (miniType != null)
            {
                this.sbRequBody.Append("Content-Type:");
                this.sbRequBody.Append(miniType);
                this.sbRequBody.Append(this.CRLF);
                this.sbRequBody.Append(this.CRLF);
            }
            byte[] postFileBoundary = Encoding.UTF8.GetBytes(this.sbRequBody.ToString());
            this.postStream.Write(postFileBoundary, 0, postFileBoundary.Length);
            this.sbRequBody.Clear();
            #endregion

            #region chunk 
            //chunk buffer
            byte[] filebuffer = new byte[this.fileChunk];
            int offset = 0;
            int readByteCount = 0;
            int percent = 0;
            long fileStreamLen = filedata.Length;
            long filePosition = 0;

            #region debug info
#if DEBUG 
            Console.WriteLine("---ThreadName:{0},filedata.Position:{1}---", Thread.CurrentThread.Name,filedata.Position);
#endif
            #endregion
            //reset filedata position
            //如果讓每個執行緒共享同一個file stream時一定要重設
            filedata.Position = 0;

            #region debug info
#if DEBUG
            Console.WriteLine("---ThreadName:{0},Reset filedata.Position:{1}---", Thread.CurrentThread.Name, filedata.Position);
#endif
            #endregion

            #region stream write
            do
            {
                #region debug info
#if DEBUG
                //Console.WriteLine("ThreadName:{0},Pre filedata.Position:{1}", Thread.CurrentThread.Name, filedata.Position);
#endif
                #endregion

                //reset
                filedata.Position = filePosition;
                //read buffer
                readByteCount = filedata.Read(filebuffer, 0, this.fileChunk);
                //send current buffer
                this.postStream.Write(filebuffer, 0, readByteCount);
           
                #region progress bar
                if (this.uploadProgress != null)
                {
                    //current offset
                    offset += readByteCount;
                    filePosition = offset;
                    var currentPercent = (int)(((double)offset) / fileStreamLen * 100);
                    if (currentPercent == percent)
                        continue;

                    percent = currentPercent;

                    #region debug info
#if DEBUG
                    Console.WriteLine("ThreadName:{0}, filedata.Position:{1}", Thread.CurrentThread.Name, filedata.Position);
#endif
                    #endregion

                    //pass current upload percent
                    this.uploadProgress(percent);
                }
                #endregion

            } while (readByteCount > 0);   
           
            #endregion       
           
            #endregion
            
            #region end header
            //this.sbRequBody.Append(this.CRLF);
            byte[] CRLFByte = Encoding.UTF8.GetBytes(this.CRLF);
            this.postStream.Write(CRLFByte, 0, CRLFByte.Length);
            #endregion

            Console.WriteLine("-------------ThreadName:{0} writeFormFileData end-------------", Thread.CurrentThread.Name);
        }

        public void close()
        {
#if DEBUG
            Console.WriteLine("-------------ThreadName:{0} close start-------------", Thread.CurrentThread.Name);
#endif
            #region important!! write final boundary
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.boundary);
            this.sbRequBody.Append(this.PREFIX);
            this.sbRequBody.Append(this.CRLF);
            #region debug info
#if DEBUG
            Console.WriteLine(this.sbRequBody.ToString(), Thread.CurrentThread.Name);
#endif
            #endregion
            byte[] endBoundary = Encoding.UTF8.GetBytes(this.sbRequBody.ToString());
            this.postStream.Write(endBoundary, 0, endBoundary.Length);
            #endregion

            this.sbRequBody.Clear();
            this.postStream.Flush();
            this.postStream.Close();
#if DEBUG
            Console.WriteLine("-------------ThreadName:{0} close end-------------", Thread.CurrentThread.Name);
#endif
        }
    }

實作RESTClient,預設的HttpWebRequest是無法傳大檔的,需設定 request.AllowWriteStreamBuffering = false;,但傳太大的檔案容易造成Server的timeout。可以設request.Timeout = -1,但需自已避免無限等待的錯誤。
public delegate void uploadProgressDelegate(int percent);
        public event uploadProgressDelegate uploadProgress;

        private NameValueCollection fieldNameValueCollection = new NameValueCollection();
       
        private List<MultiPartFieldFileItem> fileItems = new List<MultiPartFieldFileItem>();

        public RESTClient()
        {
          
        }

        public void AddFormFieldItem(string fieldName,string fieldValue) {
            this.fieldNameValueCollection.Add(fieldName, fieldValue);
        }
        public void AddFormFileItem(MultiPartFieldFileItem fileItem) {
            this.fileItems.Add(fileItem);
        }
    
        public Hashtable POSTMultiPartFormData(string url) {

            Hashtable jsonObj = null;

            string boundary = new Random(DateTime.Now.Millisecond).Next().ToString();
#if DEBUG
            Console.WriteLine("POSTMultiPartFormData request url:" + url);
            Console.WriteLine("boundary:" + boundary);
#endif
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.SendChunked = true;
            request.AllowWriteStreamBuffering = false;
            request.Method = "POST";
            request.ContentType = string.Format("multipart/form-data;boundary={0}", boundary);
            request.KeepAlive = true;

            Stream stream = null;
            try
            {
                stream = request.GetRequestStream();
            }
            catch
            {
                stream.Close();
                request.Abort();
            }

            //initial Multipart-from stream 
            MultiPartFormOutputStream multipartStream = new MultiPartFormOutputStream(stream, boundary);
            multipartStream.uploadProgress += new MultiPartFormOutputStream.uploadProgressDelegate(multipartStream_uploadProgress);
            
            //field data parameter
            foreach (string fieldName in this.fieldNameValueCollection.AllKeys) {
                string fieldValue = this.fieldNameValueCollection.Get(fieldName);
                multipartStream.writeFormFieldData(fieldName,fieldValue);
            }
            this.fieldNameValueCollection.Clear();

            //file data parameter
            int fileItemsCount = this.fileItems.Count ;
            if (fileItemsCount > 0) { 
                for(int i=0;i<fileItemsCount;i++){
                    MultiPartFieldFileItem file = (MultiPartFieldFileItem)this.fileItems[i];
                    //request.ContentLength = file.Filedata.Length;
                    if (file.FileFrom == FileFrom.Stream)
                        multipartStream.writeFormFileData(file.FieldName, file.Filename, file.MIMEType, file.Filedata);
                    else
                        multipartStream.writeFromFileData(file.FieldName, file.Filename, file.MIMEType, file.Path);
                }
            }
            this.fileItems.Clear();
            //important!!
            multipartStream.send();
            WebResponse response = request.GetResponse();
           
            //Console.WriteLine("Status Description: " + (int) ((HttpWebResponse)response).StatusCode);
            stream = response.GetResponseStream();
            StreamReader reader = new StreamReader(stream);

            jsonObj = (Hashtable)JSON.JsonDecode(reader.ReadToEnd());  //responsed data decoded as JSON object

            reader.Close();
            stream.Close();
            response.Close();
            request.Abort();

            

            return jsonObj;
        }
        public void multipartStream_uploadProgress(int percent) { 
            if(this.uploadProgress!=null){
                this.uploadProgress(percent);
            }
        }
Reference:

Upload files with HTTPWebrequest (multipart/form-data)

沒有留言:

張貼留言

留個話吧:)

其他你感興趣的文章

Related Posts with Thumbnails