Win10 UWP 微信SDK使用方法和原理

作者:袖梨 2022-11-14

1.安装微信SDK for UWP

微信官方此前明确说明短时间内暂不提供RT版和UWP版的微信SDK,但眼见UWP开发之势愈烈,微信分享也必然是许多应用的标配功能,那怎么办呢,自己移植成UWP版吧。拙者提供的微信SDK是基于官方silverlight版sdk反编译后重新打包封装成UWP版本的,对部分内容稍加修改,命名空间、使用方法基本与官方文档无异。

通过nuget下载并安装微信SDK,链接https://www.nuget.org/packages/WeChatSDK/

或者在控制台输入PM> Install-Package WeChatSDK

注意项目会引用依赖Google.ProtocolBuffersLite,如果安装完成之后该依赖未自动安装,建议您Unload后Reload一下项目,或者手动安装该依赖

PM> Install-Package Google.ProtocolBuffersLite

另外,本项目代码已放在github上,请高手帮忙改进https://github.com/zhxilin/WeChatSDK

2.申请微信开放平台应用Id

到微信开放平台官网https://open.weixin.qq.com/,创建应用获得AppId和AppSecret。

3.配置Package.appxmanifest文件

与QQ SDK不同,微信SDK仅使用文件关联协议进行应用间通信,为此需要对配置文件进行配置。打开Package.appxmanifest文件,在节点下添加windows.fileTypeAssociation的声明:





.[YOUR APP ID]






注意不要漏了appid前面的“.”

4.调用微信SDK进行分享

由于用法同官方文档基本一致,故不赘述,直接代码举例说明。
(1)分享文本消息

try
{
var scene = SendMessageToWX.Req.WXSceneTimeline;
var message = new WXTextMessage
{
Title = "Sharing a text title!",
Text = "This is text content",
Description = "This is a text message.这是一个文本消息。",
ThumbData = null
};
SendMessageToWX.Req req = new SendMessageToWX.Req(message, scene);
IWXAPI api = WXAPIFactory.CreateWXAPI("[YOUR APP ID]");
var isValid = await api.SendReq(req);
}
catch (WXException ex)
{
Debug.WriteLine(ex.Message);
}

其中scene有3种类型可以选择:

SendMessageToWX.Req.WXSceneChooseByUser // 用户自行选择
SendMessageToWX.Req.WXSceneSession // 分享给好友
SendMessageToWX.Req.WXSceneTimeline // 分享到朋友圈

(2)分享图片消息

try
{
var scene = SendMessageToWX.Req.WXSceneTimeline;
var message = new WXTextMessage
{
Title = "Sharing a text title!",
Text = "This is text content",
Description = "This is a text message.这是一个文本消息。",
ThumbData = null
};
SendMessageToWX.Req req = new SendMessageToWX.Req(message, scene);
IWXAPI api = WXAPIFactory.CreateWXAPI("[YOUR APP ID]");
var isValid = await api.SendReq(req);
}
catch (WXException ex)
{
Debug.WriteLine(ex.Message);
}

(3)分享网页消息

try
{
var scene = shareScene.Scene;
var file = await Package.Current.InstalledLocation.GetFileAsync("1.png");
using (var stream = await file.OpenReadAsync())
{
var pic = new byte[stream.Size];
await stream.AsStream().ReadAsync(pic, 0, pic.Length);
var message = new WXWebpageMessage
{
WebpageUrl = "http://www.baidu.com",
Title = "Sharing a link!",
Description = "This is a link message.这是一个链接消息",
ThumbData = pic
};
SendMessageToWX.Req req = new SendMessageToWX.Req(message, scene);
IWXAPI api = WXAPIFactory.CreateWXAPI("[YOUR APP ID]");
var isValid = await api.SendReq(req);
}
}
catch (WXException ex)
{
Debug.WriteLine(ex.Message);
}

(4)登录授权

try
{
SendAuth.Req req = new SendAuth.Req("[YOUR SCOPE]", "test");
IWXAPI api = WXAPIFactory.CreateWXAPI("[YOUR APP ID]");
var isValid = await api.SendReq(req);
}
catch (WXException ex)
{
Debug.WriteLine(ex.Message);
}

