APK文件 (对应的windows服务器端已经架设好,可以直接下载进行测试)
在前面一篇文章:【源码】c#编写的安卓客户端与Windows服务器程序进行网络通信 中我们探讨了,如何通过xamarin技术,完成安卓客户端与Windows服务器的通信,这篇文章,我们探讨一下使用场景非常多的文件传输.
先谈一下为什么使用xamarin.android技术吧,之前有开发过一个公文系统,c#语言开发,服务器部署在Windows Server 2003上,客户端采用Winform技术(.net2.0),使用了一段时间后,客户提出希望系统能够支持安卓移动端。首先想到了用java语言进行开发,用java写安卓程序应该是最好不过了,但是难点出现了,就是如何让java编写的安卓客户端与现有的Windows服务器上的程序通信,探索多日无果,于是想起了xamarin.adnroid技术,使用此技术,可以集成原有的C#通信框架,TCP通信这一块就解决了.这样做还有一个好处,即能够与原有的服务器端程序无缝集成,服务器端程序同时支持Windows客户端与安卓客户端。
学习Xamarin.Android的时间不长,水平有限,希望本文能够抛砖引玉,对xamarin开发有经验的朋友请多多指点,不足之处敬请批评指正。
本Demo效果图如下
当用户点击“从服务器获取文件”按钮后,服务器端会收到相应的请求,并开始通过TCP连接发送数据,本例中,服务器发送一张图片(大小为20k),客户端收到后,新建一个名称为"msdc"的文件夹,并把文件存储在此文件夹中。`
我们来看一下开发过程:
第一步:在Main.axml文件中,增加一个按钮
<Button android:id="@+id/btnGetFile" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="从服务器获取文件" />
第二步:
客户端的MainActivity.cs文件中,编写该按钮相对应的方法
Button buttonGetFile = FindViewById<Button>(Resource.Id.btnGetFile); buttonGetFile.Click += new EventHandler(buttonGetFile_Click);
void buttonGetFile_Click(object sender, EventArgs e) { GetFileFromServer(); }
public void GetFileFromServer() { //传递的参数为本地保存的路径 string filePath = GetFileSavePath(this); //发送一个请求给服务器,服务器收到该请求后,开始发送文件 newTcpConnection.SendObject ("GetFileFromServer", filePath); }
private String GetFileSavePath(Context context) { String filePath; if (checkSDCard()) { filePath = Android.OS.Environment.GetExternalStoragePublicDirectory("").ToString() + @"/MSDC/";//File.Separator } else { filePath = context.CacheDir.AbsolutePath + @"/MSDC/"; } Java.IO.File file = new Java.IO.File(filePath); if (!file.Exists()) { Boolean b = file.Mkdirs(); } else { } return filePath; }
//检测是否存在SD卡 private Boolean checkSDCard() { if (Android.OS.Environment.ExternalStorageState.Equals(Android.OS.Environment.MediaMounted)) { return true; } else { return false; } }
第三步:看一下服务器端的处理程序
NetworkComms.AppendGlobalIncomingPacketHandler<string>("GetFileFromServer", IncomingReqMobileUpFile);
private void IncomingReqMobileUpFile(PacketHeader header, Connection connection, string filePath) { //在此Demo中,我们直接指定一个文件,进行发送 string filename = AppDomain.CurrentDomain.BaseDirectory + "Files\\" + "msdc.jpg"; string fileID = FileIDCreator.GetNextFileID(NetworkComms.NetworkIdentifier.ToString()); SendFile sendFile = new SendFile(fileID, filename, filePath, connection, customOptions ); sendFile.NowSendFile(); }
using System; using System.Collections.Generic; using System.Text; using NetworkCommsDotNet; using System.ComponentModel; using System.IO; using NetworkCommsDotNet; using DPSBase; using Mobile.Entity ; using System.Threading ; namespace MobileServer { public class SendFile { //取消文件的发送 private volatile bool canceled = false; private FileTransFailReason fleTransFailReason = FileTransFailReason.Error ; /// <summary> /// The name of the file /// 文件名 /// </summary> public string Filename { get; private set; } /// <summary> /// The connectionInfo corresponding with the source /// 连接信息 /// </summary> /// <summary> /// 收发参数 /// </summary> private SendReceiveOptions sendReceiveOptions; public SendReceiveOptions SendReceiveOptions { get { return sendReceiveOptions; } set { sendReceiveOptions = value; } } private Connection connection; public Connection Connection { get { return connection; } set { connection = value; } } //文件ID 用于管理文件 和文件的发送 取消发送相关 private string fileID; public string FileID { get { return fileID; } set { fileID = value; } } //文件传输后存储的路径 客户端传过来的路径 再传回去 private string filePath; public string Filepath { get { return filePath; } set { filePath = 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 SentBytes { get; private set; } /// <summary> /// Getter which returns the completion of this file, between 0 and 1 ///已经完成的百分比 /// </summary> public double CompletedPercent { get { return (double)SentBytes / 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> /// <summary> /// Returns true if the completed percent equals 1 /// 是否完成 /// </summary> public bool IsCompleted { get { return SentBytes == SizeBytes; } } /// <summary> /// Private object used to ensure thread safety /// </summary> object SyncRoot = new object(); /// <summary> ///Event subscribed to by GUI for updates /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Create a new ReceivedFile /// </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 SendFile(string fileID, string filename, string filePath, Connection connection, SendReceiveOptions sendReceiveOptions ) { //文件ID this.fileID = fileID; this.Filename = filename; this.filePath = filePath; this.connection = connection; this.sendReceiveOptions = sendReceiveOptions; } public void NowSendFile() { new Action(this.StartSendFile).BeginInvoke(null, null); } public void StartSendFile() { try { //Create a fileStream from the selected file //根据选择的文件创建一个文件流 FileStream stream = new FileStream(this.Filename, FileMode.Open, FileAccess.Read); //Wrap the fileStream in a threadSafeStream so that future operations are thread safe //包装成线程安全的数据流 ThreadSafeStream safeStream = new ThreadSafeStream(stream); //Get the filename without the associated path information //获取不包含路径信息的文件名 string shortFileName = System.IO.Path.GetFileName(Filename); long sendChunkSizeBytes = 4096; this.SizeBytes = stream.Length; long totalBytesSent = 0; do { //Check the number of bytes to send as the last one may be smaller long bytesToSend = (totalBytesSent + sendChunkSizeBytes < stream.Length ? sendChunkSizeBytes : stream.Length - totalBytesSent); //Wrap the threadSafeStream in a StreamSendWrapper so that we can get NetworkComms.Net //to only send part of the stream. StreamSendWrapper streamWrapper = new StreamSendWrapper(safeStream, totalBytesSent, bytesToSend); //We want to record the packetSequenceNumber //我们希望记录包的顺序号 long packetSequenceNumber; //Send the select data connection.SendObject("PartialFileData", streamWrapper, sendReceiveOptions, out packetSequenceNumber); //Send the associated SendInfo for this send so that the remote can correctly rebuild the data //把包的顺序号记录在 SendInfo类中。 connection.SendObject("PartialFileDataInfo", new SendInfo(fileID, shortFileName, filePath, stream.Length, totalBytesSent, packetSequenceNumber), sendReceiveOptions); totalBytesSent += bytesToSend; //更新已经发送的字节的属性 SentBytes += bytesToSend; ////Update the GUI with our send progress //UpdateSendProgress((double)totalBytesSent * 100 / stream.Length); if (! this.canceled) { Thread.Sleep(30); } } while ((totalBytesSent < stream.Length) && !this.canceled); //AddLineToLog("Completed file send to '" + connection.ConnectionInfo.ToString() + "'."); } catch (CommunicationException) { } catch (Exception ex) { } } } }
第四步:客户端接收服务器发来的文件
//处理文件数据 <2> NetworkComms.AppendGlobalIncomingPacketHandler<byte[]>("PartialFileData", IncomingPartialFileData); //处理文件信息 <3> NetworkComms.AppendGlobalIncomingPacketHandler<SendInfo>("PartialFileDataInfo", IncomingPartialFileDataInfo);
private void IncomingPartialFileData(PacketHeader header, Connection connection, byte[] data) { try { SendInfo info = null; ReceivedFile file = null; //Perform this in a thread safe way lock (syncLocker) { //Extract the packet sequence number from the header //The header can also user defined parameters //获取数据包的顺序号 long sequenceNumber = header.GetOption(PacketHeaderLongItems.PacketSequenceNumber); //如果数据信息字典包含 "连接信息" 和 "包顺序号" if (incomingDataInfoCache.ContainsKey(connection.ConnectionInfo) && incomingDataInfoCache[connection.ConnectionInfo].ContainsKey(sequenceNumber)) { //We have the associated SendInfo so we can add this data directly to the file //根据顺序号,获取相关SendInfo记录 info = incomingDataInfoCache[connection.ConnectionInfo][sequenceNumber]; //从信息记录字典中删除相关记录 incomingDataInfoCache[connection.ConnectionInfo].Remove(sequenceNumber); //Check to see if we have already initialised this file //检查相关连接上的文件是否存在,如果不存在,则添加相关文件{ReceivedFile} if (!receivedFiles.ContainsKey(info.FileID)) { ReceivedFile receivedFile = new ReceivedFile(info.FileID, info.Filename, info.FilePath, connection.ConnectionInfo, info.TotalBytes); receivedFile.FileTransCompleted += new Action<string>(this.receivedFile_FileTransCompleted); receivedFiles.Add(info.FileID, receivedFile); } file = receivedFiles[info.FileID]; } else { //We do not yet have the associated SendInfo so we just add the data to the cache //如果不包含顺序号,也不包含相关"连接信息",添加相关连接信息 if (!incomingDataCache.ContainsKey(connection.ConnectionInfo)) incomingDataCache.Add(connection.ConnectionInfo, new Dictionary<long, byte[]>()); //在数据字典中添加相关"顺序号"的信息 incomingDataCache[connection.ConnectionInfo].Add(sequenceNumber, data); } } //If we have everything we need we can add data to the ReceivedFile if (info != null && file != null && !file.IsCompleted) { file.AddData(info.BytesStart, 0, data.Length, data); //Perform a little clean-up 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) { //If an exception occurs we write to the log window and also create an error file } }
private void IncomingPartialFileDataInfo(PacketHeader header, Connection connection, SendInfo info) { try { byte[] data = null; ReceivedFile file = null; //Perform this in a thread safe way lock (syncLocker) { //Extract the packet sequence number from the header //The header can also user defined parameters //从 SendInfo类中获取相应数据类的信息号 以便可以对应。 long sequenceNumber = info.PacketSequenceNumber; if (incomingDataCache.ContainsKey(connection.ConnectionInfo) && incomingDataCache[connection.ConnectionInfo].ContainsKey(sequenceNumber)) { //We already have the associated data in the cache data = incomingDataCache[connection.ConnectionInfo][sequenceNumber]; incomingDataCache[connection.ConnectionInfo].Remove(sequenceNumber); //Check to see if we have already initialised this file if (!receivedFiles.ContainsKey(info.FileID)) { ReceivedFile receivedFile = new ReceivedFile(info.FileID, info.Filename, info.FilePath, connection.ConnectionInfo, info.TotalBytes); receivedFile.FileTransCompleted += new Action<string>(this.receivedFile_FileTransCompleted); receivedFiles.Add(info.FileID, receivedFile); } file = receivedFiles[info.FileID]; } else { //We do not yet have the necessary data corresponding with this SendInfo so we add the //info to the cache if (!incomingDataInfoCache.ContainsKey(connection.ConnectionInfo)) incomingDataInfoCache.Add(connection.ConnectionInfo, new Dictionary<long, SendInfo>()); incomingDataInfoCache[connection.ConnectionInfo].Add(sequenceNumber, info); } } //If we have everything we need we can add data to the ReceivedFile if (data != null && file != null && !file.IsCompleted) { file.AddData(info.BytesStart, 0, data.Length, data); //Perform a little clean-up 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) { } }
MainActivity.cs中添加相应的字典变量
//接收文件字典 Dictionary<string, ReceivedFile> receivedFiles = new Dictionary<string, ReceivedFile>(); /// <summary> /// Incoming partial data cache. Keys are ConnectionInfo, PacketSequenceNumber. Value is partial packet data. /// </summary> Dictionary<ConnectionInfo, Dictionary<long, byte[]>> incomingDataCache = new Dictionary<ConnectionInfo, Dictionary<long, byte[]>>(); /// <summary> /// Incoming sendInfo cache. Keys are ConnectionInfo, PacketSequenceNumber. Value is sendInfo. /// </summary> Dictionary<ConnectionInfo, Dictionary<long, SendInfo>> incomingDataInfoCache = new Dictionary<ConnectionInfo, Dictionary<long, SendInfo>>();
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Android.App; using Android.Content; using Android.OS; using Android.Runtime; using Android.Views; using Android.Widget; using System.IO; using NetworkCommsDotNet; using DPSBase; using Mobile.Entity; namespace Mobile.Client { public class ReceivedFile { //////传输过程 ////public event Action<string, long, long> FileTransProgress; //////传输完成 public event Action<string> FileTransCompleted; //////传输中断 ////public event Action<string, FileTransDisrupttedType> 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 string TempFilePath = ""; //文件最后的保存路径 public string SaveFilePath = ""; /// <summary> /// Create a new ReceivedFile /// </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 ReceivedFile(string fileID, string filename, string filePath, ConnectionInfo sourceInfo, long sizeBytes) { string tempSizeBytes = sizeBytes.ToString(); this.fileID = fileID; this.Filename = filename; this.SourceInfo = sourceInfo; this.SizeBytes = sizeBytes; //如果临时文件已经存在,则添加.data后缀 this.TempFilePath = filePath + filename + ".data"; while (File.Exists(this.TempFilePath)) { this.TempFilePath = this.TempFilePath + ".data"; } this.SaveFilePath = filePath + filename; //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); } /// <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); ////EventsHelper.Fire<string, long, long>(this.FileTransProgress, FileID, SizeBytes, ReceivedBytes); if (ReceivedBytes == SizeBytes) { data.Flush(); SaveFileToDisk(SaveFilePath); data.Close(); EventsHelper.Fire<string>(this.FileTransCompleted, FileID); } } catch (Exception exception) { //触发文件传输中断事件 //this.FileTransDisruptted(Filename, FileTransDisrupttedType.InnerError); ////EventsHelper.Fire<string, FileTransDisrupttedType>(this.FileTransDisruptted, FileID, FileTransDisrupttedType.InnerError); } } } } 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取消,并且触发文件传输中断事件 ////EventsHelper.Fire<string, FileTransDisrupttedType>(this.FileTransDisruptted, FileID, FileTransDisrupttedType.InnerError); } /// <summary> /// Saves the completed file to the provided saveLocation /// 保存文件到指定位置 /// </summary> /// <param name="saveLocation">Location to save file</param> public void SaveFileToDisk(string saveLocation) { if (ReceivedBytes != SizeBytes) throw new Exception("Attempted to save out file before data is complete."); if (!File.Exists(TempFilePath)) throw new Exception("The transferred file should have been created within the local application directory. Where has it gone?"); //File.Delete(saveLocation); //覆盖文件 File.Copy(TempFilePath, saveLocation, true); } /// <summary> /// Closes and releases any resources maintained by this file /// </summary> public void Close() { try { data.Dispose(); } catch (Exception) { } try { data.Close(); } catch (Exception) { } } } }
第五步:另外在Mobile.Entity类库中还添加了一个SendInfo方法,用于服务器发送文件给客户端是,传递文件相关信息
using System; using System.Collections.Generic; using System.Text; using ProtoBuf; namespace Mobile.Entity { /// <summary> /// Information class used to associate incoming data with the correct ReceivedFile /// 发送信息 /// </summary> [ProtoContract] public class SendInfo { /// <summary> /// Corresponding filename /// 文件名 /// </summary> [ProtoMember(1)] public string Filename { get; private set; } /// <summary> /// The starting point for the associated data /// 开始字节 /// </summary> [ProtoMember(2)] public long BytesStart { get; private set; } /// <summary> /// The total number of bytes expected for the whole ReceivedFile /// 总字节 /// </summary> [ProtoMember(3)] public long TotalBytes { get; private set; } /// <summary> /// The packet sequence number corresponding to the associated data /// 数据包顺序号 /// </summary> [ProtoMember(4)] public long PacketSequenceNumber { get; private set; } ///<summary> ///文件ID /// </summary> [ProtoMember(5)] public string FileID { get; private set; } //文件在本机的存储位置 [ProtoMember(6)] public string FilePath { get; private set; } /// <summary> /// Private constructor required for deserialisation /// </summary> public SendInfo() { } /// <summary> /// Create a new instance of SendInfo /// </summary> /// <param name="filename">Filename corresponding to data</param> /// <param name="totalBytes">Total bytes of the whole ReceivedFile</param> /// <param name="bytesStart">The starting point for the associated data</param> /// <param name="packetSequenceNumber">Packet sequence number corresponding to the associated data</param> public SendInfo(string fileID, string filename, string filePath, long totalBytes, long bytesStart, long packetSequenceNumber) { this.FileID = fileID; this.Filename = filename; this.FilePath = filePath; this.TotalBytes = totalBytes; this.BytesStart = bytesStart; this.PacketSequenceNumber = packetSequenceNumber; } } }
感谢大家关注