微信通讯协议的学习

2 min read

微信协议概览

微信传输协议,官方公布甚少,在微信技术总监所透漏PPT《微信之道—至简》文档中,有所体现。

微信从2011年1月发布以来,在一年之内实现了上亿用户,千万级在线,在苹果中国区App Store月下载量排行第一。 腾讯把微信的成功总结为“三位一体”,即产品的精准,项目的敏捷,以及技术的支撑。 敏捷就是试错法,用最快的迭代速度不断追求卓越。敏捷是一种态度,允许发布前十分钟的变更,并给予产品决策以最大的自由度。

微信使用的同步协议叫做SYNC,参考了微软的ActiveSync YNchronous ommunication:同步通信。没有数据发送时,传输线处于MARK状态。为了表示数据传输的开始,发送方先发送一个或两个特殊字符,该字符称为同步字符。当发送方和接收方达到同步后,就可以一个字符接一个字符地发送一大块数据,而不再需要用起始位和停止位了,这样可以明显地提高数据的传输速率。采用同步方式传送数据时,在发送过程中,收发双方还必须用一个时钟进行协调,用于确定串行传输中每一位的位置。接收数据时,接收方可利用同步字符将内部时钟与发送方保持同步,然后将同步字符后面的数据逐位移入,并转换成并行格式,供CPU读取,直至收到结束符为止。用一个Key来实现状态同步。这样一种协议在后台实现上比业界通用方案要复杂许多,但是能把客户端的实现大大简化,同时在很大程度上能够满足iPhone,安卓,塞班等多个操作系统的不同需求。

微信秉承“重后台轻客户端”的思路,因为客户端安装在用户手机上,变更成本很高;而后台则可以实现迅速的变更,在不发新版本的情况下实现新功能。以下是一个例子:微信的最初版本是不支持群聊的,第二个版本支持了群聊,但第一版客户端仍然可以在后台的变更处理之下参与群聊,只是不能够发起群聊而已。

其服务器端目前获知的几部分分别是三网专用网关服务器、登陆服务器组、负载均衡服务器组,主动推送服务器组、后台数据转换服务器组、存储阵列等几部分。由于目前没有任何能够直接从客户端保存至服务器端的功能,推测其服务方并没有用于数据记录的数据库服务器,而是在登陆服务器组中集成了用户数据库,用来记录用户授权。

因张小龙做邮箱Foxmail起家,继而又做了QQ Mail等,QQ Mail是国内第一个支持Exchange ActiveSync协议的免费邮箱,基于其从业背景,微信从一开始就采取基于ActiveSync的修改版状态同步协议Sync,也就再自然不过了。一句话:增量式、按序、可靠的状态同步传输的微信协议。

Microsoft Exchange Active Sync协议

Microsoft Exchange Active Sync协议,简称EAS,分为folderrsync(同步文件夹目录,即邮箱内有哪几个文件夹)和sync(每个文件夹内有哪些文档)两部分。

某网友总结的协议一次回话大致示范:

  • Client: synckey=0 //第一次key为0
  • Server: newsynckey=1235434 //第一次返回新key
  • Client: synckey=1235434 //使用新key查询
  • Server: newsynckey=1647645,data=*****//第一次查询,得到新key和数据
  • Client: synckey=1647645
  • Server: newsynckey=5637535,data=null //第二次查询,无新消息
  • Client: synckey=5637535
  • Server: newsynckey=8654542, data=****//第三次查询,增量同步

大致交换简图如下:

meas

如何获取新数据呢:

  • 服务器端通知,客户端获取
  • 客户端携带最新的SyncKey,发起数据请求
  • 服务器端生成最新的SyncKey连同最新数据发送给客户端
  • 基于版本号机制同步协议,可确保数据增量、有序传输
  • SyncKey,由服务器端序列号生成器生成,一旦有新消息产生,将会产生最新的SyncKey。类似于版本号

服务器端通知有状态更新,客户端主动获取自从上次更新之后有变动的状态数据,增量式,顺序式。

微信的协议

为保证稳定,微信用了长链接和短链接相结合,微信划分了http模式(short链接)和 tcp 模式(long 链接),分别应对状态协议和数据传输协议

  • weixin.qq.com dns check (112.64.237.188 112.64.200.218)
  • weixin.qq.com dns check  ( 112.64.237.186 112.64.200.240)

1)short.weixin.qq.com

