2014年11月11日 星期二

如何透過串流傳遞圖片



剛好案子的需要,透過Windows Form 把圖片檔上傳,但我對於底層的技術Socket 不是很擅長 於是我透過WCF將圖片上傳到server 端,同樣達到同樣的功能,以下記錄自我學習過程。


案例一
WCF傳輸圖片
啟動者
Foxpro程式
主要流程:
1.    輸入客戶醫事機構號、診所名稱、檔名、路徑
2.    經由WCF傳輸圖片,並接收圖片儲存成『醫事機構號_診所名稱/年月日/檔名.jpg
3.    回傳status : -1初始值 0:失敗  1:成功
4.    回傳ErrorMsg: 空白(成功執行,並未回傳錯誤訊息)
案件圖
範例
   CooperXSend cooperXSendObj = new CooperXSend();
                cooperXSendObj.ClinicNo = "1001";
                cooperXSendObj.ClinicName = "診所";
                cooperXSendObj.FilePath = "C:\Users\Public\Pictures\Sample Pictures";
                cooperXSendObj.FileName = "Jellyfish.jpg";
                cooperXSendObj.SendImage();

Console.Write(cooperXSendObj.Status + cooperXSendObj.ErrorMsg);


步驟一: server端架設WCF
IService.cs

 [ServiceContract]
public interface IService
{
        [OperationContract]
        isPassOK SaveStreamToFile(RemoteFileInfo request);

        [OperationContract]
        isPassOK SaveLog(SaveDBLog saveSqlLog);
}

[MessageContract]
public class SaveDBLog
{
    [MessageHeader(MustUnderstand = true)]
    public string SqlServer;

    [MessageHeader(MustUnderstand = true)]
    public string SqlDbName;

    [MessageHeader(MustUnderstand = true)]
    public string SqlUserID;

    [MessageHeader(MustUnderstand = true)]
    public string SqlPassword;

    [MessageHeader(MustUnderstand = true)]
    public string ClinicNo; //醫事機構號

    [MessageHeader(MustUnderstand = true)]
    public string ClinicName;//診所名稱

    [MessageHeader(MustUnderstand = true)]
    public string Physavepath;//儲存檔案實體路徑

    [MessageHeader(MustUnderstand = true)]
    public string YearMonth; //申報費用年月

    [MessageHeader(MustUnderstand = true)]
    public string CaseNO;   //案件類別

    [MessageHeader(MustUnderstand = true)]
    public string FlowKey;   //案件流水號

    [MessageHeader(MustUnderstand = true)]
    public string FileName;//檔名含路徑

    [MessageHeader(MustUnderstand = true)]
    public string SendImageErrorMsg;
}

宣告WCF的介面時,如果有使用到物件類別時,需用MessageContract 宣告成物件,MessageHeader 宣告物件屬性,用OperationContract 宣告在WCF中所有使用的方法



Service.cs


