文件傳輸在客戶端,服務器端程序的應用是非常廣泛的,穩定的文件傳輸應該可以說是Tcp通訊的核心功能。下面我們來看一下如何基於networkcomms2.3.1來進行文件傳輸。最新的 v3版本做了一些加強,變化不是很大。
使用networkcomms2.3.1框架,您無需考慮粘包等問題,框架已經幫您處理好了。
我們看一下如何發送文件,相關代碼如下:
發送文件:
public void StartSendFile() {
//聲明一個文件流 FileStream stream = null; try { //FilePath是文件路徑,打開這個文件 //根據選擇的文件創建一個文件流 stream = new FileStream(this.FilePath, FileMode.Open, FileAccess.Read, FileShare.Read); //包裝成線程安全的數據流 ThreadSafeStream safeStream = new ThreadSafeStream(stream); //獲取不包含路徑信息的文件名 string shortFileName = System.IO.Path.GetFileName(FilePath); //根據參數中設定的值來角色發送的數據包的大小 因為文件很大 可能1個G 2個G 不可以一次性都發送
//每次都只發送一部分 至於每次發送多少 我們創建了一個fileTransOptions類來進行設定
long sendChunkSizeBytes = fileTransOptions.PackageSize; //總的文件大小 this.SizeBytes = stream.Length; long totalBytesSent = 0; //用一個循環方法發送數據,直到發送完成 do { //如果剩下的字節小於上面指定的每次發送的字節,即PackageSize的值,那麼發送此次發送的字節數為 剩下的字節數 否則 發送的字節長度為 PackageSize long bytesToSend = (totalBytesSent + sendChunkSizeBytes < stream.Length ? sendChunkSizeBytes : stream.Length - totalBytesSent);
//從ThreadSafeStream線程安全流中獲取本次發送的部分 (線程安全流,totalbytesSent 是已發送的數,在此處代表從ThreadSafeStream中截取的文件的開始位置,bytesToSend 代表此次截取的文件的長度) StreamSendWrapper streamWrapper = new StreamSendWrapper(safeStream, totalBytesSent, bytesToSend); //我們希望記錄包的順序號 long packetSequenceNumber; //發送文件的數據部分 並返回此次發送的順序號 這個順序號在下面的發送文件信息時會用到 起到一個對應的作用。 connection.SendObject("PartialFileData", streamWrapper, sendFileOptions, out packetSequenceNumber); //發送上面的文件的數據部分向對應的信息 包括文件ID 文件名 在服務器上存儲的位置 文件的總長度 totalBytesSent是已發送數,在此處用來傳遞給服務器後,服務器用來定位此部分數據存放的位置 進一步合成文件 connection.SendObject("PartialFileDataInfo", new SendInfo(fileID, shortFileName, destFilePath, stream.Length, totalBytesSent, packetSequenceNumber), sendFileOptions); totalBytesSent += bytesToSend; //更新已經發送的字節的屬性 SentBytes += bytesToSend; //觸發一個事件 UI可以依據此事件更新ProgressBar 動態的顯示文件更新的過程 FileTransProgress.Raise(this, new FTProgressEventArgs(FileID, SizeBytes, totalBytesSent)); //每發送一部分文件 都Sleep幾十毫秒,不然cpu會非常高 if (!((this.fileTransOptions.SleepSpan <= 0) || this.canceled)) { Thread.Sleep(this.fileTransOptions.SleepSpan); } } while ((totalBytesSent < stream.Length) && !this.canceled); if (!this.canceled) {
//觸發文件傳輸完成事件 UI可以調閱此事件 並彈出窗口報告文件傳輸完成 FileTransCompleted.Raise(this, new FTCompleteEventArgs(fileID)); } else { //觸發文件傳輸中斷事件 FileTransDisruptted.Raise(this, new FTDisruptEventArgs(FileID, FileTransFailReason.Error)); } } catch (CommunicationException ex) { LogTools.LogException(ex, "SendFile.StartSendFile"); FileTransDisruptted.Raise(this, new FTDisruptEventArgs(FileID, FileTransFailReason.Error)); } catch (Exception ex) { LogTools.LogException(ex, "SendFile.StartSendFile"); FileTransDisruptted.Raise(this, new FTDisruptEventArgs(FileID, FileTransFailReason.Error)); } finally { if (stream != null) { stream.Close(); } } }
接收文件 首先聲明2個字典類 用來存放接收到的文件 和接收到的文件信息
/// <summary>
/// 文件數據緩存 索引是 ConnectionInfo對象 數據包的順序號 值是數據
/// </summary>
Dictionary<ConnectionInfo, Dictionary<long, byte[]>> incomingDataCache = new Dictionary<ConnectionInfo, Dictionary<long, byte[]>>();
/// <summary>
/// 文件信息數據緩存 索引是 ConnectionInfo對象 數據包的順序號 值是文件信息數據
/// </summary>
Dictionary<ConnectionInfo, Dictionary<long, SendInfo>> incomingDataInfoCache = new Dictionary<ConnectionInfo, Dictionary<long, SendInfo>>();
在接收端定義2個相對應的文件接收方法 一個用來接收文件字節部分 一個用來接收文件字節部分對應的信息類
一般在構造函數中聲明
//處理文件數據
NetworkComms.AppendGlobalIncomingPacketHandler<byte[]>("PartialFileData", IncomingPartialFileData);
//處理文件信息
NetworkComms.AppendGlobalIncomingPacketHandler<SendInfo>("PartialFileDataInfo", IncomingPartialFileDataInfo);
接收文件字節
private void IncomingPartialFileData(PacketHeader header, Connection connection, byte[] data)
{
try
{
SendInfo info = null;
ReceiveFile file = null;
//以線程安全的方式執行操作
lock (syncRoot)
{
//獲取數據包的順序號
long sequenceNumber = header.GetOption(PacketHeaderLongItems.PacketSequenceNumber);
//如果數據信息字典包含 "連接信息" 和 "包順序號"
if (incomingDataInfoCache.ContainsKey(connection.ConnectionInfo) && incomingDataInfoCache[connection.ConnectionInfo].ContainsKey(sequenceNumber))
{
//根據順序號,獲取相關SendInfo記錄
info = incomingDataInfoCache[connection.ConnectionInfo][sequenceNumber];
//從信息記錄字典中刪除相關記錄
incomingDataInfoCache[connection.ConnectionInfo].Remove(sequenceNumber);
//檢查相關連接上的文件是否存在,如果不存在,則添加相關文件{ReceiveFile}
if (!recvManager.ContainsFileID(info.FileID))
{
recvManager.AddFile(info.FileID, info.Filename, info.FilePath, connection.ConnectionInfo, info.TotalBytes);
}
file = recvManager.GetFile(info.FileID);
}
else
{
//如果不包含順序號,也不包含相關"連接信息",添加相關連接信息
if (!incomingDataCache.ContainsKey(connection.ConnectionInfo))
incomingDataCache.Add(connection.ConnectionInfo, new Dictionary<long, byte[]>());
//在數據字典中添加相關"順序號"的信息
incomingDataCache[connection.ConnectionInfo].Add(sequenceNumber, data);
}
}
if (info != null && file != null && !file.IsCompleted)
{
file.AddData(info.BytesStart, 0, data.Length, data);
file = null;
data = null;
}
else if (info == null ^ file == null)
throw new Exception("Either both are null or both are set. Info is " + (info == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed."));
}
catch (Exception ex)
{
LogTools.LogException(ex, "IncomingPartialFileDataError");
}
}
private void IncomingPartialFileDataInfo(PacketHeader header, Connection connection, SendInfo info)
{
try
{
byte[] data = null;
ReceiveFile file = null;
//以線程安全的方式執行操作
lock (syncRoot)
{
//從 SendInfo類中獲取相應數據類的信息號 以便可以對應。
long sequenceNumber = info.PacketSequenceNumber;
if (incomingDataCache.ContainsKey(connection.ConnectionInfo) && incomingDataCache[connection.ConnectionInfo].ContainsKey(sequenceNumber))
{
data = incomingDataCache[connection.ConnectionInfo][sequenceNumber];
incomingDataCache[connection.ConnectionInfo].Remove(sequenceNumber);
if (!recvManager.ContainsFileID(info.FileID))
{
recvManager.AddFile(info.FileID, info.Filename, info.FilePath, connection.ConnectionInfo, info.TotalBytes);
}
file = recvManager.GetFile(info.FileID);
}
else
{
if (!incomingDataInfoCache.ContainsKey(connection.ConnectionInfo))
incomingDataInfoCache.Add(connection.ConnectionInfo, new Dictionary<long, SendInfo>());
incomingDataInfoCache[connection.ConnectionInfo].Add(sequenceNumber, info);
}
}
if (data != null && file != null && !file.IsCompleted)
{
file.AddData(info.BytesStart, 0, data.Length, data);
file = null;
data = null;
}
else if (data == null ^ file == null)
throw new Exception("Either both are null or both are set. Data is " + (data == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed."));
}
catch (Exception ex)
{
LogTools.LogException(ex, "IncomingPartialFileDataInfo");
}
}
public class ReceiveFile
{
//傳輸過程
public event EventHandler<FTProgressEventArgs> FileTransProgress;
//傳輸完成
public event EventHandler<FTCompleteEventArgs> FileTransCompleted;
//傳輸中斷
public event EventHandler<FTDisruptEventArgs> FileTransDisruptted;
/// <summary>
/// The name of the file
/// 文件名 (沒有帶路徑)
/// </summary>
public string Filename { get; private set; }
/// <summary>
/// The connectionInfo corresponding with the source
/// 連接信息
/// </summary>
public ConnectionInfo SourceInfo { get; private set; }
//文件ID 用於管理文件 和文件的發送 取消發送相關
private string fileID;
public string FileID
{
get { return fileID; }
set { fileID = value; }
}
/// <summary>
/// The total size in bytes of the file
/// 文件的字節大小
/// </summary>
public long SizeBytes { get; private set; }
/// <summary>
/// The total number of bytes received so far
/// 目前收到的文件的帶下
/// </summary>
public long ReceivedBytes { get; private set; }
/// <summary>
/// Getter which returns the completion of this file, between 0 and 1
///已經完成的百分比
/// </summary>
public double CompletedPercent
{
get { return (double)ReceivedBytes / SizeBytes; }
//This set is required for the application to work
set { throw new Exception("An attempt to modify read-only value."); }
}
/// <summary>
/// A formatted string of the SourceInfo
/// 源信息
/// </summary>
public string SourceInfoStr
{
get { return "[" + SourceInfo.RemoteEndPoint.ToString() + "]"; }
}
/// <summary>
/// Returns true if the completed percent equals 1
/// 是否完成
/// </summary>
public bool IsCompleted
{
get { return ReceivedBytes == SizeBytes; }
}
/// <summary>
/// Private object used to ensure thread safety
/// </summary>
object SyncRoot = new object();
/// <summary>
/// A memory stream used to build the file
/// 用來創建文件的數據流
/// </summary>
Stream data;
/// <summary>
///Event subscribed to by GUI for updates
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
//臨時文件流存儲的位置
public string TempFilePath = "";
//文件最後的保存路徑
public string SaveFilePath = "";
/// <summary>
/// Create a new ReceiveFile
/// </summary>
/// <param name="filename">Filename associated with this file</param>
/// <param name="sourceInfo">ConnectionInfo corresponding with the file source</param>
/// <param name="sizeBytes">The total size in bytes of this file</param>
public ReceiveFile(string fileID, string filename, string filePath, ConnectionInfo sourceInfo, long sizeBytes)
{
this.fileID = fileID;
this.Filename = filename;
this.SourceInfo = sourceInfo;
this.SizeBytes = sizeBytes;
//如果臨時文件已經存在,則添加.data後綴
this.TempFilePath = filePath + ".data";
while (File.Exists(this.TempFilePath))
{
this.TempFilePath = this.TempFilePath + ".data";
}
this.SaveFilePath = filePath;
//We create a file on disk so that we can receive large files
//我們在硬盤上創建一個文件,使得我們可以接收大的文件
//data = new FileStream(TempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 8 * 1024, FileOptions.DeleteOnClose);
data = new FileStream(this.TempFilePath, FileMode.OpenOrCreate);
}
/// <summary>
/// Add data to file
/// 添加數據到文件中
/// </summary>
/// <param name="dataStart">Where to start writing this data to the internal memoryStream</param>
/// <param name="bufferStart">Where to start copying data from buffer</param>
/// <param name="bufferLength">The number of bytes to copy from buffer</param>
/// <param name="buffer">Buffer containing data to add</param>
public void AddData(long dataStart, int bufferStart, int bufferLength, byte[] buffer)
{
lock (SyncRoot)
{
if (!this.canceled && (this.data != null))
{
try
{
data.Seek(dataStart, SeekOrigin.Begin);
data.Write(buffer, (int)bufferStart, (int)bufferLength);
ReceivedBytes += (int)(bufferLength - bufferStart);
FileTransProgress.Raise(this, new FTProgressEventArgs(FileID, SizeBytes, ReceivedBytes));
if (ReceivedBytes == SizeBytes)
{
this.data.Flush();
this.data.Close();
if (File.Exists(this.SaveFilePath))
{
File.Delete(this.SaveFilePath);
File.Move(this.TempFilePath, this.SaveFilePath);
}
else
{
File.Move(this.TempFilePath, this.SaveFilePath);
}
FileTransCompleted.Raise(this, new FTCompleteEventArgs(FileID));
}
}
catch (Exception exception)
{
//觸發文件傳輸中斷事件
//this.FileTransDisruptted(Filename, FileTransFailReason.Error);
FileTransDisruptted.Raise(this, new FTDisruptEventArgs(FileID, FileTransFailReason.Error));
}
}
}
NotifyPropertyChanged("CompletedPercent");
NotifyPropertyChanged("IsCompleted");
}
private volatile bool canceled;
public void Cancel(FileTransFailReason disrupttedType, bool deleteTempFile)
{
try
{
this.canceled = true;
this.data.Flush();
this.data.Close();
this.data = null;
if (deleteTempFile)
{
File.Delete(this.TempFilePath);
}
}
catch (Exception)
{
}
//通知 Receiver取消,並且觸發文件傳輸中斷事件
FileTransDisruptted.Raise(this, new FTDisruptEventArgs(FileID, FileTransFailReason.Error));
}
/// <summary>
/// Closes and releases any resources maintained by this file
/// </summary>
public void Close()
{
try
{
data.Dispose();
}
catch (Exception) { }
try
{
data.Close();
}
catch (Exception) { }
}
}
public class ReceiveFileDict
{
private object syncLocker = new object();
Dictionary<string, ReceiveFile> receivedFiles = new Dictionary<string, ReceiveFile>();
public bool ContainsFileID(string fileID)
{
lock (syncLocker)
{
return receivedFiles.ContainsKey(fileID);
}
}
public ReceiveFile GetFile(string fileID)
{
lock (syncLocker)
{
return receivedFiles[fileID];
}
}
//傳輸過程
public event EventHandler<FTProgressEventArgs> FileTransProgress;
//傳輸完成
public event EventHandler<FTCompleteEventArgs> FileTransCompleted;
//傳輸中斷
public event EventHandler<FTDisruptEventArgs> FileTransDisruptted;
public event EventHandler<FTCancelEventArgs> FileCancelRecv;
public ReceiveFileDict()
{
}
public void AddFile(string fileID, string filename, string filePath, ConnectionInfo sourceInfo, long sizeBytes)
{
ReceiveFile receivedFile = new ReceiveFile(fileID,filename,filePath,sourceInfo,sizeBytes);
receivedFile.FileTransProgress += new EventHandler<FTProgressEventArgs>(receivedFile_FileTransProgress);
receivedFile.FileTransCompleted += new EventHandler<FTCompleteEventArgs>(receivedFile_FileTransCompleted);
receivedFile.FileTransDisruptted += new EventHandler<FTDisruptEventArgs>(receivedFile_FileTransDisruptted);
receivedFiles.Add(fileID, receivedFile);
}
void receivedFile_FileTransDisruptted(object sender, FTDisruptEventArgs e)
{
lock (this.syncLocker)
{
if (this.receivedFiles.ContainsKey(e.FileID))
{
this.receivedFiles.Remove(e.FileID);
}
}
FileTransDisruptted.Raise(this, e);
}
void receivedFile_FileTransCompleted(object sender, FTCompleteEventArgs e)
{
lock (this.syncLocker)
{
if (this.receivedFiles.ContainsKey(e.FileID))
{
this.receivedFiles.Remove(e.FileID);
}
}
FileTransCompleted.Raise(this, e);
}
void receivedFile_FileTransProgress(object sender, FTProgressEventArgs e)
{
FileTransProgress.Raise(this, e);
}
// 請求取消文件的接收 FileRecTransViewer中會調用此方法
public void CancelRecFile(string fileID)
{
FileCancelRecv.Raise(this, new FTCancelEventArgs(fileID));
}
}
/// <summary>
/// A wrapper around a stream to ensure it can be accessed in a thread safe way. The .net implementation of Stream.Synchronized is not suitable on its own.
/// </summary>
public class ThreadSafeStream : IDisposable
{
private Stream stream;
private object streamLocker = new object();
/// <summary>
/// If true the internal stream will be disposed once the data has been written to the network
/// </summary>
public bool CloseStreamAfterSend { get; private set; }
/// <summary>
/// Create a thread safe stream. Once any actions are complete the stream must be correctly disposed by the user.
/// </summary>
/// <param name="stream">The stream to make thread safe</param>
public ThreadSafeStream(Stream stream)
{
this.CloseStreamAfterSend = false;
this.stream = stream;
}
/// <summary>
/// Create a thread safe stream.
/// </summary>
/// <param name="stream">The stream to make thread safe.</param>
/// <param name="closeStreamAfterSend">If true the provided stream will be disposed once data has been written to the network. If false the stream must be disposed of correctly by the user</param>
public ThreadSafeStream(Stream stream, bool closeStreamAfterSend)
{
this.CloseStreamAfterSend = closeStreamAfterSend;
this.stream = stream;
}
/// <summary>
/// The total length of the internal stream
/// </summary>
public long Length
{
get { lock (streamLocker) return stream.Length; }
}
/// <summary>
/// The current position of the internal stream
/// </summary>
public long Position
{
get { lock (streamLocker) return stream.Position; }
}
/// <summary>
/// Returns data from entire Stream
/// </summary>
/// <param name="numberZeroBytesPrefex">If non zero will append N 0 value bytes to the start of the returned array</param>
/// <returns></returns>
public byte[] ToArray(int numberZeroBytesPrefex = 0)
{
lock (streamLocker)
{
stream.Seek(0, SeekOrigin.Begin);
byte[] returnData = new byte[stream.Length + numberZeroBytesPrefex];
stream.Read(returnData, numberZeroBytesPrefex, returnData.Length - numberZeroBytesPrefex);
return returnData;
}
}
/// <summary>
/// Returns data from the specified portion of Stream
/// </summary>
/// <param name="start">The start position of the desired bytes</param>
/// <param name="length">The total number of desired bytes</param>
/// <param name="numberZeroBytesPrefex">If non zero will append N 0 value bytes to the start of the returned array</param>
/// <returns></returns>
public byte[] ToArray(long start, long length, int numberZeroBytesPrefex = 0)
{
if (length>int.MaxValue)
throw new ArgumentOutOfRangeException( "length", "Unable to return array whose size is larger than int.MaxValue. Consider requesting multiple smaller arrays.");
lock (streamLocker)
{
if (start + length > stream.Length)
throw new ArgumentOutOfRangeException("length", "Provided start and length parameters reference past the end of the available stream.");
stream.Seek(start, SeekOrigin.Begin);
byte[] returnData = new byte[length + numberZeroBytesPrefex];
stream.Read(returnData, numberZeroBytesPrefex, returnData.Length - numberZeroBytesPrefex);
return returnData;
}
}
/// <summary>
/// Return the MD5 hash of the current <see cref="ThreadSafeStream"/> as a string
/// </summary>
/// <returns></returns>
public string MD5CheckSum()
{
lock (streamLocker)
{
return MD5Stream(stream);
}
}
/// <summary>
/// Return the MD5 hash of part of the current <see cref="ThreadSafeStream"/> as a string
/// </summary>
/// <param name="start">The start position in the stream</param>
/// <param name="length">The length of stream to MD5</param>
/// <returns></returns>
public string MD5CheckSum(long start, int length)
{
using (MemoryStream partialStream = new MemoryStream(length))
{
lock (streamLocker)
{
StreamWriteWithTimeout.Write(stream, start, length, partialStream, 8000, 1000, 500);
return MD5Stream(partialStream);
}
}
}
/// <summary>
/// Calculate the MD5 of the provided stream
/// </summary>
/// <param name="streamToMD5">The stream to calcualte Md5 for</param>
/// <returns></returns>
private static string MD5Stream(Stream streamToMD5)
{
streamToMD5.Seek(0, SeekOrigin.Begin);
#if WINDOWS_PHONE
using(var md5 = new DPSBase.MD5Managed())
{
#else
using (var md5 = System.Security.Cryptography.MD5.Create())
{
#endif
return BitConverter.ToString(md5.ComputeHash(streamToMD5)).Replace("-", "");
}
}
/// <summary>
/// Writes all provided data to the internal stream starting at the provided position with the stream
/// </summary>
/// <param name="data"></param>
/// <param name="startPosition"></param>
public void Write(byte[] data, long startPosition)
{
if (data == null) throw new ArgumentNullException("data");
lock (streamLocker)
{
stream.Seek(startPosition, SeekOrigin.Begin);
stream.Write(data, 0, data.Length);
stream.Flush();
}
}
/// <summary>
/// Copies data specified by start and length properties from internal stream to the provided stream.
/// </summary>
/// <param name="destinationStream">The destination stream to write to</param>
/// <param name="startPosition"></param>
/// <param name="length"></param>
/// <param name="writeBufferSize">The buffer size to use for copying stream contents</param>
/// <param name="minTimeoutMS">The minimum time allowed for any sized copy</param>
/// <param name="timeoutMSPerKBWrite">The timouts in milliseconds per KB to write</param>
/// <returns>The average time in milliseconds per byte written</returns>
public double CopyTo(Stream destinationStream, long startPosition, long length, int writeBufferSize, double timeoutMSPerKBWrite = 1000, int minTimeoutMS = 500)
{
lock (streamLocker)
return StreamWriteWithTimeout.Write(stream, startPosition, length, destinationStream, writeBufferSize, timeoutMSPerKBWrite, minTimeoutMS);
}
/// <summary>
/// Call Dispose on the internal stream
/// </summary>
public void Dispose()
{
lock (streamLocker) stream.Dispose();
}
/// <summary>
/// Call Close on the internal stream
/// </summary>
public void Close()
{
lock (streamLocker) stream.Close();
}
}
/// <summary>
/// Used to send all or parts of a stream. Particularly usefull for sending files directly from disk etc.
/// </summary>
public class StreamSendWrapper : IDisposable
{
object streamLocker = new object();
/// <summary>
/// The wrapped stream
/// </summary>
public ThreadSafeStream ThreadSafeStream { get; set; }
/// <summary>
/// The start position to read from Stream
/// </summary>
public long Start { get; private set; }
/// <summary>
/// The number of bytes to read from Stream
/// </summary>
public long Length { get; private set; }
/// <summary>
/// Create a new stream wrapper and set Start and Length to encompass the entire Stream
/// </summary>
/// <param name="stream">The underlying stream</param>
public StreamSendWrapper(ThreadSafeStream stream)
{
this.ThreadSafeStream = stream;
this.Start = 0;
this.Length = stream.Length;
}
/// <summary>
/// Create a new stream wrapper
/// </summary>
/// <param name="stream">The underlying stream</param>
/// <param name="start">The start position from where to read data</param>
/// <param name="length">The length to read</param>
public StreamSendWrapper(ThreadSafeStream stream, long start, long length)
{
if (start < 0)
throw new Exception("Provided start value cannot be less than 0.");
if (length < 0)
throw new Exception("Provided length value cannot be less than 0.");
this.ThreadSafeStream = stream;
this.Start = start;
this.Length = length;
}
/// <summary>
/// Return the MD5 for the specific part of the stream only.
/// </summary>
/// <returns></returns>
public string MD5CheckSum()
{
using (MemoryStream ms = new MemoryStream())
{
ThreadSafeStream.CopyTo(ms, Start, Length, 8000);
#if WINDOWS_PHONE
using(var md5 = new DPSBase.MD5Managed())
{
#else
using (var md5 = System.Security.Cryptography.MD5.Create())
{
#endif
return BitConverter.ToString(md5.ComputeHash(ms)).Replace("-", "");
}
}
}
/// <summary>
/// Dispose the internal ThreadSafeStream
/// </summary>
public void Dispose()
{
ThreadSafeStream.Dispose();
}
}
www.cnblogs.com/networkcomms
www.networkcomms.cn