Cindy中的Filter

来源:互联网 发布:晋中干部网络培训学校 编辑:程序博客网 时间:2024/05/06 03:31
 

Cindy中的Filter

今天和Arbow在MSN上讨论时谈到了MINA中的Filter机制,Arbow认为这是一个非常方便的功能,他提到了比如可以设定黑名单/白名单,来设定要不要剩下的Listener继续处理该事件。
 
其实本来我在考虑MessageRecognizerChain和SecureSocketSession的时候就考虑过Filter的问题,当时我的考虑是Filter的确有用,但是最大的用处就是设定黑名单/白名单,以及增加SSL支持等功能,应用应该很少会用到,而加入Filter机制后,又强迫应用手工调用下一个Listener去处理(否则下一个Listener将永远也处理不到事件),框架类还必须用NullObject模式实现一个Listener(否则应用增加的最后一个Listener调用时就会抛出空指针异常)。似乎加入的好处并不明显。
 
但通过和Arbow的对话后,我突然认识到,其实Filter的用处并不只我原来考虑的设定黑名单,增加SSL支持这么简单。
 
比如大部分的网络应用都需要登录,登录后才能正常使用相应的业务逻辑。采用原来的Iterator模式,登录处理和正常的业务逻辑需要混在一起,每次在做正常的业务逻辑之前都需要判断用户是否登录;而采用了Filter后,就可以分离登录的Listener和正常业务逻辑Listener,没有登录之前,登录的Listener永远不将控制权交给下一个Listener处理,这样子代码就会非常漂亮:)
 
既然认识到了Filter的好处,那么就需要考虑采用一种什么样的方式来加入支持。
 
  • 类似于MINA的方式,在Listener的相应接口中加入下一个Listener对象。

比如假设原来是public void sessionEstablished(Sessionsession)的声明,那么现在可以改为public void sessionEstablished(Session session,NextSessionListener nextListener)。用户需要在其实现类中手动调用下一个Listener来处理。

 

这种方法基本被我否认,一来所有的现有实现都需要变化,没办法兼容;二来出错的可能性比较大,用户应该调用nextListener.sessionEstablished(session),可万一用户敲成了nextListener.sessionClosed(session)就能导致意外的出错。(我认为这个很容易发生,因为next...的这种代码肯定会在SessionListener中出现多次,用户很有可能就复制粘贴了,拷贝完忘了改是很有可能的)

  • 通过一FilterListener类来实现过滤,该Filter类也实现SessionListener接口,如同MessageRecognizerChain类;并且提供额外的Filter接口,通过该类的addFilter方法来添加Filter实例

如下伪码也许可以表达出该含义:

SessionFilterListener filter = new SessionFilterListener();
session.addSessonListener(filter);
filter.addFilter(some filterA);
filter.addFilter(some filterB);

感觉上还是和原来的代码不太兼容

  • 单独的Filter接口,和SessionListener接口不相关;框架内部提供一个默认Filter实现

比如:

public interface SessionFilter {
    public boolean filter();    
}

public class DefaultSessionFilter implements SessionFilter {
    public boolean filter() {
        return true; //默认永远交给下一个Filter处理,这样和原来的Iterator模型相同
    }
}

public class XXXSession implements Session {
    public void addSessionListener(SessionListener listener) {
        if (listener instanceof SessionFilter) 
             listeners.addListener(listener);
        else
             listeners.addListener(new DefaultSessionFilter(listener));
    }
}

这样只在添加的时候通过instanceof检测了一次,迭代的时候就都转换为SessionFilter和SessionListener接口来处理,不再需要额外的判断,并且完全兼容Cindy以前的版本。

另外,用户如果要改变其行为,也不容易出错了,如:

public class CustomSessionListener implements SessionListener, SessionFilter {

    public boolean filter() {
        return isLogon();
    }
}

我是比较倾向于第三种方案的。