是HTTP协议扩展,运行8080 端口,http body为二进制(protobuf)。主要用途(接口):

  • 用户登录验证;
  • 好友关系(获取,添加);
  • 消息sync (newsync),自有sync机制;
  • 获取用户图像;
  • 用户注销;
  • 行为日志上报。
  • 朋友圈发表刷新

2)long.weixin.qq.com

tcp长连接,端口为8080,类似微软activesync的二进制协议。主要用途(接口):

  • 接受/发送文本消息;
  • 接受/发送语音;
  • 接受/发送图片;
  • 接受/发送视频文件等。

所有上面请求都是基于tcp长连接。在发送图片和视频文件等时,分为两个请求;第一个请求是缩略图的方式,第二个请求是全数据的方式。

3)数据报文方面

  • 增量上传策略:每次8k左右大小数据上传,服务器确认;在继续传输。
  • 图片上传:先传缩略图,传文本消息,再传具体文件
  • 下载:先下载缩略图, 在下载原图,下载的时候,全部一次推送。

Sync 同样存在一些问题:

  • SyncKey 生成维护成本:SyncKey 在ActiveSync中为字符串,客户端不需要解析,但服务端实现要用数字自增,需要强一致性,且不能回退。
  • 消息的订阅模式:采用类似Zookeeper的One time triggler 还是每条消息都推送一条通知能,ne time trigger能够避免并发通知时,获取消息时重复问题,但增加了交互成本,和客户端实现复杂性。
  • 自己发的消息,SyncKey怎么获取,其要支持多端同步发消息,保证消息同步;也只好消息发完在给自己同步一遍(自己设备发的可以不带消息体)
  • 消息推送延时加重:Sync 消息体获取方式:Notify – Ack – get – Mssage, 也就是至少第四个应用包才能返回消息,在移动网络下成本很高。

手机客户端不再Sync协议

抓包分析版本:wifi、gprs网络状况下都相同,客户端会依次尝试使用80、8080、443 端口连接服务器;消息发送、接收都使用长连接进行.

协议格式:

  • 4byte Packet Len(包含4字节本身)
  • 2byte Head Len(包含2字节本身) + 2byte Version(1) + 4byte Operation + 4byte SeqId + ….
  • (Packet Len – Head Len) Body

协议交互方式:

  • 客户端请求(一应一答,通过seqid匹配):seqid = 1 开始,依次递增,服务器回复相同的seqid 作为应答
  • 服务器推送通知(单向):seqid = 0,Operation = 7a,  客户端不需要应答

主要业务:

  • -心跳包:发起客户端请求,Operation = 0c,长度为16字节,算是最小的包
  • -发消息:发起客户端请求,Operation = ed;单点在线时发完消息后,应答携带SyncKey,不再同步,多点在线时,通过通知同步SyncKey。
  • -收消息:服务器推送消息,Operation = 7a, Body 中携带消息内容,抓包分析时,通过改变消息体大小,可能到接收方第一个包length 也会随之变化,可确认消息是push的。发起客户端单向请求,即消息的应答Ack。
  • -加密:未分析,一般来说像whatsapp那样,使用 hash(密码/OTP + 长连接第一个请求获取RandomCode) 做RC4 的密钥

APP抓包数据

1)初始连接记录

简单记录微信启动之后请求:

  • 11:20:35 dns查询weixin.qq.com 返回一组IP地址
  • 11:20:35 DNS查询 weixin.qq.com 返回一组IP地址,本次通信中,微信使用了最后一个IP作为TCP长连接的连接地址。
  • 11:20:35 http://dns.weixin.qq.com/cgi-bin/micromsg-bin/newgetdns?uin=0&clientversion=620888113&scene=0&net=1 用于请求服务器获得最优IP路径。服务器通过结算返回一个xml定义了域名:IP对应列表。仔细阅读,可看到微信已经开始了国际化的步伐:香港、加拿大、韩国等。
  • 11:20:35 获取到weixin.qq.com最优IP,然后建立到101.227.131.105的TCP长连接
  • 11:21:25 POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/getprofile HTTP/1.1 (application/octet-stream) 返回一个名为“micromsgresp.dat”的附件,估计是未阅读的离线消息
  • 11:21:31 POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/whatsnews HTTP/1.1 (application/octet-stream) 大概是资讯、订阅更新等
  • 中间进行一些资源请求等,类似于GET http://wx.qlogo.cn/mmhead/Q3auHgzwzM7NR4TYFcoNjbxZpfO9aiaE7RU5lXGUw13SMicL6iacWIf2A/96,图片等一些静态资源都会被分配到qlogo.cn域名下面
  • POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/downloadpackage HTTP/1.1 (application/octet-stream) 输出为dat文件。
  • 11:21:47 GET http://support.weixin.qq.com/cgi-bin/mmsupport-bin/reportdevice?channel=34&deviceid=A952001f7a840c2a&clientversion=620888113&platform=0&lang=zh_CN&installtype=0 HTTP/1.1 返回chunked分块数据
  • 11:21:49 POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/reportstrategy HTTP/1.1 (application/octet-stream) 心跳频率约为5分钟,登陆之后,会建立一个长连接,端口号为8080