其中[YOUR SCOPE]在开放平台申请授权登录时获取

(5)微信支付

由于微信支付权限申请比较麻烦,没有尝试,以后有实际场景再试。

5.回调结果处理

微信SDK提供了WXEntryBasePage类,继承自Page类,用来响应微信的回调,如果你的项目没有其他自定义的Page基类,建议把WXEntryBasePage作为你的Page基类;如果你已有自定义的Page基类,依旧可以处理。

WXEntryBasePage类提供public void Handle(FileActivatedEventArgs e)方法来处理回调,我们仅需在App.xaml.cs中重载OnFileActivated方法,将参数传入Hadle方法即可进行结果解析,并将不同类型的消息结果通过不同的响应方法进行通知。

WXEntryBasePage类包含以下Response方法:

public virtual void OnSendAuthResponse(SendAuth.Resp response)
{
}

public virtual void OnSendMessageToWXResponse(SendMessageToWX.Resp response)
{
}

public virtual void OnSendPayResponse(SendPay.Resp response)
{
}

以上三种Response方法分别对应登录授权、消息分享以及支付等操作的结果。



・如果你的Page基类已改成WXEntryBasePage,那么直接重载对应的这三个方法即可,如:

public override async void OnSendMessageToWXResponse(SendMessageToWX.Resp response)
{
base.OnSendMessageToWXResponse(response);
var dialog = new MessageDialog(response.ErrCode == 0 ? "分享成功": "分享失败");
await dialog.ShowAsync();
}

・如果你的Page基类没有改成WXEntryBasePage,那么你需要自己实现一个Callback类,继承自WXEntryBasePage:

public class WeChatCallback : WXEntryBasePage
{
}

并在App.xaml.cs的OnFileActivated方法中,创建该对象,并调用其Handle方法:


protected override void OnFileActivated(FileActivatedEventArgs args)
{
base.OnFileActivated(args);
try
{
var wechat = new WeChatCallback();
wechat.Handle(args);
}
catch (Exception)
{
// ignored
}
}





对于消息分享的Response,参数类型是SendMessageToWX.Resp,ErrCode == 0时表示分享成功,否则分享失败;

对于登录授权的Response,参数类型是SendAuth.Resp,其中Code即OAuth授权第一步所需的code,通过这个code,调用微信的Open API换取AccessToken,之后才能得到用户的基本资料。微信SDK并不实现Open API,所以需要自行实现。这里我写了一个WeChatSDK.Extensions类库,提供WeChatSns类,封装了部分Open API,直接调用封装好的接口即可完成整个OAuth:


public override async void OnSendAuthResponse(SendAuth.Resp response)
{
base.OnSendAuthResponse(response);
if (response.ErrCode == 0)
{
if (!string.IsNullOrEmpty(response.Code))
{
var token = await WeChatSns.GetAccessTokenAsync(response.Code);
if (token != null)
{
var user = await WeChatSns.GetUserInfoAsync(token.AccessToken, token.OpenId);
var dialog = new MessageDialog($"name:{user.Nickname}rnopenid:{user.OpenId}","授权成功");
await dialog.ShowAsync();
}
}
}
else
{
var dialog = new MessageDialog("授权失败");
await dialog.ShowAsync();
}
}

WeChatSDK.Extensions类库源码已贴在Github上,欢迎补充https://github.com/zhxilin/WeChatSDK/tree/master/UWP/WeChatSDK.Extensions

更多微信Open API请参考官方文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&lang=zh_CN

6.剖析微信SDK的原理

说完微信SDK的用法,下面谈谈它的实现原理。

要理解微信SDK的实现原理,首先要解决一个问题,理解什么是Protocol Buffers。可能很多人都注意到了微信SDK需要引用Google.ProtocolBuffersLite类库。

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前支持C++、Java、Python、C#等语言。

...

是Google 公司内部的混合语言数据标准

