这个是我最近写的一个记录调试信息的类,基本功能和以前写过的UdpTraceListener类似:通过UDP数据报文发送调试信息发送出去。为了方便调试,增加了颜色和一些简单的指令功能。感觉还比较方便,这里记录一下,以备后续使用。
static
class
Debug
{
static
UdpClient client;
static Debug()
{
client = new
UdpClient();
client.Connect(new
IPEndPoint(IPAddress.Loopback, 3001));
Clear();
}
public
static
void Write(ConsoleColor color, object obj)
{
Write(color, obj.ToString());
}
[MethodImpl(MethodImplOptions.Synchronized)]
public
static
void Write(ConsoleColor color, string message)
{
if (message == null)
return;
var dbgMsg = message;
if (dbgMsg.Length > 80)
dbgMsg = dbgMsg.Substring(0, 70) + "..." + Environment.NewLine;
Console.ForegroundColor = color;
Console.Write(dbgMsg);
SendCmd(0xff, (byte)color);
SendMsg(message);
}
public
static
void WriteLine(ConsoleColor color, object obj)
{
if (obj == null)
return;
WriteLine(color, obj.ToString());
}
public
static
void WriteLine(ConsoleColor color, string message)
{
Write(color, message + Environment.NewLine);
}
public
static
void Clear()
{
Console.Clear();
SendCmd(0xfe, 0);
}
static
void SendCmd(byte cmdType, byte cmdInfo)
{
client.Send(new
byte[] { cmdType, cmdInfo }, 2);
}
static
void SendMsg(string msg)
{
var data = Encoding.Default.GetBytes(msg);
client.Send(data, data.Length);
}
}
在前文中介绍过,MSN协议中服务器分为NS(NotificationServer)和SB(Switchboard)等几种,其中NS主要用来传输控制命令,而SB则用来传输会话,每个会话对应着一个SB。在一个MSN客户端中,是连接着一个NS和多个SB的。
创建会话
这里以两个客户端ClientA和ClientB和交谈为例,假定ClientA创建一个会话,并邀请ClientB参加会话,则它们之间的交互过程如下:
1. ClientA发送XFR命令创建SessionBoard,服务器返回SessionBoard地址
>>> XFR 15 SB\r\n
<<< XFR 15 SB 207.46.108.37:1863 CKI 17262740.1050826919.32308\r\n
服务器的XFR应答消息中包含三个参数,第一个参数为SB服务器的地址,第二个参数为认证类型,固定为CKI,第三个参数为后面服务器的认证Key值,后面会用到。
2. ClientA连接SessionBoard,并发送USR命令进行身份认证。
<o> Client Connects to 207.46.108.37 1863 (Switchboard)
>>> USR 1 example@passport.com
17262740.1050826919.32308\r\n
<<< USR 1 OK example@passport.com Example%20Name\r\n
<o> Continue SB Session . . .
USR认证时携带两个参数,第一个参数是当前用户,第二个参数即是前面的XFR应答消息中携带的Key值。
3. ClientA发送CAL命令邀请ClientB
>>> CAL 2 name_123@hotmail.com\r\n
<<< CAL 2 RINGING 11752013\r\n
PS:服务器的CAL应答是等成功邀请上ClientB后才回应的,如上图所示。但ClientA感知不到这个过程,故这里就放在一块儿了。
4. NS发送RNG命令通知ClientB参加会话,ClientB根据RNG命令中SessionBoard地址,并发送ANS命令进行身份认证。
<<< RNG 11752013
207.46.108.38:1863 CKI 849102291.520491113 example@passport.com Example%20Name\r\n
<o> Client Connects to 207.46.108.38 1863 (Switchboard)
>>> ANS 1 name_123@hotmail.com 849102291.520491113 11752013\r\n
<<< IRO 1 1 2 example@passport.com Mike\r\n
<<< IRO 1 2 2 myname@msn.com My%20Name\r\n
<<< ANS 1 OK\r\n
<o> Continue SB Session . . .
从这个协议中不难看出,MSN协议设计时就考虑了多方通信的支持的,只要重复3,4步即可,要实现多方通信主要这里就不累述了。
从常见会话过程中可以看出,建立一个会话时,客户端分为主动连接和被动连接两种,主动连接方主要完成1、2、3这三步,而被动连接方主要完成第4步。
退出会话
退出会话交互过程如下:
1. 主动退出方对SB发送OUT命令,SB则自动中断连接。
>>> OUT\r\n
<o> Switchboard Closes Connection
2. SB对会话中的其它成员发送BYE命令,通知其它成员会话有成员退出,如果只剩下一个成员,则和该成员的连接也会被中断。
<<< BYE example@passport.com 1\r\n
<o> Switchboard Closes Connection
另外,如果只有两个成员,且他们之间5分钟都没有活动,则服务器会对两个成员都发送BYE命令,此时的交互方式如下。
这时会话双方都是被动退出,交互过程和前面一样,就不累述了。
从前面的交互过程中可以看出,退出时也分为主动方和被动方:主动方需要发送OUT命令,被动方则需要响应BYE通知,还是比较简单的。
开始会话
MSN的对话都是在SB上进行的,并且对话信息都是通过MSG类型的消息发送的,它和我们打电话的过程非常类似。一个简单的对话过程如下:
>>> MSG 4 N 133\r\n
MIME-Version: 1.0\r\n
Content-Type: text/plain; charset=UTF-8\r\n
X-MMS-IM-Format: FN=Arial; EF=I; CO=0; CS=0; PF=22\r\n
\r\n
Hello! How are you?
<<< ACK 4
<<< MSG example@passport.com Mike 133\r\n
MIME-Version: 1.0\r\n
Content-Type: text/plain; charset=UTF-8\r\n
X-MMS-IM-Format: FN=Arial; EF=I; CO=0; CS=0; PF=22\r\n
\r\n
Hello! How are you?
从上可以看出,步骤3,4和步骤1,2是完全一样的,也就是说,MSN对话是不区分发送方和接收方的,所有交互都是通过SB转发,发送的消息都是单向的,接收消息都是靠通知而不是应答。这就意味着:发送方只能知道消息是否发送成功,并不能保证一定会应答,接收方收到消息通知时,并不要求要回应答。这一点和我们实际的交谈过程也是一样的。
除了类型为"text/plain"的对话内容通知消息外,另外一个比较常用的通知为那种常见的"xxx 正在输入…"的通知,这个消息的格式为:
MIME-Version: 1.0\r\n
Content-Type: text/x-msmsgscontrol\r\n
TypingUser: example@passport.com\r\n
\r\n
\r\n
如果要实现一个较为友好的客户端,需要响应和发送此消息。
由于我最初只打算实现个MSN机器人,除了这两个消息外,其它的消息(如闪屏震动,文件发送等)我都没有实现,它们的处理方式也没有太大难度,如果感兴趣可以参考我前面的参考文献地址自行实现一下,就不要在留言中问我如何实现了。
代码实现:
View Code
到此我的MSN系列文章基本完结了,由于时间和精力有限,并不打算继续深入研究和完善。相关代码我基本都在文中已经贴出来了,总共也就3百行左右,非常简单。我实现代码只是为了练习Async编程,因此只实现了一个雏形,并不完善,如果在项目中需要实现MSN协议建议使用MSNSharp等开源库,我并不打算放出完整的工程下载,文章写完后相关代码即刻删除,本地也不会保留,请勿留言索取。
如果文中关于MSN协议的说明有误,欢迎留言指正。但如果是使用本文代码中遇到了问题,请不要留言求助,我没有足够的精力来一一指正,望见谅。
前面的文章里已经介绍了如何实现用户登录,本文中主要解决目前遗留下的一个问题:看到远端好友状态和让远端好友看到你的状态。
一、联系人同步
要看到远端好友的状态,首先需要进行的一步就是联系人同步。联系人同步主要是通过SYN命令来进行的,SYN命令格式如下:
>>> SYN 1 0\r\n
<<< SYN 1 2 1\r\n
SYN命令只有一个参数,就是当前的同步序号,如果传0即可实现全同步,传大于0的数字时可以实现增量同步,当前同步到的最新序号可以从应答中获取。
服务器收到SYN命令后,然后就会把当前联系人的设置通过通知的方式一一发送过来,一个示例如下:
>>> SYN 1 0\r\n
<<< SYN 1 139 5 4\r\n
<<< GTC A\r\n
<<< BLP AL\r\n
<<< PRP PHH 01%20234\r\n
<<< PRP PHM 56%20789\r\n
<<< LSG 0 Other%20Contacts 0\r\n
<<< LSG 1 Coworkers 0\r\n
<<< LSG 2 Friends 0\r\n
<<< LSG 3 Family 0\r\n
<<< LST principal1@passport.com principal1 4\r\n
<<< LST principal2@passport.com principal2 10\r\n
<<< LST principal3@passport.com principal3 11 1,3\r\n
<<< LST principal4@passport.com principal4 11 0\r\n
<<< BPR PHH 01%20234\r\n
<<< BPR MOB Y\r\n
<<< LST principal5@passport.com principal5 12\r\n
<<< LST principal6@passport.com principal6 11 2\r\n
<<< BPR PHW 45%206789\r\n
由此可见,这里的通知还是比较多的,有GTC、BLP、LSG、LST等,这里就不一一介绍了,要详细了解的话可以看一下这个网页:http://www.hypothetic.org/docs/msn/index.php
由于通知消息是异步发送的,这里就存在一个不知道什么时候结束的问题。我这里的做法是:在发送SYN命令后,紧跟着发送一个Ping命令PNG,这样,可以简单的认为收到QNG时同步已经完成,此时交互如下:
>>> SYN 1 0\r\n
<<< SYN 1 139 5 4\r\n
>>> PNG/r/n
<<< GTC A\r\n
<<< BLP AL\r\n
<<< PRP PHH 01%20234\r\n
<<< PRP PHM 56%20789\r\n
<<< LSG 0 Other%20Contacts 0\r\n
<<< LSG 1 Coworkers 0\r\n
<<< LSG 2 Friends 0\r\n
<<< LSG 3 Family 0\r\n
<<< LST principal1@passport.com principal1 4\r\n
<<< LST principal2@passport.com principal2 10\r\n
<<< LST principal3@passport.com principal3 11 1,3\r\n
<<< LST principal4@passport.com principal4 11 0\r\n
<<< BPR PHH 01%20234\r\n
<<< BPR MOB Y\r\n
<<< LST principal5@passport.com principal5 12\r\n
<<< LST principal6@passport.com principal6 11 2\r\n
<<< BPR PHW 45%206789\r\n
<<< QNG/r/n
不过MSN协议并没有规定QNG一定会在SYN通知完成后才通知,这种做法并不一定可靠,一般情况下貌似也没有什么大问题。
前面已经介绍过,SYNC的应答通知有许多种类型,要完全实现还是比较麻烦的。另外,要实现完善的话,只靠这个通知有时还不够,往往还需要根据用户人的账户名称获取详细信息。由于时间有限,我只弄了一个同步所有人的简单实现。也没有对同步结果进行任何处理。请看本文的朋友不要进一步询问相关问题,大可自己参考前面提到的那个网页自行实现。
View Code
二.接收好友状态变化
同步完联系人后,所有的联系人默认都是离线状态。对于那些非离线状态的联系人,服务器会通过NLN,ILN,FLN命令来通知联系人状态的变化。一个状态通知的示例如下:
<<< ILN AWY example@passport.com example%20display%20name 268435492
它有四个参数,其中第一个参数表示当前的状态,目前MSN支持如下几种状态:
第二个参数是用户账号,第三个参数是昵称(不完整),第四个参数是ClientID。其内容具体参看http://www.hypothetic.org/docs/msn/notification/presence.php的Client Identification numbers一节,这里就不详细介绍了。
除了ILN外,
三. 设置我的状态
设置自己的状态主要通过CHG命令设置,示例如下:
>>> CHG 12 NLN 0\r\n
<<< CHG 12 NLN 0\r\n
CHG命令有两个参数,第一个参数是状态,第二个是ClientID。设置成功后,服务器会回一个CHG应答。如果过于频繁设置状态,则会收到 800错误 。
代码附上:
View Code
一、MSN中的Ping
在基于TCP的协议中,一般都会都会设计Ping命令来检测并踢掉僵死的连接,从而节省服务器资源。在成功登陆到服务器后,一般立即会收到到服务器发送的Ping命令CHL,如过不进行正确的应答的话,则会被服务器踢掉。为了后面的功能能正常运行,首先我们必须学习一下MSN的Ping命令。
MSN协议中的Ping命令有两种:服务器端发送的和客户端发送的。客户端发送的Ping命令非常简单:客户端发送PNG命令,服务器应答QNG,都不携带任何参数。
>>> PNG/r/n
<<< QNG/r/n
除了客户端Ping服务器外,服务器也会不定时(一般登录后就会收到一次)对客户端发送Ping命令CHL。CHL命令之间没有固定周期,它包含两个参数:第一个总会是0,第二个被称作"Challenge String",客户端会根据它来生成应答。CHL命令格式如下:
<<< CHL 0 15570131571988941333/r/n
接收Challenge命令后,必须在50秒内发送应答命令QRY到服务器,否则将被断开。应答命令QRY格式如下:
<<< CHL 0 15570131571988941333/r/n
>>> QRY 1049 msmsgs@msnmsgr.com 32/r/n
8f2f5a91b72102cd28355e9fc9000d6e (no newline)
QRY命令主要有三个参数,第一个参数是客户端ID,第二个参数是32,第三个参数是客户端应答字符串。其实前面介绍过:QRY是一个有效载荷命令,它的第二个参数是后面的内容(应答字符串),因为应答字符串长度是32,因此第二个参数也就固定为32。因此需要我们填充的是第一个参数客户端ID和第三个参数应答字符串。下面就介绍一下这两个参数的生成方式。
首先看一张表:
客户端ID | 客户端标识码 |
msmsgs@msnmsgr.com | Q1P7W2E4J9R8U3S5 |
PROD0038W!61ZTF9 | VT6PX?UQTM4WM%YR |
PROD0058#7IL2{QD | QHDCY@7R1TB6W?5B |
PROD0061VRRZH@4F | JXQ6J@TUOGYV@N0M |
其中客户端ID可以取上表中的一个(貌似也不是任取一个就可以的),而应答字符串则是CHL命令的Challenge String参数和客户端标识码的MD5值(这个也是为什么长度总是32的原因)。
例如,对于前面的CHL命令,我们取客户端字符串为msmsgs@msnmsgr.com,对应的客户端标识码为Q1P7W2E4J9R8U3S5,则对应的应答码的为:MD5(15570131571988941333 + Q1P7W2E4J9R8U3S5) = 8f2f5a91b72102cd28355e9fc9000d6e
有了上面的基础后,用C#实现一个客户端的主动Ping命令和对服务器的ping命令自动应答就十分简单了,代码如下:
View Code
二、退出登录
介绍完Ping后,顺带介绍一下上章中忘记介绍的退出登录。
退出登录最简单的方法就是正常关闭socket连接,这种方式也是非常简单有效的。但还有一种方式是对服务器发送OUT指令,服务器受到OUT指令后会主动关闭socket连接。
<<< OUT/r/n
<o> Server closes connection
除了上面介绍的客户端主动退出登录外,服务器有时也会发送OUT指令强制客户端退出登录。主要有如下两种情况:
1. 当服务器要进行维护时,将会发送OUT SSD — server shutting down,发送后就立即关闭连接。
<<< OUT SSD/r/n
<o> Server closes connection
2. 当有人在别处用你的账号登录时,原来的连接将被关闭,新连接将被建立。关闭连接前,NS将给客户端发送OUT命令并携带参数OTH
<<< OUT OTH/r/n
<o> Server closes connection
最后附带一下代码的实现。
View Code
PS:本来打算在这篇文章中介绍一下如何实现设置和获取联系人的在线状态的,但由于如果不能正确实现对服务器的Ping应答的话,一般刚登上去就会被踢掉的,故先介绍一下。设置和获取联系人的功能待下篇文章中再介绍。