2)初始消息传输

个人资料、离线未阅读消息部分等通过 POST HTTP短连接单独获取。抽取微信某次HTTP协议方式通信数据,16进制表示,每两个靠近的数字为一个byte字节:

3)微信协议可能如下:

一个消息包 = 消息头 + 消息体

消息头固定16字节长度,消息包长度定义在消息头前4个字节中。单纯摘取第0000行为例,共16个字节的头部:00 00 00 10 00 10 00 01 00 00 00 06 00 00 00 0f16进制表示,每两个紧挨着数字代表一个byte字节。

微信消息包格式:

  • 前4字节表示数据包长度,可变值为16时,意味着一个仅仅包含头部的完整的数据包(可能表示着预先定义好的业务意义),后面可能还有会别的消息包
  • 2个字节表示头部长度,固定值,0x10 = 16
  • 2个字节表示协议版本,固定值,0x01 = 1
  • 4个字节操作说明数字,可变
  • 序列号,可变
  • 头部后面紧跟着消息体,非明文,加密形式
  • 一个消息包,最小16 byte字节

4)新消息获取方式

  • TCP长连接接收到服务器通知有新消息需要获取
  • APP发起一个HTTP POST请求获取新状态消息,会带上当前SyncKey 地址为:http://short.weixin.qq.com/cgi-bin/micromsg-bin/reportstrategy HTTP/1.1,看不到明文
  • APP获取到新的消息,会再次发起一次HTTP POST请求,告诉服务器已确认收到,同时获取最新SyncKey 地址为:http://short.weixin.qq.com/cgi-bin/micromsg-bin/kvreport,看不到明文
  • 接受一个消息,TCP长连接至少交互两次,客户端发起两次HTTP POST请求
  • 服务器需要支持:状态消息获取标记,状态消息确认收取标记。只有被确认收到,此状态消息才算是被正确消费掉
  • 多个不同设备同一账号同时使用微信,同一个状态消息会会被同时分发到多个设备上

5)发送消息方式

  • 发送消息走已经建立的TCP长连接通道,发送消息到服务器,然后接受确认信息等,产生一次交互。
  • 小伙伴接收到信息阅读也都会收到服务器端通知,产生一次交互等。
  • 可以确定,微信发送消息走TCP长连接方式,因为不对自身状态数据产生影响,应该不交换SyncKey。
  • 在低速网络下,大概会看到消息发送中的提示,属于消息重发机制
  • 网络不好有时客户端会出现发送失败的红色感叹号
  • 已发送到服务器但未收到确认的消息,客户端显示红色感叹号,再次重发,服务器作为重复消息处理,反馈确认
  • 上传图片,会根据图片大小,分割成若干部分(大概5K被划分为一部分),同一时间点,客户端会发起若干次POST请求,各自上传成功之后,服务器大概会合并成一个完整图片,返回一个缩略图,显示在APP聊天窗口内。APP作为常规的文字消息发送到服务器端
  • 上传音频,则单独走TCP通道,一个两秒的录制音频,客户端录制完毕,分为两块传输,一块最大5K左右,服务端响应一条数据通知确认收到。共三次数据传输。
    音频和纯文字信息一致,都是走TCP长连接,客户端发送,服务器端确认。