[ServiceBehavior]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class Service : IService
{

    public isPassOK SaveStreamToFile(RemoteFileInfo request)
    {
        isPassOK isPassOKObj = new isPassOK();
        try
        {
            isPassOKObj.strErrorMsg = "";
            //設定儲存圖片的根路徑
            string strRootFile = ConfigurationManager.AppSettings["rootPath"].ToString();
            string strFirstDirectory = request.ClinicNo;
            string strSecondDirectory = request.YearMonth; //改成費用年月路徑
            string strSavePath = strRootFile + "\\" + strFirstDirectory + "\\" + strSecondDirectory;
            string strSaveName = request.FileName;
            Stream stream = request.FileByteStream;
            string fileFullPath = request.FileName;

            if (stream.Length == 0)
            {
                isPassOKObj.strErrorMsg = "無串流資料";
                return isPassOKObj;
            }

            #region 檢查是否需建立資料夾

            if (!Directory.Exists(strRootFile))
            {
                Directory.CreateDirectory(strRootFile);
            }

            if (!Directory.Exists(strRootFile + "\\" + strFirstDirectory))
            {
                Directory.CreateDirectory(strRootFile + "\\" + strFirstDirectory);
            }

            if (!Directory.Exists(strRootFile + "\\" + strFirstDirectory + "\\" + strSecondDirectory))
            {
                Directory.CreateDirectory(strRootFile + "\\" + strFirstDirectory + "\\" + strSecondDirectory);
            }
            #endregion

            #region 寫檔案

            using (FileStream fileStream = System.IO.File.Create(strSavePath + "\\" + strSaveName, (int)stream.Length))
            {
                int bufferLen = 65000;
                byte[] bytesInStream = new byte[stream.Length];
                int offset = 0;
                int totalBytes = (int)stream.Length;

                while (totalBytes > 0)
                {
                    //萬一圖片太小
                    if (bufferLen > totalBytes)
                    {
                        bufferLen = totalBytes;
                    }

                    int n = stream.Read(bytesInStream, offset, bufferLen);

                    fileStream.Write(bytesInStream, offset, bufferLen);

                    if (n != 0)
                    {
                        offset += n - 1;
                        totalBytes -= n;

                        if (totalBytes < bufferLen)
                        {
                            bufferLen = totalBytes;
                        }
                    }
                }

                fileStream.Close();
                stream.Close();

                isPassOKObj = isCheckedFileExist(strSavePath + "\\" + strSaveName,request.CRC);

                SaveDBLog saveDbLog = new SaveDBLog();
                saveDbLog.ClinicName = request.ClinicName;
                saveDbLog.Physavepath = strSavePath + "\\";
                saveDbLog.ClinicNo = request.ClinicNo;
                saveDbLog.YearMonth = request.YearMonth;
                saveDbLog.CaseNO = request.CaseNO;
                saveDbLog.FlowKey = request.FlowKey;
                saveDbLog.FileName = request.FileName;
                saveDbLog.SqlServer = ConfigurationManager.AppSettings["DBServer"].ToString();
                saveDbLog.SqlDbName = ConfigurationManager.AppSettings["DBName"].ToString();
                saveDbLog.SqlUserID = ConfigurationManager.AppSettings["DBUserID"].ToString();
                saveDbLog.SqlPassword = ConfigurationManager.AppSettings["DBPassword"].ToString();
                saveDbLog.SendImageErrorMsg = isPassOKObj.strErrorMsg;
                isPassOKObj.strErrorMsg += ((isPassOK)SaveLog(saveDbLog)).strErrorMsg; //寫入資料庫
            }
            #endregion
        }
        catch (Exception ex)
        {
            isPassOKObj.strErrorMsg = ex.Message;
        }
        return isPassOKObj;
    }

public isPassOK isCheckedFileExist(string strFileName, string strCheckSum)
    {
        isPassOK isPassOkObj = new isPassOK();       
        long intCheckSumTo16=0;    //foxpro 傳進的檢查碼
        long intHashSumTo16 = 0;   //c# 算出的檢查碼

        try
        {          
            FileInfo fi = new FileInfo(strFileName);
            if (fi.Exists)
            {
                CRC32 crc32 = new CRC32();
                String hash = String.Empty;
                using (FileStream fs = File.Open(strFileName, FileMode.Open)) 
                {
                    byte[] b = crc32.ComputeHash(fs);
                   
                    for (int i = b.Length-1; i >= 0; i--)
                    {
                       hash += b[i].ToString("x2").ToLower();  //每次加總bites checksum
                    }
                }

                //由客戶端加總的檢查碼
                intCheckSumTo16 = Convert.ToInt64(strCheckSum);  //如果數字太大會溢位,所以使用long
               //伺服器轉換成16進位的checksum
                intHashSumTo16 = Int64.Parse(hash, System.Globalization.NumberStyles.HexNumber);
               //比較伺服器和客戶端的檢查碼
                if (intHashSumTo16 != intCheckSumTo16)
                {
                    isPassOkObj.strErrorMsg = "比對檢核碼不一致";
                }              
            }
            else
            {
                isPassOkObj.strErrorMsg = "查無此檔案";
            }
            return isPassOkObj;
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message);
        }
    }

作者在架設WCF發生錯誤訊息,請參考http://www.roelvanlisdonk.nl/?p=1232 AspNetCompatibilityRequirements 屬性為TURE 加上此設定後,情況就正常了。

根據MSDN官方說法 當AspNetCompatibilityRequirements =false 時,WCF Service 預設執行Mixed Transports Mode,所以要明確的說明兼容模式。

回到正題,圖片從客戶端傳送到伺服器端,中間也許有封包遺失,圖片會上傳不完整… 所以要有檢查機制,告訴客戶端目前是否以傳送完成。

所以透過CRC32 CheckSum 檢查

// Tamir Khason http://khason.net/
//
// Released under MS-PL : 6-Apr-09

