聲明:微信客戶端協議是二進制協議而且加密,難以分析協議具體編碼格式,我不做逆向工程。只是簡單抓包分析業務的實現流程,在這里記錄下來用於參考學習,並不是破解協議。
參考:
Sync協議
道聽途說,加上上面參考中都是提到微信使用Sync協議。去年項目中因此也嘗試參考 Microsoft Exchange ActiveSync 協議來優化消息協議,實現過程中才發現Sync並不是表面上那么簡單。
Sync 有啥問題呢?
1. SyncKey 生成維護成本
SyncKey 在ActiveSync中為字符串,客戶端不需要解析,但服務端實現要用數字自增,需要強一致性,且不能回退。
2. 消息的訂閱模式 采用類似Zookeeper的One time triggler 還是每條消息都推送一條通知能
One time trigger能夠避免並發通知時,獲取消息時重復問題,但增加了交互成本,和客戶端實現復雜性。
3. 自己發的消息,SyncKey怎么獲取
尤其要支持多端同步發消息,保證消息同步;也只好消息發完在給自己同步一遍(自己設備發的可以不帶消息體)
4. 消息推送延時加重
Sync 消息體獲取方式:Notify - Ack - get - Mssage, 也就是至少第四個應用包才能返回消息,在移動網絡下成本很高。文中提到消息通過單獨https請求,那么延時更為嚴重了(嗯,實測新版本並非如此)。
手機客戶端不再Sync協議
抓包分析版本:Android 微信6.0, 抓包分析可參考:android 移動網絡實時抓包
在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,將隨后文章分析。
-收消息:
1. 服務器推送消息, Operation = 7a, Body 中攜帶消息內容
抓包分析時,通過改變消息體大小,可能到接收方第一個包length 也會隨之變化,可確認消息是push的。
2. 發起客戶端單向請求,即消息的應答Ack
-加密:
未分析,一般來說像whatsapp那樣,使用 hash(密碼/OTP + 長連接第一個請求獲取RandomCode) 做RC4 的密鑰
總結:
現在版本的微信消息推送,並非Sync方式,而是推送+ack方式;
從他們協議設計來看,應該最開始用的是Notify + Sync Req + Sync Rsp 方式,因為協議上是不支持服務器發起請求的;
現在改成 Sync 消息+ 單向 Ack Req 的push方式,雖然協議上怪異, 相比Sync 消息接收會更加及時。從以前看到的文章都是說用的Sync協議,應該是是后期版本做了修改,push方式更為高效、而且通過順序的SyncKey也能夠修復丟失的消息。
下面一個通知包示例:
4 byte: 265 PacketLen
2 byte: 16 Head Length
2 byte: 1 Protocol Version
4 byte: 7a Operation
4 byte: 0 SeqId //這是一個通知性消息
4 byte: unknow header
……. Body
Web客戶端
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,同步協議變得很簡潔。
{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
}
,
"AddMsgCount": 1,
"AddMsgList": [{
"MsgId": 1625734358,
"FromUserName": "@sssss",
"ToUserName": "@ssssssss2",
"MsgType": 1,
"Content": "捶地笑……",
"Status": 3,
"ImgStatus": 1,
"CreateTime": 1420109883,
"VoiceLength": 0,
"PlayLength": 0,
"FileName": "",
"FileSize": "",
"MediaId": "",
"Url": "",
"AppMsgType": 0,
"StatusNotifyCode": 0,
"StatusNotifyUserName": "",
"RecommendInfo": {
"UserName": "",
"NickName": "",
"QQNum": 0,
"Province": "",
"City": "",
"Content": "",
"Signature": "",
"Alias": "",
"Scene": 0,
"VerifyFlag": 0,
"AttrStatus": 0,
"Sex": 0,
"Ticket": "",
"OpCode": 0
}
,
"ForwardFlag": 0,
"AppInfo": {
"AppID": "",
"Type": 0
}
,
"HasProductId": 0,
"Ticket": ""
}
],
"ModContactCount": 0,
"ModContactList": [],
"DelContactCount": 0,
"DelContactList": [],
"ModChatRoomMemberCount": 0,
"ModChatRoomMemberList": [],
"Profile": {
"BitFlag": 0,
"UserName": {
"Buff": ""
}
,
"NickName": {
"Buff": ""
}
,
"BindUin": 0,
"BindEmail": {
"Buff": ""
}
,
"BindMobile": {
"Buff": ""
}
,
"Status": 0,
"Sex": 0,
"PersonalCard": 0,
"Alias": "",
"HeadImgUpdateFlag": 0,
"HeadImgUrl": "",
"Signature": ""
}
,
"ContinueFlag": 0,
"SyncKey": {
"Count": 6,
"List": [{
"Key": 1,
"Val": 624161340
}
,{
"Key": 2,
"Val": 624162166
}
,{
"Key": 3,
"Val": 624162051
}
,{
"Key": 11,
"Val": 624161867
}
,{
"Key": 201,
"Val": 1420109883
}
,{
"Key": 1000,
"Val": 1420104656
}
]
}
,
"SKey": ""
}