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; } GetFileSavePath //检测是否存在SD卡 private Boolean checkSDCard() { if (Android.OS.Environment.ExternalStorageState.Equals(Android.OS.Environment.MediaMounted)) { return true; } else { return false; } } checkSDCard 检查是否存在SD卡 第三步:看一下服务器端的处理程序 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) { } } } } SendFile方法 |
|
第四步:客户端接收服务器发来的文件
//处理文件数据 <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 } } IncomingPartialFileData 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) { } } IncomingPartialFileDataInfo 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) { } } } } ReceivedFile |