昨晚写完这篇就回家了,在路上突然想到了第三种方式的缺陷。第一是比较难以复用SessionListener,比如若干个Session共享一个SessionListener时,这种方式会有问题;第二是多线程时两次方法调用可能会产生同步的问题,系统内部肯定要先调用SessionListener的相应方法,然后再调用filter方法,在单线程问题下可能不会出现什么问题,但多线程状况下就会出现问题。

是否要回到最简单的方式呢?或者干脆就是分离Filter和Listener机制?

 

 

 

 

 

 

 

 

 

Cindy中的Filter(二)

稍稍看了看MINA中的Filter设计,其实MINA中Filter和Handler是并存的,io包下有IoFilter和IoHandler,protocol包下有ProtocolFilter和ProtocolHandler。其中Handler相当于cindy中的SessionListener,而Filter是cindy中所缺失的。
 
当我试图在cindy中也加入类似的机制时,我发现由于设计上的原因,cindy中的filter很难做到MINA中的那么灵活。加入Filter的机制我仔细考虑过了,除开MINA的这种实现外(通过NextFilter接口),比较可行的就是Servlet中的filter设计了(通过FilterChain接口),但是这两种实现其实也是大同小异。之所以不能做到那么灵活,其实是由于MINA中的设计有两层所致。
 
MINA中包括io层和protocol层,其中io层相当于操作最原始的ByteBuffer,而protocol则操纵比较高层一点的协议;Cindy中的基础对象即为Message接口,并没有将原始的ByteBuffer通过Listener传递出来。那么什么事情MINA通过Filter机制可以做到而Cindy做不到呢?
 
比如类似SSL这种事情,MINA就可以通过Filter来做,而Cindy中却只能通过Session继承来做。通过SSL来进行连接其实和普通连接没有太多区别,不同的是在连接建立好后,SSL连接需要先通过一系列的握手协议来商议密钥等等,等这些事情做完了,应用通过连接发送接收消息时,底层会用原来商议好的密钥来进行加密解密。由于MINA有两层,则可以加入一个IoFilter,当连接建立好时,这是接收和发送的数据均为握手数据,MINA完全可以过滤这类消息,等到握手完毕,才将解密后的数据发送给后面的Handler;而Cindy的Listener和Filter中只有Message这一接口(其实是相当于MINA的protocol层),而且还是由应用所设置的MessageRecognizer接口来识别出来的,所以无法加入Filter来过滤。
 
认识到这一点让我觉得非常遗憾,但是不得不承认,设计的时候我没有想的这么远……
 
写完按发布后,MSN Spaces居然直接就跳到首页去了,而且把文章丢失了,幸好我在发布前Ctrl+C了一下……


















Cindy中的Filter(三)

昨天写完Cindy中似乎没能做到MINA中Filter那样灵活,但是在回家的路上(似乎我每次有什么新想法都不是在工作的时候想到的)我又想通了。
 
我在考虑Cindy不能做到MINA中Filter那样灵活,是因为我的思维局限在SessionListener的那几个方法中。其实如果加入SessionFilter,可以不仅仅是SessionListener中的那几个方法,比如:
 
public interface SessionFilter {
    void sessionClosed(Session, SessionFilterChain) throws Exception;  
    void messageReceived(Session, SessionFilterChain) throws Exception;
    ......
    void dataReceived(Session, SessionFilterChain, ByteBuffer) throws Exception;
    void dataSent(Session, SessionFilterChain, ByteBuffer) throws Exception;
    void sendData(Session, SessionFilterChain, ByteBuffer) throws Exception;

}
 
多加入的几个和data有关的方法就能做到MINA中的IoFilter和ProtocolFilter能做到的事情。
 
到这个时候本来是没有什么问题的了,但是我又发现了一个MINA和Cindy都存在着的一个设计问题:发送出去的真实内容没有封装。
 
不管是在Cindy中还是在MINA中,最最基本的通讯类型都是ByteBuffer,这其实是有问题的。对于TCP应用而言,当然OK;但是对于UDP应用,基本的通讯类型应该是ByteBuffer+SocketAddress。在Cindy中则分别有Message和PacketMessage来表现。
 
