一、前提
- 源码是smack4.1.8
- 具体可以看下我之前分装支持单聊的代码 https://github.com/YuanClouds/SnakeXMPP
二、连接机制分析
首先 要说下XMPPTCPConnectionConfiguration,XMPPTCPConnection继承于ConnectionConfiguration,主要与父类区别在于XMPPTCPConnection提供了超时连接的处理(默认30秒),由构建模式设计,用于提供实例化XMPPTCPConnection时候所需要的一些字段,例如服务器地址、端口超时时间等。一般实例化如下:
XMPPTCPConnectionConfiguration connectionConfiguration = XMPPTCPConnectionConfiguration.builder()
.setHost(this.server)
.setPort(this.port)
.setServiceName(this.server)
.setSendPresence(true)// support presence
.setUsernameAndPassword("账号","密码")
.setConnectTimeout(1000 * 10)
// .setUsernameAndPassword(this.login,this.password)
.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)//越过证书
.build();
mConnection = new XMPPTCPConnection(connectionConfiguration);
smack底层由TCP长连接进行支持,通过创建socket套子连接后并创建读写数据流,利用xmpp协议就行通信与维护了整个TCP的长连接从而实现基本的IM通信机制。
(一)socket的创建
- 调用抽象类Abstractxmppconnection的抽象方法connect,具体实现由XmppTcpConnection中的方法connectInternal执行,具体代码如下
具体实现
@Override
protected void connectInternal() throws SmackException, IOException, XMPPException {
// Establishes the TCP connection to the server and does setup the reader and writer. Throws an exception if
// there is an error establishing the connection
connectUsingConfiguration();
// We connected successfully to the servers TCP port
socketClosed = false;
initConnection();
// ...
}
2、调用connectUsingConfiguration方法,通过从SocketFactory 抽象套接字工厂中创建socket(这里会涉及到执行DNS,查找对应的主机和端口,不详细追踪),如果在创建过程中没有异常跑出,说明已经建立了TCP握手
具体实现
// 这里源码不贴了..
packet分析
// socket创建成功后,向服务器发送stream包,高数服务器开通读写流
SENT (66458771): <stream:stream to="您的服务器地址" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
// 服务器响应后,会发送服务器支持的sasl机制有哪些,例如digest-md5、cram-md5、plain等等(后面登陆会说明)
RCV (66458771): <?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="cluster.openfire" id="2mm7ln0qqc" xml:lang="en" version="1.0">
RCV (66458771): <stream:features><starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"></starttls><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>SCRAM-SHA-1</mechanism><mechanism>CRAM-MD5</mechanism><mechanism>DIGEST-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><auth xmlns="http://jabber.org/features/iq-auth"/><register xmlns="http://jabber.org/features/iq-register"/></stream:features>
// 向服务器发起TLS握手
SENT (66458771): <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
// 服务器响应成功,握手成功后TLS保证传输通道安全
RCV (66458771): <proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
(二)读写数据流的创建
如果连接过程中没有异常Throws,则进行执行connectInternal中的initConnection-> initReaderAndWriter,初始化读写数据流OutputStreamWriter与BufferedReader,并且将两个流绑定到业务处理者packetReader、packetwriter,两者其实都是各自开启了一个可控制的死循环守护进程进行读写监听,如下:
Async.go(new Runnable() {
@Override
public void run() {
writePackets();
}
}, "Smack Packet Writer (" + getConnectionCounter() + ")");
Async.go(new Runnable() {
public void run() {
parsePackets();
}
}, "Smack Packet Reader (" + getConnectionCounter() + ")");
}
其中,在初始化packetwriter开启守护进程的时候,会先进行openStream,即通过send一个stream节点的packet StreamOpen(继承于FullStreamElement)告诉服务器开通读写流。与此同时,在初始化packetReader,将socketReader包装成XmlPullParser后赋值给packetReader的解析器parser,真正做解析的是
packetReader里面的parser(parsePackets 方法),这里是整个packet分析的重点
三 、登陆机制分析
首先写到登陆,刚刚从登陆中了解到服务器会返回sasl认证机制支持什么机制,这里首先要了解下sasl,举例其中的digest-md5机制
使用这种机制时,client与server共享同一个隐性密码,而且此密码不通过网络传输。验证过程是从服务器先提出challenge(质询)开始, 客户端使用此challenge与隐性密码计算出一个response(应答)。不同的challenge,不可能计算出相同的response;任何拥 有secret password的一方,都可以用相同的challenge算出相同的response。因此,服务器只要比较客户端返回的response是否与自己算 出的response相同,就可以知道客户端所拥有的密码是否正确。由于真正的密码并没有通过网络,所以不怕网络监测(参考)
(一)流程执行
image.png(二) 原理分析
- 调用抽象类Abstractxmppconnection的抽象方法login,具体实现由XmppTcpConnection中的方法loginNonAnonymously执行,具体嗲吗如下
@Override
protected synchronized void loginNonAnonymously(String username, String password, String resource) throws XMPPException, SmackException, IOException {
if (saslAuthentication.hasNonAnonymousAuthentication()) {
// Authenticate using SASL
if (password != null) {
saslAuthentication.authenticate(username, password, resource);
}
else {
saslAuthentication.authenticate(resource, config.getCallbackHandler());
}
} else {
throw new SmackException("No non-anonymous SASL authentication mechanism available");
}
//...
}
- 委托SASLMechanism进行sasl认定安全层,具体实现由authenticate方法进行,其实实际是向服务器发起节点为Auth的packet包
具体实现与packet分析如下
具体实现
private final void authenticate() throws SmackException, NotConnectedException {
byte[] authenticationBytes = getAuthenticationText();
String authenticationText;
//...
// 这里发送auth packet
connection.send(new AuthMechanism(getName(), authenticationText));
}
packet 分析
// 告诉服务器准备进行鉴定,并且鉴定机制为digest-mdg机制
SENT (126159128): <auth mechanism="DIGEST-MD5" xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></auth>
// 服务器响应,接收到一个challenge 质询包
RCV (126159128): <challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09ImNsdXN0ZXIub3BlbmZpcmUiLG5vbmNlPSJDcUg4K2FrVDBod2pGNThJRXEyYld1NCs1Y3BMaGdsVG1VdWw5bzFHIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz</challenge>
3、接收到咨询包后,在packetReader进行解析,部分代码如下
具体实现
case Challenge.ELEMENT:
// The server is challenging the SASL authentication made by the client
String challengeData = parser.nextText();
getSASLAuthentication().challengeReceived(challengeData);
break;
challengeReceived 方法最终委托saslmechanism执行自己的challengeReceived方法,实际作用是将返回的challenge 与隐形密码进行Base64加密,之后再将结果发送一个reponse包给后台服务器,具体代码如下
public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackException, NotConnectedException {
byte[] challenge = Base64.decode(challengeString);
byte[] response = evaluateChallenge(challenge); // 计算
if (finalChallenge) {
return;
}
Response responseStanza;
if (response == null) {
responseStanza = new Response();
}
else {
responseStanza = new Response(Base64.encodeToString(response));
}
// 响应一个reponse packet
connection.send(responseStanza);
}
packet分析
SENT <response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">dXNlcm5hbWU9IjU4NTg3Mzg4IixyZWFsbT0iY2x1c3Rlci5vcGVuZmlyZSIsY25vbmNlPSJhMTViMTNlYzIxYTExYjI3NDk4YTU3NDY4ZmUzNjI3ZWM0YmRkMWI1YjIxNTc4Njc2YTkwMWExYzY2Y2JiY2ZlIixuYz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2NsdXN0ZXIub3BlbmZpcmUiLHJlc3BvbnNlPTU3NWE2M2UwZjdlMGJmYTM4ZTZkMWU0MjMyMTNlZWQ2LGNoYXJzZXQ9dXRmLTgsbm9uY2U9IkNxSDgrYWtUMGh3akY1OElFcTJiV3U0KzVjcExoZ2xUbVV1bDlvMUci</response>
// 如果计算结果与服务器结果一致 则鉴定成功
RCV (126159128): <success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cnNwYXV0aD05ODQ4NjJiOTkzNzYxOWRlMjhmMGEyZTI4ZjUyYzQzYw==</success>
4、鉴定成功处理,接收到成功包后,在packetReader进行解析
具体实现
case Success.ELEMENT:
Success success = new Success(parser.nextText());
// We now need to bind a resource for the connection
// Open a new stream and wait for the response
openStream();
// The SASL authentication with the server was successful. The next step
// will be to bind the resource
getSASLAuthentication().authenticated(success);
break;
接收处理中做了三件事情
- 1、重新打开一个新的数据读写流
- 2、绑定用户的资源id
- 3、重新设置了用户的session
其中,在前面开通的读写流,只是为了获取服务器支持的sasl有什么机制,如果鉴定成功后,在该session中打开的读写流,将拥有不同的读写权限。而且xmpp后台会会在success节点中附加一个秘钥(暂时不知道什么用),开通成功后会再次更新session、stream通道等。最后再并通过IQ包,进行资源id绑定,成功响应后完成登陆
packet分析
// 重新打开开通读写流
SENT (126159128): <stream:stream to="cluster.openfire" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
RCV (126159128): <?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="cluster.openfire" id="7o71vk3oho" xml:lang="en" version="1.0"><stream:features><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"><optional/></session><sm xmlns='urn:xmpp:sm:2'/><sm xmlns='urn:xmpp:sm:3'/></stream:features>
// 绑定资源Id
SENT (126159128): <iq id="EIp7Y-0" type="set"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>lxj_u</resource></bind></iq>
RCV (126159128): <iq type="result" id="EIp7Y-0" to="cluster.openfire/7o71vk3oho"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>58587388@cluster.openfire/lxj_u</jid></bind></iq>
// 更新session
SENT (126159128): <iq id="EIp7Y-1" type="set"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>
RCV (126159128): <iq type="result" id="EIp7Y-1" to="58587388@cluster.openfire/lxj_u"/>
// 登陆成功
User logged (126159128): 58587388@cluster.openfire@cluster.openfire:5222/resourceid
四、最后分享下连接与登陆的packet包交互
// 连接
SENT (66458771): <stream:stream to="test.siven.com" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
RCV (66458771): <?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="cluster.openfire" id="2mm7ln0qqc" xml:lang="en" version="1.0">
RCV (66458771): <stream:features><starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"></starttls><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>SCRAM-SHA-1</mechanism><mechanism>CRAM-MD5</mechanism><mechanism>DIGEST-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><auth xmlns="http://jabber.org/features/iq-auth"/><register xmlns="http://jabber.org/features/iq-register"/></stream:features>
SENT (66458771): <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
RCV (66458771): <proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
SENT (66458771): <stream:stream to="cluster.openfire" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
RCV (66458771): <?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="cluster.openfire" id="2mm7ln0qqc" xml:lang="en" version="1.0"><stream:features><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>SCRAM-SHA-1</mechanism><mechanism>CRAM-MD5</mechanism><mechanism>DIGEST-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><auth xmlns="http://jabber.org/features/iq-auth"/><register xmlns="[http://jabber.org/features/iq-register"/></stream:features>](http://jabber.org/features/iq-register"/></stream:features>)
// 登陆
SENT (126159128): <auth mechanism="DIGEST-MD5" xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></auth>
RCV (126159128): <challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09ImNsdXN0ZXIub3BlbmZpcmUiLG5vbmNlPSJDcUg4K2FrVDBod2pGNThJRXEyYld1NCs1Y3BMaGdsVG1VdWw5bzFHIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz</challenge>
SENT (126159128): <response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">dXNlcm5hbWU9IjU4NTg3Mzg4IixyZWFsbT0iY2x1c3Rlci5vcGVuZmlyZSIsY25vbmNlPSJhMTViMTNlYzIxYTExYjI3NDk4YTU3NDY4ZmUzNjI3ZWM0YmRkMWI1YjIxNTc4Njc2YTkwMWExYzY2Y2JiY2ZlIixuYz0wMDAwMDAwMSxxb3A9YXV0aCxkaWdlc3QtdXJpPSJ4bXBwL2NsdXN0ZXIub3BlbmZpcmUiLHJlc3BvbnNlPTU3NWE2M2UwZjdlMGJmYTM4ZTZkMWU0MjMyMTNlZWQ2LGNoYXJzZXQ9dXRmLTgsbm9uY2U9IkNxSDgrYWtUMGh3akY1OElFcTJiV3U0KzVjcExoZ2xUbVV1bDlvMUci</response>
RCV (126159128): <success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cnNwYXV0aD05ODQ4NjJiOTkzNzYxOWRlMjhmMGEyZTI4ZjUyYzQzYw==</success>
SENT (126159128): <stream:stream to="cluster.openfire" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
RCV (126159128): <?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="cluster.openfire" id="7o71vk3oho" xml:lang="en" version="1.0"><stream:features><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"><optional/></session><sm xmlns='urn:xmpp:sm:2'/><sm xmlns='urn:xmpp:sm:3'/></stream:features>
SENT (126159128): <iq id="EIp7Y-0" type="set"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>resource</resource></bind></iq>
RCV (126159128): <iq type="result" id="EIp7Y-0" to="cluster.openfire/7o71vk3oho"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>58587388@cluster.openfire/lxj_u</jid></bind></iq>
SENT (126159128): <iq id="EIp7Y-1" type="set"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>
RCV (126159128): <iq type="result" id="EIp7Y-1" to="58587388@cluster.openfire/lxj_u"/>
User logged (126159128): 58587388@cluster.openfire@cluster.openfire:5222/resource
网友评论