Airdrop Anywhere - 使它在Windows上工作

2021-06-15 07:34:49

在第3集中,我们通过使用C#的AirDrop的实现工作,允许在Apple设备之间接收文件。在这一集中,我们将查看实现将其打开至非Apple设备所需的比特。

既然我们可以在Apple设备之间发送文件,我们处于一个很好的位置,开始向非Apple设备开放。然而,如集中1中所述,非Apple设备不太可能对设备之间的ADHOC无线连接具有硬件支持。这使得AirDrop的实施直接在非Apple设备上实际上不可能,无需额外的硬件。相反,我们将实现一个可以在具有支持硬件的平台上运行的代理(例如,Apple设备或运行owl的Linux设备)。

最初,我认为将事物分为三个项目是建立它的最理和方法 - AirDropanywhere.Core将包含与Airdropanywhere.Cli的核心部分,其中包含CLI组件和AirdropanyWhere.Web包含一组端点以支持非Apple设备。相反,我决定将所有服务器和客户端部件托管到AirDropanywhere.Cli中 - 这简化了构建流水线,并提供了一个可执行文件,可以充当客户端或服务器到我们的AirDrop实现。在实践中,这是使用频谱中的命令支持来实现的.Console意味着系统现在看起来像这样的东西:

让我们通过设计中的一些关键概念,然后挖掘实施细节和amp; mldr

为了支持非AirDrop设备,我们需要拥有“对等体”的概念 - 连接到我们服务器的任意客户端,并可在AirDrop-兼容设备中进行可发现。这成为代码中的核心抽象和各种子系统彼此通信的机制。对Airdroppeer说你好:

///&lt;&gt; ///对AirDrop HTTP API公开一种方法,以与任意对等体进行通信// /不直接支持AICDrop协议。 ///&lt; //摘要&gt;公共抽象类Airdroppeer {///&lt;摘要&gt; ///获取此对等体的唯一标识符。 ///&lt; //摘要&gt;公共字符串ID {GET; } ///&lt;摘要&gt; ///获取此对等体的(显示)名称。 ///&lt; //摘要&gt;公共字符串名称{GET;保护集; } ///&lt;摘要&gt; ///确定对等体是否要从发件人接收文件。 ///&lt; //摘要&gt; ///&lt; param name =&#34;请求&#34;&gt; ///一个&lt;见Cref =&#34; askrequest&#34; /&gt;代表有关发送方///的信息以及他们希望发送的文件的信息。 ///发件人////&lt; / param&gt; ///&lt;返回&gt; ///&lt; c&gt; true&lt; / c&gt;如果接收器想要接受文件传输,则&lt; c&gt; false&lt; / c&gt;除此以外。 ///&lt; /返回&gt;公共抽象百分表&lt; BOOL&GT; canacceptfileSasync(askrequest请求); ///&lt;&gt; ///通知对等体已上载文件。此方法用于从ACTrop兼容设备发送的存档中提取的每个///文件。 ///&lt; //摘要&gt; /// <&lt; param name =&#34; filepath&#34;&gt; ///提取文件的路径。 ///&lt; / param&gt;公共抽象valuetask onfileuploadeDasync(字符串Filepath); }

这相当简单 - 对等体的唯一标识符,显示名称和一些方法,以允许AirDrop HTTP API与对等体通信。这实现为抽象类而不是一个接口,因为我们需要管理框架中的标识符 - 我们将其使用它作为MDNS中的主机名和AirDrop在主机名中允许的字符尤为挑剔。我们只需使用满足AirDrop的要求的随机12个字符的alpha-numeric标识符,这意味着实现者不需要了解这个细节。

AirDroppeer的实现是由底层的凝视机制提供 - 在我们的情况下,我们允许客户端在WebSock上使用SignalR连接,因此我们提供了一个在其消息传递协议上的应用程序。当对等体连接到我们的服务器时,我们注册它以便Airdrodwherewhere.Core中的核心作品意识到其存在。同样,当对等体断开连接时,我们无法注册。该功能在Airtropservice上暴露:

///&lt;&gt; ///寄存器&lt; see cref =&#34; airdroppeer&#34; /&gt;这样它就会发现/// AirDrop-兼容设备。 ///&lt; //摘要&gt; ///&lt; param name =&#34;同行&#34;&gt; ///&lt; see cref =&#34; airdroppeer&#34; /&gt ;. ///&lt; / param&gt; Public ValueTask RegisterPeerAsync(Airdroppeer对等体); ///&lt;&gt; /// verteators一个&lt; see cref =&#34; airdroppeer&#34; /&gt;因此,它不再可被/// AirDrop-兼容设备发现。如果未注册对等体,则此操作是NO-OP。 ///&lt; //摘要&gt; ///&lt; param name =&#34;同行&#34;&gt; ///先前注册的&lt; see cref =&#34; airdroppeer&#34; /&gt ;. ///&lt; / param&gt;公共valuetask unregisterpeeraSync(Airdroppeer对等体); ///&lt;&gt; ///试图获得一个&lt;参见cref =&#34; airdroppeer&#34; /&gt;通过其唯一的标识符。 ///&lt; //摘要&gt; ///&lt; param name =&#34; id&#34;&gt;对等体的唯一标识符。&lt; / param&gt; ///&lt; param name =&#34;同行&#34;&gt; ///如果找到,则&lt; see cref =&#34; airdroppeer&#34; /&gt;由&lt; paramref name =&#34; Id&#34; /&gt; ///&lt; c&gt; null&lt; ///&gt;除此以外。 ///&lt; / param&gt; ///&lt;返回&gt; ///&lt; c&gt; true&lt; / c&gt;如果发现对等体,则为假&lt; / c&gt;除此以外。 ///&lt; /返回&gt; Public Bool TractetPeer(String ID,Out Airdroppeer对等体)

这些方法提供了核心AirDrop所需的表面积,以使非AITDOP兼容设备可发现。让我们分解它如何与代码的其他部分交互。

注册对等体AirdroSeService时,使用ID属性作为主机和amp创建MulticastDNSService的实例;实例名称。它会跟踪由ID键入的字典中的对等体。然后它告诉MulticastDNSServer在AWDL0接口上宣布那个服务的DNS记录 - 这正是前一个版本的代码中发生的事情,除非我们现在动态地宣布对等体的存在而不是静态定义它。此公告导致AirDrop使用MDNS通知的主机名调用/发现HTTP API - 在我们的情况下,主机名与对等体的唯一标识符相同。

AirdroproTehandler已被修改为在实例化时注入与请求相关联的Airdroppeer实例 - 它通过提取主机标题的第一部分并使用TrygetPeer执行查找来实现此操作。如果该主机名解析为AirDroppeer,那么它将继续像往常一样执行请求,否则它会返回HTTP 404.我说“像往常”,但AirDrop中的每个API是什么意思?

/发现 - 我们目前正在“每个人”模式(而不是“仅”联系人“模式),因此此API始终返回对等体的详细信息 - 特别是它的名称呈现在AirDrop UI中的呈现。

/询问 - 先前始终同意 - 现在它会阻止呼叫AirDroppeer.CanaCceptFileSAsync,以允许对等体决定操作是否应继续

/上传 - 以前将上传的文件直接提取到服务器的文件系统。不是很有用!现在它通知对等待的每个文件通过HTTPS将该文件暴露给对等体以允许它下载它。

卸载有效地与注册相反 - 它会删除服务器中的任何迹线,并通过0秒的TTL宣布通过MDNS宣布。使用0s TTL宣布导致下游MDNS缓存丢弃与对等体关联的记录,使其从任何AirDrop浏览器中消失。从服务器的状态删除它会导致HTTP API的任何请求返回404,因为TriceGeer不再返回对等体。

我们已经讨论了窥视是如何旨在工作的,因此让我们潜入使用信号凝视的实现细节。出于框中的信号R通过WebSockets通过WebSockets提供连接到服务器发送的事件或长轮询(以下是运输之间的差异的好帖子),但它将其全部使用“集线器”的概念摘要。客户端连接到集线器,他们可以在其上调用方法或服务器可以在客户端上调用方法 - 这是两种连接。此外,我们可以从服务器实现流传输到客户端,反之亦然。

对客户端的服务器有一些限制 - 特别是客户端无法返回对服务器的响应。但是,通过实施双向流,我们可以在信号传输的流级顶部层层层层上的完全异步请求/响应机制。考虑具有以下方法的集线器:

///&lt;&gt; ///在服务器和客户端之间启动双向流。 ///&lt; //摘要&gt; ///&lt; param name =&#34;流&#34;&gt; ///&lt;见Cref =&#34; Iasyncenumer {T}&#34; /&gt; &lt;见Cref =&#34; AirdrophubMessage&#34; /&gt;从客户端的消息///。 ///&lt; / param&gt; ///&lt; param name =&#34; cancellationToken&#34;&gt; ///&lt;见Cref =&#34; cancellationToken&#34; /&gt;用于取消操作。 ///&lt; / param&gt; ///&lt;返回&gt; ///&lt;见Cref =&#34; Iasyncenumer {T}&#34; /&gt; &lt;见Cref =&#34; AirdrophubMessage&#34; /&gt;从服务器中使用的消息///。 ///&lt; /返回&gt;公共Async Iasyncencenomable&lt; airdrophubmessage&gt; Streamasync(IasyNcenumerAble&lt; airdrophubmessage&gt; Stream,[enumeratorsCancellation]消除了CancellationTokenToken);

