您的位置:首页 > 其它

可扩展的SockBase设计和实现

2005-02-18 02:28 316 查看
可扩展的SockBase设计和实现(1)


目录
摘要
基于Sockets网络编程存在的问题
可扩展的SockBase设计
SockBase的编程实现
SockBase继承及其使用方法

摘要
System.Net 命名空间为当前网络上使用的多种协议提供了简单的编程接口,如果需要底层控制更多的编程而言,开发人员就需要使用System.Net.Sockets 命名空间了。System.Net.Sockets为需要严密控制网络访问的开发人员提供了 Windows Sockets (Winsock) 接口的托管实现。 但是Sockets编程既烦杂,又毫无可扩展性,需要开发人员自己控制消息的接受和发送以及处理,这些与业务逻辑有关的工作在编程之时就需要写入代码,一旦需求发生变化,又得改写Sockets的接受消息列表和处理。同时对于命令字符串的构造都需要底层的程序员去控制,不仅容易出错,而且不易改变。面对复杂多变的业务逻辑,这样的架构毫无可重用而言,同时给程序员提出了很高的要求,很大程度的工作量放在了做底层的重复性的劳动上。因此,为了提供一种易于扩展的Sockets编程架构,使得开发人员将注意力放在业务逻辑上,我们提出了设计可扩展的SockBase思路,同时实现了这个架构,经验表明,不仅解决了上述存在的问题,而且取得了非常好的效果。

基于Sockets网络编程存在的问题
在一般的基于Sockets网络编程中,不难出现以下代码:
while(sock != null){
temp=ReadMsg(); //调用sock.Recevie(..)函数,把byte[]转成字符串
if( temp.Trim() == "Login")
{
// do some thing…
sock.Send( TransMsg("OK"));
}
else if (temp.Trim() == "show")
{
// do some thing…
sock.Send( TransMsg(ips));
}
else if (temp.Trim() == "Upload"){
// do some thing…
sock.Send(TransMsg("OK"));
// do some thing…
sock.Send(TransMsg("OK"));
}
else if (temp.Trim() == "list"){
// do some thing…
sock.Send(TransMsg(files));
}
else if (temp.Trim() == "Get"){
// do some thing…
sock.Send(TransMsg("OK"));
temp = ReadMsg().Trim();
// do some thing…
}
}

从上面的代码中,我们可以注意到,对于所有的从客户端发来的消息,都是统一在这个while()循环中进行处理的…
对于消息命令的接收,显然一般都是和上面的代码方式类似,统一放到一个地方来执行.但是上述代码对于消息的派发是使用的Switch case的结构.这样就带来一个问题.Switch Case是在程序代码编写阶段写的,也就是所谓的硬性编入.即在程序运行过程中不可修改.这样就使得程序不能在运行过程中对用户不同的输入/不同的条件发送不同的消息,或是用户自定义的,或是程序完成后新加入的扩展的命令..同时,也还是由于Switch case结构,使得对于消息的处理也固定下来了,同样也不能动态的去修改消息处理函数.这样使得程序的扩展性很差,而且对于底层的如上述代码,对于Socket的操作,完全不能直接使用到别的软件中.(因为消息命令,处理函数不一定是完全一样的).
也就是说,在通常的Sockets的网络开发中,开发人员自己控制消息的接受和发送以及处理,这些与业务逻辑有关的工作在编程之时就需要写入代码,一旦需求发生变化,又得改写Sockets的接受消息列表和处理。同时对于命令字符串的构造都需要底层的程序员去控制,不仅容易出错,而且不易改变。面对复杂多变的业务逻辑,这样的架构毫无可重用而言,同时给程序员提出了很高的要求,很大程度的工作量放在了做底层的重复性的劳动上。

可扩展的SockBase设计
针对上面的问题,我们提出了可扩展的SockBase.可扩展性主要在于能接收任意的消息,而且能对同一个消息在不同的情况下面有不同的处理函数..
我们想到了Windows的消息处理机制.当我们要处理一个系统消息的时候,或是处理我们自定义的消息的时候,首先,我们把自定义消息加入到程序的消息列表中去,同时通过Windows编程中的消息映射的方式,运行增加对处理此消息的函数.使操作系统在收到这个消息后,能够找到我们对其进行绑定的消息处理函数,进而调用..
回到Sockets中来,我们先做出一个类似Windows消息映射表样的东西.其中有两个元素,一个就是收到的消息命令,另一个就是收到此消息后的处理函数,在程序开发者开发过程中,只要在具体的消息接收前先对消息映射表进行初始化,就够了.SockBase会自动的调用相应的消息处理函数.