使用Protocol Buffers比XML的好处在于更简单、更快、更小,对于微信这种频繁数据序列化和反序列化的场景来说非常实用。微信的数据交换在设计之初必须考虑实现消息的跨平台传输,那么对于平台无关、可扩展性超高的Protocol Buffers技术来说,就有了很大的用武之地。

简单来说,对于服务端,只需要传输定义好结构的序列化文件 .proto文件到不同平台的客户端,通过Google提供的工具,如protoc、ProtoGen等工具即可反序列化为对应平台的具体对象。可以通过一张图来理解:



.proto文件会转成二进制序列protobuf进行传输,客户端接收到二进制序列后,按照指定的格式读取到对应平台的结构类型中就可以了。整个解析过程需要 Protocol Buffers框架和Protobuf 编译器生成的代码共同完成。比如protoc将.proto文件转成二进制文件,再通过protogen读取二进制文件转成C#代码。



了解了以上知识之后,我们来看看微信SDK是怎么做的。

比如分享消息,SDK将每个消息的结构整理后填入TransactionData中,该类包含ToProto方法和FromProto方法,分别将数据结构序列化成.proto对象和将.proto对象反序列化成数据结构。ToProto方法调用了Goolge.ProtocolBuffersLite类库提供的GeneratedBuilderLite.Build()方法完成序列化;FromProto方法则调用了BuildParsed()方法,将二进制序列反序列化成数据结构。

internal TransactDataP ToProto()
{
TransactDataP.Builder builder = TransactDataP.CreateBuilder();
// ... 将TransactData中的属性填入builder后Build
return builder.Build();
}

public static SendAuthResp ParseFrom(byte[] data)
{
return CreateBuilder().MergeFrom(data).BuildParsed();
}

我们的应用将数据打包放入一个后缀为.wechat的文件中,通过Launcher启动这个文件,自然只有微信客户端能识别这个文件,所以微信被呼叫起来并通过protobuf机制处理这个文件,完成我们的请求。

至于文件关联协议,之前讲过,在Package.appxmanifest中使用AppId(如wx0123456789abcdef)填写的声明,就是为了保证微信在处理完分享或者完成登录授权后,生成数据并写入文件.wx0123456789abcdef中,通过FileTypeAssociation协议,这种后缀的文件,也只能由我们的应用来打开了,这样就保证了互调关系的唯一性。OnFileActivated接收到文件之后,把文件内的数据通过protobuf机制反序列出来就能得到结果了。



Google Protocol Buffers 之.Net应用


前言

最近接到一个跨平台的测试项目,服务端Linux,是Java开发的一系列Socket接口,客户端Windows,所以准备用.Net。本想这种跨主流平台的Socket通信应该不成问题,但随着代码进程,随着一次次反复调试,我发现我错了。花了一周时间至今两者仍呈现北方网通和南方电信的姿态。

不过总有意外惊喜,过程中认识了Protocol Buffer,比XML、比JSON更为强悍,语言无关、平台无关、更小的存储、更少的歧义、更高的性能,其实Google一直在贡献,不论是Copy Left的还是Copy Right的,回头看看我们的百度,抄IM抄商城抄游戏抄视频抄房地产,还有搜索永远排第一却打不开的百度文库,印象中JQuery盛行N久之后百度开源了一个JS库,记忆里这也是百度为中国互联网技术做的唯一贡献,大公司的责任呐,好了,再说就偏离主题了。

Protocal Buffer官方站点:http://code.google.com/p/protobuf/ ,遗憾的是不支持.Net,但社区的力量不容忽视,MySQL最近还推出社区版呢,从这个链接可以看到Protobuf的社区阵营:http://code.google.com/p/protobuf/wiki/ThirdPartyAddOns

OK,本文主要描述自己在.Net中基于应用层面使用Protobuf的一些体会,作为学习笔记与大家分享,个人能力有限,不足之处还请及时指正。


需求

Java为服务端,.Net为客户端,Socket通信,使用Protobuf进行数据封装和传输,如下图:

DEMO中构造了3个简单的.proto文件供各客户端使用:

message MyRequest {
//版本号
required int32 version =1;
//姓名
required string name =2;
//个人网站
optional string website =3[default="http://www.paotiao.com/"];
//附加数据
optional bytes data =4;
}