using System;
using System.Collections;
using System.IO;
using System.Security.Cryptography;
using System.Text;

   /// <summary>Implements a 32-bits cyclic redundancy check (CRC) hash algorithm.</summary>
   /// <remarks>This class is not intended to be used for security purposes. For security applications use MD5, SHA1, SHA256, SHA384,
   /// or SHA512 in the System.Security.Cryptography namespace.</remarks>
   public class CRC32 : HashAlgorithm {

      #region CONSTRUCTORS
      /// <summary>Creates a CRC32 object using the <see cref="DefaultPolynomial"/>.</summary>
      public CRC32() : this(DefaultPolynomial) {
      }

      /// <summary>Creates a CRC32 object using the specified polynomial.</summary>
      /// <remarks>The polynomical should be supplied in its bit-reflected form. <see cref="DefaultPolynomial"/>.</remarks>
      [CLSCompliant(false)]
      public CRC32(uint polynomial) {
         HashSizeValue = 32;
         _crc32Table = (uint[])_crc32TablesCache[polynomial];
         if (_crc32Table == null) {
            _crc32Table = CRC32._buildCRC32Table(polynomial);
            _crc32TablesCache.Add(polynomial, _crc32Table);
         }
         Initialize();
      }

      // static constructor
      static CRC32() {
         _crc32TablesCache = Hashtable.Synchronized(new Hashtable());
         _defaultCRC = new CRC32();
      }
      #endregion

      #region PROPERTIES
      /// <summary>Gets the default polynomial (used in WinZip, Ethernet, etc.)</summary>
      /// <remarks>The default polynomial is a bit-reflected version of the standard polynomial 0x04C11DB7 used by WinZip, Ethernet, etc.</remarks>
      [CLSCompliant(false)]
      public static readonly uint DefaultPolynomial = 0xEDB88320; // Bitwise reflection of 0x04C11DB7;
      #endregion

      #region METHODS
      /// <summary>Initializes an implementation of HashAlgorithm.</summary>
      public override void Initialize() {
         _crc = _allOnes;
      }

      /// <summary>Routes data written to the object into the hash algorithm for computing the hash.</summary>
      protected override void HashCore(byte[] buffer, int offset, int count) {
         for (int i = offset; i < count; i++) {
            ulong ptr = (_crc & 0xFF) ^ buffer[i];
            _crc >>= 8;
            _crc ^= _crc32Table[ptr];
         }
      }

      /// <summary>Finalizes the hash computation after the last data is processed by the cryptographic stream object.</summary>
      protected override byte[] HashFinal() {
         byte[] finalHash = new byte[4];
         ulong finalCRC = _crc ^ _allOnes;

         finalHash[0] = (byte)((finalCRC >> 0) & 0xFF);
         finalHash[1] = (byte)((finalCRC >> 8) & 0xFF);
         finalHash[2] = (byte)((finalCRC >> 16) & 0xFF);
         finalHash[3] = (byte)((finalCRC >> 24) & 0xFF);

         return finalHash;
      }

      /// <summary>Computes the CRC32 value for the given ASCII string using the <see cref="DefaultPolynomial"/>.</summary>
      public static int Compute(string asciiString) {
         _defaultCRC.Initialize();
         return ToInt32(_defaultCRC.ComputeHash(asciiString));
      }

      /// <summary>Computes the CRC32 value for the given input stream using the <see cref="DefaultPolynomial"/>.</summary>
      public static int Compute(Stream inputStream) {
         _defaultCRC.Initialize();
         return ToInt32(_defaultCRC.ComputeHash(inputStream));
      }

      /// <summary>Computes the CRC32 value for the input data using the <see cref="DefaultPolynomial"/>.</summary>
      public static int Compute(byte[] buffer) {
         _defaultCRC.Initialize();
         return ToInt32(_defaultCRC.ComputeHash(buffer));
      }

      /// <summary>Computes the hash value for the input data using the <see cref="DefaultPolynomial"/>.</summary>
      public static int Compute(byte[] buffer, int offset, int count) {
         _defaultCRC.Initialize();
         return ToInt32(_defaultCRC.ComputeHash(buffer, offset, count));
      }

      /// <summary>Computes the hash value for the given ASCII string.</summary>
      /// <remarks>The computation preserves the internal state between the calls, so it can be used for computation of a stream data.</remarks>
      public byte[] ComputeHash(string asciiString) {
         byte[] rawBytes = ASCIIEncoding.ASCII.GetBytes(asciiString);
         return ComputeHash(rawBytes);
      }

      /// <summary>Computes the hash value for the given input stream.</summary>
      /// <remarks>The computation preserves the internal state between the calls, so it can be used for computation of a stream data.</remarks>
      new public byte[] ComputeHash(Stream inputStream) {
         byte[] buffer = new byte[4096];
         int bytesRead;
         while ((bytesRead = inputStream.Read(buffer, 0, 4096)) > 0) {
            HashCore(buffer, 0, bytesRead);
         }
         return HashFinal();
      }

      /// <summary>Computes the hash value for the input data.</summary>
      /// <remarks>The computation preserves the internal state between the calls, so it can be used for computation of a stream data.</remarks>
      new public byte[] ComputeHash(byte[] buffer) {
         return ComputeHash(buffer, 0, buffer.Length);
      }

      /// <summary>Computes the hash value for the input data.</summary>
      /// <remarks>The computation preserves the internal state between the calls, so it can be used for computation of a stream data.</remarks>
      new public byte[] ComputeHash(byte[] buffer, int offset, int count) {
         HashCore(buffer, offset, count);
         return HashFinal();
      }
      #endregion

      #region PRIVATE SECTION
      private static uint _allOnes = 0xffffffff;
      private static CRC32 _defaultCRC;
      private static Hashtable _crc32TablesCache;
      private uint[] _crc32Table;
      private uint _crc;

      // Builds a crc32 table given a polynomial
      private static uint[] _buildCRC32Table(uint polynomial) {
         uint crc;
         uint[] table = new uint[256];

         // 256 values representing ASCII character codes.
         for (int i = 0; i < 256; i++) {
            crc = (uint)i;
            for (int j = 0; j < 8; j++) {
               if ((crc & 1) == 1)
                  crc = (crc >> 1) ^ polynomial;
               else
                  crc >>= 1;
            }
            table[i] = crc;
         }

         return table;
      }
      private static int ToInt32(byte[] buffer) {
         return BitConverter.ToInt32(buffer, 0);
      }
      #endregion

   }


