和飞秋的通讯实现

1,111 阅读9分钟

飞秋是什么?

飞秋(FeiQ)是一款局域网聊天传送文件的绿色软件,它参考了飞鸽传书(IPMSG)和QQ,
完全兼容飞鸽传书(IPMSG)协议,具有局域网传送方便,速度快,操作简单的优点,同
时具有QQ中的一些功能。

功能说明:
- 飞秋(FeiQ)是一款局域网内即时通信软件, 基于 TCP/IP(UDP).
- 完全兼容网上广为流传的飞鸽传书并比原来飞鸽功能更加强大.
- 不需要服务器支持.
- 支持文件/文件夹的传送 (支持大文件传送[4G以上]), 发送方和接收方

先给飞秋做下广告。以上的说明简介转自华军软件园:www.newhua.com/soft/66046.…

下面转入正题,我们公司同时之间习惯使用飞秋来进行通讯,同时也用QQ,他们各有各的好处的特点,飞秋的好处就是,不需要注册帐号,简单方便。但是飞秋在windows2003系统下经常会卡死,不知道是公司网络问题,还是其他什么问题。后来也考虑使用其他的即时im来取代它,尝试过,后来放弃了,毕竟习惯难以改变。既然如此,我们就继续用吧,不过我们想为飞秋增加一些功能,或者想使用飞秋做提醒,那么原来的飞秋就不那么方便了。于是我考虑写个客户端和它来通讯。

 

飞秋支持ipmsg协议,那就好办了。我们只需要遵守协议和消息机制,就能和其他飞秋客户端互发消息了。

ipmsg协议的内容,参考文档www.cnblogs.com/hnrainll/ar…,同时对译者感谢。飞秋的消息发送使用的是udp,消息的格式为:

1:100:shirouzu:Jupiter:32:Hello,以冒号作为分割,分为6短。各段的意思为:

版本号:消息编号:发送人姓名:发送人机器名:命令字:附加内容。

 

那么我依靠这些就能互发消息了。然后还要知道“命令字”的内容。这些命令字的常量定义,我没有找到官方文档。不过协议里提到的指令(命令字)对应的数字(int)是多少?却没有找到,而且到处找不到。后来在一篇文章里见到作者写的代码里有包含一些代码中提到了这些常量的定义,就拿来用了。该文章的地址找不到了,对作者表示歉意。

 

/*  ----------------------------------------------------------
* @名称    :    
* @描述    :    
* @创建人  :    张云飞
* @创建日期:    2011/7/11 13:27:24
* @修改记录:
* ---------------------------------------------------------- */

using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Text;

namespace  FeiQAPI
{


public   enum  IPMSG_Header
{


/*  header  */
IPMSG_VERSION  =   0x0001 ,
IPMSG_DEFAULT_PORT  =   0x0979 ,
}

public   enum  IPMSG_FileattachCommand
{


/*  file types for fileattach command  */
IPMSG_FILE_REGULAR  =   0x00000001 ,
IPMSG_FILE_DIR  =   0x00000002 ,
IPMSG_LISTGET_TIMER  =   0x0104 ,
IPMSG_LISTGETRETRY_TIMER  =   0x0105
}

public   enum  IPMSG_COMMAND :  int
{


/*  command  */
IPMSG_NOOPERATION  =   0x00000000 ,

IPMSG_BR_ENTRY  =   0x00000001 ,
IPMSG_BR_EXIT  =   0x00000002 ,
IPMSG_ANSENTRY  =   0x00000003 ,
IPMSG_BR_ABSENCE  =   0x00000004 ,

IPMSG_BR_ISGETLIST  =   0x00000010 ,
IPMSG_OKGETLIST  =   0x00000011 ,
IPMSG_GETLIST  =   0x00000012 ,
IPMSG_ANSLIST  =   0x00000013 ,
IPMSG_FILE_MTIME  =   0x00000014 ,
IPMSG_FILE_CREATETIME  =   0x00000016 ,
IPMSG_BR_ISGETLIST2  =   0x00000018 ,

IPMSG_SENDMSG  =   0x00000020 ,
IPMSG_RECVMSG  =   0x00000021 ,
IPMSG_READMSG  =   0x00000030 ,
IPMSG_DELMSG  =   0x00000031 ,

/*  option for all command  */
IPMSG_ABSENCEOPT  =   0x00000100 ,
IPMSG_SERVEROPT  =   0x00000200 ,
IPMSG_DIALUPOPT  =   0x00010000 ,
IPMSG_FILEATTACHOPT  =   0x00200000 ,
}
}

 

