如何在Windows Store和Windows Phone应用之间传送文件
首先要知道的事:它是如何工作的
如果我们曾有一些应用同时运行于Windows Phone和Win 8 Store中,我们可能会考虑使用一些版本之间的数据同步的机制。然而,由于两种程序之间无法直接访问对方的数据,这就要求它们之间能有某种方式可以进行文件传输。可行的方案的好几种,最流行的一种是使用Web Server(放置在某个地方作为中间介质)。然而,这种方式也有一些弊端,假如用户的万维网访问因为各种环境因素而被限制了,你的应用可能就没办法正常地和用户进行交互了。虽然在一般情况下的万维网访问是不会受到限制的,但实际情况并不总是这样乐观。另外,这种方式可能会产生一定金额的维护费用,你需要从你应用的收益中拿出一部分来支付这些费用。
通常情况下,用户都会拥有自己的网络连接,如:局域网,Ad-Hoc网络。很多时候,甚至连用户自己都没有意识到这一点。例如,假设用户有一个路由或者接入点,然后有一些设备(不管无线有线)连接在路由或者接入点上,这些处于当前网络中的设备形成了某种意义上的 “互联网”。此时,不管有没有万维网访问,与互联网相关的网络协议都能正常工作。
所以,我们可以跳过中间服务器,通过TCP协议实现同一网络实现设备间的直接通信。
TCP协议是互联网的基石。几乎所有访问互联网的程序都会使用TCP协议从远端服务器获取数据(大多数情况下,网络请求要经过几个服务器的路由,这里暂时不做讨论)。TCP 协议包含一个服务器(监听者)和一个客户端,这两个实体通过“请求”的阶段变化进行数据交互。“请求”必须进行初始化,客户端根据服务器的IP地址和指定端口打开一个连接即可完成。而服务器不能简单的去连接客户端。TCP协议的主要好处是任一方发送的数据包都能确保被另一方接收到。如果在传送过程中丢失了某个数据包,发送方就会重新发送丢失的数据包,直到接收方确认传送过程完成。
TCP协议基本上表现得像数据流一样,因此所有TCP套接字又称“流式套接字”。套接字是设备间互连所使用的底层基础对象。如果在数据队列中排队等待传输的数据太大,不适合放在一个单独的数据包里时,LAN驱动会把它拆分成多个包,挨个进行传送,直到所有包发送完成。但对于好奇的开发人员来说,上述过程已经被抽象化了。
TCP 套接字可以通过多种连接的方式打开:RFCOMM Bluetooth,共享同一接入点的设备(一个设备通过无线方式连接而其他设备通过以太网口连接时仍然适用),即便手机是通过USB连接到平板或者电脑上时,也依然可行。只要知道端口名称和IP地址,你就可以用TCP去连接这个星球上的任何电脑(或许地球之外的也可以)。
本文中,电脑将扮演服务器的角色,而手机将作为客户端。这里,电脑是指任何可以运行Windows Runtime程序的设备,包括ARM架构和X86架构的平板以及可以运行Windows 8的电脑。在Windows Phone 8发布之前,手机只能作为客户端。但是现在,手机也可以作为服务器来使用。有趣的是,服务器和客户端之间的界限并不明晰。任一方都可以作为服务器或者客户端,但是一个设备同时扮演这两种角色可能会导致一些不稳定的情况发生。
通过参照MSDN文档,让我们把这些将要使用的类熟悉一下。Stream Socket , Stream Socket Listener
文档非常详细,甚至都已经涵盖把事情做好的每一个步骤。
好了,让我们跳过文档,开始切入正题。我们需要启动一个监听者: 创建一个win 8 应用程序,然后创建一个看起来还过得去的界面。
这里建议创建一个新的类来存储所有东西,便于稍后使用。
我们所想发送的文件中包含了一些关于应用程序状态的随机数据。假设我们要在Windows Phone应用中继续做我们之前在Windows App上做的工作。那么我们就需要把重建的数据存储在这个要发送的的文件里。
我们把这个文件隐藏在Roaming文件夹中
string fileName = "MyFileName.txt";
你可以用以下代码创建这个文件(你可能会好奇为什么我们不await这个异步调用, 因为我们不需要这样做:该调用会在后台线程上创建。除非你想要在接下来的方法中使用这个文件,否则你不需要await它)
ApplicationData.Current.RoamingFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
使用以下代码你可以获得这个文件:
Var myfile =await ApplicationData.Current.RoamingFolder.GetFileAsync(fileName);
你可以使用FileIO 类的方法在这个文件里面写一些东西。请确保你真的在文件里写了东西,这样它的大小才会大于0。如果你想要观察当一个文件不能被放在一个数据包里时,流套接字的所产生行为,那么,你可以把写文件的那些代码放在while或者for循环中,这样你就可以人为的增加文件的大小。
此时,你已经准备了这个文件,那就该设置监听者了。由于我们不能在这个类的构造器中使用async调用,我们必须要创建一个任务,并在使用类构造器之后调用它:
这里是一些起作用的变量。
public StreamSocketListener ServerListener = new StreamSocketListener();
StreamSocket socket;
public async Task InitStuff()
{
var f = ServerListener.Control;
f.QualityOfService = SocketQualityOfService.LowLatency;
ServerListener.ConnectionReceived += ServerListener_ConnectionReceived;
await ServerListener.BindEndpointAsync(null, "13001");
}
变量 “f” 用来为流套接字监听者设置QualityOfService的属性。我们把它设为低延迟是为了确保传送能够尽可能的快。我们也必须要确保数据包能被尽可能快的处理,以避免超时(事实上这也是我们要做的工作)。
在为接收的连接设置完事件处理方法之后,我们就可以开始监听了。
await ServerListener.BindEndpointAsync(null, "13001");
上面这行代码就是做这个的。它定义了用于监听连接的主机名和端口号。因为我们的手机没有主机名,我们只是简单的把它定义为``null``来接受来自其他主机的连接。
来看一下接收连接的事件处理程序:
async void ServerListener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
字段``args``包含了用在请求中的套接字。我们应该把该套接字赋予给事先声明过的流套接字。
socket = args.Socket;
流套接字公开了两个流,一个输入流一个输出流。输入流是由远程套接字发送进来的数据流。
输出流是发给远程套接字的数据流。因为这是一个相当简单的请求和服务通信,我们并不是很关心从手机发过来的是什么。由于文件通常都以相同的方式传输,所以这里我们只看输出流:
var outputstream = socket.OutputStream;
DataWriter writer = new DataWriter(outputstream);
我们将用``writer``对象往输出流写数据。
现在,我们打开文件,读取它的内容,然后使用
data writer把这些字节写到输出流。
var myfile =await ApplicationData.Current.RoamingFolder.GetFileAsync(fileName);
var streamdata = await myfile.OpenStreamForReadAsync();
传输的第一阶段是以字节的方式发送文件的长度
if(stage==0)
{
stage++;
writer.WriteBytes(UTF8Encoding.UTF8.GetBytes((awaitstreamdata.Length.ToString()))
await writer.StoreAsync();
}
第二个阶段的代码如下。使用一个变量来计算阶段的索引。如果需要,也可以去读取客户端发送给你的数据。大家可以去了解一下``Windows 8.1``的流套接字示例,它是一个关于如何读写数据详细的例子。
if(stage==1)
{
byte[] bb = new byte[streamdata.Length];
streamdata.Read(bb, 0, (int)streamdata.Length);
writer.WriteBytes(bb);
await writer.StoreAsync();
stage=0;
}
注意``StoreAsync()
方法的调用
。这个调用清空了data writer的缓存并把所有数据注入到输出流中。我们所要做的几乎就这么多,系统会帮我们完成剩下的事情。另外一个需要注意的是我们如何发送数据。首先写这个文件的长度,然后写文件本身的内容。牢记这些,我们后面将要用到。
现在,这段代码呈现出一个大问题:整段代码并不是类型安全的。我们发送原始字节到客户端,客户端也向我们发回原始字节。其实,仅就设备之间的单个文件传送而言,我们没必要去过多的考虑类型安全。但是值得注意的是:如果你想发送复杂的数据,如基于通信的消息,你就需要考虑到你只是得到了字节这一事实,并且你需要自己来构建解析系统。
现在来看一下客户端代码。
这次我们将用到Silverlight 运行时。这种代码在Windows Phone 7和Windows Phone 8上都可以使用。
套接字类包含在System.Net.Sockets命名空间中。
创建一个新类,添加下面的字段:
MemoryStream ArrayOfDataTransfered;
string _serverName = string.Empty;
private int _port = 13001;
long FileLength = 0;
int PositionInStream = 0;
int Stage = 0;
用户需要输入服务器名称和端口号。因为这个连接是在家庭或私有网络中建立的,服务器的具体``IP``地址是可变的。直接使用电脑的名称可以解决这个问题。提供给用户一个设置页面来设置连接的话,也是一个不错的主意。下面这些行的代码应该放在初始化的方法中,比如类的构造函数。流程是这样的:
连接 > 发送 > 接收。如果你想要传送的文件不止一个,你就需要重新开始整个过程。
if (String.IsNullOrWhiteSpace(serverName))
{
throw new ArgumentNullException("serverName");
}
if (portNumber < 0 || portNumber > 65535)
{
throw new ArgumentNullException("portNumber");
}
_serverName = serverName;
_port = portNumber;
public void SendData(string data);
这个方法只是发送数据到远程服务器。下面来详细说明``:
首先我们需要``SocketAsyncEventArgs``这个对象,我们稍后将使用它。``SocketAsyncEventArgs``表示一个套接字操作。它的操作可以是发送,接收或者连接。
SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();
然后,我们需要一个服务器端,使用服务器名称和端口号来构建这个对象。
DnsEndPoint hostEntry = new DnsEndPoint(_serverName, _port);
下一步是创建套接字并设置各种属性,然后去连接远程服务器。
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(SocketEventArg_Completed);
socketEventArg.RemoteEndPoint = hostEntry;
socketEventArg.UserToken = sock;
再然后就是连接
sock.ConnectAsync(socketEventArg);
SocketEventArgs的事件处理方法如下:
void SocketEventArg_Completed(object sender, SocketAsyncEventArgs e)
{
switch (e.LastOperation)
{
case SocketAsyncOperation.Connect:
ProcessConnect(e);
break;
case SocketAsyncOperation.Receive:
ProcessReceive(e);
break;
case SocketAsyncOperation.Send:
ProcessSend(e);
break;
default:
throw new Exception("Invalid operation completed");
}
}
当操作是一个连接请求的时候,我们只需使用上面定义过的``SendData``方法发送数据到服务器就可以了,相关的代码:
byte[] buffer = Encoding.UTF8.GetBytes(dataIn);
e.SetBuffer(buffer, 0, buffer.Length);
Socket sock = e.UserToken as Socket;
sock.SendAsync(e);
dataIn ``是我们想发送到服务器的一个随机字符串。如果你想要从服务器请求多个文件,或者传输是在多个阶段完成,这个会很有用。``SendAsync``方法发送数据的原始字节到已连接的远程套接字。
ProcessSend``方法:
if (e.SocketError == SocketError.Success)
{
//Read data sent from the server
Socket sock = e.UserToken as Socket;
sock.ReceiveAsync(e);
}
else
{
ResponseReceivedEventArgs args = new ResponseReceivedEventArgs();
args.response = e.SocketError.ToString();
args.isError = true;
OnResponseReceived(args);
}
这里唯一有意思的事情是调用``ReceiveAsync(e).``这个方法基本上是文件传输的核心。
我们想要得到两个阶段的数据。第一个阶段是文件大小。 如果没有得到文件的大小,有一个可能是它太小,小到几乎不存在(把可能的最大的``64``字节的数据转化为以兆为单位,你就可以看到结果)。第二个阶段就是文件本身。
var dataFromServer = Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred);
Stage++;
FileLength = long.Parse(dataFromServer);
有意思的部分在第二阶段。基本上,它是整个流程中最重要的部分。
首先,我们将读取缓存区中的字节。
ArrayOfDataTransfered.Write(e.Buffer,0, e.BytesTransferred);
PositionInStream字段只计算到目前为止我们获取的字节数。如果它的值小于文件长度,则意味着文件还没有完全在这边。反之,则表示文件已经传输完成。
if (PositionInStream < FileLength)
{
Socket socks = e.UserToken as Socket;
socks.ReceiveAsync(e);
}
else
{
Stage = 0;
//save the file
}
如果文件还不在这里,意味着这个文件太大不适合放在一个数据包里面。也就是说,还有更多其它的包需要传送,我们需要再次调用``ReceiveAsync(e)``方法去读网络适配器的缓存区。
现在你要做的就是把内存流复制到一个单独的存储文件流中,传输即可完成。因为我们只用了一个内存流,你可能需要防止用户传送超过``100MB``大小的文件,否则就可能会触发一个内存溢出异常。如果你实在很想要传输超过``100MB``的文件,你可以直接把数据写入单独存储文件流中。如果要发送敏感数据的话,你还需要考虑套接字连接的安全问题。
附言: 可以通过以下方法提高程序的性能``:``在``Windows phone``程序中使用
ID_CAP_NETWORKING,在Windows Store程序使用Private Networks。
参见:
另外一个可以找到大量
Windows Phone 相关文章的地方是TechNet Wiki,最佳的入口是Windows Phone
Resources on the TechNet Wiki.