但是这个设计似乎也存在缺陷,比如上面新加的几个方法,我是否还需要一个SocketAddress参数来更好的表达UDP应用呢?比如:
    void dataReceived(Session, SessionFilterChain, ByteBuffer, SocketAddress) throws Exception;
 
这样参数就太多了,MINA中就忽略了该参数,只用ByteBuffer来做过滤。
 
如果上天再给我一次机会,我会这样设计:
public interface Message {
    boolean decode(Data);  //对应以前的 boolean readFromBuffer(ByteBuffer)
    Data encode(); //对应以前的 ByteBuffer[] toByteBuffer()
}
public interface Data {
    ByteBuffer getBuffer();
    void setBuffer(ByteBuffer);
    SocketAddress getAddress();
    void setAddress(SocketAddress);
}
 
再提供一个Data的默认实现,移除原来的PacketMessage类型。这样SessionListener仍旧没有变化,而SessionFilter就变为:
public interface SessionFilter {
    ......
    void dataReceived(Session, SessionFilterChain, Data) throws Exception;
    ......
}

如果再加上我原来说的MessageSource接口:
public interface MessageSource {
    boolean hasNext();
    Message next();
}
如果再加上Active Object模式:
public interface Session {
    void addSessionListener(SessionListener);
    void addSessionFilter(SessionFilter);
    ......
    Task start() throws ...;
    Task stop() throws ...;
    Task send(Message) throws ...;
    Task send(MessageSource) throws ...;
}
 
public interface Task {
    boolean cancel();
    boolean isCancelled();
    boolean complete();
    boolean complete(int timeout);
    boolean isCompleted();
}
 
这就是Cindy下一个大版本(3.0)的构思,肯定是不能完全兼容2.x版本了,各位对这些想法有什么意见?当然,2.x版本肯定是要继续维护的,并且如果要在2.x版本中加入Filter机制,我就采用上面提到的参数较多的方式来实现,只是稍微难看了点。


没有名字发表:
wow gold!All wow gold US Server 24.99$/1000G on sell! Cheap wow gold,wow gold,wow gold,Buy Cheapest/Safe/Fast WoW US EU wow gold Power leveling wow gold from the time you World of Warcraft gold ordered!wow power leveling wow power leveling power leveling wow power leveling wow powerleveling wow power levelingcheap wow power leveling wow power leveling buy wow power leveling wow power leveling buy power leveling wow power leveling cheap power leveling wow power leveling wow power leveling wow power leveling wow powerleveling wow power leveling power leveling wow power leveling wow powerleveling wow power leveling buy rolex cheap rolex wow gold wow gold wow gold wow gold -70391178749187
6 月 19 日


amingzhu 发表:
Ok, 谢谢 Crmky , 虽然你没明白我的意思, 不过我已经明白我错在那里了, 的确TCP 本身不保存消息的边界, TCP应用要来完成对消息的识别工作, 我当初就因为感觉XML Parser 没有完成所谓的消息识别的工作, 所以才产生怀疑的, 其实XmlPullParser 里面就是完成了对文本消息的识别工作, 他通过XML Parser 来完成 ,所以没有消息的长度标识也是无所谓的,再次谢谢你, 改天到北京我请吃饭啊, 哈哈!!!
9 月 20 日


crmky 发表:
不好意思,没太看明白你想表达的意思,我不知道我的解释你是否满意。

Socket的getInputStream实际上是一个pull的模式,即应用必须要明确通过getInputStream().read(...)来读取数据,否则当系统该连接网络输入缓冲区满了后就不会继续接收数据了。

可以用敞口的杯子来比喻Socket,getInputStream是一根放在杯中的吸管,read就是通过吸管来吸水。如果一直不吸水,则当杯子满了后,后面加入的水就会被丢弃掉。

Cindy实际上是一个push的模式,应用不需要主动来读取数据,当系统接收到数据后,cindy会主动通过SessionListener派发给应用。

可以用一个漏斗来比喻Cindy的SocketSession,SessionListener是漏斗的管子,只要加了水,则就会通过管子流到应用中去。当然,如果漏斗上部满了,则后面加入的水也会被丢弃掉。

