转自:http://www.blogjava.net/yongboy/archive/2014/03/05/410636.html
微信可调研点很多,这里仅仅从协议角度进行调研,会涉及到微信协议交换、消息收发等。所谓“弱水三千,只取一瓢”吧。
杂七杂八的,有些长,可直接拉到最后看结论好了。
微信传输协议,官方公布甚少,在微信技术总监所透漏PPT《微信之道—至简》文档中,有所体现。
纯个人理解:
因张小龙做邮箱Foxmail起家,继而又做了QQ Mail等,QQ Mail是国内第一个支持Exchange ActiveSync协议的免费邮箱,基于其从业背景,微信从一开始就采取基于ActiveSync的修改版状态同步协议Sync,也就再自然不过了。
一句话:增量式、按序、可靠的状态同步传输的微信协议。
大致交换简图如下:
如何获取新数据呢:
服务器端通知有状态更新,客户端主动获取自从上次更新之后有变动的状态数据,增量式,顺序式。
在线版本微信:
https://webpush.weixin.qq.com/
通过Firefox + Firebug组合调试,也能证实了微信大致通过交换SyncKey方式获取新数据的论述。
会携带上最新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秒左右。
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版本微信,没有去做。
在以往使用过程中,曾发现WEB端有丢失消息的现象,但属于偶尔现象。但Android微信客户端(只要登陆连接上来之后)貌似就没有丢失过。
发起一个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
Windows桌面端Android虚拟机中运行最新版微信(5.2),通过tcpdump/Wireshark组合封包分析,以下为分析结果。
简单记录微信启动之后请求:
11:20:35 dns查询
dns.weixin.qq.com
返回一组IP地址
11:20:35 DNS查询
long.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对应列表。仔细阅读,可看到微信已经开始了国际化的步伐:香港、加拿大、韩国等。
具体文本,请参考:https://gist.github.com/yongboy/9341884
11:20:35
获取到long.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
图片等一些静态资源都会被分配到wx.qlogo.cn域名下面
不明白做什么用途
POST http://short.weixin.qq.com/cgi-bin/micromsg-bin/downloadpackage HTTP/1.1 (application/octet-stream)
输出为micromsgresp.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)
上次使用Wireshark分析有误(得出18分钟结论),再次重新分析,心跳频率在5分钟左右。
简单目测为HTTP,初始以为是双通道HTTP,难道是自定义的用于双通道通信的HTTP协议吗,网络上可见资料都是模棱两可、语焉不详。
具体查看长连接初始数据通信,没有发现任何包含"HTTP"字样的数据,以为是微信自定义的TCP/HTTP通信格式。据分析,用于可能用于获取数据、心跳交换消息等用途吧。这个后面会详谈微信是如何做到的。
个人资料、离线未阅读消息部分等通过 POST HTTP短连接单独获取。
抽取微信某次HTTP协议方式通信数据,16进制表示,每两个靠近的数字为一个byte字节:
微信协议可能如下:
一个消息包 = 消息头 + 消息体
消息头固定16字节长度,消息包长度定义在消息头前4个字节中。
单纯摘取第0000行为例,共16个字节的头部:
00 00 00 10 00 10 00 01 00 00 00 06 00 00 00 0f
16进制表示,每两个紧挨着数字代表一个byte字节。
微信消息包格式: 1. 前4字节表示数据包长度,可变 值为16时,意味着一个仅仅包含头部的完整的数据包(可能表示着预先定义好的业务意义),后面可能还有会别的消息包 2. 2个字节表示头部长度,固定值,0x10 = 16 3. 2个字节表示谢意版本,固定值,0x01 = 1 4. 4个字节操作说明数字,可变 5. 序列号,可变 6. 头部后面紧跟着消息体,非明文,加密形式 7. 一个消息包,最小16 byte字节
通过上图(以及其它数据多次采样)分析:
开始很不解为什么会出现如此怪异的HTTP双通道长连接请求,难道基于TCP通信,然后做了一些手脚?很常规的TCP长连接,传输数据时(不是所有数据传输),被wireshark误认为HTTP长连接。这个需要做一个实验证实一下自己想法,设想如下:
写一个Ping-Pong客户端、服务器端程序,然后使用Wireshark看一下结果,是否符合判断。
Java版本的请求端,默认请求8080端口:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
/**
* 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语言版本的服务器程序,收到什么发送什么,没有任何逻辑,默认绑定8080端口:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
/**
* how to compile it:
* gcc pong_server.c -o pong_server /usr/local/lib/libev.a -lm
*/
#include
#include
#include
#include
#include
#include
#include
#include
#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
);
}
|
这里有一个现场图:
可以尝试稍微改变输出内容,去除换行符“\n”,把端口换成9000,试试看,就会发现Wireshark输出不同的结果来。
若使用原始TCP进行双向通信,则需要满足以下条件,可以被类似于Wireshark协议拦截器误认为是HTTP长连接:
因此,可以定性为微信使用了基于8080端口TCP长连接,一旦数据包中含有换行"\n"符号,就会被Wireshark误认为HTTP协议。可能微信是无心为之吧。
此时消息请求截图如下:
发送消息走已经建立的TCP长连接通道,发送消息到服务器,然后接受确认信息等,产生一次交互。
小伙伴接收到信息阅读也都会收到服务器端通知,产生一次交互等。
可以确定,微信发送消息走TCP长连接方式,因为不对自身状态数据产生影响,应该不交换SyncKey。
以上,根据有限资料和数据拦截观察总结得出,啰啰嗦嗦,勉强凑成一篇,会存在一些不正确之处,欢迎给予纠正。在多次
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=****//第三次查询,增量同步