这允许服务器向客户端发送消息(通过返回的ariasyncenumer&lt; t&gt;)和客户端通过IASyncenumerAble&lt; t&gt向服务器发送消息。流参数。 AirdrophubMessage为每个已发送的消息生成唯一ID,以及包含当前消息回复的消息的标识符的ReplyTo属性。如果未设置ReplyTo,则将消息被认为是未经请求的。

当客户端连接到集线器时,它调用StreamAxync,并且服务器旋转了一个负责从客户端的IASyncencomerable处理消息的线程。它使用通道&lt; t&gt;作为服务器产生的消息的“队列” - 作为消息发布到通道&lt; t&gt;它们被产生给信号函数,它通过与客户端的连接发送它们。

在我们的频道中使用的罩下方的罩下方&lt; t&gt;实际上是一个名为MessageWithCallback的结构,其中包含AirdrophubMessage和一个可选的回调,如果邮件响应另一个消息,则调用。通过实现ivaluetaskSource来处理回调 - 这是使用TaskCompletionsource的ValueTask等效,并且允许SignerR实现Airdroppeer的消费者等待操作的结果,即使该结果正在发生在另一个线程上(来自客户端的一个处理消息)发生)。通过存储消息的唯一标识符以及表示与连接关联的字典中的回调的唯一标识符以及IVALUETASKSource来跟踪回调。当从客户端接收到消息时,我们将检查replyto属性值是否在该字典中,如果是的话,我们可以使用客户端调用邮件的回调。

ivaluetasksource相对容易实现,运行时中有一个结构,它可以实现其大多数称为manualresetvaluetaskskourcecore&lt; t&gt;,所以我们的实现称为CallbackValuetAskource,只需包装它:

///&lt;&gt; ///实现&lt;见Cref =&#34; rivaluetasksource {t}&#34; /&gt;这使得可以///请求/响应式对话,以发生与客户端的SignalR Full ///双工连接。这用于使集线器启用到///执行回调。 ///&lt; //摘要&gt;私人类CallbackValuetAskSource:ivalueTasksource&lt; airdrophubmessage&gt; {私有ManualResetValuetAskesourceCecore&lt; airdrophubmessage&gt; _valuetasksource; public void setresult(airdrophubmessage消息)=&gt; _valuetasksource。 setResult(消息);公共AirdrophubMessage GetResult(短令牌)=&gt; _valuetasksource。 GetResult(令牌);公众valuetaskesourcestatus getstatus(短令牌)=&gt; _valuetasksource。 getStatus(令牌);公共空白onCompleted(动作&lt;&gt;&gt;延续,对象?状态,短令牌,valuetaskouskourceoncompletedflags标志)=&gt; _valuetasksource。 OnCompleted(延续,州,令牌,旗帜);公共void reset()=&gt; _valuetasksource。重启 (); }

当收到回复消息时,我们发现了一个回调,我们只需用消息调用setResult(AirdrophubMessage)。如果呼叫者正在等待valueTask,它将恢复执行的ivaluetasksource。