message MyResponse {
//版本号
required int32 version =1;
//响应结果
required int32 result =2;
}
message MyData {
//个人简介
optional string resume =1[default="I'm goodman"];
}

其中MyRequest为客户端的请求,MyResponse为服务端的响应,MyData作为一个属性附加在MyRequest的data字段中,提醒注意这个byte类型的data字段,为此花费了最多时间并最终导至放弃Protobuf-net来做跨平台的应用。


Protobuf-net

官方站点:http://code.google.com/p/protobuf-net/

Protobuf-net是第三方中最强大应用最广泛的一个,支持.Net、C#、WCF、VB,并且DEMO丰富,网上可查到的资料也最多。

生成.CS类文件

安装后通过 protogen.exe 就可将.proto文件生成.cs文件(Demo中我将命令封装在/tools/getCS.bat中):
echo on
protogen -i:ProtoMyRequest.proto -o:ProtoMyRequest.cs
protogen -i:ProtoMyResponse.proto -o:ProtoMyResponse.cs
protogen -i:ProtoMyData.proto -o:ProtoMyData.cs

接着将生成的3个.cs文件包含在项目中,同时在项目中引用protobuf-net.dll

代码示例(服务端与客户端)
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using ProtoBuf;
//自定义
using ProtoMyData;
using ProtoMyRequest;
using ProtoMyResponse;

namespace protobuf_net
{
class Program
{
private static ManualResetEvent allDone = new ManualResetEvent(false);

static void Main(string[] args)
{
beginDemo();
}

private static void beginDemo()
{
//启动服务端
TcpListener server = new TcpListener(IPAddress.Parse("127.0.0.1"), 9527);
server.Start();
server.BeginAcceptTcpClient(clientConnected, server);
Console.WriteLine("SERVER : 等待数据 ---");

//启动客户端
ThreadPool.QueueUserWorkItem(runClient);
allDone.WaitOne();

Console.WriteLine("SERVER : 退出 ---");
server.Stop();
}

//服务端处理
private static void clientConnected(IAsyncResult result)
{
try
{
TcpListener server = (TcpListener)result.AsyncState;
using (TcpClient client = server.EndAcceptTcpClient(result))
using (NetworkStream stream = client.GetStream())
{
//获取
Console.WriteLine("SERVER : 客户端已连接,读取数据 ---");
//proto-buf 使用 Base128 Varints 编码
MyRequest myRequest = Serializer.DeserializeWithLengthPrefix(stream, PrefixStyle.Base128);

//使用C# BinaryFormatter
IFormatter formatter = new BinaryFormatter();
MyData myData = (MyData)formatter.Deserialize(new MemoryStream(myRequest.data));
//MyData.MyData mydata = Serializer.DeserializeWithLengthPrefix(new MemoryStream(request.data), PrefixStyle.Base128);

Console.WriteLine("SERVER : 获取成功, myRequest.version={0}, myRequest.name={1}, myRequest.website={2}, myData.resume={3}", myRequest.version, myRequest.name, myRequest.website, myData.resume);

//响应(MyResponse)
MyResponse myResponse = new MyResponse();
myResponse.version = myRequest.version;
myResponse.result = 99;
Serializer.SerializeWithLengthPrefix(stream, myResponse, PrefixStyle.Base128);
Console.WriteLine("SERVER : 响应成功 ---");

//DEBUG
//int final = stream.ReadByte();
//if (final == 123)
//{
// Console.WriteLine("SERVER: Got client-happy marker");
//}
//else
//{
// Console.WriteLine("SERVER: OOPS! Something went wrong");
//}
Console.WriteLine("SERVER: 关闭连接 ---");
stream.Close();
client.Close();
}
}
finally
{
allDone.Set();
}
}

//客户端请求
private static void runClient(object state)
{
try
{
//构造MyData
MyData myData = new MyData();
myData.resume = "我的个人简介";

//构造MyRequest
MyRequest myRequest = new MyRequest();
myRequest.version = 1;
myRequest.name = "吴剑";
myRequest.website = "www.paotiao.com";

//使用C# BinaryFormatter
using (MemoryStream ms = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(ms, myData);
//Serializer.Serialize(ms, mydata);
myRequest.data = ms.GetBuffer();
ms.Close();
}
Console.WriteLine("CLIENT : 对象构造完毕 ...");

using (TcpClient client = new TcpClient())
{
client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9527));
Console.WriteLine("CLIENT : socket 连接成功 ...");

using (NetworkStream stream = client.GetStream())
{
//发送
Console.WriteLine("CLIENT : 发送数据 ...");
ProtoBuf.Serializer.SerializeWithLengthPrefix(stream, myRequest, PrefixStyle.Base128);

//接收
Console.WriteLine("CLIENT : 等待响应 ...");
MyResponse myResponse = ProtoBuf.Serializer.DeserializeWithLengthPrefix(stream, PrefixStyle.Base128);

Console.WriteLine("CLIENT : 成功获取结果, version={0}, result={1}", myResponse.version, myResponse.result);

//DEBUG client-happy marker
//stream.WriteByte(123);

//关闭
stream.Close();
}
client.Close();
Console.WriteLine("CLIENT : 关闭 ...");
}
}
catch (Exception error)
{
Console.WriteLine("CLIENT ERROR : {0}", error.ToString());
}
}

}//end class
}