SockBase的编程实现
上面的部分都是理论.现在我们开始完成SockBase的实现代码.

1.定义消息映射表
根据上面所提到的,需要有一个类似消息映射表的东西.这里,我们使用Hashtable来存储消息和处理函数的数据..由于Hashtable是一种键/值型的集合,所以我们把消息命令做为键,对应的消息处理函数做为值.由于消息有很多种,而且我们希望对于所有的消息,都能在一个地方去调用相应的处理函数.所以我们使用了.NET的委托做为Hashtable中的值.
定义的委托如下:
Public delegate Command(string args);

使用方法如下:
Hashtable Commands = new Hashtable();
Commands.Add( /*消息命令*/, new Command( /*具体的处理函数*/));
调用的时候只要
((Command)Commands[/*消息命令*/])(/*参数*/);
就可以了~

2,SockBase的具体实现
好,现在,关于消息映射表的准备工作已完成了.现在开始SockBase的实现:P
(1) 构造函数以及变量的声明,实现
public class SocketBase:IDisposable{
//待处理的命令处理集合
protected Hashtable m_CommandHandlerList;
protected NetworkStream readStream;
protected NetworkStream writeStream;
protected Socket m_sock;
//通过构造函数将Socket的实例传进来.
public SocketBase(Socket sock){
m_sock = sock;
readStream = new NetworkStream(m_sock);
writeStream = new NetworkStream(m_sock);
}

public void Dispose()
{
// 关闭本地套节子
try
{
if (m_sock!= null)
{
if(m_sock.Connected)
{
m_sock.Shutdown(SocketShutdown.Both);
m_sock.Close();
}
m_sock = null;
}
}
catch(Exception ex)
{
}
}
}

(2) 发送和接收函数
准备工作已完成了.现在就是我们开始对m_sock进行消息接收,以及对消息进行派发了.
首先是消息发送和接收.由于Socket的不确定性,所以很容易出现发送的多个消息在接收的时候混在一起了,所以我们决定每发一个消息就发送固定大小的包,接收时了接收相应大小的包.
在SockBase中定义一个包的固定大小:
private static int DefaulteBufferSize =5120;
public int BufferSize
{
get{
if(m_BufferSize!=0)
return m_BufferSize;
else
return DefaulteBufferSize;
}
set{m_BufferSize=value;}
}
再就是发送,接收函数
public string ReceiveMsg()
{
byte[] Recs=new byte[BufferSize];
int count = 0;
int num;
do {
num = ReadStream.Read(Recs,count,BufferSize-count);
if( num == 0){
throw new Exception("客户端不正常关闭");
}
count += num;
} while( count < BufferSize);
return System.Text.Encoding.GetEncoding(Encoding).GetString(Recs).Replace("/0","");
}