6)微信协议小结

  • 发布的消息对应一个ID(只要单个方向唯一即可,服务器端可能会根ID判断重复接收),消息重传机制确保有限次的重试,重试失败给予用户提示,发送成功会反馈确认,客户端只有收到确认信息才知道发送成功。发送消息可能不会产生新SyncKey。
  • 基于版本号(SynKey)的状态消息同步机制,增量、有序传输需求水到渠成。长连接通知/短连接获取、确认等,交互方式简单,确保了消息可靠谱、准确无误到达。
  • 客户端/服务器端都会存储消息ID处理记录,避免被重复消费客户端获取最新消息,但未确认,服务器端不会认为该消息被消费掉。下次客户端会重新获取,会查询当前消息是否被处理过。根据一些现象猜测。
  • 总体上看,微信协议跨平台(TCP或HTPP都可呈现,处理方式可统一),通过“握手”同步,很可靠,无论哪一个平台都可以支持的很好
  • 微信协议最小成本为16字节,大部分时间若干个消息包和在一起,批量传输。微信协议说不上最简洁,也不是最节省流量,但是非常成功的。
  • 若服务器检测到一些不确定因素,可能会导致微启用安全套接层SSL协议进行常规的TCP长连接传输。短连接都没有发生变化

现在版本的微信消息推送,并非Sync方式,而是推送+ack方式;从他们协议设计来看,应该最开始用的是Notify + Sync Req + Sync Rsp 方式,因为协议上是不支持服务器发起请求的;现在改成Sync消息+ 单向Ack Req 的push方式,虽然协议上怪异, 相比Sync 消息接收会更加及时。从以前看到的文章都是说用的Sync协议,应该是是后期版本做了修改,push方式更为高效、而且通过顺序的SyncKey也能够修复丢失的消息。

Web客户端使用比较标准的Sync协议

web微信客户端,使用的是比较标准的Sync协议,Sync协议也比较适合web长轮询模型。

移动客户端模式下,协议是二进制的而且有加密,很难分析;微信侧重手机端,web端主体协议应该保持与移动端一致,可通过web端推测整体协议实现,json也比较好分析。

接收一条消息后SyncKey变化:

  • synckey:1_624161340|2_624162225|3_624162051|11_624161867|201_1420112604|1000_1420104656
  • synckey:1_624161340|2_624162226|3_624162051|11_624161867|201_1420112631|1000_1420104656

可以看出:

  • Synckey 有多个,应该是应对不同业务,其中2为为所有个人消息、讨论组消息,其他可能是联系人、朋友圈、订阅号等,201 为当前时间戳。
  • SyncKey 的值为数字自增,不是从0开始,应该有个固定的初始值。
  • 发消息时,发完需要自己给再自己同步回一下SyncKey。
  • 消息增量同步结构,一堆要同步字段+是否修改FlagMask,同步协议变得很简洁。

Web抓包数据

1)发起GET长连接检测是否存在新的需要同步的数据,会携带上最新SyncKey

https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?callback=jQuery18306073923335455973_1393208247730&r=1393209241862&sid=s7c%2FsxpGRSihgZAA&uin=937355&deviceid=e542565508353877&synckey=1_620943725%7C2_620943769%7C3_620943770%7C11_620942796%7C201_1393208420%7C202_1393209127%7C1000_1393203219&_=1393209241865

返回内容:

window.synccheck={retcode:”0″,selector:”2″}

selector值大于0,表示有新的消息需要同步。据目测,心跳周期为27秒左右。

2)一旦有新数据,客户端POST请求主动获取同步的数据

https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=s7c%2FsxpGRSihgZAA&r=1393208447375

携带消息体:

{“BaseRequest”:{“Uin”:937355,”Sid”:”s7c/sxpGRSihgZAA”},”SyncKey”:{“Count”:6,”List”:[{“Key”:1,”Val”:620943725},{“Key”:2,”Val”:620943767},{“Key”:3,”Val”:620943760},{“Key”:11,”Val”:620942796},{“Key”:201,”Val”:1393208365},{“Key”:1000,”Val”:1393203219}]},”rr”:1393208447374}

会携带上最新的SyncKey,会返回复杂结构体JSON内容。但浏览端收取到消息之后,如何通知服务器端已确认收到了?Web版本微信,没有去做。

3)发送消息流程

发起一个POST提交,用于提交用户需要发送的消息

https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?sid=lQ95vHR52DiaLVqo&r=1393988414386

发送内容:

{“BaseRequest”:{“Uin”:937355,”Sid”:”lQ95vHR52DiaLVqo”,”Skey”:”A6A1ECC6A7DE59DEFF6A05F226AA334DECBA457887B25BC6″,”DeviceID”:”e937227863752975″},”Msg”:{“FromUserName”:”yongboy”,”ToUserName”:”hehe057854″,”Type”:1,”Content”:”hello”,”ClientMsgId”:1393988414380,”LocalID”:1393988414380}}