步驟二: client 加入服務參考
用戶端à端點à新增端點

輸入你專案的契約

每個屬性都有其意義,生怕才疏學淺的我,怕錯誤的觀念帶給大家,所以屬性值僅供參考而已


自動產生出來的設定值,可直接對app.config 直接調整
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_IService" />
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://192.168.1.222:85/service.svc" binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_IService" contract="ServiceReference1.IService"
                name="BasicHttpBinding_IService" />
        </client>
    </system.serviceModel>
</configuration>

客戶端的程式
    public void SendImage()
        {
            WriteEvent.WrittingEventLog writeObj = new WriteEvent.WrittingEventLog();
            try
            {
                ServiceClient wcfSendImage = new ServiceClient();
                //檢查檔名存在與否
                string strFullPathFileName = Path.Combine(FilePath, FileName);               
                if (!File.Exists(strFullPathFileName))
                {
                    _status = 0;
                    _errorMsg = "檔案不存在";
                }
                else
                {
                    #region 寫入圖片
                    FileInfo fileInfo = new FileInfo(strFullPathFileName);

                    var stream = new MemoryStream();
                    using (var file = File.OpenRead(strFullPathFileName))
                    {
                        file.CopyTo(stream);
                    }
                    stream.Position = 0L;

                    RemoteFileInfo remoteFileObj = new RemoteFileInfo();
                    remoteFileObj.ClinicName = ClinicName;
                    remoteFileObj.ClinicNo = ClinicNo;
                    remoteFileObj.YearMonth = YearMonth;
                    remoteFileObj.CaseNO = CaseNo;
                    remoteFileObj.FlowKey = FlowKey;
                    remoteFileObj.FilePath = FilePath;
                    remoteFileObj.FileName = FileName; //純檔案名稱
                    remoteFileObj.CRC = CRC;
                    remoteFileObj.FileByteStream = stream;

                    //呼叫WCF儲存至SERVER
                    _errorMsg = wcfSendImage.SaveStreamToFile( remoteFileObj.CRC, remoteFileObj.CaseNO, remoteFileObj.ClinicName, remoteFileObj.ClinicNo, remoteFileObj.FileName, remoteFileObj.FilePath,remoteFileObj.FlowKey, remoteFileObj.YearMonth, remoteFileObj.FileByteStream);
                    #endregion
                }

                //代表有錯誤訊息
                if (_errorMsg != "")
                {
//not pass
                    writeObj.writeToFile(_errorMsg);
                    _status = 0;
                }
                else
                {
                    //pass
                    _status = 1;
                }

            }
            catch (Exception ex)
            {
               writeObj.writeToFile(ex.Message);
                _status = 0;
                _errorMsg = "非預期錯誤:" + ex.Message;
            }
        }

感謝大家有耐心的讀完… 希望對大家有所收穫

沒有留言:

張貼留言