有了命令字,然后就是消息对象的MODEL了。我们构建一个消息类的实体,以便在使用中传递消息对象。

 

/*  ----------------------------------------------------------
* @名称    :    
* @描述    :    
* @创建人  :    张云飞
* @创建日期:    2011/7/11 13:35:37
* @修改记录:
* ---------------------------------------------------------- */

using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Text;

namespace  FeiQAPI
{


///   <summary>
///  版本号(1):包编号:发送者姓名:发送者主机名:命令字:附加信息
///   </summary>
public   class  IPMSG
{


string  _Version;

///   <summary>
///  版本号(1)
///   </summary>
public   string  Version
{


get  {  return  _Version; }
set  { _Version  =  value; }
}

string  _PackageID;

///   <summary>
///  包编号
///   </summary>
public   string  PackageID
{


get  {  return  _PackageID; }
set  { _PackageID  =  value; }
}

string  _SenderName;

///   <summary>
///  发送者姓名
///   </summary>
public   string  SenderName
{


get  {  return  _SenderName; }
set  { _SenderName  =  value; }
}

string  _SenderHostName;

///   <summary>
///  发送者主机名
///   </summary>
public   string  SenderHostName
{


get  {  return  _SenderHostName; }
set  { _SenderHostName  =  value; }
}
IPMSG_COMMAND _Msg_Command;

///   <summary>
///  命令字
///   </summary>
public  IPMSG_COMMAND Msg_Command
{


get  {  return  _Msg_Command; }
set  { _Msg_Command  =  value; }
}

string  _Option_Data;

///   <summary>
///  附加信息
///   </summary>
public   string  Option_Data
{


get  {  return  _Option_Data; }
set  { _Option_Data  =  value; }
}

///   <summary>
///  转换
///   </summary>
public   static  IPMSG Parse(Byte[] bytes,  int  index,  int  count)
{


return  Parse(Encoding.Default.GetString(bytes,index,count));
}

///   <summary>
///  转换
///   </summary>
///   <param name="msgStr"></param>
///   <returns></returns>
public   static  IPMSG Parse( string  msgStr)
{


if  ( string .IsNullOrEmpty(msgStr))
{


throw   new  ArgumentNullException();
}

///  版本号(1):包编号:发送者姓名:发送者主机名:命令字:附加信息
string [] strArr  =  msgStr.Split( new   char [] {  ' : '  }, StringSplitOptions.None);
Console.WriteLine( " Receive:{0} ,Split array length={1} " , msgStr, strArr.Length);
if  (strArr.Length  ==   6 )
{


IPMSG msg  =   new  IPMSG();
msg.Version  =  strArr[ 0 ];
msg.PackageID  =  strArr[ 1 ];
msg.SenderName  =  strArr[ 2 ];
msg.SenderHostName  =  strArr[ 3 ];
msg.Msg_Command  =  (IPMSG_COMMAND)Enum.Parse( typeof (IPMSG_COMMAND), strArr[ 4 ]);
msg.Option_Data  =  strArr[ 5 ];
return  msg;
}
return   null ;

}

///   <summary>
///  转化到消息
///   </summary>
///   <returns></returns>
public   string  ToMsgStr()
{


return   string .Format( " {0}:{1}:{2}:{3}:{4}:{5} " ,
Version  ??   " 1 " ,
PackageID  ??  DateTime.Now.Ticks.ToString(),
SenderName  ??   "" ,
SenderHostName  ??   "" ,
(( int )Msg_Command).ToString(),
Option_Data  ??   "" );
}

public   byte [] ToBytes()
{


string  str  =  ToMsgStr();
Console.WriteLine( " send Msg:  "   +  str);
return  Encoding.Default.GetBytes(str);
}
}
}

 