相应内容:

{“BaseResponse”: {“Ret”: 0,”ErrMsg”: “”},”MsgID”: 1020944348,”LocalID”: “1393988414380”}

再次发起一个POST请求,用于申请最新SyncKey

https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=lQ95vHR52DiaLVqo&r=1393988414756

发送内容:

{“BaseRequest”:{“Uin”:937355,”Sid”:”lQ95vHR52DiaLVqo”},”SyncKey”:{“Count”:6,”List”:[{“Key”:1,”Val”:620944310},{“Key”:2,”Val”:620944346},{“Key”:3,”Val”:620944344},{“Key”:11,”Val”:620942796},{“Key”:201,”Val”:1393988357},{“Key”:1000,”Val”:1393930108}]},”rr”:1393988414756}

响应的(部分)内容:

“SKey”: “8F8C6A03489E85E9FDF727ACB95C93C2CDCE9FB9532FC15B”

终止GET长连接,使用最新SyncKey再次发起一个新的GET长连接

https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck?callback=jQuery1830245810089652082181393988305564&r=1393988415015&sid=lQ95vHR52DiaLVqo&uin=937355&deviceid=e937227863752975&synckey=1620944310%7C2620944348%7C3620944344%7C11620942796%7C2011393988357%7C10001393930108&=1393988415016

微信多点登陆

IM产品的多点登陆逻辑特别复杂,很难做到很好的用户体验。微信最开始并不支持多点登陆,后来陆续增加的Web版、Mac版,但并不是完整意义的客户端,要说只是辅助工具。微信允许:一个移动端(下面称之为主客户端) + 一个web/mac 同时在线(下面称之为从客户端),web/mac 只能接收在线消息、发消息,不记录消息历史。这样多点逻辑就变得相对简单很多了。

1)存储

  • 服务器不用保存完整消息历史,通过客户端对push消息的ack保证消息送达,协议保证消息至少一次推送到主客户端,然后消息即可删除;服务器只存储未下发到主客户端的消息。
  • 多点时:主客户端依然采用Push推送消息(只是应该会保留一小段时间消息记录等待从Sync),从客户端Sync消息;如果主不在线,消息记录不会删除,等主重新连上下载离线消息。
  • 服务器不存储消息历史,一个是安全,再者节省硬件成本,大量短消息的存储和读取成本是非常高的,因为基本都是随机IO。whatapp 用500台机器,支撑1亿在线,100w/s 消息,只离线消息存储量是很少的。

2)未读数同步

这个很好解决,如果客户端知道自己处于多端在线情况下时,进入会话时,需要告诉服务器消息已读。消息已读也保存为一条消息,再通过Sync协议,同步到另外的客户端。web 微信会调用webwxstatusnotify 同步未读。未读变化也当成一条消息存储,且只在多端情况下存在,单点在线时未读数由客户端维护。

3)删除消息

不管是否多点在线任何删除消息操作都会同步到服务器,避免删除的消息下次有不小心同步回来了,服务器可能及时删除、也可能长期保存,客户端每次上报就没错了。移动客户端消息删除不会同步到 web版。

4)自己同步自己

每个操作(发消息、清未读等),应答后,因为SyncKey 变化了,Sync协议上会产生一个空的Sync操作用于更新SyncKey。对于主客户端:

  • 单点在线时,SyncKey通过应答,节省同步流量。
  • 多点在线时,发消息和从客户端一样,也会自己同步自己。

参考链接:

打赏作者
微信支付标点符 wechat qrcode
支付宝标点符 alipay qrcode

C语言学习:size_t

在学习C语言的时候,遇到了一个新的数据类型size_t,截止目前也没有完全理清这个类似的具体场景及出现的原因。
44 sec read

C语言学习:main()函数的正确写法

C语言虽然是一门古老的语言,但是其标准一直在完善,所以很多以前支持的语法在到当前已经不能在使用了。 C语言的版
41 sec read

Scipy数学函数的Scala实现

最近在推进项目的时候,遇到需要将线下的Python代码转化成线上的集群代码,由于机器代码环境是Scala,所以
4 min read

发表评论

电子邮件地址不会被公开。 必填项已用*标注