从代码中可以发现protobuf-net已考虑的非常周到,不论是客户端发送对象还是服务端接收对象,均只需一行代码就可实现:
//客户端发送对象
ProtoBuf.Serializer.SerializeWithLengthPrefix(stream, myRequest, PrefixStyle.Base128);
//服务端接收对象
MyRequest myRequest = Serializer.DeserializeWithLengthPrefix(stream, PrefixStyle.Base128);

所以如果Server与Client均使用.Net,Protobuf-net会是理想选择。

但我的项目需要跨平台,同时项目中又恰恰使用了byte类型字段,经过反复调试比较,发现一个关键问题:假使proto脚本和对象属性值完全一样,但只要包含byte类型的字段,那么通过Java序列化的二进制与C#序列化的二进制结果一定不同。而Protobuf中Google原生支持Java,那么几乎可以确定Protobuf-net对Protobuf的支持存在瑕疵。

后来在使用Protobuf-csharp-sport后验证了这一点,Protobuf-net使用C#的byte[]来实现bytes,而Java以及Protobuf-csharp-port均使用ByteString,前者是无符号的,后者是有符号的,语言的基本差异导致两者无法兼容,所以最终我只能放弃Protobuf-net。

Protobuf-csharp-port

官方站点:http://code.google.com/p/protobuf-csharp-port/

Protobuf-csharp-port的文档资料、DEMO、应用范围都不如Protobuf-net,但Protobuf-csharp-port更遵循Google的Protobuf,甚至应用和代码都几乎一样,所以跨平台,Protobuf-csharp-port是不二之选。

生成.CS类文件

先直接使用Google的 protoc.exe 生成二进制文件。

然后通过 protogen.exe 将二进制文件生成C#类文件(Demo中我将命令封装在/tools/getCS.bat中):

echo on
protoc --descriptor_set_out=ProtoMyRequest.protobin --include_imports ProtoMyRequest.proto
protoc --descriptor_set_out=ProtoMyResponse.protobin --include_imports ProtoMyResponse.proto
protoc --descriptor_set_out=ProtoMyData.protobin --include_imports ProtoMyData.proto

protogen ProtoMyRequest.protobin
protogen ProtoMyResponse.protobin
protogen ProtoMyData.protobin


接着将生成的3个.cs文件包含在项目中,同时在项目中引用Google.ProtocolBuffers.dll

代码示例(服务端与客户端)
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Google.ProtocolBuffers;

