前言
微信可調(diào)研點(diǎn)很多,這里僅僅從協(xié)議角度進(jìn)行調(diào)研,會(huì)涉及到微信協(xié)議交換、消息收發(fā)等。所謂“弱水三千,只取一瓢”吧。
雜七雜八的,有些長,可直接拉到最后看結(jié)論好了。
一。微信協(xié)議概覽
微信傳輸協(xié)議,官方公布甚少,在微信技術(shù)總監(jiān)所透漏PPT《微信之道—至簡》文檔中,有所體現(xiàn)。
純個(gè)人理解:
因張小龍做郵箱Foxmail起家,繼而又做了QQ Mail等,QQ Mail是國內(nèi)第一個(gè)支持Exchange ActiveSync協(xié)議的免費(fèi)郵箱,基于其從業(yè)背景,微信從一開始就采取基于ActiveSync的修改版狀態(tài)同步協(xié)議Sync,也就再自然不過了。
一句話:增量式、按序、可靠的狀態(tài)同步傳輸?shù)奈⑿艆f(xié)議。
大致交換簡圖如下:
_thumb.png)
如何獲取新數(shù)據(jù)呢:
- 服務(wù)器端通知,客戶端獲取
- 客戶端攜帶最新的SyncKey,發(fā)起數(shù)據(jù)請求
- 服務(wù)器端生成最新的SyncKey連同最新數(shù)據(jù)發(fā)送給客戶端
- 基于版本號機(jī)制同步協(xié)議,可確保數(shù)據(jù)增量、有序傳輸
- SyncKey,由服務(wù)器端序列號生成器生成,一旦有新消息產(chǎn)生,將會(huì)產(chǎn)生最新的SyncKey。類似于版本號
服務(wù)器端通知有狀態(tài)更新,客戶端主動(dòng)獲取自從上次更新之后有變動(dòng)的狀態(tài)數(shù)據(jù),增量式,順序式。
二。微信Web端簡單調(diào)試
在線版本微信:
https://webpush.weixin.qq.com/
通過Firefox + Firebug組合調(diào)試,也能證實(shí)了微信大致通過交換SyncKey方式獲取新數(shù)據(jù)的論述。
1. 發(fā)起GET長連接檢測是否存在新的需要同步的數(shù)據(jù)
會(huì)攜帶上最新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
返回內(nèi)容:
window.synccheck={retcode:"0",selector:"2"}
selector值大于0,表示有新的消息需要同步。
據(jù)目測,心跳周期為27秒左右。
2. 一旦有新數(shù)據(jù),客戶端POST請求主動(dòng)獲取同步的數(shù)據(jù)
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}
會(huì)攜帶上最新的SyncKey,會(huì)返回復(fù)雜結(jié)構(gòu)體JSON內(nèi)容。
但瀏覽端收取到消息之后,如何通知服務(wù)器端已確認(rèn)收到了?Web版本微信,沒有去做。
在以往使用過程中,曾發(fā)現(xiàn)WEB端有丟失消息的現(xiàn)象,但屬于偶爾現(xiàn)象。但Android微信客戶端(只要登陸連接上來之后)貌似就沒有丟失過。
3. 發(fā)送消息流程
-
發(fā)起一個(gè)POST提交,用于提交用戶需要發(fā)送的消息
https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?sid=lQ95vHR52DiaLVqo&r=1393988414386
發(fā)送內(nèi)容:
{"BaseRequest":{"Uin":937355,"Sid":"lQ95vHR52DiaLVqo","Skey":"A6A1ECC6A7DE59DEFF6A05F226AA334DECBA457887B25BC6","DeviceID":"e937227863752975"},"Msg":{"FromUserName":"yongboy","ToUserName":"hehe057854","Type":1,"Content":"hello","ClientMsgId":1393988414380,"LocalID":1393988414380}}
相應(yīng)內(nèi)容:
{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
}
,
"MsgID": 1020944348,
"LocalID": "1393988414380"
}
-
再次發(fā)起一個(gè)POST請求,用于申請最新SyncKey
https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/webwxsync?sid=lQ95vHR52DiaLVqo&r=1393988414756
發(fā)送內(nèi)容:
{"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}
響應(yīng)的(部分)內(nèi)容:
"SKey": "8F8C6A03489E85E9FDF727ACB95C93C2CDCE9FB9532FC15B"
-
終止GET長連接,使用最新SyncKey再次發(fā)起一個(gè)新的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
三。微信Android簡單分析
Windows桌面端Android虛擬機(jī)中運(yùn)行最新版微信(5.2),通過tcpdump/Wireshark組合封包分析,以下為分析結(jié)果。
0. 初始連接記錄
簡單記錄微信啟動(dòng)之后請求:
11:20:35 dns查詢
dns.weixin.qq.com
返回一組IP地址
11:20:35 DNS查詢
long.weixin.qq.com
返回一組IP地址,本次通信中,微信使用了最后一個(gè)IP作為TCP長連接的連接地址。
11:20:35
http:
用于請求服務(wù)器獲得最優(yōu)IP路徑。服務(wù)器通過結(jié)算返回一個(gè)xml定義了域名:IP對應(yīng)列表。仔細(xì)閱讀,可看到微信已經(jīng)開始了國際化的步伐:香港、加拿大、韓國等。
具體文本,請參考:https:
11:20:35
獲取到long.weixin.qq.com最優(yōu)IP,然后建立到101.227.131.105的TCP長連接
11:21:25
POST http:
返回一個(gè)名為“micromsgresp.dat”的附件,估計(jì)是未閱讀的離線消息
11:21:31
POST http:
大概是資訊、訂閱更新等
中間進(jìn)行一些資源請求等,類似于
GET http:
圖片等一些靜態(tài)資源都會(huì)被分配到wx.qlogo.cn域名下面
不明白做什么用途
POST http:
輸出為micromsgresp.dat文件
11:21:47
GET http:
返回chunked分塊數(shù)據(jù)
11:21:49
POST http:
1. 心跳頻率約為5分鐘
上次使用Wireshark分析有誤(得出18分鐘結(jié)論),再次重新分析,心跳頻率在5分鐘左右。
2. 登陸之后,會(huì)建立一個(gè)長連接,端口號為8080
簡單目測為HTTP,初始以為是雙通道HTTP,難道是自定義的用于雙通道通信的HTTP協(xié)議嗎,網(wǎng)絡(luò)上可見資料都是模棱兩可、語焉不詳。
具體查看長連接初始數(shù)據(jù)通信,沒有發(fā)現(xiàn)任何包含"HTTP"字樣的數(shù)據(jù),以為是微信自定義的TCP/HTTP通信格式。據(jù)分析,用于可能用于獲取數(shù)據(jù)、心跳交換消息等用途吧。這個(gè)后面會(huì)詳談微信是如何做到的。
2.0 初始消息傳輸
個(gè)人資料、離線未閱讀消息部分等通過 POST HTTP短連接單獨(dú)獲取。
2.1 二進(jìn)制簡單分析
抽取微信某次HTTP協(xié)議方式通信數(shù)據(jù),16進(jìn)制表示,每兩個(gè)靠近的數(shù)字為一個(gè)byte字節(jié):

微信協(xié)議可能如下:
一個(gè)消息包 = 消息頭 + 消息體
消息頭固定16字節(jié)長度,消息包長度定義在消息頭前4個(gè)字節(jié)中。
單純摘取第0000行為例,共16個(gè)字節(jié)的頭部:
00 00 00 10 00 10 00 01 00 00 00 06 00 00 00 0f
16進(jìn)制表示,每兩個(gè)緊挨著數(shù)字代表一個(gè)byte字節(jié)。
微信消息包格式: 1. 前4字節(jié)表示數(shù)據(jù)包長度,可變 值為16時(shí),意味著一個(gè)僅僅包含頭部的完整的數(shù)據(jù)包(可能表示著預(yù)先定義好的業(yè)務(wù)意義),后面可能還有會(huì)別的消息包 2. 2個(gè)字節(jié)表示頭部長度,固定值,0x10 = 16 3. 2個(gè)字節(jié)表示謝意版本,固定值,0x01 = 1 4. 4個(gè)字節(jié)操作說明數(shù)字,可變 5. 序列號,可變 6. 頭部后面緊跟著消息體,非明文,加密形式 7. 一個(gè)消息包,最小16 byte字節(jié)
通過上圖(以及其它數(shù)據(jù)多次采樣)分析:
- 0000 - 0040為單獨(dú)的數(shù)據(jù)包
- 0050行為下一個(gè)數(shù)據(jù)包的頭部,前四個(gè)字節(jié)值為0xca = 202,表示包含了從0050-0110共202個(gè)字節(jié)數(shù)據(jù)
- 一次數(shù)據(jù)發(fā)送,可能包含若干子數(shù)據(jù)包
- 換行符\n,16進(jìn)制表示為0x0a,在00f0行,包含了兩個(gè)換行符號
- 一個(gè)數(shù)據(jù)體換行符號用于更細(xì)粒度的業(yè)務(wù)數(shù)據(jù)分割 是否蒙對,需要問問做微信協(xié)議的同學(xué)
- 所有被標(biāo)記為HTTP協(xié)議通信所發(fā)送數(shù)據(jù)都包含換行符號
2.2 動(dòng)手試試猜想,模擬微信TCP長連接
開始很不解為什么會(huì)出現(xiàn)如此怪異的HTTP雙通道長連接請求,難道基于TCP通信,然后做了一些手腳?很常規(guī)的TCP長連接,傳輸數(shù)據(jù)時(shí)(不是所有數(shù)據(jù)傳輸),被wireshark誤認(rèn)為HTTP長連接。這個(gè)需要做一個(gè)實(shí)驗(yàn)證實(shí)一下自己想法,設(shè)想如下:
寫一個(gè)Ping-Pong客戶端、服務(wù)器端程序,然后使用Wireshark看一下結(jié)果,是否符合判斷。
Java版本的請求端,默認(rèn)請求8080端口:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
/** |
|
* Ping Client |
|
* @author nieyong |
|
*/ |
|
package com.learn; |
|
|
|
import io.netty.bootstrap.Bootstrap; |
|
import io.netty.buffer.ByteBuf; |
|
import io.netty.buffer.PooledByteBufAllocator; |
|
import io.netty.channel.ChannelFuture; |
|
import io.netty.channel.ChannelHandlerContext; |
|
import io.netty.channel.ChannelInboundHandlerAdapter; |
|
import io.netty.channel.ChannelInitializer; |
|
import io.netty.channel.ChannelOption; |
|
import io.netty.channel.EventLoopGroup; |
|
import io.netty.channel.nio.NioEventLoopGroup; |
|
import io.netty.channel.socket.SocketChannel; |
|
import io.netty.channel.socket.nio.NioSocketChannel; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
class PingClientHandler extends ChannelInboundHandlerAdapter { |
|
private final ByteBuf firstMessage; |
|
|
|
public PingClientHandler() { |
|
firstMessage = PooledByteBufAllocator.DEFAULT.buffer(22); |
|
|
|
// weixin 16 byte's header |
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(16); |
|
|
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(16); |
|
|
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(1); |
|
|
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(6); |
|
|
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(0); |
|
firstMessage.writeByte(1); |
|
|
|
// just for /n |
|
firstMessage.writeByte('\n'); // 1 byte |
|
|
|
// footer 16 byte |
|
String welcome = "hello"; // 5 byte |
|
firstMessage.writeBytes(welcome.getBytes()); |
|
} |
|
|
|
@Override |
|
public void channelActive(ChannelHandlerContext ctx) { |
|
ctx.writeAndFlush(firstMessage); |
|
} |
|
|
|
@Override |
|
public void channelRead(final ChannelHandlerContext ctx, final Object msg) |
|
throws Exception { |
|
ctx.executor().schedule(new Runnable() { |
|
@Override |
|
public void run() { |
|
ctx.channel().writeAndFlush(msg); |
|
} |
|
}, 1, TimeUnit.SECONDS); |
|
} |
|
|
|
@Override |
|
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { |
|
ctx.flush(); |
|
} |
|
|
|
@Override |
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { |
|
System.err.println("Unexpected exception from downstream :" |
|
+ cause.getMessage()); |
|
ctx.close(); |
|
} |
|
} |
|
|
|
public class PingClient { |
|
|
|
private final String host; |
|
private final int port; |
|
|
|
public PingClient(String host, int port) { |
|
this.host = host; |
|
this.port = port; |
|
} |
|
|
|
public void run() throws Exception { |
|
EventLoopGroup group = new NioEventLoopGroup(); |
|
try { |
|
Bootstrap b = new Bootstrap(); |
|
b.group(group).channel(NioSocketChannel.class) |
|
.option(ChannelOption.TCP_NODELAY, true) |
|
.handler(new ChannelInitializer<SocketChannel>() { |
|
@Override |
|
public void initChannel(SocketChannel ch) |
|
throws Exception { |
|
ch.pipeline().addLast(new PingClientHandler()); |
|
} |
|
}); |
|
|
|
ChannelFuture f = b.connect(host, port).sync(); |
|
|
|
f.channel().closeFuture().sync(); |
|
} finally { |
|
// Shut down the event loop to terminate all threads. |
|
group.shutdownGracefully(); |
|
} |
|
} |
|
|
|
public static void main(String[] args) throws Exception { |
|
String host = "127.0.0.1"; |
|
int port = 8080; |
|
|
|
if (args.length == 3) { |
|
host = args[0]; |
|
port = Integer.parseInt(args[1]); |
|
} |
|
|
|
new PingClient(host, port).run(); |
|
} |
|
} |
C語言版本的服務(wù)器程序,收到什么發(fā)送什么,沒有任何邏輯,默認(rèn)綁定8080端口:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
/** |
|
* nieyong@youku.com |
|
* how to compile it: |
|
* gcc pong_server.c -o pong_server /usr/local/lib/libev.a -lm |
|
*/ |
|
#include <arpa/inet.h> |
|
#include <stdlib.h> |
|
#include <stdio.h> |
|
#include <string.h> |
|
#include <fcntl.h> |
|
#include <errno.h> |
|
#include <err.h> |
|
#include <unistd.h> |
|
|
|
#include "../include/ev.h" |
|
|
|
static int server_port = 8080; |
|
|
|
struct ev_loop *loop; |
|
typedef struct { |
|
int fd; |
|
ev_io ev_read; |
|
} client_t; |
|
|
|
ev_io ev_accept; |
|
|
|
static void free_res(struct ev_loop *loop, ev_io *ws); |
|
|
|
int setnonblock(int fd) { |
|
int flags = fcntl(fd, F_GETFL); |
|
if (flags < 0) |
|
return flags; |
|
|
|
flags |= O_NONBLOCK; |
|
if (fcntl(fd, F_SETFL, flags) < 0) |
|
return -1; |
|
|
|
return 0; |
|
} |
|
|
|
static void read_cb(struct ev_loop *loop, ev_io *w, int revents) { |
|
client_t *client = w->data; |
|
int r = 0; |
|
char rbuff[1024]; |
|
if (revents & EV_READ) { |
|
r = read(client->fd, &rbuff, 1024); |
|
} |
|
|
|
if (EV_ERROR & revents) { |
|
fprintf(stderr, "error event in read\n"); |
|
free_res(loop, w); |
|
return ; |
|
} |
|
|
|
if (r < 0) { |
|
fprintf(stderr, "read error\n"); |
|
ev_io_stop(EV_A_ w); |
|
free_res(loop, w); |
|
return; |
|
} |
|
|
|
if (r == 0) { |
|
fprintf(stderr, "client disconnected.\n"); |
|
ev_io_stop(EV_A_ w); |
|
free_res(loop, w); |
|
return; |
|
} |
|
|
|
send(client->fd, rbuff, r, 0); |
|
} |
|
|
|
static void accept_cb(struct ev_loop *loop, ev_io *w, int revents) { |
|
struct sockaddr_in client_addr; |
|
socklen_t client_len = sizeof(client_addr); |
|
int client_fd = accept(w->fd, (struct sockaddr *) &client_addr, &client_len); |
|
if (client_fd == -1) { |
|
fprintf(stderr, "the client_fd is NULL !\n"); |
|
return; |
|
} |
|
|
|
client_t *client = malloc(sizeof(client_t)); |
|
client->fd = client_fd; |
|
if (setnonblock(client->fd) < 0) |
|
err(1, "failed to set client socket to non-blocking"); |
|
|
|
client->ev_read.data = client; |
|
|
|
ev_io_init(&client->ev_read, read_cb, client->fd, EV_READ); |
|
ev_io_start(loop, &client->ev_read); |
|
} |
|
|
|
int main(int argc, char const *argv[]) { |
|
int ch; |
|
while ((ch = getopt(argc, argv, "p:")) != -1) { |
|
switch (ch) { |
|
case 'p': |
|
server_port = atoi(optarg); |
|
break; |
|
} |
|
} |
|
|
|
loop = ev_default_loop(0); |
|
struct sockaddr_in listen_addr; |
|
int reuseaddr_on = 1; |
|
int listen_fd = socket(AF_INET, SOCK_STREAM, 0); |
|
if (listen_fd < 0) |
|
err(1, "listen failed"); |
|
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr_on, sizeof(reuseaddr_on)) == -1) |
|
err(1, "setsockopt failed"); |
|
|
|
memset(&listen_addr, 0, sizeof(listen_addr)); |
|
listen_addr.sin_family = AF_INET; |
|
listen_addr.sin_addr.s_addr = INADDR_ANY; |
|
listen_addr.sin_port = htons(server_port); |
|
|
|
if (bind(listen_fd, (struct sockaddr *) &listen_addr, sizeof(listen_addr)) < 0) |
|
err(1, "bind failed"); |
|
if (listen(listen_fd, 5) < 0) |
|
err(1, "listen failed"); |
|
if (setnonblock(listen_fd) < 0) |
|
err(1, "failed to set server socket to non-blocking"); |
|
|
|
ev_io_init(&ev_accept, accept_cb, listen_fd, EV_READ); |
|
ev_io_start(loop, &ev_accept); |
|
ev_loop(loop, 0); |
|
|
|
return 0; |
|
} |
|
|
|
static void free_res(struct ev_loop *loop, ev_io *w) { |
|
client_t *client = w->data; |
|
if (client == NULL) { |
|
fprintf(stderr, "the client is NULL !!!!!!"); |
|
return; |
|
} |
|
|
|
ev_io_stop(loop, &client->ev_read); |
|
close(client->fd); |
|
free(client); |
|
} |
這里有一個(gè)現(xiàn)場圖:

可以嘗試稍微改變輸出內(nèi)容,去除換行符“\n”,把端口換成9000,試試看,就會(huì)發(fā)現(xiàn)Wireshark輸出不同的結(jié)果來。
2.3 結(jié)論是什么呢?
若使用原始TCP進(jìn)行雙向通信,則需要滿足以下條件,可以被類似于Wireshark協(xié)議攔截器誤認(rèn)為是HTTP長連接:
- 使用80/8080端口(81/3128/8000經(jīng)測試無效) 也許8080一般被作為WEB代理服務(wù)端口,微信才會(huì)享用這個(gè)紅利吧。
- 輸出的內(nèi)容中,一定要包含換行字符"\n"
因此,可以定性為微信使用了基于8080端口TCP長連接,一旦數(shù)據(jù)包中含有換行"\n"符號,就會(huì)被Wireshark誤認(rèn)為HTTP協(xié)議。可能微信是無心為之吧。
3. 新消息獲取方式
- TCP長連接接收到服務(wù)器通知有新消息需要獲取
- APP發(fā)起一個(gè)HTTP POST請求獲取新狀態(tài)消息,會(huì)帶上當(dāng)前SyncKey 地址為:http://short.weixin.qq.com/cgi-bin/micromsg-bin/reportstrategy HTTP/1.1,看不到明文
- APP獲取到新的消息,會(huì)再次發(fā)起一次HTTP POST請求,告訴服務(wù)器已確認(rèn)收到,同時(shí)獲取最新SyncKey 地址為:http://short.weixin.qq.com/cgi-bin/micromsg-bin/kvreport,看不到明文
- 接受一個(gè)消息,TCP長連接至少交互兩次,客戶端發(fā)起兩次HTTP POST請求
具體每次交互內(nèi)容是什么,有些模糊
- 服務(wù)器需要支持:狀態(tài)消息獲取標(biāo)記,狀態(tài)消息確認(rèn)收取標(biāo)記。只有被確認(rèn)收到,此狀態(tài)消息才算是被正確消費(fèi)掉
- 多個(gè)不同設(shè)備同一賬號同時(shí)使用微信,同一個(gè)狀態(tài)消息會(huì)會(huì)被同時(shí)分發(fā)到多個(gè)設(shè)備上
此時(shí)消息請求截圖如下:

4. 發(fā)送消息方式
發(fā)送消息走已經(jīng)建立的TCP長連接通道,發(fā)送消息到服務(wù)器,然后接受確認(rèn)信息等,產(chǎn)生一次交互。
小伙伴接收到信息閱讀也都會(huì)收到服務(wù)器端通知,產(chǎn)生一次交互等。
可以確定,微信發(fā)送消息走TCP長連接方式,因?yàn)椴粚ψ陨頎顟B(tài)數(shù)據(jù)產(chǎn)生影響,應(yīng)該不交換SyncKey。
- 在低速網(wǎng)絡(luò)下,大概會(huì)看到消息發(fā)送中的提示,屬于消息重發(fā)機(jī)制
- 網(wǎng)絡(luò)不好有時(shí)客戶端會(huì)出現(xiàn)發(fā)送失敗的紅色感嘆號
- 已發(fā)送到服務(wù)器但未收到確認(rèn)的消息,客戶端顯示紅色感嘆號,再次重發(fā),服務(wù)器作為重復(fù)消息處理,反饋確認(rèn)
- 上傳圖片,會(huì)根據(jù)圖片大小,分割成若干部分(大概1.5K被劃分為一部分),同一時(shí)間點(diǎn),客戶端會(huì)發(fā)起若干次POST請求,各自上傳成功之后,服務(wù)器大概會(huì)合并成一個(gè)完整圖片,返回一個(gè)縮略圖,顯示在APP聊天窗口內(nèi)。APP作為常規(guī)的文字消息發(fā)送到服務(wù)器端
- 上傳音頻,則單獨(dú)走TCP通道,一個(gè)兩秒的錄制音頻,客戶端錄制完畢,分為兩塊傳輸,一塊最大1.5K左右,服務(wù)端響應(yīng)一條數(shù)據(jù)通知確認(rèn)收到。共三次數(shù)據(jù)傳輸。
音頻和純文字信息一致,都是走TCP長連接,客戶端發(fā)送,服務(wù)器端確認(rèn)。
四。微信協(xié)議小結(jié)
- 發(fā)布的消息對應(yīng)一個(gè)ID(只要單個(gè)方向唯一即可,服務(wù)器端可能會(huì)根ID判斷重復(fù)接收),消息重傳機(jī)制確保有限次的重試,重試失敗給予用戶提示,發(fā)送成功會(huì)反饋確認(rèn),客戶端只有收到確認(rèn)信息才知道發(fā)送成功。發(fā)送消息可能不會(huì)產(chǎn)生新SyncKey。
- 基于版本號(SynKey)的狀態(tài)消息同步機(jī)制,增量、有序傳輸需求水到渠成。長連接通知/短連接獲取、確認(rèn)等,交互方式簡單,確保了消息可靠譜、準(zhǔn)確無誤到達(dá)。
- 客戶端/服務(wù)器端都會(huì)存儲消息ID處理記錄,避免被重復(fù)消費(fèi)客戶端獲取最新消息,但未確認(rèn),服務(wù)器端不會(huì)認(rèn)為該消息被消費(fèi)掉。下次客戶端會(huì)重新獲取,會(huì)查詢當(dāng)前消息是否被處理過。根據(jù)一些現(xiàn)象猜測。
- 總體上看,微信協(xié)議跨平臺(TCP或HTPP都可呈現(xiàn),處理方式可統(tǒng)一),通過“握手”同步,很可靠,無論哪一個(gè)平臺都可以支持的很好
- 微信協(xié)議最小成本為16字節(jié),大部分時(shí)間若干個(gè)消息包和在一起,批量傳輸。微信協(xié)議說不上最簡潔,也不是最節(jié)省流量,但是非常成功的。
若服務(wù)器檢測到一些不確定因素,可能會(huì)導(dǎo)致微啟用安全套接層SSL協(xié)議進(jìn)行常規(guī)的TCP長連接傳輸。短連接都沒有發(fā)生變化
以上,根據(jù)有限資料和數(shù)據(jù)攔截觀察總結(jié)得出,啰啰嗦嗦,勉強(qiáng)湊成一篇,會(huì)存在一些不正確之處,歡迎給予糾正。在多次
五。附錄
Microsoft Exchange Active Sync協(xié)議,簡稱EAS,分為folderrsync(同步文件夾目錄,即郵箱內(nèi)有哪幾個(gè)文件夾)和sync(每個(gè)文件夾內(nèi)有哪些文檔)兩部分。
某網(wǎng)友總結(jié)的協(xié)議一次回話大致示范:
Client: synckey=0 //第一次key為0
Server: newsynckey=1235434 //第一次返回新key
Client: synckey=1235434 //使用新key查詢
Server: newsynckey=1647645,data=*****//第一次查詢,得到新key和數(shù)據(jù)
Client: synckey=1647645
Server: newsynckey=5637535,data=null //第二次查詢,無新消息
Client: synckey=5637535
Server: newsynckey=8654542, data=****//第三次查詢,增量同步
- 上頁中的相鄰請求都是隔固定時(shí)間的,如兩分鐘
- 客戶端每次使用舊key標(biāo)記自己的狀態(tài),服務(wù)端每次將新key和增量數(shù)據(jù)一起返回。
- key是遞增的,但不要求連續(xù)
- 請求的某個(gè)參數(shù)決定服務(wù)器是否立即返回