TCP是基于流的协议,这表示对于TCP本身而言,数据是没有边界的。但是对于TCP应用而言,数据是有边界的,这要靠应用自己去识别。

举个简单的例子,假设一串字节流00 01 02 03 04 05,TCP本身并不知道边界在哪里,可能这串字节流是分成三个包00 01、0203、04 05收到的,也有可能是分成两个包00 01 02、03 04 05收到的,这对TCP来说没有什么区别(除开效率上的考虑外)。

甚至可以是TCP先收到04 05,再收到02 03,再收到00 01,但是TCP协议有字段标识顺序,所以接收后又重新将顺序组装成00 01 02 03 04 05了。

但是对于应用1来说,数据可能是00 01 02、03 04 05这样分组的;对于应用2来说,数据可能是00 01、02 03、04 05这样分组的,这需要应用自己来区别。

比如对于应用1来说,

public class MessageRecognizer1 implements MessageRecognizer {
public Message recognizeMessage(....) {
if (buffer.remaining >= 3) //这里做了判断
return new Message1();
return null;
}
}

对于应用2来说,

public class MessageRecognizer1 implements MessageRecognizer {
public Message recognizeMessage(....) {
if (buffer.remaining >= 2) //这里做了判断
return new Message1();
return null;
}
}

如果采用InputStream,则对于应用1来说

getInputStream().read(...); //由于是阻塞的,一定等读完了三个字节,方法调用才返回
processMessage1(...);

对于应用2来说

getInputStream().read(...); //由于是阻塞的,一定等读完了两个字节,方法调用才返回
processMessage1(...);

所以实际上所有的边界判断都是在用户的逻辑中的,而TCP本身并不负责任何数据边界逻辑的判断。
9 月 19 日
匿名 的图片
匿名 的图片
amingzhu 发表:
JavaSocket.getInputStream() 是获取Socket 中的输入流, 而我们知道 TCP是基于流个协议,所以他不会保存消息边界的, 且本人查看 Java Socket 源码中, getInputStream 应该返回的是一定数目的字节流,其实应该是返回当前已经到达缓冲区的所有字节流, 对于文本消息来说, 没有自定义的长度就无法区分出消息,这样的话, 这段代码就感觉有问题,但事实上他也没有问题, 所以有些不解,

//============================================================================
reader = new XPPPacketReader();

reader.setXPPFactory(factory);


reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(),
CHARSET));

这是我在一个实际的程序中写的程序, 用Cindy 来写的, 但我也感觉有问题, 但也的确没出什么问题, 因为Cindy 的Message也是收到多少, 读多少, 那么这样的话应该也无法区分出消息1和消息2的啊, 注意我所说的消息是代表我所发送的一段字节流,这个字节流是一个XML 文本, 在这个文本当中我们没有长度标识啊,所以在我看来对应用来说他们是无法区分消息1和消息2的,但他们有偏偏不出问题, 不解, 期待Crmky 的解释, 谢谢!!

//============================================================================
tcpSession.addSocketSessionListener(new SessionAdapter() {
//接收到消息
public void messageReceived(Session session, Message message) {
ByteArrayMessage msg=(ByteArrayMessage)message;
InputStream InS = null ;
System.out.println(msg.toString());

ByteBuffer[] msgBuffer = msg.toByteBuffer();


for(int i=1 ;i<=msgBuffer.length-1;i++){
InS = newInputStream(msgBuffer[1]);
}
if (InS != null){
sPacket=forumNIOReader.readStream(InS);
sPacket=CharsetUtils.encode("utf-8",sPacket).toString();
session.write(new ByteArrayMessage(sPacket.getBytes()));
session.write(new ByteArrayMessage("/n".getBytes()));
}

}
...
...

}
...


public String readStream(InputStream InStream) {

reader = new XPPPacketReader();
reader.setXPPFactory(factory);

try {
reader.getXPPParser().setInput(new InputStreamReader(InStream,CHARSET));
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
} catch (XmlPullParserException e1) {
e1.printStackTrace();
}

...
}

原创粉丝点击