namespace protobuf_csharp_sport
{
class Program
{
private static ManualResetEvent allDone = new ManualResetEvent(false);

static void Main(string[] args)
{
beginDemo();
}

private static void beginDemo()
{
//启动服务端
TcpListener server = new TcpListener(IPAddress.Parse("127.0.0.1"), 9528);
server.Start();
server.BeginAcceptTcpClient(clientConnected, server);
Console.WriteLine("SERVER : 等待数据 ---");

//启动客户端
ThreadPool.QueueUserWorkItem(runClient);
allDone.WaitOne();

Console.WriteLine("SERVER : 退出 ---");
server.Stop();
}

//服务端处理
private static void clientConnected(IAsyncResult result)
{
try
{
TcpListener server = (TcpListener)result.AsyncState;
using (TcpClient client = server.EndAcceptTcpClient(result))
{
using (NetworkStream stream = client.GetStream())
{
//获取
Console.WriteLine("SERVER : 客户端已连接,数据读取中 --- ");
byte[] myRequestBuffer = new byte[49];
int myRequestLength = 0;
do
{
myRequestLength = stream.Read(myRequestBuffer, 0, myRequestBuffer.Length);
}
while (stream.DataAvailable);
MyRequest myRequest = MyRequest.ParseFrom(myRequestBuffer);
MyData myData = MyData.ParseFrom(myRequest.Data);
Console.WriteLine("SERVER : 获取成功, myRequest.Version={0}, myRequest.Name={1}, myRequest.Website={2}, myData.Resume={3}", myRequest.Version, myRequest.Name, myRequest.Website, myData.Resume);

//响应(MyResponse)
MyResponse.Builder myResponseBuilder = MyResponse.CreateBuilder();
myResponseBuilder.Version = myRequest.Version;
myResponseBuilder.Result = 99;
MyResponse myResponse = myResponseBuilder.Build();
myResponse.WriteTo(stream);
Console.WriteLine("SERVER : 响应成功 ---");

Console.WriteLine("SERVER: 关闭连接 ---");
stream.Close();
}
client.Close();
}
}
finally
{
allDone.Set();
}
}

//客户端请求
private static void runClient(object state)
{
try
{
//构造MyData
MyData.Builder myDataBuilder = MyData.CreateBuilder();
myDataBuilder.Resume = "我的个人简介";
MyData myData = myDataBuilder.Build();

//构造MyRequest
MyRequest.Builder myRequestBuilder = MyRequest.CreateBuilder();
myRequestBuilder.Version = 1;
myRequestBuilder.Name = "吴剑";
myRequestBuilder.Website = "www.paotiao.com";
//注:直接支持ByteString类型
myRequestBuilder.Data = myData.ToByteString();
MyRequest myRequest = myRequestBuilder.Build();

Console.WriteLine("CLIENT : 对象构造完毕 ...");

using (TcpClient client = new TcpClient())
{
client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9528));
Console.WriteLine("CLIENT : socket 连接成功 ...");

using (NetworkStream stream = client.GetStream())
{
//发送
Console.WriteLine("CLIENT : 发送数据 ...");
myRequest.WriteTo(stream);

//接收
Console.WriteLine("CLIENT : 等待响应 ...");
byte[] myResponseBuffer = new byte[4];
int myResponseLength = 0;
do
{
myResponseLength = stream.Read(myResponseBuffer, 0, myResponseBuffer.Length);
}
while (stream.DataAvailable);
MyResponse myResponse = MyResponse.ParseFrom(myResponseBuffer);
Console.WriteLine("CLIENT : 成功获取结果, myResponse.Version={0}, myResponse.Result={1}", myResponse.Version, myResponse.Result);

//关闭
stream.Close();
}
client.Close();
Console.WriteLine("CLIENT : 关闭 ...");
}
}
catch (Exception error)
{
Console.WriteLine("CLIENT ERROR : {0}", error.ToString());
}
}

}//end class
}



Protobuf#

官方站点:http://code.google.com/p/protosharp/

暂未测试Protobuf#


结束语

基本花了一周时间了解和学习了Google Protobuf在.NET下的应用,也找到了Protobuf跨平台的方案,但好事多魔,C# Socket发送的protobuf数据包在Java Netty 中怎么也获取不到,我想也许是平台差异,但对Java知之甚少,如有知情人士还请指点迷津。

相关文章

精彩推荐