上面的实体基本 包含了6个属性,对应协议里的6个部分。同时写了 转换的方法

ToBytes,ToMsgStr,Parse等。实现了消息对象和 字节 之间的转换,便于传输。

 

好了,现在我们构建Socket对象。

 

               _socket  =   new  Socket(AddressFamily.InterNetwork, SocketType.Dgram,
ProtocolType.Udp);

IPEndPoint ip  =   new  IPEndPoint(IPAddress.Any, _Port);
_socket.Bind(ip);

 

标准的构建socket的代码。端口我们指定为:         int _Port = 2425;

 

现在尝试发送消息,如下代码所示Dns.GetHostName是获得本级机器名的方法。 在下面的方法里,构建里一个IPMSG 对象,并对该对象的各个部分赋值,然后调用socket.SendTo方法,将数据包发送出去。

        public   void  SendMsg( string  targetIP,  string  msgText)
{


byte [] byts  =   null ;
IPMSG msg  =   new  IPMSG();
msg.SenderName  =  MyName;
msg.SenderHostName  =  Dns.GetHostName();
msg.Msg_Command  =  IPMSG_COMMAND.IPMSG_SENDMSG; // IPMSG_SENDMSG
msg.Option_Data  =  msgText;
byts  =  msg.ToBytes();

EndPoint ep  =  (EndPoint) new  IPEndPoint(IPAddress.Parse(targetIP), _Port);
_socket.SendTo(byts, ep);
}

 

那么如何接收消息呢?

 

                 byte [] buf  =   new   byte [ 1024 ];
while  (_Running)
{


// Socket socketClient = socket.Accept();
EndPoint remote  =  (EndPoint)( new  IPEndPoint(IPAddress.Any,  0 ));

int  len  =  _socket.ReceiveFrom(buf,  ref  remote);
if  (len  >   0 )
{


string  msg  =  Encoding.Default.GetString(buf,  0 , len);
Console.WriteLine(msg);
IPMSG msg1  =  IPMSG.Parse(buf,  0 , len);
Action < IPMSG >  act  =   new  Action < IPMSG > (ProcessMessage);
if  (msg  !=   null )
{


act.BeginInvoke(msg1,  null ,  null );
}
// Println(msg + Environment.NewLine);

len  =   0 ;
}
}

 

调用socket.ReceiveFrom方法接受到网络发来的数据包,使用我们写好的解析法方法,我们的解析方法遵守了ipmsg协议。

                     string msg = Encoding.Default.GetString(buf, 0, len);
IPMSG msg1 = IPMSG.Parse(buf, 0, len);
这样我们就拿到一个IPMSG对象了,该消息对象包含了其他客户机发来的消息内容,这些内容可能是,文本消息,上线消息,回复消息,文件消息等。根据消息里的 指令类型 我们需要对接受到的消息做不同的处理。

注意上面的代码中:

                       Action<IPMSG> act = new Action<IPMSG>(ProcessMessage);
if (msg != null)
{
act.BeginInvoke(msg1, null, null);
}
我是使用了一个异步委托。代码执行到这里的时候不至于阻塞。

 