public void Send(string msg)
{
byte[] sender = new Byte[BufferSize];
byte[] temp = System.Text.Encoding.Unicode.GetBytes(msg) ;
Array.Copy( temp,sender,temp.Length);
WriteStream.Write(sender,0,sender.Length);
WriteStream.Flush();
}
(3) 消息派发函数
好了,下面就是对消息进行派发的函数了:
public void CmdHandler(string ClientMessage)
{
//解析出命令
string[] cmdList=ClientMessage.Split(‘;’);
string cmdText = "";
for(int i=1;i<cmdList.Length;i++)
{
if(i==cmdList.Length-1)
{
cmdText += cmdList[i];
}
else
{
cmdText += cmdList[i]+":";
}
}
//寻找合适的匹配处理

if(m_CommandHandlerList.ContainsKey( cmdList[0] ) ) {
( ( Command ) m_ConnamdHandlerList[ cmdList[0] ) ( cmdText);
}
}
我们通过对m_CommandHandlerList中所有的键(即注册的消息命令)进行判断,如果和接收到的消息的命令是相同的,就直接去调用存在此Hashtable中对应的值(即Command委托)..
(4) SockBase运行的起点
最后的部分,整个SockBase运行的起点:
public void ListenSocket()
{
try
{
while(m_sock!=null&&m_sock.Connected)
{
//截获消息,并作出相应的处理
CmdHandler(ReceiveMsg());
}
}
}
现在我们只要直接在m_CommandHandlerList中加入我们要处理的消息的命令和处理函数,再运行ListenSocket(),就可以对接收到的消息进行相应的处理了..

SockBase继承及其使用方法
上面实现了SockBase的基本的构架.对于大部分的Sockets网络编程,都可适用.下面就是使用的方法..
这里,我们从SockBase直接继承而来一个Client_ListenThread.在此类中,我们通过构造函数,将相应的Socket的实例传给m_sock.再对消息映射表进行初始化,用一个线程专门运行ListenSockt来对接收到的消息进行派发,调用其处理函数.
public class Client_ListenThread : SocketBase
{
#region 所有字段包含命令字段
#endregion

#region 所有方法
public Client_ListenThread(Socket Client_socket) : base(socket)
{
LoadCommandHandlerList();
}

//装载所有的命令处理队列

public void LoadCommandHandlerList()
{
CommandHandlerItem.Add(“GetFile” , new Command(GetFileHandler);
CommandHandlerItem.Add(“FileOK”, Command(FileOKHandler);

}

//以下为所有命令处理函数
private void GetFileHandler(string cmdText)
{
//检查文件是否存在
if((new FileManager()).CheckFileExist(cmdTxt))
{
Send(“OK”);
}
else
{
Send(“Failure”);
}
}
private void FileOKHandler(string cmdText)
{
Dispose();
}
#endregion
}

通过下面这个函数将其运行:
listen_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

listen_socket.Listen(-1);
while(true){
Client_ListenThread clientthread=new Client_ListenThread(listen_socket.Accept());
if ( clientthread.Sock.Connected )
{
Thread fileThread = new Thread(new ThreadStart(clientthread.ListenSocket));
fileThread.IsBackground=true;
fileThread.Start();
}
}

总结
通过上述的SockBase,我们可以在不改变SockBase的前提下,对消息映射表进行动态的修改.这样使得开发人员将注意力放在业务逻辑上,极大的方便了基于Sockets的网络编程开发.

可扩展的SockBase设计和实现(2)

目录
摘要
使用Hashtable建立消息映射表的问题
消息映射类的设计和实现
消息映射类在SockBase中的使用

摘要
在上一篇文章<<可扩展的SockBase设计和实现(1)>>中,我们消息映射表是通过简单的Hashtable表来建立的.这样做,功能相对太简单,而且不便于扩展.而且Hashtable中的一些特性是我们不必要使用的.所以在这里,我们直接使用自定义的消息映射类(集合类CommandHandlerList和消息命令/处理函数类CommandHandler.)来建立消息映射表.

使用Hashtable建立消息映射表的问题
我们的消息映射表,要求是要一个消息命令,能对应着一个/或多个处理函数.同时对于存储整个消息映射表,要求能够很方便的增加/删除其中的条目.而且能够以比较方便的形式从表中找到消息对应的处理函数用来进行调用.
Hashtable是.NET Framework自带的一种键/值对集合. 当某个项目加入集合时,HashTable即调用键值的GetHashCode方法,由于所有的类都是从System.Objec继承的,所以调用该方法即可确定该类的哈希代码并且按该代码排序存储。从性能的角度看,因为键值搜索仅限于具有同样哈希代码的键值,所以HashTable能够很快地从集合中检索任意一个元素,从而减少了必须通过检查以发现匹配的键值的数量。然而,因为插入到集合中的每个对象-键值对都必须产生相应的哈希代码,所以项目插入的代价就有点高了。因此,HashTable主要运用在按照任意键值反复检索大量相对静态的数据这一场合下。
由于我们的消息全部都是字符串,所以对于对象的比较而言只要进行字符串的匹配,不需要通过Hashtable得到HashCode进行比较.同时,由于Hashtable只为键/值对,直接使用不能带来更大的扩展性.所以我们选择放弃直接使用Hashtable.使用我们自定义的类.

消息映射类的设计和实现
由上面的说明可以得到,我们要自定义消息映射类.首先定义每一个消息映射表中每一个消息及其处理函数的类.
由于收到消息和实际的处理函数不在同一个类中,而且应该是与具体的类无关的,所以我们使用了.NET提供的委托.由于通过Sockets发送/接收的消息都是字符串,所以消息处理函数的参数也就是收到的字符串,所以此委托所对应的函数的参数为string类型.而且为了方便一些特殊情况,比如收到的一个消息可以会触发多个处理函数.我们将其定义为事件.定义如下:
public delegate void CommandEventHandler(object sender , CommandHandlerArgs e);
public class CommandHandlerArgs:System.EventArgs
{
private string m_cmdText;
public string CommandText
{
get{return m_cmdText;}
set{m_cmdText=value;}
}
public CommandHandlerArgs(string CmdText)
{
m_cmdText=CmdText;
}
}
在这个CommandHandlerArgs里面,m_cmdText就是消息的参数.
现在可以开始定义我们的CommandHandler类了
public class CommandHandler
{
private string m_CmdPrefix;
private event CommandEventHandler m_exeCmd;
public string CommandPrefix
{
get{return m_CmdPrefix;}
set{m_CmdPrefix=value;}
}
public event CommandEventHandler CommandEvent
{
add{m_exeCmd+=value;}
remove{m_exeCmd-=value;}
}
public CommandHandler(string cmdPrefix)
{
m_CmdPrefix=cmdPrefix;
}
public void ExeCommand(string cmdText)
{
CommandHandlerArgs e=new CommandHandlerArgs(cmdText);
if(m_exeCmd!=null)
m_exeCmd(this,e);
}
}
在CommandHandler中,我们定义了一个Event,用来设置消息处理函数.同时,为了对接收到的消息和消息映射表中的消息进行匹配,加入了一个m_CmdPrefix,用来设置消息命令.还有就是一个直接运行的函数ExeCommand,通过此函数的参数cmdText,将Socket收到的消息传给消息处理函数.
现在就只剩消息映射表的实现了.既然是表,其中存储的为集合.为了方便,我们直接从CollectionBase 继承.代码如下:
public class CommandHandlerList:CollectionBase
{
public CommandHandler Add(CommandHandler cmdhandler)
{
List.Add(cmdhandler);
return cmdhandler;
}
public void RemoveAt(int index)
{
List.RemoveAt(index);
}
public void Remove(CommandHandler cmdhandler)
{
List.Remove(cmdhandler);
}
public int Length
{
get{return this.Count;}
}
public void Clear()
{
List.Clear();
}
}
其实这个CommandHandlerList类很简单的,就是直接用来存储各CommandHandler的.
好了,到现在,基本的消息映射表的结构完成了.现在就开始建立,运行我们这个消息映射表.

消息映射表在SockBase的使用
首先,把原有的使用Hashtable的list改成使用我们自定义的CommandHandlerList.
//待处理的命令处理集合
protected CommandHandlerList m_CommandHandlerList;
修改消息派发函数CmdHandler 中对消息进行查询调用的代码,修改后的代码如下:
foreach(CommandHandler cmd in m_CommandHandlerList)
{
if(cmd.CommandPrefix==cmdList[0])
{
cmd.ExeCommand(cmdText);
break;
}
}
最后就是初始化消息映射表的部分了.类Client_ListenThread中的LoadCommandHandlerList函数,修改后的代码如下:
public void LoadCommandHandlerList()
{
m_CommandHandlerList.Add(new CommandHandler( “GetFile” ).CommandEvent+=new CommandEventHandler(GetFileHandler);
m_CommandHandlerList.Add(new CommandHandler(“FileOK”)).CommandEvent += new CommandEventHandler(FileOKHandler);
}

到这个时候,我们的将原有的Hashtable换成我们自定义的类的消息映射表就全部完成了.

总结
由于Hashtalbe并不是很适用于我们这个自定义的消息映射表,所以我们换成了我们自己实现的CommandHandler和CommandHandlerList.

可扩展的SockBase设计和实现(3)

目录
摘要
嵌入式消息带来的问题
自定义消息命令类的设计实现
自定义消息命令类的使用

摘要
在前面的文章中,我们对于所有通过SockBase发送的消息都是直接通过嵌入式的字符串来完成的.比如”login”,”logout”,这样带来的一个问题就是如果不小心,写错一个字,在编译期是不能检查出来的.而且由于是直接内嵌在代码中的,和逻辑代码混在一起,不便于增加/修改消息.所以我们提出了使用自定义的消息命令类.

嵌入式消息带来的问题
由于这是一个基于Sockets的网络编程,所以我们通过Sockets输任何消息命令以及数据信息.在SockBase的实现中,我们通过Send(string)函数直接发送消息.在具体的使用SockBase的Send函数的时候,我们写的代码类似这样的:

private void GetFileHandler(string cmdText)
{
//检查文件是否存在
if((new FileManager()).CheckFileExist(cmdTxt))
{
Send(“OK”);
}
else
{
Send(“Failure”);
}
}
在这个里面,我们可以看到我们发送的消息就是直接写在代码中的”OK”,”Failure”等字符串.当然了,我们这个Send函数只能发送字符串..在这个程序中,这样是没问题的.
如果,后面的版本中,我们修改了消息命令体,把”OK”换成了”OKEY”,”Failure”换成了”Fail”,这时我们就得通过VS.NET IDE的搜索功能或是自己去手动的把原来的字符串替换成现在的.如果这个程序代码不多,就100多行,那很简单.一会儿就搞定了.但是如果这个程序比较大呢?有上千行代码?有很多地方使用到了这些个字符串呢?
这时,修改程序就成了一个恶梦,在修改的过程中就得异常小心,稍有不慎,程序就出现Bugs.这种情况出现的原因是因为消息是具有不确定性的,而我们在代码中是直接内嵌消息体(以字符串的形式),和逻辑代码直接混合在一起,影响程序的修改,扩展性.

自定义消息命令类的设计实现
基于上面分析的原因,我们不使用以字符串的形式的直接内嵌的消息命令.取而代之,我们使用我们自定义的消息命令类.
我们的消息命令有两部分,一部分就是消息体,另一部分就是此消息对应的参数.所以,我们定义消息命令类CommandBuilder如下:
public class CommandBuilder
{
private string m_Command;
private string m_CommandText;
private string m_AllCmdText;
//命令前缀,即命令
public string Command
{
get{return m_Command;}
set{m_Command=value;}
}
//命令内容
public string CommandText
{
get{return m_CommandText;}
set{m_CommandText=value;}
}
}
通过构造函数对其进行初始化:
//外部消息发送
public CommandBuilder(string commandPrefix,string commandText)
{
m_Command=commandPrefix;
m_CommandText=commandText;
}
//构造函数为已经完整的命令
public CommandBuilder(string AllcommandText)
{
m_AllCmdText=AllcommandText;
}
由于我们通过SockBase发送的是整个消息,包括消息体和参数,即我们得把消息体和参数连接起来.为了方便进行消息体和参数的解析,我们还得在消息体和参数中加入一个分隔符.基于上面所说的同样的原因.我们也不使用直接内嵌的字符串,使用CommandBuilder内部的一个成员变量来存放分隔符.
private string m_SpliteChars = “;”;
这样,如果我们要修改,只要直接修改这一个成员的值就行了.而且我们还可以把分隔符存储在配置文件中,使用的时候直接从配置文件进行读取.这样更加灵活.但是在这里,我们就使用上述方式.
好了,消息命令类基本的数据成员完成了.
现在提供一个函数,直接把最终的可以直接通过SockBase的Send函数发的送的字符串输出:
public string SenderMsg
{
get
{
if( m_AllCmdText!=null && m_AllCmdText!=String.Empty )
return m_AllCmdText;
else
{
return m_Command+ m_SpliteChars +m_CommandText;
}
}
}
现在消息命令类已基本完成了,现在就差对命令进行包装了.我们可以直接写成一个类的静态成员,所有对消息的引用都直接使用这个类中的静态成员.同样的,我们也可以直接写到一个配置文件中.这里,为了简单,我们就直接使用类的静态成员.定义CommandList定义如下:
public class CommandList
{
//命令字符串
public static string GETFILE = "GetFile";
public static string OK = “OK”;
public static string FAILURE = “FAILURE”;
}
好,现在就可以开始使用CommandBuilder和CommandList代替内嵌字符串了.

自定义消息命令类的使用
这里,我们就没必要去修改SockBase了,只要修改前面文章中的Client_ListenThread类即可.修改后的代码如下:
private void GetFileHandler(string cmdText)
{
CommandBuilder cmdBuilder ;
//检查文件是否存在
if((new FileManager()).CheckFileExist(cmdTxt))
{
cmdBuilder = new CommandBuilder(CommandList.GETFILE,CommandList.OK);
}
else
{
cmdBuilder = new CommandBuilder(CommandList.GETFILE,CommandList. FAILURE);
}
Send(cmdBuilder.SenderMsg);
}

总结
将程序中的字符串等封装成类的(静态)变量是一个很好的习惯,特别是在变化比较大的程序开发中.这样有利于代码的复用,以及代码的修改的维护.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