起因
最近在用C#實現一個安全通信軟件,基本思想是發送方在發送數據時先對數據進行加密再發送,同樣接收方從網絡上收到數據后先對數據進行解密再把解密后的數據遞交到上層應用。 實現方式是通過封裝TCPCLient類的一些方法,向外提供封裝好的安全SOCKET,自動完成數據的加解密。上層應用只需調用安全SOCKET進行原始數據的發送與接收並不用關心底層數據的加解密,使用方式與使用系統原始的SOCKET一樣。
public int Send(byte[] data) { try { int len = 0; byte[] full = null; if (IsEncrypt) { DataFormat sendData = new DataFormat(); sendData.SendData = data; //用己方私鑰簽名數據 sendData.SignData = Sign.SignData(data); //組裝原始數據與簽名數據 string encryptString = Convert.ToBase64String(sendData.SendData) + ClassUtility.Separator + Convert.ToBase64String(sendData.SignData); //加密發送數據 full = SCrypto.Encrypt(Encoding.ASCII.GetBytes(encryptString)); } else { //加密發送數據 full = SCrypto.Encrypt(data); } // 發送數據 len = full.Length; len = SendSource(full); return len; } catch (System.Exception ex) { ClassLog.WriteErrLog(ex); return -1; } } public byte[] Receive() { byte[] data = null; try { byte[] receive = ReceiveSource(); if (IsEncrypt) { //解密數據 string decryptString = Encoding.ASCII.GetString(SCrypto.Decrypt(receive)); //分拆原始數據與簽名 string[] DataArray = ClassUtility.splitByString(decryptString); DataFormat sendData = new DataFormat(); sendData.SendData = Convert.FromBase64String(DataArray[0]); sendData.SignData = Convert.FromBase64String(DataArray[1]); //用發送方公鑰驗證簽名 if (PubSign.VerifyData(sendData.SendData, sendData.SignData)) { data = sendData.SendData; } } else { //解密數據 data = SCrypto.Decrypt(receive); } } catch (System.Exception ex) { ClassLog.WriteErrLog(ex); } return data; }
錯誤
錯誤出現在當把通信雙方程序放在局域網內不同機器上運行,傳遞超過1M大小的文件時,程序在對接收到數據進行反序列化時拋出異常“在分析完成之前就遇到流結尾。”,但是傳遞幾k大小的文件又不會出現錯誤,如果都在同一機器上傳送文件也不會出錯。因為上層程序發送的數據是把自定義的消息類進行二進制序列化成byte數組,接收方收到數據后會進行相應的反序列操作把數據轉化成相應的消息類,所以此處錯誤會出現在對數據進行反序列的地方。
排錯
開始,以為是序列化代碼寫的有問題,不過細想下傳遞小文件和同一機器上測試都未出現錯誤,基本排除了這種可能。接着對發送的數據與接收的數據進行比對,發現收到的數據長度小於發送的數據,這也肯定了不是序列化代碼的問題,反序列化的就不是一個完整的數據,才導致反序列化時出現異常。
引起錯誤的可疑之處也縮小到了解密數據前的ReceiveSource函數,該函數的作用是從連接的緩沖區中讀取原始的數據。函數要達到的預期效果是能把一次交互過程中發送方發送的數據都能接收到,所以實現的代碼也是循環判斷緩沖區中是否有數據,有就讀出來。
public byte[] ReceiveSource() { int len = 0; int offset = 0; byte[] receive = new byte[BufferSize]; int availableSize = BufferSize; NetworkStream NetStream = Client.GetStream(); do { len = NetStream.Read(receive, offset, availableSize); offset += len; availableSize -= len; } while (NetStream.DataAvailable && availableSize > 0); if (availableSize > 0) { byte[] ReceiveData = new byte[offset]; Array.Copy(receive, ReceiveData, offset); receive = ReceiveData; } return receive; }
通過調試,發現了這個函數並沒有達到預期的效果,當發送的數據量很大時,有時沒有返回完整的數據。之后通過wireshark抓到了發送過來的數據包,也確定了數據確實是完整發送過來了只是程序沒有讀取出來,再分析代碼確認函數在循環讀取數據時考慮不周全,如果發送的數據量很大,網絡上實際需要多個TCP數據包進行發送,這樣數據包通過網絡到達接收端時前后肯定會有一定的延遲,所以數據到達接收方緩沖區也有一定的時間差,在循環讀取時有可能剛好已經讀了一些數據但下一個數據包的數據還未到達,此時緩沖區也沒有新的數據也就退出了循環,造成了接收方收到的數據不全。
修改
既然問題是接收方有可能接收不到完整的數據,易想到的解決辦法就是發送數據之前就先通知接收方將要發送的數據長度,接收方首先收到要接收的數據長度值,再循環從緩存區讀取數據,直到接收到指定長度的數據才返回。
public int SendSource(byte[] data) { NetworkStream NetStream = Client.GetStream(); //發送數據長度 byte[] dataLength = new byte[32]; System.BitConverter.GetBytes(data.Length).CopyTo(dataLength,0); NetStream.Write(dataLength, 0, dataLength.Length); NetStream.Write(data, 0, data.Length); return data.Length; } public byte[] ReceiveSource() { int len = 0; int offset = 0; int dataLength = 0; byte[] lenBytes = new byte[32]; byte[] receive = null; NetworkStream NetStream = Client.GetStream(); //接收數據長度 len = NetStream.Read(lenBytes, 0, 32); dataLength = BitConverter.ToInt32(lenBytes, 0); receive = new byte[dataLength]; while (offset < dataLength) { len = NetStream.Read(receive, offset, dataLength-offset); offset += len; } return receive; }