我们接收到了消息后,根据消息的内容做不同的处理,下面的处理消息的代码:

         private   void  ProcessMessage(IPMSG msg)
{


Console.WriteLine(msg.Msg_Command);

IPMSG_COMMAND mode  =  msg.Msg_Command;

if  ((IPMSG_COMMAND.IPMSG_ANSENTRY  &  mode)  ==  IPMSG_COMMAND.IPMSG_ANSENTRY)
{


// 让用户上线
ProcessUseOnLine(msg);
return ;
}
if  ((IPMSG_COMMAND.IPMSG_BR_ENTRY  &  mode)  ==  IPMSG_COMMAND.IPMSG_BR_ENTRY)
{


// 让用户上线
// ReceiveMsg(msg);
return ;
}
if  ((IPMSG_COMMAND.IPMSG_SENDMSG  &  mode)  ==  IPMSG_COMMAND.IPMSG_SENDMSG)
{


// 让用户上线
ReceiveMsg(msg);
return ;
}
// if ((IPMSG_COMMAND.IPMSG_RECVMSG & mode) == IPMSG_COMMAND_SENDCHECKOPT)
// {


//      // 让用户上线
//     ReceiveMsg(msg);
// }
}

 

我们是如何通知 我们上线了呢? 向局域网内的其他用户发送广播消息。收到这些消息的客户端就会将我们加入到他们的好友列表,同时各个客户端会回复一个消息给我们,籍此,我们就知道了网络上那些客户端,我们把这些客户端加入到我们的好友列表中。

 

        //发送广播,通知我上线了

        public   void  SendMyOnline()
{


byte [] byts  =   null ;
IPMSG msg  =   new  IPMSG();
msg.SenderName  =  MyName;
msg.SenderHostName  =  Dns.GetHostName();
msg.Msg_Command  =  IPMSG_COMMAND.IPMSG_BR_ENTRY;
msg.Option_Data  =   string .Format( " {0}\0{1}\0{1}{2} " , MyName, MyGroup,  "" );
byts  =  msg.ToBytes();

EndPoint ep  =  (EndPoint) new  IPEndPoint(IPAddress.Parse( " 192.168.3.255 " ), _Port);
_socket.SendTo(byts, ep);
}

然后要处理收到的 其他客户机 给我们的回复,该回复了包含了 其他各个客户机的位置信息

 

             if  ((IPMSG_COMMAND.IPMSG_ANSENTRY  &  mode)  ==  IPMSG_COMMAND.IPMSG_ANSENTRY)
{


// 让用户上线
ProcessUseOnLine(msg);
return ;
}

 

        //处理其他人 的上线消息,就是把这些消息内包含的用户地址等,加入到我们的好友列表中去。

        public   void  ProcessUseOnLine(IPMSG msg)
{


string  optionData  =  msg.Option_Data;
string [] arr  =  optionData.Split( new   char [ ' / ' ], StringSplitOptions.None);
string  displayName  =   "" ;
string  group  =   "" ;
if  (arr  !=   null   &&  arr.Length  >   0 )
{


if  (arr.Length  >   0 )
{


displayName  =  arr[ 0 ];
}
if  (arr.Length  >   1 )
{


group  =  arr[ 1 ];

}
}

_UserList.AddUser(msg.SenderName, msg.SenderHostName, displayName, group);
}

 

下面是我的好友类,和好友列表类:

 

/*  ----------------------------------------------------------
* @名称    :    
* @描述    :    
* @创建人  :    张云飞
* @创建日期:    2011/7/11 14:27:48
* @修改记录:
* ---------------------------------------------------------- */

using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Text;

namespace  FeiQAPI
{


///   <summary>
///  用户
///   </summary>
public   struct  User
{


public   string  UserName;    // 用户名
public   string  HostName;   // 机器名

// public string id;                // 节点ID。
public   string  HostIp;      // 存储IP信息,避免重复添加

public   string  Displayname;
public   string  Group;

// public object Inet;   // 存储网络信息

}
}

/*  ----------------------------------------------------------
* @名称    :    
* @描述    :    
* @创建人  :    张云飞
* @创建日期:    2011/7/11 14:35:45
* @修改记录:
* ---------------------------------------------------------- */