重要的是要注意此类上的重置方法 - 以最大限度地减少我们使用[ObjectPool的分配(https://docs.microsoft.com/en-us/aspnet/core/performance/objectspool?view=aspnetcore-5.0)为了重复使用,保留课程的一些实例。当我们想要一个实例时,我们调用ObjectPool.get()获取一个,请使用它,然后在我们完成时将其返回给池。重置允许我们稍后安全地重新使用该实例。

这是很多话!让我们来看看它在SignalSR对等体中实现的工作原理(注意:代码已熟知帖子):

// airdroprotehandler.askasync - 只需调用`canacceptfilessasync`方法public async任务Askasync(){var askrequest = ...; var canacceptfiles = await _peer。 canacceptfilessasync(askrequest); //使用结果// ...} // airdrophubpeer.canaCceptfilessync公共valuet ass&lt; BOOL&GT; canacceptfileSasync(Askrequest Askrequest){//将askrequest转换为我们的signalr客户端canacceptfilesRequestMessage请求= ...; //从池var回调= _callbackpool获取回调实例。得到 ();尝试{//将请求和其回调写入服务器&#39; s ariasyncenumer等待_serverqueue。 WriteAsync(New MessageWithCallback(请求,回调)); //通常通过客户端//在另一个线程上发送的消息来发信号通知回调。此呼叫将阻止,直到发生。 var结果=等待新的valuetask&lt; airdrophubmessage&gt;(这个,_valuetasksource。版本);如果(结果是CANACceptFilesResponseMessage TypedEdResult){返回typedResult。接受; } //永远不会发生,可能是一个错误!抛出新的InvalidcastException($&#34;无法将类型{resule.gettype()}的消息转换为{typeof(canacceptfilesresponsemessage)}&#34;);最后{//我们&#39;重新完成回调,将其返回给我们的池_callbackpool。返回(回调); }}

这可能似乎有点复杂,但它允许我们在服务器和客户端之间的完整双工流的顶部具有相当有用的请求/响应实现。重要的是,核心作品无需了解消息如何将消息转移到对等体,他们只是等待呼叫。

默认情况下,Systemr使用system.text.json串行/反序列化电线的消息。通常,这正常好,但在这里我们正在使用双向流媒体与抽象基类AirdrophubMessage。 system.text.json不知道它应该如何处理这种类型的衍生物,所以我们需要提供帮助的手 - 进入多晶jsonconverter。此JsonConverter读取和写入与对象的具体类型相关联的JSON,但在命名字段中包装它,以便它知道要反序列化的运行时类型。然后,我们将属性添加到AirdrophubMessage,从而教授它应该处理的转换器的转换器:

[polymorphicjsoninclude(&#34; connect&#34; typeof(connectmessage))] // ...更多映射在这里...公共抽象类airdrophubmessage {public string id {get;公共字符串?回复{get;公共类ConnectMessage:AirdrophubMessage {公共字符串名称{GET; }}

在使用Connect键来查找Connect键时,用于查找邮件的正确运行时类型 - 在这种情况下使用CONCHICMESSAGE。这使我们使用IASyncenuber&lt; airdrophubmessage&gt来维护我们的简单流签名。但允许我们将任何派生类型传递给连接的方。

我提到的是,我们以前的AirDrop's / Upload API的实现只是将文件提取到服务器的文件系统。现在我们可以将事物转发给同行,我们可以直接向他们代表文件块的字节数阵列,即!不幸的是,这个故事没有结束那里 - 如果我们使用浏览器连接到我们的信号服务器服务器,那么我们处理字节数组的选项有点有限 - 我们需要构建一个Blob,一旦我们拥有所有块,我们就可以使用一些创意Hacks要获取浏览器触发“下载”到用户本地计算机。除了我们已经向客户发送了字节,否则在大文件的情况下,Blob通过内存支持或临时缓冲其他地方,可能有某种限制来防止滥用。

相反,我向Kestrel实例旋转到AirDrodwhere.Cli Server实例中的Kestrel实例,它添加了一个staticfileprovider .cli服务器实例,该服务器实例将映射到服务器上的上载目录。当我们提取上传的存档时,我们将在此生成新目录,并将新文件添加到其中。提取存档后,我们通知客户端的每个文件提取,并在服务器上执行其对应的URL - 一旦客户端成功下载了它所需的所有文件,那么从服务器中删除这些文件。

这允许我们将文件直接将文件传输到客户端的浏览器,而无需沿途不必要的缓冲 - 它是一个权衡 - 我们期待服务器拥有存储空间来提取所有档案,但我们可以依赖客户端能够直接下载目标,而不是使用浏览器中的Blob和文件使用半支持的解决方法。

现在我们有一个(相对)的理智方法来凝视我们可以实施我们的第一个消费者。第一个消费者将在命令行运行,它将执行以下步骤:

使用SignalR连接到AirDrop服务器,并立即发送包含对等名称的ConnectMessage。 AirDrop将通过此公告发现对等体,并通过HTTPS致电/发现API。

等待来自服务器的CanaCceptFileRequestMessage。当联系人在AirDrop UI中删除联系人时,会发送此功能,触发通过HTTPS拨打/询问API。一旦收到,CLI会显示用户询问他们是否希望接受正在发送的文件的提示。

如果它们击中,则为然后继续,否则转到2.无论哪种方式,将响应返回为canacceptfileresponsemessage,因此服务器知道如何继续。

对于每个文件,接收FileUploadedRequestMessage并使用其中的URL从服务器下载文件。

所有这些逻辑都在Airdropaywhere.Cli项目中在CliencMand中包装。这使得频谱.Console广泛的格式化选项,以呈现提示和从文件下载过程中显示输出。

在此实现中没有特别的“魔法” - 它是一个简单的信号客户端,它呈现一些UI并使用HttpClient下载文件。唯一的怪癖是确保在通过HTTPS连接到下载或使用SignalR时忽略证书验证错误 - 在不同的计算机上运行默认ASP.N时

......