using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Text;
using  System.Net;
using  System.Net.Sockets;

namespace  FeiQAPI
{


///   <summary>
///  用户列表
///   </summary>
public   class  UserList
{


Dictionary < string , User >  _lstUsers  =   new  Dictionary < string , User > ();
public   event  EventHandler < UserEventArgs >  OnUserAdded;


public   void  AddUser( string  userName,  string  hostName,  string  displayName,  string  group)
{


string  ip  =   null ;
try
{


if  (Dns.GetHostEntry(hostName)  !=   null   &&  Dns.GetHostEntry(hostName).AddressList  !=   null   &&  Dns.GetHostEntry(hostName).AddressList.Length  >   0 )
{


ip  =  Dns.GetHostEntry(hostName).AddressList.Last().ToString();
User user  =   new  User();
user.UserName  =  userName;
user.HostName  =  hostName;
user.HostIp  =  ip;
user.Displayname  =  displayName;
user.Group  =  group;

AddUser(user);
}
}
catch  (SocketException)
{



}
catch  (Exception)
{



throw ;
}

}

public   void  AddUser(User user)
{


if  ( ! _lstUsers.ContainsKey(user.HostIp))
{


_lstUsers.Add(user.HostIp, user);
if  (OnUserAdded  !=   null )
{


OnUserAdded( this ,  new  UserEventArgs(user));
}
}
}

///   <summary>
///  获得用户
///   </summary>
///   <param name="senderName"></param>
///   <param name="hostName"></param>
///   <returns></returns>
public  User ?  GetUser( string  senderName,  string  hostName)
{


return  _lstUsers.Values.SingleOrDefault(p  =>  p.UserName  ==  senderName  &&  p.HostName  ==  hostName);
}
}
}

 

最后贴出我的完整的处理 消息 的服务器类:

 

/*  ----------------------------------------------------------
* @名称    :    
* @描述    :    
* @创建人  :    张云飞
* @创建日期:    2011/7/11 14:01:32
* @修改记录:
* ---------------------------------------------------------- */

using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Text;
using  System.Net;
using  System.Net.Sockets;
using  System.Threading;

namespace  FeiQAPI
{


///   <summary>
///  消息服务器
///   </summary>
public   class  MsgServer
{


///   <summary>
///  当接受的消息时
///   </summary>
public   event  EventHandler < IpMsg_EventArgs >  OnReceiveMsg;

int  _Port  =   2425 ;
bool  _Running  =   false ;

private   string  _MyName;
private   string  _MyGroup;

///   <summary>
///  分组
///   </summary>
public   string  MyGroup
{


get  {  return  _MyGroup; }
set  { _MyGroup  =  value; }
}

public   string  MyName
{


get  {  return  _MyName; }
set  { _MyName  =  value; }
}

public   bool  Running
{


get  {  return  _Running; }
}

Socket _socket;

UserList _UserList  =   new  UserList();

///   <summary>
///  用户列表
///   </summary>
public  UserList UserList
{


get  {  return  _UserList; }
}

// public void Send
public   void  ProcessUseOnLine(IPMSG msg)
{


string  optionData  =  msg.Option_Data;
string [] arr  =  optionData.Split( new   char [ ' / ' ], StringSplitOptions.None);
string  displayName  =   "" ;
string  group  =   "" ;
if  (arr  !=   null   &&  arr.Length  >   0 )
{


if  (arr.Length  >   0 )
{


displayName  =  arr[ 0 ];
}
if  (arr.Length  >   1 )
{


group  =  arr[ 1 ];

}
}

_UserList.AddUser(msg.SenderName, msg.SenderHostName, displayName, group);
}

public   void  Start()
{


ThreadPool.QueueUserWorkItem( new  WaitCallback( delegate
{


Run();

}));
}

private   void  Run()
{


if  (_Running  ==   false )
{


_socket  =   new  Socket(AddressFamily.InterNetwork, SocketType.Dgram,
ProtocolType.Udp);

IPEndPoint ip  =   new  IPEndPoint(IPAddress.Any, _Port);
_socket.Bind(ip);
// socket.Listen(10);

_Running  =   true ;
SendMyOnline();

byte [] buf  =   new   byte [ 1024 ];
while  (_Running)
{


// Socket socketClient = socket.Accept();
EndPoint remote  =  (EndPoint)( new  IPEndPoint(IPAddress.Any,  0 ));

int  len  =  _socket.ReceiveFrom(buf,  ref  remote);
if  (len  >   0 )
{


string  msg  =  Encoding.Default.GetString(buf,  0 , len);
Console.WriteLine(msg);
IPMSG msg1  =  IPMSG.Parse(buf,  0 , len);
Action < IPMSG >  act  =   new  Action < IPMSG > (ProcessMessage);
if  (msg  !=   null )
{


act.BeginInvoke(msg1,  null ,  null );
}
// Println(msg + Environment.NewLine);

len  =   0 ;
}
}
}
}

private   void  ProcessMessage(IPMSG msg)
{


Console.WriteLine(msg.Msg_Command);

IPMSG_COMMAND mode  =  msg.Msg_Command;

if  ((IPMSG_COMMAND.IPMSG_ANSENTRY  &  mode)  ==  IPMSG_COMMAND.IPMSG_ANSENTRY)
{


// 让用户上线
ProcessUseOnLine(msg);
return ;
}
if  ((IPMSG_COMMAND.IPMSG_BR_ENTRY  &  mode)  ==  IPMSG_COMMAND.IPMSG_BR_ENTRY)
{


// 让用户上线
// ReceiveMsg(msg);
return ;
}
if  ((IPMSG_COMMAND.IPMSG_SENDMSG  &  mode)  ==  IPMSG_COMMAND.IPMSG_SENDMSG)
{


// 让用户上线
ReceiveMsg(msg);
return ;
}
// if ((IPMSG_COMMAND.IPMSG_RECVMSG & mode) == IPMSG_COMMAND_SENDCHECKOPT)
// {


//      // 让用户上线
//     ReceiveMsg(msg);
// }
}

private   void  ReceiveMsg(IPMSG msg)
{


if  (OnReceiveMsg  !=   null )
{


OnReceiveMsg( this ,  new  IpMsg_EventArgs(msg));
}
}

public   void  SendMyOnline()
{


byte [] byts  =   null ;
IPMSG msg  =   new  IPMSG();
msg.SenderName  =  MyName;
msg.SenderHostName  =  Dns.GetHostName();
msg.Msg_Command  =  IPMSG_COMMAND.IPMSG_BR_ENTRY;
msg.Option_Data  =   string .Format( " {0}\0{1}\0{1}{2} " , MyName, MyGroup,  "" );
byts  =  msg.ToBytes();

EndPoint ep  =  (EndPoint) new  IPEndPoint(IPAddress.Parse( " 192.168.3.255 " ), _Port);
_socket.SendTo(byts, ep);
}

public   void  Stop()
{


_Running  =   false ;
}

public   void  SendMsg(User _CurrentUser,  string  msgText)
{


SendMsg(_CurrentUser.HostIp, msgText);
}

public   void  SendMsg( string  targetIP,  string  msgText)
{


byte [] byts  =   null ;
IPMSG msg  =   new  IPMSG();
msg.SenderName  =  MyName;
msg.SenderHostName  =  Dns.GetHostName();
msg.Msg_Command  =  IPMSG_COMMAND.IPMSG_SENDMSG; // IPMSG_SENDMSG
msg.Option_Data  =  msgText;
byts  =  msg.ToBytes();

EndPoint ep  =  (EndPoint) new  IPEndPoint(IPAddress.Parse(targetIP), _Port);
_socket.SendTo(byts, ep);
}
}
}

 

同时提供完整的源代码下载